Changeset 715
- Timestamp:
- 11/11/08 10:38:57 (5 years ago)
- Location:
- trunk/plugins/dashboardreportsplugin/tracdashboardreports
- Files:
-
- 1 deleted
- 2 edited
-
api.py (modified) (1 diff)
-
macros.py (modified) (2 diffs)
-
queryfiltermacro.py (deleted)
Legend:
- Unmodified
- Added
- Removed
-
trunk/plugins/dashboardreportsplugin/tracdashboardreports/api.py
r662 r715 4 4 # 5 5 6 from datetime import date, datetime7 import random8 import time9 10 6 from trac.core import * 11 7 from trac.wiki.macros import WikiMacroBase 12 from genshi.builder import tag13 from trac.web.chrome import add_script, add_stylesheet, ITemplateProvider, Chrome14 8 from trac.ticket.api import TicketSystem 15 from trac.ticket.query import Query 16 from trac.util.datefmt import to_timestamp, utc 17 from trac.util.html import html 18 19 def get_param_value(param_store, key): 20 if type(param_store) is dict and param_store.has_key(key) : 21 return param_store[key] 22 else : 23 return None 24 25 is_op = lambda x: x in ['!', '<', '>'] 26 27 class DashboardBaseMacro(WikiMacroBase): 28 9 10 class TicketCountQueryBaseMacro(WikiMacroBase): 29 11 abstract = True 30 31 severities_sql = "select distinct %s from %s order by 1" 32 33 def _process_macro_variables(self, content, req=None): 12 13 def _run_sql(self, sql, args=[]): 14 """Executes macros's sql commands. 15 16 @param sql: str 17 @param args: list 18 @return: Generator 34 19 """ 35 This method scans the content to check for variable names. If it find a 36 variable name then it replaces the name with the value. 37 """ 38 prefix="" 39 return_value = "" 40 variable = content 41 # cut the prefix and store it 42 if content[0] in ['<', '>'] : 43 prefix = content[0] 44 variable = content[1:] 45 # test to see if we have a variable 46 if variable[0] != '$' : 47 return_value = variable 48 elif variable[1:] == 'NOW' : 49 return_value = to_timestamp(datetime.now(utc)) 50 elif variable[1:] == 'TODAY' : 51 now = datetime.now(utc) 52 seconds_today = now.hour * 3600 + now.minute * 60 + now.second 53 return_value = to_timestamp(datetime.fromtimestamp(int(time.time()) 54 - seconds_today, utc)) 55 elif variable[1:] == 'YESTERDAY' : 56 return_value = to_timestamp(datetime.fromtimestamp(int(time.time()) 57 - 86400, utc)) 58 elif variable[1:] == 'USER' : 59 return req.authname 60 61 return unicode(prefix + str(return_value)) 62 63 def get_parameters(self, content, req=None): 64 global get_param_value 65 """ 66 This method processes the parameters this macro supports and that are 67 defined in the self.parameters variable declared in the implementing 68 macro class. 69 This method searches for the filter parameter, if not found then it 70 does the processing automatically. Also if the implementing class 71 defines the id and name parameters but the id is not set then the id 72 value is get from the name parameter. 73 74 """ 75 global get_param_value 76 values = content.split(',') 77 if len(values) < 1: 78 return None 79 80 # copy the default values 81 param_store = self.parameters.copy() 82 filter = get_param_value(param_store, self.get_filter()) 83 if filter is None : 84 85 # split the trac query filter 86 where = values[0].split('&') 87 filter = {} 88 for col in where : 89 name, value = col.split('=') 90 name = name.strip() 91 value = value.strip() 92 filter[name] = [self._process_macro_variables(val, req) 93 for val in value.split('|')] 94 # delete the first parameter 95 values = values[1:] 96 # put back the value 97 param_store[self.get_filter()] = filter 98 updated_params = [] 99 for param in values : 100 if len(param.split('=')) < 2 : 101 continue 102 name, value = param.split('=') 103 name = name.strip() 104 value = value.strip() 105 value = [self._process_macro_variables(val, req) 106 for val in value.split('|')] 107 108 if len(value) == 1 : 109 value = value[0] 110 111 if name in self.parameters.keys() : 112 if type(param_store[name]) is list : 113 if value in param_store[name] : 114 param_store[name] = value 115 else : 116 # bad value given then using the default 117 param_store[name] = param_store[name][0] 118 else : 119 param_store[name] = value 120 updated_params.append(name) 121 # check that the macro was called with all mandatory params 122 for name in param_store.keys() : 123 # we handled this param 124 if name in updated_params: 125 continue 126 # we diodn't received a value for this mandatory param 127 if param_store[name] is None : 128 return None 129 # get the default value for this param 130 if type(param_store[name]) is list : 131 param_store[name] = param_store[name][0] 132 # put the default div id 133 #if name == 'id' and param_store.has_key('name'): 134 # param_store[name] = param_store['name'].replace(' ', '_').lower() 135 # param_store[name] += '_' + str(random.randint(1, 100000000)) 136 return param_store 137 138 def generate_trac_query(self, resource, attach=False): 139 """ 140 Returns a string that is like a trac ticket query filter. 141 The resource parameter can be a dictionary or a list. 142 """ 143 sql = "" 144 join_by = '&' 145 #put a separator at the beginning 146 if attach : 147 sql += join_by 148 if type(resource) is dict : 149 for field in resource.keys() : 150 values = resource[field] 151 if type(values) is list : 152 sql += field + '=' + '|'.join(values) + join_by 153 else : 154 if values is None : 155 values = '' 156 sql += field + '=' + unicode(values) + join_by 157 elif type(resource) is list : 158 for col in resource : 159 sql += 'col=' + col + join_by 160 161 return sql[:-1] 162 163 def generate_sql(self, param_store, req=None): 164 """ 165 This method generates a sql query from the parameters received. 166 """ 167 global get_param_value, is_op 168 filter = get_param_value(param_store, self.get_filter()) 169 group_by = get_param_value(param_store, self.get_group_by()) 170 component = get_param_value(param_store, self.get_component()) 171 cols = self.get_columns(param_store) 172 enum_columns = ('resolution', 'priority', 'severity') 173 fields =TicketSystem(self.env).get_ticket_fields() 174 # this was from the new get_all_ticket_fields() method in the patch 175 # fields =TicketSystem(self.env).get_all_ticket_fields() 176 fields.extend([{'name': 'time', 'type': 'date', 'label': 'Created'}, 177 {'name': 'changetime', 'type': 'date', 178 'label': 'Modified'}]) 179 180 custom_fields = [f['name'] for f in fields if 'custom' in f] 181 182 def get_field_value(field): 183 if field in custom_fields : 184 return field + "_table.value" 185 return field 186 187 sql = ["SELECT ",] 188 sql_col = [] 189 for col in cols : 190 if col == 'id' : 191 sql_col.append(' count(t.' + col + ') AS ' + col + ' ') 192 else : 193 if col in custom_fields : 194 sql_col.append(col + '_table.value AS ' + col + ' ') 195 else : 196 sql_col.append('t.' + col + ' AS ' + col) 197 sql.append(','.join(sql_col)) 198 # add the from 199 sql.append(' FROM ticket t ') 200 201 # Join with ticket_custom table as necessary 202 for k in [k for k in cols if k in custom_fields]: 203 sql.append("\n INNER JOIN ticket_custom AS %s ON " \ 204 "(id=%s.ticket AND %s.name='%s')" % ((k +'_table', ) * 3 + (k,))) 205 206 # Join with the enum table for proper sorting 207 for col in [c for c in enum_columns 208 if c == group_by or c == 'priority']: 209 sql.append("\n LEFT OUTER JOIN enum AS %s ON " 210 "(%s.type='%s' AND %s.name=%s)" 211 % ((col,) * 5)) 212 213 # Join with the version/milestone tables for proper sorting 214 for col in [c for c in ['milestone', 'version'] 215 if c == component or c == group_by]: 216 sql.append("\n LEFT OUTER JOIN %s %s ON (%s.name=t.%s)" 217 % (col, col[0], col[0], col)) 218 219 def get_constraint_sql(name, value, mode, neg): 220 # strip the operator from the value 221 value = value[len(mode) + is_op(neg):] 222 # convert the value for non string fields 223 current_field = {} 224 for field in fields : 225 if field['name'] == name : 226 current_field = field 227 break 228 if current_field.has_key('type') \ 229 and current_field['type'] == 'date' : 230 value = int(value) 231 232 # add the table alias to the field name 233 if name not in custom_fields: 234 name = 't.' + name 235 else: 236 name = name + '.value' 237 238 if mode == '': 239 try : 240 value = int(value) 241 except : 242 value = "'" + value + "'" 243 return ("COALESCE(%s,'')%s=%s" % (name, 244 is_op(neg) and neg or '', value)) 245 if not value: 246 return None 247 db = self.env.get_db_cnx() 248 value = db.like_escape(value) 249 if mode == '~': 250 value = '%' + value + '%' 251 elif mode == '^': 252 value = value + '%' 253 elif mode == '$': 254 value = '%' + value 255 return ("COALESCE(%s,'') %s%s" % (name, neg == '!' and 'NOT ' or '', 256 db.like()), value) 257 258 clauses = [] 259 args = [] 260 for k, v in filter.items(): 261 if req: 262 v = [val.replace('$USER', req.authname) for val in v] 263 # get the first character of the value, determine later if it's an 264 # operation 265 neg = '' 266 mode = '' 267 if v[0] != '' : 268 neg = v[0][0] 269 mode = '' 270 if len(v[0]) > 1 and is_op(neg) and v[0][is_op(neg)] in ('~', '^', '$'): 271 mode = v[0][is_op(neg)] 272 273 # Special case id ranges 274 if k == 'id': 275 ranges = Ranges() 276 for r in v: 277 for ch in ['!', '<', '>'] : 278 r = r.replace(ch, '') 279 ranges.appendrange(r) 280 ids = [] 281 id_clauses = [] 282 for a,b in ranges.pairs: 283 if a == b: 284 ids.append(str(a)) 285 else: 286 id_clauses.append('id BETWEEN %s AND %s' % (a, b)) 287 if ids: 288 id_clauses.append('id IN (%s)' % (','.join(ids))) 289 if id_clauses: 290 clauses.append('%s(%s)' % (neg == '!' and 'NOT ' or '', 291 ' OR '.join(id_clauses))) 292 # Special case for exact matches on multiple values 293 elif not mode and len(v) > 1: 294 if k not in custom_fields: 295 col = 't.' + k 296 else: 297 col = k + '.value' 298 clauses.append("COALESCE(%s,'') %sIN (%s)" 299 % (col, neg == '!' and 'NOT ' or '', 300 ','.join(["'" + val[is_op(neg):] + "'" for val in v]))) 301 elif len(v) > 1: 302 constraint_sql = filter(None, 303 [get_constraint_sql(k, val, mode, neg) 304 for val in v]) 305 if not constraint_sql: 306 continue 307 if is_op(neg): 308 clauses.append("(" + " AND ".join( 309 [item for item in constraint_sql]) + ")") 310 else: 311 clauses.append("(" + " OR ".join( 312 [item for item in constraint_sql]) + ")") 313 elif len(v) == 1: 314 constraint_sql = get_constraint_sql(k, v[0], mode, neg) 315 if constraint_sql: 316 clauses.append(constraint_sql) 317 #clauses = filter(None, clauses) 318 if clauses: 319 sql.append("\nWHERE ") 320 if clauses: 321 sql.append(" AND ".join(clauses)) 322 323 # add the group by 324 groups = ' GROUP BY ' + get_field_value(group_by) 325 if component not in [ None, ''] : 326 groups += ', ' + get_field_value(component) 327 if component == 'milestone' : 328 groups += ', m.due' 329 sql.append(groups) 330 331 # add the order by 332 if component == 'milestone' : 333 sql.append(' ORDER BY m.due') 334 elif component not in [None, '']: 335 sql.append(' ORDER BY ' + get_field_value(component)) 336 else : 337 sql.append(' ORDER BY ' + get_field_value(group_by)) 338 339 return ' '.join(sql) 340 341 def _run_sql(self, formatter, sql): 342 """ 343 This method runs a sql query against the database and returns a list 344 with rows. 345 """ 346 self.log.debug(sql) 347 db = formatter.db 348 cursor = db.cursor() 349 sql_return = [] 20 db = self.env.get_db_cnx() 350 21 try: 351 cursor.execute(sql) 352 sql_return = [row for row in cursor if row != ""] 353 cursor.close() 22 cursor = db.cursor() 23 cursor.execute(sql, args) 24 for row in cursor: 25 yield row 354 26 except: 355 27 db.rollback() 356 28 raise 357 # return with striped null values 358 return sql_return 359 360 def get_severities(self, formatter, group_by, table="ticket"): 29 30 def get_axis_values(self, group_by): 31 """This method returns a list with values of the group-by field used 32 in the project. 33 34 @param group_by: str 35 @return: Generator 361 36 """ 362 This method returns a list with the values of the group-by field used 363 in the project. 37 sql, args = None, None 38 if group_by in [field['name'] for field in TicketSystem(self.env).get_custom_fields()]: 39 sql, args = ("SELECT DISTINCT tc.value" 40 " FROM ticket t" 41 " JOIN ticket_custom tc" 42 " ON t.id = tc.ticket" 43 " WHERE tc.name=%s", (group_by,)) 44 elif group_by in ['priority', 'severity']: 45 sql, args = ("SELECT name FROM enum e WHERE e.type=%s ORDER BY e.value", (group_by,)) 46 elif group_by == 'milestone': 47 sql, args = ("SELECT name FROM milestone WHERE completed=0 ORDER BY due", []) 48 elif group_by =='component': 49 sql, args = ("SELECT name FROM component", []) 50 elif group_by =='status': 51 for ticket_status in TicketSystem(self.env).get_all_status(): 52 yield ticket_status 53 return 54 55 if not sql: 56 raise TracError("Unable to lookup field values for field %s"%(group_by)) 57 58 for value, in self._run_sql(sql, args): 59 yield value 60 61 def populate_variables(self, req=None, query_string=''): 62 """Replaces all substrings like '$USER' with current values. 63 Please note that since Trac-0.12 values like 'today', 'yesterday', '3dayago', 'lastmonth', are 64 supported for datetime fields. 65 66 @param req: Request 67 @param query_string: str 68 @return: str 364 69 """ 365 custom_fields = TicketSystem(self.env).get_custom_fields() 366 367 if group_by in [field['name'] for field in custom_fields ] : 368 sql = "SELECT DISTINCT tc.value FROM ticket t JOIN ticket_custom tc ON t.id = tc.ticket WHERE tc.name='%s'" % (group_by) 369 elif group_by in ['priority', 'severity']: 370 sql = "select distinct %s, e.value from ticket t join enum e on t.%s = e.name where e.type='%s' order by e.value" % ((group_by, ) *3) 371 elif group_by == 'milestone' : 372 sql = "select distinct t.milestone, m.due from ticket t left join milestone m on t.milestone = m.name order by m.due" 373 else : 374 sql = self.severities_sql % (group_by, table) 375 376 values = self._run_sql(formatter, sql) 377 return [ unicode(val[0]) for val in values ] 378 379 def _get_filter_query(self, filter, add=None): 70 if req and '$USER' in query_string: 71 query_string = query_string.replace('$USER', req.authname) 72 return query_string 73 74 def get_count_per_group(self, req, query, groupby): 75 """Returns counts per each group. 76 77 @param query: ticket.query.Query 78 @param groupby: list 79 @return: generator 380 80 """ 381 Returns an href to the query page that has the '''filter''' as 382 parameter. 383 """ 384 sql = self.generate_trac_query(filter) 385 if add is not None : 386 sql += self.generate_trac_query(add, True) 387 query = Query.from_string(self.env, sql) 388 return query.get_href(self.env.href) 389 390 def add_href_to_fields(self, field_name, values): 391 """ 392 """ 393 if type(values) in [str, unicode] : 394 return self._get_filter_query({field_name: values}) 395 396 elif type(values) is list : 397 return_values = [] 398 for value in values : 399 return_values.append({'value': value, 400 'label_href': self._get_filter_query({field_name: value})}) 401 return return_values 402 403 elif type(values) is dict : 404 for key in values.keys() : 405 values[key]['label_href'] = self._get_filter_query({field_name: 406 key}) 407 return values 408 409 return values 410 411 def get_filter(self): 412 """ 413 It MUST be overwritten by the implementing macro class. 414 """ 415 return None 416 417 def get_component(self): 418 """ 419 It MUST be overwritten by the implementing macro class. 420 """ 421 return None 422 423 def get_group_by(self): 424 """ 425 It MUST be overwritten by the implementing macro class. 426 """ 427 return None 428 429 def get_columns(self, param_store): 430 """ 431 This method returns a list with the ticket fields that will be used as 432 column headers of the table view, and also it will be used to create 433 the sql query. 434 It receives as parameter the dictionary with the processed macro 435 parameters. The default method returns only the id. 436 This method MUST be overwritten by the implementing macro class. 437 """ 438 return ['id'] 439 440 def update_parameter_store(self, param_store): 441 """ 442 It CAN be overwritten by the implementing macro class. 443 """ 444 return param_store 445 446 class FilterReportMacro(DashboardBaseMacro): 447 """ 448 This class represents the base macro for the macros that display 449 information about tickets based on the query like parameter received. 450 As base class it offers support for: 451 - parsing and verifying the parameters 452 - generating a sql from the parameters received 453 - generating links to a query page that displays the tickets for a 454 particular sql query 455 - processes the sql query result and returns a summary 456 - implements the NOW and YESTERDAY query variables 457 458 The overwriting class must: 459 - define the '''template''' attribute which should contain the genshi 460 template used for rendering the output. 461 - define the '''reportName''' and '''div_id''' variables 462 - implement the '''create_view''' method and use the processed data 463 obtained in the create_view method. 464 465 466 Parameters: 467 468 The macro supports two different types of parameters: 469 - ticket query filter 470 - name - value parameters 471 The parameters are separated by comma. 472 473 The first parameter must be the a trac query filter. The syntax is like: 474 ticket_field1=[OPERATOR]value1|value2|value3[&ticket_field2......] 475 The known operators are: 476 - ! 477 - ~ 478 - > 479 - < 480 - ^ 481 - $ 482 483 After the first parameter, a number of maximum four other parameters are 484 possible. Some of them are optional and some are mandatory. 485 486 Mandatory parameters: 487 - id - the html id div of the widget 488 - group-by - the ticket field after which the sum is done 489 Optional parameters: 490 - name - the name of the widget displayed 491 - field - the ticket field after which a grouping is done 492 81 query_sql, query_args = query.get_sql(req=req) 82 for row in self._run_sql("SELECT %(groupby)s, count()" 83 " FROM (%(query)s)" 84 " GROUP BY %(groupby)s"%dict(groupby=','.join(groupby), query=query_sql), 85 query_args): 86 # example: ['priority', 'milestone', 10] => data[('priority', 'milestone')] = count 87 key, count = row[:-1], row[-1:][0] 88 yield key, count 89 90 class DashboardTicketCountsMacroChartsBurnupBase(WikiMacroBase): 91 """Display a box that shows a burnup chart of tickets 92 493 93 """ 494 94 abstract = True 495 95 496 template = None497 498 96 def expand_macro(self, formatter, name, content): 499 """ 500 This method starts the processing and calls all other methods. 501 """ 502 global get_param_value 503 # process the params of the calling macro 504 505 param_store = self.get_parameters(content, formatter.req) 506 507 if param_store is None : 508 return None 509 510 511 # call the hook to alter the param store 512 513 param_store = self.update_parameter_store(param_store) 514 # get the values from the database 515 values = self._run_sql(formatter, 516 self.generate_sql(param_store, formatter.req)) 517 518 output = self.display_sql(formatter, 519 self.compute_values(values, param_store), 520 param_store) 521 522 #self.log.debug(output) 523 return output 524 525 526 def compute_values(self, values, param_store): 527 """ 528 This method receives the values from the database and calculates totals 529 based on the group-by and component fields. 530 """ 97 98 # sql for burnup/burndown charts 99 sqlStatement="select count(t.id), t.priority, c1.value as estimate_hours, c2.value as actual_hours, t.changetime from ticket t LEFT OUTER JOIN ticket_custom c1 ON (t.id = c1.ticket AND c1.name = 'estimate_hours') LEFT OUTER JOIN ticket_custom c2 ON (t.id = c2.ticket AND c2.name = 'actual_hours') LEFT OUTER JOIN ticket_custom c ON (t.id = c.ticket AND c.name = 'resolution2') where c.value = 'Fixed' or t.status=='closed' group by t.id;" 100 101 # arrays to store graph coordinates 102 estimateHoursData = [] 103 actualHoursData = [] 104 105 # variable to tally tickets 531 106 runningTotal = 0 532 sum_per_severity = {} 107 108 # temp variable to increment x-value 109 xindex = 1 110 111 # sum of all actual/estimate values 112 yActualTotal = 0 113 yEstimateTotal = 0 114 115 # query DB 116 db = formatter.db 117 cursor = db.cursor() 118 cursor.execute(sqlStatement) 119 120 outputValues = {} 533 121 components = {} 534 component = get_param_value(param_store, self.get_component()) 535 # define the index 536 # TODO: make the flow after what the get_columns returns 537 id_index = 0 538 component_index = 2 539 group_by_index = 1 540 due_index=3 541 for row in values: 542 if sum_per_severity.has_key(row[group_by_index]): 543 sum_per_severity[row[group_by_index]] += row[id_index] 122 ticketCounts = cursor.fetchall() 123 for ticketCount in ticketCounts: 124 if None in ticketCount or u'' in ticketCount: 125 continue 126 # get ticket count 127 runningTotal+=ticketCount[0] 128 # get date of last modification 129 changetime = datetime.datetime.fromtimestamp(ticketCount[4]) 130 yActualTotal+=int(ticketCount[3]) 131 yEstimateTotal+=int(ticketCount[2]) 132 # temp variable to have different x values bc all change dates are the same for some reason 133 #actualHoursData.append('[%s,%s]' %(changetime.strftime("%W"), yActualTotal)) 134 actualHoursData.append('[%s,%s]' %(xindex, yActualTotal)) 135 estimateHoursData.append('[%s,%s]' %(xindex, yEstimateTotal)) 136 xindex+=1 137 cursor.close() 138 139 add_stylesheet(formatter.req, 'tracdashboardreports/css/dashboardreports.css') 140 141 # Not used yet. This method of adding scripts adds them too late in the page 142 # add_script(formatter.req, 'dashboardreports/js/jquery.flot.js') 143 # add_script(formatter.req, 'dashboardreports/js/excanvas.js') 144 145 return ''' 146 <div class="dashboard_report burn"> 147 148 <span class="dashboard_report_title">Burnup Chart</span> 149 <span class="dashboard_report_total">Total: %s</span> 150 <div class="chart" style="padding-top: 5px;"> 151 <div id="chartgen_burnup" style="width:600px;height:180px" class="burn charts" > 152 </div> 153 154 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/jquery.flot.js"></script> 155 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/excanvas.js"></script> 156 <script id="source" language="javascript" type="text/javascript"> 157 var data_actual = [[0,0],%s]; 158 var data_estimate = [[0,0],%s]; 159 160 var data = [{label:"actual", data:data_actual}, {label:"estimate", data:data_estimate}]; 161 var options = {legend:{position:"se", margin:13, show:true}}; 162 jQuery.plot(jQuery("#chartgen_burnup"), data, options); 163 164 </script> 165 166 ''' % (runningTotal, ",".join(actualHoursData), ",".join(estimateHoursData)) 167 168 class DashboardTicketCountsMacroChartsBurndownBase(WikiMacroBase): 169 """ 170 Display a box that shows a burndown chart of tickets 171 172 """ 173 abstract = True 174 175 def expand_macro(self, formatter, name, content): 176 177 # sql for burnup/burndown charts 178 sqlStatement_closed="select count(t.id), t.priority, c1.value as estimate_hours, c2.value as actual_hours, t.changetime from ticket t LEFT OUTER JOIN ticket_custom c1 ON (t.id = c1.ticket AND c1.name = 'estimate_hours') LEFT OUTER JOIN ticket_custom c2 ON (t.id = c2.ticket AND c2.name = 'actual_hours') LEFT OUTER JOIN ticket_custom c ON (t.id = c.ticket AND c.name = 'resolution2') where c.value = 'Fixed' or t.status=='closed' group by t.id;" 179 sqlStatement_not_closed="select count(t.id), t.priority, c1.value as estimate_hours, c2.value as actual_hours, t.changetime from ticket t LEFT OUTER JOIN ticket_custom c1 ON (t.id = c1.ticket AND c1.name = 'estimate_hours') LEFT OUTER JOIN ticket_custom c2 ON (t.id = c2.ticket AND c2.name = 'actual_hours') LEFT OUTER JOIN ticket_custom c ON (t.id = c.ticket AND c.name = 'resolution2') where c.value != 'Fixed' or t.status!='closed' group by t.id;" 180 181 # arrays to store graph coordinates 182 estimateHoursData = [] 183 actualHoursData = [] 184 185 # track ticket count 186 runningTotal = 0 187 188 # temp variable to increment x-value 189 xindex = 0 190 191 # sum of all actual/estimate hours (10 is just to have an example of project not yet finished) 192 yActualTotal = 10 193 yEstimateTotal = 10 194 195 # query DB for closed tickets 196 db = formatter.db 197 cursor_closed = db.cursor() 198 cursor_closed.execute(sqlStatement_closed) 199 200 # query DB for not closed tickets 201 cursor_not_closed = db.cursor() 202 cursor_not_closed.execute(sqlStatement_not_closed) 203 204 outputValues = {} 205 components = {} 206 ticketCounts = cursor_closed.fetchall() 207 ticketCounts_not_closed = cursor_not_closed.fetchall() 208 209 # go through not closed tickets to get total actual/estimate hours (how far up the chart starts) 210 for ticketCount in ticketCounts_not_closed: 211 yActualTotal+=int(ticketCount[2]) 212 yEstimateTotal+=int(ticketCount[3]) 213 # add actual/estimate hours from closed tickets 214 for ticketCount in ticketCounts: 215 if None in ticketCount or u'' in ticketCount: 216 continue 217 yActualTotal+=int(ticketCount[2]) 218 yEstimateTotal+=int(ticketCount[3]) 219 220 for ticketCount in ticketCounts: 221 if None in ticketCount or u'' in ticketCount: 222 continue 223 runningTotal+=ticketCount[0] 224 changetime = datetime.datetime.fromtimestamp(ticketCount[4]) 225 yActualTotal-=int(ticketCount[2]) 226 yEstimateTotal-=int(ticketCount[3]) 227 # temp variable to have different x values bc all change dates are the same for some reason 228 # actualHoursData.append('[%s,%s]' %(changetime.strftime("%W"), actualHours)) 229 actualHoursData.append('[%s,%s]' %(xindex, yActualTotal)) 230 estimateHoursData.append('[%s,%s]' %(xindex, yEstimateTotal)) 231 xindex+=1 232 cursor_closed.close() 233 cursor_not_closed.close() 234 235 add_stylesheet(formatter.req, 'tracdashboardreports/css/dashboardreports.css') 236 237 # Not used yet. This method of adding scripts adds them too late in the page 238 # add_script(formatter.req, 'dashboardreports/js/jquery.flot.js') 239 # add_script(formatter.req, 'dashboardreports/js/excanvas.js') 240 241 return ''' 242 <div class="dashboard_report burn"> 243 244 <span class="dashboard_report_title">Burndown Chart</span> 245 <span class="dashboard_report_total">Total: %s</span> 246 <div class="chart" style="padding-top: 5px;"> 247 <div id="chartgen_burndown" style="width:600px;height:180px" class="burn charts" > 248 </div> 249 250 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/jquery.flot.js"></script> 251 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/excanvas.js"></script> 252 <script id="source" language="javascript" type="text/javascript"> 253 var data_actual = [%s]; 254 var data_estimate = [%s]; 255 256 var data = [{label:"actual", data:data_actual}, {label:"estimate", data:data_estimate}]; 257 var options = {legend:{position:"ne", margin:13, show: true}}; 258 jQuery.plot(jQuery("#chartgen_burndown"), data, options); 259 260 </script> 261 262 ''' % (runningTotal, ",".join(actualHoursData), ",".join(estimateHoursData)) 263 264 class DashboardTicketCountsMacroChartsBase(WikiMacroBase): 265 """ 266 Display a box with that shows ticket count totals in a bar graph using flot 267 268 Implementations should override the REPORTS, container_name, and subtotalSql attributes 269 270 """ 271 abstract = True 272 273 REPORTS = { 274 'new': {'sql':"select count(t.id), t.priority from ticket t where t.status!='closed' group by t.priority;"}, 275 } 276 277 container_name="oforge_dashboard_report" 278 subtotalSql="SELECT s.name, s.value from enum s where s.type = 'priority' ORDER BY s.value" 279 280 def expand_macro(self, formatter, name, content): 281 282 if not content or len(content) == 0: 283 raise Exception("You must specify an argument of open, fixed, new, verified") 284 285 reportName=content 286 if not self.REPORTS.has_key(reportName): 287 raise Exception("Unknown Dashboard for '%s'"%reportName) 288 289 sqlStatement=self.REPORTS[reportName]['sql'] 290 291 outputTable = [] 292 dataArraysTable = [] 293 dataSetupArraysTable = [] 294 optionsArraysTable = [] 295 runningTotal = 0 296 xindex = 0 297 298 # query DB 299 db = formatter.db 300 cursor = db.cursor() 301 cursor.execute(sqlStatement) 302 303 outputValues = {} 304 components = {} 305 ticketCounts = cursor.fetchall() 306 # check for by-component 307 for ticketCount in ticketCounts: 308 if None in ticketCount or u'' in ticketCount: 309 continue 310 if outputValues.has_key(ticketCount[1]): 311 outputValues[ticketCount[1]]+=ticketCount[0] 544 312 else: 545 sum_per_severity[row[group_by_index]] = row[id_index] 546 if component not in [None, '']: 547 if not components.has_key(row[component_index]): 548 components[row[component_index]]={} 549 components[row[component_index]]['total'] = 0 550 if 'due' in self.get_columns(param_store) : 551 components[row[component_index]]['due'] = '' 552 components[row[component_index]][row[group_by_index]]=row[id_index] 553 components[row[component_index]]['total'] += row[id_index] 554 if 'due' in self.get_columns(param_store) : 555 components[row[component_index]]['due'] = row[due_index] 556 runningTotal+=row[id_index] 557 return sum_per_severity, components, runningTotal 558 559 def display_sql(self, formatter, sum_per_severity, param_store): 560 """ 561 It renders the output of the macro. 562 """ 563 564 data = self.create_view(formatter, 565 sum_per_severity, 566 param_store) 567 add_stylesheet(formatter.req, 'dashboardreports/css/widgets.css') 568 #self.log.debug(data) 569 template = Chrome(self.env).load_template(self.template,method='xhtml') 570 data = Chrome(self.env).populate_data(formatter.req, data) 571 572 rendered_result = template.generate(**data) 573 574 div_class="%s w%s"%(param_store.get('class'),data.get('width')) 575 return html.div(rendered_result, 576 id = param_store.get('div_id'), 577 class_ = div_class) 578 579 def create_view(self, formatter, sum_per_severity, param_store): 580 """ 581 It must be overwritten by the implementing macro class. 582 """ 583 return None 313 outputValues[ticketCount[1]]=ticketCount[0] 314 if len(ticketCount)==3: 315 if not components.has_key(ticketCount[2]): 316 components[ticketCount[2]]={} 317 components[ticketCount[2]][ticketCount[1]]=ticketCount[0] 318 runningTotal+=ticketCount[0] 319 cursor.close() 320 321 # counter for components 322 byComponentCounter = 0 323 # random number (to append to div) 324 r = random.randint(0, 1000) 325 chartWidth = 0 326 # counter for highest Y-value to determine maximum value 327 highest = 0 328 # bool to show chart legend 329 showLegend = 'false' 330 331 # query DB 332 cursor = db.cursor() 333 cursor.execute(self.subtotalSql) 334 severities = cursor.fetchall() 335 336 337 # loops through severities 338 for severity in severities: 339 ticketCount=outputValues.get(severity[0],0) 340 if severity[0]=='None': 341 ticketCount+=outputValues.get(None,0) 342 componentCols='' 343 344 # if it has multiple components, set up data here 345 # loops through table data so all severities are grouped together by components 346 if len(components)>0: 347 byComponentCounter = xindex 348 outputTable = [] 349 for compName in components.keys(): 350 compCount=components[compName].get(severity[0],0) 351 if severity[0]=='None': 352 compCount+=components[compName].get(None,0) 353 outputTable.append('[%s,%s]' %(byComponentCounter,compCount)) 354 # find highest value 355 if (compCount > highest): 356 highest = compCount 357 byComponentCounter+=(len(severities)+1) 358 # build the js arrays and flot data array here 359 dataArraysTable.append('var d%s = [%s];' %(xindex,",".join(outputTable))) 360 dataSetupArraysTable.append('{label:"%s", data:d%s, bars: {show:true}}' %(severity[0],xindex)) 361 # show severties on x-axis with just one component present 362 # labels incremented by xindex+0.5 to have better presentation 363 else: 364 # set chart div width 365 chartWidth = 280 366 # set up labels 367 optionsArraysTable.append('[%s,"%s"]' %(xindex+0.5,severity[0])) 368 dataSetupArraysTable.append('{label:"%s", data:d%s, bars: {show:true}}' %(severity[0],xindex)) 369 # if statment here so [0,0],[0,0] doesn't happen in which it fails 370 if (ticketCount==0): 371 dataArraysTable.append('var d%s = [[%s,%s]];' %(xindex,xindex,ticketCount)) 372 else: 373 dataArraysTable.append('var d%s = [[0,0],[%s,%s]];' %(xindex,xindex,ticketCount)) 374 outputTable.append('[%s,%s]' %(xindex,ticketCount)) 375 # find highest value 376 if (ticketCount > highest): 377 highest = ticketCount 378 xindex+=1 379 cursor.close() 380 381 # if chart has multiple components, set up labels here 382 if len(components)>0: 383 byComponentCounter = 3 384 for compName in components.keys(): 385 optionsArraysTable.append('[%s,"%s"]' %(byComponentCounter,compName)) 386 byComponentCounter+=len(severities)+1 387 chartWidth = 680 388 showLegend = 'true' 389 390 391 add_stylesheet(formatter.req, 'tracdashboardreports/css/dashboardreports.css') 392 # Not used yet. This method of adding scripts adds them too late in the page 393 # add_script(formatter.req, 'dashboardreports/js/jquery.flot.js') 394 # add_script(formatter.req, 'dashboardreports/js/excanvas.js') 395 396 # random value r is appended to the div id, chartgen so multiple instances of the same chart can be displayed at once 397 return ''' 398 <div class="dashboard_report %s"> 399 400 <span class="dashboard_report_title">%s Defects</span> 401 <span class="dashboard_report_total">Total: %s</span> 402 <div class="chart" style="padding-top: 5px;"> 403 <div id="chartgen%s" style="width:%spx;height:180px" class="%s charts" > 404 </div> 405 406 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/jquery.flot.js"></script> 407 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/excanvas.js"></script> 408 <script id="source" language="javascript" type="text/javascript"> 409 %s 410 411 var data = [%s]; 412 var options = {xaxis: {ticks:[%s]}, yaxis: {max:%s}, legend:{position:"nw", margin:13, show: %s}}; 413 jQuery.plot(jQuery("#chartgen%s"), data, options); 414 415 </script> 416 417 ''' % (reportName, reportName, runningTotal, r, chartWidth, reportName, "\n".join(dataArraysTable), ",".join(dataSetupArraysTable), ",".join(optionsArraysTable), highest+10, showLegend, r) 418 419 class DashboardTicketCountsMacroBase(WikiMacroBase): 420 """ 421 Display a box with that shows ticket count totals 422 423 Implementations should override the REPORTS, container_name, and subtotalSql attributes 424 425 """ 426 abstract = True 427 428 REPORTS = { 429 'new': {'sql':"select count(t.id), t.priority from ticket t where t.status!='closed' group by t.priority;"}, 430 } 431 432 container_name="oforge_dashboard_report" 433 subtotalSql="SELECT s.name, s.value from enum s where s.type = 'priority' ORDER BY s.value" 434 435 def expand_macro(self, formatter, name, content): 436 437 if not content or len(content) == 0: 438 raise Exception("You must specify an argument of open, fixed, new, verified") 439 440 reportName=content 441 if not self.REPORTS.has_key(reportName): 442 raise Exception("Unknown Dashboard for '%s'"%reportName) 443 444 sqlStatement=self.REPORTS[reportName]['sql'] 445 446 outputTable = [] 447 runningTotal = 0 448 449 # 0.10 version - db = self.env.get_db_cnx() 450 db = formatter.db 451 452 cursor = db.cursor() 453 cursor.execute(sqlStatement) 454 455 outputValues = {} 456 components = {} 457 ticketCounts = cursor.fetchall() 458 for ticketCount in ticketCounts: 459 if None in ticketCount or u'' in ticketCount: 460 continue 461 if outputValues.has_key(ticketCount[1]): 462 outputValues[ticketCount[1]]+=ticketCount[0] 463 else: 464 outputValues[ticketCount[1]]=ticketCount[0] 465 if len(ticketCount)==3: 466 if not components.has_key(ticketCount[2]): 467 components[ticketCount[2]]={} 468 components[ticketCount[2]][ticketCount[1]]=ticketCount[0] 469 runningTotal+=ticketCount[0] 470 cursor.close() 471 472 cursor = db.cursor() 473 cursor.execute(self.subtotalSql) 474 severities = cursor.fetchall() 475 for severity in severities: 476 ticketCount=outputValues.get(severity[0],0) 477 if severity[0]=='None': 478 ticketCount+=outputValues.get(None,0) 479 componentCols='' 480 if len(components)>0: 481 for compName in components.keys(): 482 compCount=components[compName].get(severity[0],0) 483 if severity[0]=='None': 484 compCount+=components[compName].get(None,0) 485 componentCols+='<td>%s</td>'%(compCount) 486 outputTable.append(' <tr><td>%s</td>%s<td>%s</td></tr>' % (severity[0], componentCols, ticketCount)) 487 cursor.close() 488 colHdrs=['<th>%s</th>'%key for key in components.keys()] 489 490 add_stylesheet(formatter.req, 'tracdashboardreports/css/dashboardreports.css') 491 492 return ''' 493 <div class="dashboard_report %s"> 494 <span class="dashboard_report_title">%s Defects</span> 495 <span class="dashboard_report_total">Total: %s</span> 496 <table class="dashboard_report_table %s" border="1"> 497 <tr><th>priority</th>%s<th>Total</th></tr> 498 %s 499 </table> 500 </div> 501 ''' % ( reportName, reportName, runningTotal, reportName, "".join(colHdrs), "\n".join(outputTable)) -
trunk/plugins/dashboardreportsplugin/tracdashboardreports/macros.py
r542 r715 4 4 # 5 5 6 import datetime7 import random8 6 from trac.core import * 9 from trac.wiki.macros import WikiMacroBase 10 from genshi.builder import tag 11 from trac.web.chrome import add_script, add_stylesheet, ITemplateProvider 7 from trac.ticket.query import Query 8 from trac.web.chrome import add_script, add_stylesheet, ITemplateProvider, Chrome 9 from trac.wiki.macros import WikiMacroBase, parse_args 10 from trac.util.html import html 11 from trac.util.text import unicode_urlencode 12 12 13 13 14 class DashboardTicketCountsMacroChartsBurnupBase(WikiMacroBase): 15 """ 16 Display a box that shows a burnup chart of tickets 14 from tracdashboardreports.api import TicketCountQueryBaseMacro, \ 15 DashboardTicketCountsMacroBase, \ 16 DashboardTicketCountsMacroChartsBurnupBase, \ 17 DashboardTicketCountsMacroChartsBurndownBase, \ 18 DashboardTicketCountsMacroChartsBase 19 20 class TicketCountQueryMacro(TicketCountQueryBaseMacro): 21 """It creates a table with ticket counts for a specified trac query and 22 grouped by a specific ticket field. 17 23 18 """ 19 abstract = True 24 Example: `[[TicketCountQuery(query=status=!closed&changetime=today;, groupby=priorty|milestone, title=Ticket Count, class=widget450)]]` 25 """ 26 def expand_macro(self, formatter, name, content): 27 args, kwargs = parse_args(content) 28 29 query_string = self.populate_variables(req=formatter, query_string=kwargs.get('query', 'col=id')) 30 groupby = kwargs.get('groupby', 'milestone').split('|') 31 title = kwargs.get('title') 32 class_ = kwargs.get('class','') 33 34 query = Query.from_string(self.env, query_string) 35 data = dict(title=title, query=query, axis=groupby, unicode_urlencode=unicode_urlencode) 36 37 # axis values 38 for groupby_item in groupby: 39 if not groupby_item in query.cols: 40 query.cols.append(groupby_item) 41 data.setdefault('axis_values', {})[groupby_item] = ['']+list(self.get_axis_values(groupby_item)) 42 43 data['counts'] = dict(self.get_count_per_group(formatter.req, query, groupby)) 44 45 # render result 46 template = Chrome(self.env).load_template('filtercountquery.html' ,method='xhtml') 47 data = Chrome(self.env).populate_data(formatter.req, data) 48 rendered_result = template.generate(**data) 49 50 add_stylesheet(formatter.req, 'tracdashboardreports/css/widgets.css') 51 52 return html.div(rendered_result, class_ = 'new-wdgt '+(len(groupby)>1 and ' w710 ' or '')+class_) 20 53 54 class RoadmapProgressMacro(WikiMacroBase): 21 55 def expand_macro(self, formatter, name, content): 56 raise TracError("Work in progress") 22 57 23 # sql for burnup/burndown charts 24 sqlStatement="select count(t.id), t.priority, c1.value as estimate_hours, c2.value as actual_hours, t.changetime from ticket t LEFT OUTER JOIN ticket_custom c1 ON (t.id = c1.ticket AND c1.name = 'estimate_hours') LEFT OUTER JOIN ticket_custom c2 ON (t.id = c2.ticket AND c2.name = 'actual_hours') LEFT OUTER JOIN ticket_custom c ON (t.id = c.ticket AND c.name = 'resolution2') where c.value = 'Fixed' or t.status=='closed' group by t.id;" 25 26 # arrays to store graph coordinates 27 estimateHoursData = [] 28 actualHoursData = [] 29 30 # variable to tally tickets 31 runningTotal = 0 32 33 # temp variable to increment x-value 34 xindex = 1 35 36 # sum of all actual/estimate values 37 yActualTotal = 0 38 yEstimateTotal = 0 39 40 # query DB 41 db = formatter.db 42 cursor = db.cursor() 43 cursor.execute(sqlStatement) 44 45 outputValues = {} 46 components = {} 47 ticketCounts = cursor.fetchall() 48 for ticketCount in ticketCounts: 49 if None in ticketCount or u'' in ticketCount: 50 continue 51 # get ticket count 52 runningTotal+=ticketCount[0] 53 # get date of last modification 54 changetime = datetime.datetime.fromtimestamp(ticketCount[4]) 55 yActualTotal+=int(ticketCount[3]) 56 yEstimateTotal+=int(ticketCount[2]) 57 # temp variable to have different x values bc all change dates are the same for some reason 58 #actualHoursData.append('[%s,%s]' %(changetime.strftime("%W"), yActualTotal)) 59 actualHoursData.append('[%s,%s]' %(xindex, yActualTotal)) 60 estimateHoursData.append('[%s,%s]' %(xindex, yEstimateTotal)) 61 xindex+=1 62 cursor.close() 63 64 add_stylesheet(formatter.req, 'dashboardreports/css/dashboardreports.css') 65 66 # Not used yet. This method of adding scripts adds them too late in the page 67 # add_script(formatter.req, 'dashboardreports/js/jquery.flot.js') 68 # add_script(formatter.req, 'dashboardreports/js/excanvas.js') 69 70 return ''' 71 <div class="dashboard_report burn"> 72 73 <span class="dashboard_report_title">Burnup Chart</span> 74 <span class="dashboard_report_total">Total: %s</span> 75 <div class="chart" style="padding-top: 5px;"> 76 <div id="chartgen_burnup" style="width:600px;height:180px" class="burn charts" > 77 </div> 78 79 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/jquery.flot.js"></script> 80 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/excanvas.js"></script> 81 <script id="source" language="javascript" type="text/javascript"> 82 var data_actual = [[0,0],%s]; 83 var data_estimate = [[0,0],%s]; 84 85 var data = [{label:"actual", data:data_actual}, {label:"estimate", data:data_estimate}]; 86 var options = {legend:{position:"se", margin:13, show:true}}; 87 jQuery.plot(jQuery("#chartgen_burnup"), data, options); 88 89 </script> 90 91 ''' % (runningTotal, ",".join(actualHoursData), ",".join(estimateHoursData)) 92 93 class DashboardTicketCountsMacroChartsBurndownBase(WikiMacroBase): 94 """ 95 Display a box that shows a burndown chart of tickets 96 97 """ 98 abstract = True 99 58 class TicketStatusMacro(WikiMacroBase): 100 59 def expand_macro(self, formatter, name, content): 101 102 # sql for burnup/burndown charts 103 sqlStatement_closed="select count(t.id), t.priority, c1.value as estimate_hours, c2.value as actual_hours, t.changetime from ticket t LEFT OUTER JOIN ticket_custom c1 ON (t.id = c1.ticket AND c1.name = 'estimate_hours') LEFT OUTER JOIN ticket_custom c2 ON (t.id = c2.ticket AND c2.name = 'actual_hours') LEFT OUTER JOIN ticket_custom c ON (t.id = c.ticket AND c.name = 'resolution2') where c.value = 'Fixed' or t.status=='closed' group by t.id;" 104 sqlStatement_not_closed="select count(t.id), t.priority, c1.value as estimate_hours, c2.value as actual_hours, t.changetime from ticket t LEFT OUTER JOIN ticket_custom c1 ON (t.id = c1.ticket AND c1.name = 'estimate_hours') LEFT OUTER JOIN ticket_custom c2 ON (t.id = c2.ticket AND c2.name = 'actual_hours') LEFT OUTER JOIN ticket_custom c ON (t.id = c.ticket AND c.name = 'resolution2') where c.value != 'Fixed' or t.status!='closed' group by t.id;" 105 106 # arrays to store graph coordinates 107 estimateHoursData = [] 108 actualHoursData = [] 109 110 # track ticket count 111 runningTotal = 0 112 113 # temp variable to increment x-value 114 xindex = 0 115 116 # sum of all actual/estimate hours (10 is just to have an example of project not yet finished) 117 yActualTotal = 10 118 yEstimateTotal = 10 119 120 # query DB for closed tickets 121 db = formatter.db 122 cursor_closed = db.cursor() 123 cursor_closed.execute(sqlStatement_closed) 124 125 # query DB for not closed tickets 126 cursor_not_closed = db.cursor() 127 cursor_not_closed.execute(sqlStatement_not_closed) 128 129 outputValues = {} 130 components = {} 131 ticketCounts = cursor_closed.fetchall() 132 ticketCounts_not_closed = cursor_not_closed.fetchall() 133 134 # go through not closed tickets to get total actual/estimate hours (how far up the chart starts) 135 for ticketCount in ticketCounts_not_closed: 136 yActualTotal+=int(ticketCount[2]) 137 yEstimateTotal+=int(ticketCount[3]) 138 # add actual/estimate hours from closed tickets 139 for ticketCount in ticketCounts: 140 if None in ticketCount or u'' in ticketCount: 141 continue 142 yActualTotal+=int(ticketCount[2]) 143 yEstimateTotal+=int(ticketCount[3]) 144 145 for ticketCount in ticketCounts: 146 if None in ticketCount or u'' in ticketCount: 147 continue 148 runningTotal+=ticketCount[0] 149 changetime = datetime.datetime.fromtimestamp(ticketCount[4]) 150 yActualTotal-=int(ticketCount[2]) 151 yEstimateTotal-=int(ticketCount[3]) 152 # temp variable to have different x values bc all change dates are the same for some reason 153 # actualHoursData.append('[%s,%s]' %(changetime.strftime("%W"), actualHours)) 154 actualHoursData.append('[%s,%s]' %(xindex, yActualTotal)) 155 estimateHoursData.append('[%s,%s]' %(xindex, yEstimateTotal)) 156 xindex+=1 157 cursor_closed.close() 158 cursor_not_closed.close() 159 160 add_stylesheet(formatter.req, 'dashboardreports/css/dashboardreports.css') 161 162 # Not used yet. This method of adding scripts adds them too late in the page 163 # add_script(formatter.req, 'dashboardreports/js/jquery.flot.js') 164 # add_script(formatter.req, 'dashboardreports/js/excanvas.js') 165 166 return ''' 167 <div class="dashboard_report burn"> 168 169 <span class="dashboard_report_title">Burndown Chart</span> 170 <span class="dashboard_report_total">Total: %s</span> 171 <div class="chart" style="padding-top: 5px;"> 172 <div id="chartgen_burndown" style="width:600px;height:180px" class="burn charts" > 173 </div> 174 175 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/jquery.flot.js"></script> 176 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/excanvas.js"></script> 177 <script id="source" language="javascript" type="text/javascript"> 178 var data_actual = [%s]; 179 var data_estimate = [%s]; 180 181 var data = [{label:"actual", data:data_actual}, {label:"estimate", data:data_estimate}]; 182 var options = {legend:{position:"ne", margin:13, show: true}}; 183 jQuery.plot(jQuery("#chartgen_burndown"), data, options); 184 185 </script> 186 187 ''' % (runningTotal, ",".join(actualHoursData), ",".join(estimateHoursData)) 188 189 class DashboardTicketCountsMacroChartsBase(WikiMacroBase): 190 """ 191 Display a box with that shows ticket count totals in a bar graph using flot 192 193 Implementations should override the REPORTS, container_name, and subtotalSql attributes 194 195 """ 196 abstract = True 197 198 REPORTS = { 199 'new': {'sql':"select count(t.id), t.priority from ticket t where t.status!='closed' group by t.priority;"}, 200 } 201 202 container_name="oforge_dashboard_report" 203 subtotalSql="SELECT s.name, s.value from enum s where s.type = 'priority' ORDER BY s.value" 204 205 def expand_macro(self, formatter, name, content): 206 207 if not content or len(content) == 0: 208 raise Exception("You must specify an argument of open, fixed, new, verified") 209 210 reportName=content 211 if not self.REPORTS.has_key(reportName): 212 raise Exception("Unknown Dashboard for '%s'"%reportName) 213 214 sqlStatement=self.REPORTS[reportName]['sql'] 215 216 outputTable = [] 217 dataArraysTable = [] 218 dataSetupArraysTable = [] 219 optionsArraysTable = [] 220 runningTotal = 0 221 xindex = 0 222 223 # query DB 224 db = formatter.db 225 cursor = db.cursor() 226 cursor.execute(sqlStatement) 227 228 outputValues = {} 229 components = {} 230 ticketCounts = cursor.fetchall() 231 # check for by-component 232 for ticketCount in ticketCounts: 233 if None in ticketCount or u'' in ticketCount: 234 continue 235 if outputValues.has_key(ticketCount[1]): 236 outputValues[ticketCount[1]]+=ticketCount[0] 237 else: 238 outputValues[ticketCount[1]]=ticketCount[0] 239 if len(ticketCount)==3: 240 if not components.has_key(ticketCount[2]): 241 components[ticketCount[2]]={} 242 components[ticketCount[2]][ticketCount[1]]=ticketCount[0] 243 runningTotal+=ticketCount[0] 244 cursor.close() 245 246 # counter for components 247 byComponentCounter = 0 248 # random number (to append to div) 249 r = random.randint(0, 1000) 250 chartWidth = 0 251 # counter for highest Y-value to determine maximum value 252 highest = 0 253 # bool to show chart legend 254 showLegend = 'false' 255 256 # query DB 257 cursor = db.cursor() 258 cursor.execute(self.subtotalSql) 259 severities = cursor.fetchall() 260 261 262 # loops through severities 263 for severity in severities: 264 ticketCount=outputValues.get(severity[0],0) 265 if severity[0]=='None': 266 ticketCount+=outputValues.get(None,0) 267 componentCols='' 268 269 # if it has multiple components, set up data here 270 # loops through table data so all severities are grouped together by components 271 if len(components)>0: 272 byComponentCounter = xindex 273 outputTable = [] 274 for compName in components.keys(): 275 compCount=components[compName].get(severity[0],0) 276 if severity[0]=='None': 277 compCount+=components[compName].get(None,0) 278 outputTable.append('[%s,%s]' %(byComponentCounter,compCount)) 279 # find highest value 280 if (compCount > highest): 281 highest = compCount 282 byComponentCounter+=(len(severities)+1) 283 # build the js arrays and flot data array here 284 dataArraysTable.append('var d%s = [%s];' %(xindex,",".join(outputTable))) 285 dataSetupArraysTable.append('{label:"%s", data:d%s, bars: {show:true}}' %(severity[0],xindex)) 286 # show severties on x-axis with just one component present 287 # labels incremented by xindex+0.5 to have better presentation 288 else: 289 # set chart div width 290 chartWidth = 280 291 # set up labels 292 optionsArraysTable.append('[%s,"%s"]' %(xindex+0.5,severity[0])) 293 dataSetupArraysTable.append('{label:"%s", data:d%s, bars: {show:true}}' %(severity[0],xindex)) 294 # if statment here so [0,0],[0,0] doesn't happen in which it fails 295 if (ticketCount==0): 296 dataArraysTable.append('var d%s = [[%s,%s]];' %(xindex,xindex,ticketCount)) 297 else: 298 dataArraysTable.append('var d%s = [[0,0],[%s,%s]];' %(xindex,xindex,ticketCount)) 299 outputTable.append('[%s,%s]' %(xindex,ticketCount)) 300 # find highest value 301 if (ticketCount > highest): 302 highest = ticketCount 303 xindex+=1 304 cursor.close() 305 306 # if chart has multiple components, set up labels here 307 if len(components)>0: 308 byComponentCounter = 3 309 for compName in components.keys(): 310 optionsArraysTable.append('[%s,"%s"]' %(byComponentCounter,compName)) 311 byComponentCounter+=len(severities)+1 312 chartWidth = 680 313 showLegend = 'true' 314 315 316 add_stylesheet(formatter.req, 'dashboardreports/css/dashboardreports.css') 317 # Not used yet. This method of adding scripts adds them too late in the page 318 # add_script(formatter.req, 'dashboardreports/js/jquery.flot.js') 319 # add_script(formatter.req, 'dashboardreports/js/excanvas.js') 320 321 # random value r is appended to the div id, chartgen so multiple instances of the same chart can be displayed at once 322 return ''' 323 <div class="dashboard_report %s"> 324 325 <span class="dashboard_report_title">%s Defects</span> 326 <span class="dashboard_report_total">Total: %s</span> 327 <div class="chart" style="padding-top: 5px;"> 328 <div id="chartgen%s" style="width:%spx;height:180px" class="%s charts" > 329 </div> 330 331 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/jquery.flot.js"></script> 332 <script language="javascript" type="text/javascript" src="../chrome/dashboardreports/js/excanvas.js"></script> 333 <script id="source" language="javascript" type="text/javascript"> 334 %s 335 336 var data = [%s]; 337 var options = {xaxis: {ticks:[%s]}, yaxis: {max:%s}, legend:{position:"nw", margin:13, show: %s}}; 338 jQuery.plot(jQuery("#chartgen%s"), data, options); 339 340 </script> 341 342 ''' % (reportName, reportName, runningTotal, r, chartWidth, reportName, "\n".join(dataArraysTable), ",".join(dataSetupArraysTable), ",".join(optionsArraysTable), highest+10, showLegend, r) 343 344 class DashboardTicketCountsMacroBase(WikiMacroBase): 345 """ 346 Display a box with that shows ticket count totals 347 348 Implementations should override the REPORTS, container_name, and subtotalSql attributes 349 350 """ 351 abstract = True 352 353 REPORTS = { 354 'new': {'sql':"select count(t.id), t.priority from ticket t where t.status!='closed' group by t.priority;"}, 355 } 356 357 container_name="oforge_dashboard_report" 358 subtotalSql="SELECT s.name, s.value from enum s where s.type = 'priority' ORDER BY s.value" 359 360 def expand_macro(self, formatter, name, content): 361 362 if not content or len(content) == 0: 363 raise Exception("You must specify an argument of open, fixed, new, verified") 364 365 reportName=content 366 if not self.REPORTS.has_key(reportName): 367 raise Exception("Unknown Dashboard for '%s'"%reportName) 368 369 sqlStatement=self.REPORTS[reportName]['sql'] 370 371 outputTable = [] 372 runningTotal = 0 373 374 # 0.10 version - db = self.env.get_db_cnx() 375 db = formatter.db 376 377 cursor = db.cursor() 378 cursor.execute(sqlStatement) 379 380 outputValues = {} 381 components = {} 382 ticketCounts = cursor.fetchall() 383 for ticketCount in ticketCounts: 384 if None in ticketCount or u'' in ticketCount: 385 continue 386 if outputValues.has_key(ticketCount[1]): 387 outputValues[ticketCount[1]]+=ticketCount[0] 388 else: 389 outputValues[ticketCount[1]]=ticketCount[0] 390 if len(ticketCount)==3: 391 if not components.has_key(ticketCount[2]): 392 components[ticketCount[2]]={} 393 components[ticketCount[2]][ticketCount[1]]=ticketCount[0] 394 runningTotal+=ticketCount[0] 395 cursor.close() 396 397 cursor = db.cursor() 398 cursor.execute(self.subtotalSql) 399 severities = cursor.fetchall() 400 for severity in severities: 401 ticketCount=outputValues.get(severity[0],0) 402 if severity[0]=='None': 403 ticketCount+=outputValues.get(None,0) 404 componentCols='' 405 if len(components)>0: 406 for compName in components.keys(): 407 compCount=components[compName].get(severity[0],0) 408 if severity[0]=='None': 409 compCount+=components[compName].get(None,0) 410 componentCols+='<td>%s</td>'%(compCount) 411 outputTable.append(' <tr><td>%s</td>%s<td>%s</td></tr>' % (severity[0], componentCols, ticketCount)) 412 cursor.close() 413 colHdrs=['<th>%s</th>'%key for key in components.keys()] 414 415 add_stylesheet(formatter.req, 'dashboardreports/css/dashboardreports.css') 416 417 return ''' 418 <div class="dashboard_report %s"> 419 <span class="dashboard_report_title">%s Defects</span> 420 <span class="dashboard_report_total">Total: %s</span> 421 <table class="dashboard_report_table %s" border="1"> 422 <tr><th>priority</th>%s<th>Total</th></tr> 423 %s 424 </table> 425 </div> 426 ''' % ( reportName, reportName, runningTotal, reportName, "".join(colHdrs), "\n".join(outputTable)) 60 raise TracError("Work in progress") 427 61 428 62 class DefectSummaryChartsBurnupMacro(DashboardTicketCountsMacroChartsBurnupBase): … … 508 142 def get_htdocs_dirs(self): 509 143 from pkg_resources import resource_filename 510 return [('dashboardreports',resource_filename(__name__, 'htdocs'))] 511 512 144 return [('tracdashboardreports',resource_filename(__name__, 'htdocs'))]
Note: See TracChangeset
for help on using the changeset viewer.
