fixed burndownchart is broken with multi component and fixed term.
@@ -1,399 +1,396 @@ | ||
1 | -# Burndown plugin | |
2 | -# Copyright (C) 2006 Sam Bloomquist <spooninator@hotmail.com> | |
3 | -# Copyright (C) 2006-2008 Daan van Etten <daan@stuq.nl> | |
4 | -# All rights reserved. | |
5 | - | |
6 | -# This software may at some point consist of voluntary contributions made by | |
7 | -# many individuals. For the exact contribution history, see the revision | |
8 | -# history and logs, available at http://projects.edgewall.com/trac/. | |
9 | -# | |
10 | -# Author: Sam Bloomquist <spooninator@hotmail.com> | |
11 | -# Author: Daan van Etten <daan@stuq.nl> | |
12 | - | |
13 | -import time | |
14 | -import datetime | |
15 | -import sys | |
16 | - | |
17 | -import dbhelper | |
18 | - | |
19 | -from trac import __version__ as tracversion_runtime | |
20 | - | |
21 | -from datetime import datetime, date | |
22 | - | |
23 | -from trac.core import * | |
24 | -from trac.config import BoolOption | |
25 | -from trac.env import IEnvironmentSetupParticipant | |
26 | -from trac.perm import IPermissionRequestor | |
27 | -from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet, add_script | |
28 | -from trac.web.main import IRequestHandler | |
29 | -from trac.util import escape, Markup, format_date | |
30 | -from trac.ticket import ITicketChangeListener | |
31 | -from trac.ticket import model | |
32 | -from trac.util.datefmt import to_datetime | |
33 | - | |
34 | -class BurndownComponent(Component): | |
35 | - implements(IEnvironmentSetupParticipant, INavigationContributor, | |
36 | - IRequestHandler, ITemplateProvider, IPermissionRequestor, ITicketChangeListener) | |
37 | - | |
38 | - tracversion=tracversion_runtime[:4] | |
39 | - | |
40 | - #--------------------------------------------------------------------------- | |
41 | - # IEnvironmentSetupParticipant methods | |
42 | - #--------------------------------------------------------------------------- | |
43 | - def environment_created(self): | |
44 | - """Called when a new Trac environment is created.""" | |
45 | - if self.environment_needs_upgrade(None): | |
46 | - self.upgrade_environment(None) | |
47 | - | |
48 | - def environment_needs_upgrade(self, db): | |
49 | - if not db: | |
50 | - db = self.env.get_db_cnx() | |
51 | - | |
52 | - needsUpgrade = True | |
53 | - | |
54 | - # See if the burndown table exists, if not, we need an upgrade | |
55 | - if dbhelper.table_exists(db, "burndown"): | |
56 | - needsUpgrade = False | |
57 | - if dbhelper.table_field_exists(db, "burndown", "week"): | |
58 | - needsUpgrade = True | |
59 | - | |
60 | - if dbhelper.table_field_exists(db, "milestone", "started"): | |
61 | - needsUpgrade = False | |
62 | - | |
63 | - return needsUpgrade | |
64 | - | |
65 | - def upgrade_environment(self, db): | |
66 | - db = self.env.get_db_cnx() | |
67 | - | |
68 | - needsCreate = True | |
69 | - needsUpgrade_milestone = True | |
70 | - needsUpgrade_burndown = False | |
71 | - | |
72 | - if dbhelper.table_exists(db, "burndown"): | |
73 | - needsCreate = False | |
74 | - if dbhelper.table_field_exists(db, "burndown", "week"): | |
75 | - needsUpgrade_burndown = True | |
76 | - | |
77 | - if dbhelper.table_field_exists(db, "milestone", "started"): | |
78 | - needsUpgrade_milestone = False | |
79 | - | |
80 | - if needsCreate: | |
81 | - print >> sys.stderr, 'Attempting to create the burndown table' | |
82 | - dbhelper.create_burndown_table(db, self.env) | |
83 | - | |
84 | - if needsUpgrade_milestone: | |
85 | - print >> sys.stderr, 'Attempting to modify the milestone table' | |
86 | - dbhelper.upgrade_milestone_table(db, self.env) | |
87 | - | |
88 | - if needsUpgrade_burndown: | |
89 | - print >> sys.stderr, 'Attempting to modify the burndown table' | |
90 | - dbhelper.upgrade_burndown_table(db, self.env) | |
91 | - | |
92 | - db.commit() | |
93 | - | |
94 | - #--------------------------------------------------------------------------- | |
95 | - # ITicketChangeListener methods | |
96 | - #--------------------------------------------------------------------------- | |
97 | - | |
98 | - def ticket_created(self, ticket): | |
99 | - self.log.debug('burndown plugin - ticket_created') | |
100 | - self.update_burndown_data() | |
101 | - | |
102 | - def ticket_changed(self, ticket, comment, author, old_values): | |
103 | - self.log.debug('burndown plugin - ticket_changed') | |
104 | - self.update_burndown_data() | |
105 | - | |
106 | - def ticket_deleted(self, ticket): | |
107 | - self.log.debug('burndown plugin - ticket_modified') | |
108 | - self.update_burndown_data() | |
109 | - | |
110 | - #--------------------------------------------------------------------------- | |
111 | - # INavigationContributor methods | |
112 | - #--------------------------------------------------------------------------- | |
113 | - def get_active_navigation_item(self, req): | |
114 | - return "burndown" | |
115 | - | |
116 | - def get_navigation_items(self, req): | |
117 | - if req.perm.has_permission("BURNDOWN_VIEW"): | |
118 | - if self.tracversion=="0.10": | |
119 | - yield 'mainnav', 'burndown', Markup('<a href="%s">Burndown</a>') % req.href.burndown() | |
120 | - else: | |
121 | - yield 'mainnav', 'burndown', Markup('<a href="%s">Burndown</a>' % req.href.burndown()) | |
122 | - | |
123 | - #--------------------------------------------------------------------------- | |
124 | - # IPermissionRequestor methods | |
125 | - #--------------------------------------------------------------------------- | |
126 | - def get_permission_actions(self): | |
127 | - return ["BURNDOWN_VIEW", "BURNDOWN_ADMIN"] | |
128 | - | |
129 | - #--------------------------------------------------------------------------- | |
130 | - # IRequestHandler methods | |
131 | - #--------------------------------------------------------------------------- | |
132 | - def match_request(self, req): | |
133 | - return req.path_info == '/burndown' | |
134 | - | |
135 | - def process_request(self, req): | |
136 | - req.perm.assert_permission('BURNDOWN_VIEW') | |
137 | - | |
138 | - db = self.env.get_db_cnx() | |
139 | - | |
140 | - milestones = dbhelper.get_milestones(db) | |
141 | - components = dbhelper.get_components(db) | |
142 | - | |
143 | - selected_milestone = None | |
144 | - if (len(milestones)>0): | |
145 | - selected_milestone = dbhelper.get_current_milestone(db, req.args.get('selected_milestone', "")) | |
146 | - | |
147 | - selected_component = req.args.get('selected_component', 'All Components') | |
148 | - | |
149 | - empty_db_for_testing = req.args.get('empty_db_for_testing', 'false') | |
150 | - if empty_db_for_testing == "true": | |
151 | - req.perm.assert_permission('TRAC_ADMIN') | |
152 | - dbhelper.empty_db_for_testing(db) | |
153 | - | |
154 | - # expose display data to the templates | |
155 | - data = {} | |
156 | - data['milestones'] = req.hdf['milestones'] = milestones | |
157 | - data['components'] = req.hdf['components'] = components | |
158 | - data['selected_milestone'] = req.hdf['selected_milestone'] = selected_milestone | |
159 | - data['selected_component'] = req.hdf['selected_component'] = selected_component | |
160 | - data['draw_graph'] = req.hdf['draw_graph'] = False | |
161 | - data['start'] = req.hdf['start'] = False | |
162 | - | |
163 | - if req.perm.has_permission("BURNDOWN_ADMIN"): | |
164 | - data['start'] = req.hdf['start'] = True # show the start and complete milestone buttons to admins | |
165 | - | |
166 | - if req.args.has_key('start'): | |
167 | - self.start_milestone(db, selected_milestone['name']) | |
168 | - | |
169 | - data['draw_graph'] = req.hdf['draw_graph'] = True | |
170 | - self.update_burndown_data() | |
171 | - | |
172 | - data['burndown_data'] = req.hdf['burndown_data'] = [] | |
173 | - burndown_data, estimated_data = self.get_burndown_data(db, selected_milestone, components, selected_component) | |
174 | - data['burndown_data'] = req.hdf['burndown_data'] = burndown_data | |
175 | - data['estimated_data'] = req.hdf['estimated_data'] = estimated_data | |
176 | - | |
177 | - add_stylesheet(req, 'hw/css/burndown.css') | |
178 | - | |
179 | - self.update_burndown_data() | |
180 | - | |
181 | - if self.tracversion=="0.10": | |
182 | - add_script(req, 'hw/js/line.js') | |
183 | - add_script(req, 'hw/js/wz_jsgraphics.js') | |
184 | - return 'burndown.cs', None | |
185 | - else: | |
186 | - data['library'] = '' | |
187 | - if data['library'] == 'flot': | |
188 | - add_script(req, 'hw/js/jquery.flot.js') | |
189 | - else: | |
190 | - add_script(req, 'hw/js/line.js') | |
191 | - add_script(req, 'hw/js/wz_jsgraphics.js') | |
192 | - | |
193 | - return 'burndown.html', data, None | |
194 | - | |
195 | - | |
196 | - def get_burndown_data(self, db, selected_milestone, components, selected_component): | |
197 | - cursor = db.cursor() | |
198 | - cursor.execute("SELECT due FROM milestone where name='%s'" % selected_milestone['name']); | |
199 | - milestone_due = cursor.fetchone()[0]; | |
200 | - self.log.info("milestone_due:") | |
201 | - self.log.info(milestone_due) | |
202 | - t = to_datetime(milestone_due) | |
203 | - due_milestone = datetime(t.year,t.month,t.day) | |
204 | - | |
205 | - delta = None | |
206 | - component_data = {} # this will be a dictionary of lists of tuples -- e.g. component_data = {'componentName':[(id, hours_remaining), (id, hours_remaining), (id, hours_remaining)]} | |
207 | - for comp in components: | |
208 | - if selected_component == 'All Components' or comp['name'] == selected_component: | |
209 | - self.log.debug("comp = %s", comp['name']) | |
210 | - self.log.debug("selected_component = %s", sselected_component) | |
211 | - sqlBurndown = "SELECT id, hours_remaining ,date"\ | |
212 | - "FROM burndown "\ | |
213 | - "WHERE milestone_name = %s AND component_name = %s "\ | |
214 | - "ORDER BY id" | |
215 | - cursor.execute(sqlBurndown, (selected_milestone['name'], comp['name'])) | |
216 | - component_data[comp['name']] = cursor.fetchall() | |
217 | - self.log.debug(component_data[comp['name']]) | |
218 | - | |
219 | - if len(components)==0: | |
220 | - comp = {'name':'-'} | |
221 | - if selected_component == 'All Components' or comp['name'] == selected_component: | |
222 | - self.log.debug("comp = %s", comp['name']) | |
223 | - self.log.debug("selected_component = %s", selected_component) | |
224 | - sqlBurndown = "SELECT id, hours_remaining, date "\ | |
225 | - "FROM burndown "\ | |
226 | - "WHERE milestone_name = %s AND component_name = %s "\ | |
227 | - "ORDER BY id" | |
228 | - cursor.execute(sqlBurndown, (selected_milestone['name'], comp['name'])) | |
229 | - component_data[comp['name']] = cursor.fetchall() | |
230 | - self.log.debug(component_data[comp['name']]) | |
231 | - | |
232 | - if len(component_data) > 0 and component_data[component_data.keys()[0]]: | |
233 | - burndown_length = len(component_data[component_data.keys()[0]]) | |
234 | - else: | |
235 | - burndown_length = 0 | |
236 | - burndown_data = [] | |
237 | - if selected_component == 'All Components': | |
238 | - for time_unit in range (0, burndown_length): | |
239 | - sumHours = 0 | |
240 | - for comp in components: | |
241 | - plotdata.append(comp['date']) | |
242 | - self.log.debug('component: %s', [comp['name']]); | |
243 | - self.log.debug('time_unit: %s', [time_unit]); | |
244 | - if (component_data[comp['name']] and len(component_data[comp['name']]) > time_unit): | |
245 | - self.log.debug('hours: %s', component_data[comp['name']][time_unit][1]); | |
246 | - sumHours += component_data[comp['name']][time_unit][1] | |
247 | - if len(components)==0: | |
248 | - comp={'name':'-'} | |
249 | - self.log.debug('component: %s', [comp['name']]); | |
250 | - self.log.debug('time_unit: %s', [time_unit]); | |
251 | - if (component_data[comp['name']] and len(component_data[comp['name']]) > time_unit): | |
252 | - self.log.debug('hours: %s', component_data[comp['name']][time_unit][1]); | |
253 | - sumHours += component_data[comp['name']][time_unit][1] | |
254 | - | |
255 | - burndown_data.append((time_unit+1, sumHours)) | |
256 | - | |
257 | - else: | |
258 | - for time_unit in range (0, len(component_data[selected_component])): | |
259 | - plotdata.append(component_data[selected_component]['date']) | |
260 | - burndown_data.append((time_unit+1, component_data[selected_component][time_unit][1])) | |
261 | - start_milestone = None | |
262 | - estimated_data = [] | |
263 | - if len(burndown_data)!=0: | |
264 | - if len(components)==0: | |
265 | - t = time.strptime(component_data['-'][0][2],'%Y/%m/%d') | |
266 | - start_milestone = datetime(t[0] ,t[1] ,t[2]) | |
267 | - else: | |
268 | - t = time.strptime(component_data[component_data.keys()[0]][0][2],'%Y/%m/%d') | |
269 | - start_milestone = datetime(t[0] ,t[1] ,t[2]) | |
270 | - initial_estimated = burndown_data[0][1] | |
271 | - | |
272 | - term = (due_milestone - start_milestone).days | |
273 | - | |
274 | - for time_unit in range (0,term): | |
275 | - estimated_data.append((time_unit+1 ,initial_estimated*(1-float(time_unit+1)/term) )) | |
276 | - | |
277 | - return (burndown_data, estimated_data) | |
278 | - | |
279 | - def start_milestone(self, db, milestone): | |
280 | - startdate = dbhelper.get_startdate_for_milestone(db, milestone) | |
281 | - | |
282 | - if startdate != None: | |
283 | - raise TracError("Milestone %s was already started." % milestone) | |
284 | - | |
285 | - dbhelper.set_startdate_for_milestone(db, milestone, int(time.time())) | |
286 | - | |
287 | - #--------------------------------------------------------------------------- | |
288 | - # ITemplateProvider methods | |
289 | - #--------------------------------------------------------------------------- | |
290 | - def get_templates_dirs(self): | |
291 | - from pkg_resources import resource_filename | |
292 | - return [resource_filename(__name__, 'templates')] | |
293 | - | |
294 | - def get_htdocs_dirs(self): | |
295 | - from pkg_resources import resource_filename | |
296 | - return [('hw', resource_filename(__name__, 'htdocs'))] | |
297 | - | |
298 | - #------------------------------------------------------------------------ | |
299 | - # update_burndown_data | |
300 | - # - add up the hours remaining for the open tickets for each open milestone and put the sums into the burndown table | |
301 | - #------------------------------------------------------------------------ | |
302 | - def update_burndown_data(self): | |
303 | - db = self.env.get_db_cnx() | |
304 | - cursor = db.cursor() | |
305 | - | |
306 | - # today's date | |
307 | - today = format_date(int(time.time())) | |
308 | - | |
309 | - milestones = dbhelper.get_milestones(db) | |
310 | - components = dbhelper.get_components(db) | |
311 | - for mile in milestones: | |
312 | - if mile['started'] and not mile['completed']: # milestone started, but not completed | |
313 | - for comp in components: | |
314 | - sqlSelect = "SELECT est.value AS estimate, ts.value AS spent "\ | |
315 | - "FROM ticket t "\ | |
316 | - " LEFT OUTER JOIN ticket_custom est ON (t.id = est.ticket AND est.name = 'estimatedhours') "\ | |
317 | - " LEFT OUTER JOIN ticket_custom ts ON (t.id = ts.ticket AND ts.name = 'totalhours') "\ | |
318 | - "WHERE t.component = %s AND t.milestone = %s"\ | |
319 | - " AND status IN ('new', 'assigned', 'reopened', 'accepted') " | |
320 | - cursor.execute(sqlSelect, [comp['name'], mile['name']]) | |
321 | - | |
322 | - rows = cursor.fetchall() | |
323 | - hours = 0 | |
324 | - estimate = 0 | |
325 | - spent = 0 | |
326 | - if rows: | |
327 | - for estimate, spent in rows: | |
328 | - if not estimate: | |
329 | - estimate = 0 | |
330 | - if not spent: | |
331 | - spent = 0 | |
332 | - | |
333 | - if (float(estimate) - float(spent)) > 0: | |
334 | - hours += float(estimate) - float(spent) | |
335 | - | |
336 | - cursor.execute("SELECT id FROM burndown WHERE date = %s AND milestone_name = %s"\ | |
337 | - "AND component_name = %s", [today, mile['name'], comp['name']]) | |
338 | - | |
339 | - row = cursor.fetchone() | |
340 | - | |
341 | - try: | |
342 | - if row: | |
343 | - cursor.execute("UPDATE burndown SET hours_remaining = %s WHERE date = %s AND milestone_name = %s"\ | |
344 | - "AND component_name = %s", [hours, today, mile['name'], comp['name']]) | |
345 | - else: | |
346 | - cursor.execute("INSERT INTO burndown(component_name, milestone_name, date, hours_remaining) "\ | |
347 | - " VALUES(%s,%s,%s,%s)", [comp['name'], mile['name'], today, hours]) | |
348 | - except Exception, inst: | |
349 | - self.log.debug(type(inst)) # the exception instance | |
350 | - self.log.debug(inst.args) # arguments stored in .args | |
351 | - self.log.debug(inst) # __str__ allows args to printed directly | |
352 | - cursor.connection.rollback() | |
353 | - else: | |
354 | - db.commit() | |
355 | - if len(components)==0: | |
356 | - comp = {'name':'-'} | |
357 | - sqlSelect = "SELECT est.value AS estimate, ts.value AS spent "\ | |
358 | - "FROM ticket t "\ | |
359 | - " LEFT OUTER JOIN ticket_custom est ON (t.id = est.ticket AND est.name = 'estimatedhours') "\ | |
360 | - " LEFT OUTER JOIN ticket_custom ts ON (t.id = ts.ticket AND ts.name = 'totalhours') "\ | |
361 | - "WHERE t.milestone = %s"\ | |
362 | - " AND status IN ('new', 'assigned', 'reopened', 'accepted') " | |
363 | - cursor.execute(sqlSelect, [mile['name']]) | |
364 | - | |
365 | - rows = cursor.fetchall() | |
366 | - hours = 0 | |
367 | - estimate = 0 | |
368 | - spent = 0 | |
369 | - if rows: | |
370 | - for estimate, spent in rows: | |
371 | - if not estimate: | |
372 | - estimate = 0 | |
373 | - if not spent: | |
374 | - spent = 0 | |
375 | - | |
376 | - if (float(estimate) - float(spent)) > 0: | |
377 | - hours += float(estimate) - float(spent) | |
378 | - | |
379 | - cursor.execute("SELECT id FROM burndown WHERE date = %s AND milestone_name = %s"\ | |
380 | - "AND component_name = %s", [today, mile['name'], comp['name']]) | |
381 | - | |
382 | - row = cursor.fetchone() | |
383 | - | |
384 | - try: | |
385 | - if row: | |
386 | - cursor.execute("UPDATE burndown SET hours_remaining = %s WHERE date = %s AND milestone_name = %s"\ | |
387 | - "AND component_name = %s", [hours, today, mile['name'], comp['name']]) | |
388 | - else: | |
389 | - cursor.execute("INSERT INTO burndown(component_name, milestone_name, date, hours_remaining) "\ | |
390 | - " VALUES(%s,%s,%s,%s)", [comp['name'], mile['name'], today, hours]) | |
391 | - except Exception, inst: | |
392 | - self.log.debug(type(inst)) # the exception instance | |
393 | - self.log.debug(inst.args) # arguments stored in .args | |
394 | - self.log.debug(inst) # __str__ allows args to printed directly | |
395 | - cursor.connection.rollback() | |
396 | - else: | |
397 | - db.commit() | |
398 | - | |
399 | - | |
1 | +# Burndown plugin | |
2 | +# Copyright (C) 2006 Sam Bloomquist <spooninator@hotmail.com> | |
3 | +# Copyright (C) 2006-2008 Daan van Etten <daan@stuq.nl> | |
4 | +# All rights reserved. | |
5 | + | |
6 | +# This software may at some point consist of voluntary contributions made by | |
7 | +# many individuals. For the exact contribution history, see the revision | |
8 | +# history and logs, available at http://projects.edgewall.com/trac/. | |
9 | +# | |
10 | +# Author: Sam Bloomquist <spooninator@hotmail.com> | |
11 | +# Author: Daan van Etten <daan@stuq.nl> | |
12 | + | |
13 | +import time | |
14 | +import datetime | |
15 | +import sys | |
16 | + | |
17 | +import dbhelper | |
18 | + | |
19 | +from trac import __version__ as tracversion_runtime | |
20 | + | |
21 | +from datetime import datetime, date | |
22 | + | |
23 | +from trac.core import * | |
24 | +from trac.config import BoolOption | |
25 | +from trac.env import IEnvironmentSetupParticipant | |
26 | +from trac.perm import IPermissionRequestor | |
27 | +from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet, add_script | |
28 | +from trac.web.main import IRequestHandler | |
29 | +from trac.util import escape, Markup, format_date | |
30 | +from trac.ticket import ITicketChangeListener | |
31 | +from trac.ticket import model | |
32 | +from trac.util.datefmt import to_datetime | |
33 | + | |
34 | +class BurndownComponent(Component): | |
35 | + implements(IEnvironmentSetupParticipant, INavigationContributor, | |
36 | + IRequestHandler, ITemplateProvider, IPermissionRequestor, ITicketChangeListener) | |
37 | + | |
38 | + tracversion=tracversion_runtime[:4] | |
39 | + | |
40 | + #--------------------------------------------------------------------------- | |
41 | + # IEnvironmentSetupParticipant methods | |
42 | + #--------------------------------------------------------------------------- | |
43 | + def environment_created(self): | |
44 | + """Called when a new Trac environment is created.""" | |
45 | + if self.environment_needs_upgrade(None): | |
46 | + self.upgrade_environment(None) | |
47 | + | |
48 | + def environment_needs_upgrade(self, db): | |
49 | + if not db: | |
50 | + db = self.env.get_db_cnx() | |
51 | + | |
52 | + needsUpgrade = True | |
53 | + | |
54 | + # See if the burndown table exists, if not, we need an upgrade | |
55 | + if dbhelper.table_exists(db, "burndown"): | |
56 | + needsUpgrade = False | |
57 | + if dbhelper.table_field_exists(db, "burndown", "week"): | |
58 | + needsUpgrade = True | |
59 | + | |
60 | + if dbhelper.table_field_exists(db, "milestone", "started"): | |
61 | + needsUpgrade = False | |
62 | + | |
63 | + return needsUpgrade | |
64 | + | |
65 | + def upgrade_environment(self, db): | |
66 | + db = self.env.get_db_cnx() | |
67 | + | |
68 | + needsCreate = True | |
69 | + needsUpgrade_milestone = True | |
70 | + needsUpgrade_burndown = False | |
71 | + | |
72 | + if dbhelper.table_exists(db, "burndown"): | |
73 | + needsCreate = False | |
74 | + if dbhelper.table_field_exists(db, "burndown", "week"): | |
75 | + needsUpgrade_burndown = True | |
76 | + | |
77 | + if dbhelper.table_field_exists(db, "milestone", "started"): | |
78 | + needsUpgrade_milestone = False | |
79 | + | |
80 | + if needsCreate: | |
81 | + print >> sys.stderr, 'Attempting to create the burndown table' | |
82 | + dbhelper.create_burndown_table(db, self.env) | |
83 | + | |
84 | + if needsUpgrade_milestone: | |
85 | + print >> sys.stderr, 'Attempting to modify the milestone table' | |
86 | + dbhelper.upgrade_milestone_table(db, self.env) | |
87 | + | |
88 | + if needsUpgrade_burndown: | |
89 | + print >> sys.stderr, 'Attempting to modify the burndown table' | |
90 | + dbhelper.upgrade_burndown_table(db, self.env) | |
91 | + | |
92 | + db.commit() | |
93 | + | |
94 | + #--------------------------------------------------------------------------- | |
95 | + # ITicketChangeListener methods | |
96 | + #--------------------------------------------------------------------------- | |
97 | + | |
98 | + def ticket_created(self, ticket): | |
99 | + self.log.debug('burndown plugin - ticket_created') | |
100 | + self.update_burndown_data() | |
101 | + | |
102 | + def ticket_changed(self, ticket, comment, author, old_values): | |
103 | + self.log.debug('burndown plugin - ticket_changed') | |
104 | + self.update_burndown_data() | |
105 | + | |
106 | + def ticket_deleted(self, ticket): | |
107 | + self.log.debug('burndown plugin - ticket_modified') | |
108 | + self.update_burndown_data() | |
109 | + | |
110 | + #--------------------------------------------------------------------------- | |
111 | + # INavigationContributor methods | |
112 | + #--------------------------------------------------------------------------- | |
113 | + def get_active_navigation_item(self, req): | |
114 | + return "burndown" | |
115 | + | |
116 | + def get_navigation_items(self, req): | |
117 | + if req.perm.has_permission("BURNDOWN_VIEW"): | |
118 | + if self.tracversion=="0.10": | |
119 | + yield 'mainnav', 'burndown', Markup('<a href="%s">Burndown</a>') % req.href.burndown() | |
120 | + else: | |
121 | + yield 'mainnav', 'burndown', Markup('<a href="%s">Burndown</a>' % req.href.burndown()) | |
122 | + | |
123 | + #--------------------------------------------------------------------------- | |
124 | + # IPermissionRequestor methods | |
125 | + #--------------------------------------------------------------------------- | |
126 | + def get_permission_actions(self): | |
127 | + return ["BURNDOWN_VIEW", "BURNDOWN_ADMIN"] | |
128 | + | |
129 | + #--------------------------------------------------------------------------- | |
130 | + # IRequestHandler methods | |
131 | + #--------------------------------------------------------------------------- | |
132 | + def match_request(self, req): | |
133 | + return req.path_info == '/burndown' | |
134 | + | |
135 | + def process_request(self, req): | |
136 | + req.perm.assert_permission('BURNDOWN_VIEW') | |
137 | + | |
138 | + db = self.env.get_db_cnx() | |
139 | + | |
140 | + milestones = dbhelper.get_milestones(db) | |
141 | + components = dbhelper.get_components(db) | |
142 | + | |
143 | + selected_milestone = None | |
144 | + if (len(milestones)>0): | |
145 | + selected_milestone = dbhelper.get_current_milestone(db, req.args.get('selected_milestone', "")) | |
146 | + | |
147 | + selected_component = req.args.get('selected_component', 'All Components') | |
148 | + | |
149 | + empty_db_for_testing = req.args.get('empty_db_for_testing', 'false') | |
150 | + if empty_db_for_testing == "true": | |
151 | + req.perm.assert_permission('TRAC_ADMIN') | |
152 | + dbhelper.empty_db_for_testing(db) | |
153 | + | |
154 | + # expose display data to the templates | |
155 | + data = {} | |
156 | + data['milestones'] = req.hdf['milestones'] = milestones | |
157 | + data['components'] = req.hdf['components'] = components | |
158 | + data['selected_milestone'] = req.hdf['selected_milestone'] = selected_milestone | |
159 | + data['selected_component'] = req.hdf['selected_component'] = selected_component | |
160 | + data['draw_graph'] = req.hdf['draw_graph'] = False | |
161 | + data['start'] = req.hdf['start'] = False | |
162 | + | |
163 | + if req.perm.has_permission("BURNDOWN_ADMIN"): | |
164 | + data['start'] = req.hdf['start'] = True # show the start and complete milestone buttons to admins | |
165 | + | |
166 | + if req.args.has_key('start'): | |
167 | + self.start_milestone(db, selected_milestone['name']) | |
168 | + | |
169 | + data['draw_graph'] = req.hdf['draw_graph'] = True | |
170 | + self.update_burndown_data() | |
171 | + | |
172 | + data['burndown_data'] = req.hdf['burndown_data'] = [] | |
173 | + burndown_data, estimated_data = self.get_burndown_data(db, selected_milestone, components, selected_component) | |
174 | + data['burndown_data'] = req.hdf['burndown_data'] = burndown_data | |
175 | + data['estimated_data'] = req.hdf['estimated_data'] = estimated_data | |
176 | + | |
177 | + add_stylesheet(req, 'hw/css/burndown.css') | |
178 | + | |
179 | + self.update_burndown_data() | |
180 | + | |
181 | + if self.tracversion=="0.10": | |
182 | + add_script(req, 'hw/js/line.js') | |
183 | + add_script(req, 'hw/js/wz_jsgraphics.js') | |
184 | + return 'burndown.cs', None | |
185 | + else: | |
186 | + data['library'] = '' | |
187 | + if data['library'] == 'flot': | |
188 | + add_script(req, 'hw/js/jquery.flot.js') | |
189 | + else: | |
190 | + add_script(req, 'hw/js/line.js') | |
191 | + add_script(req, 'hw/js/wz_jsgraphics.js') | |
192 | + | |
193 | + return 'burndown.html', data, None | |
194 | + | |
195 | + | |
196 | + def get_burndown_data(self, db, selected_milestone, components, selected_component): | |
197 | + cursor = db.cursor() | |
198 | + cursor.execute("SELECT due FROM milestone where name='%s'" % selected_milestone['name']); | |
199 | + milestone_due = cursor.fetchone()[0]; | |
200 | + t = to_datetime(milestone_due) | |
201 | + due_milestone = datetime(t.year,t.month,t.day) | |
202 | + | |
203 | + delta = None | |
204 | + component_data = {} # this will be a dictionary of lists of tuples -- e.g. component_data = {'componentName':[(id, hours_remaining), (id, hours_remaining), (id, hours_remaining)]} | |
205 | + for comp in components: | |
206 | + if selected_component == 'All Components' or comp['name'] == selected_component: | |
207 | + sqlBurndown = "SELECT id, hours_remaining ,date "\ | |
208 | + "FROM burndown "\ | |
209 | + "WHERE milestone_name = '%s' AND component_name = '%s' "\ | |
210 | + "ORDER BY id" % (selected_milestone['name'], comp['name']) | |
211 | + cursor.execute(sqlBurndown) | |
212 | + component_data[comp['name']] = cursor.fetchall() | |
213 | + self.log.debug(component_data[comp['name']]) | |
214 | + | |
215 | + if len(components)==0: | |
216 | + comp = {'name':'-'} | |
217 | + if selected_component == 'All Components' or comp['name'] == selected_component: | |
218 | + self.log.debug("comp = %s", comp['name']) | |
219 | + self.log.debug("selected_component = %s", selected_component) | |
220 | + sqlBurndown = "SELECT id, hours_remaining, date "\ | |
221 | + "FROM burndown "\ | |
222 | + "WHERE milestone_name = %s AND component_name = %s "\ | |
223 | + "ORDER BY id" | |
224 | + cursor.execute(sqlBurndown, (selected_milestone['name'], comp['name'])) | |
225 | + component_data[comp['name']] = cursor.fetchall() | |
226 | + self.log.debug(component_data[comp['name']]) | |
227 | + | |
228 | + if len(component_data) > 0 and component_data[component_data.keys()[0]]: | |
229 | + burndown_length = len(component_data[component_data.keys()[0]]) | |
230 | + else: | |
231 | + burndown_length = 0 | |
232 | + burndown_data = [] | |
233 | + plotdata = [] | |
234 | + if selected_component == 'All Components': | |
235 | + for time_unit in range (0, burndown_length): | |
236 | + sumHours = 0 | |
237 | + for comp in components: | |
238 | + plotdata.append(component_data[comp['name']][time_unit][2]) | |
239 | + self.log.debug('component: %s', [comp['name']]); | |
240 | + self.log.debug('time_unit: %s', [time_unit]); | |
241 | + if (component_data[comp['name']] and len(component_data[comp['name']]) > time_unit): | |
242 | + self.log.debug('hours: %s', component_data[comp['name']][time_unit][1]); | |
243 | + sumHours += component_data[comp['name']][time_unit][1] | |
244 | + if len(components)==0: | |
245 | + comp={'name':'-'} | |
246 | + self.log.debug('component: %s', [comp['name']]); | |
247 | + self.log.debug('time_unit: %s', [time_unit]); | |
248 | + if (component_data[comp['name']] and len(component_data[comp['name']]) > time_unit): | |
249 | + self.log.debug('hours: %s', component_data[comp['name']][time_unit][1]); | |
250 | + sumHours += component_data[comp['name']][time_unit][1] | |
251 | + | |
252 | + burndown_data.append((time_unit+1, sumHours)) | |
253 | + | |
254 | + else: | |
255 | + for time_unit in range (0, len(component_data[selected_component])): | |
256 | + plotdata.append(component_data[selected_component]['date']) | |
257 | + burndown_data.append((time_unit+1, component_data[selected_component][time_unit][1])) | |
258 | + start_milestone = None | |
259 | + estimated_data = [] | |
260 | + if len(burndown_data)!=0: | |
261 | + if len(components)==0: | |
262 | + t = time.strptime(component_data['-'][0][2],'%Y/%m/%d') | |
263 | + start_milestone = datetime(t[0] ,t[1] ,t[2]) | |
264 | + else: | |
265 | + t = time.strptime(component_data[component_data.keys()[0]][0][2],'%Y/%m/%d') | |
266 | + start_milestone = datetime(t[0] ,t[1] ,t[2]) | |
267 | + initial_estimated = burndown_data[0][1] | |
268 | + | |
269 | + term = (due_milestone - start_milestone).days + 1 | |
270 | + | |
271 | + for time_unit in range (0,term): | |
272 | + estimated_data.append((time_unit+1 ,initial_estimated*(1-float(time_unit+1)/term) )) | |
273 | + | |
274 | + return (burndown_data, estimated_data) | |
275 | + | |
276 | + def start_milestone(self, db, milestone): | |
277 | + startdate = dbhelper.get_startdate_for_milestone(db, milestone) | |
278 | + | |
279 | + if startdate != None: | |
280 | + raise TracError("Milestone %s was already started." % milestone) | |
281 | + | |
282 | + dbhelper.set_startdate_for_milestone(db, milestone, int(time.time())) | |
283 | + | |
284 | + #--------------------------------------------------------------------------- | |
285 | + # ITemplateProvider methods | |
286 | + #--------------------------------------------------------------------------- | |
287 | + def get_templates_dirs(self): | |
288 | + from pkg_resources import resource_filename | |
289 | + return [resource_filename(__name__, 'templates')] | |
290 | + | |
291 | + def get_htdocs_dirs(self): | |
292 | + from pkg_resources import resource_filename | |
293 | + return [('hw', resource_filename(__name__, 'htdocs'))] | |
294 | + | |
295 | + #------------------------------------------------------------------------ | |
296 | + # update_burndown_data | |
297 | + # - add up the hours remaining for the open tickets for each open milestone and put the sums into the burndown table | |
298 | + #------------------------------------------------------------------------ | |
299 | + def update_burndown_data(self): | |
300 | + db = self.env.get_db_cnx() | |
301 | + cursor = db.cursor() | |
302 | + | |
303 | + # today's date | |
304 | + today = format_date(int(time.time())) | |
305 | + | |
306 | + milestones = dbhelper.get_milestones(db) | |
307 | + components = dbhelper.get_components(db) | |
308 | + for mile in milestones: | |
309 | + if mile['started'] and not mile['completed']: # milestone started, but not completed | |
310 | + for comp in components: | |
311 | + sqlSelect = "SELECT est.value AS estimate, ts.value AS spent "\ | |
312 | + "FROM ticket t "\ | |
313 | + " LEFT OUTER JOIN ticket_custom est ON (t.id = est.ticket AND est.name = 'estimatedhours') "\ | |
314 | + " LEFT OUTER JOIN ticket_custom ts ON (t.id = ts.ticket AND ts.name = 'totalhours') "\ | |
315 | + "WHERE t.component = %s AND t.milestone = %s"\ | |
316 | + " AND status IN ('new', 'assigned', 'reopened', 'accepted') " | |
317 | + cursor.execute(sqlSelect, [comp['name'], mile['name']]) | |
318 | + | |
319 | + rows = cursor.fetchall() | |
320 | + hours = 0 | |
321 | + estimate = 0 | |
322 | + spent = 0 | |
323 | + if rows: | |
324 | + for estimate, spent in rows: | |
325 | + if not estimate: | |
326 | + estimate = 0 | |
327 | + if not spent: | |
328 | + spent = 0 | |
329 | + | |
330 | + if (float(estimate) - float(spent)) > 0: | |
331 | + hours += float(estimate) - float(spent) | |
332 | + | |
333 | + cursor.execute("SELECT id FROM burndown WHERE date = %s AND milestone_name = %s"\ | |
334 | + "AND component_name = %s", [today, mile['name'], comp['name']]) | |
335 | + | |
336 | + row = cursor.fetchone() | |
337 | + | |
338 | + try: | |
339 | + if row: | |
340 | + cursor.execute("UPDATE burndown SET hours_remaining = %s WHERE date = %s AND milestone_name = %s"\ | |
341 | + "AND component_name = %s", [hours, today, mile['name'], comp['name']]) | |
342 | + else: | |
343 | + cursor.execute("INSERT INTO burndown(component_name, milestone_name, date, hours_remaining) "\ | |
344 | + " VALUES(%s,%s,%s,%s)", [comp['name'], mile['name'], today, hours]) | |
345 | + except Exception, inst: | |
346 | + self.log.debug(type(inst)) # the exception instance | |
347 | + self.log.debug(inst.args) # arguments stored in .args | |
348 | + self.log.debug(inst) # __str__ allows args to printed directly | |
349 | + cursor.connection.rollback() | |
350 | + else: | |
351 | + db.commit() | |
352 | + if len(components)==0: | |
353 | + comp = {'name':'-'} | |
354 | + sqlSelect = "SELECT est.value AS estimate, ts.value AS spent "\ | |
355 | + "FROM ticket t "\ | |
356 | + " LEFT OUTER JOIN ticket_custom est ON (t.id = est.ticket AND est.name = 'estimatedhours') "\ | |
357 | + " LEFT OUTER JOIN ticket_custom ts ON (t.id = ts.ticket AND ts.name = 'totalhours') "\ | |
358 | + "WHERE t.milestone = %s"\ | |
359 | + " AND status IN ('new', 'assigned', 'reopened', 'accepted') " | |
360 | + cursor.execute(sqlSelect, [mile['name']]) | |
361 | + | |
362 | + rows = cursor.fetchall() | |
363 | + hours = 0 | |
364 | + estimate = 0 | |
365 | + spent = 0 | |
366 | + if rows: | |
367 | + for estimate, spent in rows: | |
368 | + if not estimate: | |
369 | + estimate = 0 | |
370 | + if not spent: | |
371 | + spent = 0 | |
372 | + | |
373 | + if (float(estimate) - float(spent)) > 0: | |
374 | + hours += float(estimate) - float(spent) | |
375 | + | |
376 | + cursor.execute("SELECT id FROM burndown WHERE date = %s AND milestone_name = %s"\ | |
377 | + "AND component_name = %s", [today, mile['name'], comp['name']]) | |
378 | + | |
379 | + row = cursor.fetchone() | |
380 | + | |
381 | + try: | |
382 | + if row: | |
383 | + cursor.execute("UPDATE burndown SET hours_remaining = %s WHERE date = %s AND milestone_name = %s"\ | |
384 | + "AND component_name = %s", [hours, today, mile['name'], comp['name']]) | |
385 | + else: | |
386 | + cursor.execute("INSERT INTO burndown(component_name, milestone_name, date, hours_remaining) "\ | |
387 | + " VALUES(%s,%s,%s,%s)", [comp['name'], mile['name'], today, hours]) | |
388 | + except Exception, inst: | |
389 | + self.log.debug(type(inst)) # the exception instance | |
390 | + self.log.debug(inst.args) # arguments stored in .args | |
391 | + self.log.debug(inst) # __str__ allows args to printed directly | |
392 | + cursor.connection.rollback() | |
393 | + else: | |
394 | + db.commit() | |
395 | + | |
396 | + |