From 83a260c6c04bbe0788fb7d7f7df5d77691d0220d Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Thu, 8 Dec 2016 17:55:37 +0000 Subject: [PATCH] Merged in [12482] from rjsparks@nostrum.com: Improves control of email headers for review summary messages. Provides team-specific templates for review summary messages. Fixes #2092 and #2082. - Legacy-Id: 12483 Note: SVN reference [12482] has been migrated to Git commit 06179c7485dac6c69b2aa30e66095a23a4460209 --- .../migrations/0003_review_summary_email.py | 152 ++++++++++++++++++ ietf/group/tests_review.py | 24 ++- ietf/group/views_review.py | 70 ++++++-- .../0008_review_summary_triggers.py | 30 ++++ ietf/name/fixtures/names.json | 33 +++- .../group/email_open_review_assignments.html | 2 +- .../group/email_open_review_assignments.txt | 8 - 7 files changed, 297 insertions(+), 22 deletions(-) create mode 100644 ietf/dbtemplate/migrations/0003_review_summary_email.py create mode 100644 ietf/mailtrigger/migrations/0008_review_summary_triggers.py delete mode 100644 ietf/templates/group/email_open_review_assignments.txt diff --git a/ietf/dbtemplate/migrations/0003_review_summary_email.py b/ietf/dbtemplate/migrations/0003_review_summary_email.py new file mode 100644 index 000000000..3fa6ffbe6 --- /dev/null +++ b/ietf/dbtemplate/migrations/0003_review_summary_email.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +def forward(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + Group = apps.get_model('group','Group') + + DBTemplate.objects.create( + path='/group/defaults/email/open_assignments.txt', + title='Default template for review team open assignment summary email', + type_id='django', + group=None, + content="""{% autoescape off %}Subject: Open review assignments in {{group.acronym}} + +The following reviewers have assignments:{% for r in review_requests %}{% ifchanged r.section %} + +{{r.section}} + +{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %} +{{ r.reviewer.person.plain_name|ljust:"22" }} {% if r.section == 'Early review requests:' %}{{ r.deadline|date:"Y-m-d" }}{% else %}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.doc_id }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %} {{ r.earlier_review_mark }}{% endfor %} + +* Other revision previously reviewed +** This revision already reviewed + +{% if rotation_list %}Next in the reviewer rotation: + +{% for p in rotation_list %} {{ p }} +{% endfor %}{% endif %}{% endautoescape %} +""" + ) + + DBTemplate.objects.create( + path='/group/genart/email/open_assignments.txt', + title='Genart open assignment summary', + type_id='django', + group=Group.objects.get(acronym='genart'), + content="""{% autoescape off %}Subject: Review Assignments + +Hi all, + +The following reviewers have assignments:{% for r in review_requests %}{% ifchanged r.section %} + +{{r.section}} + +{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %} +{{ r.reviewer.person.plain_name|ljust:"22" }} {% if r.section == 'Early review requests:' %}{{ r.deadline|date:"Y-m-d" }}{% else %}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.doc_id }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %} {{ r.earlier_review_mark }}{% endfor %} + +* Other revision previously reviewed +** This revision already reviewed + +{% if rotation_list %}Next in the reviewer rotation: + +{% for p in rotation_list %} {{ p }} +{% endfor %}{% endif %} +The LC and Telechat review templates are included below: +------------------------------------------------------- + +-- Begin LC Template -- +I am the assigned Gen-ART reviewer for this draft. The General Area +Review Team (Gen-ART) reviews all IETF documents being processed +by the IESG for the IETF Chair. Please treat these comments just +like any other last call comments. + +For more information, please see the FAQ at + +. + +Document: +Reviewer: +Review Date: +IETF LC End Date: +IESG Telechat date: (if known) + +Summary: + +Major issues: + +Minor issues: + +Nits/editorial comments: + +-- End LC Template -- + +-- Begin Telechat Template -- +I am the assigned Gen-ART reviewer for this draft. The General Area +Review Team (Gen-ART) reviews all IETF documents being processed +by the IESG for the IETF Chair. Please wait for direction from your +document shepherd or AD before posting a new version of the draft. + +For more information, please see the FAQ at + +. + +Document: +Reviewer: +Review Date: +IETF LC End Date: +IESG Telechat date: (if known) + +Summary: + +Major issues: + +Minor issues: + +Nits/editorial comments: + +-- End Telechat Template -- +{% endautoescape %} +""" + ) + + DBTemplate.objects.create( + path='/group/secdir/email/open_assignments.txt', + title='Secdir open assignment summary', + type_id='django', + group=Group.objects.get(acronym='secdir'), + content="""{% autoescape off %}Subject: Assignments + +Review instructions and related resources are at: +http://tools.ietf.org/area/sec/trac/wiki/SecDirReview{% for r in review_requests %}{% ifchanged r.section %} + +{{r.section}} + +{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %} +{{ r.reviewer.person.plain_name|ljust:"22" }}{{ r.earlier_review|yesno:'R, , ' }}{% if r.section == 'Early review requests:' %}{{ r.deadline|date:"Y-m-d" }}{% else %}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.doc_id }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %}{% endfor %} + +{% if rotation_list %}Next in the reviewer rotation: + +{% for p in rotation_list %} {{ p }} +{% endfor %}{% endif %}{% endautoescape %} +""" + ) + +def reverse(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + DBTemplate.objects.filter(path__in=['/group/defaults/email/open_assignments.txt', + '/group/genart/email/open_assignments.txt', + '/group/secdir/email/open_assignments.txt',]).delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('dbtemplate', '0002_auto_20141222_1749'), + ('group', '0009_auto_20150930_0758'), + ] + + operations = [ + migrations.RunPython(forward,reverse) + ] diff --git a/ietf/group/tests_review.py b/ietf/group/tests_review.py index c67ef3906..440fd5725 100644 --- a/ietf/group/tests_review.py +++ b/ietf/group/tests_review.py @@ -19,6 +19,7 @@ from ietf.review.utils import ( from ietf.name.models import ReviewTypeName, ReviewResultName, ReviewRequestStateName import ietf.group.views_review from ietf.utils.mail import outbox, empty_outbox +from ietf.dbtemplate.factories import DBTemplateFactory class ReviewTests(TestCase): def test_review_requests(self): @@ -284,6 +285,19 @@ class ReviewTests(TestCase): def test_email_open_review_assignments(self): doc = make_test_data() review_req1 = make_review_data(doc) + DBTemplateFactory.create(path='/group/defaults/email/open_assignments.txt', + type_id='django', + content = """ + {% autoescape off %} + Reviewer Deadline Draft + {% for r in review_requests %}{{ r.reviewer.person.plain_name|ljust:"22" }} {{ r.deadline|date:"Y-m-d" }} {{ r.doc_id }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %} + {% endfor %} + {% if rotation_list %}Next in the reviewer rotation: + + {% for p in rotation_list %} {{ p }} + {% endfor %}{% endif %} + {% endautoescape %} + """) group = review_req1.team @@ -302,14 +316,20 @@ class ReviewTests(TestCase): empty_outbox() r = self.client.post(url, { - "to": group.list_email, + "to": 'toaddr@bogus.test', + "cc": 'ccaddr@bogus.test', + "reply_to": 'replytoaddr@bogus.test', + "frm" : 'fromaddr@bogus.test', "subject": "Test subject", "body": "Test body", "action": "email", }) self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox), 1) - self.assertTrue(group.list_email in outbox[0]["To"]) + self.assertTrue('toaddr' in outbox[0]["To"]) + self.assertTrue('ccaddr' in outbox[0]["Cc"]) + self.assertTrue('replytoaddr' in outbox[0]["Reply-To"]) + self.assertTrue('fromaddr' in outbox[0]["From"]) self.assertEqual(outbox[0]["subject"], "Test subject") self.assertTrue("Test body" in outbox[0].get_payload(decode=True).decode("utf-8")) diff --git a/ietf/group/views_review.py b/ietf/group/views_review.py index 894e379ed..a569370c7 100644 --- a/ietf/group/views_review.py +++ b/ietf/group/views_review.py @@ -1,10 +1,13 @@ import datetime, math from collections import defaultdict +import debug # pyflakes:ignore + from django.shortcuts import render, redirect, get_object_or_404 from django.http import Http404, HttpResponseForbidden, HttpResponseRedirect from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse as urlreverse +from django.db.models import Max from django import forms from django.template.loader import render_to_string @@ -23,13 +26,17 @@ from ietf.review.utils import (can_manage_review_requests_for_team, reviewer_rotation_list, latest_review_requests_for_reviewers, augment_review_requests_with_events) +from ietf.doc.models import LastCallDocEvent from ietf.group.models import Role from ietf.group.utils import get_group_or_404, construct_group_menu_context from ietf.person.fields import PersonEmailChoiceField from ietf.name.models import ReviewRequestStateName -from ietf.utils.mail import send_mail_text -from ietf.utils.fields import DatepickerDateField +from ietf.utils.mail import send_mail_text, parse_preformatted +from ietf.utils.fields import DatepickerDateField, MultiEmailField from ietf.ietfauth.utils import user_is_person +from ietf.dbtemplate.models import DBTemplate +from ietf.mailtrigger.utils import gather_address_lists +from ietf.mailtrigger.models import Recipient def get_open_review_requests_for_team(team, assignment_status=None): open_review_requests = ReviewRequest.objects.filter( @@ -327,7 +334,10 @@ def manage_review_requests(request, acronym, group_type=None, assignment_status= }) class EmailOpenAssignmentsForm(forms.Form): - to = forms.EmailField(widget=forms.EmailInput(attrs={ "readonly": True })) + frm = forms.CharField(label="From", widget=forms.EmailInput(attrs={"readonly":True})) + to = MultiEmailField() + cc = MultiEmailField(required=False) + reply_to = MultiEmailField(required=False) subject = forms.CharField() body = forms.CharField(widget=forms.Textarea) @@ -345,7 +355,32 @@ def email_open_review_assignments(request, acronym, group_type=None): state__in=("requested", "accepted"), ).exclude( reviewer=None, - ).prefetch_related("reviewer", "type", "state", "doc").distinct().order_by("deadline", "reviewer")) + ).prefetch_related("reviewer", "type", "state", "doc").distinct().order_by("reviewer","-deadline")) + + review_requests.sort(key=lambda r:r.reviewer.person.last_name()+r.reviewer.person.first_name()) + + for r in review_requests: + if r.doc.telechat_date(): + r.section = 'For telechat %s' % r.doc.telechat_date().isoformat() + r.section_order='0'+r.section + elif r.type_id == 'early': + r.section = 'Early review requests:' + r.section_order='2' + else: + r.section = 'Last calls:' + r.section_order='1' + e = r.doc.latest_event(LastCallDocEvent, type="sent_last_call") + r.lastcall_ends = e and e.expires.date().isoformat() + r.earlier_review = ReviewRequest.objects.filter(doc=r.doc,reviewer__in=r.reviewer.person.email_set.all(),state="completed") + if r.earlier_review: + req_rev = r.requested_rev or r.doc.rev + earlier_review_rev = r.earlier_review.aggregate(Max('reviewed_rev'))['reviewed_rev__max'] + if req_rev == earlier_review_rev: + r.earlier_review_mark = '**' + else: + r.earlier_review_mark = '*' + + review_requests.sort(key=lambda r: r.section_order) back_url = request.GET.get("next") if not back_url: @@ -359,20 +394,35 @@ def email_open_review_assignments(request, acronym, group_type=None): if request.method == "POST" and request.POST.get("action") == "email": form = EmailOpenAssignmentsForm(request.POST) if form.is_valid(): - send_mail_text(request, form.cleaned_data["to"], None, form.cleaned_data["subject"], form.cleaned_data["body"]) - + send_mail_text(request, form.cleaned_data["to"], form.cleaned_data["frm"], form.cleaned_data["subject"], form.cleaned_data["body"],cc=form.cleaned_data["cc"],extra={"Reply-to":", ".join(form.cleaned_data["reply_to"])}) return HttpResponseRedirect(back_url) else: - to = group.list_email - subject = "Open review assignments in {}".format(group.acronym) + (to,cc) = gather_address_lists('review_assignments_summarized',group=group) + reply_to = Recipient.objects.get(slug='group_secretaries').gather(group=group) + frm = request.user.person.formatted_email() - body = render_to_string("group/email_open_review_assignments.txt", { + templateqs = DBTemplate.objects.filter(path="/group/%s/email/open_assignments.txt" % group.acronym) + if templateqs.exists(): + template = templateqs.first() + else: + template = DBTemplate.objects.get(path="/group/defaults/email/open_assignments.txt") + + partial_msg = render_to_string(template.path, { "review_requests": review_requests, "rotation_list": reviewer_rotation_list(group)[:10], + "group" : group, }) + + (msg,_,_) = parse_preformatted(partial_msg) + + body = msg.get_payload() + subject = msg['Subject'] form = EmailOpenAssignmentsForm(initial={ - "to": to, + "to": ", ".join(to), + "cc": ", ".join(cc), + "reply_to": ", ".join(reply_to), + "frm": frm, "subject": subject, "body": body, }) diff --git a/ietf/mailtrigger/migrations/0008_review_summary_triggers.py b/ietf/mailtrigger/migrations/0008_review_summary_triggers.py new file mode 100644 index 000000000..3dc0db581 --- /dev/null +++ b/ietf/mailtrigger/migrations/0008_review_summary_triggers.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +def forward(apps, schema_editor): + MailTrigger=apps.get_model('mailtrigger','MailTrigger') + Recipient=apps.get_model('mailtrigger','Recipient') + + annc = MailTrigger.objects.create( + slug='review_assignments_summarized', + desc='Recipients when an review team secretary send a summary of open review assignments', + ) + annc.to = Recipient.objects.filter(slug__in=['group_mail_list',]) + annc.cc = [] + + +def reverse(apps, schema_editor): + MailTrigger=apps.get_model('mailtrigger','MailTrigger') + MailTrigger.objects.filter(slug__in=['review_assignments_summarized']).delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('mailtrigger', '0007_add_interim_announce'), + ] + + operations = [ + migrations.RunPython(forward, reverse) + ] diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 255631d25..bae17f940 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -1641,6 +1641,16 @@ "model": "name.liaisonstatementeventtypename", "pk": "private_comment" }, +{ + "fields": { + "order": 11, + "used": true, + "name": "Re-sent", + "desc": "" + }, + "model": "name.liaisonstatementeventtypename", + "pk": "resent" +}, { "fields": { "order": 1, @@ -1891,6 +1901,16 @@ "model": "name.reviewrequeststatename", "pk": "completed" }, +{ + "fields": { + "order": 20, + "used": false, + "name": "Unknown", + "desc": "" + }, + "model": "name.reviewrequeststatename", + "pk": "unknown" +}, { "fields": { "order": 1, @@ -2466,7 +2486,7 @@ "order": 3, "used": true, "name": "IRTF", - "desc": "Independent Submission Editor stream" + "desc": "IRTF Stream" }, "model": "name.streamname", "pk": "irtf" @@ -6199,6 +6219,17 @@ "model": "mailtrigger.mailtrigger", "pk": "resurrection_requested" }, +{ + "fields": { + "cc": [], + "to": [ + "group_mail_list" + ], + "desc": "Recipients when an review team secretary send a summary of open review assignments" + }, + "model": "mailtrigger.mailtrigger", + "pk": "review_assignments_summarized" +}, { "fields": { "cc": [ diff --git a/ietf/templates/group/email_open_review_assignments.html b/ietf/templates/group/email_open_review_assignments.html index 70083a618..74dcc5b03 100644 --- a/ietf/templates/group/email_open_review_assignments.html +++ b/ietf/templates/group/email_open_review_assignments.html @@ -17,7 +17,7 @@ {% buttons %} Cancel - + {% endbuttons %} {% else %} diff --git a/ietf/templates/group/email_open_review_assignments.txt b/ietf/templates/group/email_open_review_assignments.txt deleted file mode 100644 index ef6b3f692..000000000 --- a/ietf/templates/group/email_open_review_assignments.txt +++ /dev/null @@ -1,8 +0,0 @@ -{% autoescape off %} -Reviewer Deadline Draft -{% for r in review_requests %}{{ r.reviewer.person.plain_name|ljust:"22" }} {{ r.deadline|date:"Y-m-d" }} {{ r.doc_id }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %} -{% endfor %} -{% if rotation_list %}Next in the reviewer rotation: - -{% for p in rotation_list %} {{ p }} -{% endfor %}{% endif %}{% endautoescape %}