Changeset 4909
- Timestamp:
- 12/01/08 08:13:29 (1 month ago)
- Files:
-
- estimationtoolsplugin/branches/optilude-change-line/estimationtools/burndownchart.py (modified) (6 diffs)
- estimationtoolsplugin/branches/optilude-change-line/estimationtools/tests/burndownchart.py (modified) (14 diffs)
- estimationtoolsplugin/branches/optilude-change-line/estimationtools/tests/workloadchart.py (modified) (2 diffs)
- estimationtoolsplugin/branches/optilude-change-line/estimationtools/utils.py (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
estimationtoolsplugin/branches/optilude-change-line/estimationtools/burndownchart.py
r4733 r4909 2 2 from datetime import datetime 3 3 from datetime import timedelta 4 from estimationtools.utils import parse_options, execute_query , get_estimation_field,\5 get_closed_states 4 from estimationtools.utils import parse_options, execute_query 5 from estimationtools.utils import get_estimation_field, get_closed_states, get_initial_estimation_field 6 6 from trac.core import TracError 7 7 from trac.util.html import Markup … … 10 10 import copy 11 11 12 DEFAULT_OPTIONS = {'width': '800', 'height': '200', 'color': 'ff9900 '}12 DEFAULT_OPTIONS = {'width': '800', 'height': '200', 'color': 'ff9900,aa8800'} 13 13 14 14 class BurndownChart(WikiMacroBase): … … 28 28 * `color`: color specified as 6-letter string of hexadecimal values in the format `RRGGBB`. 29 29 Defaults to `ff9900`, a nice orange. 30 * `title`: the title of the graph. If omitted, defaults to the title of the first milestone given, if any. 31 * `change`: set to 1 to include a second line showing what the burndown would've looked like, had 32 there not been any scope change 33 * `interval_days`: the number of days between points on the x axis, defaulting to 1 30 34 31 35 Examples: … … 37 41 """ 38 42 43 initial_estimation_field = get_initial_estimation_field() 39 44 estimation_field = get_estimation_field() 40 45 closed_states = get_closed_states() 41 46 42 47 def render_macro(self, req, name, content): 43 48 44 49 # prepare options 45 50 options, query_args = parse_options(self.env.get_db_cnx(), content, copy.copy(DEFAULT_OPTIONS)) … … 52 57 options['enddate'] = options['startdate'] + timedelta(days=1) 53 58 59 change = options['change'] 60 54 61 # calculate data 55 timetable = self._calculate_timetable(options, query_args, req) 56 57 # scale data 58 xdata, ydata, maxhours = self._scale_data(timetable, options) 59 62 timetable, delta = self._calculate_timetable(options, query_args, req) 63 timetable_less_change = {} 64 65 if change: 66 67 cumulative_change = Decimal(0) 68 for current_date in sorted(timetable.keys()): 69 cumulative_change += delta.get(current_date, Decimal(0)) 70 timetable_less_change[current_date] = timetable[current_date] - cumulative_change 71 72 dates = sorted(timetable.keys()) 73 60 74 # build html for google chart api 61 dates = sorted(timetable.keys()) 62 bottomaxis = "0:|" + ("|").join([str(date.day) for date in dates]) + \ 75 76 chart_params = {} 77 chart_params['cht'] = 'lxy' 78 chart_params['chtt'] = self._get_title(options) 79 chart_params['chco'] = options['color'] 80 chart_params['chs'] = "%sx%s" % (options['width'], options['height']) 81 chart_params['chg'] = "100.0,100.0,1,0" # create top and right bounding line by using grid" 82 chart_params['chxt'] = "x,x,x,y" 83 84 # Add scaled data 85 maxhours = max(timetable.values()) 86 if change: 87 maxhours = max(maxhours, max(timetable_less_change.values())) 88 89 xdata, ydata,maxhours = self._scale_data(timetable, dates, maxhours, options) 90 chart_params['chd'] = "t:%s|%s" % (",".join(xdata), ",".join(ydata),) 91 cxdata = cydata = None 92 if change: 93 cxdata, cydata,maxhours = self._scale_data(timetable_less_change, dates, maxhours, options) 94 chart_params['chd'] += "|%s|%s" % (",".join(cxdata), ",".join(cydata),) 95 96 if change: 97 chart_params['chdl'] = "Current|Less change" 98 99 # Add axes 100 chart_params['chxl'] = "0:|" + "|".join([str(date.day) for date in dates]) + \ 63 101 "|1:|%s|%s" % (dates[0].month, dates[ - 1].month) + \ 64 102 "|2:|%s|%s" % (dates[0].year, dates[ - 1].year) 65 leftaxis = "3,0,%s" % maxhours 66 67 # mark weekends 103 chart_params['chxr'] = "3,0,%s" % maxhours 104 105 # Add weekends 106 downtime = self._mark_downtime(options, dates, xdata, ydata) 107 if downtime: 108 chart_params['chm'] = '|'.join(downtime) 109 110 return Markup("<img src=\"http://chart.apis.google.com/chart?%s\" alt=\"Burndown Chart\" />" % 111 "&".join("%s=%s" % (k,v) for k,v in chart_params.items())) 112 113 def _calculate_timetable(self, options, query_args, req): 114 db = self.env.get_db_cnx() 115 116 estimation_field = self.estimation_field 117 initial_estimation_field = self.initial_estimation_field or estimation_field 118 one_field = (estimation_field == initial_estimation_field) 119 120 # create dictionary with entry for each day of the required time period 121 timetable = {} 122 delta = {} 123 124 current_date = options['startdate'] 125 max_date = current_date 126 while current_date <= options['enddate']: 127 timetable[current_date] = Decimal(0) 128 delta[current_date] = Decimal(0) 129 max_date = current_date 130 current_date += timedelta(days=options.get('interval_days', 1)) 131 132 # Ensure we have today's date in the timetable as well, in case interval_days 133 # caused us to stop before it. 134 if options['today'] <= options['enddate']: 135 timetable[options['today']] = Decimal(0) 136 delta[options['today']] = Decimal(0) 137 138 # get current values for all tickets within milestone and sprints 139 140 query_args[estimation_field + "!"] = None 141 if estimation_field != initial_estimation_field: 142 query_args[initial_estimation_field + "!"] = None 143 query_args['status' + "!"] = None 144 145 tickets = execute_query(self.env, req, query_args) 146 147 # add the open effort for each ticket for each day to the timetable 148 149 for t in tickets: 150 151 # Record the current (latest) status and estimate, and ticket 152 # creation date 153 154 creation_date = t['time'].date() 155 latest_status = t['status'] 156 latest_estimate = self._cast_estimate(t[estimation_field]) 157 if not latest_estimate: 158 latest_estimate = Decimal(0) 159 latest_initial_estimate = self._cast_estimate(t[initial_estimation_field]) 160 if not latest_initial_estimate: 161 latest_initial_estimate = Decimal(0) 162 163 # Fetch change history for status and effort fields for this ticket 164 history_cursor = db.cursor() 165 history_cursor.execute("SELECT " 166 "DISTINCT c.field as field, c.time AS time, c.oldvalue as oldvalue, c.newvalue as newvalue " 167 "FROM ticket t, ticket_change c " 168 "WHERE t.id = %s and c.ticket = t.id and (c.field=%s or c.field=%s or c.field='status')" 169 "ORDER BY c.time ASC", [t['id'], estimation_field, initial_estimation_field]) 170 171 # Build up two dictionaries, mapping dates when effort/status 172 # changed, to the latest effort/status on that day (in case of 173 # several changes on the same day). Also record the oldest known 174 # effort/status, i.e. that at the time of ticket creation 175 176 estimate_history = {} 177 initial_estimate_history = {} 178 status_history = {} 179 180 earliest_estimate = None 181 earliest_initial_estimate = None 182 earliest_status = None 183 184 for row in history_cursor: 185 row_field, row_time, row_old, row_new = row 186 event_date = datetime.fromtimestamp(row_time, utc).date() 187 if row_field == estimation_field: 188 new_value = self._cast_estimate(row_new) 189 if new_value is not None: 190 estimate_history[event_date] = new_value 191 if earliest_estimate is None: 192 earliest_estimate = self._cast_estimate(row_old) 193 # note: not using elif since estimation_field and initial_estimate_field could be the same! 194 if row_field == initial_estimation_field: 195 new_value = self._cast_estimate(row_new) 196 if new_value is not None: 197 initial_estimate_history[event_date] = new_value 198 if earliest_initial_estimate is None: 199 earliest_initial_estimate = self._cast_estimate(row_old) 200 if row_field == 'status': 201 status_history[event_date] = row_new 202 if earliest_status is None: 203 earliest_status = row_old 204 205 # If we don't know already (i.e. the ticket effort/status was 206 # not changed on the creation date), set the effort on the 207 # creation date. It may be that we don't have an "earliest" 208 # estimate/status, because it was never changed. In this case, 209 # use the current (latest) value. 210 211 if not creation_date in estimate_history: 212 if earliest_estimate is not None: 213 estimate_history[creation_date] = earliest_estimate 214 else: 215 estimate_history[creation_date] = latest_estimate 216 217 if not creation_date in initial_estimate_history: 218 if earliest_initial_estimate is not None: 219 initial_estimate_history[creation_date] = earliest_initial_estimate 220 else: 221 initial_estimate_history[creation_date] = latest_initial_estimate 222 223 if not creation_date in status_history: 224 if earliest_status is not None: 225 status_history[creation_date] = earliest_status 226 else: 227 status_history[creation_date] = latest_status 228 229 # There is a risk that the history table is messed up. Trust 230 # the ticket/ticket_custom tables more. 231 232 estimate_history[options['today']] = latest_estimate 233 initial_estimate_history[options['today']] = latest_initial_estimate 234 status_history[options['today']] = latest_status 235 236 # Finally add estimates to the timetable. Treat any period where the 237 # ticket was closed as estimate 0. We need to loop from ticket 238 # creation date, not just from the timetable start date, since 239 # it's possible that the ticket was changed between these two 240 # dates. 241 242 current_date = creation_date 243 current_estimate = None 244 previous_initial_estimate = Decimal(0) 245 is_open = None 246 247 while current_date <= options['enddate']: 248 249 if current_date in status_history: 250 is_open = (status_history[current_date] not in self.closed_states) 251 252 if current_date in estimate_history: 253 current_estimate = estimate_history[current_date] 254 255 if current_date in initial_estimate_history: 256 effort_delta = initial_estimate_history[current_date] - previous_initial_estimate 257 if effort_delta != Decimal(0): 258 previous_initial_estimate = initial_estimate_history[current_date] 259 if current_date > options['startdate'] and current_date in delta: 260 delta[current_date] += effort_delta 261 262 if current_date in timetable and current_date >= options['startdate'] and is_open: 263 timetable[current_date] += current_estimate 264 265 current_date += timedelta(days=1) 266 267 return timetable, delta 268 269 def _scale_data(self, timetable, dates, maxhours, options): 270 # create sorted list of dates 271 272 if maxhours <= Decimal(0): 273 maxhours = Decimal(100) 274 ydata = [str(self._round(timetable[d] * Decimal(100) / maxhours)) 275 for d in dates] 276 xdata = [str(self._round(x * Decimal(100) / (len(dates) - 1))) 277 for x in range((options['enddate'] - options['startdate']).days + 1)] 278 279 # mark ydata invalid that is after today 280 if options['enddate'] > options['today']: 281 remaining_days = (options['enddate'] - options['today']).days; 282 ydata = ydata[: - remaining_days] + ['-1' for x in xrange(0, remaining_days)] 283 284 return xdata, ydata, maxhours 285 286 def _mark_downtime(self, options, dates, xdata, ydata): 287 288 # Only mark weekends if we are showning a day-by-day burndown 289 if options.get('interval_days', 1) != 1: 290 return [] 291 68 292 weekends = [] 69 293 saturday = None … … 84 308 if len(dates) > 0 and dates[ - 1].weekday() == 5: 85 309 weekends.append("R,f1f1f1,0,%s,1.0" % (Decimal(1) - halfday)) 86 87 title = '' 88 if options.get('milestone'): 310 return weekends 311 312 def _get_title(self, options): 313 title = options.get('title', '') 314 if not title and 'milestone' in options: 89 315 title = options['milestone'].split('|')[0] 90 91 return Markup("<img src=\"http://chart.apis.google.com/chart?" 92 "chs=%sx%s" 93 "&chd=t:%s|%s" 94 "&cht=lxy" 95 "&chxt=x,x,x,y" 96 "&chxl=%s" 97 "&chxr=%s" 98 "&chm=%s" 99 "&chg=100.0,100.0,1,0" # create top and right bounding line by using grid 100 "&chco=%s" 101 "&chtt=%s\" " 102 "alt=\'Burndown Chart\' />" 103 % (options['width'], options['height'], 104 ",".join(xdata), ",".join(ydata), bottomaxis, leftaxis, 105 "|".join(weekends), options['color'], title)) 106 107 def _calculate_timetable(self, options, query_args, req): 108 db = self.env.get_db_cnx() 109 110 # create dictionary with entry for each day of the required time period 111 timetable = {} 112 113 current_date = options['startdate'] 114 while current_date <= options['enddate']: 115 timetable[current_date] = Decimal(0) 116 current_date += timedelta(days=1) 117 118 # get current values for all tickets within milestone and sprints 119 120 query_args[self.estimation_field + "!"] = None 121 tickets = execute_query(self.env, req, query_args) 122 123 # add the open effort for each ticket for each day to the timetable 124 125 for t in tickets: 126 127 # Record the current (latest) status and estimate, and ticket 128 # creation date 129 130 creation_date = t['time'].date() 131 latest_status = t['status'] 132 latest_estimate = self._cast_estimate(t[self.estimation_field]) 133 if latest_estimate is None: 134 latest_estimate = Decimal(0) 135 136 # Fetch change history for status and effort fields for this ticket 137 history_cursor = db.cursor() 138 history_cursor.execute("SELECT " 139 "DISTINCT c.field as field, c.time AS time, c.oldvalue as oldvalue, c.newvalue as newvalue " 140 "FROM ticket t, ticket_change c " 141 "WHERE t.id = %s and c.ticket = t.id and (c.field=%s or c.field='status')" 142 "ORDER BY c.time ASC", [t['id'], self.estimation_field]) 143 144 # Build up two dictionaries, mapping dates when effort/status 145 # changed, to the latest effort/status on that day (in case of 146 # several changes on the same day). Also record the oldest known 147 # effort/status, i.e. that at the time of ticket creation 148 149 estimate_history = {} 150 status_history = {} 151 152 earliest_estimate = None 153 earliest_status = None 154 155 for row in history_cursor: 156 row_field, row_time, row_old, row_new = row 157 event_date = datetime.fromtimestamp(row_time, utc).date() 158 if row_field == self.estimation_field: 159 new_value = self._cast_estimate(row_new) 160 if new_value is not None: 161 estimate_history[event_date] = new_value 162 if earliest_estimate is None: 163 earliest_estimate = self._cast_estimate(row_old) 164 elif row_field == 'status': 165 status_history[event_date] = row_new 166 if earliest_status is None: 167 earliest_status = row_old 168 169 # If we don't know already (i.e. the ticket effort/status was 170 # not changed on the creation date), set the effort on the 171 # creation date. It may be that we don't have an "earliest" 172 # estimate/status, because it was never changed. In this case, 173 # use the current (latest) value. 174 175 if not creation_date in estimate_history: 176 if earliest_estimate is not None: 177 estimate_history[creation_date] = earliest_estimate 178 else: 179 estimate_history[creation_date] = latest_estimate 180 if not creation_date in status_history: 181 if earliest_status is not None: 182 status_history[creation_date] = earliest_status 183 else: 184 status_history[creation_date] = latest_status 185 186 # Finally estimates to the timetable. Treat any period where the 187 # ticket was closed as estimate 0. We need to loop from ticket 188 # creation date, not just from the timetable start date, since 189 # it's possible that the ticket was changed between these two 190 # dates. 191 192 current_date = creation_date 193 current_estimate = None 194 is_open = None 195 196 while current_date <= options['enddate']: 197 if current_date in status_history: 198 is_open = (status_history[current_date] not in self.closed_states) 199 200 if current_date in estimate_history: 201 current_estimate = estimate_history[current_date] 202 203 if current_date >= options['startdate'] and is_open: 204 timetable[current_date] += current_estimate 205 206 current_date += timedelta(days=1) 207 208 return timetable 209 210 def _scale_data(self, timetable, options): 211 # create sorted list of dates 212 dates = timetable.keys() 213 dates.sort() 214 215 maxhours = max(timetable.values()) 216 217 if maxhours <= Decimal(0): 218 maxhours = Decimal(100) 219 ydata = [str(self._round(timetable[d] * Decimal(100) / maxhours)) 220 for d in dates] 221 xdata = [str(self._round(x * Decimal(100) / (len(dates) - 1))) 222 for x in range((options['enddate'] - options['startdate']).days + 1)] 223 224 # mark ydata invalid that is after today 225 if options['enddate'] > options['today']: 226 remaining_days = (options['enddate'] - options['today']).days; 227 ydata = ydata[: - remaining_days] + ['-1' for x in xrange(0, remaining_days)] 228 229 return xdata, ydata, maxhours 316 return title 230 317 231 318 def _round(self, decimal_): estimationtoolsplugin/branches/optilude-change-line/estimationtools/tests/burndownchart.py
r4733 r4909 15 15 self.env = EnvironmentStub(default_data = True) 16 16 self.env.config.set('ticket-custom', 'hours_remaining', 'text') 17 self.env.config.set('ticket-custom', 'hours_initial', 'text') 17 18 self.env.config.set('estimation-tools', 'estimation_field', 'hours_remaining') 19 self.env.config.set('estimation-tools', 'initial_estimation_field', 'hours_initial') 18 20 self.req = Mock(href = Href('/'), 19 21 abs_href = Href('http://www.example.com/'), … … 25 27 ticket['summary'] = 'Test Ticket' 26 28 ticket['hours_remaining'] = estimation 29 ticket['hours_initial'] = estimation 27 30 ticket['milestone'] = 'milestone1' 28 31 return ticket.insert() … … 34 37 for key in keys: 35 38 ticket['hours_remaining'] = history[key] 39 ticket.save_changes("me", "testing", datetime.combine(key, datetime.now(utc).timetz())) 40 41 def _change_ticket_initial_estimations(self, id, history): 42 ticket = Ticket(self.env, id) 43 keys = history.keys() 44 keys.sort() 45 for key in keys: 46 ticket['hours_initial'] = history[key] 36 47 ticket.save_changes("me", "testing", datetime.combine(key, datetime.now(utc).timetz())) 37 48 … … 55 66 db = self.env.get_db_cnx() 56 67 options, query_args = parse_options(db, "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28", {}) 57 timetable = chart._calculate_timetable(options, query_args, self.req) 58 xdata, ydata, maxhours = chart._scale_data(timetable, options) 68 timetable, _ = chart._calculate_timetable(options, query_args, self.req) 69 dates = sorted(timetable.keys()) 70 xdata, ydata, maxhours = chart._scale_data(timetable, dates, Decimal(0), options) 59 71 self.assertEqual(xdata, ['0.00', '12.50', '25.00', '37.50', '50.00', '62.50', '75.00', '87.50', '100.00']) 60 72 self.assertEqual(ydata, ['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']) … … 71 83 day2 = day1 + timedelta(days=1) 72 84 day3 = day2 + timedelta(days=1) 73 options = {'today': day3, 'startdate': day1, 'enddate': day3 , 'closedstates': ['closed']}85 options = {'today': day3, 'startdate': day1, 'enddate': day3} 74 86 query_args = {'milestone': "milestone1"} 75 87 self._insert_ticket('10') 76 timetable = chart._calculate_timetable(options, query_args, self.req)88 timetable, _ = chart._calculate_timetable(options, query_args, self.req) 77 89 self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(10), day3: Decimal(10)}) 78 90 … … 82 94 day2 = day1 + timedelta(days=1) 83 95 day3 = day2 + timedelta(days=1) 84 options = {'today': day3, 'startdate': day1, 'enddate': day3 , 'closedstates': ['closed']}96 options = {'today': day3, 'startdate': day1, 'enddate': day3} 85 97 self._insert_ticket('10') 86 timetable = chart._calculate_timetable(options, {}, self.req)98 timetable, _ = chart._calculate_timetable(options, {}, self.req) 87 99 self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(10), day3: Decimal(10)}) 88 100 … … 92 104 day2 = day1 + timedelta(days=1) 93 105 day3 = day2 + timedelta(days=1) 94 options = {'today': day3, 'startdate': day1, 'enddate': day3 , 'closedstates': ['closed']}106 options = {'today': day3, 'startdate': day1, 'enddate': day3} 95 107 query_args = {'milestone': "milestone1"} 96 108 ticket1 = self._insert_ticket('10') 97 109 self._change_ticket_estimations(ticket1, {day2:'5', day3:'0'}) 98 110 99 timetable = chart._calculate_timetable(options, query_args, self.req)111 timetable, _ = chart._calculate_timetable(options, query_args, self.req) 100 112 self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(5), day3: Decimal(0)}) 101 113 … … 105 117 day2 = day1 + timedelta(days=1) 106 118 day3 = day2 + timedelta(days=1) 107 options = {'today': day3, 'startdate': day1, 'enddate': day3 , 'closedstates': ['closed']}119 options = {'today': day3, 'startdate': day1, 'enddate': day3} 108 120 query_args = {'milestone': "milestone1"} 109 121 ticket1 = self._insert_ticket('10') 110 122 self._change_ticket_estimations(ticket1, {day2:'5'}) 111 123 self._change_ticket_states(ticket1, {day3: 'closed'}) 112 timetable = chart._calculate_timetable(options, query_args, self.req)124 timetable, _ = chart._calculate_timetable(options, query_args, self.req) 113 125 self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(5), day3: Decimal(0)}) 114 126 … … 118 130 day2 = day1 + timedelta(days=1) 119 131 day3 = day2 + timedelta(days=1) 120 options = {'today': day3, 'startdate': day1, 'enddate': day3 , 'closedstates': ['closed']}132 options = {'today': day3, 'startdate': day1, 'enddate': day3} 121 133 query_args = {'milestone': "milestone1"} 122 134 ticket1 = self._insert_ticket('10') 123 135 self._change_ticket_states(ticket1, {day2: 'closed'}) 124 136 self._change_ticket_estimations(ticket1, {day3:'5'}) 125 timetable = chart._calculate_timetable(options, query_args, self.req)137 timetable, _ = chart._calculate_timetable(options, query_args, self.req) 126 138 self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(0), day3: Decimal(0)}) 127 139 … … 132 144 day3 = day2 + timedelta(days=1) 133 145 day4 = day3 + timedelta(days=1) 134 options = {'today': day4, 'startdate': day1, 'enddate': day4 , 'closedstates': ['closed']}146 options = {'today': day4, 'startdate': day1, 'enddate': day4} 135 147 query_args = {'milestone': "milestone1"} 136 148 ticket1 = self._insert_ticket('10') 137 149 self._change_ticket_estimations(ticket1, {day3:'5'}) 138 150 self._change_ticket_states(ticket1, {day2: 'closed', day4: 'new'}) 139 timetable = chart._calculate_timetable(options, query_args, self.req)151 timetable, _ = chart._calculate_timetable(options, query_args, self.req) 140 152 self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(0), day3: Decimal(0), day4: Decimal(5)}) 141 153 … … 145 157 day2 = day1 + timedelta(days=1) 146 158 day3 = day2 + timedelta(days=1) 147 options = {'today': day3, 'startdate': day1, 'enddate': day3 , 'closedstates': ['closed']}159 options = {'today': day3, 'startdate': day1, 'enddate': day3} 148 160 query_args = {'milestone': "milestone1"} 149 161 ticket1 = self._insert_ticket('10') … … 152 164 self._change_ticket_estimations(ticket2, {day2:'1', day3:'2'}) 153 165 154 timetable = chart._calculate_timetable(options, query_args, self.req)166 timetable, _ = chart._calculate_timetable(options, query_args, self.req) 155 167 self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(6), day3: Decimal(2)}) 156 168 … … 161 173 day3 = day2 + timedelta(days=1) 162 174 day4 = day3 + timedelta(days=1) 163 options = {'today': day 3, 'startdate': day1, 'enddate': day3, 'closedstates': ['closed']}175 options = {'today': day4, 'startdate': day1, 'enddate': day3} 164 176 query_args = {'milestone': "milestone1"} 165 177 ticket1 = self._insert_ticket('10') 166 178 self._change_ticket_estimations(ticket1, {day2:'5', day4:''}) 167 179 168 timetable = chart._calculate_timetable(options, query_args, self.req)180 timetable, _ = chart._calculate_timetable(options, query_args, self.req) 169 181 self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(5), day3: Decimal(5)}) 170 182 … … 174 186 day2 = day1 + timedelta(days=1) 175 187 day3 = day2 + timedelta(days=1) 176 options = {'today': day3, 'startdate': day1, 'enddate': day3 , 'closedstates': ['closed']}188 options = {'today': day3, 'startdate': day1, 'enddate': day3} 177 189 query_args = {'milestone': "milestone1"} 178 190 ticket1 = self._insert_ticket('10') 179 191 self._change_ticket_estimations(ticket1, {day2: 'IGNOREME', day3:'5'}) 180 timetable = chart._calculate_timetable(options, query_args, self.req)192 timetable, _ = chart._calculate_timetable(options, query_args, self.req) 181 193 self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(10), day3: Decimal(5)}) 194 195 def test_calculate_delta_no_new_or_changed(self): 196 chart = BurndownChart(self.env) 197 day1 = datetime.now(utc).date() 198 day2 = day1 + timedelta(days=1) 199 day3 = day2 + timedelta(days=1) 200 options = {'today': day3, 'startdate': day1, 'enddate': day3, 'change': True} 201 query_args = {'milestone': "milestone1"} 202 ticket1 = self._insert_ticket('10') 203 self._change_ticket_estimations(ticket1, {day2:'5', day3:''}) 204 ticket2 = self._insert_ticket('0') 205 self._change_ticket_estimations(ticket2, {day2:'1', day3:'2'}) 206 207 timetable, delta = chart._calculate_timetable(options, query_args, self.req) 208 self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(6), day3: Decimal(2)}) 209 self.assertEqual(delta, {day1: Decimal(0), day2: Decimal(0), day3: Decimal(0)}) 210 211 def test_calculate_delta_with_changed_estimates(self): 212 chart = BurndownChart(self.env) 213 day1 = datetime.now(utc).date() 214 day2 = day1 + timedelta(days=1) 215 day3 = day2 + timedelta(days=1) 216 options = {'today': day3, 'startdate': day1, 'enddate': day3, 'change': True} 217 query_args = {'milestone': "milestone1"} 218 ticket1 = self._insert_ticket('10') 219 ticket2 = self._insert_ticket('5') 220 self._change_ticket_initial_estimations(ticket1, {day2:'8'}) # -2 221 self._change_ticket_initial_estimations(ticket2, {day3:'8'}) # +3 222 import time; time.sleep(1) # Avoid time resolution problem 223 self._change_ticket_estimations(ticket2, {day2:'1', day3:'2'}) 224 225 timetable, delta = chart._calculate_timetable(options, query_args, self.req) 226 self.assertEqual(timetable, {day1: Decimal(15), day2: Decimal(11), day3: Decimal(12)}) 227 self.assertEqual(delta, {day1: Decimal(0), day2: Decimal(-2), day3: Decimal(3)}) 228 229 def test_date_intervals(self): 230 chart = BurndownChart(self.env) 231 day1 = datetime.now(utc).date() 232 day2 = day1 + timedelta(days=1) 233 day3 = day2 + timedelta(days=1) 234 day4 = day3 + timedelta(days=1) 235 day5 = day4 + timedelta(days=1) 236 options = {'today': day5, 'startdate': day1, 'enddate': day5, 'interval_days': 2} 237 query_args = {'milestone': "milestone1"} 238 ticket1 = self._insert_ticket('10') 239 ticket2 = self._insert_ticket('5') 240 self._change_ticket_estimations(ticket2, {day3:'1', day4:'2', day5:'3'}) 241 242 timetable, delta = chart._calculate_timetable(options, query_args, self.req) 243 244 self.assertEqual(timetable, {day1: Decimal(15), day3: Decimal(11), day5: Decimal(13)}) 245 self.assertEqual(delta, {day1: Decimal(0), day3: Decimal(0), day5: Decimal(0)}) 246 247 def test_date_intervals_always_includes_today(self): 248 chart = BurndownChart(self.env) 249 day1 = datetime.now(utc).date() 250 day2 = day1 + timedelta(days=1) 251 day3 = day2 + timedelta(days=1) 252 day4 = day3 + timedelta(days=1) 253 day5 = day4 + timedelta(days=1) 254 day6 = day4 + timedelta(days=1) 255 options = {'today': day5, 'startdate': day1, 'enddate': day5, 'interval_days': 2} 256 query_args = {'milestone': "milestone1"} 257 ticket1 = self._insert_ticket('10') 258 ticket2 = self._insert_ticket('5') 259 self._change_ticket_estimations(ticket2, {day3:'1', day4:'2', day5:'3',day6:'4'}) 260 261 timetable, delta = chart._calculate_timetable(options, query_args, self.req) 262 263 self.assertEqual(timetable, {day1: Decimal(15), day3: Decimal(11), day5: Decimal(13), day6: Decimal(14)}) 264 self.assertEqual(delta, {day1: Decimal(0), day3: Decimal(0), day5: Decimal(0), day6: Decimal(0)}) estimationtoolsplugin/branches/optilude-change-line/estimationtools/tests/workloadchart.py
r4448 r4909 32 32 result = workload_chart.render_macro(self.req, "", "milestone=milestone1") 33 33 self.assertEqual(result, u'<img src="http://chart.apis.google.com/chart?chs=400x100&'\ 34 'chd=t:10,30,20&cht=p3&chtt=Workload 60h ( 0workdays left)&'\34 'chd=t:10,30,20&cht=p3&chtt=Workload 60h (1 workdays left)&'\ 35 35 'chl=A 10h|C 30h|B 20h&chco=ff9900" alt=\'Workload Chart\' />') 36 36 … … 44 44 result = workload_chart.render_macro(self.req, "", "milestone=milestone1") 45 45 self.assertEqual(result, u'<img src="http://chart.apis.google.com/chart?chs=400x100&'\ 46 'chd=t:10,30,20&cht=p3&chtt=Workload 60h ( 0workdays left)&'\46 'chd=t:10,30,20&cht=p3&chtt=Workload 60h (1 workdays left)&'\ 47 47 'chl=A 10h|C 30h|B 20h&chco=ff9900" alt=\'Workload Chart\' />' ) estimationtoolsplugin/branches/optilude-change-line/estimationtools/utils.py
r4893 r4909 6 6 from trac.ticket.query import Query 7 7 8 AVAILABLE_OPTIONS = ['startdate', 'enddate', 'today', 'width', 'height', 'color' ]8 AVAILABLE_OPTIONS = ['startdate', 'enddate', 'today', 'width', 'height', 'color', 'title', 'change', 'interval_days'] 9 9 10 10 def get_estimation_field(): … … 12 12 doc="""Defines what custom field should be used to calculate estimation charts. 13 13 Defaults to 'estimatedhours'""") 14 15 def get_initial_estimation_field(): 16 return Option('estimation-tools', 'initial_estimation_field', '', 17 doc="""When calculating project change, use this field instead of the one set for estimation_field. 18 Defaults to be the same as 'estimation_field'""") 14 19 15 20 def get_closed_states(): … … 62 67 options['today'] = datetime.now().date() 63 68 69 if 'interval_days' in options: 70 try: 71 options['interval_days'] = int(options['interval_days']) 72 except (ValueError, TypeError): 73 options['interval_days'] = 1 74 else: 75 options['interval_days'] = 1 76 77 if 'change' in options and options['change'].lower() not in ('false', '0'): 78 options['change'] = True 79 else: 80 options['change'] = False 81 64 82 # all arguments that are no key should be treated as part of the query 65 83 query_args = {} … … 70 88 71 89 def execute_query(env, req, query_args): 72 # set maximum number of returned tickets to 0 to get all tickets at once73 query_args['max'] = 074 90 query_string = '&'.join(['%s=%s' % item for item in query_args.iteritems()]) 75 91 query = Query.from_string(env, query_string)
