diff --git a/comments/models.py b/comments/models.py index 482df7a1e2e550b4b4dd91b3ed1feaa48d41fe3d..be9a3678b2a50eabb989c354259d5e1a2eca6a56 100644 --- a/comments/models.py +++ b/comments/models.py @@ -114,6 +114,7 @@ class Comment(TimeStampedModel): to_object = self.content_object while True: + # Loop because of possible nested relations if (isinstance(to_object, Submission) or isinstance(to_object, Commentary) or isinstance(to_object, ThesisLink)): return to_object diff --git a/comments/views.py b/comments/views.py index ed8bbb8e6d3b4e84e2a491cdf00e616a55b631f2..b79ee9d9d3496a24ad1dd98bb0cefdcf3893e42d 100644 --- a/comments/views.py +++ b/comments/views.py @@ -1,7 +1,7 @@ __copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" - +import time from django.contrib.auth.decorators import permission_required, login_required from django.contrib import messages from django.core.urlresolvers import reverse @@ -47,6 +47,18 @@ def new_comment(request, **kwargs): new_comment.save() new_comment.grant_permissions() + # Mails + mail_sender = DirectMailUtil( + mail_code='commenters/inform_commenter_comment_received', + instance=new_comment) + mail_sender.send() + + if isinstance(new_comment.core_content_object, Submission): + mail_sender = DirectMailUtil( + mail_code='eic/inform_eic_comment_received', + instance=new_comment) + mail_sender.send() + messages.success(request, strings.acknowledge_submit_comment) return redirect(_object.get_absolute_url()) context = {'form': form} @@ -168,6 +180,7 @@ def vet_submitted_comment(request, comment_id): @permission_required('scipost.can_submit_comments', raise_exception=True) +@transaction.atomic def reply_to_comment(request, comment_id): comment = get_object_or_404(Comment, pk=comment_id) @@ -184,7 +197,6 @@ def reply_to_comment(request, comment_id): else: # No idea what this could be, but just to be sure is_author = related_object.author == request.user.contributor - form = CommentForm(request.POST or None, request.FILES or None) if form.is_valid(): newcomment = form.save(commit=False) @@ -194,6 +206,19 @@ def reply_to_comment(request, comment_id): newcomment.save() newcomment.grant_permissions() + mail_sender = DirectMailUtil( + mail_code='commenters/inform_commenter_comment_received', + instance=newcomment, + delayed_processing=True) + mail_sender.send() + + if isinstance(newcomment.core_content_object, Submission): + mail_sender = DirectMailUtil( + mail_code='eic/inform_eic_comment_received', + instance=newcomment, + delayed_processing=True) + mail_sender.send() + messages.success(request, '<h3>Thank you for contributing a Reply</h3>' 'It will soon be vetted by an Editor.') return redirect(newcomment.content_object.get_absolute_url()) @@ -218,6 +243,18 @@ def reply_to_report(request, report_id): newcomment.save() newcomment.grant_permissions() + mail_sender = DirectMailUtil( + mail_code='eic/inform_eic_comment_received', + instance=newcomment, + delayed_processing=True) + mail_sender.send() + + mail_sender = DirectMailUtil( + mail_code='commenters/inform_commenter_comment_received', + instance=newcomment, + delayed_processing=True) + mail_sender.send() + messages.success(request, '<h3>Thank you for contributing a Reply</h3>' 'It will soon be vetted by an Editor.') return redirect(newcomment.content_object.get_absolute_url()) diff --git a/mails/admin.py b/mails/admin.py index 6967486fda3158c00f55d1233d4b8b1d568be0a8..7fa9f68ca1ae63a3c6fe1de6fba0c9b1be615140 100644 --- a/mails/admin.py +++ b/mails/admin.py @@ -8,7 +8,8 @@ from .models import MailLog class MailLogAdmin(admin.ModelAdmin): - list_display = ['__str__', 'to_recipients', 'created', 'processed'] + list_display = ['__str__', 'to_recipients', 'created', 'status'] + list_filter = ['status'] readonly_fields = ('created', 'latest_activity') diff --git a/mails/backends/filebased.py b/mails/backends/filebased.py index 1c0e26d9f14c23b5e337bcaface786a3933f714d..71688950a3c5ad2eefc6748415643ce13c39c5b7 100644 --- a/mails/backends/filebased.py +++ b/mails/backends/filebased.py @@ -49,11 +49,23 @@ class ModelEmailBackend(FileBacked): except AttributeError: pass + content_object = None + mail_code = '' + if 'delayed_processing' in email_message.extra_headers and email_message.extra_headers: + status = 'not_rendered' + content_object = email_message.extra_headers.get('content_object', None) + mail_code = email_message.extra_headers.get('mail_code', '') + else: + status = 'rendered' + MailLog.objects.create( body=body, subject=subject, body_html=body_html, to_recipients=to_recipients, bcc_recipients=bcc_recipients, - from_email=from_email) + from_email=from_email, + status=status, + content_object=content_object, + mail_code=mail_code) return True diff --git a/mails/management/commands/send_mails.py b/mails/management/commands/send_mails.py index c684889d1b0d83caff363ed2422b52963fcb0b9a..a810d2fcd96d88ce9ee67246875f352c8acdb801 100644 --- a/mails/management/commands/send_mails.py +++ b/mails/management/commands/send_mails.py @@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand from django.conf import settings from ...models import MailLog - +from ...utils import DirectMailUtil class Command(BaseCommand): """ @@ -13,6 +13,19 @@ class Command(BaseCommand): '--id', type=int, required=False, help='The id in the `MailLog` table for a specific mail, Leave blank to send all') + def _process_mail(self, mail): + """ + Render the templates for the mail if not done yet. + """ + mail_util = DirectMailUtil( + mail_code=mail.mail_code, + instance=mail.content_object) # This will process the mail, but: not send yet! + + MailLog.objects.filter(id=mail.id).update( + body=mail_util.mail_data['message'], + body_html=mail_util.mail_data['html_message'], + status='rendered') + def send_mails(self, mails): from django.core.mail import get_connection, EmailMultiAlternatives @@ -28,6 +41,10 @@ class Command(BaseCommand): connection = get_connection(backend=backend, fail_silently=False) count = 0 for db_mail in mails: + if db_mail.status == 'not_rendered': + self._process_mail(db_mail) + db_mail.refresh_from_db() + mail = EmailMultiAlternatives( db_mail.subject, db_mail.body, @@ -42,6 +59,7 @@ class Command(BaseCommand): if response: count += 1 db_mail.processed = True + db_mail.status = 'sent' db_mail.save() return count @@ -49,6 +67,6 @@ class Command(BaseCommand): if options.get('id'): mails = MailLog.objects.filter(id=options['id']) else: - mails = MailLog.objects.unprocessed() + mails = MailLog.objects.not_sent() nr_mails = self.send_mails(mails) self.stdout.write('Sent {} mails.'.format(nr_mails)) diff --git a/mails/managers.py b/mails/managers.py index 1be95a3768d1353c9452e5dec6b7df91d3e6aa5a..782b3ebdefe6f72d467558879ad6d3e59c89939a 100644 --- a/mails/managers.py +++ b/mails/managers.py @@ -2,5 +2,11 @@ from django.db import models class MailLogQuerySet(models.QuerySet): - def unprocessed(self): - return self.filter(processed=False) + def not_sent(self): + return self.filter(status__in=['not_rendered', 'rendered']) + + def unrendered(self): + return self.filter(status='not_rendered') + + def rendered(self): + return self.filter(status='rendered') diff --git a/mails/migrations/0004_auto_20181217_1050.py b/mails/migrations/0004_auto_20181217_1050.py new file mode 100644 index 0000000000000000000000000000000000000000..8c0c98cc78df395af5b042a57457b36714e551cf --- /dev/null +++ b/mails/migrations/0004_auto_20181217_1050.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-12-17 09:50 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('mails', '0003_auto_20180502_1807'), + ] + + operations = [ + migrations.AddField( + model_name='maillog', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='maillog', + name='mail_code', + field=models.CharField(blank=True, max_length=254), + ), + migrations.AddField( + model_name='maillog', + name='object_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='maillog', + name='status', + field=models.CharField(choices=[('not_rendered', 'Not rendered'), ('rendered', 'Rendered'), ('sent', 'Sent')], default='rendered', max_length=16), + ), + ] diff --git a/mails/migrations/0005_auto_20181217_1051.py b/mails/migrations/0005_auto_20181217_1051.py new file mode 100644 index 0000000000000000000000000000000000000000..e1f8697828702b72183a411f43733e16bba0907b --- /dev/null +++ b/mails/migrations/0005_auto_20181217_1051.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-12-17 09:51 +from __future__ import unicode_literals + +from django.db import migrations + + +def set_statuses(apps, schema_editor): + MailLog = apps.get_model('mails', 'MailLog') + MailLog.objects.filter(processed=True).update(status='sent') + + +class Migration(migrations.Migration): + + dependencies = [ + ('mails', '0004_auto_20181217_1050'), + ] + + operations = [ + migrations.RunPython(set_statuses, reverse_code=migrations.RunPython.noop), + ] diff --git a/mails/mixins.py b/mails/mixins.py index 1dbb645792a5083b3a085362e3fdc456fde8d2e3..00a40447701e0dd7e3247f6eadf22092f9f64fef 100644 --- a/mails/mixins.py +++ b/mails/mixins.py @@ -2,6 +2,7 @@ __copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" +import time import re import json import inspect @@ -25,6 +26,7 @@ class MailUtilsMixin: message = '' original_recipient = '' mail_sent = False + delayed_processing = False def __init__(self, *args, **kwargs): """Init an instance for a specific mail_code. @@ -68,10 +70,11 @@ class MailUtilsMixin: self.instance = self.get_object(**kwargs) # Digest the templates - mail_template = loader.get_template('email/%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) + if not self.delayed_processing: + mail_template = loader.get_template('email/%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) # Damn slow. # Gather Recipients data try: @@ -200,7 +203,12 @@ class MailUtilsMixin: '%s <%s>' % (self.mail_data['from_address_name'], self.mail_data['from_address']), self.mail_data['recipients'], bcc=self.mail_data['bcc_list'], - reply_to=[self.mail_data['from_address']]) + reply_to=[self.mail_data['from_address']], + headers={ + 'delayed_processing': self.delayed_processing, + 'content_object': self.get_object(), + 'mail_code': self.mail_code, + }) # Send html version if available if 'html_message' in self.mail_data: diff --git a/mails/models.py b/mails/models.py index e77a920cb305247cf47a914e1deb278d65bc24d6..88d210eeca6dfc20e78688c37f2ae5c3c1e972b9 100644 --- a/mails/models.py +++ b/mails/models.py @@ -3,10 +3,20 @@ __license__ = "AGPL v3" from django.db import models +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import ArrayField from .managers import MailLogQuerySet +MAIL_NOT_RENDERED, MAIL_RENDERED = 'not_rendered', 'rendered' +MAIL_SENT = 'sent' +MAIL_STATUSES = ( + (MAIL_NOT_RENDERED, 'Not rendered'), + (MAIL_RENDERED, 'Rendered'), + (MAIL_SENT, 'Sent'), +) + class MailLog(models.Model): """ @@ -15,6 +25,12 @@ class MailLog(models.Model): the chosen MailBackend. """ processed = models.BooleanField(default=False) + status = models.CharField(max_length=16, choices=MAIL_STATUSES, default=MAIL_RENDERED) + + mail_code = models.CharField(max_length=254, blank=True) + content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField(blank=True, null=True) + content_object = GenericForeignKey('content_type', 'object_id') body = models.TextField() body_html = models.TextField(blank=True) diff --git a/mails/utils.py b/mails/utils.py index 8eb019df4dad59996a94c3a0efabccff6fbccafa..9ac090d6e5a46ebd472bfed436dfcfaa1d2fe450 100644 --- a/mails/utils.py +++ b/mails/utils.py @@ -14,5 +14,6 @@ class DirectMailUtil(MailUtilsMixin): def __init__(self, mail_code, *args, **kwargs): kwargs['mail_code'] = mail_code kwargs['instance'] = kwargs.pop('instance', None) + self.delayed_processing = kwargs.pop('delayed_processing', False) super().__init__(*args, **kwargs) self.validate() diff --git a/scipost/static/scipost/assets/config/preconfig.scss b/scipost/static/scipost/assets/config/preconfig.scss index f088db22de2bb3dab0775cb03e44aa960cf0568f..fe1ed4f29bfc5ca7b9791214f14779b59037b86b 100644 --- a/scipost/static/scipost/assets/config/preconfig.scss +++ b/scipost/static/scipost/assets/config/preconfig.scss @@ -5,9 +5,9 @@ // General variable structure // // -$base-border-radius: 0; -$border-radius: 0; -$border-radius-lg: 2px; +$border-radius-base: 2px; +$border-radius-small: 1px; +$border-radius-large: 2px; // Alert // @@ -55,11 +55,11 @@ $body-color: $scipost-darkblue; // Alerts // -$alert-border-radius: $base-border-radius; +$alert-border-radius: $border-radius-base; // Cards // -$card-border-radius: $base-border-radius; +$card-border-radius: $border-radius-base; $card-spacer-x: 0.75rem; $card-spacer-y: 0.5rem; $card-shadow-color: #ccc; @@ -76,7 +76,7 @@ $breadcrumb-divider-color: $scipost-orange; // Dropdown // -$dropdown-border-radius: $base-border-radius; +$dropdown-border-radius: $border-radius-base; $dropdown-border-color: $scipost-darkblue; $dropdown-item-padding-y: 0.35rem; $dropdown-item-padding-x: 1.0rem; @@ -89,11 +89,10 @@ $input-btn-padding-y: 0.25rem; $input-btn-padding-x-lg: 0.75rem; $input-btn-padding-y-lg: 0.325rem; $input-btn-line-height-lg: 1.4; -$input-border-radius-sm: $base-border-radius; -$input-border-radius: $base-border-radius; -$input-border-radius-lg: $base-border-radius; +$input-border-radius-sm: $border-radius-base; +$input-border-radius: $border-radius-base; +$input-border-radius-lg: $border-radius-base; $enable-rounded: true !important; -$btn-transition: none; $input-height: calc(1.5rem + 2px); $input-height-lg: calc(2.0rem + 2px); @@ -148,10 +147,20 @@ $navbar-padding-y: 0.2rem; $nav-tabs-border-color: $scipost-darkblue; $nav-tabs-link-active-border-color: $scipost-darkblue $scipost-darkblue $nav-tabs-link-active-bg; -$input-border-radius: 0; -$btn-border-radius: $base-border-radius; -$btn-border-radius-sm: $base-border-radius; -$btn-border-radius-lg: $base-border-radius; +$input-border-radius: $border-radius-base; +$input-border-radius-large: $border-radius-large; +$input-border-radius-small: $border-radius-small; + + +// Allows for customizing button radius independently from global border radius +$btn-border-radius: $border-radius-base; +$btn-border-radius-lg: $border-radius-large; +$btn-border-radius-sm: $border-radius-small; + +$btn-line-height-sm: 1.2; +$btn-line-height-sm: 1.2; + +$btn-transition: none; // Block quote diff --git a/scipost/static/scipost/assets/css/_form.scss b/scipost/static/scipost/assets/css/_form.scss index 5776122ffdd1a469f4ad315dc8cb5a94a6b535c3..c51ab07815a8be92e27189eb9d6bdedd747e2d60 100644 --- a/scipost/static/scipost/assets/css/_form.scss +++ b/scipost/static/scipost/assets/css/_form.scss @@ -4,7 +4,7 @@ */ .form-control { font-family: inherit; - border-radius: $base-border-radius; + border-radius: $border-radius-base; } .has-error .form-control { diff --git a/scipost/static/scipost/assets/css/_labels.scss b/scipost/static/scipost/assets/css/_labels.scss index 5d5a4108092e561bb08cda51e68d5d7665db30bd..afafcdc1de6c1b460fad5db918e6f2b6a6ac210a 100644 --- a/scipost/static/scipost/assets/css/_labels.scss +++ b/scipost/static/scipost/assets/css/_labels.scss @@ -5,7 +5,9 @@ $label-padding-x: 0.6rem !default; $label-padding-y: 0.25rem !default; -$label-line-height: 1.2; +$label-line-height: 1.0; +$label-line-height-sm: 0.9; +$label-line-height-lg: 1.2; $label-font-weight: $font-weight-normal !default; $label-box-shadow: none; $label-font-size: inherit; @@ -38,8 +40,8 @@ $label-danger-color: $white; $label-danger-bg: $red !default; $label-danger-border: $red !default; -$label-padding-x-sm: 0.25rem; -$label-padding-y-sm: 0.15rem; +$label-padding-x-sm: $label-padding-x; +$label-padding-y-sm: $label-padding-y; $label-padding-x-lg: 0.75rem !default; $label-padding-y-lg: 0.5rem !default; @@ -57,7 +59,7 @@ $label-transition: all .2s ease-in-out !default; .label { display: inline-block; font-weight: $label-font-weight; - line-height: $label-line-height !important; + line-height: $label-line-height; text-align: center; white-space: nowrap; vertical-align: middle; @@ -68,7 +70,7 @@ $label-transition: all .2s ease-in-out !default; border: $label-border-width solid transparent; box-shadow: $label-box-shadow; - @include button-size($label-padding-y, $label-padding-x, $label-font-size, $label-border-radius, $label-border-radius); + @include button-size($label-padding-y, $label-padding-x, $label-font-size, $label-line-height, $label-border-radius); @include transition($label-transition); } @@ -151,11 +153,11 @@ a.label-danger, .label-lg { // line-height: ensure even-numbered height of button next to large input - @include button-size($label-padding-y-lg, $label-padding-x-lg, $font-size-lg, $label-border-radius-lg, $btn-border-radius-lg); + @include button-size($label-padding-y-lg, $label-padding-x-lg, $font-size-lg, $label-line-height-lg, $label-border-radius-lg); } .label-sm { // line-height: ensure proper height of button next to small input - @include button-size($label-padding-y-sm, $label-padding-x-sm, $font-size-sm, $label-border-radius-sm, $btn-border-radius-sm); + @include button-size($label-padding-y-sm, $label-padding-x-sm, $font-size-sm, $label-line-height-sm, $label-border-radius-sm); } diff --git a/scipost/static/scipost/assets/css/_list_group.scss b/scipost/static/scipost/assets/css/_list_group.scss index 264f3b0cc35a92fc8ecc210a1b31a0c501241250..3eba4ebb4a87c24b1c427517cb1a7e39cfefce2f 100644 --- a/scipost/static/scipost/assets/css/_list_group.scss +++ b/scipost/static/scipost/assets/css/_list_group.scss @@ -18,9 +18,7 @@ ul.events-list { border: 0; .time { - max-width: 10rem; min-width: 10rem; - width: 10rem; } } } diff --git a/submissions/admin.py b/submissions/admin.py index 43a8cf5f2693715798f60c5fa9be8f2c55555fa7..4535d7767c20f104f586128b6a730318a496158b 100644 --- a/submissions/admin.py +++ b/submissions/admin.py @@ -36,6 +36,7 @@ class SubmissionAdminForm(forms.ModelForm): required=False, queryset=Contributor.objects.order_by('user__last_name')) is_resubmission_of = forms.ModelChoiceField( + required=False, queryset=Submission.objects.order_by('-preprint__identifier_w_vn_nr')) class Meta: diff --git a/submissions/forms.py b/submissions/forms.py index d1409ffe6239699749a970d5d2c2e31b68663d8c..8bba2618c2268d6fd4431662721308ae81a95faa 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -22,6 +22,7 @@ from .constants import ( STATUS_EIC_ASSIGNED, CYCLE_DEFAULT, CYCLE_DIRECT_REC, STATUS_PREASSIGNED, STATUS_REPLACED, STATUS_FAILED_PRESCREENING, STATUS_DEPRECATED, STATUS_ACCEPTED, STATUS_DECLINED) from . import exceptions, helpers +from .helpers import to_ascii_only from .models import ( Submission, RefereeInvitation, Report, EICRecommendation, EditorialAssignment, iThenticateReport, EditorialCommunication) @@ -455,7 +456,12 @@ class SubmissionForm(forms.ModelForm): Check if author list matches the Contributor submitting. """ author_list = self.cleaned_data['author_list'] - if not self.requested_by.last_name.lower() in author_list.lower(): + + # Remove punctuation and convert to ASCII-only string. + clean_author_name = to_ascii_only(self.requested_by.last_name) + clean_author_list = to_ascii_only(author_list) + + if not clean_author_name in clean_author_list: error_message = ('Your name does not match that of any of the authors. ' 'You are not authorized to submit this preprint.') self.add_error('author_list', error_message) @@ -1119,19 +1125,23 @@ class VetReportForm(forms.Form): def process_vetting(self, current_contributor): """Set the right report status and update submission fields if needed.""" - self.report.vetted_by = current_contributor + report = self.cleaned_data['report'] if self.cleaned_data['action_option'] == REPORT_ACTION_ACCEPT: # Accept the report as is - self.report.status = STATUS_VETTED - self.report.submission.latest_activity = timezone.now() - self.report.submission.save() + Report.objects.filter(id=report.id).update( + status=STATUS_VETTED, + vetted_by=current_contributor, + ) + report.submission.touch() elif self.cleaned_data['action_option'] == REPORT_ACTION_REFUSE: # The report is rejected - self.report.status = self.cleaned_data['refusal_reason'] + Report.objects.filter(id=report.id).update( + status=self.cleaned_data['refusal_reason'], + ) else: raise exceptions.InvalidReportVettingValue(self.cleaned_data['action_option']) - self.report.save() - return self.report + report.refresh_from_db() + return report ################### diff --git a/submissions/helpers.py b/submissions/helpers.py index f5f6c2eec90610caaee1abe4aa4ce9d3560ba11a..b9cac5c9b25be45f47de78637c9c06c48883b2a0 100644 --- a/submissions/helpers.py +++ b/submissions/helpers.py @@ -2,7 +2,9 @@ __copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" +import re import requests +import unicodedata from .exceptions import ArxivPDFNotFound @@ -43,3 +45,11 @@ def check_unverified_author(submission, user): return ( user.last_name in submission.author_list and not submission.authors_false_claims.filter(user=user).exists()) + + +def to_ascii_only(str): + """ + Convert string to lowercase, ASCII-only characters without punctuation and whitespaces. + """ + str = re.sub(r'[^\w]','', str).lower() + return unicodedata.normalize('NFKD', str).encode('ascii','ignore') diff --git a/submissions/models.py b/submissions/models.py index 426281dfa2868ac8cd1312331284abb9896a9fe6..6e91dcdb7c56bf73f06531b295abf6e0efbc3653 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -39,6 +39,7 @@ from scipost.behaviors import TimeStampedModel from scipost.constants import TITLE_CHOICES from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS from scipost.fields import ChoiceArrayField +from scipost.models import Contributor from scipost.storage import SecureFileStorage from journals.constants import SCIPOST_JOURNALS_DOMAINS from journals.models import Publication @@ -388,6 +389,21 @@ class Submission(models.Model): return self.editorial_assignments.filter(status=STATUS_PREASSIGNED).exists() + def has_inadequate_pool_composition(self): + """ + Check whether the EIC actually in the pool of the Submission. + + (Could happen on resubmission or reassignment after wrong Journal selection) + """ + if not self.editor_in_charge: + # None assigned yet. + return False + + pool_contributors_ids = Contributor.objects.filter( + fellowships__pool=self).values_list('id', flat=True) + return self.editor_in_charge.id not in pool_contributors_ids + + class SubmissionEvent(SubmissionRelatedObjectMixin, TimeStampedModel): """Private message directly related to a Submission. diff --git a/submissions/templates/partials/submissions/pool/required_actions_block.html b/submissions/templates/partials/submissions/pool/required_actions_block.html index 7b82edb3c133143bfd807bc3cdf5273d7c151b08..388cdf8eb97c84b015442f9e7beff716f0a39957 100644 --- a/submissions/templates/partials/submissions/pool/required_actions_block.html +++ b/submissions/templates/partials/submissions/pool/required_actions_block.html @@ -5,8 +5,8 @@ </div> <div class="card-body"> <ul class="mb-0"> - {% for action in submission.cycle.required_actions %} - <li>{{action.1}}</li> + {% for code, action in submission.cycle.required_actions.items %} + <li>{{ action }}</li> {% empty %} <li>No actions required</li> {% endfor %} diff --git a/submissions/templates/partials/submissions/pool/required_actions_tooltip.html b/submissions/templates/partials/submissions/pool/required_actions_tooltip.html index 4b42c758a933bfe49aefcc6b88a2efbfc293f5b5..0fd1c822b83ac0866e1bcc4e0f908b7048aa2303 100644 --- a/submissions/templates/partials/submissions/pool/required_actions_tooltip.html +++ b/submissions/templates/partials/submissions/pool/required_actions_tooltip.html @@ -2,8 +2,8 @@ <i class="fa fa-exclamation-circle {{ classes }}" data-toggle="tooltip" data-html="true" title=" Required Actions: <ul class='mb-0 pl-3 text-left'> - {% for action in submission.cycle.required_actions %} - <li>{{ action.1 }}</li> + {% for code, action in submission.cycle.required_actions.items %} + <li>{{ action }}</li> {% endfor %} </ul> "></i> diff --git a/submissions/templates/partials/submissions/pool/submission_li.html b/submissions/templates/partials/submissions/pool/submission_li.html index 708bfc31b328b65af23c9f6808eb7ea4a2b03952..e69c4a07014a94c6916c56ed9f51083bf9a2c0ef 100644 --- a/submissions/templates/partials/submissions/pool/submission_li.html +++ b/submissions/templates/partials/submissions/pool/submission_li.html @@ -1,4 +1,7 @@ {% load submissions_pool %} +{% load user_groups %} + +{% is_edcol_admin request.user as is_editorial_admin %} <div class="icons"> {% include 'partials/submissions/pool/submission_tooltip.html' with submission=submission %} @@ -53,7 +56,14 @@ <span class="label label-sm label-secondary">{{ submission.get_status_display }}</span> </div> </div> - + {% if is_editorial_admin and submission.has_inadequate_pool_composition %} + <div class="border border-danger text-danger mt-1 py-1 px-2"> + <strong> + <i class="fa fa-exclamation-triangle"></i> + Notice to admin: The current editor is not assigned to the pool. Therefore, the editor will not be able to reach the editorial page. + </strong> + </div> + {% endif %} {% if submission.cycle.has_required_actions %} <div class="card-text bg-danger text-white mt-1 py-1 px-2"> This Submission contains required actions, <a href="{% url 'submissions:pool' submission.preprint.identifier_w_vn_nr %}" class="text-white" data-toggle="dynamic" data-target="#container_{{ submission.id }}">click to see details.</a> {% include 'partials/submissions/pool/required_actions_tooltip.html' with submission=submission classes='text-white' %} diff --git a/submissions/templates/partials/submissions/pool/submission_reports_summary_table.html b/submissions/templates/partials/submissions/pool/submission_reports_summary_table.html index be733be294f8a265ac1360a58b639d33822bc3c4..76c0b2924b33edbc67da1168437e794b605c703a 100644 --- a/submissions/templates/partials/submissions/pool/submission_reports_summary_table.html +++ b/submissions/templates/partials/submissions/pool/submission_reports_summary_table.html @@ -12,13 +12,8 @@ <tbody> {% for report in submission.reports.all %} <tr{% if report.is_unvetted %} class="table-warning"{% endif %}> - <td class="text-center"> - {% if report.is_unvetted %} - <div class="text-center" data-toggle="tooltip" data-title="This Report has not yet been vetted." data-html="true"> - <i class="fa fa-info"></i> - <i class="fa fa-long-arrow-right" ></i> - </div> - {% endif %} + <td class="px-4"> + <strong>{{ report.report_nr }}</strong> </td> <td> {{ report.author }} diff --git a/submissions/templates/submissions/pool/editorial_page.html b/submissions/templates/submissions/pool/editorial_page.html index 8838426481a95ffc161e402f03de1f9736c63f6f..4a24c7fcb56decdccc777e5affceae81f2ab7fd2 100644 --- a/submissions/templates/submissions/pool/editorial_page.html +++ b/submissions/templates/submissions/pool/editorial_page.html @@ -312,6 +312,90 @@ {% endif %} {% else %} {% if full_access %} + <h3>All available actions</h3> + <ul class="mb-5"> + {% if submission.refereeing_cycle != 'direct_rec' %} + {% if submission.in_refereeing_phase %} + <li> + {% if submission.referee_invitations.all %} + <a href="{% url 'submissions:select_referee' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}">Invite an additional referee</a> + {% else %} + <a href="{% url 'submissions:select_referee' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}">Invite the first referee here</a> + {% endif %} + </li> + <li>Extend the refereeing deadline (currently {{ submission.reporting_deadline|date:'Y-m-d' }}{% if submission.reporting_deadline_has_passed %} <span class="ml-1 label label-sm label-outline-danger text-uppercase">The reporting deadline has passed</span>{% endif %}) by + <a href="{% url 'submissions:extend_refereeing_deadline' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr days=2 %}">2 days</a>, + <a href="{% url 'submissions:extend_refereeing_deadline' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr days=7 %}">1 week</a> or + <a href="{% url 'submissions:extend_refereeing_deadline' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr days=14 %}">2 weeks</a> + </li> + {% endif %} + <li> + Set refereeing deadline: + <form class="form-inline d-inline-block" action="{% url 'submissions:set_refereeing_deadline' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}" method="post"> + {% csrf_token %} + <div class="d-inline-block mx-2"> + {% for field in set_deadline_form.visible_fields %} + {{ field|add_css_class:'form-control' }} + {{ field }} + {% endfor %} + </div> + + <input class="btn btn-outline-secondary btn-sm" type="submit" value="Set deadline"/> + </form> + </li> + + {% if submission.is_open_for_reporting %} + <li><a href="{% url 'submissions:close_refereeing_round' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}">Close the refereeing round</a> (deactivates submission of new Reports and Comments)</li> + {% endif %} + {% endif %} + {% with submission.reports.awaiting_vetting as reports %} + {% if reports %} + <li> + Vet submitted Report{{reports|pluralize}}: + <ul class="mb-1"> + {% for report in reports %} + <li><a href="{% url 'submissions:vet_submitted_report' report.id %}">Report {{ report.report_nr }} by {{ report.author }} ({{ report.get_report_type_display }})</a></li> + {% endfor %} + </ul> + </li> + {% else %} + <li>All Reports have been vetted.</li> + {% endif %} + {% endwith %} + + {% with submission.comments_set_complete.awaiting_vetting as comments %} + {% if comments %} + <li> + Vet submitted Comment{{comments|pluralize}}: + <ul class="mb-1"> + {% for comment in comments %} + <li><a href="{% url 'comments:vet_submitted_comment' comment.id %}">{{comment}}</a></li> + {% endfor %} + </ul> + </li> + {% else %} + <li>All Comments have been vetted.</li> + {% endif %} + {% endwith %} + {% if submission.eic_recommendation_required %} + <li> + {% if submission.eicrecommendations.last %} + <a href="{% url 'submissions:reformulate_eic_recommendation' submission.preprint.identifier_w_vn_nr %}">Reformulate Editorial Recommendation</a> + {% else %} + <a href="{% url 'submissions:eic_recommendation' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr %}">Formulate an Editorial Recommendation.</a> + {% endif %} + <p> + If you recommend revisions, this will be communicated directly to the Authors, who will be asked to resubmit. + <br> + If you recommend acceptance or rejection, this will be put to the Editorial College for ratification. + </p> + </li> + {% elif submission.eicrecommendations.last %} + {% if submission.eicrecommendations.last.may_be_reformulated %} + <li><a href="{% url 'submissions:reformulate_eic_recommendation' submission.preprint.identifier_w_vn_nr %}">Reformulate Editorial Recommendation</a></li> + {% endif %} + {% endif %} + </ul> {% if submission.refereeing_cycle != 'direct_rec' %} <h3 class="mt-3" id="referee-details">Refereeing invitations</h3> diff --git a/submissions/utils.py b/submissions/utils.py index 8855acd84ab628be25cbe6ac1bc550a682f2c3bd..8513efc00e93057591eb028216e43ba7a3b0bece 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -876,20 +876,6 @@ class SubmissionUtils(BaseMailUtil): [cls._context['invitation'].referee.user.email], email_subject) - @classmethod - def email_EIC_report_delivered(cls): - """ Requires loading 'report' attribute. """ - cls._send_mail(cls, 'report_delivered_eic', - [cls._context['report'].submission.editor_in_charge.user.email], - 'Report delivered') - - @classmethod - def email_referee_report_delivered(cls): - """ Requires loading 'report' attribute. """ - cls._send_mail(cls, 'report_delivered_referee', - [cls._context['report'].author.user.email], - 'Report delivered') - @classmethod def acknowledge_report_email(cls): """ Requires loading 'report' attribute. """ diff --git a/submissions/views.py b/submissions/views.py index 65f82868b4005d024527aa5bed8a5125d7531f8d..8fe120f2a563eef608235f8ac1b930063233b8ce 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -1,6 +1,7 @@ __copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" +import time import datetime import feedparser import strings @@ -51,6 +52,7 @@ from common.utils import workdays_between from invitations.constants import STATUS_SENT from invitations.models import RegistrationInvitation from journals.models import Journal +from mails.utils import DirectMailUtil from mails.views import MailEditingSubView from ontology.models import Topic from ontology.forms import SelectTopicForm @@ -689,6 +691,12 @@ def editorial_assignment(request, identifier_w_vn_nr, assignment_id=None): if form.is_normal_cycle(): # Inform authors about new status. SubmissionUtils.send_author_prescreening_passed_email() + else: + # Inform authors about new status. + mail_sender = DirectMailUtil( + mail_code='authors/inform_authors_eic_assigned_direct_eic', + assignment=submission) + mail_sender.send() submission.add_general_event('The Editor-in-charge has been assigned.') notify_editor_assigned(request.user, assignment, False) @@ -1534,9 +1542,16 @@ def submit_report(request, identifier_w_vn_nr): 'identifier_w_vn_nr': identifier_w_vn_nr})) # Send mails if report is submitted - SubmissionUtils.load({'report': newreport}, request) - SubmissionUtils.email_EIC_report_delivered() - SubmissionUtils.email_referee_report_delivered() + mail_sender = DirectMailUtil( + mail_code='referees/inform_referee_report_received', + instance=newreport, + delayed_processing=True) + mail_sender.send() + mail_sender = DirectMailUtil( + mail_code='eic/inform_eic_report_received', + instance=newreport, + delayed_processing=True) + mail_sender.send() # Add SubmissionEvents for the EIC only, as it can also be rejected still submission.add_event_for_eic('%s has submitted a new Report.' @@ -1596,7 +1611,6 @@ def vet_submitted_report(request, report_id): # Add SubmissionEvent for the EIC report.submission.add_event_for_eic('The Report by %s is vetted.' % report.author.user.last_name) - if report.status == STATUS_VETTED: SubmissionUtils.send_author_report_received_email() diff --git a/templates/email/authors/inform_authors_eic_assigned_direct_rec.html b/templates/email/authors/inform_authors_eic_assigned_direct_rec.html new file mode 100644 index 0000000000000000000000000000000000000000..e2f489e94ae9dc7430be4b0c90f6f6134ebd57dc --- /dev/null +++ b/templates/email/authors/inform_authors_eic_assigned_direct_rec.html @@ -0,0 +1,19 @@ +<p> + Dear {{ submission.submitted_by.get_title_display }} {{ submission.submitted_by.user.last_name }}, +</p> +<p> + For your information, a Contributor Comment has been posted on a recent Report on your Submission + <br><br> + {{ submission.title }} + <br>by {{ submission.author_list }}<br> + (see https://scipost.org{{ submission.get_absolute_url }}. +</p> + +<p>has been assigned to an editor. The editor chose to directly formulate an Editorial Recommendation.</p> +<p>You will be informed shortly by email about the status of this Editorial Recommendation.</p> + +<p> + Sincerely, + <br> + The SciPost Team. +</p> diff --git a/templates/email/authors/inform_authors_eic_assigned_direct_rec.json b/templates/email/authors/inform_authors_eic_assigned_direct_rec.json new file mode 100644 index 0000000000000000000000000000000000000000..8d952bcb257fede2c6d20158fe25f078df5d1ed2 --- /dev/null +++ b/templates/email/authors/inform_authors_eic_assigned_direct_rec.json @@ -0,0 +1,8 @@ +{ + "subject": "SciPost: Editor assigned", + "to_address": "submitted_by.user.email", + "bcc_to": "edadmin@scipost.org", + "from_address_name": "SciPost Refereeing", + "from_address": "refereeing@scipost.org", + "context_object": "submission" +} diff --git a/templates/email/commenters/inform_commenter_comment_received.html b/templates/email/commenters/inform_commenter_comment_received.html new file mode 100644 index 0000000000000000000000000000000000000000..4f648aba05a3470f9bdda7871a4cbe8ece242a04 --- /dev/null +++ b/templates/email/commenters/inform_commenter_comment_received.html @@ -0,0 +1,32 @@ +<p>Dear {{comment.author.get_title_display}} {{comment.author.user.last_name}},</p> +<p> + We hereby confirm reception of your Comment, concerning + + <br/> + {{comment.core_content_object.title}} + {% if comment.core_content_object.author_list %} + <br> + by {{comment.core_content_object.author_list}}. + {% elif comment.core_content_object.author %} + <br> + by {{comment.core_content_object.author}}. + {% endif %} +</p> +<p> + We copy it below for your convenience. + <br> + Your Comment will soon be vetted, at which point you will receive an email update from us. +</p> +<p> + Thank you for your contribution,<br><br> + The SciPost Team. +</p> + +<br> +<p> + Comment: + <br> + {{ comment.comment_text|linebreaksbr }} +</p> + +{% include 'email/_footer.html' %} diff --git a/templates/email/commenters/inform_commenter_comment_received.json b/templates/email/commenters/inform_commenter_comment_received.json new file mode 100644 index 0000000000000000000000000000000000000000..d70d7953ed6e60d3f6c4dfb37383dba28fe481a9 --- /dev/null +++ b/templates/email/commenters/inform_commenter_comment_received.json @@ -0,0 +1,8 @@ +{ + "subject": "SciPost: Comment received", + "to_address": "author.user.email", + "bcc_to": "edadmin@scipost.org", + "from_address_name": "SciPost Comments", + "from_address": "edadmin@scipost.org", + "context_object": "comment" +} diff --git a/templates/email/eic/inform_eic_comment_received.html b/templates/email/eic/inform_eic_comment_received.html new file mode 100644 index 0000000000000000000000000000000000000000..b4e2c780466369e3196e6478cdb40ee125bd6de4 --- /dev/null +++ b/templates/email/eic/inform_eic_comment_received.html @@ -0,0 +1,19 @@ +<p>Dear {{ comment.core_content_object.editor_in_charge.get_title_display }} {{ comment.core_content_object.editor_in_charge.user.last_name }},</p> + +<p> + {{ comment.author.get_title_display }} {{ comment.author.user.last_name }} has delivered a Comment for Submission: +</p> +<p> + {{ comment.core_content_object.title }} + <br/> + by {{ comment.core_content_object.author_list }}. +</p> +<p> + Please vet this Comment on <a href="https://scipost.org{% url 'submissions:editorial_page' comment.core_content_object.preprint.identifier_w_vn_nr %}">the editorial page</a>. +</p> +<p> + Many thanks in advance for your collaboration,<br> + The SciPost Team. +</p> + +{% include 'email/_footer.html' %} diff --git a/templates/email/eic/inform_eic_comment_received.json b/templates/email/eic/inform_eic_comment_received.json new file mode 100644 index 0000000000000000000000000000000000000000..90f695dc362f97df8a00f14d636d9f2de08681ec --- /dev/null +++ b/templates/email/eic/inform_eic_comment_received.json @@ -0,0 +1,8 @@ +{ + "subject": "SciPost: Comment delivered", + "to_address": "core_content_object.editor_in_charge.user.email", + "bcc_to": "edadmin@scipost.org", + "from_address_name": "SciPost Refereeing", + "from_address": "refereeing@scipost.org", + "context_object": "comment" +} diff --git a/templates/email/report_delivered_eic.html b/templates/email/eic/inform_eic_report_received.html similarity index 100% rename from templates/email/report_delivered_eic.html rename to templates/email/eic/inform_eic_report_received.html diff --git a/templates/email/eic/inform_eic_report_received.json b/templates/email/eic/inform_eic_report_received.json new file mode 100644 index 0000000000000000000000000000000000000000..a6ebfd554488bcca4b930d499b6b1124a7ab62fc --- /dev/null +++ b/templates/email/eic/inform_eic_report_received.json @@ -0,0 +1,8 @@ +{ + "subject": "SciPost: Report delivered", + "to_address": "submission.editor_in_charge.user.email", + "bcc_to": "edadmin@scipost.org", + "from_address_name": "SciPost Editorial Admin", + "from_address": "submissions@scipost.org", + "context_object": "report" +} diff --git a/templates/email/report_delivered_referee.html b/templates/email/referees/inform_referee_report_received.html similarity index 100% rename from templates/email/report_delivered_referee.html rename to templates/email/referees/inform_referee_report_received.html diff --git a/templates/email/referees/inform_referee_report_received.json b/templates/email/referees/inform_referee_report_received.json new file mode 100644 index 0000000000000000000000000000000000000000..300cb92e972cfa9438809bbff7211c26e8686440 --- /dev/null +++ b/templates/email/referees/inform_referee_report_received.json @@ -0,0 +1,8 @@ +{ + "subject": "SciPost: Report delivered", + "to_address": "author.user.email", + "bcc_to": "edadmin@scipost.org", + "from_address_name": "SciPost Editorial Admin", + "from_address": "submissions@scipost.org", + "context_object": "report" +} diff --git a/templates/email/report_delivered_eic.txt b/templates/email/report_delivered_eic.txt deleted file mode 100644 index dae336cd5874d192c31780ad5ddde5a4891839fa..0000000000000000000000000000000000000000 --- a/templates/email/report_delivered_eic.txt +++ /dev/null @@ -1,12 +0,0 @@ -Dear {{ report.submission.editor_in_charge.get_title_display }} {{ report.submission.editor_in_charge.user.last_name }}, - -Referee {{ report.author.get_title_display }} {{ report.author.user.last_name }} has delivered a Report for Submission - -{{ report.submission.title }} -by {{ report.submission.author_list }}. - -Please vet this Report via your personal page at -https://scipost.org{% url 'scipost:personal_page' %}, under the Editorial Actions tab. - -Many thanks in advance for your collaboration, -The SciPost Team. diff --git a/templates/email/report_delivered_referee.txt b/templates/email/report_delivered_referee.txt deleted file mode 100644 index 0f1ad9982bdd609c5a7b25feda58af55a8483714..0000000000000000000000000000000000000000 --- a/templates/email/report_delivered_referee.txt +++ /dev/null @@ -1,11 +0,0 @@ -Dear {{ report.author.get_title_display }} {{ report.author.user.last_name }}, - -We hereby confirm reception of your Report on Submission - -{{ report.submission.title }} -by {{ report.submission.author_list }}. - -We are immensely grateful for your time and effort. Your Report will soon be vetted by the Submission's Editor-in-charge, at which point you will receive an email update from us. - -Many thanks again, -The SciPost Team.