Merge lp:~salgado/launchpad-work-items-tracker/blueprints-over-time into lp:~linaro-automation/launchpad-work-items-tracker/linaro
- blueprints-over-time
- Merge into linaro
Proposed by
Guilherme Salgado
Status: | Merged |
---|---|
Merged at revision: | 316 |
Proposed branch: | lp:~salgado/launchpad-work-items-tracker/blueprints-over-time |
Merge into: | lp:~linaro-automation/launchpad-work-items-tracker/linaro |
Diff against target: |
793 lines (+478/-33) 15 files modified
collect (+2/-0) collect_roadmap (+9/-0) html-report (+1/-0) lpworkitems/collect_roadmap.py (+26/-0) lpworkitems/database.py (+17/-3) lpworkitems/factory.py (+32/-1) lpworkitems/models.py (+26/-5) lpworkitems/models_roadmap.py (+15/-0) lpworkitems/tests/test_collect.py (+0/-1) lpworkitems/tests/test_collect_roadmap.py (+23/-5) lpworkitems/tests/test_factory.py (+1/-1) lpworkitems/tests/test_models.py (+17/-2) report_tools.py (+46/-15) roadmap-bp-chart (+254/-0) templates/roadmap_lane.html (+9/-0) |
To merge this branch: | bzr merge lp:~salgado/launchpad-work-items-tracker/blueprints-over-time |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mattias Backman (community) | Approve | ||
Review via email: mp+85921@code.launchpad.net |
Commit message
Description of the change
This branch adds a new table which stores the number of blueprints per (day, status, lane). This is then used on the roadmap page to show a graph of blueprints per status/day for the quarter.
To post a comment you must log in.
Revision history for this message
Mattias Backman (mabac) wrote : | # |
- 326. By Guilherme Salgado
-
merge linaro branch
- 327. By Guilherme Salgado
-
Improve a docstring
- 328. By Guilherme Salgado
-
Rework the tests and fix blueprints_
over_time( ) - 329. By Guilherme Salgado
-
Fix blueprints_
over_time - 330. By Guilherme Salgado
-
Remove some commented out code
- 331. By Guilherme Salgado
-
Re-add a test assertion I accidentally removed
- 332. By Guilherme Salgado
-
Remove an unnecessary import
Revision history for this message
Mattias Backman (mabac) wrote : | # |
No more staging artifacts that I can find.
review:
Approve
- 333. By Guilherme Salgado
-
Create a new function in collect_roadmap which takes care of deleting and recreating the blueprint status per day
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'collect' |
2 | --- collect 2011-12-07 09:03:27 +0000 |
3 | +++ collect 2011-12-15 18:49:25 +0000 |
4 | @@ -755,6 +755,8 @@ |
5 | |
6 | # reset status for current day |
7 | collector.clear_todays_workitems() |
8 | + # We can delete all blueprints while keeping work items for previous days |
9 | + # because there's no foreign key reference from WorkItem to Blueprint. |
10 | collector.clear_blueprints() |
11 | collector.clear_metas() |
12 | collector.clear_complexitys() |
13 | |
14 | === modified file 'collect_roadmap' |
15 | --- collect_roadmap 2011-12-15 12:59:35 +0000 |
16 | +++ collect_roadmap 2011-12-15 18:49:25 +0000 |
17 | @@ -245,6 +245,13 @@ |
18 | logger.setLevel(logging.DEBUG) |
19 | |
20 | |
21 | +def update_todays_blueprint_daily_count_per_state(collector): |
22 | + """Clear today's entries and create them again to reflect the current |
23 | + state of blueprints.""" |
24 | + collector.clear_todays_blueprint_daily_count_per_state() |
25 | + collector.store_roadmap_bp_count_per_state() |
26 | + |
27 | + |
28 | def main(): |
29 | report_tools.fix_stdouterr() |
30 | |
31 | @@ -274,6 +281,8 @@ |
32 | |
33 | kanban_import(collector, cfg, opts.board, opts.api_token) |
34 | |
35 | + update_todays_blueprint_daily_count_per_state(collector) |
36 | + |
37 | store.commit() |
38 | |
39 | os.unlink(lock_path) |
40 | |
41 | === modified file 'html-report' |
42 | --- html-report 2011-12-07 09:03:27 +0000 |
43 | +++ html-report 2011-12-15 18:49:25 +0000 |
44 | @@ -495,6 +495,7 @@ |
45 | data.update(dict(page_type="roadmap_lane")) |
46 | data.update(dict(lane_title=title)) |
47 | data.update(dict(lanes=lanes)) |
48 | + data.update(dict(chart_url=opts.chart_url)) |
49 | print report_tools.fill_template( |
50 | "roadmap_lane.html", data, theme=opts.theme) |
51 | |
52 | |
53 | === modified file 'lpworkitems/collect_roadmap.py' |
54 | --- lpworkitems/collect_roadmap.py 2011-12-13 09:24:21 +0000 |
55 | +++ lpworkitems/collect_roadmap.py 2011-12-15 18:49:25 +0000 |
56 | @@ -1,3 +1,5 @@ |
57 | +import datetime |
58 | + |
59 | from lpworkitems import models_roadmap |
60 | from utils import unicode_or_None |
61 | |
62 | @@ -24,6 +26,30 @@ |
63 | def store_card(self, card): |
64 | self.store.add(card) |
65 | |
66 | + def clear_todays_blueprint_daily_count_per_state(self): |
67 | + self._clear_all( |
68 | + models_roadmap.BlueprintDailyCountPerState, |
69 | + models_roadmap.BlueprintDailyCountPerState.day == datetime.date.today()) |
70 | + |
71 | + def store_roadmap_bp_count_per_state(self): |
72 | + query = """ |
73 | + SELECT implementation, lane_id, count(*) |
74 | + FROM specs |
75 | + JOIN meta on spec = specs.name |
76 | + JOIN card on roadmap_id = value |
77 | + WHERE key = 'Roadmap id' |
78 | + GROUP BY implementation, lane_id |
79 | + """ |
80 | + day = datetime.date.today() |
81 | + result = self.store.execute(query) |
82 | + for status, lane_id, count in result: |
83 | + obj = models_roadmap.BlueprintDailyCountPerState() |
84 | + obj.day = day |
85 | + obj.status = status |
86 | + obj.lane_id = lane_id |
87 | + obj.count = count |
88 | + self.store.add(obj) |
89 | + |
90 | |
91 | def get_json_item(data, item_name): |
92 | item = data[item_name] |
93 | |
94 | === modified file 'lpworkitems/database.py' |
95 | --- lpworkitems/database.py 2011-11-03 15:48:13 +0000 |
96 | +++ lpworkitems/database.py 2011-12-15 18:49:25 +0000 |
97 | @@ -7,13 +7,13 @@ |
98 | store.execute('''CREATE TABLE version ( |
99 | db_layout_ref INT NOT NULL |
100 | )''') |
101 | - store.execute('''INSERT INTO version VALUES (14)''') |
102 | + store.execute('''INSERT INTO version VALUES (15)''') |
103 | |
104 | store.execute('''CREATE TABLE specs ( |
105 | name VARCHAR(255) PRIMARY KEY, |
106 | url VARCHAR(1000) NOT NULL, |
107 | priority CHAR(20), |
108 | - implementation CHAR(30), |
109 | + implementation CHAR(30) NOT NULL, |
110 | assignee CHAR(50), |
111 | team CHAR(50), |
112 | status VARCHAR(5000) NOT NULL, |
113 | @@ -25,6 +25,13 @@ |
114 | roadmap_notes VARCHAR(5000) |
115 | )''') |
116 | |
117 | + store.execute('''CREATE TABLE spec_daily_count_per_state ( |
118 | + status VARCHAR(5000) NOT NULL, |
119 | + day DATE NOT NULL, |
120 | + lane_id REFERENCES lane(lane_id), |
121 | + count INT NOT NULL |
122 | + )''') |
123 | + |
124 | store.execute('''CREATE TABLE work_items ( |
125 | description VARCHAR(1000) NOT NULL, |
126 | spec VARCHAR(255) REFERENCES specs(name), |
127 | @@ -234,7 +241,14 @@ |
128 | store.execute('ALTER TABLE card ADD COLUMN url VARCHAR(200)') |
129 | store.execute('ALTER TABLE card ADD COLUMN is_healthy BOOLEAN') |
130 | store.execute('UPDATE version SET db_layout_ref = 14') |
131 | - |
132 | + if ver == 14: |
133 | + store.execute('''CREATE TABLE spec_daily_count_per_state ( |
134 | + status VARCHAR(5000) NOT NULL, |
135 | + day DATE NOT NULL, |
136 | + lane_id REFERENCES lane(lane_id), |
137 | + count INT NOT NULL |
138 | + )''') |
139 | + store.execute('UPDATE version SET db_layout_ref = 15') |
140 | |
141 | def get_store(dbpath): |
142 | '''Open/initialize database. |
143 | |
144 | === modified file 'lpworkitems/factory.py' |
145 | --- lpworkitems/factory.py 2011-06-14 22:00:21 +0000 |
146 | +++ lpworkitems/factory.py 2011-12-15 18:49:25 +0000 |
147 | @@ -11,6 +11,7 @@ |
148 | TeamStructure, |
149 | Workitem, |
150 | ) |
151 | +from lpworkitems.models_roadmap import BlueprintDailyCountPerState, Card |
152 | |
153 | |
154 | class Factory(object): |
155 | @@ -63,6 +64,8 @@ |
156 | url = self.getUniqueUnicode(prefix=name+"_url") |
157 | if status is None: |
158 | status = self.getUniqueUnicode(prefix=name+"_status") |
159 | + if implementation is None: |
160 | + implementation = u'Unknown' |
161 | blueprint.name = name |
162 | blueprint.url = url |
163 | blueprint.status = status |
164 | @@ -109,8 +112,11 @@ |
165 | self.store.add(workitem) |
166 | return workitem |
167 | |
168 | - def make_meta(self, store=True): |
169 | + def make_meta(self, key=None, value=None, blueprint=None, store=True): |
170 | meta = Meta() |
171 | + meta.key = key |
172 | + meta.value = value |
173 | + meta.blueprint = blueprint |
174 | if store: |
175 | self.store.add(meta) |
176 | return meta |
177 | @@ -155,3 +161,28 @@ |
178 | if store: |
179 | self.store.add(person) |
180 | return person |
181 | + |
182 | + def make_blueprint_daily_count_per_state(self, status=None, count=1, |
183 | + day=None, store=True): |
184 | + if status is None: |
185 | + status = self.getUniqueUnicode() |
186 | + if day is None: |
187 | + day = datetime.date.today() |
188 | + obj = BlueprintDailyCountPerState() |
189 | + obj.day = day |
190 | + obj.status = status |
191 | + obj.count = count |
192 | + obj.lane_id = 1 |
193 | + if store: |
194 | + self.store.add(obj) |
195 | + return obj |
196 | + |
197 | + def make_card(self, store=True): |
198 | + name = self.getUniqueUnicode() |
199 | + card_id = self.getUniqueInteger() |
200 | + lane_id = self.getUniqueInteger() |
201 | + roadmap_id = self.getUniqueUnicode() |
202 | + card = Card(name, card_id, lane_id, roadmap_id) |
203 | + if store: |
204 | + self.store.add(card) |
205 | + return card |
206 | |
207 | === modified file 'lpworkitems/models.py' |
208 | --- lpworkitems/models.py 2011-12-07 09:03:27 +0000 |
209 | +++ lpworkitems/models.py 2011-12-15 18:49:25 +0000 |
210 | @@ -2,7 +2,15 @@ |
211 | import re |
212 | from utils import unicode_or_None |
213 | |
214 | -from storm.locals import Date, Reference, ReferenceSet, Unicode |
215 | +from storm.locals import Date, Int, Reference, ReferenceSet, Unicode |
216 | + |
217 | +ROADMAP_STATUSES_MAP = { |
218 | + u'Completed': [u'Implemented'], |
219 | + u'Blocked': [u'Needs Infrastructure', u'Blocked', u'Deferred'], |
220 | + u'In Progress': [u'Deployment', u'Needs Code Review', |
221 | + u'Beta Available', u'Good progress', |
222 | + u'Slow progress', u'Started'], |
223 | + u'Planned': [u'Unknown', u'Not started', u'Informational']} |
224 | |
225 | |
226 | def fill_blueprint_info_from_launchpad(model_bp, lp_bp): |
227 | @@ -48,6 +56,14 @@ |
228 | project = Unicode() |
229 | |
230 | |
231 | +def get_roadmap_status_for_bp_implementation_status(implementation): |
232 | + for key in ROADMAP_STATUSES_MAP: |
233 | + if implementation in ROADMAP_STATUSES_MAP[key]: |
234 | + return key |
235 | + # XXX: Is None the appropriate return value here? |
236 | + return None |
237 | + |
238 | + |
239 | class Blueprint(object): |
240 | |
241 | __storm_table__ = "specs" |
242 | @@ -81,6 +97,15 @@ |
243 | lp_bp.whiteboard, "Roadmap\s+Notes") |
244 | return model_bp |
245 | |
246 | + @property |
247 | + def roadmap_status(self): |
248 | + return get_roadmap_status_for_bp_implementation_status( |
249 | + self.implementation) |
250 | + |
251 | + |
252 | +def current_date(): |
253 | + return datetime.date.today() |
254 | + |
255 | |
256 | class Person(object): |
257 | |
258 | @@ -108,10 +133,6 @@ |
259 | superteam_name = Unicode(name="team") |
260 | |
261 | |
262 | -def current_date(): |
263 | - return datetime.date.today() |
264 | - |
265 | - |
266 | class Meta(object): |
267 | |
268 | __storm_table__ = "meta" |
269 | |
270 | === modified file 'lpworkitems/models_roadmap.py' |
271 | --- lpworkitems/models_roadmap.py 2011-11-03 15:48:13 +0000 |
272 | +++ lpworkitems/models_roadmap.py 2011-12-15 18:49:25 +0000 |
273 | @@ -4,6 +4,8 @@ |
274 | |
275 | from storm.locals import Date, Reference, ReferenceSet, Unicode, Int, Bool |
276 | |
277 | +from lpworkitems import models |
278 | + |
279 | |
280 | class Card(object): |
281 | |
282 | @@ -43,3 +45,16 @@ |
283 | def __init__(self, name, lane_id): |
284 | self.lane_id = lane_id |
285 | self.name = name |
286 | + |
287 | + |
288 | +def current_date(): |
289 | + return datetime.date.today() |
290 | + |
291 | + |
292 | +class BlueprintDailyCountPerState(object): |
293 | + __storm_table__ = 'spec_daily_count_per_state' |
294 | + __storm_primary__ = 'status', 'day' |
295 | + day = Date(default_factory=current_date) |
296 | + status = Unicode() |
297 | + lane_id = Int() |
298 | + count = Int() |
299 | |
300 | === modified file 'lpworkitems/tests/test_collect.py' |
301 | --- lpworkitems/tests/test_collect.py 2011-06-14 22:00:21 +0000 |
302 | +++ lpworkitems/tests/test_collect.py 2011-12-15 18:49:25 +0000 |
303 | @@ -184,7 +184,6 @@ |
304 | self.store.find( |
305 | Milestone, Milestone.name == name).one()) |
306 | |
307 | - |
308 | def test_store_blueprint_stores_blueprint(self): |
309 | blueprint = self.factory.make_blueprint(store=False) |
310 | ret = self.collector.store_blueprint(blueprint) |
311 | |
312 | === modified file 'lpworkitems/tests/test_collect_roadmap.py' |
313 | --- lpworkitems/tests/test_collect_roadmap.py 2011-12-13 09:24:21 +0000 |
314 | +++ lpworkitems/tests/test_collect_roadmap.py 2011-12-15 18:49:25 +0000 |
315 | @@ -1,15 +1,13 @@ |
316 | +import datetime |
317 | + |
318 | from lpworkitems.collect_roadmap import ( |
319 | CollectorStore, |
320 | get_json_item, |
321 | - lookup_kanban_priority, |
322 | ) |
323 | +from lpworkitems.models_roadmap import BlueprintDailyCountPerState |
324 | from lpworkitems.error_collector import ( |
325 | ErrorCollector, |
326 | ) |
327 | -from lpworkitems.models_roadmap import ( |
328 | - Card, |
329 | - Lane, |
330 | - ) |
331 | from lpworkitems.testing import TestCaseWithFakeLaunchpad |
332 | |
333 | |
334 | @@ -26,6 +24,26 @@ |
335 | fn() |
336 | self.assertEqual(0, self.store.find(cls).count()) |
337 | |
338 | + def test_clear_todays_blueprint_daily_count_per_state(self): |
339 | + self.factory.make_blueprint_daily_count_per_state( |
340 | + day=datetime.date.today()) |
341 | + self.assertClears( |
342 | + BlueprintDailyCountPerState, |
343 | + self.collector.clear_todays_blueprint_daily_count_per_state) |
344 | + |
345 | + def test_store_roadmap_bp_count_per_state(self): |
346 | + bp = self.factory.make_blueprint() |
347 | + card = self.factory.make_card() |
348 | + meta = self.factory.make_meta( |
349 | + key=u'Roadmap id', value=card.roadmap_id, blueprint=bp) |
350 | + self.collector.store_roadmap_bp_count_per_state() |
351 | + self.assertEqual( |
352 | + 1, self.store.find(BlueprintDailyCountPerState).count()) |
353 | + entry = self.store.find(BlueprintDailyCountPerState).one() |
354 | + self.assertEqual(1, entry.count) |
355 | + self.assertEqual(card.lane_id, entry.lane_id) |
356 | + self.assertEqual(bp.implementation, entry.status) |
357 | + |
358 | # XXX Add tests for the roadmap classes. |
359 | |
360 | |
361 | |
362 | === modified file 'lpworkitems/tests/test_factory.py' |
363 | --- lpworkitems/tests/test_factory.py 2011-06-04 18:48:23 +0000 |
364 | +++ lpworkitems/tests/test_factory.py 2011-12-15 18:49:25 +0000 |
365 | @@ -162,7 +162,7 @@ |
366 | implementation = u"Implemented" |
367 | self.assert_with_and_without( |
368 | self.factory.make_blueprint, "implementation", implementation, |
369 | - Equals(None)) |
370 | + Equals("Unknown")) |
371 | |
372 | def test_uses_assignee_name(self): |
373 | assignee_name = self.factory.getUniqueUnicode( |
374 | |
375 | === modified file 'lpworkitems/tests/test_models.py' |
376 | --- lpworkitems/tests/test_models.py 2011-12-06 15:20:43 +0000 |
377 | +++ lpworkitems/tests/test_models.py 2011-12-15 18:49:25 +0000 |
378 | @@ -6,8 +6,11 @@ |
379 | extract_last_path_segment_from_url, |
380 | extract_user_name_from_url, |
381 | get_whiteboard_section, |
382 | - ) |
383 | -from lpworkitems.testing import TestCaseWithFakeLaunchpad |
384 | + ROADMAP_STATUSES_MAP, |
385 | + ) |
386 | +from lpworkitems.testing import ( |
387 | + TestCaseWithFakeLaunchpad, |
388 | + ) |
389 | |
390 | |
391 | class GetWhiteboardSectionTests(TestCase): |
392 | @@ -42,6 +45,18 @@ |
393 | |
394 | class BlueprintTests(TestCaseWithFakeLaunchpad): |
395 | |
396 | + def test_roadmap_status(self): |
397 | + roadmap_status = "Completed" |
398 | + bp_implementation = ROADMAP_STATUSES_MAP[roadmap_status][0] |
399 | + bp_status = self.factory.make_blueprint( |
400 | + implementation=bp_implementation) |
401 | + self.assertEqual(roadmap_status, bp_status.roadmap_status) |
402 | + |
403 | + def test_roadmap_status_unknown_status(self): |
404 | + blueprint = self.factory.make_blueprint( |
405 | + implementation=u"Not Expected") |
406 | + self.assertEqual(None, blueprint.roadmap_status) |
407 | + |
408 | def test_from_launchpad_sets_name(self): |
409 | name = self.factory.getUniqueUnicode(prefix="lpblueprint") |
410 | lp_bp = self.lp.make_blueprint(name=name) |
411 | |
412 | === modified file 'report_tools.py' |
413 | --- report_tools.py 2011-12-12 07:35:35 +0000 |
414 | +++ report_tools.py 2011-12-15 18:49:25 +0000 |
415 | @@ -14,7 +14,8 @@ |
416 | Card, |
417 | ) |
418 | from lpworkitems.models import ( |
419 | - Meta, |
420 | + Meta, ROADMAP_STATUSES_MAP, |
421 | + get_roadmap_status_for_bp_implementation_status, |
422 | ) |
423 | |
424 | valid_states = [u'inprogress', u'blocked', u'todo', u'done', u'postponed'] |
425 | @@ -206,12 +207,17 @@ |
426 | def roadmap_pages(my_path, database, basename, config, lane, root=None): |
427 | cfg = load_config(config) |
428 | fh = open(basename + '.html', 'w') |
429 | + # XXX fix this. the intention is to have the chart in the same dir as the |
430 | + # html. This is not essential but makes linking in roadmap_lane.html easier |
431 | + chart_name = '/'.join(basename.split('/')[:-1]) + '/current_quarter.svg' |
432 | try: |
433 | args = [os.path.join(my_path, 'html-report'), '-d', database] |
434 | args += ['--report-type', 'roadmap_page'] |
435 | args += ['--lane', lane.name] |
436 | if root: |
437 | args += ['--root', root] |
438 | + if lane.is_current: |
439 | + args += ['--chart', chart_name] |
440 | report_args(args, theme=get_theme(cfg)) |
441 | proc = Popen(args, stdout=fh) |
442 | print basename + '.html' |
443 | @@ -219,6 +225,13 @@ |
444 | finally: |
445 | fh.close() |
446 | |
447 | + if lane.is_current: |
448 | + args = [os.path.join(my_path, 'roadmap-bp-chart'), '-d', database, |
449 | + '-o', chart_name] |
450 | + proc = Popen(args) |
451 | + print chart_name |
452 | + proc.wait() |
453 | + |
454 | |
455 | def roadmap_cards(my_path, database, basename, config, card, root=None): |
456 | cfg = load_config(config) |
457 | @@ -341,6 +354,34 @@ |
458 | return escape(html, True) |
459 | |
460 | |
461 | +def blueprints_over_time(store): |
462 | + '''Calculate blueprint development over time for the current lane. |
463 | + |
464 | + We do not need to care about teams or groups since this is intended for the |
465 | + roadmap overview. |
466 | + |
467 | + Return date -> state -> count mapping. states are |
468 | + {planned,inprogress,completed,blocked}. |
469 | + ''' |
470 | + data = {} |
471 | + result = store.execute(""" |
472 | + SELECT status, day, count |
473 | + FROM spec_daily_count_per_state |
474 | + JOIN lane on lane.lane_id = spec_daily_count_per_state.lane_id |
475 | + WHERE lane.is_current = 1 |
476 | + """) |
477 | + for status, day, count in result: |
478 | + roadmap_status = get_roadmap_status_for_bp_implementation_status( |
479 | + status) |
480 | + assert roadmap_status is not None |
481 | + if day not in data: |
482 | + data[day] = {} |
483 | + if roadmap_status not in data[day]: |
484 | + data[day][roadmap_status] = 0 |
485 | + data[day][roadmap_status] += count |
486 | + return data |
487 | + |
488 | + |
489 | def workitems_over_time(store, team=None, group=None, milestone_collection=None): |
490 | '''Calculate work item development over time. |
491 | |
492 | @@ -978,21 +1019,11 @@ |
493 | |
494 | def card_blueprints_by_status(store, roadmap_id): |
495 | blueprints = card_blueprints(store, roadmap_id) |
496 | - statuses = {'Completed': ['Implemented'], |
497 | - 'Blocked': ['Needs Infrastructure', 'Blocked', 'Deferred'], |
498 | - 'In Progress': ['Deployment', 'Needs Code Review', |
499 | - 'Beta Available', 'Good progress', |
500 | - 'Slow progress', 'Started'], |
501 | - 'Planned': ['Unknown', 'Not started', 'Informational']} |
502 | - bp_by_status = {'In Progress': [], |
503 | - 'Blocked': [], |
504 | - 'Planned': [], |
505 | - 'Completed': []} |
506 | + bp_by_status = {} |
507 | + for key in ROADMAP_STATUSES_MAP: |
508 | + bp_by_status[key] = [] |
509 | for bp in blueprints: |
510 | - for status in statuses.iterkeys(): |
511 | - if bp.implementation in statuses[status]: |
512 | - bp_by_status[status].append(bp) |
513 | - break |
514 | + bp_by_status[bp.roadmap_status].append(bp) |
515 | return bp_by_status |
516 | |
517 | |
518 | |
519 | === added file 'roadmap-bp-chart' |
520 | --- roadmap-bp-chart 1970-01-01 00:00:00 +0000 |
521 | +++ roadmap-bp-chart 2011-12-15 18:49:25 +0000 |
522 | @@ -0,0 +1,254 @@ |
523 | +#!/usr/bin/python |
524 | +# |
525 | +# Create a blueprint tracking chart from a blueprint database. |
526 | +# |
527 | +# Copyright (C) 2010, 2011 Canonical Ltd. |
528 | +# License: GPL-3 |
529 | + |
530 | +import optparse, datetime, sys |
531 | +import report_tools |
532 | + |
533 | +from pychart import * |
534 | + |
535 | +def date_to_ordinal(s): |
536 | + '''Turn yyyy-mm-dd strings to ordinals''' |
537 | + return report_tools.date_to_python(s).toordinal() |
538 | + |
539 | + |
540 | +def ordinal_to_date(ordinal): |
541 | + '''Turn an ordinal date into a string''' |
542 | + d = datetime.date.fromordinal(int(ordinal)) |
543 | + return d.strftime('%Y-%m-%d') |
544 | + |
545 | +def format_date(ordinal): |
546 | + d = datetime.date.fromordinal(int(ordinal)) |
547 | + return '/a60{}' + d.strftime('%b %d, %y') |
548 | + |
549 | +def do_chart(data, start_date, end_date, trend_start, title, filename, only_weekdays, inverted): |
550 | + #set up default values |
551 | + format = 'svg' |
552 | + height = 450 |
553 | + width = 1000 |
554 | + legend_x = 700 |
555 | + legend_y = 200 |
556 | + title_x = 300 |
557 | + title_y = 350 |
558 | + |
559 | + if inverted: |
560 | + legend_x=200 |
561 | + |
562 | + # Tell pychart to use colors |
563 | + theme.use_color = True |
564 | + theme.default_font_size = 12 |
565 | + theme.reinitialize() |
566 | + |
567 | + # turn into pychart data model and calculate maximum number of WIs |
568 | + max_items = 1 # start at 1 to avoid zero div |
569 | + lastactive = 0 |
570 | + pcdata = [] |
571 | + |
572 | + for date in xrange(date_to_ordinal(start_date), date_to_ordinal(end_date)+1): |
573 | + if (not only_weekdays or datetime.date.fromordinal(date).weekday() < 5): |
574 | + end_date = ordinal_to_date(date) |
575 | + i = data.get(ordinal_to_date(date), {}) |
576 | + count = i.get('Completed', 0) + i.get('Planned', 0) + i.get('Blocked', 0) + i.get('In Progress', 0) |
577 | + if max_items < count: |
578 | + max_items = count |
579 | + pcdata.append((date, i.get('Planned', 0),0, |
580 | + i.get('Blocked', 0),0, |
581 | + i.get('In Progress', 0),0, |
582 | + i.get('Completed',0),0, count)) |
583 | + if count > 0: |
584 | + lastactive = len(pcdata) - 1 |
585 | + |
586 | + # add some extra space to look nicer |
587 | + max_items = int(max_items * 1.05) |
588 | + |
589 | + x_interval = len(pcdata)/20 |
590 | + if max_items > 500: |
591 | + y_interval = max_items/200*10 |
592 | + elif max_items < 20: |
593 | + y_interval = 1 |
594 | + else: |
595 | + y_interval = max_items/20 |
596 | + |
597 | + # create the chart object |
598 | + chart_object.set_defaults(area.T, size=(width, height), |
599 | + y_range=(0, None), x_coord=category_coord.T(pcdata, 0)) |
600 | + |
601 | + # tell the chart object it will use a bar chart, and will |
602 | + # use the data list for it's model |
603 | + chart_object.set_defaults(bar_plot.T, data=pcdata) |
604 | + |
605 | + # create the chart area |
606 | + # tell it to start at coords 0,0 |
607 | + # tell it the labels, and the tics, etc.. |
608 | + # HACK: to prevent 0 div |
609 | + if max_items == 0: |
610 | + max_items = 1 |
611 | + ar = area.T(legend=legend.T(loc=(legend_x,legend_y)), loc=(0,0), |
612 | + x_axis=axis.X(label='Date', tic_interval=x_interval,format=format_date), |
613 | + y_axis=axis.Y(label='Blueprints', tic_interval=y_interval), |
614 | + y_range=(0, max_items)) |
615 | + |
616 | + #initialize the blar_plot fill styles |
617 | + bar_plot.fill_styles.reset() |
618 | + |
619 | + # create each set of data to plot |
620 | + # note that index zero is the label col |
621 | + # for each column of data, tell it what to use for the legend and |
622 | + # what color to make the bar, no lines, and |
623 | + # what plot to stack on |
624 | + |
625 | + tlabel = '' |
626 | + |
627 | + if inverted: |
628 | + plot1 = bar_plot.T(label='Completed' + tlabel, hcol=7) |
629 | + plot1.fill_style = fill_style.Plain(bgcolor=color.seagreen) |
630 | + |
631 | + plot3 = bar_plot.T(label='In Progress' + tlabel, hcol=5, stack_on = plot1) |
632 | + plot3.fill_style = fill_style.Plain(bgcolor=color.gray65) |
633 | + |
634 | + plot5 = bar_plot.T(label='Blocked' + tlabel, hcol=3, stack_on = plot3) |
635 | + plot5.fill_style = fill_style.Plain(bgcolor=color.red1) |
636 | + |
637 | + plot7 = bar_plot.T(label='Planned' + tlabel, hcol=1, stack_on = plot5) |
638 | + plot7.fill_style = fill_style.Plain(bgcolor=color.darkorange1) |
639 | + else: |
640 | + plot1 = bar_plot.T(label='Planned' + tlabel, hcol=1) |
641 | + plot1.fill_style = fill_style.Plain(bgcolor=color.darkorange1) |
642 | + |
643 | + plot3 = bar_plot.T(label='Blocked' + tlabel, hcol=3, stack_on = plot1) |
644 | + plot3.fill_style = fill_style.Plain(bgcolor=color.red1) |
645 | + |
646 | + plot5 = bar_plot.T(label='In Progress' + tlabel, hcol=5, stack_on = plot3) |
647 | + plot5.fill_style = fill_style.Plain(bgcolor=color.gray65) |
648 | + |
649 | + plot7 = bar_plot.T(label='Completed' + tlabel, hcol=7, stack_on = plot5) |
650 | + plot7.fill_style = fill_style.Plain(bgcolor=color.seagreen) |
651 | + |
652 | + |
653 | + plot1.line_style = None |
654 | + plot3.line_style = None |
655 | + plot5.line_style = None |
656 | + plot7.line_style = None |
657 | + |
658 | + plot11 = bar_plot.T(label='total', hcol=9) |
659 | + plot11.fill_style = None |
660 | + plot11.line_style = line_style.gray30 |
661 | + |
662 | + # create the canvas with the specified filename and file format |
663 | + can = canvas.init(filename,format) |
664 | + |
665 | + # add the data to the area and draw it |
666 | + ar.add_plot(plot1, plot3, plot5, plot7) |
667 | + ar.draw() |
668 | + |
669 | + # title |
670 | + tb = text_box.T(loc=(title_x, title_y), text=title, line_style=None) |
671 | + tb.fill_style = None |
672 | + tb.draw() |
673 | + |
674 | +# |
675 | +# main |
676 | +# |
677 | + |
678 | +# argv parsing |
679 | +optparser = optparse.OptionParser() |
680 | +optparser.add_option('-d', '--database', |
681 | + help='Path to database', dest='database', metavar='PATH') |
682 | +optparser.add_option('-t', '--team', |
683 | + help='Restrict report to a particular team', dest='team') |
684 | +optparser.add_option('-m', '--milestone', |
685 | + help='Restrict report to a particular milestone', dest='milestone') |
686 | +optparser.add_option('-o', '--output', |
687 | + help='Output file', dest='output') |
688 | +optparser.add_option('--trend-start', type='int', |
689 | + help='Explicitly set start of trend line', dest='trendstart') |
690 | +optparser.add_option('-u', '--user', |
691 | + help='Run for this user', dest='user') |
692 | +optparser.add_option('--only-weekdays', action='store_true', |
693 | + help='Skip Saturdays and Sundays in the resulting graph', dest='only_weekdays') |
694 | +optparser.add_option('--inverted', action='store_true', |
695 | + help='Generate an inverted burndown chart', dest='inverted') |
696 | +optparser.add_option('-s', '--start-date', |
697 | + help='Explicitly set the start date of the burndown data', dest='start_date') |
698 | +optparser.add_option('-e', '--end-date', |
699 | + help='Explicitly set the end date of the burndown data', dest='end_date') |
700 | +optparser.add_option('--no-foreign', action='store_true', default=False, |
701 | + help='Do not show foreign totals separate', dest='noforeign') |
702 | +optparser.add_option('--group', |
703 | + help='Run for this group', dest='group') |
704 | +optparser.add_option('--date', |
705 | + help='Run for this date', dest='date') |
706 | + |
707 | +(opts, args) = optparser.parse_args() |
708 | +if not opts.database: |
709 | + optparser.error('No database given') |
710 | +if not opts.output: |
711 | + optparser.error('No output file given') |
712 | + |
713 | +if opts.user and opts.team: |
714 | + optparser.error('team and user options are mutually exclusive') |
715 | +if opts.user and opts.group: |
716 | + optparser.error('user and group options are mutually exclusive') |
717 | +if opts.team and opts.group: |
718 | + optparser.error('team and group options are mutually exclusive') |
719 | +if opts.milestone and opts.date: |
720 | + optparser.error('milestone and date options are mutually exclusive') |
721 | + |
722 | +# The typing allows polymorphic behavior |
723 | +if opts.user: |
724 | + opts.team = report_tools.user_string(opts.user) |
725 | +elif opts.team: |
726 | + opts.team = report_tools.team_string(opts.team) |
727 | + |
728 | +store = report_tools.get_store(opts.database) |
729 | + |
730 | +milestone_collection = None |
731 | +if opts.milestone: |
732 | + milestone_collection = report_tools.get_milestone(store, opts.milestone) |
733 | +elif opts.date: |
734 | + milestone_collection = report_tools.MilestoneGroup( |
735 | + report_tools.date_to_python(opts.date)) |
736 | + |
737 | + |
738 | +# get date -> state -> count mapping |
739 | +data = report_tools.blueprints_over_time(store) |
740 | + |
741 | +if len(data) == 0: |
742 | + print 'WARNING: no blueprints, not generating chart (team: %s, group: %s, due date: %s)' % ( |
743 | + opts.team or 'all', opts.group or 'none', milestone_collection and milestone_collection.display_name or 'none') |
744 | + sys.exit(0) |
745 | + |
746 | +# calculate start/end date if no dates are given |
747 | +if opts.start_date is None: |
748 | + start_date = sorted(data.keys())[0] |
749 | +else: |
750 | + start_date=opts.start_date |
751 | + |
752 | +if opts.end_date is None: |
753 | + if milestone_collection is not None: |
754 | + end_date = milestone_collection.due_date_str |
755 | + else: |
756 | + end_date=report_tools.milestone_due_date(store) |
757 | +else: |
758 | + end_date=opts.end_date |
759 | + |
760 | +if not start_date or not end_date or date_to_ordinal(start_date) > date_to_ordinal(end_date): |
761 | + print 'WARNING: empty date range, not generating chart (team: %s, group: %s, due date: %s)' % ( |
762 | + opts.team or 'all', opts.group or 'none', milestone_collection and milestone_collection.display_name or 'none') |
763 | + sys.exit(0) |
764 | + |
765 | +# title |
766 | +if opts.team: |
767 | + title = '/20' + opts.team |
768 | +elif opts.group: |
769 | + title = "/20" + opts.group |
770 | +else: |
771 | + title = '/20all teams' |
772 | + |
773 | +if milestone_collection is not None: |
774 | + title += ' (%s)' % milestone_collection.name |
775 | + |
776 | +do_chart(data, start_date, end_date, opts.trendstart, title, opts.output, opts.only_weekdays, opts.inverted) |
777 | |
778 | === modified file 'templates/roadmap_lane.html' |
779 | --- templates/roadmap_lane.html 2011-12-12 09:17:20 +0000 |
780 | +++ templates/roadmap_lane.html 2011-12-15 18:49:25 +0000 |
781 | @@ -41,3 +41,12 @@ |
782 | % endfor |
783 | </table> |
784 | |
785 | +% if chart_url != 'burndown.svg': |
786 | +<!-- The cli option defaults to burndown.svg! :( --> |
787 | +<div class="overview_graph"> |
788 | +<h3>Blueprint progress</h3><p><a href="current_quarter.svg">(enlarge)</a></p> |
789 | +<object |
790 | + height="500" width="833" |
791 | + data="current_quarter.svg" type="image/svg+xml">Blueprint progress</object> |
792 | +</div> |
793 | +% endif |
On Thu, Dec 15, 2011 at 6:58 PM, Guilherme Salgado infrastructure) /code.launchpad .net/~salgado/ launchpad- work-items- tracker/ blueprints- over-time/ +merge/ 85921 /code.launchpad .net/~salgado/ launchpad- work-items- tracker/ blueprints- over-time/ +merge/ 85921 args.append( "--debug" ) args.append( "--mail" )
<email address hidden> wrote:
> Guilherme Salgado has proposed merging lp:~salgado/launchpad-work-items-tracker/blueprints-over-time into lp:~linaro-infrastructure/launchpad-work-items-tracker/linaro.
>
> Requested reviews:
> Linaro Infrastructure (linaro-
>
> For more details, see:
> https:/
>
> This branch adds a new table which stores the number of blueprints per (day, status, lane). This is then used on the roadmap page to show a graph of blueprints per status/day for the quarter.
> --
> https:/
> Your team Linaro Infrastructure is requested to review the proposed merge of lp:~salgado/launchpad-work-items-tracker/blueprints-over-time into lp:~linaro-infrastructure/launchpad-work-items-tracker/linaro.
>
> === modified file 'all-projects'
> --- all-projects 2011-12-07 09:03:27 +0000
> +++ all-projects 2011-12-15 17:57:13 +0000
> @@ -117,8 +117,6 @@
>
> if opts.debug:
> extra_collect_
> - else:
> - extra_collect_
This should only be removed on staging. It's just so we don't email
about errors while testing.
> args): write(" collect failed for %s" % project_name) clear_todays_ workitems( ) clear_blueprint s() clear_metas( ) clear_complexit ys()
> if not collect(source_dir, db_file, config_file, extra_collect_
> sys.stderr.
>
> === modified file 'collect'
> --- collect 2011-12-07 09:03:27 +0000
> +++ collect 2011-12-15 17:57:13 +0000
> @@ -6,6 +6,7 @@
> # Copyright (C) 2010, 2011 Canonical Ltd.
> # License: GPL-3
>
> +import datetime
> import urllib, re, sys, optparse, smtplib, pwd, os, urlparse
> import logging
> from email.mime.text import MIMEText
> @@ -755,6 +756,8 @@
>
> # reset status for current day
> collector.
> + # We can delete all blueprints while keeping work items for previous days
> + # because there's no foreign key reference from WorkItem to Blueprint.
> collector.
> collector.
> collector.
>
> === modified file 'collect_roadmap'
> --- collect_roadmap 2011-12-15 12:59:35 +0000
> +++ collect_roadmap 2011-12-15 17:57:13 +0000
> @@ -51,9 +51,15 @@
> except urllib2.HTTPError, e:
> print "HTTP error for url '%s': %d" % (url, e.code)
> except urllib2.URLError, e:
> +<<<<<<< TREE
> print "Network error for url '%s': %s" % (url, e.reason.args[1])
> except ValueError, e:
> print "Data error for url '%s': %s" % (url, e.message)
> +=======
> + print "Network error: %s" % e.reason.args[1] opts.database) store, '', error_collector) clear_todays_ blueprint_ daily_count_ per_state( ) clear_lanes( ) clear_cards( ) import( collector, cfg, opt...
> + except ValueError, e:
> + print "Data error: %s" % e.message
> +>>>>>>> MERGE-SOURCE
>
> return data
>
> @@ -269,11 +275,14 @@
> store = get_store(
> collector = CollectorStore(
>
> + collector.
> collector.
> collector.
>
> kanban_