def render_macro(self, req, name, content): db = self.env.get_db_cnx() # prepare options options, query_args = parse_options(db, content, copy.copy(DEFAULT_OPTIONS)) query_args[self.estimation_field + "!"] = None tickets = execute_query(self.env, req, query_args) sum = 0.0 estimations = {} for ticket in tickets: try: estimation = float(ticket[self.estimation_field]) owner = ticket['owner'] sum += estimation if estimations.has_key(owner): estimations[owner] += estimation else: estimations[owner] = estimation except: pass estimations_string = [] labels = [] for owner, estimation in estimations.iteritems(): labels.append( "%s %s%s" % (owner, str(int(estimation)), self.estimation_suffix)) estimations_string.append(str(int(estimation))) # Title title = 'Workload' # calculate remaining work time if options.get('today') and options.get('enddate'): currentdate = options['today'] day = timedelta(days=1) days_remaining = 0 while currentdate <= options['enddate']: if currentdate.weekday() < 5: days_remaining += 1 currentdate += day title += ' %s%s (%s workdays left)' % ( int(sum), self.estimation_suffix, days_remaining) return Markup( "<img src=\"http://chart.apis.google.com/chart?" "chs=%sx%s" "&chd=t:%s" "&cht=p3" "&chtt=%s" "&chl=%s" "&chco=%s\" " "alt=\'Workload Chart\' />" % (options['width'], options['height'], ",".join(estimations_string), title, "|".join(labels), options['color']))
def render_macro(self, req, name, content): db = self.env.get_db_cnx() # prepare options options, query_args = parse_options(db, content, copy.copy(DEFAULT_OPTIONS)) query_args[self.estimation_field + "!"] = None tickets = execute_query(self.env, req, query_args) sum = 0.0 estimations = {} for ticket in tickets: try: estimation = float(ticket[self.estimation_field]) owner = ticket['owner'] sum += estimation if estimations.has_key(owner): estimations[owner] += estimation else: estimations[owner] = estimation except: pass estimations_string = [] labels = [] for owner, estimation in estimations.iteritems(): labels.append("%s %s%s" % (owner, str(int(estimation)), self.estimation_suffix)) estimations_string.append(str(int(estimation))) # Title title = 'Workload' # calculate remaining work time if options.get('today') and options.get('enddate'): currentdate = options['today'] day = timedelta(days=1) days_remaining = 0 while currentdate <= options['enddate']: if currentdate.weekday() < 5: days_remaining += 1 currentdate += day title += ' %s%s (%s workdays left)' % (int(sum), self.estimation_suffix, days_remaining) return Markup("<img src=\"http://chart.apis.google.com/chart?" "chs=%sx%s" "&chd=t:%s" "&cht=p3" "&chtt=%s" "&chl=%s" "&chco=%s\" " "alt=\'Workload Chart\' />" % (options['width'], options['height'], ",".join(estimations_string), title, "|".join(labels), options['color']))
def render_macro(self, req, name, content): _, options = parse_args(content, strict=False) # we have to add custom estimation field to query so that field is added to # resulting ticket list options[self.estimation_field + "!"] = None tickets = execute_query(self.env, req, options) sum = 0.0 for t in tickets: try: sum += float(t[self.estimation_field]) except: pass return "%s" % int(sum)
def expand_macro(self, formatter, name, content): req = formatter.req _ignore, options = parse_args(content, strict=False) # we have to add custom estimated field to query so that field is added to # resulting ticket list options[self.estimation_field + "!"] = None tickets = execute_query(self.env, req, options) sum = 0.0 for t in tickets: try: sum += float(t[self.estimation_field]) except: pass return "%g" % round(sum, 2)
def expand_macro(self, formatter, name, content): req = formatter.req _ignore, options = parse_args(content, strict=False) # we have to add custom remaining field to query so that field is added to # resulting ticket list options[self.remaining_field + "!"] = None # ignore closed tickets options['status!'] = "|".join(self.closed_states) tickets = execute_query(self.env, req, options) sum = 0.0 for t in tickets: try: sum += float(t[self.remaining_field]) except: pass return "%g" % round(sum, 2)
def _calculate_timetable(self, options, query_args, req): db = self.env.get_db_cnx() # create dictionary with entry for each day of the required time period timetable = {} current_date = options['startdate'] while current_date <= options['enddate']: timetable[current_date] = Decimal(0) current_date += timedelta(days=1) # get current values for all tickets within milestone and sprints query_args[self.estimation_field + "!"] = None tickets = execute_query(self.env, req, query_args) # add the open effort for each ticket for each day to the timetable for t in tickets: # Record the current (latest) status and estimate, and ticket # creation date creation_date = t['time'].date() latest_status = t['status'] latest_estimate = self._cast_estimate(t[self.estimation_field]) if latest_estimate is None: latest_estimate = Decimal(0) # Fetch change history for status and effort fields for this ticket history_cursor = db.cursor() history_cursor.execute( "SELECT " "DISTINCT c.field as field, c.time AS time, c.oldvalue as oldvalue, c.newvalue as newvalue " "FROM ticket t, ticket_change c " "WHERE t.id = %s and c.ticket = t.id and (c.field=%s or c.field='status')" "ORDER BY c.time ASC", [t['id'], self.estimation_field]) # Build up two dictionaries, mapping dates when effort/status # changed, to the latest effort/status on that day (in case of # several changes on the same day). Also record the oldest known # effort/status, i.e. that at the time of ticket creation estimate_history = {} status_history = {} earliest_estimate = None earliest_status = None for row in history_cursor: row_field, row_time, row_old, row_new = row event_date = from_timestamp(row_time).date() if row_field == self.estimation_field: new_value = self._cast_estimate(row_new) if new_value is not None: estimate_history[event_date] = new_value if earliest_estimate is None: earliest_estimate = self._cast_estimate(row_old) elif row_field == 'status': status_history[event_date] = row_new if earliest_status is None: earliest_status = row_old # If we don't know already (i.e. the ticket effort/status was # not changed on the creation date), set the effort on the # creation date. It may be that we don't have an "earliest" # estimate/status, because it was never changed. In this case, # use the current (latest) value. if not creation_date in estimate_history: if earliest_estimate is not None: estimate_history[creation_date] = earliest_estimate else: estimate_history[creation_date] = latest_estimate if not creation_date in status_history: if earliest_status is not None: status_history[creation_date] = earliest_status else: status_history[creation_date] = latest_status # Finally estimates to the timetable. Treat any period where the # ticket was closed as estimate 0. We need to loop from ticket # creation date, not just from the timetable start date, since # it's possible that the ticket was changed between these two # dates. current_date = creation_date current_estimate = None is_open = None while current_date <= options['enddate']: if current_date in status_history: is_open = (status_history[current_date] not in self.closed_states) if current_date in estimate_history: current_estimate = estimate_history[current_date] if current_date >= options['startdate'] and is_open: timetable[current_date] += current_estimate current_date += timedelta(days=1) return timetable
def expand_macro(self, formatter, name, content, args=None): req = formatter.req # prepare options options, query_args = parse_options(self.env, content, copy.copy(DEFAULT_OPTIONS)) query_args[self.remaining_field + "!"] = None tickets = execute_query(self.env, req, query_args) sum = 0.0 estimations = {} for ticket in tickets: if ticket['status'] in self.closed_states: continue try: estimation = float(ticket[self.remaining_field]) owner = ticket['owner'] sum += estimation if owner in estimations: estimations[owner] += estimation else: estimations[owner] = estimation except: pass estimations_string = [] labels = [] for owner, estimation in estimations.iteritems(): # Note: Unconditional obfuscation of owner in case it represents # an email adress, and as the chart API doesn't support SSL # (plain http transfer only, from either client or server). labels.append("%s %g%s" % (obfuscate_email_address(owner), round(estimation, 2), self.estimation_suffix)) estimations_string.append(str(int(estimation))) # Title title = 'Workload' # calculate remaining work time if options.get('today') and options.get('enddate'): currentdate = options['today'] day = timedelta(days=1) days_remaining = 0 while currentdate <= options['enddate']: if currentdate.weekday() < 5: days_remaining += 1 currentdate += day title += ' %g%s (~%s workdays left)' % (round(sum, 2), self.estimation_suffix, days_remaining) chart_args = unicode_urlencode( {'chs': '%sx%s' % (options['width'], options['height']), 'chf': 'bg,s,00000000', 'chd': 't:%s' % ",".join(estimations_string), 'cht': 'p3', 'chtt': title, 'chl': "|".join(labels), 'chco': options['color']}) self.log.debug("WorkloadChart data: %s", chart_args) if self.serverside_charts: return tag.image( src="%s?data=%s" % (req.href.estimationtools('chart'), unicode_quote(chart_args)), alt="Workload Chart (server)") else: return tag.image( src="http://chart.googleapis.com/chart?%s" % chart_args, alt="Workload Chart (client)")
def expand_macro(self, formatter, name, content): req = formatter.req db = self.env.get_db_cnx() # prepare options options, query_args = parse_options(db, content, copy.copy(DEFAULT_OPTIONS)) query_args[self.estimation_field + "!"] = None query_args['col'] = '|'.join([self.totalhours_field, 'owner']) tickets = execute_query(self.env, req, query_args) sum = 0.0 estimations = {} for ticket in tickets: if ticket['status'] in self.closed_states: continue try: estimation = float(ticket[self.estimation_field]) - float(ticket[self.totalhours_field]) owner = ticket['owner'] sum += estimation if estimations.has_key(owner): estimations[owner] += estimation else: estimations[owner] = estimation except: pass data = [] data.append(['Owner', 'Workload']) for owner, estimation in estimations.iteritems(): estimation = max(0, estimation) label = "%s %g%s" % (obfuscate_email_address(owner), round(estimation, 2), self.estimation_suffix) data.append([label, float(estimation)]) # Title title = 'Workload' # calculate remaining work time if options.get('today') and options.get('enddate'): currentdate = options['today'] day = timedelta(days=1) days_remaining = 0 while currentdate <= options['enddate']: if currentdate.weekday() < 5: days_remaining += 1 currentdate += day title += ' %g%s (~%s workdays left)' % (round(sum, 2), self.estimation_suffix, days_remaining) element_id = 'chart-%d' % random.randint(0, 0xffffffff) args = { 'containerId': element_id, 'chartType': 'PieChart', 'options': { 'width': int(options['width']), 'height': int(options['height']), 'title': title, 'legend': { 'position': 'labeled' }, 'pieSliceText': 'none', 'tooltip': 'percentage', }, } script = "EstimationCharts.push(function() {\n" script += 'var data=' + to_json(data) + ";\n" script += 'var args=' + to_json(args) + ";\n" script += 'DrawWorkloadChart(data, args);' script += '});' return tag.div(tag.div(id=element_id), tag.script(script))
def _calculate_timetable_spent(self, options, query_args, req): # create dictionary with entry for each day of the required time period timetable = {} current_date = options['startdate'] while current_date <= options['enddate']: timetable[current_date] = Decimal(0) current_date += timedelta(days=1) # get current values for all tickets within milestone and sprints query_args[self.spent_field + "!"] = None tickets = execute_query(self.env, req, query_args) # add the open effort for each ticket for each day to the timetable for t in tickets: # Record the current (latest) status and estimate, and ticket # creation date creation_date = t['time'].date() latest_status = t['status'] latest_estimate = self._cast_estimate(t[self.spent_field]) if latest_estimate is None: latest_estimate = Decimal(0) # Fetch change history for status and effort fields for this ticket # Build up two dictionaries, mapping dates when effort/status # changed, to the latest effort/status on that day (in case of # several changes on the same day). Also record the oldest known # effort/status, i.e. that at the time of ticket creation estimate_history = {} status_history = {} earliest_estimate = None earliest_status = None for row in self.env.db_query(""" SELECT DISTINCT c.field AS field, c.time AS time, c.oldvalue AS oldvalue, c.newvalue AS newvalue FROM ticket t, ticket_change c WHERE t.id = %s AND c.ticket = t.id AND (c.field=%s OR c.field='status') ORDER BY c.time ASC """, [t['id'], self.spent_field]): row_field, row_time, row_old, row_new = row event_date = from_utimestamp(row_time).date() if row_field == self.spent_field: new_value = self._cast_estimate(row_new) if new_value is not None: estimate_history[event_date] = new_value if earliest_estimate is None: earliest_estimate = self._cast_estimate(row_old) elif row_field == 'status': status_history[event_date] = row_new if earliest_status is None: earliest_status = row_old # If we don't know already (i.e. the ticket effort/status was # not changed on the creation date), set the effort on the # creation date. It may be that we don't have an "earliest" # estimate/status, because it was never changed. In this case, # use the current (latest) value. if creation_date not in estimate_history: if earliest_estimate is not None: estimate_history[creation_date] = earliest_estimate else: estimate_history[creation_date] = latest_estimate if creation_date not in status_history: if earliest_status is not None: status_history[creation_date] = earliest_status else: status_history[creation_date] = latest_status # Finally estimates to the timetable. Treat any period where the # ticket was closed as estimate 0. We need to loop from ticket # creation date, not just from the timetable start date, since # it's possible that the ticket was changed between these two # dates. current_date = creation_date current_estimate = None is_open = None while current_date <= options['enddate']: if current_date in status_history: is_open = ( status_history[current_date] not in self.closed_states) if current_date in estimate_history: current_estimate = estimate_history[current_date] if current_date >= options['startdate'] and is_open: timetable[current_date] += current_estimate current_date += timedelta(days=1) return timetable
def expand_macro(self, formatter, name, content): req = formatter.req db = self.env.get_db_cnx() # prepare options options, query_args = parse_options(db, content, copy.copy(DEFAULT_OPTIONS)) query_args[self.estimation_field + "!"] = None tickets = execute_query(self.env, req, query_args) sum = 0.0 estimations = {} for ticket in tickets: if ticket['status'] in self.closed_states: continue try: estimation = float(ticket[self.estimation_field] or 0.0) if options.get('remainingworkload'): completion_cursor = db.cursor() completion_cursor.execute( "SELECT t.value AS totalhours, c.value AS complete, d.value AS due_close FROM ticket tk LEFT JOIN ticket_custom t ON (tk.id = t.ticket AND t.name = 'totalhours') LEFT JOIN ticket_custom c ON (tk.id = c.ticket AND c.name = 'complete') LEFT JOIN ticket_custom d ON (tk.id = d.ticket AND d.name = 'due_close') WHERE tk.id = %s" % ticket['id']) for row in completion_cursor: ticket['totalhours'], ticket['complete'], ticket[ 'due_close'] = row break # skip ticket ticket if due date is later than 'enddate': if options.get('showdueonly'): if not ticket['due_close']: continue # skip tickets with empty ETA when in 'showdueonly' mode due_close = parse_date(ticket['due_close'], ["%Y/%m/%d"]) startdate = options.get('startdate') enddate = options.get('enddate') if startdate and startdate > due_close: continue # skip tickets with ETA in the past if enddate and enddate < due_close: continue # skip tickets with ETA in the future pass totalhours = float(ticket['totalhours'] or 0.0) completed = (float(ticket['complete'] or 0.0) / 100) * estimation completed_hours = min(estimation, max(totalhours, completed)) estimation -= completed_hours pass owner = ticket['owner'] sum += estimation if estimations.has_key(owner): estimations[owner] += estimation else: estimations[owner] = estimation except: raise # Title title = 'Workload' days_remaining = None # calculate remaining work time if options.get('today') and options.get('enddate'): currentdate = options['today'] day = timedelta(days=1) days_remaining = 0 while currentdate <= options['enddate']: if currentdate.weekday() < 5: days_remaining += 1 currentdate += day title += ' %g%s (~%s workdays left)' % (round( sum, 2), self.estimation_suffix, days_remaining) estimations_string = [] labels = [] workhoursperday = max(float(options.get('workhoursperday')), 0.0) chts = '000000' for owner, estimation in estimations.iteritems(): # Note: Unconditional obfuscation of owner in case it represents # an email adress, and as the chart API doesn't support SSL # (plain http transfer only, from either client or server). label = "%s %g%s" % (obfuscate_email_address(owner), round(estimation, 2), self.estimation_suffix) if days_remaining != None: user_remaining_hours = days_remaining * workhoursperday if not user_remaining_hours or (estimation / user_remaining_hours) > 1: label = "%s (~%g hours left)!" % ( label, round(user_remaining_hours, 2) ) # user does not have enough hours left chts = 'FF0000' # set chart title style to red pass pass labels.append(label) estimations_string.append(str(int(estimation))) pass chart_args = unicode_urlencode({ 'chs': '%sx%s' % (options['width'], options['height']), 'chf': 'bg,s,00000000', 'chd': 't:%s' % ",".join(estimations_string), 'cht': 'p3', 'chtt': title, 'chts': chts, 'chl': "|".join(labels), 'chco': options['color'] }) self.log.debug("WorkloadChart data: %s" % repr(chart_args)) if self.serverside_charts: return tag.image( src="%s?data=%s" % (req.href.estimationtools('chart'), unicode_quote(chart_args)), alt="Workload Chart (server)") else: return tag.image(src="https://chart.googleapis.com/chart?%s" % chart_args, alt="Workload Chart (client)")
def _calculate_timetable(self, options, query_args, req): db = self.env.get_db_cnx() estimation_field = self.estimation_field initial_estimation_field = self.initial_estimation_field or estimation_field one_field = (estimation_field == initial_estimation_field) # create dictionary with entry for each day of the required time period timetable = {} delta = {} current_date = options['startdate'] max_date = current_date while current_date <= options['enddate']: timetable[current_date] = Decimal(0) delta[current_date] = Decimal(0) max_date = current_date current_date += timedelta(days=options.get('interval_days', 1)) # Ensure we have today's date in the timetable as well, in case interval_days # caused us to stop before it. if options['today'] <= options['enddate']: timetable[options['today']] = Decimal(0) delta[options['today']] = Decimal(0) # get current values for all tickets within milestone and sprints query_args[estimation_field + "!"] = None if estimation_field != initial_estimation_field: query_args[initial_estimation_field + "!"] = None query_args['status' + "!"] = None tickets = execute_query(self.env, req, query_args) # add the open effort for each ticket for each day to the timetable for t in tickets: # Record the current (latest) status and estimate, and ticket # creation date creation_date = t['time'].date() latest_status = t['status'] latest_estimate = self._cast_estimate(t[estimation_field]) if not latest_estimate: latest_estimate = Decimal(0) latest_initial_estimate = self._cast_estimate( t[initial_estimation_field]) if not latest_initial_estimate: latest_initial_estimate = Decimal(0) # Fetch change history for status and effort fields for this ticket history_cursor = db.cursor() history_cursor.execute( "SELECT " "DISTINCT c.field as field, c.time AS time, c.oldvalue as oldvalue, c.newvalue as newvalue " "FROM ticket t, ticket_change c " "WHERE t.id = %s and c.ticket = t.id and (c.field=%s or c.field=%s or c.field='status')" "ORDER BY c.time ASC", [t['id'], estimation_field, initial_estimation_field]) # Build up two dictionaries, mapping dates when effort/status # changed, to the latest effort/status on that day (in case of # several changes on the same day). Also record the oldest known # effort/status, i.e. that at the time of ticket creation estimate_history = {} initial_estimate_history = {} status_history = {} earliest_estimate = None earliest_initial_estimate = None earliest_status = None for row in history_cursor: row_field, row_time, row_old, row_new = row event_date = datetime.fromtimestamp(row_time, utc).date() if row_field == estimation_field: new_value = self._cast_estimate(row_new) if new_value is not None: estimate_history[event_date] = new_value if earliest_estimate is None: earliest_estimate = self._cast_estimate(row_old) # note: not using elif since estimation_field and initial_estimate_field could be the same! if row_field == initial_estimation_field: new_value = self._cast_estimate(row_new) if new_value is not None: initial_estimate_history[event_date] = new_value if earliest_initial_estimate is None: earliest_initial_estimate = self._cast_estimate( row_old) if row_field == 'status': status_history[event_date] = row_new if earliest_status is None: earliest_status = row_old # If we don't know already (i.e. the ticket effort/status was # not changed on the creation date), set the effort on the # creation date. It may be that we don't have an "earliest" # estimate/status, because it was never changed. In this case, # use the current (latest) value. if not creation_date in estimate_history: if earliest_estimate is not None: estimate_history[creation_date] = earliest_estimate else: estimate_history[creation_date] = latest_estimate if not creation_date in initial_estimate_history: if earliest_initial_estimate is not None: initial_estimate_history[ creation_date] = earliest_initial_estimate else: initial_estimate_history[ creation_date] = latest_initial_estimate if not creation_date in status_history: if earliest_status is not None: status_history[creation_date] = earliest_status else: status_history[creation_date] = latest_status # There is a risk that the history table is messed up. Trust # the ticket/ticket_custom tables more. estimate_history[options['today']] = latest_estimate initial_estimate_history[ options['today']] = latest_initial_estimate status_history[options['today']] = latest_status # Finally add estimates to the timetable. Treat any period where the # ticket was closed as estimate 0. We need to loop from ticket # creation date, not just from the timetable start date, since # it's possible that the ticket was changed between these two # dates. current_date = creation_date current_estimate = None previous_initial_estimate = Decimal(0) is_open = None while current_date <= options['enddate']: if current_date in status_history: is_open = (status_history[current_date] not in self.closed_states) if current_date in estimate_history: current_estimate = estimate_history[current_date] if current_date in initial_estimate_history: effort_delta = initial_estimate_history[ current_date] - previous_initial_estimate if effort_delta != Decimal(0): previous_initial_estimate = initial_estimate_history[ current_date] if current_date > options[ 'startdate'] and current_date in delta: delta[current_date] += effort_delta if current_date in timetable and current_date >= options[ 'startdate'] and is_open: timetable[current_date] += current_estimate current_date += timedelta(days=1) return timetable, delta
def _calculate_timetable(self, options, query_args, req): db = self.env.get_db_cnx() estimation_field = self.estimation_field initial_estimation_field = self.initial_estimation_field or estimation_field one_field = (estimation_field == initial_estimation_field) # create dictionary with entry for each day of the required time period timetable = {} delta = {} current_date = options['startdate'] max_date = current_date while current_date <= options['enddate']: timetable[current_date] = Decimal(0) delta[current_date] = Decimal(0) max_date = current_date current_date += timedelta(days=options.get('interval_days', 1)) # Ensure we have today's date in the timetable as well, in case interval_days # caused us to stop before it. if options['today'] <= options['enddate']: timetable[options['today']] = Decimal(0) delta[options['today']] = Decimal(0) # get current values for all tickets within milestone and sprints query_args[estimation_field + "!"] = None if estimation_field != initial_estimation_field: query_args[initial_estimation_field + "!"] = None query_args['status' + "!"] = None tickets = execute_query(self.env, req, query_args) # add the open effort for each ticket for each day to the timetable for t in tickets: # Record the current (latest) status and estimate, and ticket # creation date creation_date = t['time'].date() latest_status = t['status'] latest_estimate = self._cast_estimate(t[estimation_field]) if not latest_estimate: latest_estimate = Decimal(0) latest_initial_estimate = self._cast_estimate(t[initial_estimation_field]) if not latest_initial_estimate: latest_initial_estimate = Decimal(0) # Fetch change history for status and effort fields for this ticket history_cursor = db.cursor() history_cursor.execute("SELECT " "DISTINCT c.field as field, c.time AS time, c.oldvalue as oldvalue, c.newvalue as newvalue " "FROM ticket t, ticket_change c " "WHERE t.id = %s and c.ticket = t.id and (c.field=%s or c.field=%s or c.field='status')" "ORDER BY c.time ASC", [t['id'], estimation_field, initial_estimation_field]) # Build up two dictionaries, mapping dates when effort/status # changed, to the latest effort/status on that day (in case of # several changes on the same day). Also record the oldest known # effort/status, i.e. that at the time of ticket creation estimate_history = {} initial_estimate_history = {} status_history = {} earliest_estimate = None earliest_initial_estimate = None earliest_status = None for row in history_cursor: row_field, row_time, row_old, row_new = row event_date = datetime.fromtimestamp(row_time, utc).date() if row_field == estimation_field: new_value = self._cast_estimate(row_new) if new_value is not None: estimate_history[event_date] = new_value if earliest_estimate is None: earliest_estimate = self._cast_estimate(row_old) # note: not using elif since estimation_field and initial_estimate_field could be the same! if row_field == initial_estimation_field: new_value = self._cast_estimate(row_new) if new_value is not None: initial_estimate_history[event_date] = new_value if earliest_initial_estimate is None: earliest_initial_estimate = self._cast_estimate(row_old) if row_field == 'status': status_history[event_date] = row_new if earliest_status is None: earliest_status = row_old # If we don't know already (i.e. the ticket effort/status was # not changed on the creation date), set the effort on the # creation date. It may be that we don't have an "earliest" # estimate/status, because it was never changed. In this case, # use the current (latest) value. if not creation_date in estimate_history: if earliest_estimate is not None: estimate_history[creation_date] = earliest_estimate else: estimate_history[creation_date] = latest_estimate if not creation_date in initial_estimate_history: if earliest_initial_estimate is not None: initial_estimate_history[creation_date] = earliest_initial_estimate else: initial_estimate_history[creation_date] = latest_initial_estimate if not creation_date in status_history: if earliest_status is not None: status_history[creation_date] = earliest_status else: status_history[creation_date] = latest_status # There is a risk that the history table is messed up. Trust # the ticket/ticket_custom tables more. estimate_history[options['today']] = latest_estimate initial_estimate_history[options['today']] = latest_initial_estimate status_history[options['today']] = latest_status # Finally add estimates to the timetable. Treat any period where the # ticket was closed as estimate 0. We need to loop from ticket # creation date, not just from the timetable start date, since # it's possible that the ticket was changed between these two # dates. current_date = creation_date current_estimate = None previous_initial_estimate = Decimal(0) is_open = None while current_date <= options['enddate']: if current_date in status_history: is_open = (status_history[current_date] not in self.closed_states) if current_date in estimate_history: current_estimate = estimate_history[current_date] if current_date in initial_estimate_history: effort_delta = initial_estimate_history[current_date] - previous_initial_estimate if effort_delta != Decimal(0): previous_initial_estimate = initial_estimate_history[current_date] if current_date > options['startdate'] and current_date in delta: delta[current_date] += effort_delta if current_date in timetable and current_date >= options['startdate'] and is_open: timetable[current_date] += current_estimate current_date += timedelta(days=1) return timetable, delta