Merge lp:~jib/launchpad-work-items-tracker/complexity-points into lp:launchpad-work-items-tracker
- complexity-points
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Pitt (community) | Needs Fixing | ||
Review via email: mp+29043@code.launchpad.net |
Commit message
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.
- 192. By Jos Boumans <jib@lucid-server>
-
* apply style notes as suggested by pitti
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
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(' ', ' ').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 ): |
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)): defs[i] )
+ list.append(
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!