Merge lp:~work-items-tracker-hackers/launchpad-work-items-tracker/blueprints-api into lp:launchpad-work-items-tracker
- blueprints-api
- Merge into trunk
Proposed by
Martin Pitt
Status: | Merged |
---|---|
Merged at revision: | 264 |
Proposed branch: | lp:~work-items-tracker-hackers/launchpad-work-items-tracker/blueprints-api |
Merge into: | lp:launchpad-work-items-tracker |
Diff against target: |
1283 lines (+563/-435) 9 files modified
.bzrignore (+1/-0) .testr.conf (+3/-0) collect (+134/-187) generate-all (+10/-1) html-report (+140/-247) report_tools.py (+17/-0) templates/burndown.html (+241/-0) templates/test.html (+1/-0) tests.py (+16/-0) |
To merge this branch: | bzr merge lp:~work-items-tracker-hackers/launchpad-work-items-tracker/blueprints-api |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Pitt (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
This branch contains lp:~james-w/launchpad-work-items-tracker/blueprints-api, lp:~james-w/launchpad-work-items-tracker/templates, and the other excellent work from James. It requires installing python-mako on people.
To post a comment you must log in.
Revision history for this message

Martin Pitt (pitti) : | # |
review:
Approve
Revision history for this message

James Westby (james-w) wrote : | # |
Revision history for this message

Martin Pitt (pitti) wrote : | # |
It's installed now, merged. Thanks James!
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file '.bzrignore' |
2 | --- .bzrignore 1970-01-01 00:00:00 +0000 |
3 | +++ .bzrignore 2011-02-09 12:26:14 +0000 |
4 | @@ -0,0 +1,1 @@ |
5 | +.testrepository |
6 | |
7 | === added file '.testr.conf' |
8 | --- .testr.conf 1970-01-01 00:00:00 +0000 |
9 | +++ .testr.conf 2011-02-09 12:26:14 +0000 |
10 | @@ -0,0 +1,3 @@ |
11 | +[DEFAULT] |
12 | +test_command=python -m subunit.run $IDLIST |
13 | +test_id_list_default=tests.test_suite |
14 | |
15 | === modified file 'collect' |
16 | --- collect 2011-01-29 09:08:54 +0000 |
17 | +++ collect 2011-02-09 12:26:14 +0000 |
18 | @@ -3,11 +3,11 @@ |
19 | # Pull work items from various sources (blueprint whiteboards, linked blueprint |
20 | # bugs, wiki pages) and put them into a database. |
21 | |
22 | -import urllib, re, sys, optparse, smtplib, pwd, os |
23 | +import urllib, re, sys, optparse, smtplib, pwd, os, urlparse |
24 | from email.mime.text import MIMEText |
25 | import sqlite3 as dbapi2 |
26 | |
27 | -from launchpadlib.launchpad import Launchpad, EDGE_SERVICE_ROOT |
28 | +from launchpadlib.launchpad import Launchpad |
29 | |
30 | import report_tools |
31 | |
32 | @@ -59,51 +59,20 @@ |
33 | dbg('Queueing data error: ' + s) |
34 | |
35 | |
36 | +def web_link(item): |
37 | + """Get a link to the Launchpad API object on the website.""" |
38 | + api_link = item.self_link |
39 | + parts = urlparse.urlparse(api_link) |
40 | + return parts.scheme + "://" + parts.netloc.replace("api.", "") + "/" + parts.path.split("/", 2)[2] |
41 | + |
42 | + |
43 | ######################################################################## |
44 | # |
45 | # Functions for parsing Launchpad data |
46 | # |
47 | ######################################################################## |
48 | |
49 | -def lp_blueprints_from_list(url, name_pattern = None): |
50 | - '''Return blueprints from a LP list view. |
51 | - |
52 | - This can optionally specify a name pattern (which is mostly useful for |
53 | - testing and debugging, to only parse a small subset of blueprints) |
54 | - |
55 | - Return a dictionary name -> (url, milestone). |
56 | - ''' |
57 | - blueprint_name_filter = re.compile('href="(/[a-zA-Z0-9-]+/\+spec/%s[^"]+)"(?!.*class.*sprite)' % |
58 | - (name_pattern or '.')) |
59 | - milestone_re = re.compile('href=".*/[a-zA-Z0-9-]+/\+milestone/([^"]+)"') |
60 | - |
61 | - result = {} |
62 | - scan_tr_end = False |
63 | - milestone = None |
64 | - bp = None |
65 | - dbg("lp_blueprints_from_list(): Loading '%s'" % url) |
66 | - for l in urllib.urlopen(url): |
67 | - if scan_tr_end: |
68 | - m = milestone_re.search(l) |
69 | - if m: |
70 | - milestone = m.group(1) |
71 | - dbg('lp_blueprints_from_list(): current spec milestone: ' + milestone) |
72 | - if '</tr>' in l: |
73 | - scan_tr_end = False |
74 | - if bp: |
75 | - result[bp.split('/')[-1]] = (bp, milestone) |
76 | - bp = None |
77 | - milestone = None |
78 | - else: |
79 | - m = blueprint_name_filter.search(l) |
80 | - if m: |
81 | - bp = report_tools.blueprints_base_url + m.group(1) |
82 | - dbg('lp_blueprints_from_list(): found BP: ' + bp) |
83 | - scan_tr_end = True |
84 | - |
85 | - return result |
86 | - |
87 | -def lp_import_blueprint(db, name, url, contents): |
88 | +def lp_import_blueprint(db, bp): |
89 | '''Import details of a blueprint into the specs table. |
90 | |
91 | If a blueprint does not have a status, use the description. |
92 | @@ -117,56 +86,50 @@ |
93 | cur.execute('SELECT name FROM milestones') |
94 | valid_milestones = [m[0] for m in cur] + [None] |
95 | |
96 | - queries = { |
97 | - 'priority': '<dt>Priority:</dt>\s*<dd>\s*([^\n]*)', |
98 | - 'definition': '<dt>Definition:</dt>\s*<dd>\s*([^\n]*)', |
99 | - 'implementation': '<dt>Implementation:</dt>\s*<dd>\s*([^\n]*)', |
100 | - 'approver': '<dt>Approver:</dt>\s*<dd>\s*<a href="https://.*?launchpad.net/~([a-zA-Z0-9_-]+)" ', |
101 | - 'drafter': '<dt>Drafter:</dt>\s*<dd>\s*<a href="https://.*?launchpad.net/~([a-zA-Z0-9_-]+)" ', |
102 | - 'assignee': '<dt>Assignee:</dt>\s*<dd>\s*<a href="https://.*?launchpad.net/~([a-zA-Z0-9_-]+)" ', |
103 | - 'milestone': '<dt>Milestone target:</dt>\s*<dd>\s*<a href="https://.*?launchpad.net/\w+/\+milestone/([a-zA-Z0-9_\.-]+)"', |
104 | - 'description': '<div class="top-portlet">\s*<p>(.*?)</p>', |
105 | - 'status': '(?:<p>|^)[Ss]tatus:\s*<br />(.*?)</p>', |
106 | - 'details_url': 'href="([^"]*)">Read the full specification</a>', |
107 | - 'roadmap_notes': '(?:<p>|^)[Rr]oadmap\s+[Nn]otes:\s*<br />(.*?)</p>', |
108 | - } |
109 | + def get_whiteboard_section(heading): |
110 | + if bp.whiteboard is None: |
111 | + return None |
112 | + regex = '^' + heading + ':\s*\n(.*?)\n\n' |
113 | + m = re.search(regex, bp.whiteboard, re.S | re.I | re.M) |
114 | + if m is not None: |
115 | + section = m.group(1).strip() |
116 | + else: |
117 | + section = None |
118 | + return section |
119 | |
120 | data = {} |
121 | - for key, r in queries.iteritems(): |
122 | - m = re.search(r, contents, re.S) |
123 | - if not m: |
124 | - dbg('lp_import_blueprint(%s): did not find a match for key %s' % (name, key)) |
125 | - if key == 'description': |
126 | - data[key] = '(no description)' |
127 | - else: |
128 | - data[key] = None |
129 | - else: |
130 | - data[key] = m.group(1).strip() |
131 | + data['priority'] = bp.priority |
132 | + data['definition'] = bp.definition_status |
133 | + data['implementation'] = bp.implementation_status |
134 | + data['approver'] = bp.approver and bp.approver.name or None |
135 | + data['drafter'] = bp.drafter and bp.drafter.name or None |
136 | + data['assignee'] = bp.assignee and bp.assignee.name or None |
137 | + data['milestone'] = bp.milestone and bp.milestone.name or None |
138 | + data['description'] = bp.summary or "(no description)" |
139 | + data['status'] = get_whiteboard_section('Status') |
140 | + data['details_url'] = bp.specification_url |
141 | + data['roadmap_notes'] = get_whiteboard_section('Roadmap\s+Notes') |
142 | |
143 | # ignore "later" milestone" |
144 | if data['milestone'] == 'later': |
145 | - dbg('lp_import_blueprint(%s): spec has "later" milestone, ignoring it' % name) |
146 | + dbg('lp_import_blueprint(%s): spec has "later" milestone, ignoring it' % bp.name) |
147 | return False |
148 | |
149 | if data['milestone'] not in valid_milestones: |
150 | - data_error(url, 'milestone "%s" is unknown/invalid' % data['milestone'], True) |
151 | + data_error(web_link(bp), 'milestone "%s" is unknown/invalid' % data['milestone'], True) |
152 | |
153 | # if we have an explicit status: in the whiteboard, use that; otherwise use |
154 | # description as status text |
155 | if data['status']: |
156 | - data['status'] = data['status'].replace('<br />', |
157 | - '\n').replace('</div>', '').replace(' ', ' ').replace( |
158 | - '<wbr></wbr>', '').strip() |
159 | + data['status'] = data['status'].strip() |
160 | else: |
161 | - data['status'] = data['description'].replace('<br />', |
162 | - '\n').replace('</div>', '').replace(' ', ' ').replace( |
163 | - '<wbr></wbr>', '').strip() |
164 | + data['status'] = data['description'].strip() |
165 | del data['description'] |
166 | |
167 | - dbg('lp_import_blueprint(%s): finished parsing; data: %s' % (name, str(data))) |
168 | + dbg('lp_import_blueprint(%s): finished parsing; data: %s' % (bp.name, str(data))) |
169 | |
170 | db.cursor().execute('INSERT INTO specs VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?,?)', |
171 | - (name, url, data['priority'], data['implementation'], |
172 | + (bp.name, web_link(bp), data['priority'], data['implementation'], |
173 | data['assignee'], data['status'], data['milestone'], |
174 | data['definition'], data['drafter'], data['approver'], |
175 | data['details_url'], data['roadmap_notes'])) |
176 | @@ -178,10 +141,7 @@ |
177 | |
178 | ''' |
179 | |
180 | - ### cargo culted from parse_blueprint_workitem |
181 | - line = line.replace('<br />', '').replace('</div>', '').replace('</p>', |
182 | - '').replace('<wbr></wbr>', '').replace(' ', ' ').replace( |
183 | - '<a href="/', '<a href="https://launchpad.net/').strip() |
184 | + line = line.strip() |
185 | if not line: |
186 | return |
187 | |
188 | @@ -201,14 +161,9 @@ |
189 | db.commit() |
190 | |
191 | def parse_complexity_item(lp, db, line, bp_name, bp_url, def_milestone, def_assignee): |
192 | - |
193 | - line = line.replace('<br />', '').replace('</div>', '').replace('</p>', |
194 | - '').replace('<wbr></wbr>', '').replace(' ', ' ').replace( |
195 | - '<a href="/', '<a href="https://launchpad.net/').strip() |
196 | - |
197 | - # remove special characters people tend to type |
198 | + line = line.strip() |
199 | + # remove special characters people tend to type |
200 | line = re.sub('[^\w -.]', '', line) |
201 | - |
202 | if not line: |
203 | return |
204 | |
205 | @@ -219,17 +174,15 @@ |
206 | assignee = None |
207 | |
208 | try: |
209 | - list = line.split() |
210 | - list.reverse() |
211 | + complexity_list = line.split() |
212 | + complexity_list.reverse() |
213 | |
214 | # we may not have any values in the list, so append our |
215 | # default values in the right order so they can be mapped |
216 | defs = [None, def_milestone, def_assignee] |
217 | - for i in range(len(list), len(defs)): |
218 | - list.append(defs[i]) |
219 | - |
220 | - (num, milestone, assignee) = list |
221 | - |
222 | + for i in range(len(complexity_list), len(defs)): |
223 | + complexity_list.append(defs[i]) |
224 | + (num, milestone, assignee) = complexity_list |
225 | if not num: |
226 | data_error(bp_url, 'No complexity points defined for %s' % line) |
227 | |
228 | @@ -238,17 +191,17 @@ |
229 | cur = db.cursor() |
230 | cur.execute('INSERT INTO complexity VALUES(?,?,?,?,date(CURRENT_TIMESTAMP))', |
231 | (assignee, num, milestone, bp_name)) |
232 | - |
233 | except ValueError: |
234 | data_error(bp_url, "\tComplexity line '%s' could not be parsed %s" % (line, ValueError)) |
235 | |
236 | + |
237 | def parse_blueprint_workitem(line, default_assignee, milestone, |
238 | blueprint_url, launchpad, result_list): |
239 | '''Parse a work item line of a blueprint whiteboard.''' |
240 | |
241 | - line = line.replace('<br />', '').replace('</div>', '').replace('</p>', |
242 | - '').replace('<wbr></wbr>', '').replace(' ', ' ').replace( |
243 | - '<a href="/', '<a href="https://launchpad.net/').strip() |
244 | + # FIXME: we lose bug linking etc. that is done by the tales |
245 | + # formatters in LP here. |
246 | + line = line.strip() |
247 | if not line: |
248 | return |
249 | dbg("\tworkitem (clean): '%s'" % (line)) |
250 | @@ -291,13 +244,10 @@ |
251 | |
252 | result_list.append((desc, state, assignee, milestone)) |
253 | |
254 | -def follow_blueprint_buglink(bugnum, default_assignee, blueprint_name, |
255 | +def follow_blueprint_buglink(bug, default_assignee, blueprint_name, |
256 | launchpad, release, default_milestone, result_list): |
257 | '''Query launchpad for the information on a linked bug''' |
258 | |
259 | - bugnum = int(bugnum) |
260 | - dbg('follow_blueprint_buglink(): processing bug %i (default milestone: %s)' % (bugnum, default_milestone)) |
261 | - bug = launchpad.bugs[bugnum] |
262 | for task in bug.bug_tasks: |
263 | if task.status == 'Unknown': |
264 | # this can happen for upstream tasks |
265 | @@ -338,7 +288,7 @@ |
266 | if better_task: |
267 | continue |
268 | |
269 | - desc = '<a href="https://launchpad.net/bugs/%d">LP: #%d</a>: ' % (bugnum, bugnum) + bug.title |
270 | + desc = '<a href="https://launchpad.net/bugs/%d">LP: #%d</a>: ' % (bug.id, bug.id) + bug.title |
271 | if rtype != 'distribution': |
272 | desc += ' (%s)' % target.name |
273 | |
274 | @@ -363,32 +313,27 @@ |
275 | return word |
276 | return None |
277 | |
278 | -def lp_import_blueprint_workitems(lp, db, bp_name, bp_url, contents, release): |
279 | +def lp_import_blueprint_workitems(lp, db, bp, release): |
280 | '''Collect work items from a Launchpad blueprint. |
281 | |
282 | This includes work items from the whiteboard as well as linked bugs. |
283 | ''' |
284 | - linked_bugs_re = re.compile('<div id="bug_links".*>', re.I) |
285 | - bugnum_re = re.compile('<a href="https://bugs\..*launchpad\.net/bugs/([0-9]+)" class="sprite bug">') |
286 | - work_items_re = re.compile('(<p>|^)work items(.*)\s*:\s*<br />', re.I) |
287 | - meta_re = re.compile( '(<p>|^)Meta.*?:<br />', re.I ) |
288 | - complexity_re = re.compile( '(<p>|^)Complexity.*?:<br />', re.I ) |
289 | + work_items_re = re.compile('^work items(.*)\s*:\s*$', re.I) |
290 | + meta_re = re.compile('^Meta.*?:$', re.I) |
291 | + complexity_re = re.compile('^Complexity.*?:$', re.I) |
292 | |
293 | in_workitems_block = False |
294 | - in_linked_bugs_block = False |
295 | in_meta_block = False |
296 | in_complexity_block = False |
297 | - found_linked_bugs = False |
298 | work_items = [] |
299 | - found_wb_workitems = False |
300 | milestone = None |
301 | |
302 | cur = db.cursor() |
303 | - cur.execute('SELECT assignee, milestone, implementation FROM specs WHERE name = ?', (bp_name,)) |
304 | + cur.execute('SELECT assignee, milestone, implementation FROM specs WHERE name = ?', (bp.name,)) |
305 | (spec_assignee, spec_milestone, spec_implementation) = cur.fetchone() |
306 | |
307 | dbg('lp_import_blueprint_workitems(): processing %s (spec milestone: %s, spec assignee: %s, spec implementation: %s)' % ( |
308 | - bp_name, spec_milestone, spec_assignee, spec_implementation)) |
309 | + bp.name, spec_milestone, spec_assignee, spec_implementation)) |
310 | |
311 | cur.execute('SELECT team FROM teams WHERE name = ?', (spec_assignee,)) |
312 | assignee_teams = [t[0] for t in cur] |
313 | @@ -396,69 +341,51 @@ |
314 | cur.execute('SELECT name FROM milestones') |
315 | valid_milestones = [m[0] for m in cur] |
316 | |
317 | - for l in contents.splitlines(): |
318 | - |
319 | - if not in_workitems_block and not in_linked_bugs_block \ |
320 | - and not in_meta_block and not in_complexity_block: |
321 | - m = work_items_re.search(l) |
322 | - if m: |
323 | - in_workitems_block = True |
324 | - found_wb_workitems = True |
325 | - dbg('lp_import_blueprint_workitems(): starting work items block at ' + l) |
326 | - if not milestone: |
327 | - milestone = milestone_extract(m.group(2), valid_milestones) |
328 | - dbg(' ... setting milestone to ' + str(milestone)) |
329 | - if linked_bugs_re.search(l): |
330 | - in_linked_bugs_block = True |
331 | - found_linked_bugs = True |
332 | - if meta_re.search(l): |
333 | - in_meta_block = True |
334 | - if complexity_re.search(l): |
335 | - in_complexity_block = True |
336 | - continue |
337 | - |
338 | - if in_workitems_block: |
339 | - dbg("\tworkitem (raw): '%s'" % (l.strip())) |
340 | - parse_blueprint_workitem(l, spec_assignee, milestone or |
341 | - spec_milestone, bp_url, lp, work_items) |
342 | - |
343 | - if '</p>' in l: |
344 | - dbg('lp_import_blueprint_workitems(): closing work items block with line: ' + l) |
345 | - in_workitems_block = False |
346 | - milestone = None |
347 | - |
348 | - if in_linked_bugs_block: |
349 | - dbg("\tbug block line (raw): '%s'" % (l.strip())) |
350 | - m = bugnum_re.search(l) |
351 | - if m and lp: |
352 | - follow_blueprint_buglink(m.group(1), spec_assignee, |
353 | - bp_name, lp, release, |
354 | - milestone or spec_milestone, work_items) |
355 | - |
356 | - if '</div>' in l: |
357 | - in_linked_bugs_block = False |
358 | - continue |
359 | - |
360 | - if in_meta_block: |
361 | - dbg("\tmeta line (raw): '%s'" % (l.strip())) |
362 | - parse_meta_item( lp, db, l, bp_name ) |
363 | - |
364 | - # done with the meta information? |
365 | - if '</p>' in l: |
366 | - in_meta_block = False |
367 | - continue |
368 | - |
369 | - if in_complexity_block: |
370 | - dbg("\tcomplexity block line (raw): '%s'" % (l.strip())) |
371 | - parse_complexity_item(lp, db, l, bp_name, bp_url, spec_milestone, spec_assignee) |
372 | - |
373 | - # done with the meta information? |
374 | - if '</p>' in l: |
375 | - in_complexity_block = False |
376 | - continue |
377 | - |
378 | - if found_wb_workitems and '</div>' in l: |
379 | - break |
380 | + if bp.whiteboard: |
381 | + for l in bp.whiteboard.splitlines(): |
382 | + |
383 | + if (not in_workitems_block |
384 | + and not in_meta_block and not in_complexity_block): |
385 | + m = work_items_re.search(l) |
386 | + if m: |
387 | + in_workitems_block = True |
388 | + dbg('lp_import_blueprint_workitems(): starting work items block at ' + l) |
389 | + if not milestone: |
390 | + milestone = milestone_extract(m.group(1), valid_milestones) |
391 | + dbg(' ... setting milestone to ' + str(milestone)) |
392 | + if meta_re.search(l): |
393 | + in_meta_block = True |
394 | + if complexity_re.search(l): |
395 | + in_complexity_block = True |
396 | + continue |
397 | + |
398 | + if in_workitems_block: |
399 | + dbg("\tworkitem (raw): '%s'" % (l.strip())) |
400 | + if not l.strip(): |
401 | + dbg('lp_import_blueprint_workitems(): closing work items block with line: ' + l) |
402 | + in_workitems_block = False |
403 | + milestone = None |
404 | + parse_blueprint_workitem(l, spec_assignee, milestone or |
405 | + spec_milestone, web_link(bp), lp, work_items) |
406 | + |
407 | + if in_meta_block: |
408 | + dbg("\tmeta line (raw): '%s'" % (l.strip())) |
409 | + if not l.strip(): |
410 | + in_meta_block = False |
411 | + continue |
412 | + parse_meta_item(lp, db, l, bp.name) |
413 | + |
414 | + if in_complexity_block: |
415 | + dbg("\tcomplexity block line (raw): '%s'" % (l.strip())) |
416 | + if not l.strip(): |
417 | + in_complexity_block = False |
418 | + continue |
419 | + parse_complexity_item(lp, db, l, bp.name, web_link(bp), spec_milestone, spec_assignee) |
420 | + |
421 | + if lp: |
422 | + for bug in bp.bugs: |
423 | + follow_blueprint_buglink(bug, spec_assignee, bp.name, lp, release, |
424 | + spec_milestone, work_items) |
425 | |
426 | if not work_items: |
427 | #data_error(bp_url, 'no work items defined', True) |
428 | @@ -474,7 +401,8 @@ |
429 | |
430 | for (desc, status, assignee, milestone) in work_items: |
431 | db.cursor().execute('INSERT INTO work_items VALUES (?, ?, ?, ?, ?, date(CURRENT_TIMESTAMP))', |
432 | - (desc, bp_name, status, assignee, milestone)) |
433 | + (desc, bp.name, status, assignee, milestone)) |
434 | + |
435 | |
436 | def lp_import_milestones(lp_project, db): |
437 | '''Import milestones from Launchpad into DB. |
438 | @@ -550,29 +478,41 @@ |
439 | db.cursor().execute('INSERT INTO work_items VALUES (?, ?, ?, ?, ?, date(CURRENT_TIMESTAMP))', |
440 | (title, name, state, assignee, milestone)) |
441 | |
442 | -def lp_import(db, cfg, name_pattern = None): |
443 | +def lp_import(db, cfg, name_pattern=None): |
444 | '''Collect blueprint work items and status from Launchpad into DB.''' |
445 | |
446 | - lp = Launchpad.login_with('ubuntu-work-items', service_root=EDGE_SERVICE_ROOT) |
447 | + lp = Launchpad.login_with('ubuntu-work-items', service_root="production", version="devel") |
448 | + projects = [] |
449 | |
450 | if 'release' in cfg: |
451 | lp_project = lp.distributions['ubuntu'].getSeries(name_or_version=cfg['release']) |
452 | - url = '%s/ubuntu/%s/+specs?batch=300' % ( |
453 | - report_tools.blueprints_base_url, cfg['release']) |
454 | + projects.append(lp_project) |
455 | else: |
456 | assert 'project' in cfg, 'Configuration needs to specify project or release' |
457 | lp_project = lp.projects[cfg['project']] |
458 | - url = '%s/%s/+specs?batch=300' % ( |
459 | - report_tools.blueprints_base_url, cfg['project']) |
460 | + projects.append(lp_project) |
461 | |
462 | lp_import_milestones(lp_project, db) |
463 | lp_import_teams(lp, db, cfg) |
464 | |
465 | - for (bp, (url, milestone)) in lp_blueprints_from_list(url, name_pattern).iteritems(): |
466 | - dbg('lp_import(): downloading %s from %s' % (bp, url)) |
467 | - contents = urllib.urlopen(url).read().decode('UTF-8') |
468 | - if lp_import_blueprint(db, bp, url, contents): |
469 | - lp_import_blueprint_workitems(lp, db, bp, url, contents, cfg.get('release')) |
470 | + extra_projects = cfg.get('extra_projects', []) |
471 | + for extra_project_name in extra_projects: |
472 | + extra_project = lp.projects[extra_project_name] |
473 | + lp_import_milestones(extra_project, db) |
474 | + projects.append(extra_project) |
475 | + |
476 | + if name_pattern: |
477 | + name_filter = re.compile(name_pattern) |
478 | + |
479 | + for project in projects: |
480 | + # XXX: should this be valid_ or all_specifications? |
481 | + for bp in project.valid_specifications: |
482 | + if name_pattern: |
483 | + if not name_filter.search(bp.name): |
484 | + continue |
485 | + dbg('lp_import(): downloading %s from %s' % (bp.name, bp.self_link)) |
486 | + if lp_import_blueprint(db, bp): |
487 | + lp_import_blueprint_workitems(lp, db, bp, cfg.get('release')) |
488 | |
489 | lp_import_bug_workitems(lp_project, db, cfg) |
490 | |
491 | @@ -944,6 +884,12 @@ |
492 | if 'bug_status_map' in cfg: |
493 | bug_wi_states.update(cfg['bug_status_map']) |
494 | |
495 | + lock_path = opts.database + ".collect_lock" |
496 | + lock_f = open(lock_path, "wb") |
497 | + if report_tools.lock_file(lock_f) is None: |
498 | + print "Another instance is already running" |
499 | + sys.exit(0) |
500 | + |
501 | db = get_db(opts.database) |
502 | |
503 | # reset status for current day |
504 | @@ -962,3 +908,4 @@ |
505 | db.commit() |
506 | send_error_mails(cfg) |
507 | |
508 | + os.unlink(lock_path) |
509 | |
510 | === modified file 'generate-all' |
511 | --- generate-all 2010-09-14 15:49:33 +0000 |
512 | +++ generate-all 2011-02-09 12:26:14 +0000 |
513 | @@ -39,6 +39,12 @@ |
514 | trend_starts = {} |
515 | burnup_chart_teams = [] |
516 | |
517 | +lock_path = opts.database + ".generate_lock" |
518 | +lock_f = open(lock_path, "wb") |
519 | +if report_tools.lock_file(lock_f) is None: |
520 | + print "Another instance is already running" |
521 | + sys.exit(0) |
522 | + |
523 | db = report_tools.get_db(opts.database) |
524 | |
525 | # get milestones and teams |
526 | @@ -64,10 +70,12 @@ |
527 | |
528 | for u in users: |
529 | for m in milestones: |
530 | - # entire cycle report for user |
531 | target = u + '-' + m |
532 | basename = os.path.join(usersubdir, target) |
533 | report_tools.run_reports(my_path, opts.database, basename, opts.config, milestone=m, user=u, trend_starts=trend_starts, burnup=u in burnup_chart_teams) |
534 | + # entire cycle report for user |
535 | + basename = os.path.join(usersubdir, u) |
536 | + report_tools.run_reports(my_path, opts.database, basename, opts.config, milestone=None, user=u, trend_starts=trend_starts, burnup=u in burnup_chart_teams) |
537 | |
538 | # per team/milestone reports |
539 | for t in teams: |
540 | @@ -103,3 +111,4 @@ |
541 | src.close() |
542 | dest.close() |
543 | |
544 | +os.unlink(lock_path) |
545 | |
546 | === modified file 'html-report' |
547 | --- html-report 2011-01-11 15:09:44 +0000 |
548 | +++ html-report 2011-02-09 12:26:14 +0000 |
549 | @@ -8,116 +8,87 @@ |
550 | from report_tools import escape_url, escape_html |
551 | import report_tools |
552 | |
553 | -def format_priority(priority): |
554 | - prio_colors = { |
555 | - 'Undefined': 'gray', |
556 | - 'Low': 'blue', |
557 | - 'Medium': '#f60', |
558 | - 'High': 'red', |
559 | - 'Essential': 'red' |
560 | - } |
561 | - |
562 | - p = escape_html(priority or '') |
563 | - col = prio_colors.get(p) |
564 | - if col: |
565 | - return '<span style="color: %s">%s</span>' % (col, p) |
566 | - return p |
567 | + |
568 | +class WorkitemTarget(object): |
569 | + |
570 | + def __init__(self, todo=0, blocked=0, inprogress=0, postponed=0, |
571 | + done=0): |
572 | + self.todo = todo |
573 | + self.blocked = blocked |
574 | + self.inprogress = inprogress |
575 | + self.postponed = postponed |
576 | + self.done = done |
577 | + |
578 | + @property |
579 | + def percent_complete(self): |
580 | + completed = self.postponed + self.done |
581 | + total = completed + self.todo + self.blocked + self.inprogress |
582 | + return int((float(completed)/total)*100 + 0.5) |
583 | + |
584 | + |
585 | +class Blueprint(WorkitemTarget): |
586 | + |
587 | + def __init__(self, name, url, complexity=0, |
588 | + todo=0, blocked=0, inprogress=0, postponed=0, |
589 | + done=0, implementation=None, priority=None, |
590 | + status=None): |
591 | + super(Blueprint, self).__init__( |
592 | + todo=todo, blocked=blocked, inprogress=inprogress, |
593 | + postponed=postponed, done=done) |
594 | + self.name = name |
595 | + self.url = url |
596 | + self.complexity = complexity |
597 | + self.implementation = implementation |
598 | + self.priority = priority |
599 | + self.status = status |
600 | + |
601 | +class Assignee(WorkitemTarget): |
602 | + |
603 | + def __init__(self, name, url, complexity=0, todo_wis=[], |
604 | + blocked_wis=[], inprogress_wis=[], postponed_wis=[], |
605 | + done_wis=[]): |
606 | + super(Assignee, self).__init__( |
607 | + todo=len(todo_wis), blocked=len(blocked_wis), |
608 | + inprogress=len(inprogress_wis), postponed=len(postponed_wis), |
609 | + done=len(done_wis)) |
610 | + self.name = name |
611 | + self.url = url |
612 | + self.complexity = complexity |
613 | + self.todo_wis = todo_wis |
614 | + self.blocked_wis = blocked_wis |
615 | + self.inprogress_wis = inprogress_wis |
616 | + self.postponed_wis = postponed_wis |
617 | + self.done_wis = done_wis |
618 | + |
619 | |
620 | def spec_completion(db, team, milestone): |
621 | data = report_tools.blueprint_completion(db, team, milestone) |
622 | |
623 | - # avoid showing empty columns |
624 | - has_priority = any([i['status'] for i in data.itervalues()]) |
625 | - has_complexity = any([i['complexity'] > 0 for i in data.itervalues()]) |
626 | - |
627 | - print '''<h2>Status by specification</h2> |
628 | -<p>(Click header to sort)</p> |
629 | -<table id="byspecification"> |
630 | - <thead> |
631 | - <tr><th>Specification</th><th>%s</th> <th>todo</th><th>blocked</th><th>inprogress</th><th>postponed</th><th>done</th> <th>Completion</th> <th>%s</th> <th>Status</th></tr> |
632 | - </thead> |
633 | -''' % (has_complexity and 'Complexity' or '', |
634 | - has_priority and 'Priority' or '') |
635 | - |
636 | - completion = [] |
637 | - for (bp, i) in data.iteritems(): |
638 | - completion.append((bp, |
639 | - int(float(i['postponed']+i['done'])/(i['todo']+i['blocked']+i['done']+i['postponed']+i['inprogress'])*100 + 0.5))) |
640 | - |
641 | - completion.sort(key=lambda k: k[1]*100+report_tools.priority_value(data[k[0]]['priority']), reverse=True) |
642 | - |
643 | - for (bp, percent) in completion: |
644 | - print ' <tr><td>%s</td> <td>%s</td><td>%i</td><td>%i</td><td>%i</td><td>%i</td><td>%i</td> <td>%i%%<br/>' \ |
645 | - '<span style="font-size: 70%%">%s</span></td> <td>%s</td> <td>%s</td></tr>' % ( |
646 | - '<a href="%s">%s</a>' % (data[bp]['url'], escape_html(bp)), |
647 | - data[bp]['complexity'] or '', |
648 | - data[bp]['todo'], data[bp]['blocked'], data[bp]['inprogress'], data[bp]['postponed'], data[bp]['done'], |
649 | - percent, |
650 | - data[bp]['implementation'], |
651 | - format_priority(data[bp]['priority']), |
652 | - data[bp]['status']) |
653 | - |
654 | - print '</table>' |
655 | - |
656 | -def by_assignee(db, team, milestone): |
657 | - data = report_tools.assignee_completion(db, team, milestone) |
658 | - |
659 | - # avoid showing empty columns |
660 | - has_complexity = any([i['complexity'] > 0 for i in data.itervalues()]) |
661 | - |
662 | - print ''' |
663 | -<h2>Status by assignee</h2> |
664 | -<p>(Click header to sort)</p> |
665 | -<table id="byassignee"> |
666 | - <thead> |
667 | - <tr><th>Assignee</th> <th>%s</th><th>todo</th><th>blocked</th><th>inprogress</th><th>postponed</th><th>done</th> <th>Completion</th></tr> |
668 | - </thead> |
669 | -''' % (has_complexity and 'Complexity' or '', ) |
670 | - |
671 | - completion = [] |
672 | - for a, i in data.iteritems(): |
673 | - (todo, blocked, inprogress, postponed, done) = (len(i['todo']), len(i['blocked']), len(i['inprogress']), len(i['postponed']), len(i['done'])) |
674 | - completion.append((a, |
675 | - int(float(postponed+done) / (todo+blocked+inprogress+done+postponed)*100 + 0.5))) |
676 | - |
677 | - completion.sort(key=lambda k: k[0], reverse=False) |
678 | - |
679 | - for (a, percent) in completion: |
680 | - a_html = escape_url(a or team or 'nobody') |
681 | - # no user pages exist w/o milestones, and its silly to link back to ourselves, so fall back to launchpad specs page |
682 | - if not milestone or isinstance(team, report_tools.user_string): |
683 | - url = '%s/~%s/+specs?role=assignee' % (report_tools.blueprints_base_url, a_html) |
684 | - else: |
685 | - target = a_html + '-' + milestone |
686 | - url = 'u/%s.html' % target |
687 | - print ' <tr><td><a href="%s">%s</a></td> <td>%s</td><td>%i</td><td>%i</td><td>%i</td><td>%i</td><td>%i</td> <td>%i%%</td></tr>' % ( |
688 | - url, a_html, data[a]['complexity'] or '', len(data[a]['todo']), len(data[a]['blocked']), len(data[a]['inprogress']), len(data[a]['postponed']), |
689 | - len(data[a]['done']), percent) |
690 | - print '</table>' |
691 | - |
692 | -def work_item_details(db, team, milestone): |
693 | - data = report_tools.assignee_completion(db, team, milestone) |
694 | - |
695 | - import sys |
696 | - |
697 | - # avoid showing empty columns |
698 | - has_priority = False |
699 | - for i in data.itervalues(): |
700 | - for status in report_tools.valid_states: |
701 | - for wi in i[status]: |
702 | - if wi[2]: |
703 | - has_priority = True |
704 | - break |
705 | - if has_priority: |
706 | - break |
707 | - |
708 | - print ''' |
709 | -<h2>Work item details</h2> |
710 | -<table id="byworkitem"> |
711 | - <thead> |
712 | - <tr><th>Assignee</th> <th>Status</th> <th>Blueprint</th> <th>%s</th> <th>Work item</th></tr> |
713 | - </thead> |
714 | -''' % (has_priority and 'Priority' or '') |
715 | + blueprints = [] |
716 | + for bp, i in data.items(): |
717 | + blueprints.append( |
718 | + Blueprint(bp, i['url'], complexity=i['complexity'], |
719 | + todo=i['todo'], blocked=i['blocked'], |
720 | + inprogress=i['inprogress'], |
721 | + postponed=i['postponed'], done=i['done'], |
722 | + implementation=i['implementation'], |
723 | + priority=i['priority'], status=i['status'])) |
724 | + |
725 | + # avoid showing empty columns |
726 | + specs_have_status = any([bp.status for bp in blueprints]) |
727 | + specs_have_complexity = any([bp.complexity > 0 for bp in blueprints]) |
728 | + |
729 | + blueprints.sort( |
730 | + key=lambda bp: bp.percent_complete*100+report_tools.priority_value(bp.priority), |
731 | + reverse=True) |
732 | + |
733 | + return dict( |
734 | + specs_have_status=specs_have_status, |
735 | + specs_have_complexity=specs_have_complexity, |
736 | + blueprints=blueprints) |
737 | + |
738 | +def get_assignee_completion(db, team, milestone): |
739 | + data = report_tools.assignee_completion(db, team, milestone) |
740 | |
741 | def _sort(a, b): |
742 | # Takes [blueprint, workitem, priority, url] in a and b. |
743 | @@ -128,38 +99,49 @@ |
744 | # then by blueprint |
745 | return cmp(a[0], b[0]) |
746 | |
747 | - for a, i in sorted(data.iteritems(), key=lambda k: k[0]): |
748 | - todo_len = len(i['todo']) |
749 | - blocked_len = len(i['blocked']) |
750 | - postponed_len = len(i['postponed']) |
751 | - done_len = len(i['done']) |
752 | - inprogress_len = len(i['inprogress']) |
753 | - a_html = escape_url(a or team or 'nobody') |
754 | - a_url = '%s/~%s/+specs?role=assignee' % (report_tools.blueprints_base_url, a_html) |
755 | - rows = {'todo': '<td rowspan="%s">todo</td>' % todo_len, |
756 | - 'blocked': '<td rowspan="%s">blocked</td>' % blocked_len, |
757 | - 'postponed': '<td rowspan="%s">postponed</td>' % postponed_len, |
758 | - 'done': '<td rowspan="%s">done</td>' % done_len, |
759 | - 'inprogress': '<td rowspan="%s">inprogress</td>' % inprogress_len} |
760 | - printed_assignee = False |
761 | + assignees = [] |
762 | + for name, info in data.items(): |
763 | + name = name or team or 'nobody' |
764 | + name_html = escape_url(name or team or 'nobody') |
765 | + if not milestone or isinstance(team, report_tools.user_string): |
766 | + url = '%s/~%s/+specs?role=assignee' % (report_tools.blueprints_base_url, name_html) |
767 | + else: |
768 | + target = name_html + '-' + milestone |
769 | + url = 'u/%s.html' % target |
770 | + assignees.append( |
771 | + Assignee(name, url, complexity=info['complexity'], |
772 | + todo_wis=sorted(info['todo'], cmp=_sort), |
773 | + blocked_wis=info['blocked'], |
774 | + inprogress_wis=info['inprogress'], |
775 | + postponed_wis=info['postponed'], |
776 | + done_wis=info['done'])) |
777 | + return assignees |
778 | + |
779 | + |
780 | +def by_assignee(db, team, milestone): |
781 | + assignees = get_assignee_completion(db, team, milestone) |
782 | + assignees.sort(key=lambda a: a.name, reverse=False) |
783 | + |
784 | + # avoid showing empty columns |
785 | + assignees_have_complexity = any([a.complexity > 0 for a in assignees]) |
786 | + |
787 | + # avoid showing empty columns |
788 | + workitems_have_priority = False |
789 | + for assignee in assignees: |
790 | for status in report_tools.valid_states: |
791 | - printed_status = False |
792 | - i[status].sort(cmp=_sort) |
793 | - for (bp, item, priority, url) in i[status]: |
794 | - print ' <tr>', |
795 | - if not printed_assignee: |
796 | - print '<td rowspan="%s"><a href="%s" name="%s">%s</a></td> ' % ( |
797 | - todo_len+blocked_len+postponed_len+done_len+inprogress_len, a_url, escape_url(a or team or 'nobody'), |
798 | - a_html) |
799 | - printed_assignee = True |
800 | - if not printed_status: |
801 | - print rows[status] |
802 | - printed_status = True |
803 | - print '<td>%s</td> <td>%s</td> <td>%s</td></tr>' % ( |
804 | - '<a href="%s">%s</a>' % (url, escape_html(bp)), |
805 | - format_priority(priority), |
806 | - item) |
807 | - print '</table>' |
808 | + for wi in getattr(assignee, status+"_wis", []): |
809 | + if wi[2]: |
810 | + workitems_have_priority = True |
811 | + break |
812 | + if workitems_have_priority: |
813 | + break |
814 | + |
815 | + return dict( |
816 | + assignees_have_complexity=assignees_have_complexity, |
817 | + assignees=assignees, |
818 | + workitems_have_priority=workitems_have_priority, |
819 | + valid_states=report_tools.valid_states) |
820 | + |
821 | |
822 | def html(db, team, milestone, chart_url, title=None): |
823 | '''Print work item status as HTML.''' |
824 | @@ -173,118 +155,30 @@ |
825 | if milestone: |
826 | title += ' (%s)' % milestone |
827 | |
828 | - print '''<html> |
829 | -<head> |
830 | - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> |
831 | - <title>Work item status: %s</title> |
832 | - <style type="text/css"> |
833 | - body { background: #F0F0E0; color: black; } |
834 | - a { text-decoration: none; } |
835 | - table { border-collapse: collapse; border-style: solid none; |
836 | - border-width: 3px; margin-bottom: 3ex; empty-cells: show; } |
837 | - table th { text-align: left; border-style: none none solid none; |
838 | - border-width: 3px; padding-right: 10px; } |
839 | - table td { text-align: left; border-style: none none dotted none; |
840 | - border-width: 1px; padding-right: 10px; } |
841 | - table#byworkitem td { vertical-align: top; } |
842 | - |
843 | - a { color: blue; } |
844 | - </style> |
845 | - |
846 | - |
847 | - <script type="text/javascript" src="jquery-1.4.min.js"></script> |
848 | - <script type="text/javascript" src="jquery.tablesorter.min.js"></script> |
849 | - |
850 | - <script type="text/javascript"> |
851 | - // documentation at: http://tablesorter.com/ |
852 | - |
853 | - $(document).ready( function() { |
854 | - // add parser through the tablesorter addParser method |
855 | - $.tablesorter.addParser({ |
856 | - // set a unique id |
857 | - id: 'priority', |
858 | - is: function(s) { |
859 | - // return false so this parser is not auto detected |
860 | - return false; |
861 | - }, |
862 | - format: function(s) { |
863 | - // format your data for normalization |
864 | - return s.toLowerCase() |
865 | - .replace(/essential/,5) |
866 | - .replace(/high/,4) |
867 | - .replace(/medium/,3) |
868 | - .replace(/low/,2) |
869 | - .replace(/undefined/,1) |
870 | - .replace(/not/,0); |
871 | - |
872 | - }, |
873 | - // set type, either numeric or text |
874 | - type: 'numeric' |
875 | - }); |
876 | - |
877 | - $.tablesorter.addParser({ |
878 | - // set a unique id |
879 | - id: 'completion', |
880 | - is: function(s) { |
881 | - // return false so this parser is not auto detected |
882 | - return false; |
883 | - }, |
884 | - format: function(s) { |
885 | - // format your data for normalization |
886 | - return s.replace(/^(\d+).*/,'\1'); |
887 | - |
888 | - }, |
889 | - // set type, either numeric or text |
890 | - type: 'numeric' |
891 | - }); |
892 | - |
893 | - // order they appear on the page |
894 | - $("#byspecification").tablesorter({ |
895 | - // sort on priority, spec ascending |
896 | - sortList: [[5,1],[0,0]], |
897 | - headers: { |
898 | - 4: { sorter:'completion' }, |
899 | - 5: { sorter:'priority' } |
900 | - } |
901 | - }); |
902 | - |
903 | - $("#byassignee").tablesorter({ |
904 | - // sort on name, ascending |
905 | - sortList: [[0,0]] |
906 | - }); |
907 | - |
908 | - // with the 'todo/done' and assignee not appearing on every |
909 | - // row, this does not lend itself well to sorting |
910 | - //$("#byworkitem").tablesorter(); |
911 | - |
912 | - }); |
913 | - </script> |
914 | -</head> |
915 | - |
916 | -<body> |
917 | - |
918 | -<h1>Burndown for %s</h1> |
919 | - |
920 | -<p><object height="600" width="1000" data="%s" type="image/svg+xml">Burndown SVG</object></p> |
921 | -''' % (title, title, chart_url) |
922 | - |
923 | - spec_completion(db, team, milestone) |
924 | - by_assignee(db, team, milestone) |
925 | - work_item_details(db, team, milestone) |
926 | - time_stats(db, team, milestone) |
927 | - |
928 | - print '</body></html>' |
929 | + data = dict(title=title, chart_url=chart_url) |
930 | + data.update(spec_completion(db, team, milestone)) |
931 | + data.update(by_assignee(db, team, milestone)) |
932 | + data.update(time_stats(db, team, milestone)) |
933 | + print report_tools.fill_template("burndown.html", data) |
934 | + |
935 | + |
936 | +class WorkitemsOnDate(object): |
937 | + |
938 | + def __init__(self, date, done, delta_done, todo, delta_todo): |
939 | + self.date = date |
940 | + self.done = done |
941 | + self.delta_done = delta_done |
942 | + self.todo = todo |
943 | + self.delta_todo = delta_todo |
944 | + |
945 | |
946 | def time_stats(db, team, milestone): |
947 | data = report_tools.workitems_over_time(db, team, milestone) |
948 | |
949 | - print ''' |
950 | -<h2>Development over time</h2> |
951 | -<table id="time_stats"> |
952 | - <tr><th>Date</th><th>done</th><th>todo</th></tr> |
953 | -''' |
954 | prev_done = None |
955 | prev_todo = None |
956 | + |
957 | + workitems_by_day = [] |
958 | for (date, states) in sorted(data.iteritems(), key=lambda k: k[0]): |
959 | todo = states.get('todo', 0) + states.get('inprogress', 0) |
960 | done = states.get('done', 0) + states.get('postponed', 0) |
961 | @@ -306,10 +200,9 @@ |
962 | delta_todo = '' |
963 | prev_todo = todo |
964 | prev_done = done |
965 | - print ' <tr><td>%s</td><td>%i%s</td><td>%i%s</td></tr>' % ( |
966 | - date, done, delta_done, todo, delta_todo) |
967 | - |
968 | -print '</table>' |
969 | + workitems_by_day.append(WorkitemsOnDate(date, done, delta_done, |
970 | + todo, delta_todo)) |
971 | + return dict(workitems_by_day=workitems_by_day) |
972 | |
973 | # |
974 | # main |
975 | |
976 | === modified file 'report_tools.py' |
977 | --- report_tools.py 2010-12-03 12:41:46 +0000 |
978 | +++ report_tools.py 2011-02-09 12:26:14 +0000 |
979 | @@ -6,6 +6,8 @@ |
980 | import sqlite3 as dbapi2 |
981 | from subprocess import Popen |
982 | from cgi import escape |
983 | +import errno |
984 | +import fcntl |
985 | |
986 | valid_states = ['inprogress','blocked','todo','done', 'postponed'] |
987 | blueprints_base_url = 'https://blueprints.launchpad.net' |
988 | @@ -547,6 +549,21 @@ |
989 | |
990 | return trans.get(name, 0) |
991 | |
992 | +def lock_file(f): |
993 | + try: |
994 | + fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB) |
995 | + return f |
996 | + except IOError, e: |
997 | + if e.errno in (errno.EACCES, errno.EAGAIN): |
998 | + return None |
999 | + raise |
1000 | + |
1001 | +def fill_template(name, data): |
1002 | + from mako.lookup import TemplateLookup |
1003 | + lookup = TemplateLookup(directories=['templates']) |
1004 | + template = lookup.get_template(name) |
1005 | + return template.render_unicode(**data) |
1006 | + |
1007 | if __name__ == '__main__': |
1008 | # some test stuff |
1009 | db = get_db(sys.argv[1]) |
1010 | |
1011 | === added directory 'templates' |
1012 | === added file 'templates/burndown.html' |
1013 | --- templates/burndown.html 1970-01-01 00:00:00 +0000 |
1014 | +++ templates/burndown.html 2011-02-09 12:26:14 +0000 |
1015 | @@ -0,0 +1,241 @@ |
1016 | +<html> |
1017 | +<head> |
1018 | + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> |
1019 | + <title>Work item status: %{title}</title> |
1020 | + <style type="text/css"> |
1021 | + body { background: #F0F0E0; color: black; } |
1022 | + a { text-decoration: none; } |
1023 | + table { border-collapse: collapse; border-style: solid none; |
1024 | + border-width: 3px; margin-bottom: 3ex; empty-cells: show; } |
1025 | + table th { text-align: left; border-style: none none solid none; |
1026 | + border-width: 3px; padding-right: 10px; } |
1027 | + table td { text-align: left; border-style: none none dotted none; |
1028 | + border-width: 1px; padding-right: 10px; } |
1029 | + table#byworkitem td { vertical-align: top; } |
1030 | + |
1031 | + a { color: blue; } |
1032 | + |
1033 | + td.priority_Undefined { color: gray; } |
1034 | + td.priority_Low { color: blue; } |
1035 | + td.priority_Medium { color: #f60; } |
1036 | + td.priority_High { color: red; } |
1037 | + td.priority_Essential { color: red; } |
1038 | + td span.implementation_status { font-size: 70%; } |
1039 | + </style> |
1040 | + |
1041 | + |
1042 | + <script type="text/javascript" src="jquery-1.4.min.js"></script> |
1043 | + <script type="text/javascript" src="jquery.tablesorter.min.js"></script> |
1044 | + |
1045 | + <script type="text/javascript"> |
1046 | + // documentation at: http://tablesorter.com/ |
1047 | + |
1048 | + $(document).ready( function() { |
1049 | + // add parser through the tablesorter addParser method |
1050 | + $.tablesorter.addParser({ |
1051 | + // set a unique id |
1052 | + id: 'priority', |
1053 | + is: function(s) { |
1054 | + // return false so this parser is not auto detected |
1055 | + return false; |
1056 | + }, |
1057 | + format: function(s) { |
1058 | + // format your data for normalization |
1059 | + return s.toLowerCase() |
1060 | + .replace(/essential/,5) |
1061 | + .replace(/high/,4) |
1062 | + .replace(/medium/,3) |
1063 | + .replace(/low/,2) |
1064 | + .replace(/undefined/,1) |
1065 | + .replace(/not/,0); |
1066 | + |
1067 | + }, |
1068 | + // set type, either numeric or text |
1069 | + type: 'numeric' |
1070 | + }); |
1071 | + |
1072 | + $.tablesorter.addParser({ |
1073 | + // set a unique id |
1074 | + id: 'completion', |
1075 | + is: function(s) { |
1076 | + // return false so this parser is not auto detected |
1077 | + return false; |
1078 | + }, |
1079 | + format: function(s) { |
1080 | + // format your data for normalization |
1081 | + return s.replace(/^(\d+).*/,'\1'); |
1082 | + |
1083 | + }, |
1084 | + // set type, either numeric or text |
1085 | + type: 'numeric' |
1086 | + }); |
1087 | + |
1088 | + // order they appear on the page |
1089 | + $("#byspecification").tablesorter({ |
1090 | + // sort on priority, spec ascending |
1091 | + sortList: [[5,1],[0,0]], |
1092 | + headers: { |
1093 | + 4: { sorter:'completion' }, |
1094 | + 5: { sorter:'priority' } |
1095 | + } |
1096 | + }); |
1097 | + |
1098 | + $("#byassignee").tablesorter({ |
1099 | + // sort on name, ascending |
1100 | + sortList: [[0,0]] |
1101 | + }); |
1102 | + |
1103 | + // with the 'todo/done' and assignee not appearing on every |
1104 | + // row, this does not lend itself well to sorting |
1105 | + //$("#byworkitem").tablesorter(); |
1106 | + |
1107 | + }); |
1108 | + </script> |
1109 | +</head> |
1110 | + |
1111 | +<body> |
1112 | + |
1113 | +<h1>Burndown for ${title}</h1> |
1114 | + |
1115 | +<p><object height="600" width="1000" data="${chart_url}" type="image/svg+xml">Burndown SVG</object></p> |
1116 | + |
1117 | +<h2>Status by specification</h2> |
1118 | +<p>(Click header to sort)</p> |
1119 | +<table id="byspecification"> |
1120 | + <thead> |
1121 | + <tr> |
1122 | + <th>Specification</th> |
1123 | +% if specs_have_complexity: |
1124 | + <th>Complexity</th> |
1125 | +% endif |
1126 | + <th>todo</th> |
1127 | + <th>blocked</th> |
1128 | + <th>inprogress</th> |
1129 | + <th>postponed</th> |
1130 | + <th>done</th> |
1131 | + <th>Completion</th> |
1132 | + <th>Priority</th> |
1133 | +% if specs_have_status: |
1134 | + <th>Status</th> |
1135 | +% endif |
1136 | + </tr> |
1137 | + </thead> |
1138 | +% for bp in blueprints: |
1139 | + <tr> |
1140 | + <td><a href="${bp.url}">${bp.name}</a></td> |
1141 | +% if has_complexity: |
1142 | + <td>${bp.complexity}</td> |
1143 | +% endif |
1144 | + <td>${bp.todo}</td> |
1145 | + <td>${bp.blocked}</td> |
1146 | + <td>${bp.inprogress}</td> |
1147 | + <td>${bp.postponed}</td> |
1148 | + <td>${bp.done}</td> |
1149 | + <td>${bp.percent_complete}%<br /> |
1150 | + <span class="implementation_status">${bp.implementation}</span></td> |
1151 | + <td class="priority_${bp.priority}">${bp.priority}</td> |
1152 | + <td>${bp.status}</td> |
1153 | + </tr> |
1154 | +% endfor |
1155 | +</table> |
1156 | + |
1157 | +<h2>Status by assignee</h2> |
1158 | +<p>(Click header to sort)</p> |
1159 | +<table id="byassignee"> |
1160 | +<thead> |
1161 | + <tr> |
1162 | + <th>Assignee</th> |
1163 | +% if assignees_have_complexity: |
1164 | + <th>Complexity</th> |
1165 | +% endif |
1166 | + <th>todo</th> |
1167 | + <th>blocked</th> |
1168 | + <th>inprogress</th> |
1169 | + <th>postponed</th> |
1170 | + <th>done</th> |
1171 | + <th>Completion</th> |
1172 | + </tr> |
1173 | +</thead> |
1174 | +% for a in assignees: |
1175 | + <tr> |
1176 | + <td><a href="${a.url}">${a.name}</a></td> |
1177 | +% if assignees_have_complexity: |
1178 | + <td>${a.complexity}</td> |
1179 | +% endif |
1180 | + <td>${a.todo}</td> |
1181 | + <td>${a.blocked}</td> |
1182 | + <td>${a.inprogress}</td> |
1183 | + <td>${a.postponed}</td> |
1184 | + <td>${a.done}</td> |
1185 | + <td>${a.percent_complete}%</td> |
1186 | + </tr> |
1187 | +% endfor |
1188 | +</table> |
1189 | + |
1190 | +<h2>Work item details</h2> |
1191 | +<table id="byworkitem"> |
1192 | +<thead> |
1193 | + <tr> |
1194 | + <th>Assignee</th> |
1195 | + <th>Status</th> |
1196 | + <th>Blueprint</th> |
1197 | +% if workitems_have_priority: |
1198 | + <th>Priority</th> |
1199 | +% endif |
1200 | + <th>Work item</th> |
1201 | + </tr> |
1202 | +</thead> |
1203 | + |
1204 | +% for a in assignees: |
1205 | +<% |
1206 | + printed_assignee = False |
1207 | +%> |
1208 | +% for status in valid_states: |
1209 | +<% |
1210 | + printed_status = False |
1211 | +%> |
1212 | +% for bp, item, priority, url in getattr(a, status+"_wis"): |
1213 | + <tr> |
1214 | +% if not printed_assignee: |
1215 | + <td rowspan="${a.todo+a.blocked+a.postponed+a.done+a.inprogress}"><a href="${a.url}" name="${a.name}">${a.name}</a></td> |
1216 | + <% |
1217 | + printed_assignee = True |
1218 | + %> |
1219 | +% endif |
1220 | +% if not printed_status: |
1221 | + <td rowspan="${getattr(a, status)}">${status}</td> |
1222 | + <% |
1223 | + printed_status = True |
1224 | + %> |
1225 | +% endif |
1226 | + <td><a href="${url}">${bp}</a></td> |
1227 | +% if workitems_have_priority: |
1228 | + <td class="priority_${priority}">${priority}</td> |
1229 | +% endif |
1230 | + <td>${item}</td> |
1231 | + </tr> |
1232 | +% endfor |
1233 | +% endfor |
1234 | +% endfor |
1235 | + |
1236 | +</table> |
1237 | + |
1238 | +<h2>Development over time</h2> |
1239 | +<table id="time_stats"> |
1240 | + <thead> |
1241 | + <tr> |
1242 | + <th>Date</th> |
1243 | + <th>done</th> |
1244 | + <th>todo</th> |
1245 | + </tr> |
1246 | + </thead> |
1247 | +% for count_on_day in workitems_by_day: |
1248 | + <tr> |
1249 | + <td>${count_on_day.date}</td> |
1250 | + <td>${count_on_day.done}${count_on_day.delta_done}</td> |
1251 | + <td>${count_on_day.todo}${count_on_day.delta_todo}</td> |
1252 | + </tr> |
1253 | +% endfor |
1254 | +</table> |
1255 | +</body> |
1256 | +</html> |
1257 | |
1258 | === added file 'templates/test.html' |
1259 | --- templates/test.html 1970-01-01 00:00:00 +0000 |
1260 | +++ templates/test.html 2011-02-09 12:26:14 +0000 |
1261 | @@ -0,0 +1,1 @@ |
1262 | +Testing ${data} |
1263 | |
1264 | === added file 'tests.py' |
1265 | --- tests.py 1970-01-01 00:00:00 +0000 |
1266 | +++ tests.py 2011-02-09 12:26:14 +0000 |
1267 | @@ -0,0 +1,16 @@ |
1268 | +from testtools import TestCase |
1269 | +import report_tools |
1270 | + |
1271 | + |
1272 | +class FillTemplateTests(TestCase): |
1273 | + |
1274 | + def test_finds_template(self): |
1275 | + output = report_tools.fill_template("test.html", dict(data="foo")) |
1276 | + self.assertEqual("Testing foo\n", output) |
1277 | + |
1278 | + |
1279 | +def test_suite(): |
1280 | + from unittest import TestLoader |
1281 | + loader = TestLoader() |
1282 | + suite = loader.loadTestsFromName(__name__) |
1283 | + return suite |
Oh yeah, I forgot mako wasn't on people, sorry. I would have mentioned
this otherwise.
Thanks,
James