diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index d4d3509138ded063f4c1dcf5b06de07e6f5e449c..0bc456e6fc11f3f29c79da458918f705935d5c44 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -89,6 +89,7 @@ INSTALLED_APPS = ( 'commentaries', 'comments', 'finances', + 'invitations', 'journals', 'mails', 'mailing_lists', diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py index 5122b6590b62f946fcac51070dbc94fff95a3b50..2fe82bebd8ad297c4505b5fc1f5794bca5e8035d 100644 --- a/SciPost_v1/urls.py +++ b/SciPost_v1/urls.py @@ -36,6 +36,7 @@ urlpatterns = [ url(r'^comments/', include('comments.urls', namespace="comments")), url(r'^funders/', include('funders.urls', namespace="funders")), url(r'^finances/', include('finances.urls', namespace="finances")), + url(r'^invitations/', include('invitations.urls', namespace="invitations")), url(r'^journals/', include('journals.urls.general', namespace="journals")), url(r'^mailing_list/', include('mailing_lists.urls', namespace="mailing_lists")), url(r'^submissions/', include('submissions.urls', namespace="submissions")), diff --git a/scipost/templates/scipost/_assignments_summary_as_td.html b/invitations/__init__.py similarity index 100% rename from scipost/templates/scipost/_assignments_summary_as_td.html rename to invitations/__init__.py diff --git a/invitations/admin.py b/invitations/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..a2012f98edee74417477674d8bf2c17a310ec307 --- /dev/null +++ b/invitations/admin.py @@ -0,0 +1,24 @@ +from django.contrib import admin + +from .models import RegistrationInvitation, CitationNotification + + +class RegistrationInvitationAdmin(admin.ModelAdmin): + date_hierarchy = 'date_sent_first' + search_fields = ['first_name', 'last_name', 'email', 'invitation_key'] + list_display = ['__str__', 'invitation_type', 'invited_by', 'status'] + list_filter = ['invitation_type', 'message_style', 'status'] + + +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/apps.py b/invitations/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..15b9fc396a092b57fd1df51a1daeaff6afc1d176 --- /dev/null +++ b/invitations/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class InvitationsConfig(AppConfig): + name = 'invitations' diff --git a/invitations/constants.py b/invitations/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..9de09698378d2332d28ad699f7fdb06c5257be46 --- /dev/null +++ b/invitations/constants.py @@ -0,0 +1,24 @@ +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'), +) + + +INVITATION_FORMAL, INVITATION_PERSONAL = ('F', 'P') +INVITATION_STYLE = ( + (INVITATION_FORMAL, 'Formal'), + (INVITATION_PERSONAL, 'Personal'), +) + + +INVITATION_EDITORIAL_FELLOW, INVITATION_CONTRIBUTOR, INVITATION_REFEREEING = ('F', 'C', 'R') +INVITATION_TYPE = ( + (INVITATION_EDITORIAL_FELLOW, 'Editorial Fellow'), + (INVITATION_CONTRIBUTOR, 'Contributor'), + (INVITATION_REFEREEING, 'Refereeing'), +) diff --git a/invitations/forms.py b/invitations/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..45e5fa3b5b9360981891ac511315f544f6115708 --- /dev/null +++ b/invitations/forms.py @@ -0,0 +1,231 @@ +from django import forms +from django.contrib import messages + +from journals.models import Publication +from scipost.models import Contributor +from submissions.models import Submission + +from . import constants +from .models import RegistrationInvitation, CitationNotification + +from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField + + +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 SuggestionSearchForm(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) + declines = RegistrationInvitation.objects.declined().filter( + last_name__icontains=last_name) + return contributors, invitations, declines + 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() + + +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 + fields = ( + 'title', + 'first_name', + 'last_name', + 'email', + 'message_style', + 'personal_message') + + def __init__(self, *args, **kwargs): + # 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'] + del self.fields['personal_message'] + + + def clean_email(self): + email = self.cleaned_data['email'] + if Contributor.objects.filter(user__email=email).exists(): + self.add_error('email', 'This email address is already associated to a Contributor') + elif RegistrationInvitation.objects.declined().filter(email=email).exists(): + self.add_error('email', 'This person has already declined an earlier invitation') + + return email + + 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 RegistrationInvitationMapToContributorForm(AcceptRequestMixin, forms.ModelForm): + contributor = None + + class Meta: + model = RegistrationInvitation + fields = () + + 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): + 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 new file mode 100644 index 0000000000000000000000000000000000000000..84cff9b31c325853e9d784a9c2bb17de288e8565 --- /dev/null +++ b/invitations/managers.py @@ -0,0 +1,48 @@ +from django.db import models + +from . import constants + + +class RegistrationInvitationQuerySet(models.QuerySet): + def for_fellows(self): + return self.filter(invitation_type=constants.INVITATION_EDITORIAL_FELLOW) + + def not_for_fellows(self): + return self.exclude(invitation_type=constants.INVITATION_EDITORIAL_FELLOW) + + def declined(self): + return self.filter(status=constants.STATUS_DECLINED) + + def drafts(self): + return self.filter(status=constants.STATUS_DRAFT) + + def declined_or_without_response(self): + return self.filter(status__in=[constants.STATUS_DECLINED, + constants.STATUS_SENT, + constants.STATUS_DRAFT, + constants.STATUS_SENT_AND_EDITED]) + + 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/0001_initial.py b/invitations/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..4904ed6f19f62413c0dea8974ba27888b3cd1bb4 --- /dev/null +++ b/invitations/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-17 12:48 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('submissions', '0008_auto_20180127_2208'), + ('journals', '0013_auto_20180216_0850'), + ('scipost', '0004_auto_20180212_1932'), + ] + + operations = [ + migrations.CreateModel( + name='RegistrationInvitation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4)), + ('first_name', models.CharField(max_length=30)), + ('last_name', models.CharField(max_length=150)), + ('email', models.EmailField(max_length=254)), + ('status', models.CharField(choices=[('draft', 'Draft'), ('sent', 'Sent'), ('declined', 'Declined'), ('register', 'Registered')], default='draft', max_length=8)), + ('message_style', models.CharField(choices=[('F', 'Formal'), ('P', 'Personal')], default='F', max_length=1)), + ('personal_message', models.TextField(blank=True)), + ('invitation_type', models.CharField(choices=[('F', 'Editorial Fellow'), ('C', 'Contributor'), ('R', 'Refereeing')], default='C', max_length=2)), + ('invitation_key', models.CharField(max_length=40, unique=True)), + ('key_expires', models.DateTimeField(default=django.utils.timezone.now)), + ('date_sent_first', models.DateTimeField(default=django.utils.timezone.now)), + ('date_sent_last', models.DateTimeField(default=django.utils.timezone.now)), + ('number_of_reminders', models.PositiveSmallIntegerField(default=0)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('cited_in_publication', models.ManyToManyField(blank=True, related_name='_registrationinvitation_cited_in_publication_+', to='journals.Publication')), + ('cited_in_submission', models.ManyToManyField(blank=True, related_name='_registrationinvitation_cited_in_submission_+', to='submissions.Submission')), + ('invited_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invitations_sent', to='scipost.Contributor')), + ], + options={ + 'ordering': ['last_name'], + }, + ), + ] diff --git a/invitations/migrations/0002_auto_20180217_1449.py b/invitations/migrations/0002_auto_20180217_1449.py new file mode 100644 index 0000000000000000000000000000000000000000..1232fd833f8c4fb86ef6d42796f821e29c7a2d93 --- /dev/null +++ b/invitations/migrations/0002_auto_20180217_1449.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-17 13:49 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='registrationinvitation', + name='date_sent_first', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='registrationinvitation', + name='date_sent_last', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/invitations/migrations/0003_auto_20180217_1510.py b/invitations/migrations/0003_auto_20180217_1510.py new file mode 100644 index 0000000000000000000000000000000000000000..f637c0d2a5421fd839f1ca9becc7c8ec983f27f6 --- /dev/null +++ b/invitations/migrations/0003_auto_20180217_1510.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-17 14:10 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0002_auto_20180217_1449'), + ] + + operations = [ + migrations.AlterField( + model_name='registrationinvitation', + name='invited_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invitations_sent', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/invitations/migrations/0004_registrationinvitation_created_by.py b/invitations/migrations/0004_registrationinvitation_created_by.py new file mode 100644 index 0000000000000000000000000000000000000000..637bb72f4d55bcd33d650874707945fe48019a5e --- /dev/null +++ b/invitations/migrations/0004_registrationinvitation_created_by.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-17 14:51 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('invitations', '0003_auto_20180217_1510'), + ] + + operations = [ + migrations.AddField( + model_name='registrationinvitation', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invitations_created', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/invitations/migrations/0005_auto_20180217_1554.py b/invitations/migrations/0005_auto_20180217_1554.py new file mode 100644 index 0000000000000000000000000000000000000000..e88a5f2347f686379859f89835eea73bf2636d6b --- /dev/null +++ b/invitations/migrations/0005_auto_20180217_1554.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-17 14:54 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0004_registrationinvitation_created_by'), + ] + + operations = [ + migrations.AlterField( + model_name='registrationinvitation', + name='created_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations_created', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/invitations/migrations/0006_auto_20180217_1600.py b/invitations/migrations/0006_auto_20180217_1600.py new file mode 100644 index 0000000000000000000000000000000000000000..d987c7dbe6de259709ae09005ebac2a38e390851 --- /dev/null +++ b/invitations/migrations/0006_auto_20180217_1600.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-17 15:00 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0005_auto_20180217_1554'), + ] + + operations = [ + migrations.RenameField( + model_name='registrationinvitation', + old_name='number_of_reminders', + new_name='times_sent', + ), + ] 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/migrations/0011_auto_20180220_1139.py b/invitations/migrations/0011_auto_20180220_1139.py new file mode 100644 index 0000000000000000000000000000000000000000..dc988e8fba91f2cb20835b8e35c4482a9885f2ec --- /dev/null +++ b/invitations/migrations/0011_auto_20180220_1139.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-20 10:39 +from __future__ import unicode_literals + +from django.db import migrations + +# Hack +from django.contrib.auth import get_user_model + + +def transfer_old_invitations_to_new_tables(apps, schema_editor): + OldRegistrationInvitation = apps.get_model('scipost', 'RegistrationInvitation') + OldCitationNotification = apps.get_model('scipost', 'CitationNotification') + NewRegistrationInvitation = apps.get_model('invitations', 'RegistrationInvitation') + NewCitationNotification = apps.get_model('invitations', 'CitationNotification') + + random_user = get_user_model().objects.filter(is_superuser=True).first() + if not random_user: + random_user = get_user_model().objects.first() + + # Registration Invitations first + for invitation in OldRegistrationInvitation.objects.all(): + new_inv = NewRegistrationInvitation( + title=invitation.title, + first_name=invitation.first_name, + last_name=invitation.last_name, + email=invitation.email, + invitation_type=invitation.invitation_type, + created_by_id=invitation.invited_by.user.id if invitation.invited_by else random_user.id, + invited_by_id=invitation.invited_by.user.id if invitation.invited_by else None, + message_style=invitation.message_style, + personal_message=invitation.personal_message, + times_sent=invitation.nr_reminders + 1, + date_sent_first=invitation.date_sent, + date_sent_last=invitation.date_last_reminded, + created=invitation.date_sent, + modified=invitation.date_sent, + key_expires=invitation.key_expires, + invitation_key=invitation.invitation_key, + ) + if new_inv.invitation_type in ['ci', 'cp']: + new_inv.invitation_type = 'C' + + if not invitation.responded: + new_inv.status = 'sent' + elif invitation.declined: + new_inv.status = 'declined' + elif invitation.responded and not invitation.declined: + new_inv.status = 'register' + else: + new_inv.status = 'draft' + new_inv.save() + + if invitation.cited_in_submission: + NewCitationNotification.objects.create( + invitation_id=new_inv.id, + created_by_id=invitation.invited_by.user.id if invitation.invited_by else random_user.id, + created=new_inv.created, + modified=new_inv.modified, + submission_id=invitation.cited_in_submission.id, + date_sent=invitation.date_sent_first, + processed=(new_inv.status in ['declined', 'register', 'sent']), + ) + if invitation.cited_in_publication: + NewCitationNotification.objects.create( + invitation_id=new_inv.id, + created_by_id=invitation.invited_by.user.id if invitation.invited_by else random_user.id, + created=new_inv.created, + modified=new_inv.modified, + publication_id=invitation.cited_in_publication.id, + date_sent=invitation.date_sent_first, + processed=(new_inv.status in ['declined', 'register', 'sent']), + ) + + # Old CitationNotifications + for notification in OldCitationNotification.objects.all(): + NewCitationNotification.objects.create( + contributor_id=notification.contributor.id if notification.contributor else None, + created_by_id=random_user.id, + submission_id=notification.cited_in_submission.id if notification.cited_in_submission else None, + publication_id=notification.cited_in_publication.id if notification.cited_in_publication else None, + processed=notification.processed, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0010_auto_20180218_1613'), + ] + + operations = [ + migrations.RunPython(transfer_old_invitations_to_new_tables), + ] diff --git a/invitations/migrations/__init__.py b/invitations/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/invitations/mixins.py b/invitations/mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..8635746bd3746735a6aad2e61a6236f525557f80 --- /dev/null +++ b/invitations/mixins.py @@ -0,0 +1,48 @@ +from django.db import transaction +from django.contrib import messages +from django.contrib.auth.mixins import 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. + """ + send_mail = None + + def post(self, request, *args, **kwargs): + # Intercept the specific submit value before validation the form so `MailEditorMixin` + # can use this data. + if self.send_mail is None: + self.send_mail = request.POST.get('save', '') == 'save_and_send' + if self.send_mail: + self.send_mail = request.user.has_perm('scipost.can_manage_registration_invitations') + + # Communicate with the `MailEditorMixin` whether the mails should go out or not. + self.has_permission_to_send_mail = self.send_mail + return super().post(request, *args, **kwargs) + + @transaction.atomic + def form_valid(self, form): + # Communication with the user. + model_name = self.object._meta.verbose_name + model_name = model_name[:1].upper() + model_name[1:] # Hack it to capitalize the name + if self.send_mail: + self.object.mail_sent() + messages.success(self.request, '{} updated and sent'.format(model_name)) + else: + messages.success(self.request, '{} updated'.format(model_name)) + return super().form_valid(form) diff --git a/invitations/models.py b/invitations/models.py new file mode 100644 index 0000000000000000000000000000000000000000..aeac667d9229d512250b43b897fa1ec10a5ae2d2 --- /dev/null +++ b/invitations/models.py @@ -0,0 +1,188 @@ +import datetime +import hashlib +import random +import string + +from django.db import models, IntegrityError +from django.conf import settings +from django.utils import timezone + +from . import constants +from .managers import RegistrationInvitationQuerySet, CitationNotificationQuerySet + +from scipost.constants import TITLE_CHOICES + + +class RegistrationInvitation(models.Model): + """ + Invitation to particular persons for registration + """ + title = models.CharField(max_length=4, choices=TITLE_CHOICES) + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=150) + email = models.EmailField() + status = models.CharField(max_length=8, choices=constants.REGISTATION_INVITATION_STATUSES, + default=constants.STATUS_DRAFT) + + # Text content + message_style = models.CharField(max_length=1, choices=constants.INVITATION_STYLE, + default=constants.INVITATION_FORMAL) + personal_message = models.TextField(blank=True) + invited_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, + blank=True, null=True, related_name='invitations_sent') + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='invitations_created') + + # Related to objects + invitation_type = models.CharField(max_length=2, choices=constants.INVITATION_TYPE, + default=constants.INVITATION_CONTRIBUTOR) + + # Response keys + invitation_key = models.CharField(max_length=40, unique=True) + key_expires = models.DateTimeField(default=timezone.now) + + # Timestamps + date_sent_first = models.DateTimeField(null=True, blank=True) + date_sent_last = models.DateTimeField(null=True, blank=True) + times_sent = models.PositiveSmallIntegerField(default=0) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + objects = RegistrationInvitationQuerySet.as_manager() + + class Meta: + ordering = ['last_name'] + + def __str__(self): + return '{} {} on {}'.format(self.first_name, self.last_name, + self.created.strftime("%Y-%m-%d")) + + def save(self, *args, **kwargs): + self.refresh_keys(commit=False) + return super().save(*args, **kwargs) + + def refresh_keys(self, force_new_key=False, commit=True): + # Generate email activation key and link + if not self.invitation_key or force_new_key: + # TODO: Replace this all by the `secrets` package available from python 3.6(!) + salt = '' + for i in range(5): + salt += random.choice(string.ascii_letters) + salt = salt.encode('utf8') + invitationsalt = self.last_name.encode('utf8') + self.invitation_key = hashlib.sha1(salt + invitationsalt).hexdigest() + self.key_expires = timezone.now() + datetime.timedelta(days=365) + if commit: + self.save() + + def mail_sent(self, user=None): + """ + Update instance fields as if a new invitation mail has been sent out. + """ + if self.status == constants.STATUS_DRAFT: + self.status = constants.STATUS_SENT + if not self.date_sent_first: + self.date_sent_first = timezone.now() + self.date_sent_last = timezone.now() + self.invited_by = user or self.created_by + self.times_sent += 1 + self.citation_notifications.update(processed=True) + self.save() + + @property + def has_responded(self): + return self.status in [constants.STATUS_DECLINED, constants.STATUS_REGISTERED] + + +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.unprocessed().filter( + models.Q(contributor=self.contributor) | models.Q(invitation=self.invitation)) + + def get_first_related_contributor(self): + return self.related_notifications().filter(contributor__isnull=False).first() + + @property + def email(self): + if self.invitation: + return self.invitation.email + elif self.contributor: + return self.contributor.user.email + + @property + def last_name(self): + if self.invitation: + return self.invitation.last_name + elif self.contributor: + return self.contributor.last_name + + @property + def get_title(self): + if self.invitation: + return self.invitation.get_title_display() + elif self.contributor: + return self.contributor.get_title_display() 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/scipost/templates/scipost/registration_invitations_cleanup.html b/invitations/templates/invitations/registrationinvitation_cleanup.html similarity index 57% rename from scipost/templates/scipost/registration_invitations_cleanup.html rename to invitations/templates/invitations/registrationinvitation_cleanup.html index 539c00d807dae937ee87b2376f359458b5a9c72f..d4a9620d11b2da9a4a47b459c02150028362260c 100644 --- a/scipost/templates/scipost/registration_invitations_cleanup.html +++ b/invitations/templates/invitations/registrationinvitation_cleanup.html @@ -1,11 +1,11 @@ {% extends 'scipost/_personal_page_base.html' %} -{% block pagetitle %}: registration invitations cleanup{% endblock pagetitle %} +{% block pagetitle %}: Registration Invitations cleanup{% endblock pagetitle %} {% block breadcrumb_items %} - {{block.super}} - <a href="{% url 'scipost:registration_invitations' %}" class="breadcrumb-item">Registration invitations</a> + {{ block.super }} + <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> <span class="breadcrumb-item">Cleanup</span> {% endblock %} @@ -15,13 +15,13 @@ <div class="row"> <div class="col-12"> <h1 class="highlight">Registration Invitations Cleanup</h1> + <h3>Email duplicates (a contributor exists with the email address in these invitations)</h3> </div> </div> <div class="row"> <div class="col-12"> - <h3>Email duplicates (a contributor exists with the email address in these invitations)</h3> <table class="table"> <thead> <tr> @@ -34,29 +34,23 @@ </tr> </thead> <tbody> - {% for inv in invs_to_cleanup %} + {% for inv in invitations %} <tr> <td>{{ inv.last_name }}</td> <td>{{ inv.first_name }}</td> <td>{{ inv.email }}</td> - <td>{{ inv.date_sent }} </td> - <td>{{ inv.invitation_type }}</td> - <td>{{ inv.invited_by.user.last_name }}</td> - <td> - <a href="{% url 'scipost:remove_registration_invitation' invitation_id=inv.id %}">Remove</a> - </td> + <td>{{ inv.date_sent_first }} </td> + <td>{{ inv.get_invitation_type_display }}</td> + <td>{{ inv.invited_by }}</td> + <td><a href="{% url 'invitations:delete' inv.id %}">Remove</a></td> </tr> {% empty %} <tr> - <td colspan="7"> - There were no duplicate emails found in the sets of Contributors/Invitations. - </td> + <td colspan="7">There were no duplicate emails found in the sets of Contributors/Invitations.</td> </tr> {% endfor %} </tbody> </table> - - <p>Return to the <a href="{% url 'scipost:registration_invitations' %}">Registration Invitations</a> page.</p> </div> </div> diff --git a/invitations/templates/invitations/registrationinvitation_confirm_delete.html b/invitations/templates/invitations/registrationinvitation_confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..c65958c8547340e2b5dc9f79d564c029eb5e5166 --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_confirm_delete.html @@ -0,0 +1,27 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% block pagetitle %}: Delete Registration Invitation{% endblock pagetitle %} + +{% block breadcrumb_items %} + {{block.super}} + <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> + <span class="breadcrumb-item">Delete {{ object.id }}</span> +{% endblock %} + +{% block content %} + +<div class="row"> + <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> + {% include 'partials/invitations/registrationinvitation_summary.html' with invitation=object %} + + <form method="post"> + {% csrf_token %} + <input type="submit" class="btn btn-danger" value="Delete Registration Invitation"> + </form> + + </div> +</div> + +{% endblock %} diff --git a/invitations/templates/invitations/registrationinvitation_form.html b/invitations/templates/invitations/registrationinvitation_form.html new file mode 100644 index 0000000000000000000000000000000000000000..878ad6087d05b52a539f6ff611d3958c1d5b61b5 --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_form.html @@ -0,0 +1,37 @@ +{% 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> + + <form method="post"> + {% csrf_token %} + {{ 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> + </div> +</div> + +{% endblock %} + +{% block footer_script %} + {{ block.super }} + {{ form.media }} +{% endblock footer_script %} 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..922c6f743be867698e6c4cb2513ae40ef85300f8 --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_form_add_new.html @@ -0,0 +1,100 @@ +{% 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> + <p> + If you want to invite a new Contributor to SciPost, first try to use the following search form to see if this person already is available in the SciPost database. + </p> + + {% if suggestion_search_form %} + <h3 class="mb-1">Search for existing Contributor</h3> + <form method="get"> + {{ suggestion_search_form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Search"> + {% if suggestion_search_form.is_bound %} + <a href="{% url 'invitations:new' %}" class="ml-2 btn btn-link">Cancel search</a> + {% endif %} + </form> + + <hr class="divider"> + {% if suggestion_search_form.is_bound %} + {% if suggested_invitations %} + <h3>Registration Invitations found</h3> + <p> + If the person you are trying to invite is within this list of Registration Invitations, please use it by extending that particular invitation. + </p> + <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 %} + {% if declined_invitations %} + <h3>Declined Registration Invitations</h3> + <p> + If the person you are trying to invite is within this list of Registration Invitations, do not invite them again. They have already declined an earlier invitation. + </p> + <ul class="mb-2"> + {% for inv in declined_invitations %} + <li>{{ inv.first_name }} {{ inv.last_name }}: {{ inv.email }}</a></li> + {% endfor %} + </ul> + {% endif %} + + <h3>Citation Notification</h3> + <p> + If the person you are trying to invite is already a registered Contributor, it'll be listed in the following form. If not, you can <a href="{% url 'invitations:new' %}?prefill-last_name={{ suggestion_search_form.last_name.value|urlencode }}">write a new Registration Invitation</a>. + </p> + {% else %} + <h3 class="mb-1">...or write a new Registration Invitation</h3> + {% endif %} + + {% endif %} + + {% if suggestion_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 new file mode 100644 index 0000000000000000000000000000000000000000..9ef7afeeaa17c0fda2f5ba2d806ae1866e807c97 --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_list.html @@ -0,0 +1,61 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Registration Invitations{% endblock pagetitle %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Registration Invitations</span> +{% endblock %} + +{% block content %} + +<h1 class="highlight">Registration Invitations</h1> + +<div class="row"> + <div class="col-md-6"> + <h3>Actions</h3> + <ul class="mb-0"> + {% 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-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">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> + +{% endblock %} diff --git a/invitations/templates/invitations/registrationinvitation_reminder_form.html b/invitations/templates/invitations/registrationinvitation_reminder_form.html new file mode 100644 index 0000000000000000000000000000000000000000..c328668dc3dc632039fbaa7ddca64a6cbb37920f --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_reminder_form.html @@ -0,0 +1,34 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% block pagetitle %}: Send reminder for 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">Send reminder</span> +{% endblock %} + +{% block content %} + +<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> + +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Send reminder"> + </form> + + </div> +</div> + +{% endblock %} 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..7ec0ed3b93c9dba68c585b86f630ccfc8b294c96 --- /dev/null +++ b/invitations/templates/partials/invitations/citationnotification_table.html @@ -0,0 +1,63 @@ +<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> + {% if notification.contributor %} + <a href="{% url 'invitations:citation_notification_process' notification.id %}">Process citation</a> + {% elif notification.invitation %} + {% if notification.invitation.status == 'draft' %} + <a href="{% url 'invitations:update' notification.invitation.id %}">Edit/send invitation</a> + {% elif notification.invitation.status == 'send' or notification.invitation.status == 'edited' %} + <a href="{% url 'invitations:send_reminder' notification.invitation.id %}">Send reminder</a> + {% endif %} + {% endif %} + </td> + </tr> + {% empty %} + <tr> + <td colspan="7"> + All Citation Notifications have been processed. + </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 new file mode 100644 index 0000000000000000000000000000000000000000..8bd8eac5c87b49e4378e5234d4f46505e5ea66aa --- /dev/null +++ b/invitations/templates/partials/invitations/registrationinvitation_table.html @@ -0,0 +1,71 @@ +{% load scipost_extras %} + +<table class="table"> + <thead> + <tr> + <th>Name</th> + <th>Email</th> + <th>Status</th> + <th>Type</th> + <th>Drafted by</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 }}, {{ invitation.first_name }}</td> + <td>{{ invitation.email }}</td> + <td{% if invitation.status == 'draft' %} class="text-warning"{% endif %}>{{ 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.times_sent %} + <strong>{{ invitation.times_sent }}</strong> time{{ invitation.times_sent|pluralize }} + · {{ invitation.date_sent_last|timesince }} ago + {% else %} + - + {% endif %} + </td> + {% if perms.scipost.can_manage_registration_invitations %} + <td> + <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 %} + </tr> + {% empty %} + <tr> + <td colspan="9">No Invitations found.</td> + </tr> + {% endfor %} + </tbody> +</table> diff --git a/invitations/tests.py b/invitations/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/invitations/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/invitations/urls.py b/invitations/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..d53b7ea3ef19fb78c221549ca4d81d78fedc8f33 --- /dev/null +++ b/invitations/urls.py @@ -0,0 +1,26 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url(r'^$', views.RegistrationInvitationsView.as_view(), name='list'), + 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]+)/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 new file mode 100644 index 0000000000000000000000000000000000000000..60e8351a36c128701dbb63b0f83886d9373f8692 --- /dev/null +++ b/invitations/views.py @@ -0,0 +1,201 @@ +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +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 UpdateView, DeleteView + +from .forms import RegistrationInvitationForm, RegistrationInvitationReminderForm,\ + RegistrationInvitationMarkForm, RegistrationInvitationMapToContributorForm,\ + CitationNotificationForm, SuggestionSearchForm, 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(PermissionsMixin, ListView): + permission_required = 'scipost.can_create_registration_invitations' + 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.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('date_sent_last', 'last_name') + context['search_form'] = search_form + return context + + def get_queryset(self, *args, **kwargs): + qs = super().get_queryset(*args, **kwargs) + if not self.request.user.has_perm('scipost.can_invite_fellows'): + qs = qs.not_for_fellows() + return qs + + +class CitationNotificationsView(PermissionsMixin, ListView): + permission_required = 'scipost.can_manage_registration_invitations' + queryset = CitationNotification.objects.unprocessed().prefetch_related( + 'invitation', 'contributor', 'contributor__user') + + +class CitationNotificationsProcessView(PermissionsMixin, RequestArgumentMixin, + MailEditorMixin, UpdateView): + permission_required = 'scipost.can_manage_registration_invitations' + form_class = CitationNotificationProcessForm + queryset = CitationNotification.objects.unprocessed() + success_url = reverse_lazy('invitations:citation_notification_list') + mail_code = 'citation_notification' + + @transaction.atomic + def form_valid(self, form): + """ + Form is valid; use the MailEditorMixin to send out the mail if + (possible) Contributor didn't opt-out from mails. + """ + form.get_all_notifications().update(processed=True) + contributor = form.get_all_notifications().filter(contributor__isnull=False).first() + self.send_mail = (contributor and contributor.accepts_SciPost_emails) or not contributor + return super().form_valid(form) + + +@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 = [] + declined_invitations = [] + + # Only take specific GET data to prevent for unexpected bound forms. + search_data = {} + initial_search_data = {} + if request.GET.get('last_name'): + search_data['last_name'] = request.GET['last_name'] + if request.GET.get('prefill-last_name'): + initial_search_data['last_name'] = request.GET['prefill-last_name'] + suggestion_search_form = SuggestionSearchForm(search_data or None, + initial=initial_search_data) + if suggestion_search_form.is_valid(): + contributors, suggested_invitations, declined_invitations = suggestion_search_form.search() + citation_form = CitationNotificationForm(request.POST or None, contributors=contributors, + prefix='notification', request=request) + + # New citation is related to a Contributor: RegistationInvitation + invitation_form = RegistrationInvitationForm(request.POST or None, request=request, + prefix='invitation', + initial=initial_search_data) + 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') + + # 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') + + context = { + 'suggestion_search_form': suggestion_search_form, + 'citation_form': citation_form, + 'suggested_invitations': suggested_invitations, + 'declined_invitations': declined_invitations, + 'invitation_form': invitation_form, + } + return render(request, 'invitations/registrationinvitation_form_add_new.html', context) + + +class RegistrationInvitationsUpdateView(RequestArgumentMixin, PermissionsMixin, + SaveAndSendFormMixin, MailEditorMixin, UpdateView): + permission_required = 'scipost.can_create_registration_invitations' + form_class = RegistrationInvitationForm + 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() + if not self.request.user.has_perm('scipost.can_invite_fellows'): + qs = qs.not_for_fellows() + if not self.request.user.has_perm('scipost.can_manage_registration_invitations'): + qs = qs.invited_by(self.request.user) + return qs + + +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.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, MailEditorMixin, 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' + mail_code = 'registration_invitation_reminder' + + +class RegistrationInvitationsDeleteView(PermissionsMixin, DeleteView): + permission_required = 'scipost.can_manage_registration_invitations' + model = RegistrationInvitation + success_url = reverse_lazy('invitations:list') + + +@login_required +@permission_required('scipost.can_manage_registration_invitations', raise_exception=True) +def cleanup(request): + """ + Compares the email addresses of invitations with those in the + database of registered Contributors. Flags overlaps. + """ + contributor_email_list = Contributor.objects.values_list('user__email', flat=True) + invitations = RegistrationInvitation.objects.sent().filter(email__in=contributor_email_list) + context = { + 'invitations': invitations + } + return render(request, 'invitations/registrationinvitation_cleanup.html', context) diff --git a/journals/templatetags/lookup.py b/journals/templatetags/lookup.py index bdfeb24b0ce148daab9e22cc133a37abf5e7cc2a..9323d78aa1ca21a73e50a8022eb758cf7c90e8c3 100644 --- a/journals/templatetags/lookup.py +++ b/journals/templatetags/lookup.py @@ -28,4 +28,4 @@ class PublicationLookup(LookupChannel): Right now only used for draft registration invitations. May be extended in the future for other purposes as well. """ - return request.user.has_perm('can_draft_registration_invitations') + return request.user.has_perm('can_create_registration_invitations') diff --git a/mailing_lists/views.py b/mailing_lists/views.py index 07792fff8517e31ac61e744087daeb4fec78637b..0512a16009c9678e76dc52c42e1673cac71e307e 100644 --- a/mailing_lists/views.py +++ b/mailing_lists/views.py @@ -9,7 +9,7 @@ from django.views.generic.list import ListView from django.core.urlresolvers import reverse from django.shortcuts import redirect, get_object_or_404 -from scipost.models import RegistrationInvitation +from invitations.models import RegistrationInvitation from .forms import MailchimpUpdateForm from .models import MailchimpList diff --git a/mails/forms.py b/mails/forms.py index a7a8b241a3b22343728d21d1e28536af52da02b4..b69d0d037d23de0d2663445ba2e806606e7c0a1c 100644 --- a/mails/forms.py +++ b/mails/forms.py @@ -1,133 +1,62 @@ -import re -import json -import inspect -from html2text import HTML2Text - from django import forms -from django.core.mail import EmailMultiAlternatives -from django.contrib.auth import get_user_model -from django.conf import settings -from django.template import loader - -from scipost.models import Contributor +from .mixins import MailUtilsMixin from .widgets import SummernoteEditor -class EmailTemplateForm(forms.Form): +class EmailTemplateForm(forms.Form, MailUtilsMixin): subject = forms.CharField(max_length=250, label="Subject*") text = forms.CharField(widget=SummernoteEditor, label="Text*") extra_recipient = forms.EmailField(label="Optional: bcc this email to", required=False) + prefix = 'mail_form' def __init__(self, *args, **kwargs): - self.mail_code = kwargs.pop('mail_code') - self.mail_fields = None - super().__init__(*args) - - # Gather data - mail_template = loader.get_template('mail_templates/%s.html' % self.mail_code) - 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.recipient = None - if self.object: - recipient = self.object - for attr in self.mail_data.get('to_address').split('.'): - recipient = getattr(recipient, attr) - if inspect.ismethod(recipient): - recipient = recipient() - self.recipient = recipient + # This form shouldn't be is_bound==True is there is any non-relavant POST data given. + data = args[0] + 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) - if not self.recipient: + if not self.original_recipient: self.fields['extra_recipient'].label = "Send this email to" self.fields['extra_recipient'].required = True # Set the data as initials - self.fields['text'].initial = mail_template + self.fields['text'].initial = self.mail_template self.fields['subject'].initial = self.mail_data['subject'] def save_data(self): # Get text and html - html_message = self.cleaned_data['text'] - handler = HTML2Text() - message = handler.handle(html_message) + self.html_message = self.cleaned_data['text'] + self.subject = self.cleaned_data['subject'] + self.validate_message() # Get recipients list. Try to send through BCC to prevent privacy issues! - bcc_list = [] - if self.mail_data.get('bcc_to', False) and self.object: - if re.match("[^@]+@[^@]+\.[^@]+", self.mail_data.get('bcc_to')): - bcc_list = [self.mail_data.get('bcc_to')] - else: - bcc_to = self.object - for attr in self.mail_data.get('bcc_to').split('.'): - bcc_to = getattr(bcc_to, attr) - - if not isinstance(bcc_to, list): - bcc_list = [bcc_to] - else: - bcc_list = bcc_to - elif re.match("[^@]+@[^@]+\.[^@]+", self.mail_data.get('bcc_to', '')): - bcc_list = [self.mail_data.get('bcc_to')] - - if self.cleaned_data.get('extra_recipient') and self.recipient: - bcc_list.append(self.cleaned_data.get('extra_recipient')) - elif self.cleaned_data.get('extra_recipient') and not self.recipient: - self.recipient = [self.cleaned_data.get('extra_recipient')] - elif not self.recipient: + if self.cleaned_data.get('extra_recipient') and self.original_recipient: + self.bcc_list.append(self.cleaned_data.get('extra_recipient')) + elif self.cleaned_data.get('extra_recipient') and not self.original_recipient: + self.original_recipient = [self.cleaned_data.get('extra_recipient')] + elif not self.original_recipient: self.add_error('extra_recipient', 'Please fill the bcc field to send the mail.') - # Check the send list - if isinstance(self.recipient, list): - recipients = self.recipient - elif not isinstance(self.recipient, str): - try: - recipients = list(self.recipient) - except TypeError: - recipients = [self.recipient] - else: - recipients = [self.recipient] - recipients = list(recipients) - - # Check if email needs to be taken from instance - _recipients = [] - for recipient in recipients: - if isinstance(recipient, Contributor): - _recipients.append(recipient.user.email) - elif isinstance(recipient, get_user_model()): - _recipients.append(recipient.email) - elif isinstance(recipient, str): - _recipients.append(recipient) - - self.mail_fields = { - 'subject': self.cleaned_data['subject'], - 'message': message, - 'html_message': html_message, - 'recipients': _recipients, - 'bcc_list': bcc_list, - } + self.validate_recipients() + self.save_mail_data() def clean(self): data = super().clean() self.save_data() return data - def send(self): - # Send the mail - email = EmailMultiAlternatives( - self.mail_fields['subject'], - self.mail_fields['message'], - '%s <%s>' % (self.mail_data.get('from_address_name', 'SciPost'), - self.mail_data.get('from_address', 'no-reply@scipost.org')), # From - self.mail_fields['recipients'], # To - bcc=self.mail_fields['bcc_list'], - 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) + +class HiddenDataForm(forms.Form): + def __init__(self, form, *args, **kwargs): + super().__init__(form.data, *args, **kwargs) + for name, field in form.fields.items(): + self.fields[name] = field + self.fields[name].widget = forms.HiddenInput() diff --git a/mails/mixins.py b/mails/mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..029097f2431274a82fb2de98a3837c6ba5594cb9 --- /dev/null +++ b/mails/mixins.py @@ -0,0 +1,214 @@ +import re +import json +import inspect +from html2text import HTML2Text + +from django.core.mail import EmailMultiAlternatives +from django.contrib import messages +from django.contrib.auth import get_user_model +from django.conf import settings +from django.template import loader + +from scipost.models import Contributor + + +from . import forms + + +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 + has_permission_to_send_mail = True + + 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. + """ + if not self.has_permission_to_send_mail: + # Don't use the mail form; don't send out the mail. + return super().post(request, *args, **kwargs) + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + self.mail_form = forms.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=forms.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. + """ + # Don't use the mail form; don't send out the mail. + if not self.has_permission_to_send_mail: + return super().form_valid(form) + + try: + self.mail_form.send() + except AttributeError: + # self.mail_form is None + raise AttributeError('Did you check the order in which MailEditorMixin is used?') + messages.success(self.request, 'Mail sent') + return super().form_valid(form) + + +class MailUtilsMixin: + """ + This mixin takes care of inserting the default data into the Utils or Form. + """ + object = None + mail_fields = {} + mail_template = '' + html_message = '' + message = '' + + def __init__(self, *args, **kwargs): + self.mail_code = kwargs.pop('mail_code') + self.instance = kwargs.pop('instance', None) + + # Gather meta data + json_location = '%s/mails/templates/mail_templates/%s.json' % (settings.BASE_DIR, + self.mail_code) + try: + self.mail_data = json.loads(open(json_location).read()) + except OSError: + raise NotImplementedError(('You did not create a valid .html and .json file ' + 'for mail_code: %s' % self.mail_code)) + + # Save central object/instance + self.object = self.get_object(**kwargs) + + # Digest the templates + mail_template = loader.get_template('mail_templates/%s.html' % self.mail_code) + if self.instance and self.mail_data.get('context_object'): + kwargs[self.mail_data['context_object']] = self.instance + self.mail_template = mail_template.render(kwargs) + + # Gather Recipients data + self.original_recipient = '' + if self.object: + recipient = self.object + for attr in self.mail_data.get('to_address').split('.'): + recipient = getattr(recipient, attr) + if inspect.ismethod(recipient): + recipient = recipient() + self.original_recipient = recipient + + self.subject = self.mail_data['subject'] + + + def validate_recipients(self): + # Get recipients list. Try to send through BCC to prevent privacy issues! + self.bcc_list = [] + if self.mail_data.get('bcc_to', False) and self.object: + if re.match("[^@]+@[^@]+\.[^@]+", self.mail_data.get('bcc_to')): + self.bcc_list = [self.mail_data.get('bcc_to')] + else: + bcc_to = self.object + for attr in self.mail_data.get('bcc_to').split('.'): + bcc_to = getattr(bcc_to, attr) + + if not isinstance(bcc_to, list): + self.bcc_list = [bcc_to] + else: + self.bcc_list = bcc_to + elif re.match("[^@]+@[^@]+\.[^@]+", self.mail_data.get('bcc_to', '')): + self.bcc_list = [self.mail_data.get('bcc_to')] + + # Check the send list + if isinstance(self.original_recipient, list): + recipients = self.original_recipient + elif not isinstance(self.original_recipient, str): + try: + recipients = list(self.original_recipient) + except TypeError: + recipients = [self.original_recipient] + else: + recipients = [self.original_recipient] + recipients = list(recipients) + + # Check if email needs to be taken from an instance + _recipients = [] + for recipient in recipients: + if isinstance(recipient, Contributor): + _recipients.append(recipient.user.email) + elif isinstance(recipient, get_user_model()): + _recipients.append(recipient.email) + elif isinstance(recipient, str): + _recipients.append(recipient) + self.recipients = _recipients + + def validate_message(self): + if not self.html_message: + self.html_message = self.mail_template + handler = HTML2Text() + self.message = handler.handle(self.html_message) + + def validate(self): + """ + Ease workflow by called this wrapper validation method. + + Only to be used when the default data is used, eg. not in the EmailTemplateForm. + """ + self.validate_message() + self.validate_recipients() + self.save_mail_data() + + def save_mail_data(self): + self.mail_fields = { + 'subject': self.subject, + 'message': self.message, + 'html_message': self.html_message, + 'recipients': self.recipients, + 'bcc_list': self.bcc_list, + } + + def get_object(self, **kwargs): + if self.object: + return self.object + if self.instance: + return self.instance + + if self.mail_data.get('context_object'): + return kwargs.get(self.mail_data['context_object'], None) + + def send(self): + # Send the mail + email = EmailMultiAlternatives( + self.mail_fields['subject'], + self.mail_fields['message'], + '%s <%s>' % (self.mail_data.get('from_address_name', 'SciPost'), + self.mail_data.get('from_address', 'no-reply@scipost.org')), # From + self.mail_fields['recipients'], # To + bcc=self.mail_fields['bcc_list'], + 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() diff --git a/mails/templates/mail_templates/citation_notification.html b/mails/templates/mail_templates/citation_notification.html new file mode 100644 index 0000000000000000000000000000000000000000..61021214c03b162b488e198f6d853bdc45256d03 --- /dev/null +++ b/mails/templates/mail_templates/citation_notification.html @@ -0,0 +1,65 @@ + +Dear {{ notification.get_title }} {{ notification.last_name }}, + +<br> +<br> + +<p> + We would like to notify you that your work has been cited in + + {% if notification.related_notifications.for_publications %} + {% if notification.related_notifications.for_publications|length > 1 %}{{ notification.related_notifications.for_publications|length }} papers{% else %}a paper{% endif %} + published by SciPost: + + <ul> + {% for notification in notification.related_notifications.for_publications %} + <li> + <a href="https://doi.org/{{ notification.publication.doi_label }}">{{ notification.publication.citation }}</a> + <br> + {{ notification.publication.title }} + <br> + <i>by {{ notification.publication.author_list }}</i> + </li> + {% endfor %} + </ul> + {% endif %} + + {% if notification.related_notifications.for_submissions %} + {% if notification.related_notifications.for_submissions|length > 1 %}{{ notification.related_notifications.for_submissions|length }} manuscripts{% else %}a manuscript{% endif %} + submitted to SciPost, + + <ul> + {% for notification in notification.related_notifications.for_submissions %} + <li> + {{ notification.submission.title }} + <br> + <i>by {{ notification.submission.author_list }}</i> + <br> + <a href="https://scipost.org/{{ notification.submission.get_absolute_url }}">View the submission's page</a> + </li> + {% endfor %} + </ul> + {% endif %} +</p> + +{% if notification.related_notifications.for_publications %} + <p>We hope you will find this paper of interest to your own research.</p> +{% else %} + <p>You might for example consider reporting or commenting on the above submission before the refereeing deadline.</p> +{% endif %} + +<p> + Best regards, + <br> + The SciPost Team +</p> + +{% if notification.get_first_related_contributor %} + <p style="font-size: 10px;"> + Don\'t want to receive such emails? <a href="https://scipost.org/{% url 'scipost:unsubscribe' notification.get_first_related_contributor.id notification.get_first_related_contributor.activation_key %}">Unsubscribe</a> + </p> +{% endif %} + + + +{% include 'email/_footer.html' %} diff --git a/mails/templates/mail_templates/citation_notification.json b/mails/templates/mail_templates/citation_notification.json new file mode 100644 index 0000000000000000000000000000000000000000..a9314819355439cff7eb54d7fae2942f9b647524 --- /dev/null +++ b/mails/templates/mail_templates/citation_notification.json @@ -0,0 +1,8 @@ +{ + "subject": "SciPost: citation notification", + "to_address": "email", + "bcc_to": "admin@scipost.org", + "from_address_name": "SciPost Admin", + "from_address": "admin@scipost.org", + "context_object": "notification" +} diff --git a/mails/templates/mail_templates/registration_invitation.html b/mails/templates/mail_templates/registration_invitation.html new file mode 100644 index 0000000000000000000000000000000000000000..fceaa5f9956115a983731e522ee8a74e28fb4f16 --- /dev/null +++ b/mails/templates/mail_templates/registration_invitation.html @@ -0,0 +1,204 @@ +{% if invitation.invitation_type == 'F' %} + <strong>RE: Invitation to join the Editorial College of SciPost</strong> + <br> +{% endif %} + +Dear {% if invitation.message_style == 'F' %}{{ invitation.get_title_display }} {{ invitation.last_name }}{% else %}{{ invitation.first_name }}{% endif %}, + +<br><br> + +{% if invitation.personal_message %} + {{ invitation.personal_message|linebreaksbr }} + <br> +{% endif %} + + +{% if invitation.invitation_type == 'R' %} + {# Referee invite #} + <p> + We would hereby like to cordially invite you to become a Contributor on SciPost (this is required in order to deliver reports; our records show that you are not yet registered); + for your convenience, we have prepared a pre-filled <a href="https://scipost.org/invitation/{{ invitation.invitation_key }}">registration form</a> for you. + After activation of your registration, you will be allowed to contribute, in particular by providing referee reports. + </p> + <p> + To ensure timely processing of the submission (out of respect for the authors), + we would appreciate a quick accept/decline response from you, ideally within the next 2 days. + </p> + <p> + If you are <strong>not</strong> able to provide a Report, you can let us know by simply <a href="https://scipost.org/submissions/decline_ref_invitation/{{ invitation.invitation_key }}"> clicking here</a>. + </p> + <p> + If you are able to provide a Report, you can confirm this after registering and logging in (you will automatically be prompted for a confirmation). + </p> + <p> + We very much hope that we can count on your expertise, + <br> + Many thanks in advance, + <br> + The SciPost Team + </p> + +{% elif invitation.invitation_type == 'C' %} + {# "Regular" invite #} + {% if invitation.citation_notifications.for_publications %} + <p> + Your work has been cited in + {% if invitation.citation_notifications.for_publications|length > 1 %}{{ invitation.citation_notifications.for_publications|length }} papers{% else %}a paper{% endif %} + published by SciPost: + </p> + + <ul> + {% for notification in invitation.citation_notifications.for_publications %} + <li> + <a href="https://doi.org/{{ notification.publication.doi_label }}">{{ notification.publication.citation }}</a> + <br> + {{ notification.publication.title }} + <br> + <i>by {{ notification.publication.author_list }}</i> + </li> + {% endfor %} + </ul> + {% endif %} + + {% if invitation.citation_notifications.for_submissions %} + <p> + Your work has been cited in + {% if invitation.citation_notifications.for_submissions|length > 1 %}{{ invitation.citation_notifications.for_submissions|length }} manuscripts{% else %}a manuscript{% endif %} + submitted to SciPost, + </p> + + <ul> + {% for notification in invitation.citation_notifications.for_submissions %} + <li> + {{ notification.submission.title }} + <br> + <i>by {{ notification.submission.author_list }}</i> + <br> + <a href="https://scipost.org/{{ notification.submission.get_absolute_url }}">View the submission's page</a> + </li> + {% endfor %} + </ul> + {% endif %} + <p> + I would hereby like to use this opportunity to quickly introduce you to the SciPost initiative, and to invite you to become an active Contributor. + </p> + <p> + In summary, SciPost.org is a publication portal managed by + professional scientists, offering (among others) high-quality + Open Access journals with innovative forms of refereeing, and a + means of commenting on all existing literature. SciPost is established as + a not-for-profit foundation devoted to serving the interests of the + international scientific community. + </p> + <p> + The site is anchored at <a href="https://scipost.org">scipost.org</a>. + Many further details about SciPost, its principles, ideals and implementation can be found at + the <a href="https://scipost.org/about">about</a> + and <a href="https://scipost.org/FAQ">FAQ</a> pages.</p> + <p>As a professional academic, you can register at the + <a href="https://scipost.org/register">registration page</a>, + enabling you to contribute to the site's contents, for example by offering submissions, reports and comments. + </p> + <p> + For your convenience, a partly pre-filled <a href="https://scipost.org/invitation/{{ invitation.invitation_key }}">registration form</a> + has been prepared for you (you can in any case still register at the + <a href="https://scipost.org/register">registration page</a>). + </p> + <p> + If you do develop sympathy for the initiative, besides participating in the + online platform, we would be very grateful if you considered submitting a + publication to one of the journals within the near future, in order to help + establish their reputation. We'll also be looking forward to your reaction, + comments and suggestions about the initiative, which we hope you will find + useful to your work as a professional scientist. + </p> + <p> + Many thanks in advance for taking a few minutes to look into it, + <br> + On behalf of the SciPost Foundation, + <br> + <br> + {{ invitation.invited_by.contributor.get_title_display }} {{ invitation.invited_by.first_name }} {{ invitation.invited_by.last_name }} + </p> +{% elif invitation.invitation_type == 'F' %} + {# Fellow invite #} + <p> + You will perhaps have already heard about SciPost, a publication + portal established by and for professional scientists. + </p> + <p> + SciPost.org is legally based on a not-for-profit foundation and will + operate in perpetuity as a non-commercial entity at the exclusive service + of the academic sector, bringing a cost-slashing alternative to existing practices. + </p> + <p> + SciPost offers a collection of two-way open + access (no subscription fees, no author fees) journals with extremely + stringent (peer-witnessed) refereeing, overseen by + our Editorial College (exclusively composed + of established, professionally practising scientists). The whole process is + designed to ensure the highest achievable scientific quality while making the + editorial workflow as light and efficient as possible. + </p> + <p> + To go straight to the point, on behalf of the SciPost Foundation + and in view of your professional expertise, I hereby would + like to invite you to become an Editorial Fellow and thus join the + Editorial College of SciPost Physics. + </p> + <p> + Please note that only well-known and respected senior academics are + being contacted for this purpose. Academic reputation and involvement + in the community are the most important criteria guiding our + considerations of who should belong to the Editorial College. + </p> + <p> + To help you in considering this, it would be best if you were to take + the time to look at the website itself, which is anchored at scipost.org. + Besides looking around the site, you can also personally register + (to become a Contributor, without necessarily committing to membership + of the Editorial College, this to be discussed separately) by visiting + the following <a href="https://scipost.org/invitation/{{ invitation.cited_in_publication.citation }}"> + single-use link</a>, containing a partly pre-filled form for your convenience. + </p> + <p> + Many details about the initiative + can then be found at scipost.org/about and at scipost.org/FAQ. + Functioning of the College will proceed according to the by-laws set + out in scipost.org/EdCol_by-laws. + </p> + <p> + Since the success of this initiative is dependent on the involvement of + the very people it is meant to serve, we'd be very grateful if you were + to give due consideration to this proposal. We would expect you to + commit just 2-4 hours per month to help perform Editorial duties; we will + constantly adjust the number of Editorial Fellows to ensure this is the case. You + could try it out for 6 months or a year, and of course you could quit + any time you wished. + </p> + <p> + I'd be happy to provide you with more information, should you require + it. In view of our development plans, I would be grateful if you could + react (by replying to this email) within the next two or three weeks, + if possible. I'll be looking forward to your reaction, your comments + and suggestions, be they positive or negative. If you need more time + to consider, that's also fine; just let me know. + </p> + <p> + On behalf of the SciPost Foundation,<br> + <br> + <br> + <br>Prof. dr Jean-Sébastien Caux + <br>--------------------------------------------- + <br>Institute for Theoretial Physics + <br>University of Amsterdam + <br>Science Park 904 + <br>1098 XH Amsterdam + <br>The Netherlands + <br>--------------------------------------------- + <br>tel.: +31 (0)20 5255775\nfax: +31 (0)20 5255778 + <br>--------------------------------------------- + </p> +{% endif %} + +{% include 'email/_footer.html' %} diff --git a/mails/templates/mail_templates/registration_invitation.json b/mails/templates/mail_templates/registration_invitation.json new file mode 100644 index 0000000000000000000000000000000000000000..639804acfbc0fb392c5675b52a7a53fb42106189 --- /dev/null +++ b/mails/templates/mail_templates/registration_invitation.json @@ -0,0 +1,8 @@ +{ + "subject": "SciPost: invitation", + "to_address": "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/mail_templates/registration_invitation_reminder.html b/mails/templates/mail_templates/registration_invitation_reminder.html new file mode 100644 index 0000000000000000000000000000000000000000..586e34be8c6596c7a31b793dfc74e116407c45dc --- /dev/null +++ b/mails/templates/mail_templates/registration_invitation_reminder.html @@ -0,0 +1,4 @@ +<strong>Reminder: Invitation to SciPost</strong> +<br> +<br> +{% include 'mail_templates/registration_invitation.html' %} diff --git a/mails/templates/mail_templates/registration_invitation_reminder.json b/mails/templates/mail_templates/registration_invitation_reminder.json new file mode 100644 index 0000000000000000000000000000000000000000..0d280a9cf2705ca469019528cf3b0b0b0d4bd02c --- /dev/null +++ b/mails/templates/mail_templates/registration_invitation_reminder.json @@ -0,0 +1,8 @@ +{ + "subject": "RE: SciPost: invitation", + "to_address": "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/mail_templates/registration_invitation_renewal.html b/mails/templates/mail_templates/registration_invitation_renewal.html index 1456cda9998e5d04cd4325fbabe39009cde14a1a..c5c8e849c30d3f6f7d235ce0b237e364d5c2882d 100644 --- a/mails/templates/mail_templates/registration_invitation_renewal.html +++ b/mails/templates/mail_templates/registration_invitation_renewal.html @@ -274,7 +274,7 @@ </p> <p>On behalf of the SciPost Foundation, - <br/>Prof. dr Jean-Sébastien Caux + <br/>Prof. dr Jean-Sébastien Caux <br/>--------------------------------------------- <br/>Institute for Theoretial Physics <br/>University of Amsterdam diff --git a/mails/templates/mails/mail_form.html b/mails/templates/mails/mail_form.html index fbb170d4dcffc6286e03a3737fdc24d8eef863dc..4b4f9dd316e2268e89e46af6bad12d20ea79ded3 100644 --- a/mails/templates/mails/mail_form.html +++ b/mails/templates/mails/mail_form.html @@ -11,6 +11,7 @@ <form enctype="multipart/form-data" method="post"> {% csrf_token %} + {% if transfer_data_form %}{{ transfer_data_form }}{% endif %} {{ form|bootstrap }} <div class="form-group row"> <div class="offset-md-2 col-md-10"> diff --git a/mails/utils.py b/mails/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..ee0d910d627e623f9c64064bb1894ce615aec123 --- /dev/null +++ b/mails/utils.py @@ -0,0 +1,14 @@ +from . import mixins + + +class DirectMailUtil(mixins.MailUtilsMixin): + """ + Same templates and json files as the form EmailTemplateForm, but this will directly send + the mails out, without intercepting and showing the mail editor to the user. + """ + + def __init__(self, mail_code, instance, *args, **kwargs): + kwargs['mail_code'] = mail_code + kwargs['instance'] = instance + super().__init__(*args, **kwargs) + self.validate() diff --git a/mails/views.py b/mails/views.py index f7fedc01ded99d81c6f90b0d33f17e8489024ca9..61e4e3ccc9b79ca7e5221092335e3df649aa853c 100644 --- a/mails/views.py +++ b/mails/views.py @@ -1,6 +1,6 @@ from django.shortcuts import render -from .forms import EmailTemplateForm +from .forms import EmailTemplateForm, HiddenDataForm class MailEditingSubView(object): @@ -14,6 +14,9 @@ class MailEditingSubView(object): 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() diff --git a/scipost/admin.py b/scipost/admin.py index 3b8059b73e28851d0ebc3e071091ae2b846fc0b2..c24d9cb4b428eaf0bcf0908c2a390185b1590c1b 100644 --- a/scipost/admin.py +++ b/scipost/admin.py @@ -5,12 +5,9 @@ from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User, Permission from scipost.models import Contributor, Remark,\ - DraftInvitation,\ - RegistrationInvitation,\ AuthorshipClaim, PrecookedEmail,\ EditorialCollege, EditorialCollegeFellowship, UnavailabilityPeriod -from journals.models import Publication from partners.admin import ContactToUserInline from production.admin import ProductionUserInline from submissions.models import Submission @@ -122,49 +119,6 @@ class RemarkAdmin(admin.ModelAdmin): admin.site.register(Remark, RemarkAdmin) -class DraftInvitationAdminForm(forms.ModelForm): - cited_in_submission = forms.ModelChoiceField( - required=False, - queryset=Submission.objects.order_by('-arxiv_identifier_w_vn_nr')) - cited_in_publication = forms.ModelChoiceField( - required=False, - queryset=Publication.objects.order_by('-publication_date')) - - class Meta: - model = DraftInvitation - fields = '__all__' - - -class DraftInvitationAdmin(admin.ModelAdmin): - search_fields = ['first_name', 'last_name', 'email', 'processed'] - form = DraftInvitationAdminForm - - -admin.site.register(DraftInvitation, DraftInvitationAdmin) - - -class RegistrationInvitationAdminForm(forms.ModelForm): - cited_in_submission = forms.ModelChoiceField( - required=False, - queryset=Submission.objects.order_by('-arxiv_identifier_w_vn_nr')) - cited_in_publication = forms.ModelChoiceField( - required=False, - queryset=Publication.objects.order_by('-publication_date')) - - class Meta: - model = RegistrationInvitation - fields = '__all__' - - -class RegistrationInvitationAdmin(admin.ModelAdmin): - search_fields = ['first_name', 'last_name', 'email', 'invitation_key'] - list_display = ['__str__', 'invitation_type', 'invited_by', 'responded'] - list_filter = ['invitation_type', 'message_style', 'responded', 'declined'] - date_hierarchy = 'date_sent' - form = RegistrationInvitationAdminForm - - -admin.site.register(RegistrationInvitation, RegistrationInvitationAdmin) admin.site.register(AuthorshipClaim) admin.site.register(Permission) diff --git a/scipost/forms.py b/scipost/forms.py index 60c748382e617c1bc3a971b3caefdddb3ebb3f63..9f68b07836b683bf59804cdf3fb3e37647dd66d0 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -19,10 +19,9 @@ from captcha.fields import ReCaptchaField from ajax_select.fields import AutoCompleteSelectField from haystack.forms import ModelSearchForm as HayStackSearchForm -from .constants import SCIPOST_DISCIPLINES, TITLE_CHOICES, SCIPOST_FROM_ADDRESSES,\ - INVITATION_CITED_SUBMISSION, INVITATION_CITED_PUBLICATION +from .constants import SCIPOST_DISCIPLINES, TITLE_CHOICES, SCIPOST_FROM_ADDRESSES from .decorators import has_contributor -from .models import Contributor, DraftInvitation, RegistrationInvitation,\ +from .models import Contributor, DraftInvitation,\ UnavailabilityPeriod, PrecookedEmail from affiliations.models import Affiliation, Institution @@ -145,8 +144,7 @@ class DraftInvitationForm(forms.ModelForm): model = DraftInvitation fields = ['title', 'first_name', 'last_name', 'email', 'invitation_type', - 'cited_in_submission', 'cited_in_publication' - ] + 'cited_in_submission', 'cited_in_publication'] def __init__(self, *args, **kwargs): ''' @@ -161,19 +159,14 @@ class DraftInvitationForm(forms.ModelForm): if self.instance.id: return email - if RegistrationInvitation.objects.filter(email=email).exists(): - self.add_error('email', 'This email address has already been used for an invitation') - elif DraftInvitation.objects.filter(email=email).exists(): - self.add_error('email', ('This email address has already been' - ' used for a draft invitation')) - elif User.objects.filter(email=email).exists(): + if User.objects.filter(email=email).exists(): self.add_error('email', 'This email address is already associated to a Contributor') return email def clean_invitation_type(self): invitation_type = self.cleaned_data['invitation_type'] - if invitation_type == 'F' and not self.current_user.has_perm('scipost.can_invite_Fellows'): + if invitation_type == 'F' and not self.current_user.has_perm('scipost.can_invite_fellows'): self.add_error('invitation_type', ('You do not have the authorization' ' to send a Fellow-type invitation.' ' Consider Contributor, or cited (sub/pub).')) @@ -184,95 +177,6 @@ class DraftInvitationForm(forms.ModelForm): return invitation_type -class ContributorsFilterForm(forms.Form): - names = forms.CharField(widget=forms.Textarea()) - include_invitations = forms.BooleanField(required=False, initial=True, - label='Include invitations in the filter.') - - def filter(self): - names_found = [] - names_not_found = [] - invitations_found = [] - r = self.cleaned_data['names'].replace('\r', '\n').split('\n') - include_invitations = self.cleaned_data.get('include_invitations', False) - for name in r: - last_name = name.split(',')[0] - if not last_name: - continue - if Contributor.objects.filter(user__last_name__istartswith=last_name).exists(): - names_found.append(name) - elif include_invitations and RegistrationInvitation.objects.pending_response().filter( - last_name__istartswith=last_name).exists(): - invitations_found.append(name) - else: - names_not_found.append(name) - return names_found, names_not_found, invitations_found - - -class RegistrationInvitationForm(forms.ModelForm): - cited_in_submission = AutoCompleteSelectField('submissions_lookup', required=False) - cited_in_publication = AutoCompleteSelectField('publication_lookup', required=False) - - class Meta: - model = RegistrationInvitation - fields = ['title', 'first_name', 'last_name', 'email', - 'invitation_type', - 'cited_in_submission', 'cited_in_publication', - 'message_style', 'personal_message' - ] - - def __init__(self, *args, **kwargs): - ''' - This form has a required keyword argument `current_user` which is used for validation of - the form fields. - ''' - self.current_user = kwargs.pop('current_user') - if kwargs.get('initial', {}).get('cited_in_submission', False): - kwargs['initial']['cited_in_submission'] = kwargs['initial']['cited_in_submission'].id - if kwargs.get('initial', {}).get('cited_in_publication', False): - kwargs['initial']['cited_in_publication'] = kwargs['initial']['cited_in_publication'].id - - super().__init__(*args, **kwargs) - self.fields['personal_message'].widget.attrs.update( - {'placeholder': ('NOTE: a personal phrase or two.' - ' The bulk of the text will be auto-generated.')}) - - self.fields['cited_in_publication'] = forms.ModelChoiceField( - queryset=Publication.objects.all().order_by('-publication_date'), - required=False) - - def clean(self): - data = self.cleaned_data - if data.get('invitation_type') == INVITATION_CITED_SUBMISSION: - if not data.get('cited_in_submission'): - self.add_error('cited_in_submission', 'Please state the Submission cited.') - if data.get('invitation_type') == INVITATION_CITED_PUBLICATION: - if not data.get('cited_in_publication'): - self.add_error('cited_in_publication', 'Please state the Publication cited.') - return data - - def clean_email(self): - email = self.cleaned_data['email'] - if RegistrationInvitation.objects.filter(email=email).exists(): - self.add_error('email', 'This email address has already been used for an invitation') - elif User.objects.filter(email=email).exists(): - self.add_error('email', 'This email address is already associated to a Contributor') - - return email - - def clean_invitation_type(self): - invitation_type = self.cleaned_data['invitation_type'] - if invitation_type == 'F' and not self.current_user.has_perm('scipost.can_invite_Fellows'): - self.add_error('invitation_type', ('You do not have the authorization' - ' to send a Fellow-type invitation.' - ' Consider Contributor, or cited (sub/pub).')) - if invitation_type == 'R': - self.add_error('invitation_type', ('Referee-type invitations must be made by the' - ' Editor-in-charge at the relevant Submission' - '\'s Editorial Page. ')) - return invitation_type - - class ModifyPersonalMessageForm(forms.Form): personal_message = forms.CharField(widget=forms.Textarea()) diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index 36a786563117ac25975e17bc3520a63a35a04084..38cb301326cb85344b42192f17ec18983356bcd4 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -66,27 +66,20 @@ class Command(BaseCommand): content_type=content_type) # Registration and invitations - change_draft_invitation, created = Permission.objects.get_or_create( - codename='change_draftinvitation', - defaults={ - 'name': 'Can vet registration requests', - 'content_type': content_type_draft_invitation - } - ) can_vet_registration_requests, created = Permission.objects.get_or_create( codename='can_vet_registration_requests', name='Can vet registration requests', content_type=content_type) - can_draft_registration_invitations, created = Permission.objects.get_or_create( - codename='can_draft_registration_invitations', - name='Can draft registration invitations', + can_create_registration_invitations, created = Permission.objects.get_or_create( + codename='can_create_registration_invitations', + name='Can create registration invitations', content_type=content_type) can_manage_registration_invitations, created = Permission.objects.get_or_create( codename='can_manage_registration_invitations', name='Can manage registration invitations', content_type=content_type) - can_invite_Fellows, created = Permission.objects.get_or_create( - codename='can_invite_Fellows', + can_invite_fellows, created = Permission.objects.get_or_create( + codename='can_invite_fellows', name='Can invite Fellows', content_type=content_type) can_resend_registration_requests, created = Permission.objects.get_or_create( @@ -284,7 +277,7 @@ class Command(BaseCommand): SciPostAdmin.permissions.set([ can_read_all_privacy_sensitive_data, can_manage_registration_invitations, - change_draft_invitation, + can_create_registration_invitations can_email_group_members, can_email_particulars, can_resend_registration_requests, @@ -315,14 +308,14 @@ class Command(BaseCommand): AdvisoryBoard.permissions.set([ can_manage_registration_invitations, - change_draft_invitation, + can_create_registration_invitations can_attend_VGMs, can_view_statistics, ]) EditorialAdmin.permissions.set([ can_view_pool, - can_invite_Fellows, + can_invite_fellows, can_assign_submissions, can_do_plagiarism_checks, can_oversee_refereeing, @@ -372,12 +365,12 @@ class Command(BaseCommand): ]) Ambassadors.permissions.set([ + can_create_registration_invitations, can_manage_registration_invitations, - change_draft_invitation, ]) JuniorAmbassadors.permissions.set([ - can_draft_registration_invitations, + can_create_registration_invitations, ]) ProductionSupervisors.permissions.set([ diff --git a/scipost/managers.py b/scipost/managers.py index db457fd9bd20d2ea05fafc34f92021b5f50d5740..9233bc9be2ea6f0bf4840ce9410658b46f4019ad 100644 --- a/scipost/managers.py +++ b/scipost/managers.py @@ -2,8 +2,7 @@ from django.db import models from django.db.models import Q from django.utils import timezone -from .constants import CONTRIBUTOR_NORMAL, INVITATION_EDITORIAL_FELLOW,\ - CONTRIBUTOR_NEWLY_REGISTERED, AUTHORSHIP_CLAIM_PENDING +from .constants import CONTRIBUTOR_NORMAL, CONTRIBUTOR_NEWLY_REGISTERED, AUTHORSHIP_CLAIM_PENDING today = timezone.now().date() @@ -37,25 +36,6 @@ class ContributorQuerySet(models.QuerySet): return self.filter(user__groups__name='Editorial College') -class RegistrationInvitationManager(models.Manager): - def pending_invited_fellows(self): - return self.filter(invitation_type=INVITATION_EDITORIAL_FELLOW, - responded=False, declined=False) - - def declined_invited_fellows(self): - return self.filter(invitation_type=INVITATION_EDITORIAL_FELLOW, - responded=False, declined=True) - - def declined(self): - return self.filter(responded=True, declined=True) - - def pending_response(self): - return self.filter(responded=False) - - def declined_or_without_response(self): - return self.filter(Q(responded=True, declined=True) | Q(responded=False)) - - class UnavailabilityPeriodManager(models.Manager): def today(self): return self.filter(start__lte=today, end__gte=today) 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/models.py b/scipost/models.py index b217d78e78b1c207bbb7cb6aab19ae709dc0438d..46e448359e087de28e4b7c138255143d56f45354 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -18,7 +18,7 @@ from .constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS,\ AUTHORSHIP_CLAIM_PENDING, AUTHORSHIP_CLAIM_STATUS,\ CONTRIBUTOR_NEWLY_REGISTERED from .fields import ChoiceArrayField -from .managers import FellowManager, ContributorQuerySet, RegistrationInvitationManager,\ +from .managers import FellowManager, ContributorQuerySet,\ UnavailabilityPeriodManager, AuthorshipClaimQuerySet today = timezone.now().date() @@ -189,11 +189,11 @@ class DraftInvitation(models.Model): class RegistrationInvitation(models.Model): """ - Invitation to particular persons for registration + Deprecated: Use the `invitations` app """ title = models.CharField(max_length=4, choices=TITLE_CHOICES) - first_name = models.CharField(max_length=30, default='') - last_name = models.CharField(max_length=30, default='') + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) email = models.EmailField() invitation_type = models.CharField(max_length=2, choices=INVITATION_TYPE, default=INVITATION_CONTRIBUTOR) @@ -210,7 +210,7 @@ class RegistrationInvitation(models.Model): invitation_key = models.CharField(max_length=40, unique=True) key_expires = models.DateTimeField(default=timezone.now) date_sent = models.DateTimeField(default=timezone.now) - invited_by = models.ForeignKey(Contributor, + invited_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, blank=True, null=True) nr_reminders = models.PositiveSmallIntegerField(default=0) @@ -218,29 +218,11 @@ class RegistrationInvitation(models.Model): responded = models.BooleanField(default=False) declined = models.BooleanField(default=False) - objects = RegistrationInvitationManager() - - class Meta: - ordering = ['last_name'] - - def __str__(self): - return (self.first_name + ' ' + self.last_name - + ' on ' + self.date_sent.strftime("%Y-%m-%d")) - - def refresh_keys(self, force_new_key=False): - # Generate email activation key and link - if not self.invitation_key or force_new_key: - salt = "" - for i in range(5): - salt = salt + random.choice(string.ascii_letters) - salt = salt.encode('utf8') - invitationsalt = self.last_name.encode('utf8') - self.invitation_key = hashlib.sha1(salt + invitationsalt).hexdigest() - self.key_expires = timezone.now() + datetime.timedelta(days=365) - self.save() - class CitationNotification(models.Model): + """ + Deprecated: Use the `invitations` app + """ contributor = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE) cited_in_submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE, @@ -250,16 +232,6 @@ class CitationNotification(models.Model): 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_identifier_w_vn_nr - elif self.cited_in_publication: - text += self.cited_in_publication.citation() - if self.processed: - text += ' (processed)' - return text - class AuthorshipClaim(models.Model): claimant = models.ForeignKey('scipost.Contributor', 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; diff --git a/scipost/templates/partials/scipost/personal_page/editorial_actions.html b/scipost/templates/partials/scipost/personal_page/editorial_actions.html index b8fc99a5ff64bcbe90296a7f436111cbd2c5e4cb..0996ec371eb0ca3527c2fc3deb66cf7d6e3e089a 100644 --- a/scipost/templates/partials/scipost/personal_page/editorial_actions.html +++ b/scipost/templates/partials/scipost/personal_page/editorial_actions.html @@ -10,7 +10,7 @@ </div> <div class="row"> - {% if perms.scipost.can_vet_registration_requests or perms.scipost.can_draft_registration_invitations or perms.scipost.can_manage_registration_invitations %} + {% if perms.scipost.can_vet_registration_requests or perms.scipost.can_create_registration_invitations or perms.scipost.can_resend_registration_requests %} <div class="col-md-4"> <h3>Registration actions</h3> <ul> @@ -20,19 +20,15 @@ {% if perms.scipost.can_resend_registration_requests %} <li><a href="{% url 'scipost:registration_requests' %}">Awaiting validation</a> ({{ nr_reg_awaiting_validation }})</li> {% endif %} - {% if perms.scipost.can_draft_registration_invitations %} - <li><a href="{% url 'scipost:contributors_filter' %}">Contributors filter</a></li> - <li><a href="{% url 'scipost:draft_registration_invitation' %}">Draft a Registration Invitation</a></li> - {% endif %} - {% if perms.scipost.can_manage_registration_invitations %} - <li><a href="{% url 'scipost:registration_invitations' %}">Manage Registration Invitations</a></li> + {% if perms.scipost.can_create_registration_invitations %} + <li><a href="{% url 'invitations:list' %}">Manage Registration Invitations</a></li> {% endif %} </ul> {% if perms.scipost.can_manage_registration_invitations %} <h3>Notifications</h3> <ul> - <li><a href="{% url 'scipost:citation_notifications' %}">Manage citation notifications</a></li> + <li><a href="{% url 'invitations:citation_notification_list' %}">Manage citation notifications</a></li> </ul> {% endif %} diff --git a/scipost/templates/scipost/_draft_registration_tables.html b/scipost/templates/scipost/_draft_registration_tables.html deleted file mode 100644 index 76765d3f3e4b824531631a0ed6349fc29c29c4aa..0000000000000000000000000000000000000000 --- a/scipost/templates/scipost/_draft_registration_tables.html +++ /dev/null @@ -1,425 +0,0 @@ - -<h2 class="highlight">Invitations sent (response pending)</h2> - -<h3>Editorial Fellows ({{sent_reg_inv_fellows|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_sent_reg_inv_fellows">view/hide +</a> - -<table class="table" id="table_sent_reg_inv_fellows" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - <th></th> - </tr> - </thead> - <tbody> - {% for fellow in sent_reg_inv_fellows %} - <tr> - <td>{{ fellow.last_name }}</td> - <td>{{ fellow.first_name }}</td> - <td>{{ fellow.get_title_display }}</td> - <td>{{ fellow.email }}</td> - <td>{{ fellow.date_sent }} </td> - <td>{{ fellow.get_invitation_type_display }}</td> - <td>{{ fellow.invited_by.user.first_name }} {{ fellow.invited_by.user.last_name }}</td> - <td> - {% if perms.scipost.can_invite_Fellows %} - <a href="{% url 'scipost:renew_registration_invitation' invitation_id=fellow.id %}">Renew</a> ({{ fellow.nr_reminders }}) {% if fellow.date_last_reminded %}(last: {{ fellow.date_last_reminded|date:"Y-m-d" }}){% endif %} - · - <a href="{% url 'scipost:mark_reg_inv_as_declined' invitation_id=fellow.id %}">Declined</a> - {% endif %} - </td> - - </tr> - {% empty %} - <tr> - <td colspan="7">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - -<h3>Normal Contributors ({{sent_reg_inv_contrib|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_sent_reg_inv_contrib">view/hide +</a> - -<table class="table" id="table_sent_reg_inv_contrib" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Title</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - <th></th> - </tr> - </thead> - <tbody> - {% for invitation in sent_reg_inv_contrib %} - <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> - <td>{{ invitation.get_title_display }}</td> - <td>{{ invitation.email }}</td> - <td>{{ invitation.date_sent }} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.invited_by.user.first_name }} {{ invitation.invited_by.user.last_name }}</td> - <td> - {% if perms.scipost.can_invite_Fellows %} - <a href="{% url 'scipost:renew_registration_invitation' invitation_id=invitation.id %}">Renew</a> ({{ invitation.nr_reminders }}) {% if invitation.date_last_reminded %}(last: {{ invitation.date_last_reminded|date:"Y-m-d" }}){% endif %} - · - <a href="{% url 'scipost:mark_reg_inv_as_declined' invitation_id=invitation.id %}">Declined</a> - {% endif %} - </td> - </tr> - {% empty %} - <tr> - <td colspan="7">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - - -<h3>Referees ({{sent_reg_inv_ref|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_sent_reg_inv_ref">view/hide +</a> - -<table class="table" id="table_sent_reg_inv_ref" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Title</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - <th></th> - </tr> - </thead> - <tbody> - {% for invitation in sent_reg_inv_ref %} - <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> - <td>{{ invitation.get_title_display }}</td> - <td>{{ invitation.email }}</td> - <td>{{ invitation.date_sent }} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.invited_by.user.first_name }} {{ invitation.invited_by.user.last_name }}</td> - <td> - {% if perms.scipost.can_invite_Fellows %} - <a href="{% url 'scipost:renew_registration_invitation' invitation_id=invitation.id %}">Renew</a> ({{ invitation.nr_reminders }}) {% if invitation.date_last_reminded %}(last: {{ invitation.date_last_reminded|date:"Y-m-d" }}){% endif %} - {% endif %} - </td> - </tr> - {% empty %} - <tr> - <td colspan="7">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - -<h3>Cited in sub ({{sent_reg_inv_cited_sub|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_sent_reg_inv_cited_sub">view/hide +</a> - -<table class="table" id="table_sent_reg_inv_cited_sub" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Title</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - <th></th> - </tr> - </thead> - <tbody> - {% for invitation in sent_reg_inv_cited_sub %} - <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> - <td>{{ invitation.get_title_display }}</td> - <td>{{ invitation.email }}</td> - <td>{{ invitation.date_sent }} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.invited_by.user.first_name }} {{ invitation.invited_by.user.last_name }}</td> - <td> - {% if perms.scipost.can_invite_Fellows %} - <a href="{% url 'scipost:renew_registration_invitation' invitation_id=invitation.id %}">Renew</a> ({{ invitation.nr_reminders }}) {% if invitation.date_last_reminded %}(last: {{ invitation.date_last_reminded|date:"Y-m-d" }}){% endif %} - {% endif %} - </td> - </tr> - {% empty %} - <tr> - <td colspan="7">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - - -<h3>Cited in pub ({{sent_reg_inv_cited_pub|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_sent_reg_inv_cited_pub">view/hide +</a> - -<table class="table" id="table_sent_reg_inv_cited_pub" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Title</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - <th></th> - </tr> - </thead> - <tbody> - {% for invitation in sent_reg_inv_cited_pub %} - <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> - <td>{{ invitation.get_title_display }}</td> - <td>{{ invitation.email }}</td> - <td>{{ invitation.date_sent }} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.invited_by.user.first_name }} {{ invitation.invited_by.user.last_name }}</td> - <td> - {% if perms.scipost.can_invite_Fellows %} - <a href="{% url 'scipost:renew_registration_invitation' invitation_id=invitation.id %}">Renew</a> ({{ invitation.nr_reminders }}) {% if invitation.date_last_reminded %}(last: {{ invitation.date_last_reminded|date:"Y-m-d" }}){% endif %} - {% endif %} - </td> - </tr> - {% empty %} - <tr> - <td colspan="7">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - -<h2 class="highlight">Invitations sent (responded)</h2> - -<h3>Editorial Fellows ({{resp_reg_inv_fellow|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_resp_reg_inv_fellow">view/hide +</a> - -<table class="table" id="table_resp_reg_inv_fellow" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Title</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - </tr> - </thead> - <tbody> - {% for invitation in resp_reg_inv_fellow %} - <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> - <td>{{ invitation.get_title_display }}</td> - <td>{{ invitation.email }}</td> - <td>{{ invitation.date_sent }} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.invited_by.user.first_name }} {{ invitation.invited_by.user.last_name }}</td> - </tr> - {% empty %} - <tr> - <td colspan="6">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - -<h3>Normal Contributors ({{resp_reg_inv_contrib|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_resp_reg_inv_contrib">view/hide +</a> - -<table class="table" id="table_resp_reg_inv_contrib" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Title</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - </tr> - </thead> - <tbody> - {% for invitation in resp_reg_inv_contrib %} - <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> - <td>{{ invitation.get_title_display }}</td> - <td>{{ invitation.email }}</td> - <td>{{ invitation.date_sent }} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.invited_by.user.first_name }} {{ invitation.invited_by.user.last_name }}</td> - </tr> - {% empty %} - <tr> - <td colspan="6">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - -<h3>Referees ({{resp_reg_inv_ref|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_resp_reg_inv_ref">view/hide +</a> - -<table class="table" id="table_resp_reg_inv_ref" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Title</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - </tr> - </thead> - <tbody> - {% for invitation in resp_reg_inv_ref %} - <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> - <td>{{ invitation.get_title_display }}</td> - <td>{{ invitation.email }}</td> - <td>{{ invitation.date_sent }} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.invited_by.user.first_name }} {{ invitation.invited_by.user.last_name }}</td> - </tr> - {% empty %} - <tr> - <td colspan="6">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - -<h3>Cited in sub ({{resp_reg_inv_cited_sub|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_resp_reg_inv_cited_sub">view/hide +</a> - -<table class="table" id="table_resp_reg_inv_cited_sub" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Title</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - </tr> - </thead> - <tbody> - {% for invitation in resp_reg_inv_cited_sub %} - <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> - <td>{{ invitation.get_title_display }}</td> - <td>{{ invitation.email }}</td> - <td>{{ invitation.date_sent }} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.invited_by.user.first_name }} {{ invitation.invited_by.user.last_name }}</td> - </tr> - {% empty %} - <tr> - <td colspan="6">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - -<h3>Cited in pub ({{resp_reg_inv_cited_pub|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_resp_reg_inv_cited_pub">view/hide +</a> - -<table class="table" id="table_resp_reg_inv_cited_pub" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Title</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - </tr> - </thead> - <tbody> - {% for invitation in resp_reg_inv_cited_pub %} - <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> - <td>{{ invitation.get_title_display }}</td> - <td>{{ invitation.email }}</td> - <td>{{ invitation.date_sent }} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.invited_by.user.first_name }} {{ invitation.invited_by.user.last_name }}</td> - </tr> - {% empty %} - <tr> - <td colspan="6">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - -<h3>Declined ({{decl_reg_inv|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_decl_reg_inv">view/hide +</a> - -<table class="table" id="table_decl_reg_inv" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Title</th> - <th>Email</th> - <th>Date sent</th> - <th>Type</th> - <th>Invited by</th> - </tr> - </thead> - <tbody> - {% for invitation in decl_reg_inv %} - <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> - <td>{{ invitation.get_title_display }}</td> - <td>{{ invitation.email }}</td> - <td>{{ invitation.date_sent }} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.invited_by.user.first_name }} {{ invitation.invited_by.user.last_name }}</td> - </tr> - {% empty %} - <tr> - <td colspan="6">No invitations found.</td> - </tr> - {% endfor %} - </tbody> -</table> - -<h2 class="highlight">List of already-registered contributors ({{names_reg_contributors|length}})</h3> -<a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#registered_contributors">view/hide +</a> - -<div class="card-columns" id="registered_contributors" style="display: none;"> - {% for first_name, last_name in names_reg_contributors %} - <div class="card border-0"> - {{ last_name }}, {{ first_name }} - </div> - {% endfor %} -</div> diff --git a/scipost/templates/scipost/citation_notifications.html b/scipost/templates/scipost/citation_notifications.html deleted file mode 100644 index 49c6616840c6dd543d95e78c5b4fa3cb724c30a0..0000000000000000000000000000000000000000 --- a/scipost/templates/scipost/citation_notifications.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends 'scipost/_personal_page_base.html' %} - -{% block pagetitle %}: citation notifications{% endblock pagetitle %} - -{% block breadcrumb_items %} - {{block.super}} - <span class="breadcrumb-item">Pool</span> -{% endblock %} - -{% block content %} - - -<div class="row"> - <div class="col-12"> - <h1 class="highlight">Citation notifications to process</h1> - {% if errormessage %} - <h3 class="text-danger">{{ errormessage }}</h3> - {% endif %} - - <ul> - {% for un in unprocessed_notifications %} - <li> - {{ un }} <a href="{% url 'scipost:process_citation_notification' cn_id=un.id %}">Process this notification</a> - </li> - {% empty %} - <li>There are no citation notifications to process.</li> - {% endfor %} - </ul> - - <p>Return to your <a href="{% url 'scipost:personal_page' %}">personal page</a>.</p> - </div> -</div> - - - -{% endblock content %} diff --git a/scipost/templates/scipost/contributors_filter.html b/scipost/templates/scipost/contributors_filter.html deleted file mode 100644 index 12418bd8a252a1be98829df8c4ee790cd806ce2a..0000000000000000000000000000000000000000 --- a/scipost/templates/scipost/contributors_filter.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends 'scipost/_personal_page_base.html' %} - -{% load bootstrap %} - -{% block pagetitle %}: contributors filter{% endblock pagetitle %} - -{% block breadcrumb_items %} - {{block.super}} - <a href="{% url 'scipost:draft_registration_invitation' %}" class="breadcrumb-item">Draft registration invitation</a> - <span class="breadcrumb-item">Contributors filter</span> -{% endblock %} - -{% block content %} - -<div class="row"> - <div class="col-12"> - <h1 class="highlight">Contributors filter</h1> - - <p>This form can be used to split your list of names into a list of names with registered or already invited Contributors and a list of unknown names according to the current database.</p> - <p>Please, for every name use the format <code>{last name}</code> or <code>{last name}, {first name}</code> and use one name per line.</p> - - <form method="post"> - {% csrf_token %} - {{ form|bootstrap }} - <input type="submit" class="btn btn-primary" value="Filter"/> - </form> - </div> -</div> - -{% if form.is_bound %} - - <hr class="divider"> - <h2>Filter result</h2> - {% if names_not_found %} - <h3>New names</h3> - <pre class="mb-3"><code>{% for name in names_not_found %}{{ name }}{% if not forloop.last %}<br>{% endif %}{% endfor %}</code></pre> - {% endif %} - - {% if names_found %} - <h3>Names found in the system</h3> - <pre class="mb-3"><code>{% for name in names_found %}{{ name }}{% if not forloop.last %}<br>{% endif %}{% endfor %}</code></pre> - {% endif %} - - {% if invitations_found %} - <h3>Invitations (pending response) found in database</h3> - <pre class="mb-3"><code>{% for name in invitations_found %}{{ name }}{% if not forloop.last %}<br>{% endif %}{% endfor %}</code></pre> - {% endif %} -{% endif %} - -{% endblock %} diff --git a/scipost/templates/scipost/draft_registration_invitation.html b/scipost/templates/scipost/draft_registration_invitation.html deleted file mode 100644 index 4f8900f25de969cfeed59c63bbccb061a33c4c6d..0000000000000000000000000000000000000000 --- a/scipost/templates/scipost/draft_registration_invitation.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends 'scipost/_personal_page_base.html' %} - -{% load bootstrap %} - -{% block pagetitle %}: registration invitations{% endblock pagetitle %} - -{% block breadcrumb_items %} - {{block.super}} - <span class="breadcrumb-item">Draft registration invitation</span> -{% endblock %} - -{% block content %} - -<script> -$(document).ready(function(){ - - $('#id_invitation_type').on('change', function() { - switch ($(this).val()) { - case "ci": - $("#id_cited_in_submission").parents('.form-group').show(); - $("#id_cited_in_publication").parents('.form-group').hide(); - break; - case "cp": - $("#id_cited_in_submission").parents('.form-group').hide(); - $("#id_cited_in_publication").parents('.form-group').show(); - break; - default: - $("#id_cited_in_submission").parents('.form-group').hide(); - $("#id_cited_in_publication").parents('.form-group').hide(); - } - }).trigger('change'); -}); -</script> - -<div class="row"> - <div class="col-12"> - <h1 class="highlight">Draft a registration invitation</h1> - <p>If you have a list of names you want to check with the current database of users, <a href="{% url 'scipost:contributors_filter' %}">please click here</a>.</p> - </div> -</div> - -<div class="row"> - <div class="col-12"> - <h2 class="highlight">Draft a new invitation</h2> - {% if errormessage %} - <h3 class="text-danger">{{ errormessage }}</h3> - {% endif %} - - <form action="{% url 'scipost:draft_registration_invitation' %}" method="post"> - {% csrf_token %} - {{ form.media }} - {{ form|bootstrap }} - <input type="submit" class="btn btn-primary" value="Submit"/> - </form> - </div> -</div> - -<div class="row"> - <div class="col-12"> - <h2 class="highlight">Existing drafts (to be processed by Admin) ({{existing_drafts|length}})</h2> - <a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_existing_drafts">view/hide +</a> - - <table class="table" id="table_existing_drafts"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Email</th> - <th>Date drafted</th> - <th>Type</th> - <th>Drafted by</th> - <th></th> - </tr> - </thead> - <tbody> - {% for draft in existing_drafts %} - <tr> - <td>{{ draft.last_name }}</td> - <td>{{ draft.first_name }}</td> - <td>{{ draft.email }}</td> - <td>{{ draft.date_drafted }} </td> - <td>{{ draft.get_invitation_type_display }}</td> - <td>{{ draft.drafted_by.user.first_name }} {{ draft.drafted_by.user.last_name }}</td> - <td> - {% if draft.drafted_by.user == request.user %} - <a href="{% url 'scipost:edit_draft_reg_inv' draft.id %}">Edit</a> - {% endif %}</td> - </tr> - {% empty %} - <tr> - <td colspan="7">No drafts found.</td> - </tr> - {% endfor %} - </tbody> - </table> - </div> -</div> - -{% endblock %} diff --git a/scipost/templates/scipost/edit_draft_reg_inv.html b/scipost/templates/scipost/edit_draft_reg_inv.html deleted file mode 100644 index f6fd6afe34f7e90492f7e502306e8cc1e280bc1f..0000000000000000000000000000000000000000 --- a/scipost/templates/scipost/edit_draft_reg_inv.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends 'scipost/_personal_page_base.html' %} - -{% block pagetitle %}: edit draft reg inv{% endblock pagetitle %} - -{% block breadcrumb_items %} - {{block.super}} - <a href="{% url 'scipost:registration_invitations' %}" class="breadcrumb-item">Registration Invitations</a> - <span class="breadcrumb-item">Pool</span> -{% endblock %} - -{% load bootstrap %} - -{% block content %} - -<script> -$(document).ready(function(){ - - $('#id_invitation_type').on('change', function() { - switch ($(this).val()) { - case "ci": - $("#id_cited_in_submission").parents('.form-group').show(); - $("#id_cited_in_publication").parents('.form-group').hide(); - break; - case "cp": - $("#id_cited_in_submission").parents('.form-group').hide(); - $("#id_cited_in_publication").parents('.form-group').show(); - break; - default: - $("#id_cited_in_submission").parents('.form-group').hide(); - $("#id_cited_in_publication").parents('.form-group').hide(); - } - }).trigger('change'); -}); -</script> - -<div class="row"> - <div class="col-12"> - <h1 class="highlight">Edit a draft registration invitation</h1> - <form action="{% url 'scipost:edit_draft_reg_inv' draft_id=draft_inv_form.instance.id %}" method="post"> - {% csrf_token %} - {{draft_inv_form.media}} - {{draft_inv_form|bootstrap}} - <input type="submit" class="btn btn-secondary"> - </form> - </div> -</div> - -{% endblock content %} diff --git a/scipost/templates/scipost/edit_invitation_personal_message.html b/scipost/templates/scipost/edit_invitation_personal_message.html deleted file mode 100644 index 5a63e60729afb5e3f300043644f926ab7cfbb2e7..0000000000000000000000000000000000000000 --- a/scipost/templates/scipost/edit_invitation_personal_message.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends 'scipost/base.html' %} - -{% load scipost_extras %} -{% load bootstrap %} - -{% block pagetitle %}: registration invitation: edit personal message{% endblock pagetitle %} - -{% block content %} - -<h2>Edit invitation's personal message: for {{ invitation.first_name }} {{ invitation.last_name }}</h2> - -{% if errormessage %} - <h3 class="text-danger">{{ errormessage }}</h3> -{% endif %} -<form action="{% url 'scipost:edit_invitation_personal_message' invitation_id=invitation.id %}" method="post"> - {% csrf_token %} - {{ form|bootstrap }} - <input type="submit" value="Submit" class="btn btn-primary"> -</form> - -{% endblock %} diff --git a/scipost/templates/scipost/registration_invitation_sent.html b/scipost/templates/scipost/registration_invitation_sent.html deleted file mode 100644 index f2908e563f0affa74ab03d291cfa0f62a1d13e15..0000000000000000000000000000000000000000 --- a/scipost/templates/scipost/registration_invitation_sent.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends 'scipost/base.html' %} - -{% block pagetitle %}: registration invitation sent{% endblock pagetitle %} - -{% block content %} - -<h1>Registration Invitation sent</h1> -<p>Return to the <a href="{% url 'scipost:registration_invitations' %}">registration invitations page</a>.</p> - -{% endblock content %} diff --git a/scipost/templates/scipost/registration_invitations.html b/scipost/templates/scipost/registration_invitations.html deleted file mode 100644 index bcd610d61a0b261225a22c21b222e0e5f2f8f528..0000000000000000000000000000000000000000 --- a/scipost/templates/scipost/registration_invitations.html +++ /dev/null @@ -1,121 +0,0 @@ -{% extends 'scipost/_personal_page_base.html' %} - -{% block pagetitle %}: registration invitations{% endblock pagetitle %} - -{% load scipost_extras %} -{% load bootstrap %} - -{% block breadcrumb_items %} - {{block.super}} - <span class="breadcrumb-item">Registration invitations</span> -{% endblock %} - -{% block content %} - -<script> -$(document).ready(function(){ - - $('#id_invitation_type').on('change', function() { - switch ($(this).val()) { - case "ci": - $("#id_cited_in_submission").parents('.form-group').show(); - $("#id_cited_in_publication").parents('.form-group').hide(); - break; - case "cp": - $("#id_cited_in_submission").parents('.form-group').hide(); - $("#id_cited_in_publication").parents('.form-group').show(); - break; - default: - $("#id_cited_in_submission").parents('.form-group').hide(); - $("#id_cited_in_publication").parents('.form-group').hide(); - } - }).trigger('change'); -}); -</script> - -<div class="row"> - <div class="col-12"> - <div class="card card-grey"> - <div class="card-body"> - <h1 class="card-title">Registration Invitations</h1> - {% if request.user|is_in_group:'SciPost Administrators' %} - <h3>Perform a <a href="{% url 'scipost:registration_invitations_cleanup' %}">cleanup</a> of existing invitations.</h3> - {% endif %} - </div> - </div> - </div> -</div> - -<div class="row"> - <div class="col-12"> - <h2 class="highlight">Send a new invitation</h2> - {% if errormessage %} - <h3 class="text-danger">{{ errormessage }}</h3> - {% endif %} - <form action="{% url 'scipost:registration_invitations' %}" method="post"> - {% csrf_token %} - {{reg_inv_form.media}} - {{reg_inv_form|bootstrap}} - <input type="submit" class="btn btn-primary" value="Submit"> - </form> - </div> -</div> - - -<div class="row"> - <div class="col-12"> - <h2 class="highlight">Existing drafts (to be processed by Admin)</h2> - <a href="javascript:void(0)" class="btn mb-2" data-toggle="toggle" data-target="#table_existing_drafts">view/hide ({{existing_drafts|length}}) +</a> - - <table class="table" id="table_existing_drafts" style="display: none;"> - <thead> - <tr> - <th>Last name</th> - <th>First name</th> - <th>Email</th> - <th>Date drafted</th> - <th>Type</th> - <th>Drafted by</th> - <th colspan="2">Actions</th> - </tr> - </thead> - <tbody> - {% for draft in existing_drafts %} - <tr> - <td>{{ draft.last_name }}</td> - <td>{{ draft.first_name }}</td> - <td>{{ draft.email }}</td> - <td>{{ draft.date_drafted }} </td> - <td>{{ draft.get_invitation_type_display }}</td> - <td>{{ draft.drafted_by.user.first_name }} {{ draft.drafted_by.user.last_name }}</td> - <td> - <a href="{% url 'scipost:edit_draft_reg_inv' draft_id=draft.id %}">Edit</a> | - <a href="{% url 'scipost:registration_invitations_from_draft' draft_id=draft.id %}">Process</a> | - <a href="{% url 'scipost:mark_draft_inv_as_processed' draft_id=draft.id %}">Mark as processed</a> - </td> - <td> - <ul> - {% for ac in draft|associated_contributors %} - <li> - <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> - </li> - {% empty %} - <li>No associated contributors found.</li> - {% endfor %} - </ul> - </td> - </tr> - {% empty %} - <tr> - <td colspan="8">No drafts found.</td> - </tr> - {% endfor %} - </tbody> - </table> - </div> -</div> - -{% include 'scipost/_draft_registration_tables.html' %} - - -{% endblock %} diff --git a/scipost/templatetags/scipost_extras.py b/scipost/templatetags/scipost_extras.py index 567bbb642ddddcf99221efdbb6cd3357f89b5dd9..4e9f47e186c03bdb18ae71c828c29efa40108073 100644 --- a/scipost/templatetags/scipost_extras.py +++ b/scipost/templatetags/scipost_extras.py @@ -38,8 +38,7 @@ def is_in_group(user, group_name): @register.filter(name='associated_contributors') def associated_contributors(draft): - return Contributor.objects.filter( - user__last_name__icontains=draft.last_name) + return Contributor.objects.filter(user__last_name__icontains=draft.last_name) def is_modulo(counter, total, modulo): diff --git a/scipost/urls.py b/scipost/urls.py index 9ab0cf02bef3b221475c65d090119be5b56fd827..9da0a19f216c11051b08f1b2064ccb6774057ea9 100644 --- a/scipost/urls.py +++ b/scipost/urls.py @@ -83,44 +83,9 @@ urlpatterns = [ url(r'^registration_requests$', views.registration_requests, name="registration_requests"), url(r'^registration_requests/(?P<contributor_id>[0-9]+)/reset$', views.registration_requests_reset, name="registration_requests_reset"), - url(r'^registration_invitations/(?P<draft_id>[0-9]+)$', - views.registration_invitations, name="registration_invitations_from_draft"), - url(r'^registration_invitations$', - views.registration_invitations, name="registration_invitations"), - url(r'^draft_registration_invitation$', - views.draft_registration_invitation, name="draft_registration_invitation"), - url(r'^contributors_filter$', views.contributors_filter, name="contributors_filter"), - 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"), - url(r'^remove_registration_invitation/(?P<invitation_id>[0-9]+)$', - views.remove_registration_invitation, - name="remove_registration_invitation"), - url(r'^edit_invitation_personal_message/(?P<invitation_id>[0-9]+)$', - views.edit_invitation_personal_message, - name="edit_invitation_personal_message"), - url(r'^renew_registration_invitation/(?P<invitation_id>[0-9]+)$', - views.renew_registration_invitation, - name="renew_registration_invitation"), - url(r'^mark_reg_inv_as_declined/(?P<invitation_id>[0-9]+)$', - views.mark_reg_inv_as_declined, - name="mark_reg_inv_as_declined"), - url(r'^registration_invitation_sent$', - TemplateView.as_view(template_name='scipost/registration_invitation_sent.html'), - name='registration_invitation_sent'), - # Registration invitations + # Registration invitations (Never change this route! Thank you.) url(r'^invitation/(?P<key>.+)$', views.invitation, name='invitation'), - 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 8c95c5ddc109f882dba1f4052172eaedfce83fe2..de045624ee6fcf1d0e422099b61dcf75f3834aad 100644 --- a/scipost/utils.py +++ b/scipost/utils.py @@ -1,17 +1,3 @@ -import datetime -import hashlib -import random -import string - -from django.contrib.auth.models import User - -from django.core.mail import EmailMultiAlternatives -from django.core.urlresolvers import reverse -from django.template import Context, Template -from django.utils import timezone - -from .models import DraftInvitation, RegistrationInvitation - from common.utils import BaseMailUtil @@ -83,41 +69,6 @@ class Utils(BaseMailUtil): mail_sender = 'registration@scipost.org' mail_sender_title = 'SciPost registration' - @classmethod - def password_mismatch(cls): - if cls.form.cleaned_data['password'] != cls.form.cleaned_data['password_verif']: - return True - else: - return False - - @classmethod - def username_already_taken(cls): - if User.objects.filter(username=cls.form.cleaned_data['username']).exists(): - return True - else: - return False - - @classmethod - def email_already_taken(cls): - if User.objects.filter(email=cls.form.cleaned_data['email']).exists(): - return True - else: - return False - - @classmethod - def email_already_invited(cls): - if RegistrationInvitation.objects.filter(email=cls.form.cleaned_data['email']).exists(): - return True - else: - return False - - @classmethod - def email_already_drafted(cls): - if DraftInvitation.objects.filter(email=cls.form.cleaned_data['email']).exists(): - return True - else: - return False - @classmethod def send_registration_email(cls): """ @@ -141,520 +92,3 @@ class Utils(BaseMailUtil): cls._send_mail(cls, 'new_activation_link', [cls._context['contributor'].user.email], 'new email activation link') - - @classmethod - def create_draft_invitation(cls): - invitation = DraftInvitation( - title=cls.form.cleaned_data['title'], - first_name=cls.form.cleaned_data['first_name'], - last_name=cls.form.cleaned_data['last_name'], - email=cls.form.cleaned_data['email'], - invitation_type=cls.form.cleaned_data['invitation_type'], - cited_in_submission=cls.form.cleaned_data['cited_in_submission'], - cited_in_publication=cls.form.cleaned_data['cited_in_publication'], - drafted_by=cls.contributor, - ) - invitation.save() - - @classmethod - def create_invitation(cls): - invitation = RegistrationInvitation( - title=cls.form.cleaned_data['title'], - first_name=cls.form.cleaned_data['first_name'], - last_name=cls.form.cleaned_data['last_name'], - email=cls.form.cleaned_data['email'], - invitation_type=cls.form.cleaned_data['invitation_type'], - cited_in_submission=cls.form.cleaned_data['cited_in_submission'], - cited_in_publication=cls.form.cleaned_data['cited_in_publication'], - invited_by=cls.contributor, - message_style=cls.form.cleaned_data['message_style'], - personal_message=cls.form.cleaned_data['personal_message'], - ) - Utils.load({'invitation': invitation}) - - @classmethod - def send_registration_invitation_email(cls, renew=False): - signature = (cls.invitation.invited_by.get_title_display() + ' ' - + cls.invitation.invited_by.user.first_name + ' ' - + cls.invitation.invited_by.user.last_name) - if not renew: - # Generate email activation key and link - salt = "" - for i in range(5): - salt = salt + random.choice(string.ascii_letters) - salt = salt.encode('utf8') - invitationsalt = cls.invitation.last_name - invitationsalt = invitationsalt.encode('utf8') - cls.invitation.invitation_key = hashlib.sha1(salt+invitationsalt).hexdigest() - cls.invitation.key_expires = datetime.datetime.strftime( - datetime.datetime.now() + datetime.timedelta(days=365), "%Y-%m-%d %H:%M:%S") - if renew: - cls.invitation.nr_reminders += 1 - cls.invitation.date_last_reminded = timezone.now() - cls.invitation.save() - email_text = '' - email_text_html = '' - email_context = {} - if renew: - email_text += ('Reminder: Invitation to SciPost\n' - '-------------------------------\n\n') - email_text_html += ('<strong>Reminder: Invitation to SciPost</strong>' - '<br/><hr/><br/>') - if cls.invitation.invitation_type == 'F': - email_text += 'RE: Invitation to join the Editorial College of SciPost\n\n' - email_text_html += ('<strong>RE: Invitation to join the Editorial College ' - 'of SciPost</strong><br/><hr/><br/>') - email_text += 'Dear ' - email_text_html += 'Dear ' - if cls.invitation.message_style == 'F': - email_text += cls.invitation.get_title_display() + ' ' + cls.invitation.last_name - email_text_html += '{{ title }} {{ last_name }}' - email_context['title'] = cls.invitation.get_title_display() - email_context['last_name'] = cls.invitation.last_name - else: - email_text += cls.invitation.first_name - email_text_html += '{{ first_name }}' - email_context['first_name'] = cls.invitation.first_name - email_text += ',\n\n' - email_text_html += ',<br/>' - if len(cls.invitation.personal_message) > 3: - email_text += cls.invitation.personal_message + '\n\n' - email_text_html += '\n{{ personal_message|linebreaks }}<br/>\n' - email_context['personal_message'] = cls.invitation.personal_message - - # This text to be put in C, ci invitations - summary_text = ( - '\n\nIn summary, SciPost.org is a publication portal managed by ' - 'professional scientists, offering (among others) high-quality ' - 'Open Access journals with innovative forms of refereeing, and a ' - 'means of commenting on all existing literature. SciPost is established as ' - 'a not-for-profit foundation devoted to serving the interests of the ' - 'international scientific community.' - '\n\nThe site is anchored at https://scipost.org. Many further details ' - 'about SciPost, its principles, ideals and implementation can be found at ' - 'https://scipost.org/about and https://scipost.org/FAQ.' - '\n\nAs a professional academic, you can register at ' - 'https://scipost.org/register, enabling you to contribute to the site\'s ' - 'contents, for example by offering submissions, reports and comments.' - '\n\nFor your convenience, a partly pre-filled registration ' - 'form has been prepared for you at ' - 'https://scipost.org/invitation/' + cls.invitation.invitation_key - + ' (you can in any case still register at ' - 'https://scipost.org/register).\n\n' - 'If you do develop sympathy for the initiative, besides participating in the ' - 'online platform, we would be very grateful if you considered submitting a ' - 'publication to one of the journals within the near future, in order to help ' - 'establish their reputation. We\'ll also be looking forward to your reaction, ' - 'comments and suggestions about the initiative, which we hope you will find ' - 'useful to your work as a professional scientist.' - '\n\nMany thanks in advance for taking a few minutes to look into it,' - '\n\nOn behalf of the SciPost Foundation,\n\n' - + signature + '\n' - ) - - summary_text_html = ( - '\n<p>In summary, SciPost.org is a publication portal managed by ' - 'professional scientists, offering (among others) high-quality ' - 'Open Access journals with innovative forms of refereeing, and a ' - 'means of commenting on all existing literature. SciPost is established as ' - 'a not-for-profit foundation devoted to serving the interests of the ' - 'international scientific community.</p>' - '\n<p>The site is anchored at <a href="https://scipost.org">scipost.org</a>. ' - 'Many further details ' - 'about SciPost, its principles, ideals and implementation can be found at ' - 'the <a href="https://scipost.org/about">about</a> ' - 'and <a href="https://scipost.org/FAQ">FAQ</a> pages.</p>' - '<p>As a professional academic, you can register at the ' - '<a href="https://scipost.org/register">registration page</a>, ' - 'enabling you to contribute to the site\'s ' - 'contents, for example by offering submissions, reports and comments.</p>' - '\n<p>For your convenience, a partly pre-filled ' - '<a href="https://scipost.org/invitation/{{ invitation_key }}">registration form</a>' - ' has been prepared for you (you can in any case still register at the ' - '<a href="https://scipost.org/register">registration page</a>).</p>' - '\n<p>If you do develop sympathy for the initiative, besides participating in the ' - 'online platform, we would be very grateful if you considered submitting a ' - 'publication to one of the journals within the near future, in order to help ' - 'establish their reputation. We\'ll also be looking forward to your reaction, ' - 'comments and suggestions about the initiative, which we hope you will find ' - 'useful to your work as a professional scientist.</p>' - '\n<p>Many thanks in advance for taking a few minutes to look into it,</p>' - '<p>On behalf of the SciPost Foundation,</p>' - # '<br/>Prof. dr Jean-Sébastien Caux' - # '<br/>---------------------------------------------' - # '<br/>Institute for Theoretical Physics' - # '<br/>University of Amsterdam' - # '<br/>Science Park 904' - # '<br/>1098 XH Amsterdam<br/>The Netherlands' - # '<br/>---------------------------------------------' - # '<br/>tel.: +31 (0)20 5255775' - # '<br/>fax: +31 (0)20 5255778' - # '<br/>---------------------------------------------' - '<p>' + signature + '</p>' - ) - email_context['invitation_key'] = cls.invitation.invitation_key - - if cls.invitation.invitation_type == 'R': - # Refereeing invitation - # Details of the submission to referee are already in the personal_message field - email_text += ( - 'We would hereby like to cordially invite you ' - 'to become a Contributor on SciPost ' - '(this is required in order to deliver reports; ' - 'our records show that you are not yet registered); ' - 'for your convenience, we have prepared a pre-filled form for you at\n\n' - 'https://scipost.org/invitation/' + cls.invitation.invitation_key + '\n\n' - 'after which your registration will be activated, allowing you to contribute, ' - 'in particular by providing referee reports.\n\n' - 'To ensure timely processing of the submission (out of respect for the authors), ' - 'we would appreciate a quick accept/decline ' - 'response from you, ideally within the next 2 days.\n\n' - 'If you are not able to provide a Report, you can let us know by simply ' - 'navigating to \n\nhttps://scipost.org/submissions/decline_ref_invitation/' - + cls.invitation.invitation_key + '\n\n' - 'If you are able to provide a Report, you can confirm this after registering ' - 'and logging in (you will automatically be prompted for a confirmation).\n\n' - 'We very much hope that we can count on your expertise,\n\n' - 'Many thanks in advance,\n\nThe SciPost Team') - email_text_html += ( - '\n<p>We would hereby like to cordially invite you ' - 'to become a Contributor on SciPost ' - '(this is required in order to deliver reports; ' - 'our records show that you are not yet registered); ' - 'for your convenience, we have prepared a pre-filled ' - '<a href="https://scipost.org/invitation/{{ invitation_key }}">registration form</a> ' - 'for you. After activation of your registration, you will be allowed to contribute, ' - 'in particular by providing referee reports.</p>' - '<p>To ensure timely processing of the submission (out of respect for the authors), ' - 'we would appreciate a quick accept/decline ' - 'response from you, ideally within the next 2 days.</p>' - '<p>If you are <strong>not</strong> able to provide a Report, ' - 'you can let us know by simply ' - '<a href="https://scipost.org/submissions/decline_ref_invitation/{{ invitation_key }}">' - 'clicking here</a>.</p>' - '<p>If you are able to provide a Report, you can confirm this after registering ' - 'and logging in (you will automatically be prompted for a confirmation).</p>' - '<p>We very much hope that we can count on your expertise,</p>' - '<p>Many thanks in advance,</p>' - '<p>The SciPost Team</p>') - - email_text += SCIPOST_SUMMARY_FOOTER - email_text_html += SCIPOST_SUMMARY_FOOTER_HTML - email_text_html += '<br/>' + EMAIL_FOOTER - html_template = Template(email_text_html) - html_version = html_template.render(Context(email_context)) - emailmessage = EmailMultiAlternatives( - 'SciPost: refereeing request (and registration invitation)', email_text, - 'SciPost Refereeing <refereeing@scipost.org>', - [cls.invitation.email], - cc=[cls.invitation.invited_by.user.email], - bcc=['refereeing@scipost.org'], - reply_to=['refereeing@scipost.org']) - emailmessage.attach_alternative(html_version, 'text/html') - - elif cls.invitation.invitation_type == 'ci': - # Has been cited in a Submission. Invite! - email_text += ( - 'Your work has been cited in a manuscript submitted to SciPost,' - '\n\n' + cls.invitation.cited_in_submission.title - + ' by ' + cls.invitation.cited_in_submission.author_list + '.\n\n' - 'I would hereby like to use this opportunity to quickly introduce ' - 'you to the SciPost initiative, and to invite you to become an active ' - 'Contributor to the site. 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>' - '\n<p>I would hereby like to use this opportunity to quickly introduce ' - 'you to the SciPost initiative, and to invite you to become an active ' - 'Contributor to the site. You might for example consider reporting or ' - 'commenting on the above submission before the refereeing deadline.</p>') - email_context['sub_title'] = cls.invitation.cited_in_submission.title - email_context['sub_author_list'] = cls.invitation.cited_in_submission.author_list - email_context['arxiv_identifier_w_vn_nr'] = cls.invitation.cited_in_submission.arxiv_identifier_w_vn_nr - - email_text += summary_text - email_text_html += summary_text_html - email_text_html += '<br/>' + EMAIL_FOOTER - html_template = Template(email_text_html) - html_version = html_template.render(Context(email_context)) - emailmessage = EmailMultiAlternatives( - 'SciPost: invitation', email_text, - 'SciPost registration <registration@scipost.org>', - [cls.invitation.email], - cc=[cls.invitation.invited_by.user.email], - bcc=['registration@scipost.org'], - reply_to=['registration@scipost.org']) - emailmessage.attach_alternative(html_version, 'text/html') - - elif cls.invitation.invitation_type == 'cp': - # Has been cited in a Publication. Invite! - email_text += ( - 'Your work has been cited in a paper published by SciPost,' - '\n\n' + cls.invitation.cited_in_publication.title - + '\nby ' + cls.invitation.cited_in_publication.author_list + - '\n\n(published as ' + cls.invitation.cited_in_publication.citation() - + ').\n\n' - 'I would hereby like to use this opportunity to quickly introduce ' - 'you to the SciPost initiative, and to invite you to become an active ' - 'Contributor to the site.') - 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>' - '\n<p>I would hereby like to use this opportunity to quickly introduce ' - 'you to the SciPost initiative, and to invite you to become an active ' - 'Contributor to the site.</p>') - email_context['pub_title'] = cls.invitation.cited_in_publication.title - email_context['pub_author_list'] = cls.invitation.cited_in_publication.author_list - email_context['doi_label'] = cls.invitation.cited_in_publication.doi_label - email_context['citation'] = cls.invitation.cited_in_publication.citation() - email_text += summary_text - email_text_html += summary_text_html - email_text_html += '<br/>' + EMAIL_FOOTER - html_template = Template(email_text_html) - html_version = html_template.render(Context(email_context)) - emailmessage = EmailMultiAlternatives( - 'SciPost: invitation', email_text, - 'SciPost registration <registration@scipost.org>', - [cls.invitation.email], - cc=[cls.invitation.invited_by.user.email], - bcc=['registration@scipost.org'], - reply_to=['registration@scipost.org']) - emailmessage.attach_alternative(html_version, 'text/html') - - elif cls.invitation.invitation_type == 'C': - email_text += ('I would hereby like to quickly introduce ' - 'you to a scientific publishing initiative ' - 'called SciPost, and to invite you to become an active Contributor.') - email_text += summary_text - email_text_html += ( - '<p>I would hereby like to quickly introduce ' - 'you to a scientific publishing initiative ' - 'called SciPost, and to invite you to become an active Contributor.</p>') - email_text_html += summary_text_html + '<br/>' + EMAIL_FOOTER - html_template = Template(email_text_html) - html_version = html_template.render(Context(email_context)) - emailmessage = EmailMultiAlternatives( - 'SciPost: invitation', email_text, - 'SciPost registration <registration@scipost.org>', - [cls.invitation.email], - cc=[cls.invitation.invited_by.user.email], - bcc=['registration@scipost.org'], - reply_to=['registration@scipost.org']) - emailmessage.attach_alternative(html_version, 'text/html') - - elif cls.invitation.invitation_type == 'F': - email_text += ( - '\nYou will perhaps have already heard about SciPost, a publication ' - 'portal established by and for professional scientists.\n' - '\nSciPost.org is legally based on a not-for-profit foundation and will ' - 'operate in perpetuity as a non-commercial entity at the exclusive service ' - 'of the academic sector, bringing a cost-slashing alternative to existing ' - 'practices.\n' - '\nSciPost offers a collection of two-way open ' - 'access (no subscription fees, no author fees) journals with extremely ' - 'stringent (peer-witnessed) refereeing, overseen by ' - 'our Editorial College (exclusively composed ' - 'of established, professionally practising scientists). The whole process is ' - 'designed to ensure the highest achievable scientific quality while making the ' - 'editorial workflow as light and efficient as possible.\n' - '\nTo go straight to the point, on behalf of the foundation ' - 'and in view of your professional expertise, I hereby would ' - 'like to invite you to become an Editorial Fellow and thus join the ' - 'Editorial College of SciPost Physics.\n\n' - 'Please note that only well-known and respected senior academics are ' - 'being contacted for this purpose. Academic reputation and involvement ' - 'in the community are the most important criteria guiding our ' - 'considerations of who should belong to the Editorial College.\n' - '\nTo help you in considering this, it would be best if you were to take ' - 'the time to look at the website itself, which is anchored at scipost.org. ' - 'Besides looking around the side, you can also personally register ' - '(to become a Contributor, without necessarily committing to membership ' - 'of the Editorial College, this to be discussed separately) by visiting ' - 'the following single-use link, containing a partly pre-filled form for ' - 'your convenience: \n\n' - 'https://scipost.org/invitation/' + cls.invitation.invitation_key + '.\n' - '\nMany details about the initiative ' - 'can then be found at scipost.org/about and at scipost.org/FAQ. ' - 'Functioning of the College will proceed according to the by-laws set ' - 'out in scipost.org/EdCol_by-laws.\n\n' - 'Since the success of this initiative is dependent on the involvement of ' - 'the very people it is meant to serve, we\'d be very grateful if you were ' - 'to give due consideration to this proposal. We would expect you to ' - 'commit just 2-4 hours per month to help perform Editorial duties; we will ' - 'adjust the number of Editorial Fellows to ensure this is the case. You ' - 'could try it out for 6 months or a year, and of course you could quit ' - 'any time you wished.\n\n' - 'I\'d be happy to provide you with more information, should you require ' - 'it. In view of our development plans, I would be grateful if you could ' - 'react (by replying to this email) within the next two or three weeks, ' - 'if possible. I\'ll be looking forward to your reaction, your comments ' - 'and suggestions, be they positive or negative. If you need more time ' - 'to consider, that\'s also fine; just let me know.\n\n' - 'On behalf of the SciPost Foundation,\n\n' - 'Prof. dr Jean-Sébastien Caux\n---------------------------------------------' - '\nInstitute for Theoretial Physics\nUniversity of Amsterdam' - '\nScience Park 904\n1098 XH Amsterdam\nThe Netherlands' - '\n---------------------------------------------\n' - 'tel.: +31 (0)20 5255775\nfax: +31 (0)20 5255778' - '\n---------------------------------------------') - email_text_html += ( - '\n<p>You will perhaps have already heard about SciPost, a publication ' - 'portal established by and for professional scientists. ' - '\n<p>SciPost.org is legally based on a not-for-profit foundation and will ' - 'operate in perpetuity as a non-commercial entity at the exclusive service ' - 'of the academic sector, bringing a cost-slashing alternative to existing ' - 'practices.</p>' - '<p>SciPost offers a collection of two-way open ' - 'access (no subscription fees, no author fees) journals with extremely ' - 'stringent (peer-witnessed) refereeing, overseen by ' - 'our Editorial College (exclusively composed ' - 'of established, professionally practising scientists). The whole process is ' - 'designed to ensure the highest achievable scientific quality while making the ' - 'editorial workflow as light and efficient as possible.</p>' - '\n<p>To go straight to the point, on behalf of the SciPost Foundation ' - 'and in view of your professional expertise, I hereby would ' - 'like to invite you to become an Editorial Fellow and thus join the ' - 'Editorial College of SciPost Physics.</p>' - '\n<p>Please note that only well-known and respected senior academics are ' - 'being contacted for this purpose. Academic reputation and involvement ' - 'in the community are the most important criteria guiding our ' - 'considerations of who should belong to the Editorial College.</p>' - '\n<p>To help you in considering this, it would be best if you were to take ' - 'the time to look at the website itself, which is anchored at scipost.org. ' - 'Besides looking around the site, you can also personally register ' - '(to become a Contributor, without necessarily committing to membership ' - 'of the Editorial College, this to be discussed separately) by visiting ' - 'the following <a href="https://scipost.org/invitation/{{ invitation_key }}">' - 'single-use link</a>, containing a partly pre-filled form for ' - 'your convenience.</p>' - '\n<p>Many details about the initiative ' - 'can then be found at scipost.org/about and at scipost.org/FAQ. ' - 'Functioning of the College will proceed according to the by-laws set ' - 'out in scipost.org/EdCol_by-laws.</p>' - '\n<p>Since the success of this initiative is dependent on the involvement of ' - 'the very people it is meant to serve, we\'d be very grateful if you were ' - 'to give due consideration to this proposal. We would expect you to ' - 'commit just 2-4 hours per month to help perform Editorial duties; we will ' - 'constantly adjust the number of Editorial Fellows to ensure this is the case. You ' - 'could try it out for 6 months or a year, and of course you could quit ' - 'any time you wished.</p>' - '\n<p>I\'d be happy to provide you with more information, should you require ' - 'it. In view of our development plans, I would be grateful if you could ' - 'react (by replying to this email) within the next two or three weeks, ' - 'if possible. I\'ll be looking forward to your reaction, your comments ' - 'and suggestions, be they positive or negative. If you need more time ' - 'to consider, that\'s also fine; just let me know.</p>' - '<p>On behalf of the SciPost Foundation,</p>' - '<br/>Prof. dr Jean-Sébastien Caux' - '<br/>---------------------------------------------' - '<br/>Institute for Theoretial Physics' - '<br/>University of Amsterdam' - '<br/>Science Park 904<br/>1098 XH Amsterdam<br/>The Netherlands' - '<br/>---------------------------------------------' - '<br/>tel.: +31 (0)20 5255775\nfax: +31 (0)20 5255778' - '<br/>---------------------------------------------\n') - - email_text_html += '<br/>' + EMAIL_FOOTER - html_template = Template(email_text_html) - html_version = html_template.render(Context(email_context)) - emailmessage = EmailMultiAlternatives( - 'SciPost registration invitation', email_text, - 'J-S Caux <jscaux@scipost.org>', - [cls.invitation.email], - cc=[cls.invitation.invited_by.user.email], - bcc=['registration@scipost.org'], - reply_to=['registration@scipost.org']) - emailmessage.attach_alternative(html_version, 'text/html') - - # 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 = {} - email_text = ('Dear ' + cls.notification.contributor.get_title_display() + - ' ' + cls.notification.contributor.user.last_name) - email_text_html = 'Dear {{ title }} {{ last_name }}' - email_context['title'] = cls.notification.contributor.get_title_display() - email_context['last_name'] = cls.notification.contributor.user.last_name - email_text += ',\n\n' - email_text_html += ',<br/>' - if cls.notification.cited_in_publication: - url_unsubscribe = reverse('scipost:unsubscribe', - args=[cls.notification.contributor.id, - cls.notification.contributor.activation_key]) - email_text += ( - 'We would like to notify you that ' - '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\nWe hope you will find this paper of interest to your own research.' - '\n\nBest regards,\n\nThe SciPost Team' - '\n\nDon\'t want to receive such emails? Unsubscribe by visiting ' - + url_unsubscribe + '.') - email_text_html += ( - '<p>We would like to notify you that ' - 'your work has been cited in a paper published by SciPost,</p>' - '<p>{{ title }}</p><p>by {{ pub_author_list }}</p>' - '<p>(published as <a href="https://scipost.org/{{ doi_label }}">' - '{{ citation }}</a>).</p>' - '<p>We hope you will find this paper of interest to your own research.</p>' - '<p>Best regards,</p><p>The SciPost Team</p><br/>' - + EMAIL_FOOTER + '<br/>' - '\n<p style="font-size: 10px;">Don\'t want to receive such emails? ' - '<a href="%s">Unsubscribe</a>.</p>' % url_unsubscribe) - email_context['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() - email_context['key'] = cls.notification.contributor.activation_key - html_template = Template(email_text_html) - html_version = html_template.render(Context(email_context)) - elif cls.notification.cited_in_submission: - url_unsubscribe = reverse('scipost:unsubscribe', - args=[cls.notification.contributor.id, - cls.notification.contributor.activation_key]) - 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.\n\n' - 'Best regards,\n\nThe SciPost Team' - '\n\nDon\'t want to receive such emails? Unsubscribe by visiting ' - + url_unsubscribe + '.') - 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>' - '<p>Best regards,</p><p>The SciPost Team</p><br/>' - + EMAIL_FOOTER + '<br/>' - '\n<p style="font-size: 10px;">Don\'t want to receive such emails? ' - '<a href="%s">Unsubscribe</a>.</p>' % url_unsubscribe) - 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 - email_context['key'] = cls.notification.contributor.activation_key - - 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 b9d8656f910498ac50b5d7993d097c7f8b7eb00b..3097897d4be736bc225a97543b6cc026cebd921c 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -23,29 +23,27 @@ from django.views.debug import cleanse_setting from django.views.static import serve from guardian.decorators import permission_required -from guardian.shortcuts import assign_perm, get_objects_for_user from haystack.generic_views import SearchView from .constants import SCIPOST_SUBJECT_AREAS, subject_areas_raw_dict, SciPost_from_addresses_dict,\ CONTRIBUTOR_NORMAL from .decorators import has_contributor -from .models import Contributor, CitationNotification, UnavailabilityPeriod,\ - DraftInvitation, RegistrationInvitation,\ +from .models import Contributor, UnavailabilityPeriod,\ AuthorshipClaim, EditorialCollege, EditorialCollegeFellowship -from .forms import AuthenticationForm, DraftInvitationForm, UnavailabilityPeriodForm,\ - RegistrationForm, RegistrationInvitationForm, AuthorshipClaimForm,\ - ModifyPersonalMessageForm, SearchForm, VetRegistrationForm, reg_ref_dict,\ +from .forms import AuthenticationForm, UnavailabilityPeriodForm,\ + RegistrationForm, AuthorshipClaimForm,\ + SearchForm, VetRegistrationForm, reg_ref_dict,\ UpdatePersonalDataForm, UpdateUserDataForm, PasswordChangeForm,\ - EmailGroupMembersForm, EmailParticularForm, SendPrecookedEmailForm,\ - ContributorsFilterForm + EmailGroupMembersForm, EmailParticularForm, SendPrecookedEmailForm from .utils import Utils, EMAIL_FOOTER, SCIPOST_SUMMARY_FOOTER, SCIPOST_SUMMARY_FOOTER_HTML from affiliations.forms import AffiliationsFormset from colleges.permissions import fellowship_or_admin_required from commentaries.models import Commentary from comments.models import Comment +from invitations.constants import STATUS_REGISTERED +from invitations.models import RegistrationInvitation from journals.models import Publication, Journal, PublicationAuthorsTable -from mails.views import MailEditingSubView from news.models import NewsItem from submissions.models import Submission, RefereeInvitation,\ Report, EICRecommendation @@ -140,8 +138,8 @@ def register(request): Utils.send_registration_email() # Disable invitations related to the new Contributor - (RegistrationInvitation.objects.filter(email=form.cleaned_data['email']) - .update(responded=True)) + RegistrationInvitation.objects.declined_or_without_response().filter( + email=form.cleaned_data['email']).update(status=STATUS_REGISTERED) context = { 'ack_header': 'Thanks for registering to SciPost.', @@ -163,7 +161,7 @@ def invitation(request, key): the default registration form. """ invitation = get_object_or_404(RegistrationInvitation, invitation_key=key) - if invitation.responded: + if invitation.has_responded: errormessage = ('This invitation token has already been used, ' 'or this email address is already associated to a registration.') elif timezone.now() > invitation.key_expires: @@ -352,286 +350,6 @@ def registration_requests_reset(request, contributor_id): return redirect(reverse('scipost:registration_requests')) -@permission_required('scipost.can_draft_registration_invitations', return_403=True) -def draft_registration_invitation(request): - """ - For officers to prefill registration invitations. - This is similar to the registration_invitations method, - which is used to complete the invitation process. - """ - form = DraftInvitationForm(request.POST or None, current_user=request.user) - if form.is_valid(): - invitation = form.save(commit=False) - invitation.drafted_by = request.user.contributor - invitation.save() - - # Assign permission to 'drafter' to edit the draft afterwards - assign_perm('comments.change_draftinvitation', request.user, invitation) - messages.success(request, 'Draft invitation saved.') - return redirect(reverse('scipost:draft_registration_invitation')) - - existing_drafts = DraftInvitation.objects.filter(processed=False).order_by('last_name') - - context = { - 'form': form, - 'existing_drafts': existing_drafts, - } - return render(request, 'scipost/draft_registration_invitation.html', context) - - -@permission_required('scipost.can_draft_registration_invitations', return_403=True) -def contributors_filter(request): - """ - For Invitation Officers that use lists of scientists as a to-do. This - view returns all entries of those lists with users that are certainly not registered - or invitated. - """ - names_found = names_not_found = invitations_found = None - form = ContributorsFilterForm(request.POST or None) - if form.is_valid(): - names_found, names_not_found, invitations_found = form.filter() - - context = { - 'form': form, - 'names_found': names_found, - 'names_not_found': names_not_found, - 'invitations_found': invitations_found, - } - return render(request, 'scipost/contributors_filter.html', context) - - -@login_required -def edit_draft_reg_inv(request, draft_id): - """ - Edit DraftInvitation instance. It's only possible to edit istances created by the User itself. - """ - draft = get_object_or_404((get_objects_for_user(request.user, 'scipost.change_draftinvitation') - .filter(processed=False)), - id=draft_id) - - draft_inv_form = DraftInvitationForm(request.POST or None, current_user=request.user, - instance=draft) - if draft_inv_form.is_valid(): - draft = draft_inv_form.save() - messages.success(request, 'Draft invitation saved.') - return redirect(reverse('scipost:registration_invitations')) - - context = {'draft_inv_form': draft_inv_form} - 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) - 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_invite_Fellows', return_403=True) -def registration_invitations(request, draft_id=None): - """ Overview and tools for administrators """ - # List invitations sent; send new ones - associated_contributors = None - initial = {} - if draft_id: - # Fill draft data if draft_id given - 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, - 'email': draft.email, - 'invitation_type': draft.invitation_type, - 'cited_in_submission': draft.cited_in_submission, - 'cited_in_publication': draft.cited_in_publication, - } - - # Send invitation from form information - reg_inv_form = RegistrationInvitationForm(request.POST or None, initial=initial, - current_user=request.user) - if reg_inv_form.is_valid(): - invitation = reg_inv_form.save(commit=False) - invitation.invited_by = request.user.contributor - invitation.save() - invitation.refresh_keys() - - Utils.load({'invitation': invitation}) - Utils.send_registration_invitation_email() - (DraftInvitation.objects.filter(email=reg_inv_form.cleaned_data['email']) - .update(processed=True)) - - messages.success(request, 'Registration Invitation sent') - return redirect(reverse('scipost:registration_invitations')) - - sent_reg_inv = RegistrationInvitation.objects.filter(responded=False, declined=False) - sent_reg_inv_fellows = sent_reg_inv.filter(invitation_type='F').order_by('last_name') - sent_reg_inv_contrib = sent_reg_inv.filter(invitation_type='C').order_by('last_name') - sent_reg_inv_ref = sent_reg_inv.filter(invitation_type='R').order_by('last_name') - sent_reg_inv_cited_sub = sent_reg_inv.filter(invitation_type='ci').order_by('last_name') - sent_reg_inv_cited_pub = sent_reg_inv.filter(invitation_type='cp').order_by('last_name') - - resp_reg_inv = RegistrationInvitation.objects.filter(responded=True, declined=False) - resp_reg_inv_fellows = resp_reg_inv.filter(invitation_type='F').order_by('last_name') - resp_reg_inv_contrib = resp_reg_inv.filter(invitation_type='C').order_by('last_name') - resp_reg_inv_ref = resp_reg_inv.filter(invitation_type='R').order_by('last_name') - resp_reg_inv_cited_sub = resp_reg_inv.filter(invitation_type='ci').order_by('last_name') - resp_reg_inv_cited_pub = resp_reg_inv.filter(invitation_type='cp').order_by('last_name') - - decl_reg_inv = RegistrationInvitation.objects.filter(responded=True, declined=True) - - names_reg_contributors = Contributor.objects.filter( - status=1).order_by('user__last_name').values_list( - 'user__first_name', 'user__last_name') - existing_drafts = DraftInvitation.objects.filter(processed=False).order_by('last_name') - - context = { - 'reg_inv_form': reg_inv_form, - 'sent_reg_inv_fellows': sent_reg_inv_fellows, - 'sent_reg_inv_contrib': sent_reg_inv_contrib, - 'sent_reg_inv_ref': sent_reg_inv_ref, - 'sent_reg_inv_cited_sub': sent_reg_inv_cited_sub, - 'sent_reg_inv_cited_pub': sent_reg_inv_cited_pub, - 'resp_reg_inv_fellows': resp_reg_inv_fellows, - 'resp_reg_inv_contrib': resp_reg_inv_contrib, - 'resp_reg_inv_ref': resp_reg_inv_ref, - 'resp_reg_inv_cited_sub': resp_reg_inv_cited_sub, - 'resp_reg_inv_cited_pub': resp_reg_inv_cited_pub, - '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) - - -@permission_required('scipost.can_manage_registration_invitations', return_403=True) -def registration_invitations_cleanup(request): - """ - Compares the email addresses of invitations with those in the - database of registered Contributors. Flags overlaps. - """ - contributor_email_list = Contributor.objects.values_list('user__email', flat=True) - invs_to_cleanup = RegistrationInvitation.objects.filter( - responded=False, email__in=contributor_email_list) - context = {'invs_to_cleanup': invs_to_cleanup} - return render(request, 'scipost/registration_invitations_cleanup.html', context) - - -@permission_required('scipost.can_manage_registration_invitations', return_403=True) -def remove_registration_invitation(request, invitation_id): - """ - Remove an invitation (called from registration_invitations_cleanup). - """ - invitation = get_object_or_404(RegistrationInvitation, pk=invitation_id) - invitation.delete() - return redirect(reverse('scipost:registration_invitations_cleanup')) - - -@permission_required('scipost.can_manage_registration_invitations', return_403=True) -def edit_invitation_personal_message(request, invitation_id): - """ - - DOES THIS THING STILL WORK? OR CAN IT BE REMOVED? - - -- JdW (August 14th, 2017) - - """ - invitation = get_object_or_404(RegistrationInvitation, pk=invitation_id) - errormessage = None - if request.method == 'POST': - form = ModifyPersonalMessageForm(request.POST) - if form.is_valid(): - invitation.personal_message = form.cleaned_data['personal_message'] - invitation.save() - return redirect(reverse('scipost:registration_invitations')) - else: - errormessage = 'The form was invalid.' - else: - form = ModifyPersonalMessageForm( - initial={'personal_message': invitation.personal_message, }) - context = {'invitation': invitation, - 'form': form, 'errormessage': errormessage, } - return render(request, 'scipost/edit_invitation_personal_message.html', context) - - -@permission_required('scipost.can_invite_Fellows', return_403=True) -def renew_registration_invitation(request, invitation_id): - """ - Renew an invitation (called from registration_invitations). - """ - invitation = get_object_or_404(RegistrationInvitation, pk=invitation_id) - - # Utils.load({'invitation': invitation}) - # Utils.send_registration_invitation_email(True) - mail_request = MailEditingSubView(request, mail_code='registration_invitation_renewal', - invitation=invitation) - if mail_request.is_valid(): - invitation.nr_reminders += 1 - invitation.date_last_reminded = timezone.now() - invitation.save() - invitation.refresh_keys() - messages.success(request, 'Registration invitation has been sent.') - mail_request.send() - return redirect('scipost:registration_invitations') - else: - return mail_request.return_render() - - -@permission_required('scipost.can_manage_registration_invitations', return_403=True) -def mark_reg_inv_as_declined(request, invitation_id): - """ - Mark an invitation as declined (called from registration_invitations.html). - """ - invitation = get_object_or_404(RegistrationInvitation, pk=invitation_id) - invitation.responded = True - invitation.declined = True - invitation.save() - 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() - if notification.contributor.accepts_SciPost_emails: - 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): """ This view shows and processes a user's login session. diff --git a/submissions/models.py b/submissions/models.py index 99b5b94ce18dec2828e43db2d9f45e4ae93668c9..61d1f8555517f1afd33a60a1a6dd7b0a5c1f589b 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -336,11 +336,11 @@ class RefereeInvitation(SubmissionRelatedObjectMixin, models.Model): referee = models.ForeignKey('scipost.Contributor', related_name='referee_invitations', blank=True, null=True, on_delete=models.CASCADE) title = models.CharField(max_length=4, choices=TITLE_CHOICES) - first_name = models.CharField(max_length=30, default='') - last_name = models.CharField(max_length=30, default='') + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) email_address = models.EmailField() # if Contributor not found, person is invited to register - invitation_key = models.CharField(max_length=40, default='') + invitation_key = models.CharField(max_length=40) date_invited = models.DateTimeField(default=timezone.now) invited_by = models.ForeignKey('scipost.Contributor', related_name='referee_invited_by', blank=True, null=True, on_delete=models.CASCADE) diff --git a/submissions/templatetags/lookup.py b/submissions/templatetags/lookup.py index c2f7e549124208067d554ffd7ce304d5f8758ae2..5f1f0e24ff0f9a393da923fd739a879d7524af18 100644 --- a/submissions/templatetags/lookup.py +++ b/submissions/templatetags/lookup.py @@ -27,4 +27,4 @@ class SubmissionLookup(LookupChannel): Right now only used for draft registration invitations. May be extended in the future for other purposes as well. """ - return request.user.has_perm('can_draft_registration_invitations') + return request.user.has_perm('can_create_registration_invitations') diff --git a/submissions/views.py b/submissions/views.py index 813cb8e31aff54214618e413461e2e0ebc71ae5b..3e38c390df9dbd678cb6a5da184c86304a5ded8f 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -39,10 +39,12 @@ from colleges.permissions import fellowship_required, fellowship_or_admin_requir from mails.views import MailEditingSubView from scipost.forms import ModifyPersonalMessageForm, RemarkForm from scipost.mixins import PaginationMixin -from scipost.models import Contributor, Remark, RegistrationInvitation -from scipost.utils import Utils +from scipost.models import Contributor, Remark from comments.forms import CommentForm +from invitations.constants import INVITATION_REFEREEING +from invitations.models import RegistrationInvitation +from mails.utils import DirectMailUtil from production.forms import ProofsDecisionForm from production.models import ProductionStream @@ -765,44 +767,44 @@ def recruit_referee(request, arxiv_identifier_w_vn_nr): ref_recruit_form = RefereeRecruitmentForm(request.POST) if ref_recruit_form.is_valid(): # TODO check if email already taken - ref_invitation = RefereeInvitation( - submission=submission, - title=ref_recruit_form.cleaned_data['title'], - first_name=ref_recruit_form.cleaned_data['first_name'], - last_name=ref_recruit_form.cleaned_data['last_name'], - email_address=ref_recruit_form.cleaned_data['email_address'], - date_invited=timezone.now(), - invited_by=request.user.contributor) - ref_invitation.save() + ref_invitation = ref_recruit_form.save(commit=False) + ref_invitation.submission = submission + ref_invitation.invited_by = request.user.contributor + # Create and send a registration invitation - ref_inv_message_head = ('On behalf of the Editor-in-charge ' + - submission.editor_in_charge.get_title_display() + ' ' + - submission.editor_in_charge.user.last_name + - ', we would like to invite you to referee a Submission to ' + - submission.get_submitted_to_journal_display() + - ', namely\n\n' + submission.title + - '\nby ' + submission.author_list + - '\n (see https://scipost.org/submission/' - + submission.arxiv_identifier_w_vn_nr + ').') + ref_inv_message_head = ( + 'On behalf of the Editor-in-charge {eic_title} {eic_last_name}, we would' + 'like to invite you to referee a Submission to {journal}, namely' + '\n{sub_title}' + '\nby {sub_author_list}' + '\n(see https://scipost.org/{sub_url}).' + ).format( + eic_title=submission.editor_in_charge.get_title_display(), + eic_last_name=submission.editor_in_charge.user.last_name, + journal=submission.get_submitted_to_journal_display(), + sub_title=submission.title, + sub_author_list=submission.author_list, + sub_url=submission.get_absolute_url()) reg_invitation = RegistrationInvitation( title=ref_recruit_form.cleaned_data['title'], first_name=ref_recruit_form.cleaned_data['first_name'], last_name=ref_recruit_form.cleaned_data['last_name'], email=ref_recruit_form.cleaned_data['email_address'], - invitation_type='R', - invited_by=request.user.contributor, - message_style='F', - personal_message=ref_inv_message_head, - ) + invitation_type=INVITATION_REFEREEING, + created_by=request.user.contributor.user, + invited_by=request.user.contributor.user, + personal_message=ref_inv_message_head) + reg_invitation.save() - Utils.load({'invitation': reg_invitation}) - Utils.send_registration_invitation_email() + # Copy the key to the refereeing invitation + ref_invitation.invitation_key = reg_invitation.invitation_key + ref_invitation.save() + mail_sender = DirectMailUtil(mail_code='registration_invitation', + instance=reg_invitation) + mail_sender.send() submission.add_event_for_author('A referee has been invited.') submission.add_event_for_eic('%s has been recruited and invited as a referee.' % ref_recruit_form.cleaned_data['last_name']) - # Copy the key to the refereeing invitation: - ref_invitation.invitation_key = reg_invitation.invitation_key - ref_invitation.save() return redirect(reverse('submissions:editorial_page', kwargs={'arxiv_identifier_w_vn_nr': arxiv_identifier_w_vn_nr}))