Skip to content

Commit 4c7b284

Browse files
committed
Add a utility function for extracting information about review
requests for a given set of teams/reviewers (making it trivial to compute statistics), revamp the related doc event code to support this by referencing the review request directly, add a reviewer overview page with recent performance for each reviewer as well as settings/unavailable periods. Fix some bugs and shuffle some of the review code a bit around. Finish the importer from the previous Perl-based review tool, importing log entries, figuring out whether a given review is early/telechat/last call and fixing corner cases. - Legacy-Id: 12080
1 parent c586feb commit 4c7b284

30 files changed

Lines changed: 873 additions & 346 deletions

ietf/community/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ def notify_events(sender, instance, **kwargs):
9898
if instance.doc.type_id != 'draft':
9999
return
100100

101+
if getattr(instance, "skip_community_list_notification", False):
102+
return
103+
101104
from ietf.community.utils import notify_event_to_subscribers
102105
notify_event_to_subscribers(instance)
103106

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import models, migrations
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('name', '0015_insert_review_name_data'),
11+
('review', '0001_initial'),
12+
('doc', '0014_auto_20160824_2218'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='ReviewRequestDocEvent',
18+
fields=[
19+
('docevent_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='doc.DocEvent')),
20+
('review_request', models.ForeignKey(to='review.ReviewRequest')),
21+
('state', models.ForeignKey(blank=True, to='name.ReviewRequestStateName', null=True)),
22+
],
23+
options={
24+
},
25+
bases=('doc.docevent',),
26+
),
27+
migrations.AlterField(
28+
model_name='docevent',
29+
name='type',
30+
field=models.CharField(max_length=50, choices=[(b'new_revision', b'Added new revision'), (b'changed_document', b'Changed document metadata'), (b'added_comment', b'Added comment'), (b'deleted', b'Deleted document'), (b'changed_state', b'Changed state'), (b'changed_stream', b'Changed document stream'), (b'expired_document', b'Expired document'), (b'extended_expiry', b'Extended expiry of document'), (b'requested_resurrect', b'Requested resurrect'), (b'completed_resurrect', b'Completed resurrect'), (b'changed_consensus', b'Changed consensus'), (b'published_rfc', b'Published RFC'), (b'added_suggested_replaces', b'Added suggested replacement relationships'), (b'reviewed_suggested_replaces', b'Reviewed suggested replacement relationships'), (b'changed_group', b'Changed group'), (b'changed_protocol_writeup', b'Changed protocol writeup'), (b'changed_charter_milestone', b'Changed charter milestone'), (b'initial_review', b'Set initial review time'), (b'changed_review_announcement', b'Changed WG Review text'), (b'changed_action_announcement', b'Changed WG Action text'), (b'started_iesg_process', b'Started IESG process on document'), (b'created_ballot', b'Created ballot'), (b'closed_ballot', b'Closed ballot'), (b'sent_ballot_announcement', b'Sent ballot announcement'), (b'changed_ballot_position', b'Changed ballot position'), (b'changed_ballot_approval_text', b'Changed ballot approval text'), (b'changed_ballot_writeup_text', b'Changed ballot writeup text'), (b'changed_rfc_editor_note_text', b'Changed RFC Editor Note text'), (b'changed_last_call_text', b'Changed last call text'), (b'requested_last_call', b'Requested last call'), (b'sent_last_call', b'Sent last call'), (b'scheduled_for_telechat', b'Scheduled for telechat'), (b'iesg_approved', b'IESG approved document (no problem)'), (b'iesg_disapproved', b'IESG disapproved document (do not publish)'), (b'approved_in_minute', b'Approved in minute'), (b'iana_review', b'IANA review comment'), (b'rfc_in_iana_registry', b'RFC is in IANA registry'), (b'rfc_editor_received_announcement', b'Announcement was received by RFC Editor'), (b'requested_publication', b'Publication at RFC Editor requested'), (b'sync_from_rfc_editor', b'Received updated information from RFC Editor'), (b'requested_review', b'Requested review'), (b'assigned_review_request', b'Assigned review request'), (b'closed_review_request', b'Closed review request')]),
31+
preserve_default=True,
32+
),
33+
]

ietf/doc/models.py

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

1515
from ietf.group.models import Group
1616
from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdLevelName, StdLevelName,
17-
DocRelationshipName, DocReminderTypeName, BallotPositionName )
17+
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName )
1818
from ietf.person.models import Email, Person
1919
from ietf.utils.admin import admin_link
2020

@@ -685,7 +685,8 @@ class DocReminder(models.Model):
685685

686686
# review
687687
("requested_review", "Requested review"),
688-
("changed_review_request", "Changed review request"),
688+
("assigned_review_request", "Assigned review request"),
689+
("closed_review_request", "Closed review request"),
689690
]
690691

691692
class DocEvent(models.Model):
@@ -816,11 +817,14 @@ class TelechatDocEvent(DocEvent):
816817
telechat_date = models.DateField(blank=True, null=True)
817818
returning_item = models.BooleanField(default=False)
818819

820+
class ReviewRequestDocEvent(DocEvent):
821+
review_request = models.ForeignKey('review.ReviewRequest')
822+
state = models.ForeignKey(ReviewRequestStateName, blank=True, null=True)
823+
819824
# charter events
820825
class InitialReviewDocEvent(DocEvent):
821826
expires = models.DateTimeField(blank=True, null=True)
822827

823-
824828
# dumping store for removed events
825829
class DeletedEvent(models.Model):
826830
content_type = models.ForeignKey(ContentType)

ietf/doc/tests_review.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def test_close_request(self):
131131
review_req = reload_db_objects(review_req)
132132
self.assertEqual(review_req.state_id, "withdrawn")
133133
e = doc.latest_event()
134-
self.assertEqual(e.type, "changed_review_request")
134+
self.assertEqual(e.type, "closed_review_request")
135135
self.assertTrue("closed" in e.desc.lower())
136136
self.assertEqual(len(outbox), 1)
137137
self.assertTrue("closed" in unicode(outbox[0]).lower())
@@ -255,7 +255,7 @@ def test_assign_reviewer(self):
255255
reviewer=plain_email,
256256
)
257257

258-
reviewer_settings = ReviewerSettings.objects.get(person__email=plain_email)
258+
reviewer_settings = ReviewerSettings.objects.get(person__email=plain_email, team=review_req.team)
259259
reviewer_settings.filter_re = doc.name
260260
reviewer_settings.skip_next = 1
261261
reviewer_settings.save()
@@ -383,7 +383,7 @@ def test_reject_reviewer_assignment(self):
383383
review_req = reload_db_objects(review_req)
384384
self.assertEqual(review_req.state_id, "rejected")
385385
e = doc.latest_event()
386-
self.assertEqual(e.type, "changed_review_request")
386+
self.assertEqual(e.type, "closed_review_request")
387387
self.assertTrue("rejected" in e.desc)
388388
self.assertEqual(ReviewRequest.objects.filter(doc=review_req.doc, team=review_req.team, state="requested").count(), 1)
389389
self.assertEqual(len(outbox), 1)

ietf/doc/views_review.py

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from django.template.loader import render_to_string
1010
from django.core.urlresolvers import reverse as urlreverse
1111

12-
from ietf.doc.models import Document, NewRevisionDocEvent, DocEvent, State, DocAlias, LastCallDocEvent
12+
from ietf.doc.models import (Document, NewRevisionDocEvent, State, DocAlias,
13+
LastCallDocEvent, ReviewRequestDocEvent)
1314
from ietf.name.models import ReviewRequestStateName, ReviewResultName, DocTypeName
1415
from ietf.review.models import ReviewRequest
1516
from ietf.group.models import Group
@@ -103,12 +104,14 @@ def request_review(request, name):
103104
review_req.team = team
104105
review_req.save()
105106

106-
DocEvent.objects.create(
107+
ReviewRequestDocEvent.objects.create(
107108
type="requested_review",
108109
doc=doc,
109110
by=request.user.person,
110111
desc="Requested {} review by {}".format(review_req.type.name, review_req.team.acronym.upper()),
111112
time=review_req.time,
113+
review_request=review_req,
114+
state=None,
112115
)
113116

114117
return redirect('doc_view', name=doc.name)
@@ -272,22 +275,24 @@ def reject_reviewer_assignment(request, name, request_id):
272275
review_req.state = ReviewRequestStateName.objects.get(slug="rejected")
273276
review_req.save()
274277

275-
DocEvent.objects.create(
276-
type="changed_review_request",
278+
ReviewRequestDocEvent.objects.create(
279+
type="closed_review_request",
277280
doc=review_req.doc,
278281
by=request.user.person,
279282
desc="Assignment of request for {} review by {} to {} was rejected".format(
280283
review_req.type.name,
281284
review_req.team.acronym.upper(),
282285
review_req.reviewer.person,
283286
),
287+
review_request=review_req,
288+
state=review_req.state,
284289
)
285290

286291
# make a new unassigned review request
287292
new_review_req = make_new_review_request_from_existing(review_req)
288293
new_review_req.save()
289294

290-
msg = render_to_string("doc/mail/reviewer_assignment_rejected.txt", {
295+
msg = render_to_string("review/reviewer_assignment_rejected.txt", {
291296
"by": request.user.person,
292297
"message_to_secretary": form.cleaned_data.get("message_to_secretary")
293298
})
@@ -393,7 +398,7 @@ def complete_review(request, name, request_id):
393398
strip_prefix(review_req.doc.name, "draft-"),
394399
form.cleaned_data["reviewed_rev"],
395400
review_req.team.acronym,
396-
review_req.type.slug if review_req.type.slug != "unknown" else "",
401+
review_req.type.slug,
397402
review_req.reviewer.person.ascii_parts()[3],
398403
datetime.date.today().isoformat(),
399404
]
@@ -403,8 +408,16 @@ def complete_review(request, name, request_id):
403408
name = "-".join(c for c in name_components if c).lower()
404409
if not Document.objects.filter(name=name).exists():
405410
review = Document.objects.create(name=name)
411+
DocAlias.objects.create(document=review, name=review.name)
406412
break
407413

414+
review.type = DocTypeName.objects.get(slug="review")
415+
review.rev = "00"
416+
review.title = "{} Review of {}-{}".format(review_req.type.name, review_req.doc.name, form.cleaned_data["reviewed_rev"])
417+
review.group = review_req.team
418+
if review_submission == "link":
419+
review.external_url = form.cleaned_data['review_url']
420+
408421
e = NewRevisionDocEvent.objects.create(
409422
type="new_revision",
410423
doc=review,
@@ -414,15 +427,9 @@ def complete_review(request, name, request_id):
414427
time=review.time,
415428
)
416429

417-
review.type = DocTypeName.objects.get(slug="review")
418-
review.rev = "00"
419-
review.title = "{} Review of {}-{}".format(review_req.type.name, review_req.doc.name, form.cleaned_data["reviewed_rev"])
420-
review.group = review_req.team
421-
if review_submission == "link":
422-
review.external_url = form.cleaned_data['review_url']
423-
review.save_with_history([e])
424430
review.set_state(State.objects.get(type="review", slug="active"))
425-
DocAlias.objects.create(document=review, name=review.name)
431+
432+
review.save_with_history([e])
426433

427434
# save file on disk
428435
if review_submission == "upload":
@@ -441,17 +448,25 @@ def complete_review(request, name, request_id):
441448
review_req.review = review
442449
review_req.save()
443450

444-
DocEvent.objects.create(
445-
type="changed_review_request",
451+
need_to_email_review = review_submission != "link" and review_req.team.list_email
452+
453+
desc = "Request for {} review by {} {}: {}. Reviewer: {}.".format(
454+
review_req.type.name,
455+
review_req.team.acronym.upper(),
456+
review_req.state.name,
457+
review_req.result.name,
458+
review_req.reviewer.person,
459+
)
460+
if need_to_email_review:
461+
desc += " " + "Sent review to list."
462+
463+
close_event = ReviewRequestDocEvent.objects.create(
464+
type="closed_review_request",
446465
doc=review_req.doc,
447466
by=request.user.person,
448-
desc="Request for {} review by {} {}: {}. Reviewer: {}".format(
449-
review_req.type.name,
450-
review_req.team.acronym.upper(),
451-
review_req.state.name,
452-
review_req.result.name,
453-
review_req.reviewer,
454-
),
467+
desc=desc,
468+
review_request=review_req,
469+
state=review_req.state,
455470
)
456471

457472
if review_req.state_id == "part-completed":
@@ -463,36 +478,29 @@ def complete_review(request, name, request_id):
463478
url = urlreverse("ietf.doc.views_review.review_request", kwargs={ "name": new_review_req.doc.name, "request_id": new_review_req.pk })
464479
url = request.build_absolute_uri(url)
465480

466-
msg = render_to_string("doc/mail/partially_completed_review.txt", {
481+
msg = render_to_string("review/partially_completed_review.txt", {
467482
'new_review_req_url': url,
468483
"by": request.user.person,
469484
"new_review_req": new_review_req,
470485
})
471486

472487
email_review_request_change(request, review_req, subject, msg, request.user.person, notify_secretary=True, notify_reviewer=False, notify_requested_by=False)
473488

474-
if review_submission != "link" and review_req.team.list_email:
489+
if need_to_email_review:
475490
# email the review
476491
subject = "{} of {}-{}".format("Partial review" if review_req.state_id == "part-completed" else "Review", review_req.doc.name, review_req.reviewed_rev)
477492
msg = send_mail(request, [(review_req.team.name, review_req.team.list_email)], None,
478493
subject,
479-
"doc/mail/completed_review.txt", {
494+
"review/completed_review.txt", {
480495
"review_req": review_req,
481496
"content": encoded_content.decode("utf-8"),
482497
},
483498
cc=form.cleaned_data["cc"])
484499

485-
e = DocEvent.objects.create(
486-
type="changed_review_request",
487-
doc=review_req.doc,
488-
by=request.user.person,
489-
desc="Sent review to list.",
490-
)
491-
492500
list_name = mailarch.list_name_from_email(review_req.team.list_email)
493501
if list_name:
494502
review.external_url = mailarch.construct_message_url(list_name, email.utils.unquote(msg["Message-ID"]))
495-
review.save_with_history([e])
503+
review.save_with_history([close_event])
496504

497505
return redirect("doc_view", name=review_req.review.name)
498506
else:

ietf/group/features.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __init__(self, group):
3131
if group in active_review_teams():
3232
self.has_reviews = True
3333
import ietf.group.views
34-
self.default_tab = ietf.group.views.review_requests
34+
self.default_tab = ietf.group.views_review.review_requests
3535

3636
if group.type_id == "dir":
3737
self.admin_roles = ["chair", "secr"]

ietf/group/tests_info.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
from ietf.utils.mail import outbox, empty_outbox
3232
from ietf.utils.test_data import make_test_data, create_person, make_review_data
3333
from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent, reload_db_objects
34-
import ietf.group.views
3534

3635
def group_urlreverse_list(group, viewname):
3736
return [
@@ -336,31 +335,6 @@ def test_materials(self):
336335
self.assertEqual(r.status_code, 200)
337336
self.assertTrue(doc.title not in unicontent(r))
338337

339-
def test_review_requests(self):
340-
doc = make_test_data()
341-
review_req = make_review_data(doc)
342-
343-
group = review_req.team
344-
345-
for url in [ urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym }),
346-
urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym , 'group_type': group.type_id}),
347-
]:
348-
r = self.client.get(url)
349-
self.assertEqual(r.status_code, 200)
350-
self.assertTrue(review_req.doc.name in unicontent(r))
351-
self.assertTrue(unicode(review_req.reviewer.person) in unicontent(r))
352-
353-
url = urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym })
354-
355-
# close request, listed under closed
356-
review_req.state_id = "completed"
357-
review_req.result_id = "ready"
358-
review_req.save()
359-
360-
r = self.client.get(url)
361-
self.assertEqual(r.status_code, 200)
362-
self.assertTrue(review_req.doc.name in unicontent(r))
363-
364338
def test_history(self):
365339
draft = make_test_data()
366340
group = draft.group

ietf/group/tests_review.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,31 @@
1515
from ietf.utils.mail import outbox, empty_outbox
1616

1717
class ReviewTests(TestCase):
18+
def test_review_requests(self):
19+
doc = make_test_data()
20+
review_req = make_review_data(doc)
21+
22+
group = review_req.team
23+
24+
for url in [ urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym }),
25+
urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym , 'group_type': group.type_id}),
26+
]:
27+
r = self.client.get(url)
28+
self.assertEqual(r.status_code, 200)
29+
self.assertTrue(review_req.doc.name in unicontent(r))
30+
self.assertTrue(unicode(review_req.reviewer.person) in unicontent(r))
31+
32+
url = urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym })
33+
34+
# close request, listed under closed
35+
review_req.state_id = "completed"
36+
review_req.result_id = "ready"
37+
review_req.save()
38+
39+
r = self.client.get(url)
40+
self.assertEqual(r.status_code, 200)
41+
self.assertTrue(review_req.doc.name in unicontent(r))
42+
1843
def test_suggested_review_requests(self):
1944
doc = make_test_data()
2045
review_req = make_review_data(doc)

ietf/group/urls_info_details.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
(r'^materials/new/(?P<doc_type>[\w-]+)/$', 'ietf.doc.views_material.edit_material', { 'action': "new" }, "group_new_material"),
3131
(r'^archives/$', 'ietf.group.views.derived_archives'),
3232
(r'^photos/$', views.group_photos),
33-
(r'^reviews/$', views.review_requests),
33+
(r'^reviews/$', views_review.review_requests),
3434
(r'^reviews/manage/$', views_review.manage_review_requests),
3535
(r'^reviews/email-assignments/$', views_review.email_open_review_assignments),
3636
(r'^reviewers/$', views_review.reviewer_overview),

0 commit comments

Comments
 (0)