Merge lp:~jcsackett/charmworld/better-stats-window into lp:~juju-jitsu/charmworld/trunk

Proposed by j.c.sackett
Status: Merged
Approved by: j.c.sackett
Approved revision: 328
Merged at revision: 394
Proposed branch: lp:~jcsackett/charmworld/better-stats-window
Merge into: lp:~juju-jitsu/charmworld/trunk
Diff against target: 657 lines (+423/-77)
7 files modified
charmworld/jobs/review.py (+103/-60)
charmworld/jobs/tests/test_review.py (+110/-0)
charmworld/static/jspark.js (+107/-0)
charmworld/templates/review.pt (+15/-2)
charmworld/utils.py (+16/-13)
charmworld/views/tests/test_tools.py (+34/-1)
charmworld/views/tools.py (+38/-1)
To merge this branch: bzr merge lp:~jcsackett/charmworld/better-stats-window
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+186044@code.launchpad.net

Commit message

Gets and displays review latency statistics in the review queue

Description of the change

This branch updates the review job and the charm-review view to calculate and
display latency information for review items.

charmworld/jobs/review.py
-------------------------
The review job has been altered in several ways.
* The global log has been done away with; the log created in `main` is passed
  into the other functions.
* A new function, `get_tasks_and_proposals` does the work of pulling items from
  Launchpad. Rather than pulling only those items to be displayed in the queue,
  it pulls all items so that latency can be properly calculated.
* The loops that created entries out of tasks and proposals first calculates the
  latency of any item created in the last 6 months as well as any item that is
  still open, even if it is older than 6 months. It then determines if an item
  is still in an "open" status and if so adds it to the review queue. These new
  rules supersede the "not in progress" rule used for juju doc proposals.
* Average, min, and max latency across all items is stored in a new collection,
  `review_queue_latency`. This collection is used to display the current queue
  latency as well as the trend in the charm-review view.

`get_tasks_and_proposals` is greedy--it grabs any possibly related proposal or
task. This is a tradeoff so we don't have to make two fetches for each sort of
item; one for anything created in the last six months, and another for anything
that is still open. In theory our greatest delay is connection time between
charmworld and launchpad--I'm open to arguments refuting that and altering this
to do fetches and create a set of all unique items found in the pair of each
requests from LP.

charmworld/views/tools.py
-------------------------
The charm-review view is still pretty simple, though it grabs a bit more data.
* The latency stats for the last six months are pulled from the new collection
  created in the review job. The most recent item is used to display the current
  min, max, and average. The set of data is used to display sparklines showing
  the 6 month history.
* A new function, `get_sparklines`, uses the stat data to create data strings
  that can be used by a new sparkline library.

charmworld/templates/review.pt and charmworld/static/jspark.js
--------------------------------------------------------------
The template has been updated to show the stats and the sparklines, and to
correct an odd oversight in showing the age of items.
* A new javascript file has been added--this is from resig's set of jquery
  tools. While D3 provides richer visualizations, it is very heavyweight and
  likley unecessary unless/until we're doing a lot more reporting on this site.
* The review queue loads the stats at the top of the listing, along with the
  currently displayed item count. The sparkline spans are automatically turned
  into graphics by jspark. If there is no data or insufficient data (i.e. only
  one data point) jspark removes the span.
* Age is now from date_created, not date_modified, for the entry. As an example
  of why, there is a 22 month old item that was being shown as only a few weeks
  old.

Other
-----
* test_tools.py has been moved into the test directory.
* The portion of `pretty_timedelta` that actually creates a human readable display
  has been broken out as `prettify_timedelta`; this is used both in
  `pretty_timedelta` and in the charm_review view to prettify the min, max, and
  mean latencies.
* tests have been added

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

Thanks. This looks good to me. In theory, I think we do want to do two fetches (open and <6 months old), so that the size of the response is proportional to the size of the data we want to evaluate. But I'm happy to wait until we need that.

review: Approve
Revision history for this message
j.c.sackett (jcsackett) wrote :

> Thanks. This looks good to me. In theory, I think we do want to do two
> fetches (open and <6 months old), so that the size of the response is
> proportional to the size of the data we want to evaluate. But I'm happy to
> wait until we need that.

Cool; I'll put in a comment in that function suggesting we want to re-evaluate it in time so the TODO doesn't get lost. Thanks!

327. By j.c.sackett

Link to bug

328. By j.c.sackett

Comment capturing future TODO in get_tasks_and_proposals

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmworld/jobs/review.py'
--- charmworld/jobs/review.py 2013-06-12 20:49:47 +0000
+++ charmworld/jobs/review.py 2013-09-17 15:22:38 +0000
@@ -13,13 +13,13 @@
13 - use dateutil date parsing instead of string comparsion.13 - use dateutil date parsing instead of string comparsion.
14 - merge proposals show target with series/branch.14 - merge proposals show target with series/branch.
1515
16"""16"""
17import datetime
18import itertools
19import logging
1720
18from launchpadlib.launchpad import Launchpad21from launchpadlib.launchpad import Launchpad
19
20import itertools
21import pymongo22import pymongo
22import logging
2323
24from charmworld.utils import (24from charmworld.utils import (
25 configure_logging,25 configure_logging,
@@ -30,10 +30,14 @@
30from utils import lock30from utils import lock
31from utils import LockHeld31from utils import LockHeld
3232
33log = None33
3434def get_tasks_and_proposals(log):
3535 '''Gets review items (bugs and proposals) from launchpad.'''
36def update_review_queue(db):36 # TODO: Eventually these single requests should be broken into two each:
37 # one to get anything created within the window, and one to get anything
38 # older than the window that is still open. Right now this is probably the
39 # more performant way to go about it, but that's going to change as the data
40 # outside of our window gets larger.
37 with Timer() as timer:41 with Timer() as timer:
38 lp = Launchpad.login_anonymously('charm-tools',42 lp = Launchpad.login_anonymously('charm-tools',
39 'production',43 'production',
@@ -45,66 +49,91 @@
45 charm_contributors = lp.people['charm-contributors']49 charm_contributors = lp.people['charm-contributors']
4650
47 log.debug("lp login time %0.2f", timer.duration())51 log.debug("lp login time %0.2f", timer.duration())
48 bug_statuses = [
49 'New', 'Confirmed', 'Triaged', 'In Progress', 'Fix Committed']
5052
51 with Timer() as timer:53 with Timer() as timer:
52 bugs = charm.searchTasks(54 bugs = charm.searchTasks(
53 tags=['new-formula', 'new-charm'],55 tags=['new-formula', 'new-charm'],
54 tags_combinator="Any",56 tags_combinator="Any")
55 status=bug_statuses)
56 # Best practice dictates that charmers be subscribed to all bugs57 # Best practice dictates that charmers be subscribed to all bugs
57 # and merge proposals for official source package branches.58 # and merge proposals for official source package branches.
58 charmers_bugs = charmers.searchTasks(status=bug_statuses)59 charmers_bugs = charmers.searchTasks()
59 proposals = charmers.getRequestedReviews(status="Needs review")60 proposals = charmers.getRequestedReviews()
60 contributor_proposals = charm_contributors.getRequestedReviews(61 contributor_proposals = charm_contributors.getRequestedReviews()
61 status="Needs review")
6262
63 juju = lp.projects['juju']63 juju = lp.projects['juju']
64 doc_proposals = [mp for mp in juju.getMergeProposals()64 doc_proposals = [mp for mp in juju.getMergeProposals()
65 if mp.target_branch_link.endswith("~juju/juju/docs")]65 if mp.target_branch_link.endswith("~juju/juju/docs")]
6666
67 log.debug("lp query time %0.2f", timer.duration())67 log.debug("lp query time %0.2f", timer.duration())
68 doc_proposals = [mp for mp in doc_proposals if not mp.queue_status in (68 return (itertools.chain(bugs, charmers_bugs),
69 'Work in progress',)]69 itertools.chain(proposals, contributor_proposals, doc_proposals))
7070
71 r_queue = list()71
72 for task in itertools.chain(bugs, charmers_bugs):72def update_review_queue(db, tasks, proposals, log):
73 entry = {73 '''Updates the review queue information.'''
74 'date_modified': task.bug.date_last_updated,74 bug_statuses = [
75 'date_created': task.date_created,75 'New', 'Confirmed', 'Triaged', 'In Progress', 'Fix Committed']
76 'summary': task.title.split('"')[1].strip(),76 proposal_statuses = ['Needs review', 'Needs fixing', 'Needs information']
77 'item': task.web_link,77 now = datetime.datetime.utcnow()
78 'status': task.status,78 window = now - datetime.timedelta(days=180) # 6 month window for latency
79 }79
80 r_queue.append(entry)80 r_queue = []
8181 latencies = []
82 for proposal in itertools.chain(proposals,82 for task in tasks:
83 contributor_proposals,83 if (task.date_created.replace(tzinfo=None) >= window or
84 doc_proposals):84 task.status in bug_statuses):
8585 if task.date_confirmed:
86 parts = proposal.target_branch_link.rsplit('/', 3)86 latency = task.date_confirmed - task.date_created
87 if parts[-1] == "trunk":87 else:
88 parts.pop(-1)88 # Now has been calculated in UTC, and LP returns UTC TZ datetimes.
8989 # We can safely remove the TZ, letting us get the timedelta.
90 target = "/".join(parts[2:])90 latency = now - task.date_created.replace(tzinfo=None)
91 origin = "/".join(proposal.source_branch_link.rsplit('/', 3)[1:])91 latencies.append(latency.total_seconds())
9292
93 num_comments = proposal.all_comments.total_size93 if task.status in bug_statuses:
94 if num_comments != 0:94 entry = {
95 last_modified = proposal.all_comments[95 'date_modified': task.bug.date_last_updated,
96 num_comments - 1].date_created96 'date_created': task.date_created,
97 else:97 'summary': task.title.split('"')[1].strip(),
98 last_modified = proposal.date_created98 'item': task.web_link,
9999 'status': task.status,
100 entry = {100 }
101 'date_modified': last_modified,101 r_queue.append(entry)
102 'date_created': proposal.date_created,102
103 'summary': "Merge for %s from %s" % (target, origin),103 for proposal in proposals:
104 'item': proposal.web_link,104 if (proposal.date_created.replace(tzinfo=None) >= window or
105 'status': proposal.queue_status,105 proposal.queue_status in proposal_statuses):
106 }106 if proposal.date_reviewed:
107 r_queue.append(entry)107 latency = proposal.date_reviewed - proposal.date_review_requested
108 else:
109 # Now has been calculated in UTC, and LP returns UTC TZ datetimes.
110 # We can safely remove the TZ, letting us get the timedelta.
111 latency = now - proposal.date_review_requested.replace(tzinfo=None)
112 latencies.append(latency.total_seconds())
113
114 if proposal.queue_status in proposal_statuses:
115 parts = proposal.target_branch_link.rsplit('/', 3)
116 if parts[-1] == "trunk":
117 parts.pop(-1)
118 target = "/".join(parts[2:])
119 origin = "/".join(proposal.source_branch_link.rsplit('/', 3)[1:])
120
121 num_comments = proposal.all_comments.total_size
122 if num_comments != 0:
123 last_modified = proposal.all_comments[
124 num_comments - 1].date_created
125 else:
126 last_modified = proposal.date_created
127
128 entry = {
129 'latency': latency.total_seconds(),
130 'date_modified': last_modified,
131 'date_created': proposal.date_created,
132 'summary': "Merge for %s from %s" % (target, origin),
133 'item': proposal.web_link,
134 'status': proposal.queue_status,
135 }
136 r_queue.append(entry)
108137
109 # Drop old collection138 # Drop old collection
110 log.info(139 log.info(
@@ -113,11 +142,24 @@
113142
114 # Bulk insert143 # Bulk insert
115 log.info("Updating review queue (%d items)" % len(r_queue))144 log.info("Updating review queue (%d items)" % len(r_queue))
116 db['review_queue'].insert(r_queue)145 if (r_queue != []):
117146 # Needs check as bulk insert of an empty list is prohibited
147 db['review_queue'].insert(r_queue)
148
149 # Get stats:
150 stats = latency_stats(latencies)
151 if stats:
152 stats['date'] = now
153 db.review_queue_latency.insert(stats)
154
155def latency_stats(queue):
156 if queue != []:
157 min_latency = min(queue)
158 mean_latency = sum(queue)/len(queue)
159 max_latency = max(queue)
160 return {'min': min_latency, 'max': max_latency, 'mean': mean_latency}
118161
119def main():162def main():
120 global log
121 configure_logging()163 configure_logging()
122 log = logging.getLogger("charm.review")164 log = logging.getLogger("charm.review")
123 c = pymongo.Connection(MONGO_URL, safe=True)165 c = pymongo.Connection(MONGO_URL, safe=True)
@@ -126,7 +168,8 @@
126 try:168 try:
127 with lock('ingest-review',169 with lock('ingest-review',
128 int(settings['script_lease_time']) * 60, db, log):170 int(settings['script_lease_time']) * 60, db, log):
129 update_review_queue(db)171 tasks, proposals = get_tasks_and_proposals(log)
172 update_review_queue(db, tasks, proposals, log)
130 except LockHeld, error:173 except LockHeld, error:
131 log.warn(str(error))174 log.warn(str(error))
132175
133176
=== added file 'charmworld/jobs/tests/test_review.py'
--- charmworld/jobs/tests/test_review.py 1970-01-01 00:00:00 +0000
+++ charmworld/jobs/tests/test_review.py 2013-09-17 15:22:38 +0000
@@ -0,0 +1,110 @@
1# Copyright 2012, 2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3import datetime
4import logging
5
6from charmworld.jobs.review import update_review_queue
7from charmworld.testing import factory
8from charmworld.testing import MongoTestBase
9
10
11NOW = datetime.datetime.now()
12YESTERDAY = NOW - datetime.timedelta(1)
13TWO_DAYS = NOW - datetime.timedelta(2)
14
15
16class MockBug:
17 date_last_updated = NOW
18
19
20class MockTask:
21
22 def __init__(self, confirmed=False):
23 self.date_created = TWO_DAYS
24 self.title = u'Bug #0 in a collection: "A bug task"'
25 self.web_link = "http://example.com"
26 self.bug = MockBug()
27 if confirmed:
28 self.status = "Confirmed"
29 self.date_confirmed = YESTERDAY
30 else:
31 self.date_confirmed = None
32 self.status = "New"
33
34
35class MockCommentCollection:
36 total_size = 0
37
38
39class MockProposal:
40
41 def __init__(self, reviewed=False):
42 base = 'https://api.launchpad.net/devel/'
43 self.target_branch_link = base + '~charmers/charms/precise/foo/trunk'
44 self.source_branch_link = base + '~bar/charms/precise/foo/fnord'
45 self.all_comments = MockCommentCollection()
46 self.date_review_requested = TWO_DAYS
47 self.date_created = TWO_DAYS
48 self.web_link = 'http://example.com'
49 if reviewed:
50 self.date_reviewed = YESTERDAY
51 self.queue_status = "Approved"
52 else:
53 self.date_reviewed = None
54 self.queue_status = "Needs review"
55
56
57class ReviewTest(MongoTestBase):
58
59 def test_review_handles_empty_queue(self):
60 self.db.review_queue.insert([{'foo': 'bar'}])
61 log = logging.getLogger('foo')
62 update_review_queue(self.db, [], [], log)
63 self.assertEqual(0, self.db.review_queue.count())
64
65 def test_review_handles_bugs(self):
66 log = logging.getLogger('foo')
67 update_review_queue(self.db, [MockTask()], [], log)
68 self.assertEqual(1, self.db.review_queue.count())
69 item = self.db.review_queue.find_one()
70 self.assertEqual(NOW.date(), item['date_modified'].date())
71 self.assertEqual(TWO_DAYS.date(), item['date_created'].date())
72 self.assertEqual('A bug task', item['summary'])
73 self.assertEqual('http://example.com', item['item'])
74 self.assertEqual('New', item['status'])
75
76 def test_review_handles_proposals(self):
77 log = logging.getLogger('foo')
78 update_review_queue(self.db, [], [MockProposal()], log)
79 self.assertEqual(1, self.db.review_queue.count())
80 item = self.db.review_queue.find_one()
81 self.assertEqual(TWO_DAYS.date(), item['date_modified'].date())
82 self.assertEqual(TWO_DAYS.date(), item['date_created'].date())
83 self.assertEqual('Merge for foo from precise/foo/fnord', item['summary'])
84 self.assertEqual('http://example.com', item['item'])
85 self.assertEqual('Needs review', item['status'])
86
87 def test_review_calculates_latency(self):
88 log = logging.getLogger('foo')
89 self.assertEqual(0, self.db.review_queue_latency.count())
90 update_review_queue(self.db, [], [MockProposal(True)], log)
91 self.assertEqual(1, self.db.review_queue_latency.count())
92 item = self.db.review_queue_latency.find_one()
93 self.assertEqual(NOW.date(), item['date'].date())
94 self.assertEqual(1, datetime.timedelta(seconds=item['max']).days)
95 self.assertEqual(1, datetime.timedelta(seconds=item['mean']).days)
96 self.assertEqual(1, datetime.timedelta(seconds=item['min']).days)
97
98 def test_review_discards_closed_bugs(self):
99 tasks = [MockTask(), MockTask()]
100 tasks[0].status = 'Fix released'
101 log = logging.getLogger('foo')
102 update_review_queue(self.db, tasks, [], log)
103 self.assertEqual(1, self.db.review_queue.count())
104
105 def test_review_discards_approved_proposals(self):
106 proposals = [MockProposal(), MockProposal()]
107 proposals[0].queue_status = 'Approved'
108 log = logging.getLogger('foo')
109 update_review_queue(self.db, [], proposals, log)
110 self.assertEqual(1, self.db.review_queue.count())
0111
=== added file 'charmworld/static/jspark.js'
--- charmworld/static/jspark.js 1970-01-01 00:00:00 +0000
+++ charmworld/static/jspark.js 2013-09-17 15:22:38 +0000
@@ -0,0 +1,107 @@
1/**
2 * Javascript Sparklines Library
3 * Written By John Resig
4 * http://ejohn.org/projects/jspark/
5 *
6 * This work is tri-licensed under the MPL, GPL, and LGPL:
7 * http://www.mozilla.org/MPL/
8 *
9 * To use, place your data points within your HTML, like so:
10 * <span class="sparkline">10,8,20,5...</span>
11 *
12 * in your CSS you might want to have the rule:
13 * .sparkline { display: none }
14 * so that non-compatible browsers don't see a huge pile of numbers.
15 *
16 * Finally, include this library in your header, like so:
17 * <script language="javascript" src="jspark.js"></script>
18 */
19
20addEvent( window, "load", function() {
21 var a = document.getElementsByTagName("*") || document.all;
22
23 for ( var i = 0; i < a.length; i++ )
24 if ( has( a[i].className, "sparkline" ) )
25 sparkline( a[i] );
26} );
27
28function has(s,c) {
29 var r = new RegExp("(^| )" + c + "\W*");
30 return ( r.test(s) ? true : false );
31}
32
33function addEvent( obj, type, fn ) {
34 if ( obj.attachEvent ) {
35 obj['e'+type+fn] = fn;
36 obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
37 obj.attachEvent( 'on'+type, obj[type+fn] );
38 } else
39 obj.addEventListener( type, fn, false );
40}
41
42function removeEvent( obj, type, fn ) {
43 if ( obj.detachEvent ) {
44 obj.detachEvent( 'on'+type, obj[type+fn] );
45 obj[type+fn] = null;
46 } else
47 obj.removeEventListener( type, fn, false );
48}
49
50
51function sparkline(o) {
52 var p = o.innerHTML.split(',');
53 while ( o.childNodes.length > 0 )
54 o.removeChild( o.firstChild );
55
56 var nw = "auto";
57 var nh = "auto";
58 if ( window.getComputedStyle ) {
59 nw = window.getComputedStyle( o, null ).width;
60 nh = window.getComputedStyle( o, null ).height;
61 }
62
63 if ( nw != "auto" ) nw = nw.substr( 0, nw.length - 2 );
64 if ( nh != "auto" ) nh = nh.substr( 0, nh.length - 2 );
65
66 var f = 2;
67 var w = ( nw == "auto" || nw == 0 ? p.length * f : nw - 0 );
68 var h = ( nh == "auto" || nh == 0 ? "1em" : nh );
69
70 var co = document.createElement("canvas");
71
72 if ( co.getContext ) o.style.display = 'inline';
73 else return false;
74
75 co.style.height = h;
76 co.style.width = w;
77 co.width = w;
78 o.appendChild( co );
79
80 var h = co.offsetHeight;
81 co.height = h;
82
83 var min = 9999;
84 var max = -1;
85
86 for ( var i = 0; i < p.length; i++ ) {
87 p[i] = p[i] - 0;
88 if ( p[i] < min ) min = p[i];
89 if ( p[i] > max ) max = p[i];
90 }
91
92 if ( co.getContext ) {
93 var c = co.getContext("2d");
94 c.strokeStyle = "red";
95 c.lineWidth = 1.0;
96 c.beginPath();
97
98 for ( var i = 0; i < p.length; i++ ) {
99 if ( i == 0 )
100 c.moveTo( (w / p.length) * i, h - (((p[i] - min) / (max - min)) * h) );
101 c.lineTo( (w / p.length) * i, h - (((p[i] - min) / (max - min)) * h) );
102 }
103
104 c.stroke();
105 o.style.display = 'inline';
106 }
107}
0108
=== modified file 'charmworld/templates/review.pt'
--- charmworld/templates/review.pt 2013-09-05 09:47:11 +0000
+++ charmworld/templates/review.pt 2013-09-17 15:22:38 +0000
@@ -1,9 +1,22 @@
1<html tal:define="main load: main.pt" metal:use-macro="main.macros['page']">1<html tal:define="main load: main.pt" metal:use-macro="main.macros['page']">
2 <body>2 <body>
3 <metal:block fill-slot="js_include">
4 <script type="text/javascript"
5 src="${request.static_url('charmworld:static/jspark.js')}">
6 </script>
7 </metal:block>
3<metal:block metal:fill-slot="content">8<metal:block metal:fill-slot="content">
4 <div class="row no-border">9 <div class="row no-border">
5 <h1>Charmers Review Queue</h1>10 <h1>Charmers Review Queue</h1>
6 <p class="note">${len(queue)} reviews</p>11 <p class="note">
12 <strong>Items:</strong> ${len(queue)}
13 <strong>Max wait time:</strong> ${max}
14 <span class="sparklinesline">${sparklines['maxes']}</span>
15 <strong>Average wait time:</strong> ${mean}
16 <span class="sparklinesline">${sparklines['means']}</span>
17 <strong>Min wait time:</strong> ${min}
18 <span class="sparklinesline">${sparklines['mins']}</span>
19 </p>
7 <table>20 <table>
8 <thead>21 <thead>
9 <tr>22 <tr>
@@ -15,7 +28,7 @@
15 </tr>28 </tr>
16 </thead>29 </thead>
17 <tr tal:repeat="item queue">30 <tr tal:repeat="item queue">
18 <td tal:content="age_format(item['date_modified'])"></td>31 <td tal:content="age_format(item['date_created'])"></td>
19 <td tal:content="item['date_created'].strftime('%Y/%m/%d')"></td>32 <td tal:content="item['date_created'].strftime('%Y/%m/%d')"></td>
20 <td tal:content="item['date_modified'].strftime('%Y/%m/%d')"></td>33 <td tal:content="item['date_modified'].strftime('%Y/%m/%d')"></td>
21 <td>34 <td>
2235
=== renamed file 'charmworld/test_utils.py' => 'charmworld/tests/test_utils.py'
=== modified file 'charmworld/utils.py'
--- charmworld/utils.py 2013-09-09 13:45:45 +0000
+++ charmworld/utils.py 2013-09-17 15:22:38 +0000
@@ -28,6 +28,21 @@
28)28)
2929
3030
31def prettify_timedelta(diff):
32 """Return a prettified time delta, suitable for pretty printing."""
33 age_s = int(diff.days * 86400 + diff.seconds)
34
35 for u, template in _units:
36 r = float(age_s) / float(u)
37 if r >= 1.9:
38 r = int(round(r))
39 v = template % r
40 if r > 1:
41 v += "s"
42 return v
43 return ''
44
45
31def pretty_timedelta(time1, time2=None):46def pretty_timedelta(time1, time2=None):
32 """Calculate time delta between two `datetime` objects.47 """Calculate time delta between two `datetime` objects.
33 (the result is somewhat imprecise, only use for prettyprinting).48 (the result is somewhat imprecise, only use for prettyprinting).
@@ -41,19 +56,7 @@
41 raise ValueError('The start date is before the end date.')56 raise ValueError('The start date is before the end date.')
4257
43 diff = time2 - time158 diff = time2 - time1
44 age_s = int(diff.days * 86400 + diff.seconds)59 return prettify_timedelta(diff)
45
46# if age_s <= 60 * 1.9:
47# return ngettext('%(num)i second', '%(num)i seconds', age_s)
48 for u, template in _units:
49 r = float(age_s) / float(u)
50 if r >= 1.9:
51 r = int(round(r))
52 v = template % r
53 if r > 1:
54 v += "s"
55 return v
56 return ''
5760
5861
59def configure_logging():62def configure_logging():
6063
=== modified file 'charmworld/views/tests/test_tools.py'
--- charmworld/views/tests/test_tools.py 2013-09-05 09:47:11 +0000
+++ charmworld/views/tests/test_tools.py 2013-09-17 15:22:38 +0000
@@ -4,11 +4,15 @@
4__metaclass__ = type4__metaclass__ = type
55
66
7from datetime import datetime7from datetime import (
8 datetime,
9 timedelta,
10)
811
9from charmworld.models import Charm12from charmworld.models import Charm
10from charmworld.views.tools import (13from charmworld.views.tools import (
11 charm_store_missing,14 charm_store_missing,
15 get_sparklines,
12 proof_errors,16 proof_errors,
13)17)
14from charmworld.testing import factory18from charmworld.testing import factory
@@ -31,6 +35,16 @@
3135
32class TestReviews(WebTestBase):36class TestReviews(WebTestBase):
3337
38 def test_sparklines(self):
39 stats = [
40 {'min': 0, 'max':10, 'mean': 5},
41 {'min': 4, 'max': 6, 'mean': 5.5},
42 {'min': 2, 'max': 20, 'mean': 12}]
43 sparklines = get_sparklines(stats)
44 self.assertEqual('0,4,2', stats['mins'])
45 self.assertEqual('10,6,20', stats['maxes'])
46 self.assertEqual('5,5.5,12', stats['mins'])
47
34 def test_last_modified_formatting(self):48 def test_last_modified_formatting(self):
35 self.db.review_queue.save(49 self.db.review_queue.save(
36 factory.make_review_entry(date_modified=datetime(2012, 1, 2)))50 factory.make_review_entry(date_modified=datetime(2012, 1, 2)))
@@ -52,6 +66,25 @@
52 for heading, col in zip(headings, columns)]66 for heading, col in zip(headings, columns)]
53 self.assertEqual(['2012/01/02', '2012/01/03'], colmap['Last Modified'])67 self.assertEqual(['2012/01/02', '2012/01/03'], colmap['Last Modified'])
5468
69 def test_empty_sparklines(self):
70 sparklines = get_sparklines([])
71 self.assertEqual('', sparklines['maxes'])
72 self.assertEqual('', sparklines['mins'])
73 self.assertEqual('', sparklines['means'])
74
75 def test_single_sparklines(self):
76 sparklines = get_sparklines([{'max': 1, 'min': 2, 'mean': 3}])
77 self.assertEqual('1', sparklines['maxes'])
78 self.assertEqual('2', sparklines['mins'])
79 self.assertEqual('3', sparklines['means'])
80
81 def test_sparklines(self):
82 sparklines = get_sparklines([
83 {'max': 1, 'min': 1, 'mean': 1},
84 {'max': 1, 'min': 2, 'mean': 3}])
85 self.assertEqual('1,1', sparklines['maxes'])
86 self.assertEqual('1,2', sparklines['mins'])
87 self.assertEqual('1,3', sparklines['means'])
5588
56class CharmStoreMissingTestCase(ViewTestBase):89class CharmStoreMissingTestCase(ViewTestBase):
5790
5891
=== modified file 'charmworld/views/tools.py'
--- charmworld/views/tools.py 2013-09-03 14:15:49 +0000
+++ charmworld/views/tools.py 2013-09-17 15:22:38 +0000
@@ -1,5 +1,7 @@
1# Copyright 2012, 2013 Canonical Ltd. This software is licensed under the1# Copyright 2012, 2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
3import datetime
4import math
35
4import pymongo6import pymongo
5from pyramid.view import view_config7from pyramid.view import view_config
@@ -15,6 +17,21 @@
15REPORT_CACHE = 18017REPORT_CACHE = 180
1618
1719
20def get_sparklines(stats):
21 mins = []
22 maxes = []
23 means = []
24 for s in stats:
25 mins.append(str(s['min']))
26 means.append(str(s['mean']))
27 maxes.append(str(s['max']))
28
29 return {
30 'mins': ','.join(mins),
31 'means': ','.join(means),
32 'maxes': ','.join(maxes)}
33
34
18@cached_view_config(35@cached_view_config(
19 route_name="tools",36 route_name="tools",
20 renderer="charmworld:templates/tools.pt")37 renderer="charmworld:templates/tools.pt")
@@ -43,7 +60,27 @@
43 queue = list(60 queue = list(
44 request.db.review_queue.find(61 request.db.review_queue.find(
45 sort=[('date_modified', pymongo.ASCENDING)]))62 sort=[('date_modified', pymongo.ASCENDING)]))
46 return {'queue': queue, 'age_format': utils.pretty_timedelta}63 stats = list(request.db.review_queue_latency.find(
64 date={"$gte": datetime.datetime.utcnow() - datetime.timedelta(180)},
65 sort=[('date_modified', pymongo.ASCENDING)]))
66 sparklines = get_sparklines(stats)
67 if stats:
68 current_stats = stats[0]
69 max_latency = utils.prettify_timedelta(
70 datetime.timedelta(seconds=current_stats['max']))
71 min_latency = utils.prettify_timedelta(
72 datetime.timedelta(seconds=current_stats['min']))
73 mean_latency = utils.prettify_timedelta(
74 datetime.timedelta(seconds=current_stats['mean']))
75 else:
76 max_latency = min_latency = mean_latency = 'N/A'
77 return {
78 'queue': queue,
79 'age_format': utils.pretty_timedelta,
80 'max': max_latency,
81 'mean': mean_latency,
82 'min': min_latency,
83 'sparklines': sparklines}
4784
4885
49@view_config(86@view_config(

Subscribers

People subscribed via source and target branches