Skip to content

Commit 5dd079e

Browse files
committed
Add support for assigning a reviewer to a review request, still some
corners missing. Fix a couple of other issues. - Legacy-Id: 11227
1 parent b5ef179 commit 5dd079e

11 files changed

Lines changed: 246 additions & 107 deletions

File tree

ietf/doc/tests_review.py

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,30 @@
1313
from ietf.utils.test_utils import TestCase
1414
from ietf.utils.test_data import make_test_data
1515
from ietf.utils.test_utils import login_testing_unauthorized, unicontent, reload_db_objects
16+
from ietf.utils.mail import outbox, empty_outbox
17+
1618

1719
def make_review_data(doc):
1820
team = Group.objects.create(state_id="active", acronym="reviewteam", name="Review Team", type_id="team")
1921
team.reviewresultname_set.add(ReviewResultName.objects.filter(slug__in=["issues", "ready-issues", "ready", "not-ready"]))
2022

2123
p = Person.objects.get(user__username="plain")
2224
role = Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=team)
23-
reviewer = Reviewer.objects.create(role=role, frequency=14, skip_next=0)
25+
Reviewer.objects.create(team=team, person=p, frequency=14, skip_next=0)
2426

2527
review_req = ReviewRequest.objects.create(
2628
doc=doc,
2729
team=team,
2830
type_id="early",
2931
deadline=datetime.datetime.now() + datetime.timedelta(days=20),
3032
state_id="ready",
31-
reviewer=reviewer,
33+
reviewer=role,
3234
reviewed_rev="01",
3335
)
3436

37+
p = Person.objects.get(user__username="marschairman")
38+
role = Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=team)
39+
3540
return review_req
3641

3742
class ReviewTests(TestCase):
@@ -65,29 +70,6 @@ def test_request_review(self):
6570
self.assertEqual(req.requested_rev, "01")
6671
self.assertEqual(doc.latest_event().type, "requested_review")
6772

68-
def test_request_review_by_reviewer(self):
69-
doc = make_test_data()
70-
review_req = make_review_data(doc)
71-
review_team = review_req.team
72-
73-
url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name })
74-
login_testing_unauthorized(self, "plain", url)
75-
76-
# post request
77-
deadline_date = datetime.date.today() + datetime.timedelta(days=10)
78-
79-
r = self.client.post(url, {
80-
"type": "early",
81-
"team": review_team.pk,
82-
"deadline_date": deadline_date.isoformat(),
83-
"requested_rev": "01"
84-
})
85-
self.assertEqual(r.status_code, 302)
86-
87-
req = ReviewRequest.objects.get(doc=doc, state="requested")
88-
self.assertEqual(req.state_id, "requested")
89-
self.assertEqual(req.team, review_team)
90-
9173
def test_doc_page(self):
9274
# FIXME: fill in
9375
pass
@@ -134,13 +116,56 @@ def test_withdraw_request(self):
134116
self.assertEqual(e.type, "changed_review_request")
135117
self.assertTrue("Withdrew" in e.desc)
136118

137-
def test_reject_request_assignment(self):
119+
def test_assign_reviewer(self):
120+
doc = make_test_data()
121+
review_req = make_review_data(doc)
122+
review_req.state = ReviewRequestStateName.objects.get(slug="requested")
123+
review_req.reviewer = None
124+
review_req.save()
125+
126+
assign_url = urlreverse('ietf.doc.views_review.assign_reviewer', kwargs={ "name": doc.name, "request_id": review_req.pk })
127+
128+
129+
# follow link
130+
req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
131+
self.client.login(username="secretary", password="secretary+password")
132+
r = self.client.get(req_url)
133+
self.assertEqual(r.status_code, 200)
134+
self.assertTrue(assign_url in unicontent(r))
135+
self.client.logout()
136+
137+
# get assign page
138+
login_testing_unauthorized(self, "secretary", assign_url)
139+
r = self.client.get(assign_url)
140+
self.assertEqual(r.status_code, 200)
141+
142+
# assign
143+
reviewer = Role.objects.filter(name="reviewer", group=review_req.team).first()
144+
r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk })
145+
self.assertEqual(r.status_code, 302)
146+
147+
review_req = reload_db_objects(review_req)
148+
self.assertEqual(review_req.state_id, "requested")
149+
self.assertEqual(review_req.reviewer, reviewer)
150+
151+
# re-assign
152+
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
153+
review_req.save()
154+
reviewer = Role.objects.filter(name="reviewer", group=review_req.team).exclude(pk=reviewer.pk).first()
155+
r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk })
156+
self.assertEqual(r.status_code, 302)
157+
158+
review_req = reload_db_objects(review_req)
159+
self.assertEqual(review_req.state_id, "requested")
160+
self.assertEqual(review_req.reviewer, reviewer)
161+
162+
def test_reject_reviewer_assignment(self):
138163
doc = make_test_data()
139164
review_req = make_review_data(doc)
140165
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
141166
review_req.save()
142167

143-
reject_url = urlreverse('ietf.doc.views_review.reject_request_assignment', kwargs={ "name": doc.name, "request_id": review_req.pk })
168+
reject_url = urlreverse('ietf.doc.views_review.reject_reviewer_assignment', kwargs={ "name": doc.name, "request_id": review_req.pk })
144169

145170

146171
# follow link
@@ -155,16 +180,18 @@ def test_reject_request_assignment(self):
155180
login_testing_unauthorized(self, "secretary", reject_url)
156181
r = self.client.get(reject_url)
157182
self.assertEqual(r.status_code, 200)
158-
self.assertTrue(unicode(review_req.reviewer.role.person) in unicontent(r))
183+
self.assertTrue(unicode(review_req.reviewer.person) in unicontent(r))
159184

160185
# reject
161-
r = self.client.post(reject_url, { "action": "reject" })
186+
empty_outbox()
187+
r = self.client.post(reject_url, { "action": "reject", "message_to_secretary": "Test message" })
162188
self.assertEqual(r.status_code, 302)
163189

164190
review_req = reload_db_objects(review_req)
165191
self.assertEqual(review_req.state_id, "rejected")
166192
e = doc.latest_event()
167193
self.assertEqual(e.type, "changed_review_request")
168-
self.assertTrue("unassigned" in e.desc)
169-
self.assertEqual(ReviewRequest.objects.filter(doc=review_req.doc, team=review_req.team, state="accepted").count(), 1)
194+
self.assertTrue("rejected" in e.desc)
195+
self.assertEqual(ReviewRequest.objects.filter(doc=review_req.doc, team=review_req.team, state="requested").count(), 1)
196+
self.assertEqual(len(outbox), 1)
170197

ietf/doc/urls_review.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
url(r'^$', views_review.request_review),
66
url(r'^(?P<request_id>[0-9]+)/$', views_review.review_request),
77
url(r'^(?P<request_id>[0-9]+)/withdraw/$', views_review.withdraw_request),
8-
url(r'^(?P<request_id>[0-9]+)/rejectassignment/$', views_review.reject_request_assignment),
8+
url(r'^(?P<request_id>[0-9]+)/assignreviewer/$', views_review.assign_reviewer),
9+
url(r'^(?P<request_id>[0-9]+)/rejectreviewerassignment/$', views_review.reject_reviewer_assignment),
910
)
1011

ietf/doc/utils.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -89,22 +89,6 @@ def can_adopt_draft(user, doc):
8989
group__state="active",
9090
person__user=user).exists())
9191

92-
def can_request_review_of_doc(user, doc):
93-
if not user.is_authenticated():
94-
return False
95-
96-
from ietf.review.utils import active_review_teams
97-
if Role.objects.filter(name="reviewer", person__user=user, group__in=active_review_teams()):
98-
return True
99-
100-
return is_authorized_in_doc_stream(user, doc)
101-
102-
def can_manage_review_requests_for_team(user, team):
103-
if not user.is_authenticated():
104-
return False
105-
106-
return Role.objects.filter(name="secretary", person__user=user, group=team).exists() or has_role(user, "Secretariat")
107-
10892
def two_thirds_rule( recused=0 ):
10993
# For standards-track, need positions from 2/3 of the non-recused current IESG.
11094
active = Role.objects.filter(name="ad",group__type="area",group__state="active").count()

ietf/doc/views_doc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@
4848
from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_with_revision,
4949
can_adopt_draft, get_chartering_type, get_document_content, get_tags_for_stream_id,
5050
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
51-
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus,
52-
can_request_review_of_doc )
51+
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus )
5352
from ietf.community.utils import augment_docs_with_tracking_info
5453
from ietf.group.models import Role
5554
from ietf.group.utils import can_manage_group, can_manage_materials
@@ -63,6 +62,7 @@
6362
from ietf.meeting.models import Session
6463
from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions
6564
from ietf.review.models import ReviewRequest
65+
from ietf.review.utils import can_request_review_of_doc
6666

6767
def render_document_top(request, doc, tab, name):
6868
tabs = []

ietf/doc/views_review.py

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
from django.contrib.auth.decorators import login_required
77

88
from ietf.doc.models import Document, NewRevisionDocEvent, DocEvent
9-
from ietf.doc.utils import can_request_review_of_doc, can_manage_review_requests_for_team
109
from ietf.ietfauth.utils import is_authorized_in_doc_stream, user_is_person
11-
from ietf.review.models import ReviewRequest, ReviewRequestStateName
12-
from ietf.review.utils import active_review_teams
10+
from ietf.name.models import ReviewRequestStateName
11+
from ietf.group.models import Role
12+
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
1315
from ietf.utils.fields import DatepickerDateField
1416

1517
class RequestReviewForm(forms.ModelForm):
@@ -89,7 +91,6 @@ def request_review(request, name):
8991
time=review_req.time,
9092
)
9193

92-
# FIXME: if I'm a reviewer, auto-assign to myself?
9394
return redirect('doc_view', name=doc.name)
9495

9596
else:
@@ -104,21 +105,25 @@ def review_request(request, name, request_id):
104105
doc = get_object_or_404(Document, name=name)
105106
review_req = get_object_or_404(ReviewRequest, pk=request_id)
106107

107-
is_reviewer = review_req.reviewer and user_is_person(request.user, review_req.reviewer.role.person)
108+
is_reviewer = review_req.reviewer and user_is_person(request.user, review_req.reviewer.person)
108109
can_manage_req = can_manage_review_requests_for_team(request.user, review_req.team)
109110

110111
can_withdraw_request = (review_req.state_id in ["requested", "accepted"]
111112
and is_authorized_in_doc_stream(request.user, doc))
112113

113-
can_reject_request_assignment = (review_req.state_id in ["requested", "accepted"]
114-
and review_req.reviewer_id is not None
115-
and (is_reviewer or can_manage_req))
114+
can_assign_reviewer = (review_req.state_id in ["requested", "accepted"]
115+
and is_authorized_in_doc_stream(request.user, doc))
116+
117+
can_reject_reviewer_assignment = (review_req.state_id in ["requested", "accepted"]
118+
and review_req.reviewer_id is not None
119+
and (is_reviewer or can_manage_req))
116120

117121
return render(request, 'doc/review/review_request.html', {
118122
'doc': doc,
119123
'review_req': review_req,
120124
'can_withdraw_request': can_withdraw_request,
121-
'can_reject_request_assignment': can_reject_request_assignment,
125+
'can_reject_reviewer_assignment': can_reject_reviewer_assignment,
126+
'can_assign_reviewer': can_assign_reviewer,
122127
})
123128

124129
def withdraw_request(request, name, request_id):
@@ -150,52 +155,96 @@ def withdraw_request(request, name, request_id):
150155
'review_req': review_req,
151156
})
152157

153-
class RejectRequestAssignmentForm(forms.Form):
158+
class PersonEmailLabeledRoleModelChoiceField(forms.ModelChoiceField):
159+
def __init__(self, *args, **kwargs):
160+
if not "queryset" in kwargs:
161+
kwargs["queryset"] = Role.objects.select_related("person", "email")
162+
super(PersonEmailLabeledRoleModelChoiceField, self).__init__(*args, **kwargs)
163+
164+
def label_from_instance(self, role):
165+
return u"{} <{}>".format(role.person.name, role.email.address)
166+
167+
class AssignReviewerForm(forms.Form):
168+
reviewer = PersonEmailLabeledRoleModelChoiceField(widget=forms.RadioSelect, empty_label="(None)", required=False)
169+
170+
def __init__(self, review_req, *args, **kwargs):
171+
super(AssignReviewerForm, self).__init__(*args, **kwargs)
172+
f = self.fields["reviewer"]
173+
f.queryset = f.queryset.filter(name="reviewer", group=review_req.team)
174+
if review_req.reviewer:
175+
f.initial = review_req.reviewer_id
176+
177+
def assign_reviewer(request, name, request_id):
178+
doc = get_object_or_404(Document, name=name)
179+
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
180+
181+
can_manage_req = can_manage_review_requests_for_team(request.user, review_req.team)
182+
183+
if not can_manage_req:
184+
return HttpResponseForbidden("You do not have permission to perform this action")
185+
186+
if request.method == "POST" and request.POST.get("action") == "assign":
187+
form = AssignReviewerForm(review_req, request.POST)
188+
if form.is_valid():
189+
reviewer = form.cleaned_data["reviewer"]
190+
assign_review_request_to_reviewer(review_req, reviewer, request.user.person)
191+
192+
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
193+
else:
194+
form = AssignReviewerForm(review_req)
195+
196+
return render(request, 'doc/review/assign_reviewer.html', {
197+
'doc': doc,
198+
'review_req': review_req,
199+
'form': form,
200+
})
201+
202+
class RejectReviewerAssignmentForm(forms.Form):
154203
message_to_secretary = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional explanation of rejection, will be emailed to team secretary")
155204

156-
def reject_request_assignment(request, name, request_id):
205+
def reject_reviewer_assignment(request, name, request_id):
157206
doc = get_object_or_404(Document, name=name)
158207
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
159208

160209
if not review_req.reviewer:
161210
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
162211

163-
is_reviewer = user_is_person(request.user, review_req.reviewer.role.person)
212+
is_reviewer = user_is_person(request.user, review_req.reviewer.person)
164213
can_manage_req = can_manage_review_requests_for_team(request.user, review_req.team)
165214

166215
if not (is_reviewer or can_manage_req):
167216
return HttpResponseForbidden("You do not have permission to perform this action")
168217

169218
if request.method == "POST" and request.POST.get("action") == "reject":
170-
# reject the old request
171-
prev_state = review_req.state
219+
# reject the request
172220
review_req.state = ReviewRequestStateName.objects.get(slug="rejected")
173221
review_req.save()
174222

175-
# assignment of reviewer is currently considered minutia, so
176-
# not reported in the log
177-
if prev_state.slug == "accepted":
178-
DocEvent.objects.create(
179-
type="changed_review_request",
180-
doc=doc,
181-
by=request.user.person,
182-
desc="Request for {} review by {} is unassigned".format(review_req.type.name, review_req.team.acronym.upper()),
183-
)
184-
185-
# make a new, open review request
186-
ReviewRequest.objects.create(
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(
187236
time=review_req.time,
188237
type=review_req.type,
189238
doc=review_req.doc,
190239
team=review_req.team,
191240
deadline=review_req.deadline,
192241
requested_rev=review_req.requested_rev,
193-
state=prev_state,
242+
state=ReviewRequestStateName.objects.get(slug="requested"),
194243
)
195244

196-
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
245+
return redirect(review_request, name=new_review_req.doc.name, request_id=new_review_req.pk)
197246

198-
return render(request, 'doc/review/reject_request_assignment.html', {
247+
return render(request, 'doc/review/reject_reviewer_assignment.html', {
199248
'doc': doc,
200249
'review_req': review_req,
201250
})

ietf/review/migrations/0001_initial.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,20 @@ class Migration(migrations.Migration):
1010
('group', '0008_auto_20160505_0523'),
1111
('name', '0012_insert_review_name_data'),
1212
('doc', '0012_auto_20160207_0537'),
13+
('person', '0006_auto_20160503_0937'),
1314
]
1415

1516
operations = [
1617
migrations.CreateModel(
1718
name='Reviewer',
1819
fields=[
1920
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
20-
('frequency', models.IntegerField(help_text=b'Can review every N days')),
21-
('available', models.DateTimeField(help_text=b'When will this reviewer be available again', null=True, blank=True)),
21+
('frequency', models.IntegerField(default=30, help_text=b'Can review every N days')),
22+
('unavailable_until', models.DateTimeField(help_text=b'When will this reviewer be available again', null=True, blank=True)),
2223
('filter_re', models.CharField(max_length=255, blank=True)),
2324
('skip_next', models.IntegerField(help_text=b'Skip the next N review assignments')),
24-
('role', models.OneToOneField(to='group.Role')),
25+
('person', models.ForeignKey(to='person.Person')),
26+
('team', models.ForeignKey(to='group.Group')),
2527
],
2628
options={
2729
},
@@ -38,7 +40,7 @@ class Migration(migrations.Migration):
3840
('doc', models.ForeignKey(related_name='review_request_set', to='doc.Document')),
3941
('result', models.ForeignKey(blank=True, to='name.ReviewResultName', null=True)),
4042
('review', models.OneToOneField(null=True, blank=True, to='doc.Document')),
41-
('reviewer', models.ForeignKey(blank=True, to='review.Reviewer', null=True)),
43+
('reviewer', models.ForeignKey(blank=True, to='group.Role', null=True)),
4244
('state', models.ForeignKey(to='name.ReviewRequestStateName')),
4345
('team', models.ForeignKey(to='group.Group')),
4446
('type', models.ForeignKey(to='name.ReviewTypeName')),

0 commit comments

Comments
 (0)