Skip to content

Commit b5ef179

Browse files
committed
Add support for rejecting a review assignment
- Legacy-Id: 11225
1 parent 44e1353 commit b5ef179

10 files changed

Lines changed: 159 additions & 14 deletions

File tree

ietf/doc/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ class DocReminder(models.Model):
707707

708708
# review
709709
("requested_review", "Requested review"),
710-
("withdrew_review_request", "Withdrew review"),
710+
("changed_review_request", "Changed review request"),
711711
]
712712

713713
class DocEvent(models.Model):

ietf/doc/tests_review.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,63 @@ def test_withdraw_request(self):
108108
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
109109
review_req.save()
110110

111-
url = urlreverse('ietf.doc.views_review.withdraw_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
112-
login_testing_unauthorized(self, "secretary", url)
111+
withdraw_url = urlreverse('ietf.doc.views_review.withdraw_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
113112

114-
r = self.client.get(url)
113+
114+
# follow link
115+
req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
116+
self.client.login(username="secretary", password="secretary+password")
117+
r = self.client.get(req_url)
118+
self.assertEqual(r.status_code, 200)
119+
self.assertTrue(withdraw_url in unicontent(r))
120+
self.client.logout()
121+
122+
# get withdraw page
123+
login_testing_unauthorized(self, "secretary", withdraw_url)
124+
r = self.client.get(withdraw_url)
115125
self.assertEqual(r.status_code, 200)
116126

117127
# withdraw
118-
r = self.client.post(url, { "action": "withdraw" })
128+
r = self.client.post(withdraw_url, { "action": "withdraw" })
119129
self.assertEqual(r.status_code, 302)
120130

121131
review_req = reload_db_objects(review_req)
122132
self.assertEqual(review_req.state_id, "withdrawn")
123-
self.assertEqual(doc.latest_event().type, "withdrew_review_request")
133+
e = doc.latest_event()
134+
self.assertEqual(e.type, "changed_review_request")
135+
self.assertTrue("Withdrew" in e.desc)
136+
137+
def test_reject_request_assignment(self):
138+
doc = make_test_data()
139+
review_req = make_review_data(doc)
140+
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
141+
review_req.save()
142+
143+
reject_url = urlreverse('ietf.doc.views_review.reject_request_assignment', kwargs={ "name": doc.name, "request_id": review_req.pk })
144+
145+
146+
# follow link
147+
req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
148+
self.client.login(username="secretary", password="secretary+password")
149+
r = self.client.get(req_url)
150+
self.assertEqual(r.status_code, 200)
151+
self.assertTrue(reject_url in unicontent(r))
152+
self.client.logout()
153+
154+
# get reject page
155+
login_testing_unauthorized(self, "secretary", reject_url)
156+
r = self.client.get(reject_url)
157+
self.assertEqual(r.status_code, 200)
158+
self.assertTrue(unicode(review_req.reviewer.role.person) in unicontent(r))
159+
160+
# reject
161+
r = self.client.post(reject_url, { "action": "reject" })
162+
self.assertEqual(r.status_code, 302)
163+
164+
review_req = reload_db_objects(review_req)
165+
self.assertEqual(review_req.state_id, "rejected")
166+
e = doc.latest_event()
167+
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)
170+

ietf/doc/urls_review.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
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),
89
)
910

ietf/doc/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ def can_request_review_of_doc(user, doc):
9999

100100
return is_authorized_in_doc_stream(user, doc)
101101

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+
102108
def two_thirds_rule( recused=0 ):
103109
# For standards-track, need positions from 2/3 of the non-recused current IESG.
104110
active = Role.objects.filter(name="ad",group__type="area",group__state="active").count()

ietf/doc/views_doc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def document_main(request, name, rev=None):
357357
published = doc.latest_event(type="published_rfc")
358358
started_iesg_process = doc.latest_event(type="started_iesg_process")
359359

360-
review_requests = ReviewRequest.objects.filter(doc=doc)
360+
review_requests = ReviewRequest.objects.filter(doc=doc).exclude(state__in=["withdrawn", "rejected"])
361361

362362
return render_to_response("doc/document_draft.html",
363363
dict(doc=doc,

ietf/doc/views_review.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
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
10-
from ietf.ietfauth.utils import is_authorized_in_doc_stream
9+
from ietf.doc.utils import can_request_review_of_doc, can_manage_review_requests_for_team
10+
from ietf.ietfauth.utils import is_authorized_in_doc_stream, user_is_person
1111
from ietf.review.models import ReviewRequest, ReviewRequestStateName
1212
from ietf.review.utils import active_review_teams
1313
from ietf.utils.fields import DatepickerDateField
@@ -104,10 +104,21 @@ def review_request(request, name, request_id):
104104
doc = get_object_or_404(Document, name=name)
105105
review_req = get_object_or_404(ReviewRequest, pk=request_id)
106106

107+
is_reviewer = review_req.reviewer and user_is_person(request.user, review_req.reviewer.role.person)
108+
can_manage_req = can_manage_review_requests_for_team(request.user, review_req.team)
109+
110+
can_withdraw_request = (review_req.state_id in ["requested", "accepted"]
111+
and is_authorized_in_doc_stream(request.user, doc))
112+
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))
116+
107117
return render(request, 'doc/review/review_request.html', {
108118
'doc': doc,
109119
'review_req': review_req,
110-
'can_withdraw_request': review_req.state_id in ["requested", "accepted"] and is_authorized_in_doc_stream(request.user, doc),
120+
'can_withdraw_request': can_withdraw_request,
121+
'can_reject_request_assignment': can_reject_request_assignment,
111122
})
112123

113124
def withdraw_request(request, name, request_id):
@@ -122,7 +133,7 @@ def withdraw_request(request, name, request_id):
122133
review_req.save()
123134

124135
DocEvent.objects.create(
125-
type="withdrew_review_request",
136+
type="changed_review_request",
126137
doc=doc,
127138
by=request.user.person,
128139
desc="Withdrew request for {} review by {}".format(review_req.type.name, review_req.team.acronym.upper()),
@@ -138,3 +149,53 @@ def withdraw_request(request, name, request_id):
138149
'doc': doc,
139150
'review_req': review_req,
140151
})
152+
153+
class RejectRequestAssignmentForm(forms.Form):
154+
message_to_secretary = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional explanation of rejection, will be emailed to team secretary")
155+
156+
def reject_request_assignment(request, name, request_id):
157+
doc = get_object_or_404(Document, name=name)
158+
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
159+
160+
if not review_req.reviewer:
161+
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
162+
163+
is_reviewer = user_is_person(request.user, review_req.reviewer.role.person)
164+
can_manage_req = can_manage_review_requests_for_team(request.user, review_req.team)
165+
166+
if not (is_reviewer or can_manage_req):
167+
return HttpResponseForbidden("You do not have permission to perform this action")
168+
169+
if request.method == "POST" and request.POST.get("action") == "reject":
170+
# reject the old request
171+
prev_state = review_req.state
172+
review_req.state = ReviewRequestStateName.objects.get(slug="rejected")
173+
review_req.save()
174+
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(
187+
time=review_req.time,
188+
type=review_req.type,
189+
doc=review_req.doc,
190+
team=review_req.team,
191+
deadline=review_req.deadline,
192+
requested_rev=review_req.requested_rev,
193+
state=prev_state,
194+
)
195+
196+
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
197+
198+
return render(request, 'doc/review/reject_request_assignment.html', {
199+
'doc': doc,
200+
'review_req': review_req,
201+
})

ietf/name/migrations/0012_insert_review_name_data.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ 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="noresponse", name="No Response", order=6)
15-
ReviewRequestStateName.objects.get_or_create(slug="completed", name="Completed", order=7)
15+
ReviewRequestStateName.objects.get_or_create(slug="part-completed", name="Partially Completed", order=6)
16+
ReviewRequestStateName.objects.get_or_create(slug="completed", name="Completed", order=8)
17+
1618

1719
ReviewTypeName = apps.get_model("name", "ReviewTypeName")
1820
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, Completed"""
92+
No Response, Partially Completed, Completed"""
9393
class ReviewTypeName(NameModel):
9494
"""Early Review, Last Call, Telechat"""
9595
class ReviewResultName(NameModel):
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{% extends "base.html" %}
2+
{# Copyright The IETF Trust 2016, All Rights Reserved #}
3+
{% load origin bootstrap3 static %}
4+
5+
{% block title %}Reject review assignment for {{ review_req.doc.name }}{% endblock %}
6+
7+
{% block content %}
8+
{% origin %}
9+
<h1>Reject review assignment<br><small>{{ review_req.doc.name }}</small></h1>
10+
11+
<p>{{ review_req.reviewer.role.person }} is currently assigned to do the review. Do you want to reject this assignment?</p>
12+
13+
<form method="post">
14+
{% csrf_token %}
15+
16+
{% buttons %}
17+
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=doc.canonical_name request_id=review_req.pk %}">Cancel</a>
18+
<button type="submit" class="btn btn-primary" name="action" value="reject">Reject assignment</button>
19+
{% endbuttons %}
20+
</form>
21+
22+
{% endblock %}

ietf/templates/doc/review/review_request.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,13 @@ <h1>Review request<br><small>{{ review_req.doc.name }}</small></h1>
6464
<tr>
6565
<th></th>
6666
<th>Reviewer</th>
67-
<td>{{ review_req.reviewer.role.person }}</td>
67+
<td>
68+
{{ review_req.reviewer.role.person }}
69+
70+
{% if can_reject_request_assignment %}
71+
<a class="btn btn-default btn-xs" href="{% url "ietf.doc.views_review.reject_request_assignment" name=doc.name request_id=review_req.pk %}"><span class="fa fa-ban"></span> Reject request assignment</a>
72+
{% endif %}
73+
</td>
6874
</tr>
6975
{% endif %}
7076

0 commit comments

Comments
 (0)