Skip to content

Commit 06179c7

Browse files
committed
Improves control of email headers for review summary messages. Provides team-specific templates for review summary messages. Fixes ietf-tools#2092 and ietf-tools#2082. Commit ready for merge.
- Legacy-Id: 12482
1 parent 63a9599 commit 06179c7

7 files changed

Lines changed: 297 additions & 22 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations
5+
6+
def forward(apps, schema_editor):
7+
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
8+
Group = apps.get_model('group','Group')
9+
10+
DBTemplate.objects.create(
11+
path='/group/defaults/email/open_assignments.txt',
12+
title='Default template for review team open assignment summary email',
13+
type_id='django',
14+
group=None,
15+
content="""{% autoescape off %}Subject: Open review assignments in {{group.acronym}}
16+
17+
The following reviewers have assignments:{% for r in review_requests %}{% ifchanged r.section %}
18+
19+
{{r.section}}
20+
21+
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %}
22+
{{ 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 %}
23+
24+
* Other revision previously reviewed
25+
** This revision already reviewed
26+
27+
{% if rotation_list %}Next in the reviewer rotation:
28+
29+
{% for p in rotation_list %} {{ p }}
30+
{% endfor %}{% endif %}{% endautoescape %}
31+
"""
32+
)
33+
34+
DBTemplate.objects.create(
35+
path='/group/genart/email/open_assignments.txt',
36+
title='Genart open assignment summary',
37+
type_id='django',
38+
group=Group.objects.get(acronym='genart'),
39+
content="""{% autoescape off %}Subject: Review Assignments
40+
41+
Hi all,
42+
43+
The following reviewers have assignments:{% for r in review_requests %}{% ifchanged r.section %}
44+
45+
{{r.section}}
46+
47+
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %}
48+
{{ 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 %}
49+
50+
* Other revision previously reviewed
51+
** This revision already reviewed
52+
53+
{% if rotation_list %}Next in the reviewer rotation:
54+
55+
{% for p in rotation_list %} {{ p }}
56+
{% endfor %}{% endif %}
57+
The LC and Telechat review templates are included below:
58+
-------------------------------------------------------
59+
60+
-- Begin LC Template --
61+
I am the assigned Gen-ART reviewer for this draft. The General Area
62+
Review Team (Gen-ART) reviews all IETF documents being processed
63+
by the IESG for the IETF Chair. Please treat these comments just
64+
like any other last call comments.
65+
66+
For more information, please see the FAQ at
67+
68+
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
69+
70+
Document:
71+
Reviewer:
72+
Review Date:
73+
IETF LC End Date:
74+
IESG Telechat date: (if known)
75+
76+
Summary:
77+
78+
Major issues:
79+
80+
Minor issues:
81+
82+
Nits/editorial comments:
83+
84+
-- End LC Template --
85+
86+
-- Begin Telechat Template --
87+
I am the assigned Gen-ART reviewer for this draft. The General Area
88+
Review Team (Gen-ART) reviews all IETF documents being processed
89+
by the IESG for the IETF Chair. Please wait for direction from your
90+
document shepherd or AD before posting a new version of the draft.
91+
92+
For more information, please see the FAQ at
93+
94+
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
95+
96+
Document:
97+
Reviewer:
98+
Review Date:
99+
IETF LC End Date:
100+
IESG Telechat date: (if known)
101+
102+
Summary:
103+
104+
Major issues:
105+
106+
Minor issues:
107+
108+
Nits/editorial comments:
109+
110+
-- End Telechat Template --
111+
{% endautoescape %}
112+
"""
113+
)
114+
115+
DBTemplate.objects.create(
116+
path='/group/secdir/email/open_assignments.txt',
117+
title='Secdir open assignment summary',
118+
type_id='django',
119+
group=Group.objects.get(acronym='secdir'),
120+
content="""{% autoescape off %}Subject: Assignments
121+
122+
Review instructions and related resources are at:
123+
http://tools.ietf.org/area/sec/trac/wiki/SecDirReview{% for r in review_requests %}{% ifchanged r.section %}
124+
125+
{{r.section}}
126+
127+
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %}
128+
{{ 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 %}
129+
130+
{% if rotation_list %}Next in the reviewer rotation:
131+
132+
{% for p in rotation_list %} {{ p }}
133+
{% endfor %}{% endif %}{% endautoescape %}
134+
"""
135+
)
136+
137+
def reverse(apps, schema_editor):
138+
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
139+
DBTemplate.objects.filter(path__in=['/group/defaults/email/open_assignments.txt',
140+
'/group/genart/email/open_assignments.txt',
141+
'/group/secdir/email/open_assignments.txt',]).delete()
142+
143+
class Migration(migrations.Migration):
144+
145+
dependencies = [
146+
('dbtemplate', '0002_auto_20141222_1749'),
147+
('group', '0009_auto_20150930_0758'),
148+
]
149+
150+
operations = [
151+
migrations.RunPython(forward,reverse)
152+
]

ietf/group/tests_review.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ietf.name.models import ReviewTypeName, ReviewResultName, ReviewRequestStateName
2020
import ietf.group.views_review
2121
from ietf.utils.mail import outbox, empty_outbox
22+
from ietf.dbtemplate.factories import DBTemplateFactory
2223

2324
class ReviewTests(TestCase):
2425
def test_review_requests(self):
@@ -284,6 +285,19 @@ def test_manage_review_requests(self):
284285
def test_email_open_review_assignments(self):
285286
doc = make_test_data()
286287
review_req1 = make_review_data(doc)
288+
DBTemplateFactory.create(path='/group/defaults/email/open_assignments.txt',
289+
type_id='django',
290+
content = """
291+
{% autoescape off %}
292+
Reviewer Deadline Draft
293+
{% 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 %}
294+
{% endfor %}
295+
{% if rotation_list %}Next in the reviewer rotation:
296+
297+
{% for p in rotation_list %} {{ p }}
298+
{% endfor %}{% endif %}
299+
{% endautoescape %}
300+
""")
287301

288302
group = review_req1.team
289303

@@ -302,14 +316,20 @@ def test_email_open_review_assignments(self):
302316

303317
empty_outbox()
304318
r = self.client.post(url, {
305-
"to": group.list_email,
319+
"to": 'toaddr@bogus.test',
320+
"cc": 'ccaddr@bogus.test',
321+
"reply_to": 'replytoaddr@bogus.test',
322+
"frm" : 'fromaddr@bogus.test',
306323
"subject": "Test subject",
307324
"body": "Test body",
308325
"action": "email",
309326
})
310327
self.assertEqual(r.status_code, 302)
311328
self.assertEqual(len(outbox), 1)
312-
self.assertTrue(group.list_email in outbox[0]["To"])
329+
self.assertTrue('toaddr' in outbox[0]["To"])
330+
self.assertTrue('ccaddr' in outbox[0]["Cc"])
331+
self.assertTrue('replytoaddr' in outbox[0]["Reply-To"])
332+
self.assertTrue('fromaddr' in outbox[0]["From"])
313333
self.assertEqual(outbox[0]["subject"], "Test subject")
314334
self.assertTrue("Test body" in outbox[0].get_payload(decode=True).decode("utf-8"))
315335

ietf/group/views_review.py

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import datetime, math
22
from collections import defaultdict
33

4+
import debug # pyflakes:ignore
5+
46
from django.shortcuts import render, redirect, get_object_or_404
57
from django.http import Http404, HttpResponseForbidden, HttpResponseRedirect
68
from django.contrib.auth.decorators import login_required
79
from django.core.urlresolvers import reverse as urlreverse
10+
from django.db.models import Max
811
from django import forms
912
from django.template.loader import render_to_string
1013

@@ -23,13 +26,17 @@
2326
reviewer_rotation_list,
2427
latest_review_requests_for_reviewers,
2528
augment_review_requests_with_events)
29+
from ietf.doc.models import LastCallDocEvent
2630
from ietf.group.models import Role
2731
from ietf.group.utils import get_group_or_404, construct_group_menu_context
2832
from ietf.person.fields import PersonEmailChoiceField
2933
from ietf.name.models import ReviewRequestStateName
30-
from ietf.utils.mail import send_mail_text
31-
from ietf.utils.fields import DatepickerDateField
34+
from ietf.utils.mail import send_mail_text, parse_preformatted
35+
from ietf.utils.fields import DatepickerDateField, MultiEmailField
3236
from ietf.ietfauth.utils import user_is_person
37+
from ietf.dbtemplate.models import DBTemplate
38+
from ietf.mailtrigger.utils import gather_address_lists
39+
from ietf.mailtrigger.models import Recipient
3340

3441
def get_open_review_requests_for_team(team, assignment_status=None):
3542
open_review_requests = ReviewRequest.objects.filter(
@@ -327,7 +334,10 @@ def manage_review_requests(request, acronym, group_type=None, assignment_status=
327334
})
328335

329336
class EmailOpenAssignmentsForm(forms.Form):
330-
to = forms.EmailField(widget=forms.EmailInput(attrs={ "readonly": True }))
337+
frm = forms.CharField(label="From", widget=forms.EmailInput(attrs={"readonly":True}))
338+
to = MultiEmailField()
339+
cc = MultiEmailField(required=False)
340+
reply_to = MultiEmailField(required=False)
331341
subject = forms.CharField()
332342
body = forms.CharField(widget=forms.Textarea)
333343

@@ -345,7 +355,32 @@ def email_open_review_assignments(request, acronym, group_type=None):
345355
state__in=("requested", "accepted"),
346356
).exclude(
347357
reviewer=None,
348-
).prefetch_related("reviewer", "type", "state", "doc").distinct().order_by("deadline", "reviewer"))
358+
).prefetch_related("reviewer", "type", "state", "doc").distinct().order_by("reviewer","-deadline"))
359+
360+
review_requests.sort(key=lambda r:r.reviewer.person.last_name()+r.reviewer.person.first_name())
361+
362+
for r in review_requests:
363+
if r.doc.telechat_date():
364+
r.section = 'For telechat %s' % r.doc.telechat_date().isoformat()
365+
r.section_order='0'+r.section
366+
elif r.type_id == 'early':
367+
r.section = 'Early review requests:'
368+
r.section_order='2'
369+
else:
370+
r.section = 'Last calls:'
371+
r.section_order='1'
372+
e = r.doc.latest_event(LastCallDocEvent, type="sent_last_call")
373+
r.lastcall_ends = e and e.expires.date().isoformat()
374+
r.earlier_review = ReviewRequest.objects.filter(doc=r.doc,reviewer__in=r.reviewer.person.email_set.all(),state="completed")
375+
if r.earlier_review:
376+
req_rev = r.requested_rev or r.doc.rev
377+
earlier_review_rev = r.earlier_review.aggregate(Max('reviewed_rev'))['reviewed_rev__max']
378+
if req_rev == earlier_review_rev:
379+
r.earlier_review_mark = '**'
380+
else:
381+
r.earlier_review_mark = '*'
382+
383+
review_requests.sort(key=lambda r: r.section_order)
349384

350385
back_url = request.GET.get("next")
351386
if not back_url:
@@ -359,20 +394,35 @@ def email_open_review_assignments(request, acronym, group_type=None):
359394
if request.method == "POST" and request.POST.get("action") == "email":
360395
form = EmailOpenAssignmentsForm(request.POST)
361396
if form.is_valid():
362-
send_mail_text(request, form.cleaned_data["to"], None, form.cleaned_data["subject"], form.cleaned_data["body"])
363-
397+
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"])})
364398
return HttpResponseRedirect(back_url)
365399
else:
366-
to = group.list_email
367-
subject = "Open review assignments in {}".format(group.acronym)
400+
(to,cc) = gather_address_lists('review_assignments_summarized',group=group)
401+
reply_to = Recipient.objects.get(slug='group_secretaries').gather(group=group)
402+
frm = request.user.person.formatted_email()
368403

369-
body = render_to_string("group/email_open_review_assignments.txt", {
404+
templateqs = DBTemplate.objects.filter(path="/group/%s/email/open_assignments.txt" % group.acronym)
405+
if templateqs.exists():
406+
template = templateqs.first()
407+
else:
408+
template = DBTemplate.objects.get(path="/group/defaults/email/open_assignments.txt")
409+
410+
partial_msg = render_to_string(template.path, {
370411
"review_requests": review_requests,
371412
"rotation_list": reviewer_rotation_list(group)[:10],
413+
"group" : group,
372414
})
415+
416+
(msg,_,_) = parse_preformatted(partial_msg)
417+
418+
body = msg.get_payload()
419+
subject = msg['Subject']
373420

374421
form = EmailOpenAssignmentsForm(initial={
375-
"to": to,
422+
"to": ", ".join(to),
423+
"cc": ", ".join(cc),
424+
"reply_to": ", ".join(reply_to),
425+
"frm": frm,
376426
"subject": subject,
377427
"body": body,
378428
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations
5+
6+
def forward(apps, schema_editor):
7+
MailTrigger=apps.get_model('mailtrigger','MailTrigger')
8+
Recipient=apps.get_model('mailtrigger','Recipient')
9+
10+
annc = MailTrigger.objects.create(
11+
slug='review_assignments_summarized',
12+
desc='Recipients when an review team secretary send a summary of open review assignments',
13+
)
14+
annc.to = Recipient.objects.filter(slug__in=['group_mail_list',])
15+
annc.cc = []
16+
17+
18+
def reverse(apps, schema_editor):
19+
MailTrigger=apps.get_model('mailtrigger','MailTrigger')
20+
MailTrigger.objects.filter(slug__in=['review_assignments_summarized']).delete()
21+
22+
class Migration(migrations.Migration):
23+
24+
dependencies = [
25+
('mailtrigger', '0007_add_interim_announce'),
26+
]
27+
28+
operations = [
29+
migrations.RunPython(forward, reverse)
30+
]

0 commit comments

Comments
 (0)