Skip to content

Commit b9f4b70

Browse files
committed
Add simple email notifications for assigning/rejecting review requests
- Legacy-Id: 11236
1 parent 5dd079e commit b9f4b70

5 files changed

Lines changed: 116 additions & 44 deletions

File tree

ietf/doc/tests_review.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def test_withdraw_request(self):
107107
self.assertEqual(r.status_code, 200)
108108

109109
# withdraw
110+
empty_outbox()
110111
r = self.client.post(withdraw_url, { "action": "withdraw" })
111112
self.assertEqual(r.status_code, 302)
112113

@@ -115,6 +116,8 @@ def test_withdraw_request(self):
115116
e = doc.latest_event()
116117
self.assertEqual(e.type, "changed_review_request")
117118
self.assertTrue("Withdrew" in e.desc)
119+
self.assertEqual(len(outbox), 1)
120+
self.assertTrue("withdrawn" in unicode(outbox[0]))
118121

119122
def test_assign_reviewer(self):
120123
doc = make_test_data()
@@ -140,24 +143,31 @@ def test_assign_reviewer(self):
140143
self.assertEqual(r.status_code, 200)
141144

142145
# assign
146+
empty_outbox()
143147
reviewer = Role.objects.filter(name="reviewer", group=review_req.team).first()
144148
r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk })
145149
self.assertEqual(r.status_code, 302)
146150

147151
review_req = reload_db_objects(review_req)
148152
self.assertEqual(review_req.state_id, "requested")
149153
self.assertEqual(review_req.reviewer, reviewer)
154+
self.assertEqual(len(outbox), 1)
155+
self.assertTrue("assigned" in unicode(outbox[0]))
150156

151157
# re-assign
158+
empty_outbox()
152159
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
153160
review_req.save()
154161
reviewer = Role.objects.filter(name="reviewer", group=review_req.team).exclude(pk=reviewer.pk).first()
155162
r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk })
156163
self.assertEqual(r.status_code, 302)
157164

158165
review_req = reload_db_objects(review_req)
159-
self.assertEqual(review_req.state_id, "requested")
166+
self.assertEqual(review_req.state_id, "requested") # check that state is reset
160167
self.assertEqual(review_req.reviewer, reviewer)
168+
self.assertEqual(len(outbox), 2)
169+
self.assertTrue("cancelled your assignment" in unicode(outbox[0]))
170+
self.assertTrue("assigned" in unicode(outbox[1]))
161171

162172
def test_reject_reviewer_assignment(self):
163173
doc = make_test_data()
@@ -194,4 +204,5 @@ def test_reject_reviewer_assignment(self):
194204
self.assertTrue("rejected" in e.desc)
195205
self.assertEqual(ReviewRequest.objects.filter(doc=review_req.doc, team=review_req.team, state="requested").count(), 1)
196206
self.assertEqual(len(outbox), 1)
197-
207+
self.assertTrue("Test message" in unicode(outbox[0]))
208+

ietf/doc/views_review.py

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
from ietf.name.models import ReviewRequestStateName
1111
from ietf.group.models import Role
1212
from ietf.review.models import ReviewRequest
13-
from ietf.review.utils import active_review_teams, assign_review_request_to_reviewer
14-
from ietf.review.utils import can_request_review_of_doc, can_manage_review_requests_for_team
13+
from ietf.review.utils import (active_review_teams, assign_review_request_to_reviewer,
14+
can_request_review_of_doc, can_manage_review_requests_for_team,
15+
email_about_review_request)
1516
from ietf.utils.fields import DatepickerDateField
1617

1718
class RequestReviewForm(forms.ModelForm):
@@ -134,6 +135,7 @@ def withdraw_request(request, name, request_id):
134135
return HttpResponseForbidden("You do not have permission to perform this action")
135136

136137
if request.method == "POST" and request.POST.get("action") == "withdraw":
138+
prev_state = review_req.state
137139
review_req.state = ReviewRequestStateName.objects.get(slug="withdrawn")
138140
review_req.save()
139141

@@ -144,9 +146,12 @@ def withdraw_request(request, name, request_id):
144146
desc="Withdrew request for {} review by {}".format(review_req.type.name, review_req.team.acronym.upper()),
145147
)
146148

147-
if review_req.state_id != "requested":
148-
# FIXME: handle this case - by emailing?
149-
pass
149+
if prev_state.slug != "requested":
150+
email_about_review_request(
151+
request, review_req,
152+
"Withdrew review request for %s" % review_req.doc.name,
153+
"Review request has been withdrawn by %s." % request.user.person,
154+
by=request.user.person, notify_secretary=False, notify_reviewer=True)
150155

151156
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
152157

@@ -187,7 +192,7 @@ def assign_reviewer(request, name, request_id):
187192
form = AssignReviewerForm(review_req, request.POST)
188193
if form.is_valid():
189194
reviewer = form.cleaned_data["reviewer"]
190-
assign_review_request_to_reviewer(review_req, reviewer, request.user.person)
195+
assign_review_request_to_reviewer(request, review_req, reviewer)
191196

192197
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
193198
else:
@@ -216,35 +221,48 @@ def reject_reviewer_assignment(request, name, request_id):
216221
return HttpResponseForbidden("You do not have permission to perform this action")
217222

218223
if request.method == "POST" and request.POST.get("action") == "reject":
219-
# reject the request
220-
review_req.state = ReviewRequestStateName.objects.get(slug="rejected")
221-
review_req.save()
224+
form = RejectReviewerAssignmentForm(request.POST)
225+
if form.is_valid():
226+
# reject the request
227+
review_req.state = ReviewRequestStateName.objects.get(slug="rejected")
228+
review_req.save()
222229

223-
DocEvent.objects.create(
224-
type="changed_review_request",
225-
doc=review_req.doc,
226-
by=request.user.person,
227-
desc="Assignment of request for {} review by {} to {} was rejected".format(
228-
review_req.type.name,
229-
review_req.team.acronym.upper(),
230-
review_req.reviewer.person,
231-
),
232-
)
233-
234-
# make a new unassigned review request
235-
new_review_req = ReviewRequest.objects.create(
236-
time=review_req.time,
237-
type=review_req.type,
238-
doc=review_req.doc,
239-
team=review_req.team,
240-
deadline=review_req.deadline,
241-
requested_rev=review_req.requested_rev,
242-
state=ReviewRequestStateName.objects.get(slug="requested"),
243-
)
230+
DocEvent.objects.create(
231+
type="changed_review_request",
232+
doc=review_req.doc,
233+
by=request.user.person,
234+
desc="Assignment of request for {} review by {} to {} was rejected".format(
235+
review_req.type.name,
236+
review_req.team.acronym.upper(),
237+
review_req.reviewer.person,
238+
),
239+
)
240+
241+
# make a new unassigned review request
242+
new_review_req = ReviewRequest.objects.create(
243+
time=review_req.time,
244+
type=review_req.type,
245+
doc=review_req.doc,
246+
team=review_req.team,
247+
deadline=review_req.deadline,
248+
requested_rev=review_req.requested_rev,
249+
state=ReviewRequestStateName.objects.get(slug="requested"),
250+
)
251+
252+
msg = u"Reviewer assignment rejected by %s." % request.user.person
244253

245-
return redirect(review_request, name=new_review_req.doc.name, request_id=new_review_req.pk)
254+
m = form.cleaned_data.get("message_to_secretary")
255+
if m:
256+
msg += "\n\n" + "Explanation:" + "\n" + m
257+
258+
email_about_review_request(request, review_req, "Reviewer assignment rejected", msg, by=request.user.person, notify_secretary=True, notify_reviewer=True)
259+
260+
return redirect(review_request, name=new_review_req.doc.name, request_id=new_review_req.pk)
261+
else:
262+
form = RejectReviewerAssignmentForm()
246263

247264
return render(request, 'doc/review/reject_reviewer_assignment.html', {
248265
'doc': doc,
249266
'review_req': review_req,
267+
'form': form,
250268
})

ietf/review/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class ReviewRequest(models.Model):
2727
time = models.DateTimeField(auto_now_add=True)
2828
type = models.ForeignKey(ReviewTypeName)
2929
doc = models.ForeignKey(Document, related_name='review_request_set')
30-
team = models.ForeignKey(Group)
30+
team = models.ForeignKey(Group, limit_choices_to=~models.Q(reviewresultname=None))
3131
deadline = models.DateTimeField()
3232
requested_rev = models.CharField(verbose_name="requested revision", max_length=16, blank=True, help_text="Fill in if a specific revision is to be reviewed, e.g. 02")
3333

ietf/review/utils.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from django.contrib.sites.models import Site
2+
13
from ietf.group.models import Group, Role
24
from ietf.doc.models import DocEvent
35
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream
46
from ietf.review.models import ReviewRequestStateName
7+
from ietf.utils.mail import send_mail
58

69
def active_review_teams():
710
# if there's a ReviewResultName defined, it's a review team
@@ -17,16 +20,47 @@ def can_manage_review_requests_for_team(user, team):
1720
if not user.is_authenticated():
1821
return False
1922

20-
return Role.objects.filter(name="secretary", person__user=user, group=team).exists() or has_role(user, "Secretariat")
23+
return Role.objects.filter(name__in=["secretary", "delegate"], person__user=user, group=team).exists() or has_role(user, "Secretariat")
24+
25+
def email_about_review_request(request, review_req, subject, msg, by, notify_secretary, notify_reviewer):
26+
"""Notify possibly both secretary and reviewer about change, skipping
27+
a party if the change was done by that party."""
28+
29+
def extract_email_addresses(roles):
30+
if any(r.person == by for r in roles if r):
31+
return []
32+
else:
33+
return [r.formatted_email() for r in roles if r]
34+
35+
to = []
36+
37+
if notify_secretary:
38+
to += extract_email_addresses(Role.objects.filter(name__in=["secretary", "delegate"], group=review_req.team).distinct())
39+
if notify_reviewer:
40+
to += extract_email_addresses([review_req.reviewer])
2141

22-
def assign_review_request_to_reviewer(review_req, reviewer, by):
42+
if not to:
43+
return
44+
45+
send_mail(request, list(set(to)), None, subject, "doc/mail/review_request_changed.txt", {
46+
"domain": Site.objects.get_current().domain,
47+
"review_req": review_req,
48+
"msg": msg,
49+
})
50+
51+
52+
def assign_review_request_to_reviewer(request, review_req, reviewer):
2353
assert review_req.state_id in ("requested", "accepted")
2454

25-
if review_req.reviewer == reviewer:
55+
if reviewer == review_req.reviewer:
2656
return
2757

28-
prev_state = review_req.state
29-
prev_reviewer = review_req.reviewer
58+
if review_req.reviewer:
59+
email_about_review_request(
60+
request, review_req,
61+
"Unassigned from review of %s" % review_req.doc.name,
62+
"%s has cancelled your assignment to the review." % request.user.person,
63+
by=request.user.person, notify_secretary=False, notify_reviewer=True)
3064

3165
review_req.state = ReviewRequestStateName.objects.get(slug="requested")
3266
review_req.reviewer = reviewer
@@ -35,14 +69,16 @@ def assign_review_request_to_reviewer(review_req, reviewer, by):
3569
DocEvent.objects.create(
3670
type="changed_review_request",
3771
doc=review_req.doc,
38-
by=by,
72+
by=request.user.person,
3973
desc="Request for {} review by {} is assigned to {}".format(
4074
review_req.type.name,
4175
review_req.team.acronym.upper(),
4276
review_req.reviewer.person if review_req.reviewer else "(None)",
4377
),
4478
)
45-
46-
if prev_state.slug != "requested" and prev_reviewer:
47-
# FIXME: email old reviewer?
48-
pass
79+
80+
email_about_review_request(
81+
request, review_req,
82+
"Assigned to review of %s" % review_req.doc.name,
83+
"%s has assigned you to review the document." % request.user.person,
84+
by=request.user.person, notify_secretary=False, notify_reviewer=True)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{% autoescape off %}
2+
{{ review_req.type.name }} review of: {{ review_req.doc.name }}{% if review_req.requested_rev %}-{{ review_req.requested_rev }}{% endif %}
3+
https://{{ domain }}{% url "ietf.doc.views_review.review_request" name=review_req.doc.name request_id=review_req.pk %}
4+
5+
{{ msg|wordwrap:72 }}
6+
7+
{% endautoescape %}

0 commit comments

Comments
 (0)