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
1=== modified file 'collect'
2--- collect 2010-06-18 14:30:21 +0000
3+++ collect 2010-07-08 00:41:40 +0000
4@@ -12,6 +12,7 @@
5 import report_tools
6
7 debug = False
8+#debug = True
9
10 # if this is None, data errors go to stderr; if this is a list of (spec_name,
11 # msg) tuples, data errors get collected here and mailed out at the end
12@@ -112,6 +113,7 @@
13 'description': '<div class="top-portlet">\s*<p>(.*?)</p>',
14 'status': '(?:<p>|^)[Ss]tatus:\s*<br />(.*?)</p>',
15 'details_url': 'href="([^"]*)">Read the full specification</a>',
16+ 'roadmap_notes': '(?:<p>|^)[Rr]oadmap\s+[Nn]otes:\s*<br />(.*?)</p>',
17 }
18
19 data = {}
20@@ -148,11 +150,11 @@
21
22 dbg('lp_import_blueprint(%s): finished parsing; data: %s' % (name, str(data)))
23
24- db.cursor().execute('INSERT INTO specs VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?)',
25+ db.cursor().execute('INSERT INTO specs VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?,?)',
26 (name, url, data['priority'], data['implementation'],
27 data['assignee'], data['status'], data['milestone'],
28 data['definition'], data['drafter'], data['approver'],
29- data['details_url']))
30+ data['details_url'], data['roadmap_notes']))
31
32 return True
33
34@@ -183,6 +185,52 @@
35 (key, value, bp_name))
36 db.commit()
37
38+def parse_complexity_item( lp, db, line, bp_name, bp_url, def_milestone, def_assignee):
39+
40+ ### cargo culted from parse_blueprint_workitem
41+ line = line.replace('<br />', '').replace('</div>', '').replace('</p>',
42+ '').replace('<wbr></wbr>', '').replace('&nbsp;', ' ').replace(
43+ '<a href="/', '<a href="https://launchpad.net/').strip()
44+
45+ ### remove special characters people tend to type
46+ line = re.sub('[^\w -.]', '', line)
47+
48+ if not line:
49+ return
50+
51+ dbg("\tParsing complexity line '%s'" % line)
52+
53+ num = None
54+ milestone = None
55+ assignee = None
56+
57+ try:
58+ ### line.split().reverse() throws a typeerror exception
59+ list = line.split()
60+ list.reverse()
61+
62+ ### we may not have any values in the list, so append our
63+ ### default values in the right order so they can be mapped
64+ ### if we *dont* have enough values, an exception occurs
65+ defs = [ None, def_milestone, def_assignee ]
66+
67+ for i in range(len(list), len(defs)):
68+ list.append(defs[i])
69+
70+ (num, milestone, assignee) = list
71+
72+ if not num:
73+ data_error(bp_url, "No complexity points defined for %s" % line)
74+
75+ dbg("\tComplexity: %s MS: %s Who: %s" % (num, milestone, assignee))
76+
77+ cur = db.cursor()
78+ cur.execute('INSERT INTO complexity VALUES(?,?,?,?,date(CURRENT_TIMESTAMP))',
79+ (assignee, num, milestone, bp_name))
80+ db.commit()
81+
82+ except ValueError:
83+ data_error(bp_url, "\tComplexity line '%s' could not be parsed %s" % (line, ValueError))
84
85 def parse_blueprint_workitem(line, default_assignee, milestone,
86 blueprint_url, launchpad, allow_inprogress, result_list):
87@@ -326,10 +374,12 @@
88 bugnum_re = re.compile('<a href="https://bugs\..*launchpad\.net/bugs/([0-9]+)" class="sprite bug">')
89 work_items_re = re.compile('(<p>|^)work items(.*)\s*:\s*<br />', re.I)
90 meta_re = re.compile( '(<p>|^)Meta.*?:<br />', re.I )
91+ complexity_re = re.compile( '(<p>|^)Complexity.*?:<br />', re.I )
92
93 in_workitems_block = False
94 in_linked_bugs_block = False
95 in_meta_block = False
96+ in_complexity_block = False
97 found_linked_bugs = False
98 work_items = []
99 found_wb_workitems = False
100@@ -347,7 +397,11 @@
101
102 for l in contents.splitlines():
103
104- if not in_workitems_block and not in_linked_bugs_block and not in_meta_block:
105+ if not in_workitems_block and not in_linked_bugs_block \
106+ and not in_meta_block and not in_complexity_block:
107+
108+ #dbg( 'processing: %s' % l )
109+
110 m = work_items_re.search(l)
111 if m:
112 in_workitems_block = True
113@@ -361,6 +415,8 @@
114 found_linked_bugs = True
115 if meta_re.search(l):
116 in_meta_block = True
117+ if complexity_re.search(l):
118+ in_complexity_block = True
119 continue
120
121 if in_workitems_block:
122@@ -394,6 +450,16 @@
123 in_meta_block = False
124 continue
125
126+ if in_complexity_block:
127+ dbg("\tcomplexity block line (raw): '%s'" % (l.strip()))
128+
129+ parse_complexity_item(lp, db, l, bp_name, bp_url, spec_milestone, spec_assignee)
130+
131+ ### done with the meta information?
132+ if '</p>' in l:
133+ in_complexity_block = False
134+ continue
135+
136 if found_wb_workitems and '</div>' in l:
137 break
138
139@@ -465,7 +531,7 @@
140 lp_import_teams(lp, db, cfg)
141
142 for (bp, (url, milestone)) in lp_blueprints_from_list(url, name_pattern).iteritems():
143- dbg('lp_import(): downloading ' + bp)
144+ dbg('lp_import(): downloading %s from %s' % (bp, url))
145 contents = urllib.urlopen(url).read().decode('UTF-8')
146 if lp_import_blueprint(db, bp, url, contents):
147 lp_import_blueprint_workitems(lp, db, bp, url, contents, cfg.get('release'))
148@@ -598,7 +664,7 @@
149 db.cursor().execute('INSERT INTO work_items VALUES (?, ?, ?, ?, ?, date(CURRENT_TIMESTAMP))',
150 (desc, name, state, assignee, milestone))
151
152- db.cursor().execute("INSERT INTO specs VALUES (?, ?, 'Medium', 'Unknown', NULL, NULL, ?, NULL, NULL, NULL, NULL, NULL)",
153+ db.cursor().execute("INSERT INTO specs VALUES (?, ?, 'Medium', 'Unknown', NULL, NULL, ?, NULL, NULL, NULL, NULL, NULL, NULL)",
154 (name, spec_url, status))
155
156 ########################################################################
157@@ -621,7 +687,7 @@
158 cur.execute('''CREATE TABLE version (
159 db_layout_ref INT NOT NULL
160 )''')
161- cur.execute('''INSERT INTO version VALUES (3)''')
162+ cur.execute('''INSERT INTO version VALUES (4)''')
163
164 cur.execute('''CREATE TABLE specs (
165 name VARCHAR(255) PRIMARY KEY,
166@@ -635,7 +701,8 @@
167 definition CHAR(30),
168 drafter CHAR(50),
169 approver CHAR(50),
170- details_url VARCHAR(1000)
171+ details_url VARCHAR(1000),
172+ roadmap_notes VARCHAR(5000)
173 )''')
174
175 cur.execute('''CREATE TABLE work_items (
176@@ -663,6 +730,14 @@
177 spec VARCHAR(255) REFERENCES specs(name),
178 date TIMESTAMP NOT NULL
179 )''')
180+
181+ cur.execute('''CREATE TABLE complexity (
182+ assignee VARCHAR(255),
183+ points VARCHAR(255),
184+ milestone CHAR(50) REFERENCES milestones(name),
185+ spec VARCHAR(255) REFERENCES specs(name),
186+ date TIMESTAMP NOT NULL
187+ )''')
188
189 db.commit()
190 else:
191@@ -694,6 +769,21 @@
192 db.commit()
193 dbg('DB upgrade finished')
194 ver = 3
195+
196+ if ver == 3:
197+ dbg('Upgrading DB to layout version 4')
198+ cur.execute('''CREATE TABLE complexity (
199+ assignee VARCHAR(255),
200+ points VARCHAR(255),
201+ milestone CHAR(50) REFERENCES milestones(name),
202+ spec VARCHAR(255) REFERENCES specs(name),
203+ date TIMESTAMP NOT NULL
204+ )''')
205+ cur.execute('ALTER TABLE specs ADD COLUMN roadmap_notes VARCHAR(5000)')
206+ cur.execute('UPDATE version SET db_layout_ref = 4')
207+ db.commit()
208+ dbg('DB upgrade finished')
209+ ver = 4
210
211
212 return db
213@@ -791,6 +881,7 @@
214 cur.execute('DELETE FROM work_items WHERE date = date(CURRENT_TIMESTAMP)')
215 cur.execute('DELETE FROM specs')
216 cur.execute('DELETE FROM meta')
217+ cur.execute('DELETE FROM complexity')
218 if opts.refresh:
219 cur.execute('DELETE FROM milestones')
220 cur.execute('DELETE FROM teams')
221
222=== modified file 'html-report'
223--- html-report 2010-05-18 08:31:00 +0000
224+++ html-report 2010-07-08 00:41:40 +0000
225@@ -7,6 +7,10 @@
226 from report_tools import escape_url, escape_html
227 import report_tools
228
229+
230+# import pprint
231+# pp = pprint.PrettyPrinter(indent=4)
232+
233 def format_priority(priority):
234 prio_colors = {
235 'Undefined': 'gray',
236@@ -21,13 +25,13 @@
237 if col:
238 return '<span style="color: %s">%s</span>' % (col, p)
239 return p
240-
241+
242 def spec_completion(db, team, milestone):
243 print '''<h2>Status by specification</h2>
244 <p>(Click header to sort)</p>
245 <table id="byspecification">
246 <thead>
247- <tr><th>Specification</th> <th>todo</th><th>postponed</th><th>done</th> <th>Completion</th> <th>Priority</th> <th>Status</th></tr>
248+ <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>
249 </thead>
250 '''
251 data = report_tools.blueprint_completion(db, team, milestone)
252@@ -40,9 +44,10 @@
253 completion.sort(key=lambda k: k[1]*100+report_tools.priority_value(data[k[0]]['priority']), reverse=True)
254
255 for (bp, percent) in completion:
256- print ' <tr><td>%s</td> <td>%i</td><td>%i</td><td>%i</td> <td>%i%%<br/>' \
257+ print ' <tr><td>%s</td> <td>%s</td><td>%i</td><td>%i</td><td>%i</td> <td>%i%%<br/>' \
258 '<span style="font-size: 70%%">%s</span></td> <td>%s</td> <td>%s</td></tr>' % (
259 '<a href="%s">%s</a>' % (data[bp]['url'], escape_html(bp)),
260+ data[bp]['complexity'],
261 data[bp]['todo'], data[bp]['postponed'], data[bp]['done'],
262 percent,
263 data[bp]['implementation'],
264@@ -57,7 +62,7 @@
265 <p>(Click header to sort)</p>
266 <table id="byassignee">
267 <thead>
268- <tr><th>Assignee</th> <th>todo</th><th>postponed</th><th>done</th> <th>Completion</th></tr>
269+ <tr><th>Assignee</th> <th>Complexity</th><th>todo</th><th>postponed</th><th>done</th> <th>Completion</th></tr>
270 </thead>
271 '''
272 data = report_tools.assignee_completion(db, team, milestone)
273@@ -73,8 +78,8 @@
274 for (a, percent) in completion:
275 a_html = escape_url(a or team or 'nobody')
276 url = '%s/~%s/+specs?role=assignee' % (report_tools.blueprints_base_url, a_html)
277- print ' <tr><td><a href="%s">%s</a></td> <td>%i</td><td>%i</td><td>%i</td> <td>%i%%</td></tr>' % (
278- url, a_html, len(data[a]['todo']), len(data[a]['postponed']),
279+ 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>' % (
280+ url, a_html, data[a]['complexity'], len(data[a]['todo']), len(data[a]['postponed']),
281 len(data[a]['done']), percent)
282 print '</table>'
283
284
285=== modified file 'report_tools.py'
286--- report_tools.py 2010-04-06 13:48:48 +0000
287+++ report_tools.py 2010-07-08 00:41:40 +0000
288@@ -139,7 +139,7 @@
289 if team:
290 # include both direct team assignments, as well as assignmnets to
291 # members of that team
292- cur.execute('SELECT s.name, COUNT(DISTINCT w.description), s.status, s.priority, s.implementation, s.url '
293+ cur.execute('SELECT s.name, COUNT(DISTINCT w.description), s.status, s.priority, s.implementation, s.url, s.roadmap_notes '
294 'FROM work_items w, specs s ON w.spec = s.name '
295 ' LEFT JOIN teams ON (s.assignee = teams.name OR w.assignee = teams.name)'
296 'WHERE w.status = ? AND w.date = ? AND'
297@@ -147,20 +147,26 @@
298 'GROUP BY w.spec' % ms_sql,
299 (s, last_date, team, team, team))
300 else:
301- cur.execute('SELECT s.name, COUNT(DISTINCT w.description), s.status, s.priority, s.implementation, s.url '
302+ cur.execute('SELECT s.name, COUNT(DISTINCT w.description), s.status, s.priority, s.implementation, s.url, s.roadmap_notes '
303 'FROM work_items w, specs s '
304 'ON w.spec = s.name '
305 'WHERE w.status = ? AND w.date = ? %s '
306 'GROUP BY w.spec' % ms_sql,
307 (s, last_date))
308
309- for (bp, num, status, priority, impl, url) in cur:
310+ for (bp, num, status, priority, impl, url, roadmap_notes) in cur:
311 info = data.setdefault(bp, {'todo': 0, 'done': 0, 'postponed': 0})
312 info[s] = num
313 info['status'] = status or ''
314 info['priority'] = priority
315 info['implementation'] = impl
316 info['url'] = url
317+ info['roadmap_notes'] = roadmap_notes
318+
319+ cur2 = db.cursor();
320+ cur2.execute('SELECT SUM(points) FROM complexity as w WHERE spec = ? AND date = ? %s' % ms_sql, (bp, last_date))
321+ for points in cur2:
322+ info['complexity'] = points[0]
323
324 return data
325
326@@ -258,6 +264,11 @@
327 info = data.setdefault(a, {'todo': [], 'done': [], 'postponed': []})
328 info[s].append([bp, description, priority, url])
329
330+ cur2 = db.cursor();
331+ cur2.execute('SELECT SUM(points) FROM complexity as w WHERE assignee = ? AND date = ? %s' % ms_sql, (a, last_date))
332+ for points in cur2:
333+ info['complexity'] = points[0]
334+
335 return data
336
337 def spec_information( db, config, team=None, milestone=None ):

Subscribers

People subscribed via source and target branches

to all changes: