diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index fae909495..4c1c1ed3b 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -46,7 +46,36 @@ def email_ad_approved_doc(request, doc, text): dict(text=text, docname=doc.filename_with_rev()), bcc=bcc) - + +def email_ad_approved_conflict_review(request, review, ok_to_publish): + """Email notification when AD approves a conflict review""" + conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target.document + (to, cc) = gather_address_lists("ad_approved_conflict_review") + frm = request.user.person.formatted_email() + send_mail(request, + to, + frm, + "Approved: %s" % review.title, + "doc/conflict_review/ad_approval_pending_email.txt", + dict(ok_to_publish=ok_to_publish, + review=review, + conflictdoc=conflictdoc), + cc=cc) + +def email_ad_approved_status_change(request, status_change, related_doc_info): + """Email notification when AD approves a status change""" + (to, cc) = gather_address_lists("ad_approved_status_change") + frm = request.user.person.formatted_email() + send_mail(request, + to, + frm, + "Approved: %s" % status_change.title, + "doc/status_change/ad_approval_pending_email.txt", + dict( + related_doc_info=related_doc_info + ), + cc=cc) + def email_stream_changed(request, doc, old_stream, new_stream, text=""): """Email the change text to the notify group and to the stream chairs""" streams = [] diff --git a/ietf/doc/tests_conflict_review.py b/ietf/doc/tests_conflict_review.py index ee4ac4e89..8be3e89f0 100644 --- a/ietf/doc/tests_conflict_review.py +++ b/ietf/doc/tests_conflict_review.py @@ -306,13 +306,66 @@ class ConflictReviewTests(TestCase): else: self.assertIn( 'NOT be published', ''.join(wrap(get_payload_text(outbox[0]), 2**16))) - def test_approve_reqnopub(self): + """Test secretariat approving a conf review FROM the appr-reqnopub-pend state""" self.approve_test_helper('appr-reqnopub') def test_approve_noprob(self): + """Test secretariat approving a conf review FROM the appr-reqnopub-pend state""" self.approve_test_helper('appr-noprob') + def approval_pend_notice_test_helper(self, approve_type, role): + """Test notification email when review state changed to a -pend state + + Sets up, clears outbox, and changes state. If notifications are sent, + asserts basic properties common to all approve_types. + + Caller should inspect outbox to access notifications if any. + """ + doc = Document.objects.get(name='conflict-review-imaginary-irtf-submission') + url = urlreverse('ietf.doc.views_conflict_review.change_state',kwargs=dict(name=doc.name)) + + login_testing_unauthorized(self, role, url) + empty_outbox() + + # Issue the request + pending_pk = str(State.objects.get(used=True, + slug=approve_type+'-pend', + type__slug='conflrev').pk) + r = self.client.post(url,dict(review_state=pending_pk,comment='some comment or other')) + + # Check the results + self.assertEqual(r.status_code, 302) + + # If we received a notification, check the common features for all approve_types + if len(outbox) > 0: + notification = outbox[0] + self.assertIn(doc.title, notification['Subject']) + self.assertIn('iesg-secretary@ietf.org', notification['To']) + self.assertTrue(notification['Subject'].startswith('Approved:')) + + def test_approval_pend_notice_ad_reqnopub(self): + """Test notification email when AD puts doc in appr-reqnopub-pend state""" + self.approval_pend_notice_test_helper('appr-reqnopub', 'ad') + self.assertEqual(len(outbox), 1) + self.assertIn('NOT be published', get_payload_text(outbox[0])) + + def test_no_approval_pend_notice_secr_reqnopub(self): + """Test notification email when secretariat puts doc in appr-reqnopub-pend state""" + self.approval_pend_notice_test_helper('appr-reqnopub', 'secretariat') + self.assertEqual(len(outbox), 0) # no notification should be sent + + def test_approval_pend_notice_ad_noprob(self): + """Test notification email when AD puts doc in appr-noprob-pend state""" + self.approval_pend_notice_test_helper('appr-noprob', 'ad') + self.assertEqual(len(outbox), 1) + self.assertIn('IESG has no problem', get_payload_text(outbox[0])) + + def test_no_approval_pend_notice_secr_noprob(self): + """Test notification email when secretariat puts doc in appr-noprob-pend state""" + self.approval_pend_notice_test_helper('appr-noprob', 'secretariat') + self.assertEqual(len(outbox), 0) + def setUp(self): IndividualDraftFactory(name='draft-imaginary-independent-submission') ConflictReviewFactory(name='conflict-review-imaginary-irtf-submission',review_of=IndividualDraftFactory(name='draft-imaginary-irtf-submission',stream_id='irtf'),notify='notifyme@example.net') diff --git a/ietf/doc/tests_status_change.py b/ietf/doc/tests_status_change.py index cbfd131f2..5daa5adab 100644 --- a/ietf/doc/tests_status_change.py +++ b/ietf/doc/tests_status_change.py @@ -23,7 +23,7 @@ from ietf.doc.views_status_change import default_approval_text from ietf.group.models import Person from ietf.iesg.models import TelechatDate from ietf.utils.test_utils import TestCase -from ietf.utils.mail import outbox +from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import login_testing_unauthorized @@ -335,6 +335,52 @@ class StatusChangeTests(TestCase): self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith('The following approval message was sent')) + def approval_pend_notice_test_helper(self, role): + """Test notification email when review state changed to the appr-pend state""" + doc = Document.objects.get(name='status-change-imaginary-mid-review') + url = urlreverse('ietf.doc.views_status_change.change_state',kwargs=dict(name=doc.name)) + + # Add some status change related documents + doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9999'),relationship_id='tois') + doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9998'),relationship_id='tohist') + # And a non-status change related document + doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc14'),relationship_id='updates') + + login_testing_unauthorized(self, role, url) + empty_outbox() + + # Issue the request + appr_pend_pk = str(State.objects.get(used=True, + slug='appr-pend', + type__slug='statchg').pk) + r = self.client.post(url,dict(new_state=appr_pend_pk,comment='some comment or other')) + + # Check the results + self.assertEqual(r.status_code, 302) + + if role == 'ad': + self.assertEqual(len(outbox), 1) + notification = outbox[0] + self.assertIn(doc.title, notification['Subject']) + self.assertIn('iesg-secretary@ietf.org', notification['To']) + self.assertTrue(notification['Subject'].startswith('Approved:')) + notification_text = get_payload_text(notification) + self.assertIn('The AD has approved changing the status', notification_text) + self.assertIn(DocAlias.objects.get(name='rfc9999').document.canonical_name(), notification_text) + self.assertIn(DocAlias.objects.get(name='rfc9998').document.canonical_name(), notification_text) + self.assertNotIn(DocAlias.objects.get(name='rfc14').document.canonical_name(), notification_text) + self.assertNotIn('No value found for', notification_text) # make sure all interpolation values were set + else: + self.assertEqual(len(outbox), 0) + + def test_approval_pend_notice_ad(self): + """Test that an approval notice is sent to secretariat when AD approves status change""" + self.approval_pend_notice_test_helper('ad') + + def test_no_approval_pend_notice_secr(self): + """Test that no approval notice is sent when secretariat approves status change""" + self.approval_pend_notice_test_helper('secretariat') + def test_edit_relations(self): doc = Document.objects.get(name='status-change-imaginary-mid-review') url = urlreverse('ietf.doc.views_status_change.edit_relations',kwargs=dict(name=doc.name)) diff --git a/ietf/doc/views_conflict_review.py b/ietf/doc/views_conflict_review.py index dd41021bc..ae5bf104b 100644 --- a/ietf/doc/views_conflict_review.py +++ b/ietf/doc/views_conflict_review.py @@ -19,7 +19,7 @@ from ietf.doc.models import ( BallotDocEvent, BallotPositionDocEvent, DocAlias, Document, NewRevisionDocEvent, State ) from ietf.doc.utils import ( add_state_change_event, close_open_ballots, create_ballot_if_not_open, update_telechat ) -from ietf.doc.mails import email_iana +from ietf.doc.mails import email_iana, email_ad_approved_conflict_review from ietf.doc.forms import AdForm from ietf.group.models import Role, Group from ietf.iesg.models import TelechatDate @@ -79,6 +79,15 @@ def change_state(request, name, option=None): pos.save() # Consider mailing that position to 'iesg_ballot_saved' send_conflict_eval_email(request,review) + elif (new_state.slug in ("appr-reqnopub-pend", "appr-noprob-pend") + and has_role(request.user, "Area Director")): + if new_state.slug == "appr-noprob-pend": + ok_to_publish = True + else: + ok_to_publish = False + email_ad_approved_conflict_review(request, + review, + ok_to_publish) return redirect('ietf.doc.views_doc.document_main', name=review.name) diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index b5f628e2e..6dc3f2843 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -18,6 +18,7 @@ from django.conf import settings from django.utils.encoding import force_text import debug # pyflakes:ignore +from ietf.doc.mails import email_ad_approved_status_change from ietf.doc.models import ( Document, DocAlias, State, DocEvent, BallotDocEvent, BallotPositionDocEvent, NewRevisionDocEvent, WriteupDocEvent, STATUSCHANGE_RELATIONS ) @@ -90,6 +91,21 @@ def change_state(request, name, option=None): dict(doc=status_change, url = status_change.get_absolute_url(), )) + elif new_state.slug == 'appr-pend' and has_role(request.user, "Area Director"): + related_docs = status_change.relateddocument_set.filter( + relationship__slug__in=STATUSCHANGE_RELATIONS + ) + related_doc_info = [ + dict(title=rel_doc.target.document.title, + canonical_name=rel_doc.target.document.canonical_name(), + newstatus=newstatus(rel_doc)) + for rel_doc in related_docs + ] + email_ad_approved_status_change( + request, + status_change, + related_doc_info=related_doc_info, + ) return redirect('ietf.doc.views_doc.document_main', name=status_change.name) else: diff --git a/ietf/mailtrigger/migrations/0014_add_ad_approved_conflict_review.py b/ietf/mailtrigger/migrations/0014_add_ad_approved_conflict_review.py new file mode 100644 index 000000000..b16d2bde3 --- /dev/null +++ b/ietf/mailtrigger/migrations/0014_add_ad_approved_conflict_review.py @@ -0,0 +1,31 @@ +# Copyright The IETF Trust 2020, All Rights Reserved + +from django.db import migrations + + +def forward(apps, schema_editor): + MailTrigger = apps.get_model('mailtrigger', 'MailTrigger') + Recipient = apps.get_model('mailtrigger', 'Recipient') + + ad_approved_conflict_review = MailTrigger.objects.create( + slug='ad_approved_conflict_review', + desc='Recipients when AD approves a conflict review pending announcement', + ) + ad_approved_conflict_review.to.add( + Recipient.objects.get(pk='iesg_secretary') + ) + + +def reverse(apps, schema_editor): + MailTrigger = apps.get_model('mailtrigger', 'MailTrigger') + MailTrigger.objects.filter(slug='ad_approved_conflict_review').delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('mailtrigger', '0013_add_irsg_ballot_saved'), + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/mailtrigger/migrations/0015_add_ad_approved_status_change.py b/ietf/mailtrigger/migrations/0015_add_ad_approved_status_change.py new file mode 100644 index 000000000..8fce9cac0 --- /dev/null +++ b/ietf/mailtrigger/migrations/0015_add_ad_approved_status_change.py @@ -0,0 +1,31 @@ +# Copyright The IETF Trust 2020, All Rights Reserved + +from django.db import migrations + + +def forward(apps, schema_editor): + MailTrigger = apps.get_model('mailtrigger', 'MailTrigger') + Recipient = apps.get_model('mailtrigger', 'Recipient') + + ad_approved_conflict_review = MailTrigger.objects.create( + slug='ad_approved_status_change', + desc='Recipients when AD approves a status change pending announcement', + ) + ad_approved_conflict_review.to.add( + Recipient.objects.get(pk='iesg_secretary') + ) + + +def reverse(apps, schema_editor): + MailTrigger = apps.get_model('mailtrigger', 'MailTrigger') + MailTrigger.objects.filter(slug='ad_approved_status_change').delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('mailtrigger', '0014_add_ad_approved_conflict_review'), + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index a844b90eb..fb4b18669 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -3145,6 +3145,28 @@ "model": "group.groupfeatures", "pk": "wg" }, + { + "fields": { + "cc": [], + "desc": "Recipients when AD approves a conflict review pending announcement", + "to": [ + "iesg_secretary" + ] + }, + "model": "mailtrigger.mailtrigger", + "pk": "ad_approved_conflict_review" + }, + { + "fields": { + "cc": [], + "desc": "Recipients when AD approves a status change pending announcement", + "to": [ + "iesg_secretary" + ] + }, + "model": "mailtrigger.mailtrigger", + "pk": "ad_approved_status_change" + }, { "fields": { "cc": [ diff --git a/ietf/templates/doc/conflict_review/ad_approval_pending_email.txt b/ietf/templates/doc/conflict_review/ad_approval_pending_email.txt new file mode 100644 index 000000000..9988ce569 --- /dev/null +++ b/ietf/templates/doc/conflict_review/ad_approval_pending_email.txt @@ -0,0 +1,12 @@ +{% load ietf_filters %}{% load mail_filters %}{% autoescape off %} +{% filter wordwrap:78 %}{{ review.title }} has been approved and the result is ready to announce. + +{% if ok_to_publish %} +The IESG has no problem with the publication of '{{ conflictdoc.title }}' {{ conflictdoc.file_tag|safe }} as {{ conflictdoc|std_level_prompt_with_article }}. +{% else %} +The IESG recommends that '{{ conflictdoc.title }}' {{ conflictdoc.file_tag|safe }} NOT be published as {{ conflictdoc|std_level_prompt_with_article }}. +{% endif %} + +{% endfilter %} + +{% endautoescape %} diff --git a/ietf/templates/doc/status_change/ad_approval_pending_email.txt b/ietf/templates/doc/status_change/ad_approval_pending_email.txt new file mode 100644 index 000000000..23ef47155 --- /dev/null +++ b/ietf/templates/doc/status_change/ad_approval_pending_email.txt @@ -0,0 +1,9 @@ +{% load ietf_filters %}{% load mail_filters %}{% autoescape off %} +{% filter wordwrap:78 %}The AD has approved changing the status of the following {% if related_doc_info|length == 1 %}document{% else %}documents{% endif %}: +{% for relateddoc in related_doc_info %}- {{relateddoc.title }} + ({{relateddoc.canonical_name }}) to {{ relateddoc.newstatus }} +{% endfor %} +An announcement has not yet been sent. +{% endfilter %} + +{% endautoescape %}