Merge lp:~jib/launchpad-work-items-tracker/complexity-points into lp:launchpad-work-items-tracker

Proposed by Jos Boumans
Status: Merged
Merged at revision: 190
Proposed branch: lp:~jib/launchpad-work-items-tracker/complexity-points
Merge into: lp:launchpad-work-items-tracker
Diff against target: 337 lines (+123/-16)
3 files modified
collect (+98/-7)
html-report (+11/-6)
report_tools.py (+14/-3)
To merge this branch: bzr merge lp:~jib/launchpad-work-items-tracker/complexity-points
Reviewer Review Type Date Requested Status
Martin Pitt (community) Needs Fixing
Review via email: mp+29043@code.launchpad.net

Description of the change

This adds supports for more meta information in the whiteboard, most notably complexity points and notes. This requires a database change which will be done automatically by the 'collect' script.

These are both used by the server team to do capacity planning and to generate their roadmaps.

Existing functionality should remain unchanged and users of the HTML version of the WI tracker will now see 2 extra columns in the overview.

To post a comment you must log in.
190. By Jos Boumans <jib@lucid-server>

* moin import needs to be aware of the notes column as well
* make sure we set the db version to 4 on new creation

191. By Jos Boumans <jib@lucid-server>

* fix typo

Revision history for this message
Martin Pitt (pitti) wrote :

Thanks Jos.

Some issues (in the interest of "teach a man to fish.." I'll mention them here):

 - Please use data_error() for real errors in the BP, not just dbg (in the except ValueError)
 - data_error() takes the spec URL as the first argument, you have to pass that down
 - Please follow PEP 8 formatting, i. e. f(x), not f( x)
 - as for the list.reverse() thing, that's expected. reverse() is applied inline, so you have to call it on a variable.

  - as for the default setting:

   + for i in range(len(list), len(defs)):
   + list.append(defs[i])

   How about something like

    list += defs[len(list):]

   ? (untested)

  - I don't quite understand the concept of "complexity". Why can't we just add a new column "complexity" to the spec table?

  - html-report defines a new PrettyPrinter pp, but does not use it anywhere

  - Why a new "notes" column? specifications can already set a "Status:" block in the whiteboard, which seems to be pretty much the same? I wouldn't like to have redundancy like that

Thanks!

review: Needs Fixing
192. By Jos Boumans <jib@lucid-server>

* apply style notes as suggested by pitti

Revision history for this message
Martin Pitt (pitti) wrote :

Thanks, so r192 fixes (most of) the PEP-8, data_error(), and the PrettyPrinter thing.

That leaves the question about the "complexity" column, and why we need notes in the first place.

You might have answered that in a previous mail, but for some reason I don't have it, and apparently that answer didn't make it here for some reason. Can you please copy your answer here again? Sorry for the trouble.

193. By Jos Boumans <jib@lucid-server>

* rename 'notes' to 'roadmap_notes' and educate the parser to
  use 'Roadmap Notes:' instead of 'Notes:'
* Don't show notes in the html report

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'collect'
--- collect 2010-06-18 14:30:21 +0000
+++ collect 2010-07-08 00:41:40 +0000
@@ -12,6 +12,7 @@
12import report_tools12import report_tools
1313
14debug = False14debug = False
15#debug = True
1516
16# if this is None, data errors go to stderr; if this is a list of (spec_name,17# if this is None, data errors go to stderr; if this is a list of (spec_name,
17# msg) tuples, data errors get collected here and mailed out at the end18# msg) tuples, data errors get collected here and mailed out at the end
@@ -112,6 +113,7 @@
112 'description': '<div class="top-portlet">\s*<p>(.*?)</p>',113 'description': '<div class="top-portlet">\s*<p>(.*?)</p>',
113 'status': '(?:<p>|^)[Ss]tatus:\s*<br />(.*?)</p>',114 'status': '(?:<p>|^)[Ss]tatus:\s*<br />(.*?)</p>',
114 'details_url': 'href="([^"]*)">Read the full specification</a>',115 'details_url': 'href="([^"]*)">Read the full specification</a>',
116 'roadmap_notes': '(?:<p>|^)[Rr]oadmap\s+[Nn]otes:\s*<br />(.*?)</p>',
115 }117 }
116118
117 data = {}119 data = {}
@@ -148,11 +150,11 @@
148150
149 dbg('lp_import_blueprint(%s): finished parsing; data: %s' % (name, str(data)))151 dbg('lp_import_blueprint(%s): finished parsing; data: %s' % (name, str(data)))
150152
151 db.cursor().execute('INSERT INTO specs VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?)',153 db.cursor().execute('INSERT INTO specs VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?,?)',
152 (name, url, data['priority'], data['implementation'],154 (name, url, data['priority'], data['implementation'],
153 data['assignee'], data['status'], data['milestone'],155 data['assignee'], data['status'], data['milestone'],
154 data['definition'], data['drafter'], data['approver'],156 data['definition'], data['drafter'], data['approver'],
155 data['details_url']))157 data['details_url'], data['roadmap_notes']))
156158
157 return True159 return True
158160
@@ -183,6 +185,52 @@
183 (key, value, bp_name))185 (key, value, bp_name))
184 db.commit()186 db.commit()
185187
188def parse_complexity_item( lp, db, line, bp_name, bp_url, def_milestone, def_assignee):
189
190 ### cargo culted from parse_blueprint_workitem
191 line = line.replace('<br />', '').replace('</div>', '').replace('</p>',
192 '').replace('<wbr></wbr>', '').replace('&nbsp;', ' ').replace(
193 '<a href="/', '<a href="https://launchpad.net/').strip()
194
195 ### remove special characters people tend to type
196 line = re.sub('[^\w -.]', '', line)
197
198 if not line:
199 return
200
201 dbg("\tParsing complexity line '%s'" % line)
202
203 num = None
204 milestone = None
205 assignee = None
206
207 try:
208 ### line.split().reverse() throws a typeerror exception
209 list = line.split()
210 list.reverse()
211
212 ### we may not have any values in the list, so append our
213 ### default values in the right order so they can be mapped
214 ### if we *dont* have enough values, an exception occurs
215 defs = [ None, def_milestone, def_assignee ]
216
217 for i in range(len(list), len(defs)):
218 list.append(defs[i])
219
220 (num, milestone, assignee) = list
221
222 if not num:
223 data_error(bp_url, "No complexity points defined for %s" % line)
224
225 dbg("\tComplexity: %s MS: %s Who: %s" % (num, milestone, assignee))
226
227 cur = db.cursor()
228 cur.execute('INSERT INTO complexity VALUES(?,?,?,?,date(CURRENT_TIMESTAMP))',
229 (assignee, num, milestone, bp_name))
230 db.commit()
231
232 except ValueError:
233 data_error(bp_url, "\tComplexity line '%s' could not be parsed %s" % (line, ValueError))
186234
187def parse_blueprint_workitem(line, default_assignee, milestone,235def parse_blueprint_workitem(line, default_assignee, milestone,
188 blueprint_url, launchpad, allow_inprogress, result_list):236 blueprint_url, launchpad, allow_inprogress, result_list):
@@ -326,10 +374,12 @@
326 bugnum_re = re.compile('<a href="https://bugs\..*launchpad\.net/bugs/([0-9]+)" class="sprite bug">')374 bugnum_re = re.compile('<a href="https://bugs\..*launchpad\.net/bugs/([0-9]+)" class="sprite bug">')
327 work_items_re = re.compile('(<p>|^)work items(.*)\s*:\s*<br />', re.I)375 work_items_re = re.compile('(<p>|^)work items(.*)\s*:\s*<br />', re.I)
328 meta_re = re.compile( '(<p>|^)Meta.*?:<br />', re.I )376 meta_re = re.compile( '(<p>|^)Meta.*?:<br />', re.I )
377 complexity_re = re.compile( '(<p>|^)Complexity.*?:<br />', re.I )
329378
330 in_workitems_block = False379 in_workitems_block = False
331 in_linked_bugs_block = False380 in_linked_bugs_block = False
332 in_meta_block = False381 in_meta_block = False
382 in_complexity_block = False
333 found_linked_bugs = False383 found_linked_bugs = False
334 work_items = []384 work_items = []
335 found_wb_workitems = False385 found_wb_workitems = False
@@ -347,7 +397,11 @@
347397
348 for l in contents.splitlines():398 for l in contents.splitlines():
349399
350 if not in_workitems_block and not in_linked_bugs_block and not in_meta_block:400 if not in_workitems_block and not in_linked_bugs_block \
401 and not in_meta_block and not in_complexity_block:
402
403 #dbg( 'processing: %s' % l )
404
351 m = work_items_re.search(l)405 m = work_items_re.search(l)
352 if m:406 if m:
353 in_workitems_block = True407 in_workitems_block = True
@@ -361,6 +415,8 @@
361 found_linked_bugs = True415 found_linked_bugs = True
362 if meta_re.search(l):416 if meta_re.search(l):
363 in_meta_block = True417 in_meta_block = True
418 if complexity_re.search(l):
419 in_complexity_block = True
364 continue420 continue
365421
366 if in_workitems_block:422 if in_workitems_block:
@@ -394,6 +450,16 @@
394 in_meta_block = False450 in_meta_block = False
395 continue451 continue
396452
453 if in_complexity_block:
454 dbg("\tcomplexity block line (raw): '%s'" % (l.strip()))
455
456 parse_complexity_item(lp, db, l, bp_name, bp_url, spec_milestone, spec_assignee)
457
458 ### done with the meta information?
459 if '</p>' in l:
460 in_complexity_block = False
461 continue
462
397 if found_wb_workitems and '</div>' in l:463 if found_wb_workitems and '</div>' in l:
398 break464 break
399465
@@ -465,7 +531,7 @@
465 lp_import_teams(lp, db, cfg)531 lp_import_teams(lp, db, cfg)
466532
467 for (bp, (url, milestone)) in lp_blueprints_from_list(url, name_pattern).iteritems():533 for (bp, (url, milestone)) in lp_blueprints_from_list(url, name_pattern).iteritems():
468 dbg('lp_import(): downloading ' + bp)534 dbg('lp_import(): downloading %s from %s' % (bp, url))
469 contents = urllib.urlopen(url).read().decode('UTF-8')535 contents = urllib.urlopen(url).read().decode('UTF-8')
470 if lp_import_blueprint(db, bp, url, contents):536 if lp_import_blueprint(db, bp, url, contents):
471 lp_import_blueprint_workitems(lp, db, bp, url, contents, cfg.get('release'))537 lp_import_blueprint_workitems(lp, db, bp, url, contents, cfg.get('release'))
@@ -598,7 +664,7 @@
598 db.cursor().execute('INSERT INTO work_items VALUES (?, ?, ?, ?, ?, date(CURRENT_TIMESTAMP))',664 db.cursor().execute('INSERT INTO work_items VALUES (?, ?, ?, ?, ?, date(CURRENT_TIMESTAMP))',
599 (desc, name, state, assignee, milestone))665 (desc, name, state, assignee, milestone))
600666
601 db.cursor().execute("INSERT INTO specs VALUES (?, ?, 'Medium', 'Unknown', NULL, NULL, ?, NULL, NULL, NULL, NULL, NULL)",667 db.cursor().execute("INSERT INTO specs VALUES (?, ?, 'Medium', 'Unknown', NULL, NULL, ?, NULL, NULL, NULL, NULL, NULL, NULL)",
602 (name, spec_url, status))668 (name, spec_url, status))
603669
604########################################################################670########################################################################
@@ -621,7 +687,7 @@
621 cur.execute('''CREATE TABLE version (687 cur.execute('''CREATE TABLE version (
622 db_layout_ref INT NOT NULL688 db_layout_ref INT NOT NULL
623 )''')689 )''')
624 cur.execute('''INSERT INTO version VALUES (3)''')690 cur.execute('''INSERT INTO version VALUES (4)''')
625691
626 cur.execute('''CREATE TABLE specs (692 cur.execute('''CREATE TABLE specs (
627 name VARCHAR(255) PRIMARY KEY,693 name VARCHAR(255) PRIMARY KEY,
@@ -635,7 +701,8 @@
635 definition CHAR(30),701 definition CHAR(30),
636 drafter CHAR(50),702 drafter CHAR(50),
637 approver CHAR(50),703 approver CHAR(50),
638 details_url VARCHAR(1000)704 details_url VARCHAR(1000),
705 roadmap_notes VARCHAR(5000)
639 )''')706 )''')
640707
641 cur.execute('''CREATE TABLE work_items (708 cur.execute('''CREATE TABLE work_items (
@@ -663,6 +730,14 @@
663 spec VARCHAR(255) REFERENCES specs(name),730 spec VARCHAR(255) REFERENCES specs(name),
664 date TIMESTAMP NOT NULL731 date TIMESTAMP NOT NULL
665 )''')732 )''')
733
734 cur.execute('''CREATE TABLE complexity (
735 assignee VARCHAR(255),
736 points VARCHAR(255),
737 milestone CHAR(50) REFERENCES milestones(name),
738 spec VARCHAR(255) REFERENCES specs(name),
739 date TIMESTAMP NOT NULL
740 )''')
666741
667 db.commit()742 db.commit()
668 else:743 else:
@@ -694,6 +769,21 @@
694 db.commit()769 db.commit()
695 dbg('DB upgrade finished')770 dbg('DB upgrade finished')
696 ver = 3771 ver = 3
772
773 if ver == 3:
774 dbg('Upgrading DB to layout version 4')
775 cur.execute('''CREATE TABLE complexity (
776 assignee VARCHAR(255),
777 points VARCHAR(255),
778 milestone CHAR(50) REFERENCES milestones(name),
779 spec VARCHAR(255) REFERENCES specs(name),
780 date TIMESTAMP NOT NULL
781 )''')
782 cur.execute('ALTER TABLE specs ADD COLUMN roadmap_notes VARCHAR(5000)')
783 cur.execute('UPDATE version SET db_layout_ref = 4')
784 db.commit()
785 dbg('DB upgrade finished')
786 ver = 4
697787
698788
699 return db789 return db
@@ -791,6 +881,7 @@
791 cur.execute('DELETE FROM work_items WHERE date = date(CURRENT_TIMESTAMP)')881 cur.execute('DELETE FROM work_items WHERE date = date(CURRENT_TIMESTAMP)')
792 cur.execute('DELETE FROM specs')882 cur.execute('DELETE FROM specs')
793 cur.execute('DELETE FROM meta')883 cur.execute('DELETE FROM meta')
884 cur.execute('DELETE FROM complexity')
794 if opts.refresh:885 if opts.refresh:
795 cur.execute('DELETE FROM milestones')886 cur.execute('DELETE FROM milestones')
796 cur.execute('DELETE FROM teams')887 cur.execute('DELETE FROM teams')
797888
=== modified file 'html-report'
--- html-report 2010-05-18 08:31:00 +0000
+++ html-report 2010-07-08 00:41:40 +0000
@@ -7,6 +7,10 @@
7from report_tools import escape_url, escape_html7from report_tools import escape_url, escape_html
8import report_tools8import report_tools
99
10
11# import pprint
12# pp = pprint.PrettyPrinter(indent=4)
13
10def format_priority(priority):14def format_priority(priority):
11 prio_colors = {15 prio_colors = {
12 'Undefined': 'gray',16 'Undefined': 'gray',
@@ -21,13 +25,13 @@
21 if col:25 if col:
22 return '<span style="color: %s">%s</span>' % (col, p)26 return '<span style="color: %s">%s</span>' % (col, p)
23 return p27 return p
2428
25def spec_completion(db, team, milestone):29def spec_completion(db, team, milestone):
26 print '''<h2>Status by specification</h2>30 print '''<h2>Status by specification</h2>
27<p>(Click header to sort)</p>31<p>(Click header to sort)</p>
28<table id="byspecification">32<table id="byspecification">
29 <thead>33 <thead>
30 <tr><th>Specification</th> <th>todo</th><th>postponed</th><th>done</th> <th>Completion</th> <th>Priority</th> <th>Status</th></tr>34 <tr><th>Specification</th><th>Complexity</th> <th>todo</th><th>postponed</th><th>done</th> <th>Completion</th> <th>Priority</th> <th>Status</th></tr>
31 </thead>35 </thead>
32'''36'''
33 data = report_tools.blueprint_completion(db, team, milestone)37 data = report_tools.blueprint_completion(db, team, milestone)
@@ -40,9 +44,10 @@
40 completion.sort(key=lambda k: k[1]*100+report_tools.priority_value(data[k[0]]['priority']), reverse=True)44 completion.sort(key=lambda k: k[1]*100+report_tools.priority_value(data[k[0]]['priority']), reverse=True)
4145
42 for (bp, percent) in completion:46 for (bp, percent) in completion:
43 print ' <tr><td>%s</td> <td>%i</td><td>%i</td><td>%i</td> <td>%i%%<br/>' \47 print ' <tr><td>%s</td> <td>%s</td><td>%i</td><td>%i</td><td>%i</td> <td>%i%%<br/>' \
44 '<span style="font-size: 70%%">%s</span></td> <td>%s</td> <td>%s</td></tr>' % (48 '<span style="font-size: 70%%">%s</span></td> <td>%s</td> <td>%s</td></tr>' % (
45 '<a href="%s">%s</a>' % (data[bp]['url'], escape_html(bp)),49 '<a href="%s">%s</a>' % (data[bp]['url'], escape_html(bp)),
50 data[bp]['complexity'],
46 data[bp]['todo'], data[bp]['postponed'], data[bp]['done'],51 data[bp]['todo'], data[bp]['postponed'], data[bp]['done'],
47 percent,52 percent,
48 data[bp]['implementation'],53 data[bp]['implementation'],
@@ -57,7 +62,7 @@
57<p>(Click header to sort)</p>62<p>(Click header to sort)</p>
58<table id="byassignee">63<table id="byassignee">
59 <thead>64 <thead>
60 <tr><th>Assignee</th> <th>todo</th><th>postponed</th><th>done</th> <th>Completion</th></tr>65 <tr><th>Assignee</th> <th>Complexity</th><th>todo</th><th>postponed</th><th>done</th> <th>Completion</th></tr>
61 </thead>66 </thead>
62'''67'''
63 data = report_tools.assignee_completion(db, team, milestone)68 data = report_tools.assignee_completion(db, team, milestone)
@@ -73,8 +78,8 @@
73 for (a, percent) in completion:78 for (a, percent) in completion:
74 a_html = escape_url(a or team or 'nobody')79 a_html = escape_url(a or team or 'nobody')
75 url = '%s/~%s/+specs?role=assignee' % (report_tools.blueprints_base_url, a_html)80 url = '%s/~%s/+specs?role=assignee' % (report_tools.blueprints_base_url, a_html)
76 print ' <tr><td><a href="%s">%s</a></td> <td>%i</td><td>%i</td><td>%i</td> <td>%i%%</td></tr>' % (81 print ' <tr><td><a href="%s">%s</a></td> <td>%s</td><td>%i</td><td>%i</td><td>%i</td> <td>%i%%</td></tr>' % (
77 url, a_html, len(data[a]['todo']), len(data[a]['postponed']),82 url, a_html, data[a]['complexity'], len(data[a]['todo']), len(data[a]['postponed']),
78 len(data[a]['done']), percent)83 len(data[a]['done']), percent)
79 print '</table>'84 print '</table>'
8085
8186
=== modified file 'report_tools.py'
--- report_tools.py 2010-04-06 13:48:48 +0000
+++ report_tools.py 2010-07-08 00:41:40 +0000
@@ -139,7 +139,7 @@
139 if team:139 if team:
140 # include both direct team assignments, as well as assignmnets to140 # include both direct team assignments, as well as assignmnets to
141 # members of that team141 # members of that team
142 cur.execute('SELECT s.name, COUNT(DISTINCT w.description), s.status, s.priority, s.implementation, s.url '142 cur.execute('SELECT s.name, COUNT(DISTINCT w.description), s.status, s.priority, s.implementation, s.url, s.roadmap_notes '
143 'FROM work_items w, specs s ON w.spec = s.name '143 'FROM work_items w, specs s ON w.spec = s.name '
144 ' LEFT JOIN teams ON (s.assignee = teams.name OR w.assignee = teams.name)'144 ' LEFT JOIN teams ON (s.assignee = teams.name OR w.assignee = teams.name)'
145 'WHERE w.status = ? AND w.date = ? AND'145 'WHERE w.status = ? AND w.date = ? AND'
@@ -147,20 +147,26 @@
147 'GROUP BY w.spec' % ms_sql,147 'GROUP BY w.spec' % ms_sql,
148 (s, last_date, team, team, team))148 (s, last_date, team, team, team))
149 else:149 else:
150 cur.execute('SELECT s.name, COUNT(DISTINCT w.description), s.status, s.priority, s.implementation, s.url '150 cur.execute('SELECT s.name, COUNT(DISTINCT w.description), s.status, s.priority, s.implementation, s.url, s.roadmap_notes '
151 'FROM work_items w, specs s '151 'FROM work_items w, specs s '
152 'ON w.spec = s.name '152 'ON w.spec = s.name '
153 'WHERE w.status = ? AND w.date = ? %s '153 'WHERE w.status = ? AND w.date = ? %s '
154 'GROUP BY w.spec' % ms_sql,154 'GROUP BY w.spec' % ms_sql,
155 (s, last_date))155 (s, last_date))
156156
157 for (bp, num, status, priority, impl, url) in cur:157 for (bp, num, status, priority, impl, url, roadmap_notes) in cur:
158 info = data.setdefault(bp, {'todo': 0, 'done': 0, 'postponed': 0})158 info = data.setdefault(bp, {'todo': 0, 'done': 0, 'postponed': 0})
159 info[s] = num159 info[s] = num
160 info['status'] = status or ''160 info['status'] = status or ''
161 info['priority'] = priority161 info['priority'] = priority
162 info['implementation'] = impl162 info['implementation'] = impl
163 info['url'] = url163 info['url'] = url
164 info['roadmap_notes'] = roadmap_notes
165
166 cur2 = db.cursor();
167 cur2.execute('SELECT SUM(points) FROM complexity as w WHERE spec = ? AND date = ? %s' % ms_sql, (bp, last_date))
168 for points in cur2:
169 info['complexity'] = points[0]
164170
165 return data171 return data
166172
@@ -258,6 +264,11 @@
258 info = data.setdefault(a, {'todo': [], 'done': [], 'postponed': []})264 info = data.setdefault(a, {'todo': [], 'done': [], 'postponed': []})
259 info[s].append([bp, description, priority, url])265 info[s].append([bp, description, priority, url])
260266
267 cur2 = db.cursor();
268 cur2.execute('SELECT SUM(points) FROM complexity as w WHERE assignee = ? AND date = ? %s' % ms_sql, (a, last_date))
269 for points in cur2:
270 info['complexity'] = points[0]
271
261 return data272 return data
262273
263def spec_information( db, config, team=None, milestone=None ):274def spec_information( db, config, team=None, milestone=None ):

Subscribers

People subscribed via source and target branches

to all changes: