diff --git a/mails/mixins.py b/mails/mixins.py index 60b1c16cdcbf61d8c1b496af6cc1432c4208badc..d712b58004a4951052bad0bffcbf7e5351ff5cef 100644 --- a/mails/mixins.py +++ b/mails/mixins.py @@ -16,38 +16,55 @@ from scipost.models import Contributor class MailUtilsMixin: - """ - This mixin takes care of inserting the default data into the Utils or Form. - """ - object = None - mail_fields = {} + """This mixin takes care of inserting the default data into the Utils or Form.""" + + instance = None + mail_data = {} mail_template = '' html_message = '' message = '' original_recipient = '' def __init__(self, *args, **kwargs): + """Init an instance for a specific mail_code. + + Arguments: + -- mail_code (str) + -- subject (str) + -- to (str): Email address or relation on the `instance`. Separated by comma. + -- bcc_to (str, optional): Email address or relation on the `instance`. Separated by comma. + -- instance: Instance of central object in email. + -- from (str, optional): Plain email address. + -- from_name (str, optional): Display name for from address. + """ self.pre_validation(*args, **kwargs) super().__init__(*args) def pre_validation(self, *args, **kwargs): - """ - This method should be called when initiating the object. - """ + """Validate the incoming data to initiate a specific mail.""" self.mail_code = kwargs.pop('mail_code') self.instance = kwargs.pop('instance', None) + kwargs['object'] = self.instance # Similar template nomenclature as Django. + self.mail_data = { + 'subject': kwargs.pop('subject', ''), + 'to_address': kwargs.pop('to', ''), + 'bcc_to': kwargs.pop('bcc', ''), + 'from_address_name': kwargs.pop('from_name', 'SciPost'), + 'from_address': kwargs.pop('from', 'no-reply@scipost.org'), + } # Gather meta data - json_location = '%s/templates/email/%s.json' % (settings.BASE_DIR, - self.mail_code) + json_location = '%s/templates/email/%s.json' % (settings.BASE_DIR, self.mail_code) + try: - self.mail_data = json.loads(open(json_location).read()) + self.mail_data.update(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)) + if not self.mail_data['subject']: + 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) + # Save central object/instance if not already + self.instance = self.get_object(**kwargs) # Digest the templates mail_template = loader.get_template('email/%s.html' % self.mail_code) @@ -63,18 +80,25 @@ class MailUtilsMixin: self.subject = self.mail_data['subject'] + def get_object(self, **kwargs): + if self.instance: + return self.instance + + if self.mail_data.get('context_object'): + return kwargs.get(self.mail_data['context_object'], None) + def _validate_single_entry(self, entry): """ entry -- raw email string or path or properties leading to email mail field Returns a list of email addresses found. """ - if entry and self.object: + if entry and self.instance: if re.match("[^@]+@[^@]+\.[^@]+", entry): # Email string return [entry] else: - mail_to = self.object + mail_to = self.instance for attr in entry.split('.'): try: mail_to = getattr(mail_to, attr) @@ -135,8 +159,7 @@ class MailUtilsMixin: self.message = handler.handle(self.html_message) def validate(self): - """ - Ease workflow by called this wrapper validation method. + """Execute different validation methods. Only to be used when the default data is used, eg. not in the EmailTemplateForm. """ @@ -146,42 +169,38 @@ class MailUtilsMixin: self.save_mail_data() def save_mail_data(self): - self.mail_fields = { + """Save mail validated mail data; update default values of mail data.""" + self.mail_data.update({ 'subject': self.subject, 'message': self.message, 'html_message': self.html_message, 'recipients': self.recipients, 'bcc_list': self.bcc_list, - } + }) def set_alternative_sender(self, from_name, from_address): - """ + """TODO: REMOVE; DEPRECATED + Set an alternative from address/name from the default values received from the json config file. The arguments only take raw string data, no methods/properties! """ self.mail_data['from_address_name'] = from_name self.mail_data['from_address'] = from_address - 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 + """Send the mail assuming `mail_data` is validated and complete.""" 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') + self.mail_data['subject'], + self.mail_data['message'], + '%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']]) + + # Send html version if available + if 'html_message' in self.mail_data: + email.attach_alternative(self.mail_data['html_message'], 'text/html') + email.send(fail_silently=False) - if self.object and hasattr(self.object, 'mail_sent'): - self.object.mail_sent() + if self.instance and hasattr(self.instance, 'mail_sent'): + self.instance.mail_sent() diff --git a/mails/utils.py b/mails/utils.py index da6cfb7bd58fce44fe65ea094a56614f4ba46660..8eb019df4dad59996a94c3a0efabccff6fbccafa 100644 --- a/mails/utils.py +++ b/mails/utils.py @@ -11,8 +11,8 @@ class DirectMailUtil(MailUtilsMixin): the mails out, without intercepting and showing the mail editor to the user. """ - def __init__(self, mail_code, instance, *args, **kwargs): + def __init__(self, mail_code, *args, **kwargs): kwargs['mail_code'] = mail_code - kwargs['instance'] = instance + kwargs['instance'] = kwargs.pop('instance', None) super().__init__(*args, **kwargs) self.validate() diff --git a/mails/views.py b/mails/views.py index 36578a74a7baa349e13f3a40723c0ff63ae61cc4..1388542885fa73421ffd925c12c121ab43aeadce 100644 --- a/mails/views.py +++ b/mails/views.py @@ -20,7 +20,7 @@ class MailEditingSubView(object): @property def recipients_string(self): - return ', '.join(getattr(self.mail_form, 'mail_fields', {}).get('recipients', [''])) + return ', '.join(getattr(self.mail_form, 'mail_data', {}).get('recipients', [''])) def add_form(self, form): self.context['transfer_data_form'] = HiddenDataForm(form) @@ -79,11 +79,10 @@ class MailEditorMixin: 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 = EmailTemplateForm(request.POST or None, mail_code=self.mail_code, - instance=self.object) + instance=self.instance) if self.mail_form.is_valid(): return self.form_valid(form) diff --git a/scipost/managers.py b/scipost/managers.py index df5a60d44e538eeee8c77149556af1af3f07822e..c97758480b8ad7788aeec08fb39efd19e2879986 100644 --- a/scipost/managers.py +++ b/scipost/managers.py @@ -22,22 +22,29 @@ class FellowManager(models.Manager): class ContributorQuerySet(models.QuerySet): + """Custom defined filters for the Contributor model.""" + def active(self): + """Return all validated and vetted Contributors.""" return self.filter(user__is_active=True, status=NORMAL_CONTRIBUTOR) def available(self): + """Filter out the Contributors that have active unavailability periods.""" return self.exclude( unavailability_periods__start__lte=today, unavailability_periods__end__gte=today) def awaiting_validation(self): + """Filter Contributors that have not been validated by the user.""" return self.filter(user__is_active=False, status=NEWLY_REGISTERED) def awaiting_vetting(self): + """Filter Contributors that have not been vetted through.""" return self.filter(user__is_active=True, status=NEWLY_REGISTERED) def fellows(self): - return self.filter(user__groups__name='Editorial College') + """TODO: NEEDS UPDATE TO NEW FELLOWSHIP RELATIONS.""" + return self.filter(fellowships__isnull=False).distinct() class UnavailabilityPeriodManager(models.Manager): diff --git a/submissions/management/commands/email_fellows_tasklist.py b/submissions/management/commands/email_fellows_tasklist.py index 7fb1beabf27785818255f71caac61098076fcfe0..6d083e89038c66eb61650430efa7c270b6bfdfa6 100644 --- a/submissions/management/commands/email_fellows_tasklist.py +++ b/submissions/management/commands/email_fellows_tasklist.py @@ -4,30 +4,34 @@ __license__ = "AGPL v3" from django.core.management import BaseCommand -from ...models import Submission, EditorialAssignment -from ...utils import SubmissionUtils +from ...models import EICRecommendation +from mails.utils import DirectMailUtil from scipost.models import Contributor class Command(BaseCommand): + """Send out mail to Fellows letting them know about their open tasks.""" + help = 'Sends an email to Fellows with current and upcoming tasks list' + def handle(self, *args, **kwargs): - fellows = Contributor.objects.fellows( -# ).filter(user__last_name__istartswith='C' # temporary limitation, to ease testing - ).order_by('user__last_name') + fellows = Contributor.objects.fellows() + count = 0 for fellow in fellows: + recs_to_vote_on = EICRecommendation.objects.user_must_vote_on(fellow.user) assignments_ongoing = fellow.editorial_assignments.ongoing() assignments_to_consider = fellow.editorial_assignments.open() assignments_upcoming_deadline = assignments_ongoing.refereeing_deadline_within(days=7) - if assignments_ongoing or assignments_to_consider or assignments_upcoming_deadline: - SubmissionUtils.load( - { - 'fellow': fellow, - 'assignments_ongoing': assignments_ongoing, - 'assignments_to_consider': assignments_to_consider, - 'assignments_upcoming_deadline': assignments_upcoming_deadline, - } - ) - SubmissionUtils.email_Fellow_tasklist() + if recs_to_vote_on or assignments_ongoing or assignments_to_consider or assignments_upcoming_deadline: + mail_sender = DirectMailUtil( + mail_code='fellows/email_fellow_tasklist', + fellow=fellow, + recs_to_vote_on=recs_to_vote_on, + assignments_ongoing=assignments_ongoing, + assignments_to_consider=assignments_to_consider, + assignments_upcoming_deadline=assignments_upcoming_deadline) + mail_sender.send() + count += 1 + self.stdout.write(self.style.SUCCESS('Emailed {} fellows.'.format(count))) diff --git a/submissions/managers.py b/submissions/managers.py index 8e8a5640e2872414031c59727f6efa528b5e8f32..63680e8b83c61e39933c0cbddabc421aa5ab48c2 100644 --- a/submissions/managers.py +++ b/submissions/managers.py @@ -249,8 +249,8 @@ class EditorialAssignmentQuerySet(models.QuerySet): class EICRecommendationQuerySet(models.QuerySet): """QuerySet for the EICRecommendation model.""" - def user_may_vote_on(self, user): - """Return the subset of EICRecommendation the User is eligable to vote on.""" + def user_must_vote_on(self, user): + """Return the subset of EICRecommendation the User is requested to vote on.""" if not hasattr(user, 'contributor'): return self.none() diff --git a/submissions/utils.py b/submissions/utils.py index c2842e88f4c6fc740690d71d0f9d315cd93a8b65..3f5fbbb7645d8a5a64be543b5dc4d1d5b47dc8c4 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -1245,15 +1245,3 @@ class SubmissionUtils(BaseMailUtil): reply_to=['admin@scipost.org']) emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) - - @classmethod - def email_Fellow_tasklist(cls): - """ - Email list of current and upcoming tasks to an individual Fellow. - - Requires context to contain: - - `fellow` - """ - cls._send_mail(cls, 'email_fellow_tasklist', - [cls._context['fellow'].user.email], - 'current assignments, pending tasks') diff --git a/submissions/views.py b/submissions/views.py index e82c4eb874a18b84ab4138f5795a23d1a5d9db1c..1be73cabc7f1b4df55a5a896e717fddadccb3a88 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -401,7 +401,7 @@ def pool(request, arxiv_identifier_w_vn_nr=None): # Mainly as fallback for the old-pool while in test phase. submissions = Submission.objects.pool(request.user) - recs_to_vote_on = EICRecommendation.objects.user_may_vote_on(request.user).filter( + recs_to_vote_on = EICRecommendation.objects.user_must_vote_on(request.user).filter( submission__in=submissions) assignments_to_consider = EditorialAssignment.objects.open().filter( to=request.user.contributor) @@ -1487,7 +1487,7 @@ def vote_on_rec(request, rec_id): """Form view for Fellows to cast their vote on EICRecommendation.""" submissions = Submission.objects.pool_editable(request.user) recommendation = get_object_or_404( - EICRecommendation.objects.user_may_vote_on( + EICRecommendation.objects.user_must_vote_on( request.user).filter(submission__in=submissions), id=rec_id) form = RecommendationVoteForm(request.POST or None) diff --git a/templates/email/email_fellow_tasklist.html b/templates/email/email_fellow_tasklist.html deleted file mode 100644 index 946b6d92f136c8f5c8bbe6d5c91b26f81869652f..0000000000000000000000000000000000000000 --- a/templates/email/email_fellow_tasklist.html +++ /dev/null @@ -1,65 +0,0 @@ -{% load bootstrap %} -{% load submissions_extras %} -<p>Dear {{ fellow.get_title_display }} {{ fellow.user.last_name }},</p> -<p>Please find below a summary of your current assignments, with (if applicable) pending and upcoming required actions. Many thanks in advance for your timely intervention on any point in need of attention. Your good work as an Editorial Fellow is greatly appreciated!</p> -{% if assignments_to_consider %} -<br/> -<h3>Assignments for you to consider:</h3> -<ul> -{% for assignment in assignments_to_consider %} -<li>On submission: {{ assignment.submission }}<br> - <a href="https://scipost.org{% url 'submissions:assignment_request' assignment.id %}">Accept or decline here</a> -</li> -{% endfor %} -</ul> -{% endif %} -{% if assignments_ongoing %} -<br/> -<h3>Current assignments (Submissions for which you are Editor-in-charge):</h3> -<ul> - {% for assignment in assignments_ongoing %} - <li> - <h3><a href="https://scipost.org{% url 'submissions:submission' assignment.submission.arxiv_identifier_w_vn_nr %}">{{ assignment.submission.title }}</a></h3> - <p> - <em>by {{ assignment.submission.author_list }}</em> - </p> - {% if assignment.submission.cycle.has_required_actions %} - <h3>Required actions (go to the <a href="https://scipost.org{% url 'submissions:editorial_page' assignment.submission.arxiv_identifier_w_vn_nr %}">Editorial page</a> to carry them out):</h3> - <ul> - {% for action in assignment.submission.cycle.get_required_actions %} - <li>{{action.1}}</li> - {% empty %} - <li>No action required. Great job!</li> - {% endfor %} - </ul> - {% endif %} - </li> - {% endfor %} -</ul> -{% endif %} -{% if assignments_upcoming_deadline %} -<br/> -<h3>Upcoming refereeing deadlines:</h3> -<ul> - {% for assignment in assignments_upcoming_deadline %} - <li> - <h3><a href="https://scipost.org{% url 'submissions:pool' assignment.submission.arxiv_identifier_w_vn_nr %}">{{ assignment.submission.title }}</a></h3> - <p> - <em>by {{ assignment.submission.author_list }}</em> - </p> - <p>Refereeing deadline: {{ assignment.submission.reporting_deadline|date:"Y-m-d" }}.</p> - <p><em>You can manage this Submission from its </em><a href="https://scipost.org{% url 'submissions:editorial_page' assignment.submission.arxiv_identifier_w_vn_nr %}">Editorial page</a>.</p> - </li> - {% endfor %} -</ul> -{% endif %} -<br/> -<h3>Need help or assistance?</h3> -<p> - Don't hesitate to <a href="mailto:edadmin@scipost.org">email the editorial administration</a> if you need any assistance. -</p> -<p> - Many thanks for your valuable work,<br> - SciPost Editorial Administration -</p> -{% include 'email/_footer.html' %} diff --git a/templates/email/email_fellow_tasklist.txt b/templates/email/email_fellow_tasklist.txt deleted file mode 100644 index b10f476783fd66800e6e626120e3dd7726e27d1a..0000000000000000000000000000000000000000 --- a/templates/email/email_fellow_tasklist.txt +++ /dev/null @@ -1,47 +0,0 @@ -{% load submissions_extras %} -Dear {{ fellow.get_title_display }} {{ fellow.user.last_name }}, - -Please find below a digest of your current assignments, with (if applicable) pending and upcoming required actions. Many thanks in advance for your timely intervention on any point in need of attention. Your good work as an Editorial Fellow is greatly appreciated! - -{% if assignments_to_consider %} -Assignments for you to consider: - -{% for assignment in assignments_to_consider %} -On submission: {{ assignment.submission }} -Accept or decline at https://www.scipost.org{% url 'submissions:assignment_request' assignment.id %} -{% endfor %} -{% endif %} -{% if assignments_ongoing %} -Current assignments (Submissions for which you are Editor-in-charge): - -{% for assignment in assignments_ongoing %} -{{ assignment.submission.title }} -by {{ assignment.submission.author_list }} - -{% if assignment.submission.cycle.has_required_actions %} -Required actions: -{% for action in assignment.submission.cycle.get_required_actions %} -* {{action.1}} -{% empty %} -No action required. Great job! -{% endfor %} -{% endif %} -{% endfor %} -{% endif %} - - -{% if assignments_upcoming_deadline %} -Upcoming refereeing deadlines: - -{% for assignment in assignments_upcoming_deadline %} -{{ assignment.submission.title }} -by {{ assignment.submission.author_list }} - -Refereeing deadline: {{ assignment.submission.reporting_deadline|date:"Y-m-d" }}. -{% endfor %} -{% endif %} - -You can take action on all of these starting from your personal page at https://scipost.org/personal_page. Don't hesitate to email the editorial administration at edadmin@scipost.org if you need any assistance. - -Many thanks for your valuable work, -SciPost Editorial Administration diff --git a/templates/email/fellows/email_fellow_tasklist.html b/templates/email/fellows/email_fellow_tasklist.html new file mode 100644 index 0000000000000000000000000000000000000000..17f54bbf5a1e3adfb150eaccfe64ee1abbf7117f --- /dev/null +++ b/templates/email/fellows/email_fellow_tasklist.html @@ -0,0 +1,80 @@ +<p>Dear {{ fellow.get_title_display }} {{ fellow.user.last_name }},</p> + +<p>Please find below a summary of your current assignments, with (if applicable) pending and upcoming required actions. Many thanks in advance for your timely intervention on any point in need of attention. Your good work as an Editorial Fellow is greatly appreciated!</p> + +{% if recs_to_vote_on %} + <br/> + <h3>Recommendations for you to vote on</h3> + <p>Please go to the <a href="https://scipost.org{% url 'submissions:pool' %}">pool</a> to cast your vote on:</p> + <ul> + {% for rec in recs_to_vote_on %} + <li>{{ rec.submission.title }}</li> + {% endfor %} + </ul> +{% endif %} + +{% if assignments_to_consider %} + <br/> + <h3>Assignments for you to consider:</h3> + <ul> + {% for assignment in assignments_to_consider %} + <li> + On submission: {{ assignment.submission }}<br> + <a href="https://scipost.org{% url 'submissions:assignment_request' assignment.id %}">Accept or decline here</a> + </li> + {% endfor %} + </ul> +{% endif %} + +{% if assignments_ongoing %} + <br/> + <h3>Current assignments (Submissions for which you are Editor-in-charge):</h3> + <ul> + {% for assignment in assignments_ongoing %} + <li> + <h3><a href="https://scipost.org{% url 'submissions:submission' assignment.submission.arxiv_identifier_w_vn_nr %}">{{ assignment.submission.title }}</a></h3> + <p> + <em>by {{ assignment.submission.author_list }}</em> + </p> + {% if assignment.submission.cycle.has_required_actions %} + <h3>Required actions (go to the <a href="https://scipost.org{% url 'submissions:editorial_page' assignment.submission.arxiv_identifier_w_vn_nr %}">Editorial page</a> to carry them out):</h3> + <ul> + {% for action in assignment.submission.cycle.get_required_actions %} + <li>{{action.1}}</li> + {% empty %} + <li>No action required. Great job!</li> + {% endfor %} + </ul> + {% endif %} + </li> + {% endfor %} + </ul> +{% endif %} + +{% if assignments_upcoming_deadline %} + <br/> + <h3>Upcoming refereeing deadlines:</h3> + <ul> + {% for assignment in assignments_upcoming_deadline %} + <li> + <h3><a href="https://scipost.org{% url 'submissions:pool' assignment.submission.arxiv_identifier_w_vn_nr %}">{{ assignment.submission.title }}</a></h3> + <p> + <em>by {{ assignment.submission.author_list }}</em> + </p> + <p>Refereeing deadline: {{ assignment.submission.reporting_deadline|date:"Y-m-d" }}.</p> + <p><em>You can manage this Submission from its </em><a href="https://scipost.org{% url 'submissions:editorial_page' assignment.submission.arxiv_identifier_w_vn_nr %}">Editorial page</a>.</p> + </li> + {% endfor %} + </ul> +{% endif %} + +<br/> +<h3>Need help or assistance?</h3> +<p> + Don't hesitate to <a href="mailto:edadmin@scipost.org">email the editorial administration</a> if you need any assistance. +</p> +<p> + Many thanks for your valuable work,<br> + SciPost Editorial Administration +</p> +{% include 'email/_footer.html' %} diff --git a/templates/email/fellows/email_fellow_tasklist.json b/templates/email/fellows/email_fellow_tasklist.json new file mode 100644 index 0000000000000000000000000000000000000000..3f935b97cb52580d3f0c523a4ece1c94d775fe86 --- /dev/null +++ b/templates/email/fellows/email_fellow_tasklist.json @@ -0,0 +1,9 @@ +{ + "subject": "SciPost: current assignments, pending tasks", + "to_address_bup": "fellow.user.email", + "to_address": "J.S.Caux@uva.nl", + "bcc_to": "edadmin@scipost.org", + "from_address_name": "SciPost EdAdmin", + "from_address": "edadmin@scipost.org", + "context_object": "fellow" +}