Merge lp:~salgado/launchpad-work-items-tracker/blueprints-over-time into lp:~linaro-automation/launchpad-work-items-tracker/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
Reviewer Review Type Date Requested Status
Mattias Backman (community) Approve
Review via email: mp+85921@code.launchpad.net

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 :
Download full text (31.5 KiB)

On Thu, Dec 15, 2011 at 6:58 PM, Guilherme Salgado
<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-infrastructure)
>
> For more details, see:
> https://code.launchpad.net/~salgado/launchpad-work-items-tracker/blueprints-over-time/+merge/85921
>
> 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://code.launchpad.net/~salgado/launchpad-work-items-tracker/blueprints-over-time/+merge/85921
> 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_args.append("--debug")
> -        else:
> -            extra_collect_args.append("--mail")

This should only be removed on staging. It's just so we don't email
about errors while testing.

>
>         if not collect(source_dir, db_file, config_file, extra_collect_args):
>             sys.stderr.write("collect failed for %s" % project_name)
>
> === 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.clear_todays_workitems()
> +    # We can delete all blueprints while keeping work items for previous days
> +    # because there's no foreign key reference from WorkItem to Blueprint.
>     collector.clear_blueprints()
>     collector.clear_metas()
>     collector.clear_complexitys()
>
> === 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]
> +    except ValueError, e:
> +        print "Data error: %s" % e.message
> +>>>>>>> MERGE-SOURCE
>
>     return data
>
> @@ -269,11 +275,14 @@
>     store = get_store(opts.database)
>     collector = CollectorStore(store, '', error_collector)
>
> +    collector.clear_todays_blueprint_daily_count_per_state()
>     collector.clear_lanes()
>     collector.clear_cards()
>
>     kanban_import(collector, cfg, opt...

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

Subscribers

People subscribed via source and target branches