ScrumBurndownPlugin: burndown.py

File burndown.py, 9.7 kB (added by anonymous, 2 years ago)
Line 
1 # Burndown plugin
2 # Copyright (C) 2006 Sam Bloomquist <spooninator@hotmail.com>
3 # All rights reserved.
4 # vi: et ts=4 sw=4
5 # This software may at some point consist of voluntary contributions made by
6 # many individuals. For the exact contribution history, see the revision
7 # history and logs, available at http://projects.edgewall.com/trac/.
8 #
9 # Author: Sam Bloomquist <spooninator@hotmail.com>
10
11 import time
12
13 from trac.core import *
14 from trac.env import IEnvironmentSetupParticipant
15 from trac.perm import IPermissionRequestor
16 from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet
17 from trac.web.main import IRequestHandler
18 from trac.util import escape, Markup, format_date
19
20 class BurndownComponent(Component):
21     implements(IEnvironmentSetupParticipant, INavigationContributor,
22                     IRequestHandler, ITemplateProvider, IPermissionRequestor)
23    
24     #---------------------------------------------------------------------------
25     # IEnvironmentSetupParticipant methods
26     #---------------------------------------------------------------------------
27     def environment_created(self):
28         pass
29
30     def environment_needs_upgrade(self, db):
31         result = False
32        
33         #get a database connection if we don't already have one
34         if not db:
35             db = self.env.get_db_cnx()
36             handle_ta = True
37         else:
38             handle_ta = False
39            
40         # See if the burndown table exists, if not, return True because we need to upgrade the database
41         cursor = db.cursor()
42         cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='burndown'")
43         row = cursor.fetchone()
44         if not row:
45             result = True
46            
47         if handle_ta:
48             db.commit()
49            
50         return result
51
52     def upgrade_environment(self, db):
53         cursor = db.cursor()
54        
55         # Create the burndown table in the database
56         sqlBurndownCreate =     "CREATE TABLE burndown (" \
57                                     "    id integer PRIMARY KEY NOT NULL,"\
58                                     "    component_name text NOT NULL,"\
59                                     "    milestone_name text NOT NULL," \
60                                     "    date text NOT NULL,"\
61                                     "    hours_remaining integer NOT NULL"\
62                                     ")"
63                                    
64         cursor.execute(sqlBurndownCreate)
65        
66         sqlMilestone = [
67         #-- Add the 'started' column to the milestone table
68         """CREATE TEMP TABLE milestone_old AS SELECT * FROM milestone;""",
69         """DROP TABLE milestone;""",
70         """CREATE TABLE milestone (
71                  name            text PRIMARY KEY,
72                  due             integer, -- Due date/time
73                  completed       integer, -- Completed date/time
74                  started        integer, -- Started date/time
75                  description     text
76         );""",
77         """INSERT INTO milestone(name,due,completed,started,description)
78         SELECT name,due,completed,0,description FROM milestone_old;"""
79         ]
80         for s in sqlMilestone:
81             cursor.execute(s)
82
83     #---------------------------------------------------------------------------
84     # INavigationContributor methods
85     #---------------------------------------------------------------------------
86     def get_active_navigation_item(self, req):
87         return 'burndown'
88                
89     def get_navigation_items(self, req):
90         yield 'mainnav', 'burndown', Markup('<a href="%s">Burndown</a>', self.env.href.burndown())
91        
92     #---------------------------------------------------------------------------
93     # IPermissionRequestor methods
94     #---------------------------------------------------------------------------
95     def get_permission_actions(self):
96         return ["BURNDOWN_VIEW", "BURNDOWN_ADMIN"]
97
98     #---------------------------------------------------------------------------
99     # IRequestHandler methods
100     #---------------------------------------------------------------------------
101     def match_request(self, req):
102         return req.path_info == '/burndown'
103    
104     def process_request(self, req):
105         req.perm.assert_permission('BURNDOWN_VIEW')
106        
107         db = self.env.get_db_cnx()
108         cursor = db.cursor()
109        
110         cursor.execute("SELECT name FROM milestone")
111         milestone_lists = cursor.fetchall()
112         milestones = []
113         for mile in milestone_lists:
114             milestones.append(mile[0])
115            
116         cursor.execute("SELECT name FROM component")
117         component_lists = cursor.fetchall()
118         components = []
119         for comp in component_lists:
120             components.append(comp[0])
121        
122         selected_milestone = req.args.get('selected_milestone', milestones[0])
123         selected_component = req.args.get('selected_component', 'All Components')
124        
125         # expose display data to the clearsilver templates
126         req.hdf['milestones'] = milestones
127         req.hdf['components'] = components
128         req.hdf['selected_milestone'] = selected_milestone
129         req.hdf['selected_component'] = selected_component
130         req.hdf['draw_graph'] = False
131         req.hdf['start_complete'] = False
132        
133         if req.perm.has_permission("BURNDOWN_ADMIN"):
134             req.hdf['start_complete'] = True # show the start and complete milestone buttons to admins
135        
136         if req.args.has_key('start'):
137             self.startMilestone(db, selected_milestone)
138         elif req.args.has_key('complete'):
139             self.completeMilestone(db, selected_milestone)
140         else:
141             req.hdf['draw_graph'] = True
142             req.hdf['burndown_data'] = self.getBurndownData(db, selected_milestone, components, selected_component) # this will be a list of (id, hours_remaining) tuples
143        
144         add_stylesheet(req, 'hw/css/burndown.css')
145         return 'burndown.cs', None
146        
147     def getBurndownData(self, db, selected_milestone, components, selected_component):
148         cursor = db.cursor()
149        
150         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)]}
151         for comp in components:
152             if selected_component == 'All Components' or comp == selected_component:
153                 sqlBurndown = "SELECT id, hours_remaining "\
154                                     "FROM burndown "\
155                                     "WHERE milestone_name = '" + selected_milestone + "' AND component_name = '" + comp + "' "\
156                                     "ORDER BY id"
157                
158                 cursor.execute(sqlBurndown)
159                 component_data[comp] = cursor.fetchall()
160            
161         if component_data[component_data.keys()[0]]:
162             burndown_length = len(component_data[component_data.keys()[0]])
163         else:
164             burndown_length = 0
165         burndown_data = []
166         if selected_component == 'All Components':
167             for day in range (0, burndown_length):
168                 sumHours = 0
169                 for comp in components:
170                     sumHours += component_data[comp][day][1]
171                
172                 burndown_data.append((day+1, sumHours))
173                
174         else:
175             for day in range (0, len(component_data[selected_component])):
176                 burndown_data.append((day+1, component_data[selected_component][day][1]))
177            
178         return burndown_data
179        
180     def startMilestone(self, db, milestone):
181         cursor = db.cursor()
182         cursor.execute("SELECT started FROM milestone WHERE name = '%s'" % milestone)
183         row = cursor.fetchone()
184         if row and row[0] > 0:
185             raise TracError("Milestone '%s' was already started on %s" % (milestone, format_date(int(row[0]))))
186            
187         cursor.execute("UPDATE milestone SET started = %i WHERE name = '%s'" % (int(time.time()), milestone))
188        
189         db.commit()
190        
191     def completeMilestone(self, db, milestone):
192         cursor = db.cursor()
193         cursor.execute("SELECT completed FROM milestone WHERE name = '%s'" % milestone)
194         row = cursor.fetchone()
195         if row and row[0] > 0:
196             raise TracError("Milestone '%s' was already completed on %s" % (milestone, format_date(int(row[0]))))
197            
198         cursor.execute("UPDATE milestone SET completed = %i WHERE name = '%s'" % (int(time.time()), milestone))
199        
200         db.commit()
201        
202     #---------------------------------------------------------------------------
203     # ITemplateProvider methods
204     #---------------------------------------------------------------------------
205     def get_templates_dirs(self):
206         """
207         Return the absolute path of the directory containing the provided
208         ClearSilver templates.
209         """
210         from pkg_resources import resource_filename
211         return [resource_filename(__name__, 'templates')]
212
213     def get_htdocs_dirs(self):
214         """
215         Return a list of directories with static resources (such as style
216         sheets, images, etc.)
217
218         Each item in the list must be a `(prefix, abspath)` tuple. The
219         `prefix` part defines the path in the URL that requests to these
220         resources are prefixed with.
221         
222         The `abspath` is the absolute path to the directory containing the
223         resources on the local file system.
224         """
225         from pkg_resources import resource_filename
226         return [('hw', resource_filename(__name__, 'htdocs'))]