Merge lp:~james-w/launchpad-work-items-tracker/date-based into lp:~linaro-automation/launchpad-work-items-tracker/linaro

Proposed by James Westby
Status: Superseded
Proposed branch: lp:~james-w/launchpad-work-items-tracker/date-based
Merge into: lp:~linaro-automation/launchpad-work-items-tracker/linaro
Diff against target: 526 lines (+176/-73)
6 files modified
burndown-chart (+16/-7)
generate-all (+12/-2)
html-report (+12/-24)
report_tools.py (+84/-30)
templates/base.html (+20/-0)
templates/milestone_list.html (+32/-10)
To merge this branch: bzr merge lp:~james-w/launchpad-work-items-tracker/date-based
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle (community) Abstain
Review via email: mp+52895@code.launchpad.net

This proposal has been superseded by a proposal from 2011-03-17.

Description of the change

Hi,

This branch adds a new way of viewing milestones, by target
date rather than by milestone.

This means that you can have a page that shows info for all
milestones that land on the same day.

This is desirable, as the monthly milestones we will be moving
to may be named differently in different projects, but engineers
will want to see everything that they have to do for a particular
day on one page.

Thanks,

James

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

I don't think I understand the code well enough to say whether these changes make sense, I'm afraid. Happy to talk through the changes on mumble.

The sheer number of places that have to be changed to accept a date argument suggests that the layering in this code isn't really right, but I don't think that's a surprise to learn somehow :-)

review: Abstain
283. By James Westby

Rework to use polymorpism for milestone due dates.

284. By James Westby

Changes from review comments. Thanks Michael.

285. By James Westby

Comments from review. Thanks Michael.

Unmerged revisions

279. By James Westby

Set the right status for each bug task workitem.

278. By James Westby

Allow workitems from projects other than Ubuntu.

277. By James Westby

Correct a copy-paste error that stopped the new workitem list pages from being generated.

276. By James Westby

Generate pages for each workitem status.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'burndown-chart'
2--- burndown-chart 2011-02-16 23:59:52 +0000
3+++ burndown-chart 2011-03-10 18:14:55 +0000
4@@ -247,6 +247,8 @@
5 help='Do not show foreign totals separate', dest='noforeign')
6 optparser.add_option('--group',
7 help='Run for this group', dest='group')
8+optparser.add_option('--date',
9+ help='Run for this date', dest='date')
10
11 (opts, args) = optparser.parse_args()
12 if not opts.database:
13@@ -260,6 +262,8 @@
14 optparser.error('user and group options are mutually exclusive')
15 if opts.team and opts.group:
16 optparser.error('team and group options are mutually exclusive')
17+if opts.milestone and opts.date:
18+ optparser.error('milestone and date options are mutually exclusive')
19
20 # The typing allows polymorphic behavior
21 if opts.user:
22@@ -269,11 +273,11 @@
23
24 # get date -> state -> count mapping
25 db = report_tools.get_db(opts.database)
26-data = report_tools.workitems_over_time(db, team=opts.team, milestone=opts.milestone, group=opts.group)
27+data = report_tools.workitems_over_time(db, team=opts.team, milestone=opts.milestone, group=opts.group, date=opts.date)
28
29 if len(data) == 0:
30- print 'WARNING: no work items, not generating chart (team: %s, milestone: %s, group: %s)' % (
31- opts.team or 'all', opts.milestone or 'none', opts.group or 'none')
32+ print 'WARNING: no work items, not generating chart (team: %s, milestone: %s, group: %s, date: %s)' % (
33+ opts.team or 'all', opts.milestone or 'none', opts.group or 'none', opts.date or 'none')
34 sys.exit(0)
35
36 # calculate start/end date if no dates are given
37@@ -283,14 +287,17 @@
38 start_date=opts.start_date
39
40 if opts.end_date is None:
41- cur = db.cursor()
42- end_date=report_tools.milestone_due_date(db, milestone=opts.milestone)
43+ if opts.date:
44+ end_date = opts.date
45+ else:
46+ cur = db.cursor()
47+ end_date=report_tools.milestone_due_date(db, milestone=opts.milestone)
48 else:
49 end_date=opts.end_date
50
51 if not start_date or not end_date or date_to_ordinal(start_date) > date_to_ordinal(end_date):
52- print 'WARNING: empty date range, not generating chart (team: %s, milestone: %s)' % (
53- opts.team or 'all', opts.milestone or 'none')
54+ print 'WARNING: empty date range, not generating chart (team: %s, milestone: %s, group: %s, date: %s)' % (
55+ opts.team or 'all', opts.milestone or 'none', opts.group or 'none', opts.date or 'none')
56 sys.exit(0)
57
58 # title
59@@ -307,6 +314,8 @@
60
61 if opts.milestone:
62 title += ' (%s)' % opts.milestone
63+elif opts.date:
64+ title += ' (%s)' % opts.date
65
66 do_chart(data, start_date, end_date, opts.trendstart, title, opts.output, opts.only_weekdays, opts.inverted, noforeign)
67
68
69=== modified file 'generate-all'
70--- generate-all 2011-02-26 00:57:42 +0000
71+++ generate-all 2011-03-10 18:14:55 +0000
72@@ -34,11 +34,15 @@
73 optparser.error('output directory does not exist')
74
75 if opts.config:
76- trend_starts = report_tools.load_config(opts.config).get('trend_start', {})
77- burnup_chart_teams = report_tools.load_config(opts.config).get('burnup_chart_teams', [])
78+ cfg = report_tools.load_config(opts.config)
79+ trend_starts = cfg.get('trend_start', {})
80+ burnup_chart_teams = cfg.get('burnup_chart_teams', [])
81+ primary_team = cfg.get("primary_team", None)
82 else:
83+ cfg = None
84 trend_starts = {}
85 burnup_chart_teams = []
86+ primary_team = None
87
88 lock_path = opts.database + ".generate_lock"
89 lock_f = open(lock_path, "wb")
90@@ -104,6 +108,12 @@
91 else:
92 report_tools.run_reports(my_path, opts.database, basename, opts.config, milestone=m, team=None, root=opts.root)
93
94+# date-based milestone view
95+for mg in report_tools.milestone_groups(db, team=primary_team):
96+ due_date_str = mg.due_date_str
97+ basename = os.path.join(opts.output_dir, due_date_str)
98+ report_tools.run_reports(my_path, opts.database, basename, opts.config, team=primary_team, root=opts.root, date=due_date_str)
99+
100 # groups
101 for g in groups:
102 basename = os.path.join(groupssubdir, g)
103
104=== modified file 'html-report'
105--- html-report 2011-02-28 15:59:50 +0000
106+++ html-report 2011-03-10 18:14:55 +0000
107@@ -105,8 +105,8 @@
108 return self._assignee or self.blueprint.assignee
109
110
111-def spec_group_completion(db, team, milestone):
112- data = report_tools.spec_group_completion(db, team, milestone)
113+def spec_group_completion(db, team, milestone=None, date=None):
114+ data = report_tools.spec_group_completion(db, team, milestone=milestone, date=date)
115 if not data:
116 return dict(areas=[], group_completion_series=[], groups=[])
117 groups = []
118@@ -250,25 +250,6 @@
119 return dict(members=sorted(members))
120
121
122-class Milestone(object):
123-
124- def __init__(self, name, link, project):
125- self.name = name
126- self.link = link
127- self.project = project
128-
129-
130-def milestones_info(db, team, milestone):
131- if team:
132- template = team + "-%s.html"
133- else:
134- template = "%s.html"
135- milestones = []
136- for milestone, project in report_tools.milestone_list(db).items():
137- milestones.append(Milestone(milestone, template % milestone, project))
138- return dict(milestones=milestones)
139-
140-
141 def blueprint_count(db, team, milestone):
142 return dict(blueprint_count=report_tools.blueprint_count(db, team=team, milestone=milestone))
143
144@@ -360,13 +341,15 @@
145
146 if opts.milestone:
147 title += ' (%s)' % opts.milestone
148+ elif opts.date:
149+ title += ' (%s)' % opts.date
150
151 data = self.template_data(db, opts)
152 data.update(dict(team_name=title, chart_url=opts.chart_url))
153 data.update(spec_group_completion(db, opts.team, opts.milestone))
154 data.update(spec_completion(db, opts.team, opts.milestone))
155 data.update(by_assignee(db, opts.team, opts.milestone))
156- if opts.milestone:
157+ if opts.milestone or opts.date:
158 data.update(dict(page_type="milestones"))
159 elif isinstance(opts.team, report_tools.user_string):
160 data.update(dict(page_type="people"))
161@@ -390,7 +373,6 @@
162 data.update(dict(team_name=title, chart_url=opts.chart_url))
163 data.update(spec_group_completion(db, None, opts.milestone))
164 data.update(spec_completion(db, opts.team, opts.milestone))
165- data.update(milestones_info(db, opts.team, opts.milestone))
166 data.update(blueprint_count(db, opts.team, opts.milestone))
167 data.update(dict(page_type="overview"))
168 print report_tools.fill_template("overview.html", data)
169@@ -423,7 +405,9 @@
170
171 def milestone_list(self, db, opts):
172 data = self.template_data(db, opts)
173- data.update(milestones_info(db, opts.team, opts.milestone))
174+ data.update(
175+ milestone_groups=report_tools.milestone_groups(
176+ db, team=opts.team))
177 data.update(dict(page_type="milestones"))
178 print report_tools.fill_template("milestone_list.html", data)
179
180@@ -474,12 +458,16 @@
181 help="Root URL for the charts")
182 optparser.add_option('--status', dest="status",
183 help="Workitem status to consider for workitem_list report-type")
184+ optparser.add_option('--date', dest="date",
185+ help="Include all milestones targetted to this date.")
186
187 (opts, args) = optparser.parse_args()
188 if not opts.database:
189 optparser.error('No database given')
190 if opts.user and opts.team:
191 optparser.error('team and user options are mutually exclusive')
192+ if opts.milestone and opts.date:
193+ optparser.error('milestone and date options are mutually exclusive')
194
195 # The typing allows polymorphic behavior
196 if opts.user:
197
198=== modified file 'report_tools.py'
199--- report_tools.py 2011-02-28 18:18:24 +0000
200+++ report_tools.py 2011-03-10 18:14:55 +0000
201@@ -24,13 +24,15 @@
202 def escape_sql(text):
203 return text.replace("'", "''")
204
205-def report_args(args, team=None, user=None, milestone=None):
206+def report_args(args, team=None, user=None, milestone=None, date=None):
207 if milestone:
208 args.extend(['-m',milestone])
209 if team:
210 args.extend(['-t',team])
211 if user:
212 args.extend(['-u',user])
213+ if date:
214+ args.extend(['--date', date])
215
216 def status_overview(my_path, database, basename, config, root=None):
217 cfg = load_config(config)
218@@ -166,13 +168,17 @@
219
220
221 def run_reports(my_path, database, basename, config, milestone=None, team=None,
222- user=None, trend_starts=None, trend_override=None, burnup=False, root=None):
223+ user=None, trend_starts=None, trend_override=None, burnup=False, root=None, date=None):
224
225 ident = (team or user)
226 if milestone and ident:
227 chartname = ident + '-' + milestone
228 elif milestone:
229 chartname = 'all-' + milestone
230+ elif date and ident:
231+ chartname = "%s-%s" % (date, milestone)
232+ elif date:
233+ chartname = 'all-' + date
234 elif ident:
235 chartname = ident
236 else:
237@@ -186,16 +192,17 @@
238 args += ['--title', cfg['title']]
239 if root:
240 args += ['--root', root]
241- report_args(args, team, user, milestone)
242+ report_args(args, team=team, user=user, milestone=milestone, date=date)
243 hreport = Popen(args, stdout=hf)
244
245- jf = open(basename + '.json', 'w')
246- args = [os.path.join(my_path, 'json-report'), '-d', database, '-c', config]
247- report_args(args, team, user, milestone)
248- jreport = Popen(args, stdout=jf)
249+ if not date:
250+ jf = open(basename + '.json', 'w')
251+ args = [os.path.join(my_path, 'json-report'), '-d', database, '-c', config]
252+ report_args(args, team=team, user=user, milestone=milestone)
253+ jreport = Popen(args, stdout=jf)
254
255 args = [os.path.join(my_path, 'burndown-chart'), '-d', database, '-o', basename + '.svg']
256- report_args(args, team, user, milestone)
257+ report_args(args, team=team, user=user, milestone=milestone, date=date)
258
259 if trend_override:
260 args.extend(['--trend-start', trend_override])
261@@ -203,7 +210,7 @@
262 if milestone and ident:
263 if (ident, milestone) in trend_starts:
264 args.extend(['--trend-start', str(trend_starts[(ident, milestone)])])
265- elif ident:
266+ elif ident and not date:
267 if ident in trend_starts:
268 args.extend(['--trend-start', str(trend_starts[ident])])
269 if burnup:
270@@ -259,7 +266,7 @@
271 return escape(html, True)
272
273
274-def get_milestone_start_sql(db, milestone):
275+def get_milestone_start_sql(db, milestone=None, date=None):
276 if milestone:
277 # get milestone date range
278 cur = db.cursor()
279@@ -279,12 +286,21 @@
280 else:
281 ms_sql = "AND w.milestone='%s' AND date <= '%s'" % (
282 escape_sql(milestone), date_end)
283+ elif date:
284+ cur = db.cursor()
285+ cur.execute('SELECT MAX(due_date), MAX(due_date) < date(CURRENT_TIMESTAMP) FROM milestones WHERE due_date < ?', (escape_sql(date), ))
286+ (date_start, in_milestone) = cur.fetchone()
287+ if in_milestone:
288+ ms_sql = "AND date >= '%s' AND date <= '%s'" % (date_start, escape_sql(date))
289+ else:
290+ ms_sql = "AND date <= '%s'" % escape_sql(date)
291+ ms_sql += "AND w.milestone IN (SELECT m.name from milestones m WHERE m.due_date = '%s')" % escape_sql(date)
292 else:
293 ms_sql = ''
294 return ms_sql
295
296
297-def workitems_over_time(db, team=None, milestone=None, group=None):
298+def workitems_over_time(db, team=None, milestone=None, group=None, date=None):
299 '''Calculate work item development over time.
300
301 If team is given, this filters out blueprints for that particular team. If
302@@ -296,7 +312,7 @@
303 {todo,inprogress,done,postponed}{,_teamonly}.
304 '''
305 data = {}
306- ms_sql = get_milestone_start_sql(db, milestone)
307+ ms_sql = get_milestone_start_sql(db, milestone=milestone, date=date)
308 # include both direct team assignments, as well as assignmets to
309 # members of that team
310 cur = db.cursor()
311@@ -361,14 +377,20 @@
312 data.setdefault(date, {})[s] = num
313 return data
314
315-def spec_group_completion(db, team=None, milestone=None):
316- data = {}
317- if milestone == '':
318+def milestone_select_sql(milestone=None, date=None):
319+ if not milestone and not date:
320 ms_sql = "AND w.milestone IS NULL"
321 elif milestone:
322 ms_sql = "AND w.milestone='%s'" % escape_sql(milestone)
323+ elif date:
324+ ms_sql = "AND w.milestone IN (SELECT m.name from milestones m WHERE m.due_date = '%s'" % date
325 else:
326 ms_sql = ''
327+ return ms_sql
328+
329+def spec_group_completion(db, team=None, milestone=None, date=None):
330+ data = {}
331+ ms_sql = milestone_select_sql(milestone=milestone, date=date)
332
333 # last date
334 cur = db.cursor()
335@@ -451,19 +473,14 @@
336 return info
337
338
339-def blueprint_completion(db, team=None, milestone=None):
340+def blueprint_completion(db, team=None, milestone=None, date=None):
341 '''Determine current blueprint completion.
342
343 Return blueprint -> info mapping, with info being a map with these
344 keys: todo, inprogress, done, postponed, status, priority, implementation, url.
345 '''
346 data = {}
347- if milestone == '':
348- ms_sql = "AND w.milestone IS NULL"
349- elif milestone:
350- ms_sql = "AND w.milestone='%s'" % escape_sql(milestone)
351- else:
352- ms_sql = ''
353+ ms_sql = milestone_select_sql(milestone=milestone, date=date)
354
355 # last date
356 cur = db.cursor()
357@@ -651,7 +668,7 @@
358
359
360 def milestone_start_date(db, milestone=None):
361- ms_sql = get_milestone_start_sql(db, milestone)
362+ ms_sql = get_milestone_start_sql(db, milestone=milestone)
363 cur = db.cursor()
364 cur.execute('SELECT MIN(date) FROM work_items w '
365 'WHERE 1=1 %s GROUP BY date' % ms_sql)
366@@ -673,12 +690,13 @@
367 cycle_percent_elapsed = duration_elapsed(
368 start_date, due_date, current_date)
369 return dict(
370+ current_date=current_date,
371 days_left=weekdays_between(current_date, due_date),
372 cycle_percent_elapsed=cycle_percent_elapsed,
373 )
374
375
376-def assignee_completion(db, team=None, milestone=None, group=None):
377+def assignee_completion(db, team=None, milestone=None, group=None, date=None):
378 '''Determine current by-assignee completion.
379
380 Return assignee -> info mapping with info being a map with these
381@@ -686,12 +704,7 @@
382 workitem, priority, spec_url].
383 '''
384 data = {}
385- if milestone == '':
386- ms_sql = "AND w.milestone IS NULL"
387- elif milestone:
388- ms_sql = "AND w.milestone='%s'" % milestone
389- else:
390- ms_sql = ''
391+ ms_sql = milestone_select_sql(milestone=milestone, date=date)
392
393 # last date
394 cur = db.cursor()
395@@ -904,6 +917,47 @@
396 return milestones
397
398
399+class Milestone(object):
400+
401+ def __init__(self, name, link, project, due_date):
402+ self.name = name
403+ self.link = link
404+ self.project = project
405+ self.due_date = due_date
406+
407+
408+class MilestoneGroup(object):
409+
410+ def __init__(self, due_date):
411+ self.due_date = due_date
412+ self.milestones = []
413+
414+ @property
415+ def due_date_str(self):
416+ return self.due_date.strftime("%Y-%m-%d")
417+
418+
419+def milestone_groups(db, team=None):
420+ if team:
421+ template = team + "-%s.html"
422+ else:
423+ template = "%s.html"
424+ groups = []
425+ cur = db.cursor()
426+ cur.execute('SELECT m.name, m.project, m.due_date '
427+ 'FROM milestones m ORDER BY m.due_date, m.name, m.project')
428+ for name, project, due_date_str in cur:
429+ due_date = date_to_python(due_date_str)
430+ if len(groups) < 1 or due_date != groups[-1].due_date:
431+ group = MilestoneGroup(due_date)
432+ groups.append(group)
433+ else:
434+ group = groups[-1]
435+ group.milestones.append(
436+ Milestone(name, template % name, project, due_date))
437+ return groups
438+
439+
440 def real_names(db):
441 cur = db.cursor()
442 cur.execute('SELECT name, display_name FROM real_names')
443
444=== modified file 'templates/base.html'
445--- templates/base.html 2011-02-28 15:59:50 +0000
446+++ templates/base.html 2011-03-10 18:14:55 +0000
447@@ -12,6 +12,26 @@
448 border-width: 1px; padding-right: 10px; }
449 table#byworkitem td { vertical-align: top; }
450
451+ table#milestones {
452+ border-style: none;
453+ }
454+
455+ table#milestones td.date {
456+ border-left: 3px solid #98C13D;
457+ }
458+
459+ table#milestones td {
460+ vertical-align: bottom;
461+ border-style: none;
462+ border-bottom: none;
463+ padding-left: 0px;
464+ margin-left: 0px;
465+ }
466+
467+ table#milestones td span.connector{
468+ color: #98C13D;
469+ }
470+
471 td.priority_Undefined, th.priority_Undefined { color: gray; }
472 td.priority_Low, th.priority_Low { color: blue; }
473 td.priority_Medium, th.priority_Medium { color: #f60; }
474
475=== modified file 'templates/milestone_list.html'
476--- templates/milestone_list.html 2011-02-06 20:10:39 +0000
477+++ templates/milestone_list.html 2011-03-10 18:14:55 +0000
478@@ -7,16 +7,38 @@
479
480 <h1>Milestones</h1>
481
482-<p>These are all of the milestones during the current cycle. Click on any one of them to see
483-the tasks that are assigned to be completed by that milestone.</p>
484+<p>Here is a timeline of all the milestones during the current cycle. Click on any one of them to see
485+the tasks that are assigned to be completed by that milestone.
486+The next milestone is highlighted by the marker on the left of the timeline.</p>
487
488-% if milestones:
489-<ul>
490-% for milestone in milestones:
491-<li><a href="${base.url(milestone.link)}">${milestone.name} (${milestone.project})</a></li>
492-% endfor
493-</ul>
494+% if milestone_groups:
495+<table id="milestones">
496+<% last_group = None %>
497+% for group in milestone_groups:
498+<tr
499+## Make the height of the row proportional to the duration between this milestone
500+## and the previous. The height will not shrink below the contents though, so
501+## there is no worry about very short durations.
502+% if last_group is not None:
503+height="${(group.due_date.toordinal() - last_group.due_date.toordinal())*2}px"
504+% endif
505+
506+>
507+ <td class="current_date_marker">
508+## Point at the first milestone that is after the current date
509+% if group.due_date >= current_date and (last_group is None or last_group.due_date < current_date):
510+&gt;
511+% endif
512+ </td>
513+ <td class="date"><span class="connector">-</span> <a href="${base.url(group.due_date_str+".html")}">${group.due_date}</a></td>
514+ <td class="milestones">
515+% for milestone in group.milestones:
516+<a href="${base.url(milestone.link)}">${milestone.name} (${milestone.project})</a>
517+% endfor
518+ </td>
519+<% last_group = group %>
520+% endfor
521+</table>
522 % else:
523-<p>No teams!</p>
524+<p>No milestones!</p>
525 % endif
526-

Subscribers

People subscribed via source and target branches

to all changes: