Skip to content

Commit 64a6534

Browse files
committed
Add review tracking models, add a request review page (with test), show
review requests on doc page - Legacy-Id: 11206
1 parent 54c4c5e commit 64a6534

21 files changed

Lines changed: 753 additions & 8 deletions

ietf/doc/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,10 @@ class DocReminder(models.Model):
703703

704704
# RFC Editor
705705
("rfc_editor_received_announcement", "Announcement was received by RFC Editor"),
706-
("requested_publication", "Publication at RFC Editor requested")
706+
("requested_publication", "Publication at RFC Editor requested"),
707+
708+
# review
709+
("requested_review", "Requested review"),
707710
]
708711

709712
class DocEvent(models.Model):

ietf/doc/tests_review.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import datetime
4+
from pyquery import PyQuery
5+
6+
from django.core.urlresolvers import reverse as urlreverse
7+
8+
import debug # pyflakes:ignore
9+
10+
from ietf.review.models import ReviewRequest
11+
from ietf.person.models import Person
12+
from ietf.group.models import Group, Role
13+
from ietf.name.models import ReviewResultName
14+
from ietf.utils.test_utils import TestCase
15+
from ietf.utils.test_data import make_test_data
16+
from ietf.utils.test_utils import login_testing_unauthorized
17+
18+
def make_review_data():
19+
review_team = Group.objects.create(state_id="active", acronym="reviewteam", name="Review Team", type_id="team")
20+
review_team.reviewresultname_set.add(ReviewResultName.objects.filter(slug__in=["issues", "ready-issues", "ready", "not-ready"]))
21+
22+
p = Person.objects.get(user__username="plain")
23+
Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=review_team)
24+
25+
return review_team
26+
27+
class ReviewTests(TestCase):
28+
def test_request_review(self):
29+
doc = make_test_data()
30+
review_team = make_review_data()
31+
32+
url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name })
33+
login_testing_unauthorized(self, "secretary", url)
34+
35+
# get
36+
r = self.client.get(url)
37+
self.assertEqual(r.status_code, 200)
38+
39+
deadline_date = datetime.date.today() + datetime.timedelta(days=10)
40+
41+
# post request
42+
r = self.client.post(url, {
43+
"type": "early",
44+
"team": review_team.pk,
45+
"deadline_date": deadline_date.isoformat(),
46+
"requested_rev": "01"
47+
})
48+
self.assertEqual(r.status_code, 302)
49+
50+
req = ReviewRequest.objects.get(doc=doc)
51+
self.assertEqual(req.deadline.date(), deadline_date)
52+
self.assertEqual(req.deadline.time(), datetime.time(23, 59, 59))
53+
self.assertEqual(req.state_id, "requested")
54+
self.assertEqual(req.team, review_team)
55+
self.assertEqual(req.requested_rev, "01")
56+
self.assertEqual(doc.latest_event().type, "requested_review")
57+
58+
def test_request_review_by_reviewer(self):
59+
doc = make_test_data()
60+
review_team = make_review_data()
61+
62+
url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name })
63+
login_testing_unauthorized(self, "plain", url)
64+
65+
# post request
66+
deadline_date = datetime.date.today() + datetime.timedelta(days=10)
67+
68+
r = self.client.post(url, {
69+
"type": "early",
70+
"team": review_team.pk,
71+
"deadline_date": deadline_date.isoformat(),
72+
"requested_rev": "01"
73+
})
74+
self.assertEqual(r.status_code, 302)
75+
76+
req = ReviewRequest.objects.get(doc=doc)
77+
self.assertEqual(req.state_id, "requested")
78+
self.assertEqual(req.team, review_team)
79+
80+
def test_doc_page(self):
81+
pass
82+

ietf/doc/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from ietf.doc import views_search, views_draft, views_ballot
3737
from ietf.doc import views_status_change
3838
from ietf.doc import views_doc
39+
from ietf.doc import views_review
3940

4041
session_patterns = [
4142
url(r'^add$', views_doc.add_sessionpresentation),
@@ -73,6 +74,8 @@
7374
url(r'^(?P<name>[A-Za-z0-9._+-]+)/ballot/$', views_doc.document_ballot, name="doc_ballot"),
7475
(r'^(?P<name>[A-Za-z0-9._+-]+)/(?:(?P<rev>[0-9-]+)/)?doc.json$', views_doc.document_json),
7576
(r'^(?P<name>[A-Za-z0-9._+-]+)/ballotpopup/(?P<ballot_id>[0-9]+)/$', views_doc.ballot_popup),
77+
url(r'^(?P<name>[A-Za-z0-9._+-]+)/requestreview/$', views_review.request_review),
78+
url(r'^(?P<name>[A-Za-z0-9._+-]+)/review/(?P<request_id>[0-9]+)/$', views_review.review),
7679

7780
url(r'^(?P<name>[A-Za-z0-9._+-]+)/email-aliases/$', RedirectView.as_view(pattern_name='doc_email', permanent=False),name='doc_specific_email_aliases'),
7881

ietf/doc/utils.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ietf.doc.models import save_document_in_history
1717
from ietf.name.models import DocReminderTypeName, DocRelationshipName
1818
from ietf.group.models import Role
19-
from ietf.ietfauth.utils import has_role
19+
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream
2020
from ietf.utils import draft, markup_txt
2121
from ietf.utils.mail import send_mail
2222
from ietf.mailtrigger.utils import gather_address_lists
@@ -89,6 +89,15 @@ 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)
92101

93102
def two_thirds_rule( recused=0 ):
94103
# For standards-track, need positions from 2/3 of the non-recused current IESG.

ietf/doc/views_doc.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
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)
51+
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus,
52+
can_request_review_of_doc )
5253
from ietf.community.utils import augment_docs_with_tracking_info
5354
from ietf.group.models import Role
5455
from ietf.group.utils import can_manage_group, can_manage_materials
@@ -57,10 +58,11 @@
5758
from ietf.person.models import Email
5859
from ietf.utils.history import find_history_active_at
5960
from ietf.doc.forms import TelechatForm, NotifyForm
60-
from ietf.doc.mails import email_comment
61+
from ietf.doc.mails import email_comment
6162
from ietf.mailtrigger.utils import gather_relevant_expansions
6263
from ietf.meeting.models import Session
6364
from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions
65+
from ietf.review.models import ReviewRequest
6466

6567
def render_document_top(request, doc, tab, name):
6668
tabs = []
@@ -279,8 +281,8 @@ def document_main(request, name, rev=None):
279281
can_edit_stream_info = is_authorized_in_doc_stream(request.user, doc)
280282
can_edit_shepherd_writeup = can_edit_stream_info or user_is_person(request.user, doc.shepherd and doc.shepherd.person) or has_role(request.user, ["Area Director"])
281283
can_edit_notify = can_edit_shepherd_writeup
282-
can_edit_consensus = False
283284

285+
can_edit_consensus = False
284286
consensus = nice_consensus(default_consensus(doc))
285287
if doc.stream_id == "ietf" and iesg_state:
286288
show_in_states = set(IESG_BALLOT_ACTIVE_STATES)
@@ -294,6 +296,8 @@ def document_main(request, name, rev=None):
294296
e = doc.latest_event(ConsensusDocEvent, type="changed_consensus")
295297
consensus = nice_consensus(e and e.consensus)
296298

299+
can_request_review = can_request_review_of_doc(request.user, doc)
300+
297301
# mailing list search archive
298302
search_archive = "www.ietf.org/mail-archive/web/"
299303
if doc.stream_id == "ietf" and group.type_id == "wg" and group.list_archive:
@@ -353,6 +357,8 @@ def document_main(request, name, rev=None):
353357
published = doc.latest_event(type="published_rfc")
354358
started_iesg_process = doc.latest_event(type="started_iesg_process")
355359

360+
review_requests = ReviewRequest.objects.filter(doc=doc)
361+
356362
return render_to_response("doc/document_draft.html",
357363
dict(doc=doc,
358364
group=group,
@@ -374,6 +380,7 @@ def document_main(request, name, rev=None):
374380
can_edit_consensus=can_edit_consensus,
375381
can_edit_replaces=can_edit_replaces,
376382
can_view_possibly_replaces=can_view_possibly_replaces,
383+
can_request_review=can_request_review,
377384

378385
rfc_number=rfc_number,
379386
draft_name=draft_name,
@@ -412,6 +419,7 @@ def document_main(request, name, rev=None):
412419
search_archive=search_archive,
413420
actions=actions,
414421
presentations=presentations,
422+
review_requests=review_requests,
415423
),
416424
context_instance=RequestContext(request))
417425

ietf/doc/views_review.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import datetime
2+
3+
from django.http import HttpResponseForbidden
4+
from django.shortcuts import render, get_object_or_404, redirect
5+
from django.core.urlresolvers import reverse as urlreverse
6+
from django import forms
7+
from django.contrib.auth.decorators import login_required
8+
9+
from ietf.doc.models import Document, NewRevisionDocEvent, DocEvent
10+
from ietf.doc.utils import can_request_review_of_doc
11+
from ietf.ietfauth.utils import is_authorized_in_doc_stream
12+
from ietf.review.models import ReviewRequest, ReviewRequestStateName
13+
from ietf.review.utils import active_review_teams
14+
from ietf.utils.fields import DatepickerDateField
15+
16+
17+
class RequestReviewForm(forms.ModelForm):
18+
deadline_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1", "start-date": "+0d" })
19+
deadline_time = forms.TimeField(widget=forms.TextInput(attrs={ 'placeholder': "HH:MM" }), help_text="If time is not specified, end of day is assumed", required=False)
20+
21+
class Meta:
22+
model = ReviewRequest
23+
fields = ('type', 'team', 'deadline', 'requested_rev')
24+
25+
def __init__(self, user, doc, *args, **kwargs):
26+
super(RequestReviewForm, self).__init__(*args, **kwargs)
27+
28+
self.doc = doc
29+
30+
self.fields['type'].widget = forms.RadioSelect(choices=[t for t in self.fields['type'].choices if t[0]])
31+
32+
f = self.fields["team"]
33+
f.queryset = active_review_teams()
34+
if not is_authorized_in_doc_stream(user, doc): # user is a reviewer
35+
f.queryset = f.queryset.filter(role__name="reviewer", role__person__user=user)
36+
if len(f.queryset) < 6:
37+
f.widget = forms.RadioSelect(choices=[t for t in f.choices if t[0]])
38+
39+
self.fields["deadline"].required = False
40+
self.fields["requested_rev"].label = "Document revision"
41+
42+
def clean_deadline_date(self):
43+
v = self.cleaned_data.get('deadline_date')
44+
if v < datetime.date.today():
45+
raise forms.ValidationError("Select a future date.")
46+
return v
47+
48+
def clean_requested_rev(self):
49+
rev = self.cleaned_data.get("requested_rev")
50+
if rev:
51+
rev = rev.rjust(2, "0")
52+
53+
if not NewRevisionDocEvent.objects.filter(doc=self.doc, rev=rev).exists():
54+
raise forms.ValidationError("Could not find revision '{}' of the document.".format(rev))
55+
56+
return rev
57+
58+
def clean(self):
59+
deadline_date = self.cleaned_data.get('deadline_date')
60+
deadline_time = self.cleaned_data.get('deadline_time', None)
61+
62+
if deadline_date:
63+
if deadline_time is None:
64+
deadline_time = datetime.time(23, 59, 59)
65+
66+
self.cleaned_data["deadline"] = datetime.datetime.combine(deadline_date, deadline_time)
67+
68+
return self.cleaned_data
69+
70+
@login_required
71+
def request_review(request, name):
72+
doc = get_object_or_404(Document, name=name)
73+
74+
if not can_request_review_of_doc(request.user, doc):
75+
return HttpResponseForbidden("You do not have permission to perform this action")
76+
77+
if request.method == "POST":
78+
form = RequestReviewForm(request.user, doc, request.POST)
79+
80+
if form.is_valid():
81+
review_req = form.save(commit=False)
82+
review_req.doc = doc
83+
review_req.state = ReviewRequestStateName.objects.get(slug="requested", used=True)
84+
review_req.save()
85+
86+
DocEvent.objects.create(
87+
type="requested_review",
88+
doc=doc,
89+
by=request.user.person,
90+
desc="{} review by {} requested".format(review_req.type.name, review_req.team.acronym.upper()),
91+
)
92+
93+
# FIXME: if I'm a reviewer, auto-assign to myself?
94+
return redirect('doc_view', name=doc.name)
95+
96+
else:
97+
form = RequestReviewForm(request.user, doc)
98+
99+
return render(request, 'doc/review/request_review.html', {
100+
'doc': doc,
101+
'form': form,
102+
})
103+
104+
def review(request, name, request_id):
105+
doc = get_object_or_404(Document, name=name)
106+
review_request = get_object_or_404(ReviewRequest, pk=request_id)
107+
108+
print doc, review_request

ietf/name/admin.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
DocRelationshipName, DocTypeName, DocTagName, StdLevelName, IntendedStdLevelName,
44
DocReminderTypeName, BallotPositionName, SessionStatusName, TimeSlotTypeName,
55
ConstraintName, NomineePositionStateName, FeedbackTypeName, DBTemplateTypeName,
6-
DraftSubmissionStateName, RoomResourceName)
6+
DraftSubmissionStateName, RoomResourceName,
7+
ReviewRequestStateName, ReviewTypeName, ReviewResultName)
78

89

910
class NameAdmin(admin.ModelAdmin):
@@ -35,3 +36,6 @@ class DocTypeNameAdmin(NameAdmin):
3536
admin.site.register(DBTemplateTypeName, NameAdmin)
3637
admin.site.register(DraftSubmissionStateName, NameAdmin)
3738
admin.site.register(RoomResourceName, NameAdmin)
39+
admin.site.register(ReviewRequestStateName, NameAdmin)
40+
admin.site.register(ReviewTypeName, NameAdmin)
41+
admin.site.register(ReviewResultName, NameAdmin)

0 commit comments

Comments
 (0)