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: | 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 |
Related bugs: |
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.
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
Michael Hudson-Doyle (mwhudson) wrote : Posted in a previous version of this proposal | # |
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.
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_
- 285. By James Westby
-
Comments from review. Thanks Michael.
Preview Diff
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 | > |
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> |
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 :-)