Skip to content

Commit 19fff81

Browse files
committed
Rework closing a review request so the logic is reusable, add the more
specific close reasons to the database migration, add ReviewRequest.requested_by so it's possible to notify the requester of a review that it has been dropped. - Legacy-Id: 11520
1 parent ebc37de commit 19fff81

15 files changed

Lines changed: 168 additions & 90 deletions

ietf/doc/tests_review.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from ietf.review.models import ReviewRequest, ReviewTeamResult
1616
import ietf.review.mailarch
17-
from ietf.person.models import Email
17+
from ietf.person.models import Email, Person
1818
from ietf.name.models import ReviewResultName, ReviewRequestStateName
1919
from ietf.utils.test_utils import TestCase
2020
from ietf.utils.test_data import make_test_data, make_review_data
@@ -57,7 +57,8 @@ def test_request_review(self):
5757
"type": "early",
5858
"team": review_team.pk,
5959
"deadline_date": deadline_date.isoformat(),
60-
"requested_rev": "01"
60+
"requested_rev": "01",
61+
"requested_by": Person.objects.get(user__username="plain").pk,
6162
})
6263
self.assertEqual(r.status_code, 302)
6364

@@ -82,40 +83,40 @@ def test_review_request(self):
8283
self.assertEqual(r.status_code, 200)
8384
self.assertTrue(review_req.team.acronym.upper() in unicontent(r))
8485

85-
def test_withdraw_request(self):
86+
def test_close_request(self):
8687
doc = make_test_data()
8788
review_req = make_review_data(doc)
8889
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
8990
review_req.save()
9091

91-
withdraw_url = urlreverse('ietf.doc.views_review.withdraw_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
92+
close_url = urlreverse('ietf.doc.views_review.close_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
9293

9394

9495
# follow link
9596
req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
9697
self.client.login(username="secretary", password="secretary+password")
9798
r = self.client.get(req_url)
9899
self.assertEqual(r.status_code, 200)
99-
self.assertTrue(withdraw_url in unicontent(r))
100+
self.assertTrue(close_url in unicontent(r))
100101
self.client.logout()
101102

102-
# get withdraw page
103-
login_testing_unauthorized(self, "secretary", withdraw_url)
104-
r = self.client.get(withdraw_url)
103+
# get close page
104+
login_testing_unauthorized(self, "secretary", close_url)
105+
r = self.client.get(close_url)
105106
self.assertEqual(r.status_code, 200)
106107

107-
# withdraw
108+
# close
108109
empty_outbox()
109-
r = self.client.post(withdraw_url, { "action": "withdraw" })
110+
r = self.client.post(close_url, { "close_reason": "withdrawn" })
110111
self.assertEqual(r.status_code, 302)
111112

112113
review_req = reload_db_objects(review_req)
113114
self.assertEqual(review_req.state_id, "withdrawn")
114115
e = doc.latest_event()
115116
self.assertEqual(e.type, "changed_review_request")
116-
self.assertTrue("Withdrew" in e.desc)
117+
self.assertTrue("closed" in e.desc.lower())
117118
self.assertEqual(len(outbox), 1)
118-
self.assertTrue("withdrawn" in unicode(outbox[0]))
119+
self.assertTrue("closed" in unicode(outbox[0]).lower())
119120

120121
def test_assign_reviewer(self):
121122
doc = make_test_data()

ietf/doc/urls_review.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
urlpatterns = patterns('',
55
url(r'^$', views_review.request_review),
66
url(r'^(?P<request_id>[0-9]+)/$', views_review.review_request),
7-
url(r'^(?P<request_id>[0-9]+)/withdraw/$', views_review.withdraw_request),
7+
url(r'^(?P<request_id>[0-9]+)/close/$', views_review.close_request),
88
url(r'^(?P<request_id>[0-9]+)/assignreviewer/$', views_review.assign_reviewer),
99
url(r'^(?P<request_id>[0-9]+)/rejectreviewerassignment/$', views_review.reject_reviewer_assignment),
1010
url(r'^(?P<request_id>[0-9]+)/complete/$', views_review.complete_review),

ietf/doc/views_review.py

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
from django.template.loader import render_to_string
1111

1212
from ietf.doc.models import Document, NewRevisionDocEvent, DocEvent, State, DocAlias
13-
from ietf.ietfauth.utils import is_authorized_in_doc_stream, user_is_person
13+
from ietf.ietfauth.utils import is_authorized_in_doc_stream, user_is_person, has_role
1414
from ietf.name.models import ReviewRequestStateName, ReviewResultName, DocTypeName
1515
from ietf.review.models import ReviewRequest
16-
from ietf.person.fields import PersonEmailChoiceField
16+
from ietf.person.fields import PersonEmailChoiceField, SearchablePersonField
1717
from ietf.review.utils import (active_review_teams, assign_review_request_to_reviewer,
1818
can_request_review_of_doc, can_manage_review_requests_for_team,
19-
email_about_review_request, make_new_review_request_from_existing)
19+
email_review_request_change, make_new_review_request_from_existing,
20+
close_review_request_states, close_review_request)
2021
from ietf.review import mailarch
2122
from ietf.utils.fields import DatepickerDateField
2223
from ietf.utils.text import skip_prefix
@@ -38,7 +39,7 @@ class RequestReviewForm(forms.ModelForm):
3839

3940
class Meta:
4041
model = ReviewRequest
41-
fields = ('type', 'team', 'deadline', 'requested_rev')
42+
fields = ('requested_by', 'type', 'team', 'deadline', 'requested_rev')
4243

4344
def __init__(self, user, doc, *args, **kwargs):
4445
super(RequestReviewForm, self).__init__(*args, **kwargs)
@@ -57,6 +58,12 @@ def __init__(self, user, doc, *args, **kwargs):
5758
self.fields["deadline"].required = False
5859
self.fields["requested_rev"].label = "Document revision"
5960

61+
if has_role(user, "Secretariat"):
62+
self.fields["requested_by"] = SearchablePersonField()
63+
else:
64+
self.fields["requested_by"].widget = forms.HiddenInput()
65+
self.fields["requested_by"].initial = user.person.pk
66+
6067
def clean_deadline_date(self):
6168
v = self.cleaned_data.get('deadline_date')
6269
if v < datetime.date.today():
@@ -119,9 +126,9 @@ def review_request(request, name, request_id):
119126
is_reviewer = review_req.reviewer and user_is_person(request.user, review_req.reviewer.person)
120127
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
121128

122-
can_withdraw_request = (review_req.state_id in ["requested", "accepted"]
123-
and (is_authorized_in_doc_stream(request.user, doc)
124-
or can_manage_request))
129+
can_close_request = (review_req.state_id in ["requested", "accepted"]
130+
and (is_authorized_in_doc_stream(request.user, doc)
131+
or can_manage_request))
125132

126133
can_assign_reviewer = (review_req.state_id in ["requested", "accepted"]
127134
and can_manage_request)
@@ -147,47 +154,55 @@ def review_request(request, name, request_id):
147154
return render(request, 'doc/review/review_request.html', {
148155
'doc': doc,
149156
'review_req': review_req,
150-
'can_withdraw_request': can_withdraw_request,
157+
'can_close_request': can_close_request,
151158
'can_reject_reviewer_assignment': can_reject_reviewer_assignment,
152159
'can_assign_reviewer': can_assign_reviewer,
153160
'can_accept_reviewer_assignment': can_accept_reviewer_assignment,
154161
'can_complete_review': can_complete_review,
155162
})
156163

164+
165+
class CloseReviewRequestForm(forms.Form):
166+
close_reason = forms.ModelChoiceField(queryset=close_review_request_states(), widget=forms.RadioSelect, empty_label=None)
167+
168+
def __init__(self, can_manage_request, *args, **kwargs):
169+
super(CloseReviewRequestForm, self).__init__(*args, **kwargs)
170+
171+
if not can_manage_request:
172+
self.fields["close_reason"].queryset = self.fields["close_reason"].queryset.filter(slug__in=["withdrawn"])
173+
174+
if len(self.fields["close_reason"].queryset) == 1:
175+
self.fields["close_reason"].initial = self.fields["close_reason"].queryset.first().pk
176+
self.fields["close_reason"].widget = forms.HiddenInput()
177+
178+
157179
@login_required
158-
def withdraw_request(request, name, request_id):
180+
def close_request(request, name, request_id):
159181
doc = get_object_or_404(Document, name=name)
160182
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
161183

162-
if not is_authorized_in_doc_stream(request.user, doc):
163-
return HttpResponseForbidden("You do not have permission to perform this action")
164-
165-
if request.method == "POST" and request.POST.get("action") == "withdraw":
166-
prev_state = review_req.state
167-
review_req.state = ReviewRequestStateName.objects.get(slug="withdrawn")
168-
review_req.save()
184+
can_request = is_authorized_in_doc_stream(request.user, doc)
185+
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
169186

170-
DocEvent.objects.create(
171-
type="changed_review_request",
172-
doc=doc,
173-
by=request.user.person,
174-
desc="Withdrew request for {} review by {}".format(review_req.type.name, review_req.team.acronym.upper()),
175-
)
187+
if not (can_request or can_manage_request):
188+
return HttpResponseForbidden("You do not have permission to perform this action")
176189

177-
if prev_state.slug != "requested":
178-
email_about_review_request(
179-
request, review_req,
180-
"Withdrew review request for %s" % review_req.doc.name,
181-
"Review request has been withdrawn by %s." % request.user.person,
182-
by=request.user.person, notify_secretary=False, notify_reviewer=True)
190+
if request.method == "POST":
191+
form = CloseReviewRequestForm(can_manage_request, request.POST)
192+
if form.is_valid():
193+
close_review_request(request, review_req, form.cleaned_data["close_reason"])
183194

184195
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
196+
else:
197+
form = CloseReviewRequestForm(can_manage_request)
185198

186-
return render(request, 'doc/review/withdraw_request.html', {
199+
return render(request, 'doc/review/close_request.html', {
187200
'doc': doc,
188201
'review_req': review_req,
202+
'form': form,
189203
})
190204

205+
191206
class AssignReviewerForm(forms.Form):
192207
reviewer = PersonEmailChoiceField(widget=forms.RadioSelect, empty_label="(None)", required=False)
193208

@@ -266,7 +281,7 @@ def reject_reviewer_assignment(request, name, request_id):
266281
"message_to_secretary": form.cleaned_data.get("message_to_secretary")
267282
})
268283

269-
email_about_review_request(request, review_req, "Reviewer assignment rejected", msg, by=request.user.person, notify_secretary=True, notify_reviewer=True)
284+
email_review_request_change(request, review_req, "Reviewer assignment rejected", msg, by=request.user.person, notify_secretary=True, notify_reviewer=True, notify_requested_by=False)
270285

271286
return redirect(review_request, name=new_review_req.doc.name, request_id=new_review_req.pk)
272287
else:
@@ -438,7 +453,7 @@ def complete_review(request, name, request_id):
438453
"new_review_req": new_review_req,
439454
})
440455

441-
email_about_review_request(request, review_req, subject, msg, request.user.person, notify_secretary=True, notify_reviewer=False)
456+
email_review_request_change(request, review_req, subject, msg, request.user.person, notify_secretary=True, notify_reviewer=False, notify_requested_by=False)
442457

443458
if review_submission != "link" and review_req.team.list_email:
444459
# email the review

ietf/group/tests_review.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ietf.utils.test_data import make_test_data, make_review_data
88
from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent, reload_db_objects
99
from ietf.review.models import ReviewRequest
10-
from ietf.person.models import Email
10+
from ietf.person.models import Email, Person
1111
import ietf.group.views_review
1212

1313
class ReviewTests(TestCase):
@@ -28,6 +28,7 @@ def test_manage_review_requests(self):
2828
deadline=datetime.datetime.combine(datetime.date.today() + datetime.timedelta(days=30), datetime.time(23, 59, 59)),
2929
state_id="accepted",
3030
reviewer=review_req1.reviewer,
31+
requested_by=Person.objects.get(user__username="plain"),
3132
)
3233

3334
review_req3 = ReviewRequest.objects.create(
@@ -36,6 +37,7 @@ def test_manage_review_requests(self):
3637
type_id="early",
3738
deadline=datetime.datetime.combine(datetime.date.today() + datetime.timedelta(days=30), datetime.time(23, 59, 59)),
3839
state_id="requested",
40+
requested_by=Person.objects.get(user__username="plain"),
3941
)
4042

4143
# get

ietf/group/views_review.py

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
from django import forms
55

66
from ietf.review.models import ReviewRequest, ReviewRequestStateName
7-
from ietf.review.utils import (can_manage_review_requests_for_team,
7+
from ietf.review.utils import (can_manage_review_requests_for_team, close_review_request_states,
88
extract_revision_ordered_review_requests_for_documents,
99
assign_review_request_to_reviewer,
10-
# email_about_review_request, make_new_review_request_from_existing,
10+
close_review_request,
11+
# email_review_request_change, make_new_review_request_from_existing,
1112
suggested_review_requests_for_team)
1213
from ietf.group.utils import get_group_or_404
1314
from ietf.person.fields import PersonEmailChoiceField
@@ -21,14 +22,7 @@ class ManageReviewRequestForm(forms.Form):
2122

2223
action = forms.ChoiceField(choices=ACTIONS, widget=forms.HiddenInput, required=False)
2324

24-
CLOSE_OPTIONS = [
25-
("noreviewversion", "No review of this version"),
26-
("noreviewdocument", "No review of document"),
27-
("withdraw", "Withdraw request"),
28-
("no-response", "No response"),
29-
("overtaken", "Overtaken by events"),
30-
]
31-
close = forms.ChoiceField(choices=CLOSE_OPTIONS, required=False)
25+
close = forms.ModelChoiceField(queryset=close_review_request_states(), required=False)
3226

3327
reviewer = PersonEmailChoiceField(empty_label="(None)", required=False, label_with="person")
3428

@@ -44,9 +38,9 @@ def __init__(self, review_req, *args, **kwargs):
4438
close_initial = None
4539
if review_req.pk is None:
4640
if review_req.latest_reqs:
47-
close_initial = "noreviewversion"
41+
close_initial = "no-review-version"
4842
else:
49-
close_initial = "noreviewdocument"
43+
close_initial = "no-review-document"
5044
elif review_req.reviewer:
5145
close_initial = "no-response"
5246
else:
@@ -55,6 +49,9 @@ def __init__(self, review_req, *args, **kwargs):
5549
if close_initial:
5650
self.fields["close"].initial = close_initial
5751

52+
if review_req.pk is None:
53+
self.fields["close"].queryset = self.fields["close"].queryset.filter(slug__in=["noreviewversion", "noreviewdocument"])
54+
5855
self.fields["close"].widget.attrs["class"] = "form-control input-sm"
5956

6057
self.fields["reviewer"].queryset = self.fields["reviewer"].queryset.filter(
@@ -115,18 +112,13 @@ def manage_review_requests(request, acronym, group_type=None):
115112
form_results.append(req.form.is_valid())
116113

117114
if all(form_results):
118-
for req in review_requests:
119-
action = req.form.cleaned_data.get("action")
115+
for review_req in review_requests:
116+
action = review_req.form.cleaned_data.get("action")
120117
if action == "assign":
121-
assign_review_request_to_reviewer(request, req, req.form.cleaned_data["reviewer"])
118+
assign_review_request_to_reviewer(request, review_req, review_req.form.cleaned_data["reviewer"])
122119
elif action == "close":
123-
close_reason = req.form.cleaned_data["close"]
124-
if close_reason in ("withdraw", "no-response", "overtaken"):
125-
req.state = ReviewRequestStateName.objects.get(slug=close_reason, used=True)
126-
req.save()
127-
# FIXME: notify?
128-
else:
129-
FIXME
120+
close_review_request(request, review_req, review_req.form.cleaned_data["close"])
121+
130122

131123
kwargs = { "acronym": group.acronym }
132124
if group_type:

ietf/name/migrations/0012_insert_review_name_data.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ def insert_initial_review_data(apps, schema_editor):
1212
ReviewRequestStateName.objects.get_or_create(slug="withdrawn", name="Withdrawn", order=4)
1313
ReviewRequestStateName.objects.get_or_create(slug="overtaken", name="Overtaken By Events", order=5)
1414
ReviewRequestStateName.objects.get_or_create(slug="no-response", name="No Response", order=6)
15-
ReviewRequestStateName.objects.get_or_create(slug="part-completed", name="Partially Completed", order=7)
16-
ReviewRequestStateName.objects.get_or_create(slug="completed", name="Completed", order=8)
15+
ReviewRequestStateName.objects.get_or_create(slug="no-review-version", name="No Review of Version", order=7)
16+
ReviewRequestStateName.objects.get_or_create(slug="no-review-document", name="No Review of Document", order=8)
17+
ReviewRequestStateName.objects.get_or_create(slug="part-completed", name="Partially Completed", order=9)
18+
ReviewRequestStateName.objects.get_or_create(slug="completed", name="Completed", order=10)
1719

1820
ReviewTypeName = apps.get_model("name", "ReviewTypeName")
1921
ReviewTypeName.objects.get_or_create(slug="early", name="Early", order=1)

ietf/name/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class LiaisonStatementTagName(NameModel):
8989
"Action Required, Action Taken"
9090
class ReviewRequestStateName(NameModel):
9191
"""Requested, Accepted, Rejected, Withdrawn, Overtaken By Events,
92-
No Response, Partially Completed, Completed"""
92+
No Response, No Review of Version, No Review of Document, Partially Completed, Completed"""
9393
class ReviewTypeName(NameModel):
9494
"""Early Review, Last Call, Telechat"""
9595
class ReviewResultName(NameModel):

ietf/review/import_from_review_tool.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ def parse_timestamp(t):
161161
doc_metadata[(row.docname, row.version)] = doc_metadata[row.docname] = (parse_timestamp(row.deadline), parse_timestamp(row.telechat), parse_timestamp(row.lcend), row.status)
162162

163163

164+
system_person = Person.objects.get(name="(System)")
165+
164166
with db_con.cursor() as c:
165167
c.execute("select * from reviews order by reviewid;")
166168

@@ -207,6 +209,7 @@ def parse_timestamp(t):
207209
"state": states["requested"],
208210
"type": type_name,
209211
"deadline": deadline,
212+
"requested_by": system_person,
210213
}
211214
)
212215

ietf/review/migrations/0001_initial.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class Migration(migrations.Migration):
4242
('result', models.ForeignKey(blank=True, to='name.ReviewResultName', null=True)),
4343
('review', models.OneToOneField(null=True, blank=True, to='doc.Document')),
4444
('reviewer', models.ForeignKey(blank=True, to='person.Email', null=True)),
45+
('requested_by', models.ForeignKey(to='person.Person')),
4546
('state', models.ForeignKey(to='name.ReviewRequestStateName')),
4647
('team', models.ForeignKey(to='group.Group')),
4748
('type', models.ForeignKey(to='name.ReviewTypeName')),

ietf/review/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class ReviewRequest(models.Model):
4242
doc = models.ForeignKey(Document, related_name='review_request_set')
4343
team = models.ForeignKey(Group, limit_choices_to=~models.Q(reviewteamresult=None))
4444
deadline = models.DateTimeField()
45+
requested_by = models.ForeignKey(Person)
4546
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")
4647

4748
# Fields filled in as reviewer is assigned and as the review is

0 commit comments

Comments
 (0)