Merge lp:~james-w/launchpad-work-items-tracker/date-based into lp:~linaro-automation/launchpad-work-items-tracker/linaro
- date-based
- Merge into linaro
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 |
Related bugs: |
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.
Commit message
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
- 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
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 | +> |
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 | - |
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 :-)