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
This commit is contained in:
Henrik Levkowetz 2016-12-08 17:55:37 +00:00
parent ef301c0f31
commit 83a260c6c0
7 changed files with 297 additions and 22 deletions

View file

@ -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
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
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
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
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)
]

View file

@ -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"))

View file

@ -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,
})

View file

@ -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)
]

View file

@ -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": [

View file

@ -17,7 +17,7 @@
{% buttons %}
<a href="{{ back_url }}" class="btn btn-default pull-right">Cancel</a>
<button class="btn btn-primary" type="submit" name="action" value="email">Send to team mailing list</button>
<button class="btn btn-primary" type="submit" name="action" value="email">Send</button>
{% endbuttons %}
</form>
{% else %}

View file

@ -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 %}