11import datetime , re , itertools
2- from collections import defaultdict
2+ from collections import defaultdict , namedtuple
33
44from django .db .models import Q , Max , F
55from 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+
205261def 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
217273def 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