def test_parse_options(self): db = self.env.get_db_cnx() options, query_args = parse_options( db, "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28", {}) self.assertNotEqual(query_args['milestone'], None) self.assertNotEqual(options['startdate'], None) self.assertNotEqual(options['enddate'], None)
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 test_build_empty_chart(self): chart = BurndownChart(self.env) db = self.env.get_db_cnx() options, query_args = parse_options(db, "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28", {}) timetable = chart._calculate_timetable(options, query_args, self.req) xdata, ydata, maxhours = chart._scale_data(timetable, options) self.assertEqual(xdata, ['0.00', '12.50', '25.00', '37.50', '50.00', '62.50', '75.00', '87.50', '100.00']) self.assertEqual(ydata, ['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']) self.assertEqual(maxhours, Decimal(100))
def test_build_empty_chart(self): chart = BurndownChart(self.env) db = self.env.get_db_cnx() options, query_args = parse_options(db, "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28", {}) timetable, _ = chart._calculate_timetable(options, query_args, self.req) dates = sorted(timetable.keys()) xdata, ydata, maxhours = chart._scale_data(timetable, dates, Decimal(0), options) self.assertEqual(xdata, ['0.00', '12.50', '25.00', '37.50', '50.00', '62.50', '75.00', '87.50', '100.00']) self.assertEqual(ydata, ['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']) self.assertEqual(maxhours, Decimal(100))
def test_build_empty_chart(self): chart = BurndownChart(self.env) str = "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28" options, query_args = parse_options(self.env, str, {}) timetable = chart._calculate_timetable(options, query_args, self.req) xdata, ydata, maxhours = chart._scale_data(timetable, options) self.assertEqual(xdata, ['0.00', '12.50', '25.00', '37.50', '50.00', '62.50', '75.00', '87.50', '100.00']) self.assertEqual(ydata, ['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']) self.assertEqual(maxhours, Decimal(100))
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 expand_macro(self, formatter, name, content): # prepare options req = formatter.req options, query_args = parse_options(self.env.get_db_cnx(), content, copy.copy(DEFAULT_OPTIONS)) if not options['startdate']: raise TracError("No start date specified!") # minimum time frame is one day if (options['startdate'] >= options['enddate']): options['enddate'] = options['startdate'] + timedelta(days=1) # calculate data timetable = self._calculate_timetable(options, query_args, req) # remove weekends if not options['weekends']: for date in timetable.keys(): if date.weekday() >= 5: del timetable[date] # scale data xdata, ydata, maxhours = self._scale_data(timetable, options) # build html for google chart api dates = sorted(timetable.keys()) bottomaxis = "0:|" + ("|").join([str(date.day) for date in dates]) + \ "|1:|%s/%s|%s/%s" % (dates[0].month, dates[0].year, dates[ - 1].month, dates[ - 1].year) leftaxis = "2,0,%s" % maxhours # add line for expected progress if options['expected'] == '0': expecteddata = "" else: expecteddata = "|0,100|%s,0" % (round( Decimal(options['expected']) * 100 / maxhours, 2)) # prepare gridlines if options['gridlines'] == '0': gridlinesdata = "100.0,100.0,1,0" # create top and right bounding line by using grid else: gridlinesdata = "%s,%s" % (xdata[1], (round( Decimal(options['gridlines']) * 100 / maxhours, 4))) # mark weekends weekends = [] saturday = None index = 0 halfday = self._round(Decimal("0.5") / (len(dates) - 1)) for date in dates: if date.weekday() == 5: saturday = index if saturday and date.weekday() == 6: weekends.append( "R,%s,0,%s,%s" % (options['wecolor'], self._round((Decimal(xdata[saturday]) / 100) - halfday), self._round((Decimal(xdata[index]) / 100) + halfday))) saturday = None index += 1 # special handling if time period starts with Sundays... if len(dates) > 0 and dates[0].weekday() == 6: weekends.append("R,%s,0,0.0,%s" % (options['wecolor'], halfday)) # or ends with Saturday if len(dates) > 0 and dates[-1].weekday() == 5: weekends.append("R,%s,0,%s,1.0" % (options['wecolor'], Decimal(1) - halfday)) # chart title title = options.get('title', None) if title is None and options.get('milestone'): title = options['milestone'].split('|')[0] chart_args = unicode_urlencode({ 'chs': '%sx%s' % (options['width'], options['height']), 'chf': 'c,s,%s|bg,s,00000000' % options['bgcolor'], 'chd': 't:%s|%s%s' % (",".join(xdata), ",".join(ydata), expecteddata), 'cht': 'lxy', 'chxt': 'x,x,y', 'chxl': bottomaxis, 'chxr': leftaxis, 'chm': "|".join(weekends), 'chg': gridlinesdata, 'chco': '%s,%s' % (options['color'], options['colorexpected']), 'chtt': title }) self.log.debug("BurndownChart 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="Burndown Chart (server)") else: return tag.image(src="http://chart.googleapis.com/chart?%s" % chart_args, alt="Burndown Chart (client)")
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 expand_macro(self, formatter, name, content, args=None): # prepare options req = formatter.req options, query_args = parse_options(self.env, content, copy.copy(DEFAULT_OPTIONS)) if not options['startdate']: raise TracError("No start date specified!") # minimum time frame is one day if options['startdate'] >= options['enddate']: options['enddate'] = options['startdate'] + timedelta(days=1) # calculate data timetable = self._calculate_timetable(options, query_args, req) timetable_spent = self._calculate_timetable_spent(options, query_args, req) # remove weekends if not options['weekends']: for date in timetable.keys(): if date.weekday() >= 5: del timetable[date] del timetable_spent[date] # scale data xdata, ydata, maxhours = self._scale_data(timetable, options) xdata_spent, ydata_spent, maxhours_spent = self._scale_data(timetable_spent, options) if not options['spent']: spentdata = "|0,0|0,0" else: spentdata = "|%s|%s" % (",".join(xdata_spent), ",".join(ydata_spent)) # build html for google chart api dates = sorted(timetable.keys()) bottomaxis = "0:|" + "|".join([str(date.day) for date in dates]) + \ "|1:|%s/%s|%s/%s" % (dates[0].month, dates[0].year, dates[- 1].month, dates[- 1].year) leftaxis = "2,0,%s" % maxhours # add line for expected progress if options['expected'] == '0': expecteddata = "" else: expecteddata = "|0,100|%s,0" % ( round(Decimal(options['expected']) * 100 / maxhours, 2)) # prepare gridlines if options['gridlines'] == '0': # create top and right bounding line by using grid gridlinesdata = "100.0,100.0,1,0" else: gridlinesdata = "%s,%s" % (xdata[1], ( round(Decimal(options['gridlines']) * 100 / maxhours, 4))) # mark weekends weekends = [] saturday = None index = 0 halfday = self._round(Decimal("0.5") / (len(dates) - 1)) for date in dates: if date.weekday() == 5: saturday = index if saturday and date.weekday() == 6: weekends.append("R,%s,0,%s,%s" % (options['wecolor'], self._round((Decimal( xdata[saturday]) / 100) - halfday), self._round( (Decimal(xdata[index]) / 100) + halfday))) saturday = None index += 1 # special handling if time period starts with Sundays... if len(dates) > 0 and dates[0].weekday() == 6: weekends.append("R,%s,0,0.0,%s" % (options['wecolor'], halfday)) # or ends with Saturday if len(dates) > 0 and dates[- 1].weekday() == 5: weekends.append( "R,%s,0,%s,1.0" % (options['wecolor'], Decimal(1) - halfday)) # chart title title = options.get('title', None) if title is None and options.get('milestone'): title = options['milestone'].split('|')[0] chart_args = unicode_urlencode( {'chs': '%sx%s' % (options['width'], options['height']), 'chf': 'c,s,%s|bg,s,00000000' % options['bgcolor'], 'chd': 't:%s|%s%s%s' % (",".join(xdata), ",".join(ydata), spentdata, expecteddata), 'cht': 'lxy', 'chxt': 'x,x,y', 'chxl': bottomaxis, 'chxr': leftaxis, 'chm': "|".join(weekends), 'chg': gridlinesdata, 'chco': '%s,%s,%s' % (options['color'], options['colorspent'], options['colorexpected']), 'chdl': 'Remaining|Spent|Estimated', 'chtt': title}) self.log.debug("BurndownChart data: %s", chart_args) if self.serverside_charts: return tag.image( src="%s?data=%s" % (req.href.estimationtools('chart'), unicode_quote(chart_args)), alt="Burndown Chart (server)") else: return tag.image( src="http://chart.googleapis.com/chart?%s" % chart_args, alt="Burndown Chart (client)")
def test_parse_options(self): db = self.env.get_db_cnx() options, query_args = parse_options(db, "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28", {}) self.assertNotEqual(query_args['milestone'], None) self.assertNotEqual(options['startdate'], None) self.assertNotEqual(options['enddate'], None)
def test_parse_options(self): str = "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28" options, query_args = parse_options(self.env, str, {}) self.assertNotEqual(query_args['milestone'], None) self.assertNotEqual(options['startdate'], None) self.assertNotEqual(options['enddate'], None)
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 render_macro(self, req, name, content): # prepare options options, query_args = parse_options(self.env.get_db_cnx(), content, copy.copy(DEFAULT_OPTIONS)) if not options['startdate']: raise TracError("No start date specified!") # minimum time frame is one day if (options['startdate'] >= options['enddate']): options['enddate'] = options['startdate'] + timedelta(days=1) change = options['change'] # calculate data timetable, delta = self._calculate_timetable(options, query_args, req) timetable_less_change = {} if change: cumulative_change = Decimal(0) for current_date in sorted(timetable.keys()): cumulative_change += delta.get(current_date, Decimal(0)) timetable_less_change[ current_date] = timetable[current_date] - cumulative_change dates = sorted(timetable.keys()) # build html for google chart api chart_params = {} chart_params['cht'] = 'lxy' chart_params['chtt'] = self._get_title(options) chart_params['chco'] = options['color'] chart_params['chs'] = "%sx%s" % (options['width'], options['height']) chart_params[ 'chg'] = "100.0,100.0,1,0" # create top and right bounding line by using grid" chart_params['chxt'] = "x,x,x,y" # Add scaled data try: maxhours = max(timetable.values()) except ValueError: maxhours = 0 try: cmaxhours = max(timetable_less_change.values()) except ValueError: cmaxhours = 0 real_maxhours = max(maxhours, cmaxhours) real_maxhours = Decimal(real_maxhours) if real_maxhours <= Decimal(0): real_maxhours = Decimal(100) xdata, ydata = self._scale_data(timetable, dates, real_maxhours, options) chart_params['chd'] = "t:%s|%s" % ( ",".join(xdata), ",".join(ydata), ) cxdata = cydata = None if change: cxdata, cydata = self._scale_data(timetable_less_change, dates, real_maxhours, options) chart_params['chd'] += "|%s|%s" % ( ",".join(cxdata), ",".join(cydata), ) if change: chart_params['chdl'] = "Current|Less change" # Add axes chart_params['chxl'] = "0:|" + "|".join([str(date.day) for date in dates]) + \ "|1:|%s|%s" % (dates[0].month, dates[ - 1].month) + \ "|2:|%s|%s" % (dates[0].year, dates[ - 1].year) chart_params['chxr'] = "3,0,%d" % real_maxhours # Add weekends downtime = self._mark_downtime(options, dates, xdata, ydata) if downtime: chart_params['chm'] = '|'.join(downtime) return Markup( "<img src=\"http://chart.apis.google.com/chart?%s\" alt=\"Burndown Chart\" />" % "&".join("%s=%s" % (k, v) for k, v in chart_params.items()))
def render_macro(self, req, name, content): # prepare options options, query_args = parse_options(self.env.get_db_cnx(), content, copy.copy(DEFAULT_OPTIONS)) if not options['startdate']: raise TracError("No start date specified!") # minimum time frame is one day if (options['startdate'] >= options['enddate']): options['enddate'] = options['startdate'] + timedelta(days=1) change = options['change'] # calculate data timetable, delta = self._calculate_timetable(options, query_args, req) timetable_less_change = {} if change: cumulative_change = Decimal(0) for current_date in sorted(timetable.keys()): cumulative_change += delta.get(current_date, Decimal(0)) timetable_less_change[current_date] = timetable[current_date] - cumulative_change dates = sorted(timetable.keys()) # build html for google chart api chart_params = {} chart_params['cht'] = 'lxy' chart_params['chtt'] = self._get_title(options) chart_params['chco'] = options['color'] chart_params['chs'] = "%sx%s" % (options['width'], options['height']) chart_params['chg'] = "100.0,100.0,1,0" # create top and right bounding line by using grid" chart_params['chxt'] = "x,x,x,y" # Add scaled data try: maxhours = max(timetable.values()) except ValueError: maxhours = 0 try: cmaxhours = max(timetable_less_change.values()) except ValueError: cmaxhours = 0 real_maxhours = max(maxhours, cmaxhours) real_maxhours = Decimal(real_maxhours) if real_maxhours <= Decimal(0): real_maxhours = Decimal(100) xdata, ydata = self._scale_data(timetable, dates, real_maxhours, options) chart_params['chd'] = "t:%s|%s" % (",".join(xdata), ",".join(ydata),) cxdata = cydata = None if change: cxdata, cydata = self._scale_data(timetable_less_change, dates, real_maxhours, options) chart_params['chd'] += "|%s|%s" % (",".join(cxdata), ",".join(cydata),) if change: chart_params['chdl'] = "Current|Less change" # Add axes chart_params['chxl'] = "0:|" + "|".join([str(date.day) for date in dates]) + \ "|1:|%s|%s" % (dates[0].month, dates[ - 1].month) + \ "|2:|%s|%s" % (dates[0].year, dates[ - 1].year) chart_params['chxr'] = "3,0,%d" % real_maxhours # Add weekends downtime = self._mark_downtime(options, dates, xdata, ydata) if downtime: chart_params['chm'] = '|'.join(downtime) return Markup("<img src=\"http://chart.apis.google.com/chart?%s\" alt=\"Burndown Chart\" />" % "&".join("%s=%s" % (k,v) for k,v in chart_params.items()))