Skip to content

Commit bf55237

Browse files
committed
Add statistics section with review statistics. Fix a couple of missing tests.
- Legacy-Id: 12124
1 parent 293ecb1 commit bf55237

12 files changed

Lines changed: 444 additions & 34 deletions

File tree

ietf/externals/static/flot/jquery.flot.min.js

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ietf/externals/static/flot/jquery.flot.time.min.js

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ietf/group/tests_review.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ def test_review_requests(self):
2323

2424
group = review_req.team
2525

26-
for url in [ urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym }),
27-
urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym , 'group_type': group.type_id}),
28-
]:
26+
for url in [urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym }),
27+
urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym , 'group_type': group.type_id})]:
2928
r = self.client.get(url)
3029
self.assertEqual(r.status_code, 200)
3130
self.assertTrue(review_req.doc.name in unicontent(r))
@@ -131,13 +130,13 @@ def test_reviewer_overview(self):
131130

132131
group = review_req1.team
133132

134-
url = urlreverse(ietf.group.views_review.reviewer_overview, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })
135-
136133
# get
137-
r = self.client.get(url)
138-
self.assertEqual(r.status_code, 200)
139-
self.assertTrue(unicode(reviewer) in unicontent(r))
140-
self.assertTrue(review_req1.doc.name in unicontent(r))
134+
for url in [urlreverse(ietf.group.views_review.reviewer_overview, kwargs={ 'acronym': group.acronym }),
135+
urlreverse(ietf.group.views_review.reviewer_overview, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })]:
136+
r = self.client.get(url)
137+
self.assertEqual(r.status_code, 200)
138+
self.assertTrue(unicode(reviewer) in unicontent(r))
139+
self.assertTrue(review_req1.doc.name in unicontent(r))
141140

142141
self.client.login(username="secretary", password="secretary+password")
143142
r = self.client.get(url)
@@ -149,10 +148,12 @@ def test_manage_review_requests(self):
149148

150149
group = review_req1.team
151150

152-
url = urlreverse(ietf.group.views_review.manage_review_requests, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })
151+
url = urlreverse(ietf.group.views_review.manage_review_requests, kwargs={ 'acronym': group.acronym })
153152

154153
login_testing_unauthorized(self, "secretary", url)
155154

155+
url = urlreverse(ietf.group.views_review.manage_review_requests, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })
156+
156157
review_req2 = ReviewRequest.objects.create(
157158
doc=review_req1.doc,
158159
team=review_req1.team,
@@ -265,10 +266,12 @@ def test_email_open_review_assignments(self):
265266

266267
group = review_req1.team
267268

268-
url = urlreverse(ietf.group.views_review.email_open_review_assignments, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })
269-
269+
url = urlreverse(ietf.group.views_review.email_open_review_assignments, kwargs={ 'acronym': group.acronym })
270+
270271
login_testing_unauthorized(self, "secretary", url)
271272

273+
url = urlreverse(ietf.group.views_review.email_open_review_assignments, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })
274+
272275
r = self.client.get(url)
273276
self.assertEqual(r.status_code, 200)
274277
q = PyQuery(r.content)
@@ -299,13 +302,18 @@ def test_change_reviewer_settings(self):
299302
review_req.save()
300303

301304
url = urlreverse(ietf.group.views_review.change_reviewer_settings, kwargs={
302-
"group_type": review_req.team.type_id,
303305
"acronym": review_req.team.acronym,
304306
"reviewer_email": review_req.reviewer_id,
305307
})
306308

307309
login_testing_unauthorized(self, reviewer.user.username, url)
308310

311+
url = urlreverse(ietf.group.views_review.change_reviewer_settings, kwargs={
312+
"group_type": review_req.team.type_id,
313+
"acronym": review_req.team.acronym,
314+
"reviewer_email": review_req.reviewer_id,
315+
})
316+
309317
# get
310318
r = self.client.get(url)
311319
self.assertEqual(r.status_code, 200)

ietf/group/views_review.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import datetime, math
1+
import datetime, math, itertools
22
from collections import defaultdict
33

44
from django.shortcuts import render, redirect, get_object_or_404
@@ -105,7 +105,12 @@ def reviewer_overview(request, acronym, group_type=None):
105105

106106
today = datetime.date.today()
107107

108-
all_req_data = extract_review_request_data(teams=[group], time_from=today - datetime.timedelta(days=365))
108+
extracted_data = extract_review_request_data(teams=[group], time_from=today - datetime.timedelta(days=365), ordering=["reviewer"])
109+
req_data_for_reviewer = {}
110+
for reviewer, req_data_items in itertools.groupby(extracted_data, key=lambda data: data.reviewer):
111+
l = list(req_data_items)
112+
l.reverse()
113+
req_data_for_reviewer[reviewer] = l
109114
review_state_by_slug = { n.slug: n for n in ReviewRequestStateName.objects.all() }
110115

111116
for person in reviewers:
@@ -120,15 +125,15 @@ def reviewer_overview(request, acronym, group_type=None):
120125
for p in person.unavailable_periods)
121126

122127
MAX_REQS = 5
123-
req_data = all_req_data.get((group.pk, person.pk), [])
124-
open_reqs = sum(1 for _, _, _, _, state, _, _, _, _, _ in req_data if state in ("requested", "accepted"))
128+
req_data = req_data_for_reviewer.get(person.pk, [])
129+
open_reqs = sum(1 for d in req_data if d.state in ("requested", "accepted"))
125130
latest_reqs = []
126-
for req_pk, doc, req_time, state, deadline, result, late_days, request_to_assignment_days, assignment_to_closure_days, request_to_closure_days in req_data:
131+
for d in req_data:
127132
# any open requests pushes the others out
128-
if ((state in ("requested", "accepted") and len(latest_reqs) < MAX_REQS) or (len(latest_reqs) + open_reqs < MAX_REQS)):
129-
if assignment_to_closure_days is not None:
130-
assignment_to_closure_days = int(math.ceil(assignment_to_closure_days))
131-
latest_reqs.append((req_pk, doc, deadline, review_state_by_slug.get(state), assignment_to_closure_days))
133+
if ((d.state in ("requested", "accepted") and len(latest_reqs) < MAX_REQS) or (len(latest_reqs) + open_reqs < MAX_REQS)):
134+
latest_reqs.append((d.req_pk, d.doc, d.deadline,
135+
review_state_by_slug.get(d.state),
136+
int(math.ceil(d.assignment_to_closure_days)) if d.assignment_to_closure_days is not None else None))
132137
person.latest_reqs = latest_reqs
133138

134139
return render(request, 'group/reviewer_overview.html',

ietf/name/migrations/0015_insert_review_name_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def insert_initial_review_data(apps, schema_editor):
1010
ReviewRequestStateName.objects.get_or_create(slug="accepted", name="Accepted", order=2)
1111
ReviewRequestStateName.objects.get_or_create(slug="rejected", name="Rejected", order=3)
1212
ReviewRequestStateName.objects.get_or_create(slug="withdrawn", name="Withdrawn", order=4)
13-
ReviewRequestStateName.objects.get_or_create(slug="overtaken", name="Overtaken By Events", order=5)
13+
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)
1515
ReviewRequestStateName.objects.get_or_create(slug="no-review-version", name="Team Will not Review Version", order=7)
1616
ReviewRequestStateName.objects.get_or_create(slug="no-review-document", name="Team Will not Review Document", order=8)

ietf/review/utils.py

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import datetime, re, itertools
2-
from collections import defaultdict
2+
from collections import defaultdict, namedtuple
33

44
from django.db.models import Q, Max, F
55
from django.core.urlresolvers import reverse as urlreverse
@@ -142,8 +142,15 @@ def days_needed_to_fulfill_min_interval_for_reviewers(team):
142142

143143
return res
144144

145-
def extract_review_request_data(teams=None, reviewers=None, time_from=None, time_to=None):
146-
"""Returns a dict keyed on (team.pk, reviewer_person.pk) which lists data on each review request."""
145+
ReviewRequestData = namedtuple("ReviewRequestData", [
146+
"req_pk", "doc", "doc_pages", "req_time", "state", "deadline", "result", "team", "reviewer",
147+
"late_days",
148+
"request_to_assignment_days", "assignment_to_closure_days", "request_to_closure_days"])
149+
150+
151+
def extract_review_request_data(teams=None, reviewers=None, time_from=None, time_to=None, ordering=[]):
152+
"""Yield data on each review request, sorted by (*ordering, time)
153+
for easy use with itertools.groupby. Valid entries in *ordering are "team" and "reviewer"."""
147154

148155
filters = Q()
149156

@@ -159,13 +166,16 @@ def extract_review_request_data(teams=None, reviewers=None, time_from=None, time
159166
if time_to:
160167
filters &= Q(time__lte=time_to)
161168

162-
res = defaultdict(list)
163-
164169
# we may be dealing with a big bunch of data, so treat it carefully
165170
event_qs = ReviewRequest.objects.filter(filters)
166171

167172
# left outer join with RequestRequestDocEvent for request/assign/close time
168-
event_qs = event_qs.values_list("pk", "doc", "time", "state", "deadline", "result", "team", "reviewer__person", "reviewrequestdocevent__time", "reviewrequestdocevent__type").order_by("-time", "-pk", "-reviewrequestdocevent__time")
173+
event_qs = event_qs.values_list(
174+
"pk", "doc", "doc__pages", "time", "state", "deadline", "result", "team",
175+
"reviewer__person", "reviewrequestdocevent__time", "reviewrequestdocevent__type"
176+
)
177+
178+
event_qs = event_qs.order_by(*[o.replace("reviewer", "reviewer__person") for o in ordering] + ["time", "pk", "-reviewrequestdocevent__time"])
169179

170180
def positive_days(time_from, time_to):
171181
if time_from is None or time_to is None:
@@ -182,7 +192,7 @@ def positive_days(time_from, time_to):
182192
requested_time = assigned_time = closed_time = None
183193

184194
for e in events:
185-
req_pk, doc, req_time, state, deadline, result, team, reviewer, event_time, event_type = e
195+
req_pk, doc, doc_pages, req_time, state, deadline, result, team, reviewer, event_time, event_type = e
186196

187197
if event_type == "requested_review" and requested_time is None:
188198
requested_time = event_time
@@ -196,12 +206,58 @@ def positive_days(time_from, time_to):
196206
assignment_to_closure_days = positive_days(assigned_time, closed_time)
197207
request_to_closure_days = positive_days(requested_time, closed_time)
198208

199-
res[(team, reviewer)].append((req_pk, doc, req_time, state, deadline, result,
200-
late_days, request_to_assignment_days, assignment_to_closure_days,
201-
request_to_closure_days))
209+
d = ReviewRequestData(req_pk, doc, doc_pages, req_time, state, deadline, result, team, reviewer,
210+
late_days, request_to_assignment_days, assignment_to_closure_days,
211+
request_to_closure_days)
212+
213+
yield d
214+
215+
def aggregate_review_request_stats(review_request_data, count=None):
216+
"""Take a sequence of review request data from
217+
extract_review_request_data and compute aggregated statistics."""
218+
219+
state_dict = defaultdict(int)
220+
late_state_dict = defaultdict(int)
221+
result_dict = defaultdict(int)
222+
assignment_to_closure_days_list = []
223+
assignment_to_closure_days_count = 0
224+
225+
for (req_pk, doc, doc_pages, req_time, state, deadline, result, team, reviewer,
226+
late_days, request_to_assignment_days, assignment_to_closure_days, request_to_closure_days) in review_request_data:
227+
if count == "pages":
228+
c = doc_pages
229+
else:
230+
c = 1
231+
232+
state_dict[state] += c
233+
234+
if late_days is not None and late_days > 0:
235+
late_state_dict[state] += c
236+
237+
if state in ("completed", "part-completed"):
238+
result_dict[result] += c
239+
if assignment_to_closure_days is not None:
240+
assignment_to_closure_days_list.append(assignment_to_closure_days)
241+
assignment_to_closure_days_count += c
242+
243+
res = {}
244+
res["state"] = state_dict
245+
res["result"] = result_dict
246+
247+
res["open"] = sum(state_dict.get(s, 0) for s in ("requested", "accepted"))
248+
res["completed"] = sum(state_dict.get(s, 0) for s in ("completed", "part-completed"))
249+
res["not_completed"] = sum(state_dict.get(s, 0) for s in state_dict if s in ("rejected", "withdrawn", "overtaken", "no-response"))
250+
251+
res["open_late"] = sum(late_state_dict.get(s, 0) for s in ("requested", "accepted"))
252+
res["open_in_time"] = res["open"] - res["open_late"]
253+
res["completed_late"] = sum(late_state_dict.get(s, 0) for s in ("completed", "part-completed"))
254+
res["completed_in_time"] = res["completed"] - res["completed_late"]
255+
256+
res["average_assignment_to_closure_days"] = float(sum(assignment_to_closure_days_list)) / (assignment_to_closure_days_count or 1) if assignment_to_closure_days_list else None
202257

203258
return res
204259

260+
205261
def make_new_review_request_from_existing(review_req):
206262
obj = ReviewRequest()
207263
obj.time = review_req.time
@@ -215,6 +271,7 @@ def make_new_review_request_from_existing(review_req):
215271
return obj
216272

217273
def email_review_request_change(request, review_req, subject, msg, by, notify_secretary, notify_reviewer, notify_requested_by):
274+
218275
"""Notify stakeholders about change, skipping a party if the change
219276
was done by that party."""
220277

0 commit comments

Comments
 (0)