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

Proposed by James Westby
Status: Merged
Merged at revision: 281
Proposed branch: lp:~james-w/launchpad-work-items-tracker/date-based
Merge into: lp:~linaro-automation/launchpad-work-items-tracker/linaro
Diff against target: 601 lines (+215/-100)
5 files modified
burndown-chart (+24/-11)
generate-all (+12/-2)
html-report (+37/-20)
report_tools.py (+141/-66)
templates/milestone_list.html (+1/-1)
To merge this branch: bzr merge lp:~james-w/launchpad-work-items-tracker/date-based
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Pending
Review via email: mp+53895@code.launchpad.net

This proposal supersedes a proposal from 2011-03-10.

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 : Posted in a previous version of this proposal

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
Revision history for this message
James Westby (james-w) wrote :

Resubmitting as I missed the pre-requisite branch last time, it doesn't actually
change any of the code though.

Thanks,

James

283. By James Westby

Rework to use polymorpism for milestone due dates.

284. By James Westby

Changes from review comments. Thanks Michael.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

I think this is starting to look better, but I think you should change the name of the due_data parameter in a few places? Also I think you'll find that accessing the name of a SingleMilestone won't work, because a superclass's properties override a subclasses instance data.

Finally "This should cause a query that the fragment is used in to only consider" in milestone_select_sql.__doc__ doesn't really make sense.

285. By James Westby

Comments from review. Thanks Michael.

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-22 21:32:31 +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@@ -267,13 +271,21 @@
23 elif opts.team:
24 opts.team = report_tools.team_string(opts.team)
25
26+db = report_tools.get_db(opts.database)
27+
28+milestone_collection = None
29+if opts.milestone:
30+ milestone_collection = report_tools.get_milestone(db, opts.milestone)
31+elif opts.date:
32+ milestone_collection = report_tools.MilestoneGroup(opts.date)
33+
34+
35 # get date -> state -> count mapping
36-db = report_tools.get_db(opts.database)
37-data = report_tools.workitems_over_time(db, team=opts.team, milestone=opts.milestone, group=opts.group)
38+data = report_tools.workitems_over_time(db, team=opts.team, group=opts.group, milestone_collection=milestone_collection)
39
40 if len(data) == 0:
41- print 'WARNING: no work items, not generating chart (team: %s, milestone: %s, group: %s)' % (
42- opts.team or 'all', opts.milestone or 'none', opts.group or 'none')
43+ print 'WARNING: no work items, not generating chart (team: %s, group: %s, due date: %s)' % (
44+ opts.team or 'all', opts.group or 'none', milestone_collection and milestone_collection.display_name or 'none')
45 sys.exit(0)
46
47 # calculate start/end date if no dates are given
48@@ -283,14 +295,16 @@
49 start_date=opts.start_date
50
51 if opts.end_date is None:
52- cur = db.cursor()
53- end_date=report_tools.milestone_due_date(db, milestone=opts.milestone)
54+ if milestone_collection is not None:
55+ end_date = milestone_collection.due_date
56+ else:
57+ end_date=report_tools.milestone_due_date(db)
58 else:
59 end_date=opts.end_date
60
61 if not start_date or not end_date or date_to_ordinal(start_date) > date_to_ordinal(end_date):
62- print 'WARNING: empty date range, not generating chart (team: %s, milestone: %s)' % (
63- opts.team or 'all', opts.milestone or 'none')
64+ print 'WARNING: empty date range, not generating chart (team: %s, group: %s, due date: %s)' % (
65+ opts.team or 'all', opts.milestone or 'none', opts.group or 'none', milestone_collection and milestone_collection.display_name or 'none')
66 sys.exit(0)
67
68 # title
69@@ -305,8 +319,7 @@
70 # It never makese sense in all teams mode
71 noforeign = True
72
73-if opts.milestone:
74- title += ' (%s)' % opts.milestone
75+if milestone_collection is not None:
76+ title += ' (%s)' % milestone_collection.name
77
78 do_chart(data, start_date, end_date, opts.trendstart, title, opts.output, opts.only_weekdays, opts.inverted, noforeign)
79-
80
81=== modified file 'generate-all'
82--- generate-all 2011-02-26 00:57:42 +0000
83+++ generate-all 2011-03-22 21:32:31 +0000
84@@ -34,11 +34,15 @@
85 optparser.error('output directory does not exist')
86
87 if opts.config:
88- trend_starts = report_tools.load_config(opts.config).get('trend_start', {})
89- burnup_chart_teams = report_tools.load_config(opts.config).get('burnup_chart_teams', [])
90+ cfg = report_tools.load_config(opts.config)
91+ trend_starts = cfg.get('trend_start', {})
92+ burnup_chart_teams = cfg.get('burnup_chart_teams', [])
93+ primary_team = cfg.get("primary_team", None)
94 else:
95+ cfg = None
96 trend_starts = {}
97 burnup_chart_teams = []
98+ primary_team = None
99
100 lock_path = opts.database + ".generate_lock"
101 lock_f = open(lock_path, "wb")
102@@ -104,6 +108,12 @@
103 else:
104 report_tools.run_reports(my_path, opts.database, basename, opts.config, milestone=m, team=None, root=opts.root)
105
106+# date-based milestone view
107+for mg in report_tools.milestone_groups(db, team=primary_team):
108+ due_date_str = mg.due_date_str
109+ basename = os.path.join(opts.output_dir, due_date_str)
110+ report_tools.run_reports(my_path, opts.database, basename, opts.config, team=primary_team, root=opts.root, date=due_date_str)
111+
112 # groups
113 for g in groups:
114 basename = os.path.join(groupssubdir, g)
115
116=== modified file 'html-report'
117--- html-report 2011-03-10 16:18:45 +0000
118+++ html-report 2011-03-22 21:32:31 +0000
119@@ -105,8 +105,8 @@
120 return self._assignee or self.blueprint.assignee
121
122
123-def spec_group_completion(db, team, milestone):
124- data = report_tools.spec_group_completion(db, team, milestone)
125+def spec_group_completion(db, team, milestone_collection=None):
126+ data = report_tools.spec_group_completion(db, team, milestone_collection=milestone_collection)
127 if not data:
128 return dict(areas=[], group_completion_series=[], groups=[])
129 groups = []
130@@ -133,8 +133,8 @@
131 groups=sorted(groups, key=lambda x: report_tools.priority_value(x.priority), reverse=True))
132
133
134-def spec_completion(db, team, milestone):
135- data = report_tools.blueprint_completion(db, team, milestone)
136+def spec_completion(db, team, milestone_collection=None):
137+ data = report_tools.blueprint_completion(db, team, milestone_collection=milestone_collection)
138
139 blueprints = []
140 all_workitems = WorkitemTarget()
141@@ -189,8 +189,8 @@
142 unknown_workitems=unknown_workitems,
143 )
144
145-def get_assignee_completion(db, team=None, milestone=None, group=None):
146- data = report_tools.assignee_completion(db, team=team, milestone=milestone, group=group)
147+def get_assignee_completion(db, team=None, group=None, milestone_collection=None):
148+ data = report_tools.assignee_completion(db, team=team, group=group, milestone_collection=milestone_collection)
149
150 def _sort(a, b):
151 # Takes [blueprint, workitem, priority, url] in a and b.
152@@ -205,10 +205,10 @@
153 for name, info in data.items():
154 name = name or team or 'nobody'
155 name_html = escape_url(name or team or 'nobody')
156- if not milestone or isinstance(team, report_tools.user_string):
157+ if not milestone_collection or not milestone_collection.single_milestone or isinstance(team, report_tools.user_string):
158 url = '%s/~%s/+specs?role=assignee' % (report_tools.blueprints_base_url, name_html)
159 else:
160- target = name_html + '-' + milestone
161+ target = name_html + '-' + milestone_collection.name
162 url = 'u/%s.html' % target
163 assignees.append(
164 Assignee(name, url, complexity=info['complexity'],
165@@ -220,8 +220,9 @@
166 return assignees
167
168
169-def by_assignee(db, team=None, milestone=None, group=None):
170- assignees = get_assignee_completion(db, team=team, milestone=milestone, group=group)
171+def by_assignee(db, team=None, group=None, milestone_collection=None):
172+ assignees = get_assignee_completion(
173+ db, team=team, group=group, milestone_collection=milestone_collection)
174 assignees.sort(key=lambda a: a.name, reverse=False)
175
176 # avoid showing empty columns
177@@ -328,6 +329,14 @@
178 data.update(report_tools.days_left(db, milestone=opts.milestone))
179 return data
180
181+ def get_milestone_collection(self, db, opts):
182+ milestone_collection = None
183+ if opts.milestone:
184+ milestone_collection = report_tools.get_milestone(db, opts.milestone)
185+ elif opts.date:
186+ milestone_collection = report_tools.MilestoneGroup(opts.date)
187+ return milestone_collection
188+
189 def burndown(self, db, opts):
190 '''Print work item status as HTML.'''
191
192@@ -339,15 +348,17 @@
193 else:
194 title = opts.title
195
196- if opts.milestone:
197- title += ' (%s)' % opts.milestone
198+ milestone_collection = self.get_milestone_collection(db, opts)
199+ if milestone_collection is not None:
200+ title += ' (%s)' % milestone_collection.name
201
202 data = self.template_data(db, opts)
203 data.update(dict(team_name=title, chart_url=opts.chart_url))
204- data.update(spec_group_completion(db, opts.team, opts.milestone))
205- data.update(spec_completion(db, opts.team, opts.milestone))
206- data.update(by_assignee(db, opts.team, opts.milestone))
207- if opts.milestone:
208+ data.update(
209+ spec_group_completion(db, opts.team, milestone_collection=milestone_collection))
210+ data.update(spec_completion(db, opts.team, milestone_collection=milestone_collection))
211+ data.update(by_assignee(db, team=opts.team, milestone_collection=milestone_collection))
212+ if milestone_collection is not None:
213 data.update(dict(page_type="milestones"))
214 elif isinstance(opts.team, report_tools.user_string):
215 data.update(dict(page_type="people"))
216@@ -364,13 +375,15 @@
217 else:
218 title = opts.title
219
220- if opts.milestone:
221- title += ' (%s)' % opts.milestone
222+ milestone_collection = self.get_milestone_collection(db, opts)
223+
224+ if milestone_collection is not None:
225+ title += ' (%s)' % milestone_collection.name
226
227 data = self.template_data(db, opts)
228 data.update(dict(team_name=title, chart_url=opts.chart_url))
229- data.update(spec_group_completion(db, None, opts.milestone))
230- data.update(spec_completion(db, opts.team, opts.milestone))
231+ data.update(spec_group_completion(db, None, milestone_collection=milestone_collection))
232+ data.update(spec_completion(db, opts.team, milestone_collection=milestone_collection))
233 data.update(blueprint_count(db, opts.team, opts.milestone))
234 data.update(dict(page_type="overview"))
235 print report_tools.fill_template("overview.html", data)
236@@ -456,12 +469,16 @@
237 help="Root URL for the charts")
238 optparser.add_option('--status', dest="status",
239 help="Workitem status to consider for workitem_list report-type")
240+ optparser.add_option('--date', dest="date",
241+ help="Include all milestones targetted to this date.")
242
243 (opts, args) = optparser.parse_args()
244 if not opts.database:
245 optparser.error('No database given')
246 if opts.user and opts.team:
247 optparser.error('team and user options are mutually exclusive')
248+ if opts.milestone and opts.date:
249+ optparser.error('milestone and date options are mutually exclusive')
250
251 # The typing allows polymorphic behavior
252 if opts.user:
253
254=== modified file 'report_tools.py'
255--- report_tools.py 2011-03-10 16:18:45 +0000
256+++ report_tools.py 2011-03-22 21:32:31 +0000
257@@ -24,13 +24,15 @@
258 def escape_sql(text):
259 return text.replace("'", "''")
260
261-def report_args(args, team=None, user=None, milestone=None):
262+def report_args(args, team=None, user=None, milestone=None, date=None):
263 if milestone:
264 args.extend(['-m',milestone])
265 if team:
266 args.extend(['-t',team])
267 if user:
268 args.extend(['-u',user])
269+ if date:
270+ args.extend(['--date', date])
271
272 def status_overview(my_path, database, basename, config, root=None):
273 cfg = load_config(config)
274@@ -166,13 +168,17 @@
275
276
277 def run_reports(my_path, database, basename, config, milestone=None, team=None,
278- user=None, trend_starts=None, trend_override=None, burnup=False, root=None):
279+ user=None, trend_starts=None, trend_override=None, burnup=False, root=None, date=None):
280
281 ident = (team or user)
282 if milestone and ident:
283 chartname = ident + '-' + milestone
284 elif milestone:
285 chartname = 'all-' + milestone
286+ elif date and ident:
287+ chartname = "%s-%s" % (date, milestone)
288+ elif date:
289+ chartname = 'all-' + date
290 elif ident:
291 chartname = ident
292 else:
293@@ -186,16 +192,17 @@
294 args += ['--title', cfg['title']]
295 if root:
296 args += ['--root', root]
297- report_args(args, team, user, milestone)
298+ report_args(args, team=team, user=user, milestone=milestone, date=date)
299 hreport = Popen(args, stdout=hf)
300
301- jf = open(basename + '.json', 'w')
302- args = [os.path.join(my_path, 'json-report'), '-d', database, '-c', config]
303- report_args(args, team, user, milestone)
304- jreport = Popen(args, stdout=jf)
305+ if not date: # date support not implemented in json-report yet
306+ jf = open(basename + '.json', 'w')
307+ args = [os.path.join(my_path, 'json-report'), '-d', database, '-c', config]
308+ report_args(args, team=team, user=user, milestone=milestone)
309+ jreport = Popen(args, stdout=jf)
310
311 args = [os.path.join(my_path, 'burndown-chart'), '-d', database, '-o', basename + '.svg']
312- report_args(args, team, user, milestone)
313+ report_args(args, team=team, user=user, milestone=milestone, date=date)
314
315 if trend_override:
316 args.extend(['--trend-start', trend_override])
317@@ -203,7 +210,7 @@
318 if milestone and ident:
319 if (ident, milestone) in trend_starts:
320 args.extend(['--trend-start', str(trend_starts[(ident, milestone)])])
321- elif ident:
322+ elif ident and not date:
323 if ident in trend_starts:
324 args.extend(['--trend-start', str(trend_starts[ident])])
325 if burnup:
326@@ -259,32 +266,7 @@
327 return escape(html, True)
328
329
330-def get_milestone_start_sql(db, milestone):
331- if milestone:
332- # get milestone date range
333- cur = db.cursor()
334- cur.execute('SELECT due_date, project FROM milestones WHERE name = ?', (milestone,))
335- row = cur.fetchone()
336- date_end = row[0]
337- project = row[1]
338- cur.execute('SELECT MAX(due_date), MAX(due_date) < date(CURRENT_TIMESTAMP) FROM milestones WHERE due_date < ? AND project = ?', (date_end, project))
339- (date_start, in_milestone) = cur.fetchone()
340-
341- # if we are already within the milestone, start at the previous
342- # milestone's end date; if we are before, start at the beginning of the
343- # entire cycle, to allow planning
344- if in_milestone:
345- ms_sql = "AND w.milestone='%s' AND date >= '%s' AND date <= '%s'" % (
346- escape_sql(milestone), date_start, date_end)
347- else:
348- ms_sql = "AND w.milestone='%s' AND date <= '%s'" % (
349- escape_sql(milestone), date_end)
350- else:
351- ms_sql = ''
352- return ms_sql
353-
354-
355-def workitems_over_time(db, team=None, milestone=None, group=None):
356+def workitems_over_time(db, team=None, group=None, milestone_collection=None):
357 '''Calculate work item development over time.
358
359 If team is given, this filters out blueprints for that particular team. If
360@@ -296,10 +278,12 @@
361 {todo,inprogress,done,postponed}{,_teamonly}.
362 '''
363 data = {}
364- ms_sql = get_milestone_start_sql(db, milestone)
365+ cur = db.cursor()
366+ ms_sql = ''
367+ if milestone_collection is not None:
368+ ms_sql = milestone_collection.milestone_start_sql(cur)
369 # include both direct team assignments, as well as assignmets to
370 # members of that team
371- cur = db.cursor()
372 if team:
373 # WIs which are assigned to team members
374 if isinstance(team, user_string):
375@@ -361,14 +345,16 @@
376 data.setdefault(date, {})[s] = num
377 return data
378
379-def spec_group_completion(db, team=None, milestone=None):
380+def milestone_select_sql(milestone_collection=None):
381+ if milestone_collection is not None:
382+ ms_sql = milestone_collection.milestone_select_sql()
383+ else:
384+ ms_sql = "AND w.milestone IS NULL"
385+ return ms_sql
386+
387+def spec_group_completion(db, team=None, milestone_collection=None):
388 data = {}
389- if milestone == '':
390- ms_sql = "AND w.milestone IS NULL"
391- elif milestone:
392- ms_sql = "AND w.milestone='%s'" % escape_sql(milestone)
393- else:
394- ms_sql = ''
395+ ms_sql = milestone_select_sql(milestone_collection=milestone_collection)
396
397 # last date
398 cur = db.cursor()
399@@ -451,19 +437,14 @@
400 return info
401
402
403-def blueprint_completion(db, team=None, milestone=None):
404+def blueprint_completion(db, team=None, milestone_collection=None):
405 '''Determine current blueprint completion.
406
407 Return blueprint -> info mapping, with info being a map with these
408 keys: todo, inprogress, done, postponed, status, priority, implementation, url.
409 '''
410 data = {}
411- if milestone == '':
412- ms_sql = "AND w.milestone IS NULL"
413- elif milestone:
414- ms_sql = "AND w.milestone='%s'" % escape_sql(milestone)
415- else:
416- ms_sql = ''
417+ ms_sql = milestone_select_sql(milestone_collection=milestone_collection)
418
419 # last date
420 cur = db.cursor()
421@@ -651,7 +632,8 @@
422
423
424 def milestone_start_date(db, milestone=None):
425- ms_sql = get_milestone_start_sql(db, milestone)
426+ cur = db.cursor()
427+ ms_sql = SingleMilestone(milestone, None, None, None).milestone_start_sql(cur)
428 cur = db.cursor()
429 cur.execute('SELECT MIN(date) FROM work_items w '
430 'WHERE 1=1 %s GROUP BY date' % ms_sql)
431@@ -679,7 +661,7 @@
432 )
433
434
435-def assignee_completion(db, team=None, milestone=None, group=None):
436+def assignee_completion(db, team=None, group=None, milestone_collection=None):
437 '''Determine current by-assignee completion.
438
439 Return assignee -> info mapping with info being a map with these
440@@ -687,12 +669,7 @@
441 workitem, priority, spec_url].
442 '''
443 data = {}
444- if milestone == '':
445- ms_sql = "AND w.milestone IS NULL"
446- elif milestone:
447- ms_sql = "AND w.milestone='%s'" % milestone
448- else:
449- ms_sql = ''
450+ ms_sql = milestone_select_sql(milestone_collection=milestone_collection)
451
452 # last date
453 cur = db.cursor()
454@@ -905,21 +882,119 @@
455 return milestones
456
457
458-class Milestone(object):
459+class MilestoneCollection(object):
460+
461+ single_milestone = False
462+
463+ def __init__(self, due_date):
464+ self.due_date = due_date
465+
466+ @property
467+ def due_date_str(self):
468+ return self.due_date.strftime("%Y-%m-%d")
469+
470+ @property
471+ def display_name(self):
472+ """A name suitable for display for this collection."""
473+ raise NotImplementedError(self.display_name)
474+
475+ def milestone_start_sql(self, cur):
476+ """Returns an SQL fragment that restricts workitem data to this collection.
477+
478+ It can restrict to workitems targeted to milestones in this collection.
479+
480+ It can also restrict to workitem data records that are after this milestone
481+ started, and before it completed.
482+
483+ :param cur: a db cursor that can be used to query needed information.
484+ """
485+ raise NotImplementedError(self.milestone_start_sql)
486+
487+ def milestone_select_sql(self):
488+ """Returns an SQL fragments that restricts workitems to this collection.
489+
490+ If the returned fragment is used in an SQL query then that query should only
491+ consider workitems targeted to the milestones in this collection.
492+ """
493+ raise NotImplementedError(self.milestone_select_sql)
494+
495+
496+class SingleMilestone(MilestoneCollection):
497+
498+ single_milestone = True
499
500 def __init__(self, name, link, project, due_date):
501+ super(SingleMilestone, self).__init__(due_date)
502 self.name = name
503 self.link = link
504 self.project = project
505- self.due_date = due_date
506-
507-
508-class MilestoneGroup(object):
509+
510+ @property
511+ def display_name(self):
512+ return "milestone %s" % self.name
513+
514+ def milestone_start_sql(self, cur):
515+ cur.execute('SELECT due_date, project FROM milestones WHERE name = ?', (self.name,))
516+ row = cur.fetchone()
517+ date_end = row[0]
518+ project = row[1]
519+ cur.execute('SELECT MAX(due_date), MAX(due_date) < date(CURRENT_TIMESTAMP) FROM milestones WHERE due_date < ? AND project = ?', (date_end, project))
520+ (date_start, in_milestone) = cur.fetchone()
521+
522+ # if we are already within the milestone, start at the previous
523+ # milestone's end date; if we are before, start at the beginning of the
524+ # entire cycle, to allow planning
525+ if in_milestone:
526+ ms_sql = "AND w.milestone='%s' AND date >= '%s' AND date <= '%s'" % (
527+ escape_sql(self.name), date_start, date_end)
528+ else:
529+ ms_sql = "AND w.milestone='%s' AND date <= '%s'" % (
530+ escape_sql(self.name), date_end)
531+ return ms_sql
532+
533+ def milestone_select_sql(self):
534+ return "AND w.milestone='%s'" % escape_sql(self.name)
535+
536+
537+class MilestoneGroup(MilestoneCollection):
538
539 def __init__(self, due_date):
540- self.due_date = due_date
541+ super(MilestoneGroup, self).__init__(due_date)
542 self.milestones = []
543
544+ @property
545+ def name(self):
546+ return self.due_date_str
547+
548+ @property
549+ def display_name(self):
550+ return "any milestone on %s" % self.due_date_str
551+
552+ def milestone_start_sql(self, cur):
553+ cur.execute('SELECT MAX(due_date), MAX(due_date) < date(CURRENT_TIMESTAMP) FROM milestones WHERE due_date < ?', (escape_sql(date), ))
554+ (date_start, in_milestone) = cur.fetchone()
555+ if in_milestone:
556+ ms_sql = "AND date >= '%s' AND date <= '%s'" % (date_start, escape_sql(date))
557+ else:
558+ ms_sql = "AND date <= '%s'" % escape_sql(date)
559+ ms_sql += "AND w.milestone IN (SELECT m.name from milestones m WHERE m.due_date = '%s')" % escape_sql(date)
560+ return ms_sql
561+
562+ def milestone_select_sql(self):
563+ return (
564+ "AND w.milestone IN "
565+ "(SELECT m.name from milestones m WHERE m.due_date = '%s')"
566+ % self.due_date_str)
567+
568+
569+def get_milestone(db, milestone_name):
570+ cur = db.cursor()
571+ cur.execute(
572+ "SELECT project, due_date from milestones where name = ?",
573+ milestone_name)
574+ project, due_date = cur.fetchone()
575+ return SingleMilestone(milestone_name, None, project, date_to_python(due_date))
576+
577
578 def milestone_groups(db, team=None):
579 if team:
580@@ -938,7 +1013,7 @@
581 else:
582 group = groups[-1]
583 group.milestones.append(
584- Milestone(name, template % name, project, due_date))
585+ SingleMilestone(name, template % name, project, due_date))
586 return groups
587
588
589
590=== modified file 'templates/milestone_list.html'
591--- templates/milestone_list.html 2011-03-10 16:18:45 +0000
592+++ templates/milestone_list.html 2011-03-22 21:32:31 +0000
593@@ -30,7 +30,7 @@
594 &gt;
595 % endif
596 </td>
597- <td class="date"><span class="connector">-</span> ${group.due_date}</td>
598+ <td class="date"><span class="connector">-</span> <a href="${base.url(group.due_date_str+".html")}">${group.due_date}</a></td>
599 <td class="milestones">
600 % for milestone in group.milestones:
601 <a href="${base.url(milestone.link)}">${milestone.name} (${milestone.project})</a>

Subscribers

People subscribed via source and target branches

to all changes: