Changeset 4448
- Timestamp:
- 10/12/08 16:33:02 (3 months ago)
- Files:
-
- estimationtoolsplugin/trunk/estimationtools/burndownchart.py (modified) (8 diffs)
- estimationtoolsplugin/trunk/estimationtools/hoursremaining.py (modified) (2 diffs)
- estimationtoolsplugin/trunk/estimationtools/tests/burndownchart.py (modified) (8 diffs)
- estimationtoolsplugin/trunk/estimationtools/tests/hoursremaining.py (modified) (2 diffs)
- estimationtoolsplugin/trunk/estimationtools/tests/workloadchart.py (modified) (4 diffs)
- estimationtoolsplugin/trunk/estimationtools/utils.py (modified) (4 diffs)
- estimationtoolsplugin/trunk/estimationtools/workloadchart.py (modified) (2 diffs)
- estimationtoolsplugin/trunk/setup.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
estimationtoolsplugin/trunk/estimationtools/burndownchart.py
r4332 r4448 5 5 from trac.wiki.macros import WikiMacroBase 6 6 import copy 7 import re8 7 9 8 DEFAULT_OPTIONS = {'width': '800', 'height': '200', 'color': 'ff9900'} 10 9 11 10 class BurndownChart(WikiMacroBase): 12 """Creates burn down chart for given milestone.11 """Creates burn down chart for selected tickets. 13 12 14 This macro creates a chart that can be used to visualize the progress in a milestone ( akasprint or13 This macro creates a chart that can be used to visualize the progress in a milestone (e.g., sprint or 15 14 product backlog). 16 For a given milestone and time frame, the remaining,estimated effort is calculated.15 For a given set of tickets and a time frame, the remaining estimated effort is calculated. 17 16 18 17 The macro has the following parameters: 19 * `milestone`: '''mandatory''' parameter that specifies the milestone.18 * a comma-separated list of query parameters for the ticket selection, in the form "key=value" as specified in TracQuery#QueryLanguage. 20 19 * `startdate`: '''mandatory''' parameter that specifies the start date of the period (ISO8601 format) 21 20 * `enddate`: end date of the period. If omitted, it defaults to either the milestones `completed' date, 22 or `due`date, or today (in that order) (ISO8601 format) 23 * `sprints`: list of comma-separated name of sprints to be included in calculation. Must be surrounded by 24 brackets. 21 or `due` date, or today (in that order) (ISO8601 format) 25 22 * `width`: width of resulting diagram (defaults to 800) 26 23 * `height`: height of resulting diagram (defaults to 200) … … 30 27 Examples: 31 28 {{{ 32 [[BurndownChart(milestone = Sprint 1, startdate =2008-01-01)]]33 [[BurndownChart(milestone = Release 3.0, startdate = 2008-01-01, enddate =2008-01-15,34 width = 600, height = 100, color = 0000ff, sprints = (Sprint 1, Sprint 2))]]29 [[BurndownChart(milestone=Sprint 1, startdate=2008-01-01)]] 30 [[BurndownChart(milestone=Release 3.0|Sprint 1, startdate=2008-01-01, enddate=2008-01-15, 31 width=600, height=100, color=0000ff)]] 35 32 }}} 36 33 """ … … 39 36 40 37 def render_macro(self, req, name, content): 41 # you need 'TICKT_VIEW' or 'TICKET_VIEW_CC' (see PrivateTicketPatch) permissions 42 if not (req.perm.has_permission('TICKET_VIEW') or 43 req.perm.has_permission('TICKET_VIEW_CC')): 44 raise TracError('TICKET_VIEW or TICKET_VIEW_CC permission required') 45 options = copy.copy(DEFAULT_OPTIONS) 46 47 # replace all ',' in brackets with ';' to avoid splitting list of sprints 48 def repl(match): 49 return match.group().replace(',', ';') 50 regexp = re.compile(r'\((.*)\)') 51 content = regexp.sub(repl, content) 52 53 if content: 54 for arg in content.split(','): 55 i = arg.index('=') 56 options[arg[:i].strip()] = arg[i + 1:].strip() 38 # prepare options 39 options, query_args = parse_options(self.env.get_db_cnx(), content, copy.copy(DEFAULT_OPTIONS)) 57 40 58 # prepare options59 options = parse_options(self.env.get_db_cnx(), options)60 41 if not options['startdate']: 61 42 raise TracError("No start date specified!") 62 63 # parse list of sprints 64 sprintsarg = options.get('sprints') 65 if sprintsarg: 66 options['sprints'] = sprintsarg.strip('()').split(';') 67 43 68 44 # calculate data 69 timetable = self._calculate_timetable(options )45 timetable = self._calculate_timetable(options, query_args, req) 70 46 71 47 # scale data … … 116 92 "|".join(weekends), options['color'], options['milestone'].strip('\'\"'))) 117 93 118 def _calculate_timetable(self, options ):94 def _calculate_timetable(self, options, query_args, req): 119 95 db = self.env.get_db_cnx() 120 cursor = db.cursor()121 96 122 97 # create dictionary with entry for each day of the required time period … … 129 104 130 105 # get current values for all tickets within milestone and sprints 131 sprints = options.get('sprints') 132 if not sprints: 133 sprints = [] 134 135 sprints = [options['milestone']] + sprints 106 107 query_args[self.estimation_field + "!"] = None 108 tickets = execute_query(self.env, req, query_args) 136 109 137 select_tickets = ("SELECT " 138 "id, time, p.value as estimation " 139 "FROM ticket t, ticket_custom p " 140 "WHERE p.ticket = t.id and p.name = %%s and (t.milestone in (%s)) " 141 "ORDER BY t.id" % (',').join(['%s' for sprint in sprints])) 142 143 cursor.execute(select_tickets, [self.estimation_field] + sprints) 144 145 for id, time, estimation in cursor: 146 creationdate = datetime.fromtimestamp(time).date() 110 # print tickets 111 112 for t in tickets: 113 creationdate = t['time'].date() 114 estimation = t[self.estimation_field] 147 115 148 116 # get change history for each ticket … … 152 120 "FROM ticket t, ticket_change c " 153 121 "WHERE t.id = %s and c.ticket = t.id and c.field=%s " 154 "ORDER BY c.time DESC", [ id, self.estimation_field])122 "ORDER BY c.time DESC", [t['id'], self.estimation_field]) 155 123 156 124 nextchangedate = None … … 158 126 row = history_cursor.fetchone() 159 127 if row: 160 nextchangedate = datetime.fromtimestamp(row[0] ).date()128 nextchangedate = datetime.fromtimestamp(row[0], utc).date() 161 129 nextvalue = row[1] 162 130 … … 169 137 row = history_cursor.fetchone() 170 138 if row: 171 nextchangedate = datetime.fromtimestamp(row[0] ).date()139 nextchangedate = datetime.fromtimestamp(row[0], utc).date() 172 140 nextvalue = row[1] 173 141 else: estimationtoolsplugin/trunk/estimationtools/hoursremaining.py
r4332 r4448 1 from estimationtools.utils import get_estimation_field 2 from trac.core import TracError 1 from estimationtools.utils import get_estimation_field, execute_query 3 2 from trac.wiki.macros import WikiMacroBase 3 from trac.wiki.api import parse_args 4 4 5 5 class HoursRemaining(WikiMacroBase): 6 """Calculates remaining estimated hours for given milestone.6 """Calculates remaining estimated hours for the queried tickets. 7 7 8 `milestone` is a mandatory parameter. 8 The macro accepts a comma-separated list of query parameters for the ticket selection, 9 in the form "key=value" as specified in TracQuery#QueryLanguage. 9 10 10 11 Example: … … 17 18 18 19 def render_macro(self, req, name, content): 19 # you need 'TICKT_VIEW' or 'TICKET_VIEW_CC' (see PrivateTicketPatch) permissions 20 if not (req.perm.has_permission('TICKET_VIEW') or 21 req.perm.has_permission('TICKET_VIEW_CC')): 22 raise TracError('TICKET_VIEW or TICKET_VIEW_CC permission required') 23 options = {} 24 if content: 25 for arg in content.split(','): 26 i = arg.index('=') 27 options[arg[:i].strip()] = arg[i+1:].strip() 28 milestone = options.get('milestone') 29 if not milestone: 30 raise TracError("No milestone specified!") 31 db = self.env.get_db_cnx() 32 cursor = db.cursor() 33 cursor.execute("SELECT p.value as estimation" 34 " FROM ticket t, ticket_custom p" 35 " WHERE p.ticket = t.id and p.name = %s" 36 " AND t.milestone = %s", [self.estimation_field, milestone]) 20 _, options = parse_args(content, strict=False) 21 22 # we have to add custom estimation field to query so that field is added to 23 # resulting ticket list 24 options[self.estimation_field + "!"] = None 25 26 tickets = execute_query(self.env, req, options) 37 27 38 28 sum = 0.0 39 for estimation, in cursor:29 for t in tickets: 40 30 try: 41 sum += float( estimation)31 sum += float(t[self.estimation_field]) 42 32 except: 43 33 pass estimationtoolsplugin/trunk/estimationtools/tests/burndownchart.py
r4361 r4448 1 1 from estimationtools.burndownchart import * 2 2 from estimationtools.utils import * 3 from trac.test import EnvironmentStub 3 from trac.test import EnvironmentStub, MockPerm, Mock 4 4 from trac.ticket.model import Ticket 5 5 from trac.util.datefmt import utc 6 from trac.web.href import Href 6 7 import time 7 8 import unittest … … 14 15 self.env.config.set('ticket-custom', 'hours_remaining', 'text') 15 16 self.env.config.set('estimation-tools', 'estimation_field', 'hours_remaining') 17 self.req = Mock(href = Href('/'), 18 abs_href = Href('http://www.example.com/'), 19 perm = MockPerm(), 20 authname='anonymous') 16 21 17 22 def _insert_ticket(self, estimation): … … 32 37 def test_parse_options(self): 33 38 db = self.env.get_db_cnx() 34 options = parse_options(db, {"milestone":"milestone1", "startdate":"2008-02-20", 35 "enddate":"2008-02-28"}) 36 self.assertNotEqual(options['milestone'], None) 39 options, query_args = parse_options(db, "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28", {}) 40 self.assertNotEqual(query_args['milestone'], None) 37 41 self.assertNotEqual(options['startdate'], None) 38 42 self.assertNotEqual(options['enddate'], None) … … 41 45 chart = BurndownChart(self.env) 42 46 db = self.env.get_db_cnx() 43 options = parse_options(db, {"milestone":"milestone1", "startdate":"2008-02-20", 44 "enddate":"2008-02-28"}) 45 timetable = chart._calculate_timetable(options) 47 options, query_args = parse_options(db, "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28", {}) 48 timetable = chart._calculate_timetable(options, query_args, self.req) 46 49 xdata, ydata, maxhours = chart._scale_data(timetable, options) 47 50 self.assertEqual(xdata, ['0.0', '12.5', '25.0', '37.5', '50.0', '62.5', '75.0', '87.5', '100.0']) … … 54 57 day2 = day1 + timedelta(days=1) 55 58 day3 = day2 + timedelta(days=1) 56 options = {'today': day3, 'milestone': "milestone1", 'startdate': day1, 'enddate': day3} 59 options = {'today': day3, 'startdate': day1, 'enddate': day3} 60 query_args = {'milestone': "milestone1"} 57 61 self._insert_ticket('10') 58 timetable = chart._calculate_timetable(options) 62 timetable = chart._calculate_timetable(options, query_args, self.req) 63 self.assertEqual(timetable, {day1: 10.0, day2: 10.0, day3: 10.0}) 64 65 def test_calculate_timetable_without_milestone(self): 66 chart = BurndownChart(self.env) 67 day1 = datetime.now(utc).date() 68 day2 = day1 + timedelta(days=1) 69 day3 = day2 + timedelta(days=1) 70 options = {'today': day3, 'startdate': day1, 'enddate': day3} 71 self._insert_ticket('10') 72 timetable = chart._calculate_timetable(options, {}, self.req) 59 73 self.assertEqual(timetable, {day1: 10.0, day2: 10.0, day3: 10.0}) 60 74 … … 64 78 day2 = day1 + timedelta(days=1) 65 79 day3 = day2 + timedelta(days=1) 66 options = {'today': day3, 'milestone': "milestone1", 'startdate': day1, 'enddate': day3} 80 options = {'today': day3, 'startdate': day1, 'enddate': day3} 81 query_args = {'milestone': "milestone1"} 67 82 ticket1 = self._insert_ticket('10') 68 83 self._change_ticket(ticket1, {day2:'5', day3:'0'}) 69 84 70 timetable = chart._calculate_timetable(options )85 timetable = chart._calculate_timetable(options, query_args, self.req) 71 86 self.assertEqual(timetable, {day1: 10.0, day2: 5.0, day3: 0.0}) 72 87 … … 76 91 day2 = day1 + timedelta(days=1) 77 92 day3 = day2 + timedelta(days=1) 78 options = {'today': day3, 'milestone': "milestone1", 'startdate': day1, 'enddate': day3} 93 options = {'today': day3, 'startdate': day1, 'enddate': day3} 94 query_args = {'milestone': "milestone1"} 79 95 ticket1 = self._insert_ticket('10') 80 self._change_ticket(ticket1, {day2:'5', day3:' 0'})96 self._change_ticket(ticket1, {day2:'5', day3:''}) 81 97 ticket2 = self._insert_ticket('0') 82 98 self._change_ticket(ticket2, {day2:'1', day3:'2'}) 83 99 84 timetable = chart._calculate_timetable(options )100 timetable = chart._calculate_timetable(options, query_args, self.req) 85 101 self.assertEqual(timetable, {day1: 10.0, day2: 6.0, day3: 2.0}) 86 102 … … 91 107 day3 = day2 + timedelta(days=1) 92 108 day4 = day3 + timedelta(days=1) 93 options = {'today': day3, 'milestone': "milestone1", 'startdate': day1, 'enddate': day3} 109 options = {'today': day3, 'startdate': day1, 'enddate': day3} 110 query_args = {'milestone': "milestone1"} 94 111 ticket1 = self._insert_ticket('10') 95 self._change_ticket(ticket1, {day2:'5', day4:' 0'})112 self._change_ticket(ticket1, {day2:'5', day4:''}) 96 113 97 timetable = chart._calculate_timetable(options )114 timetable = chart._calculate_timetable(options, query_args, self.req) 98 115 self.assertEqual(timetable, {day1: 10.0, day2: 5.0, day3: 5.0}) estimationtoolsplugin/trunk/estimationtools/tests/hoursremaining.py
r4361 r4448 1 1 from estimationtools.hoursremaining import HoursRemaining 2 from trac.test import EnvironmentStub, Mock 2 from trac.test import EnvironmentStub, Mock, MockPerm 3 3 from trac.ticket.model import Ticket 4 4 from trac.web.href import Href … … 14 14 self.req = Mock(href = Href('/'), 15 15 abs_href = Href('http://www.example.com/'), 16 perm = Mock(has_permission=lambda x: x == 'TICKET_VIEW')) 16 perm = MockPerm(), 17 authname='anonymous') 17 18 18 19 def _insert_ticket(self, estimation): estimationtoolsplugin/trunk/estimationtools/tests/workloadchart.py
r4361 r4448 1 1 from estimationtools.workloadchart import WorkloadChart 2 from trac.test import EnvironmentStub, Mock 2 from trac.test import EnvironmentStub, Mock, MockPerm 3 3 from trac.ticket.model import Ticket 4 4 from trac.web.href import Href … … 14 14 self.req = Mock(href = Href('/'), 15 15 abs_href = Href('http://www.example.com/'), 16 perm = Mock(has_permission=lambda x: x == 'TICKET_VIEW')) 16 perm = MockPerm(), 17 authname='anonymous') 17 18 18 19 def _insert_ticket(self, estimation, owner): … … 31 32 result = workload_chart.render_macro(self.req, "", "milestone=milestone1") 32 33 self.assertEqual(result, u'<img src="http://chart.apis.google.com/chart?chs=400x100&'\ 33 'chd=t:10,30,20&cht=p3&chtt=Workload 60h ( 1workdays left)&'\34 'chd=t:10,30,20&cht=p3&chtt=Workload 60h (0 workdays left)&'\ 34 35 'chl=A 10h|C 30h|B 20h&chco=ff9900" alt=\'Workload Chart\' />') 35 36 … … 43 44 result = workload_chart.render_macro(self.req, "", "milestone=milestone1") 44 45 self.assertEqual(result, u'<img src="http://chart.apis.google.com/chart?chs=400x100&'\ 45 'chd=t:10,30,20&cht=p3&chtt=Workload 60h ( 1workdays left)&'\46 'chd=t:10,30,20&cht=p3&chtt=Workload 60h (0 workdays left)&'\ 46 47 'chl=A 10h|C 30h|B 20h&chco=ff9900" alt=\'Workload Chart\' />' ) estimationtoolsplugin/trunk/estimationtools/utils.py
r4332 r4448 3 3 from trac.config import Option 4 4 from trac.core import TracError 5 from trac.wiki.api import parse_args 6 from trac.ticket.query import Query 7 from trac.util.datefmt import utc 8 9 AVAILABLE_OPTIONS = ['startdate', 'enddate', 'today', 'width', 'height', 'color'] 5 10 6 11 def get_estimation_field(): … … 9 14 Defaults to 'estimatedhours'""") 10 15 11 def parse_options(db, options):12 """Parses the parameters, makes some sanity checks, and creates default svalues16 def parse_options(db, content, options): 17 """Parses the parameters, makes some sanity checks, and creates default values 13 18 for missing parameters. 14 19 """ … … 17 22 # check arguments 18 23 options['milestone'] = options.get('milestone') 19 if not options['milestone']: 20 raise TracError("No milestone specified!") 24 # if not options['milestone']: 25 # raise TracError("No milestone specified!") 26 27 _, parsed_options = parse_args(content, strict=False) 28 29 options.update(parsed_options) 21 30 22 31 startdatearg = options.get('startdate') … … 28 37 if enddatearg: 29 38 options['enddate'] = datetime(*strptime(enddatearg, "%Y-%m-%d")[0:5]).date() 30 if not options['enddate']: 39 options['milestone'] = options.get('milestone') 40 41 if not options['enddate'] and options['milestone']: 42 # use first milestone 43 milestone = options['milestone'].split('|')[0] 31 44 # try to get end date from db 32 cursor.execute("SELECT completed, due FROM milestone WHERE name = %s", [ options['milestone']])45 cursor.execute("SELECT completed, due FROM milestone WHERE name = %s", [milestone]) 33 46 row = cursor.fetchone() 34 47 if not row: 35 raise TracError("Couldn't find milestone %s" % ( options['milestone']))48 raise TracError("Couldn't find milestone %s" % (milestone)) 36 49 if row[0]: 37 50 options['enddate'] = datetime.fromtimestamp(row[0]).date() 38 51 elif row[1]: 39 52 options['enddate'] = datetime.fromtimestamp(row[1]).date() 40 else: 53 54 if not options['enddate']: 41 55 options['enddate'] = datetime.now().date() 42 56 todayarg = options.get('today') 43 57 if not todayarg: 44 58 options['today'] = datetime.now().date() 45 return options 59 60 # all arguments that are no key should be treated as part of the query 61 query_args = {} 62 for key in options.keys(): 63 if not key in AVAILABLE_OPTIONS: 64 query_args[key] = options[key] 65 return options, query_args 66 67 def execute_query(env, req, query_args): 68 query_string = '&'.join(['%s=%s' % item for item in query_args.iteritems()]) 69 query = Query.from_string(env, query_string) 70 71 tickets = query.execute(req) 72 73 tickets = [t for t in tickets 74 if ('TICKET_VIEW' or 'TICKET_VIEW_CC') in req.perm('ticket', t['id'])] 75 76 return tickets 77 estimationtoolsplugin/trunk/estimationtools/workloadchart.py
r4332 r4448 8 8 9 9 class WorkloadChart(WikiMacroBase): 10 """Creates workload chart for given milestone.10 """Creates workload chart for the selected tickets. 11 11 12 12 This macro creates a pie chart that shows the remaining estimated workload per ticket owner, 13 13 and the remaining work days. 14 14 It has the following parameters: 15 * `milestone`: '''mandatory''' parameter that specifies the milestone.15 * a comma-separated list of query parameters for the ticket selection, in the form "key=value" as specified in TracQuery#QueryLanguage. 16 16 * `width`: width of resulting diagram (defaults to 400) 17 17 * `height`: height of resulting diagram (defaults to 100) … … 29 29 30 30 def render_macro(self, req, name, content): 31 if not (req.perm.has_permission('TICKET_VIEW') or32 req.perm.has_permission('TICKET_VIEW_CC')):33 raise TracError('TICKET_VIEW or TICKET_VIEW_CC permission required')34 options = copy.copy(DEFAULT_OPTIONS)35 if content:36 for arg in content.split(','):37 i = arg.index('=')38 options[arg[:i].strip()] = arg[i+1:].strip()39 31 db = self.env.get_db_cnx() 40 options = parse_options(db, options) 41 milestone = options['milestone'] 42 cursor = db.cursor() 43 cursor.execute("SELECT owner, p.value " 44 " FROM ticket t, ticket_custom p" 45 " WHERE p.ticket = t.id and p.name = %s" 46 " AND t.milestone = %s", [self.estimation_field, milestone]) 32 # prepare options 33 options, query_args = parse_options(db, content, copy.copy(DEFAULT_OPTIONS)) 34 35 query_args[self.estimation_field + "!"] = None 36 tickets = execute_query(self.env, req, query_args) 37 47 38 sum = 0.0 48 39 estimations = {} 49 for owner, estimation in cursor:40 for ticket in tickets: 50 41 try: 51 sum += float(estimation) 42 estimation = float(ticket[self.estimation_field]) 43 owner = ticket['owner'] 44 sum += estimation 52 45 if estimations.has_key(owner): 53 estimations[owner] += float(estimation)46 estimations[owner] += estimation 54 47 else: 55 estimations[owner] = float(estimation)48 estimations[owner] = estimation 56 49 except: 57 50 pass estimationtoolsplugin/trunk/setup.py
r4361 r4448 9 9 author_email = 'hoessler@gmail.com', 10 10 description = 'Trac plugin for visualizing and quick editing of effort estimations', 11 version = '0. 3',11 version = '0.4', 12 12 license='BSD', 13 13 packages=['estimationtools'],
