Changeset 715


Ignore:
Timestamp:
11/11/08 10:38:57 (5 years ago)
Author:
cbalan
Message:

Dashboard: - Removed queryfiltermacro.

Location:
trunk/plugins/dashboardreportsplugin/tracdashboardreports
Files:
1 deleted
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/plugins/dashboardreportsplugin/tracdashboardreports/api.py

    r662 r715  
    44# 
    55 
    6 from datetime import date, datetime 
    7 import random 
    8 import time 
    9  
    106from trac.core import * 
    117from trac.wiki.macros import WikiMacroBase 
    12 from genshi.builder import tag 
    13 from trac.web.chrome import add_script, add_stylesheet, ITemplateProvider, Chrome 
    148from 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 
     10class TicketCountQueryBaseMacro(WikiMacroBase): 
    2911    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 
    3419        """ 
    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() 
    35021        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 
    35426        except: 
    35527            db.rollback() 
    35628            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 
    36136        """ 
    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   
    36469        """ 
    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 
    38080        """ 
    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 
     90class DashboardTicketCountsMacroChartsBurnupBase(WikiMacroBase): 
     91    """Display a box that shows a burnup chart of tickets 
     92     
    49393    """ 
    49494    abstract = True 
    49595 
    496     template = None 
    497      
    49896    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 
    531106        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 = {} 
    533121        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 
     168class 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 
     264class 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] 
    544312            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 
     419class 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  
    44# 
    55 
    6 import datetime 
    7 import random 
    86from 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 
     7from trac.ticket.query import Query 
     8from trac.web.chrome import add_script, add_stylesheet, ITemplateProvider, Chrome 
     9from trac.wiki.macros import WikiMacroBase, parse_args 
     10from trac.util.html import html 
     11from trac.util.text import unicode_urlencode 
    1212 
    1313 
    14 class DashboardTicketCountsMacroChartsBurnupBase(WikiMacroBase): 
    15     """ 
    16     Display a box that shows a burnup chart of tickets 
     14from tracdashboardreports.api import TicketCountQueryBaseMacro, \ 
     15                                    DashboardTicketCountsMacroBase, \ 
     16                                    DashboardTicketCountsMacroChartsBurnupBase, \ 
     17                                    DashboardTicketCountsMacroChartsBurndownBase, \ 
     18                                    DashboardTicketCountsMacroChartsBase 
     19 
     20class TicketCountQueryMacro(TicketCountQueryBaseMacro): 
     21    """It creates a table with ticket counts for a specified trac query and  
     22    grouped by a specific ticket field. 
    1723     
    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_) 
    2053 
     54class RoadmapProgressMacro(WikiMacroBase): 
    2155    def expand_macro(self, formatter, name, content): 
     56        raise TracError("Work in progress") 
    2257 
    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  
     58class TicketStatusMacro(WikiMacroBase): 
    10059    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") 
    42761 
    42862class DefectSummaryChartsBurnupMacro(DashboardTicketCountsMacroChartsBurnupBase): 
     
    508142    def get_htdocs_dirs(self): 
    509143        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.