diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 4c1c1ed3b..9737e73be 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -13,6 +13,7 @@ from django.urls import reverse as urlreverse from django.utils.encoding import force_text import debug # pyflakes:ignore +from ietf.doc.templatetags.mail_filters import std_level_prompt from ietf.utils.mail import send_mail, send_mail_text from ietf.ipr.utils import iprs_from_docs, related_docs @@ -401,7 +402,7 @@ def generate_issue_ballot_mail(request, doc, ballot): last_call_expires = e.expires if e else None last_call_has_expired = last_call_expires and last_call_expires < datetime.datetime.now() - return render_to_string("doc/mail/issue_ballot_mail.txt", + return render_to_string("doc/mail/issue_iesg_ballot_mail.txt", dict(doc=doc, doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), last_call_expires=last_call_expires, @@ -413,6 +414,58 @@ def generate_issue_ballot_mail(request, doc, ballot): ) ) +def _send_irsg_ballot_email(request, doc, ballot, subject, template): + """Send email notification when IRSG ballot is issued""" + (to, cc) = gather_address_lists('irsg_ballot_issued', doc=doc) + sender = 'IESG Secretary ' + + ballot_expired = ballot.duedate < datetime.datetime.now() + active_ballot = doc.active_ballot() + if active_ballot is None: + needed_bps = '' + else: + needed_bps = needed_ballot_positions( + doc, + list(active_ballot.active_balloter_positions().values()) + ) + + return send_mail( + request=request, + frm=sender, + to=to, + cc=cc, + subject=subject, + extra={'Reply-To': [sender]}, + template=template, + context=dict( + doc=doc, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + ballot_duedate=ballot.duedate, + ballot_expired=ballot_expired, + needed_ballot_positions=needed_bps, + )) + + +def email_irsg_ballot_issued(request, doc, ballot): + """Send email notification when IRSG ballot is issued""" + return _send_irsg_ballot_email( + request, + doc, + ballot, + 'IRSG ballot issued: %s to %s'%(doc.file_tag(), std_level_prompt(doc)), + 'doc/mail/issue_irsg_ballot_mail.txt', + ) + +def email_irsg_ballot_closed(request, doc, ballot): + """Send email notification when IRSG ballot is closed""" + return _send_irsg_ballot_email( + request, + doc, + ballot, + 'IRSG ballot closed: %s to %s'%(doc.file_tag(), std_level_prompt(doc)), + "doc/mail/close_irsg_ballot_mail.txt", + ) + def email_iana(request, doc, to, msg, cc=None): # fix up message and send it with extra info on doc in headers import email diff --git a/ietf/doc/tests_irsg_ballot.py b/ietf/doc/tests_irsg_ballot.py index b9059fd75..294aa6563 100644 --- a/ietf/doc/tests_irsg_ballot.py +++ b/ietf/doc/tests_irsg_ballot.py @@ -334,10 +334,22 @@ class BaseManipulationTests(): self.assertEqual(r.status_code, 302) self.assertIsNone(draft.ballot_open('irsg-approve')) + # No notifications should have been generated yet + self.assertEqual(len(outbox), 0) + r = self.client.post(url,{'irsg_button':'Yes', 'duedate':due }) self.assertEqual(r.status_code,302) self.assertIsNotNone(draft.ballot_open('irsg-approve')) - self.assertEqual(len(outbox),0) + + # Should have sent a notification about the new ballot + self.assertEqual(len(outbox), 1) + msg = outbox[0] + self.assertIn('IRSG ballot issued', msg['Subject']) + self.assertIn('iesg-secretary@ietf.org', msg['From']) + # Notifications are also sent to various doc-related addresses, not tested here + self.assertIn('irsg@irtf.org', msg['To']) + self.assertIn('irtf-chair@irtf.org', msg['CC']) + self.assertIn(str(due), get_payload_text(msg)) # ensure duedate is included def test_take_and_email_position(self): draft = RgDraftFactory() @@ -379,11 +391,21 @@ class BaseManipulationTests(): self.assertEqual(r.status_code, 302) self.assertIsNotNone(draft.ballot_open('irsg-approve')) + # Should not have generated a notification yet + self.assertEqual(len(outbox), 0) + r = self.client.post(url,dict(irsg_button='Yes')) self.assertEqual(r.status_code, 302) self.assertIsNone(draft.ballot_open('irsg-approve')) - self.assertEqual(len(outbox), 0) + # Closing the ballot should have generated a notification + self.assertEqual(len(outbox), 1) + msg = outbox[0] + self.assertIn('IRSG ballot closed', msg['Subject']) + self.assertIn('iesg-secretary@ietf.org', msg['From']) + # Notifications are also sent to various doc-related addresses, not tested here + self.assertIn('irsg@irtf.org', msg['To']) + self.assertIn('irtf-chair@irtf.org', msg['CC']) def test_view_outstanding_ballots(self): draft = RgDraftFactory() diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py index 0adeae231..ce9552106 100644 --- a/ietf/doc/utils_charter.py +++ b/ietf/doc/utils_charter.py @@ -235,7 +235,7 @@ def default_review_text(group, charter, by): def generate_issue_ballot_mail(request, doc, ballot): - addrs=gather_address_lists('ballot_issued',doc=doc).as_strings() + addrs=gather_address_lists('iesg_ballot_issued',doc=doc).as_strings() return render_to_string("doc/charter/issue_ballot_mail.txt", dict(doc=doc, diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 02ef19b37..4962afc2b 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -26,7 +26,7 @@ from ietf.doc.utils import ( add_state_change_event, close_ballot, close_open_ba from ietf.doc.mails import ( email_ballot_deferred, email_ballot_undeferred, extra_automation_headers, generate_last_call_announcement, generate_issue_ballot_mail, generate_ballot_writeup, generate_ballot_rfceditornote, - generate_approval_mail ) + generate_approval_mail, email_irsg_ballot_closed, email_irsg_ballot_issued ) from ietf.doc.lastcall import request_last_call from ietf.iesg.models import TelechatDate from ietf.ietfauth.utils import has_role, role_required, is_authorized_in_doc_stream @@ -635,7 +635,7 @@ def ballot_writeupnotes(request, name): msg = generate_issue_ballot_mail(request, doc, ballot) - addrs = gather_address_lists('ballot_issued',doc=doc).as_strings() + addrs = gather_address_lists('iesg_ballot_issued',doc=doc).as_strings() override = {'To':addrs.to} if addrs.cc: override['CC'] = addrs.cc @@ -1095,6 +1095,8 @@ def issue_irsg_ballot(request, name): prev_tags = [] new_tags = [] + email_irsg_ballot_issued(request, doc, ballot=e) # Send notification email + if doc.type_id == 'draft': new_state = State.objects.get(used=True, type="draft-stream-irtf", slug='irsgpoll') @@ -1130,7 +1132,10 @@ def close_irsg_ballot(request, name): if request.method == 'POST': button = request.POST.get("irsg_button") if button == 'Yes': - close_ballot(doc, by, "irsg-approve") + ballot = close_ballot(doc, by, "irsg-approve") + email_irsg_ballot_closed(request, + doc=doc, + ballot=IRSGBallotDocEvent.objects.get(pk=ballot.pk)) return HttpResponseRedirect(doc.get_absolute_url()) diff --git a/ietf/doc/views_conflict_review.py b/ietf/doc/views_conflict_review.py index ae5bf104b..85a5a9a72 100644 --- a/ietf/doc/views_conflict_review.py +++ b/ietf/doc/views_conflict_review.py @@ -147,7 +147,7 @@ def send_conflict_eval_email(request,review): doc_url = settings.IDTRACKER_BASE_URL+review.get_absolute_url(), ) ) - addrs = gather_address_lists('ballot_issued',doc=review).as_strings() + addrs = gather_address_lists('iesg_ballot_issued',doc=review).as_strings() override = {'To':addrs.to} if addrs.cc: override['Cc']=addrs.cc diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index 6dc3f2843..eb1d93e12 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -129,7 +129,7 @@ def send_status_change_eval_email(request,doc): doc_url = settings.IDTRACKER_BASE_URL+doc.get_absolute_url(), ) ) - addrs = gather_address_lists('ballot_issued',doc=doc) + addrs = gather_address_lists('iesg_ballot_issued',doc=doc) override = {'To':addrs.to } if addrs.cc: override['Cc'] = addrs.cc diff --git a/ietf/mailtrigger/migrations/0016_add_irsg_ballot_issued.py b/ietf/mailtrigger/migrations/0016_add_irsg_ballot_issued.py new file mode 100644 index 000000000..0e3accb74 --- /dev/null +++ b/ietf/mailtrigger/migrations/0016_add_irsg_ballot_issued.py @@ -0,0 +1,57 @@ +# Copyright The IETF Trust 2019-2020, All Rights Reserved +# -*- coding: utf-8 -*- + + +from django.db import migrations + + +def replace_mailtrigger(MailTrigger, old_slug, new_slug): + """Replace a MailTrigger with an equivalent using a different slug""" + # Per 0013_add_irsg_ballot_saved.py, can't just modify the existing because that + # will lose the many-to-many relations. + orig_mailtrigger = MailTrigger.objects.get(slug=old_slug) + new_mailtrigger = MailTrigger.objects.create(slug=new_slug) + new_mailtrigger.to.set(orig_mailtrigger.to.all()) + new_mailtrigger.cc.set(orig_mailtrigger.cc.all()) + new_mailtrigger.desc = orig_mailtrigger.desc + new_mailtrigger.save() + orig_mailtrigger.delete() # get rid of the obsolete MailTrigger + + +def forward(apps, schema_editor): + """Forward migration: create irsg_ballot_issued and rename ballot_issued to iesg_ballot_issued""" + # Load historical models + MailTrigger = apps.get_model('mailtrigger', 'MailTrigger') + Recipient = apps.get_model('mailtrigger', 'Recipient') + + # Create the new MailTrigger + irsg_ballot_issued = MailTrigger.objects.create( + slug='irsg_ballot_issued', + desc='Recipients when a new IRSG ballot is issued', + ) + irsg_ballot_issued.to.set(Recipient.objects.filter(slug='irsg')) + irsg_ballot_issued.cc.set(Recipient.objects.filter(slug__in=[ + 'doc_stream_manager', 'doc_affecteddoc_authors', 'doc_affecteddoc_group_chairs', + 'doc_affecteddoc_notify', 'doc_authors', 'doc_group_chairs', 'doc_group_mail_list', + 'doc_notify', 'doc_shepherd' + ])) + + # Replace existing 'ballot_issued' object with an 'iesg_ballot_issued' + replace_mailtrigger(MailTrigger, 'ballot_issued', 'iesg_ballot_issued') + + +def reverse(apps, shema_editor): + """Reverse migration: rename iesg_ballot_issued to ballot_issued and remove irsg_ballot_issued""" + MailTrigger = apps.get_model('mailtrigger', 'MailTrigger') + MailTrigger.objects.filter(slug='irsg_ballot_issued').delete() + replace_mailtrigger(MailTrigger, 'iesg_ballot_issued', 'ballot_issued') + + +class Migration(migrations.Migration): + dependencies = [ + ('mailtrigger', '0015_add_ad_approved_status_change'), + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index fb4b18669..edd8d0de5 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -3285,18 +3285,6 @@ "model": "mailtrigger.mailtrigger", "pk": "ballot_ednote_changed_late" }, - { - "fields": { - "cc": [], - "desc": "Recipients when a ballot is issued", - "to": [ - "iesg", - "iesg_secretary" - ] - }, - "model": "mailtrigger.mailtrigger", - "pk": "ballot_issued" - }, { "fields": { "cc": [], @@ -3707,6 +3695,18 @@ "model": "mailtrigger.mailtrigger", "pk": "group_personnel_change" }, + { + "fields": { + "cc": [], + "desc": "Recipients when a ballot is issued", + "to": [ + "iesg", + "iesg_secretary" + ] + }, + "model": "mailtrigger.mailtrigger", + "pk": "iesg_ballot_issued" + }, { "fields": { "cc": [ @@ -3819,6 +3819,27 @@ "model": "mailtrigger.mailtrigger", "pk": "ipr_posting_confirmation" }, + { + "fields": { + "cc": [ + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_authors", + "doc_group_chairs", + "doc_group_mail_list", + "doc_notify", + "doc_shepherd", + "doc_stream_manager" + ], + "desc": "Recipients when a new IRSG ballot is issued", + "to": [ + "irsg" + ] + }, + "model": "mailtrigger.mailtrigger", + "pk": "irsg_ballot_issued" + }, { "fields": { "cc": [ diff --git a/ietf/templates/doc/mail/close_irsg_ballot_mail.txt b/ietf/templates/doc/mail/close_irsg_ballot_mail.txt new file mode 100644 index 000000000..48944be15 --- /dev/null +++ b/ietf/templates/doc/mail/close_irsg_ballot_mail.txt @@ -0,0 +1,3 @@ +{% load ietf_filters %}{% load mail_filters %}{% autoescape off %} {% filter wordwrap:78 %}The IRSG ballot for {{ doc.file_tag }} has been closed. The evaluation for this document can be found at {{ doc_url }}{% endfilter %} + +{% endautoescape%} diff --git a/ietf/templates/doc/mail/issue_ballot_mail.txt b/ietf/templates/doc/mail/issue_iesg_ballot_mail.txt similarity index 100% rename from ietf/templates/doc/mail/issue_ballot_mail.txt rename to ietf/templates/doc/mail/issue_iesg_ballot_mail.txt diff --git a/ietf/templates/doc/mail/issue_irsg_ballot_mail.txt b/ietf/templates/doc/mail/issue_irsg_ballot_mail.txt new file mode 100644 index 000000000..cb26efc4b --- /dev/null +++ b/ietf/templates/doc/mail/issue_irsg_ballot_mail.txt @@ -0,0 +1,7 @@ +{% load ietf_filters %}{% load mail_filters %}{% autoescape off %}{% filter wordwrap:78 %}Evaluation for {{ doc.file_tag }} can be found at {{ doc_url }} + +Ballot expire{% if ballot_expired %}d{% else %}s{% endif %} on: {{ ballot_duedate }} +{% endfilter %} +{% filter wordwrap:78 %}{{ needed_ballot_positions }}{% endfilter %} + +{% endautoescape%}