Edgewall Software

Ticket #5572: milestone_groups-r5762.diff

File milestone_groups-r5762.diff, 9.2 KB (added by cboos, 17 months ago)

Updated patch, the previous one contained quite a number of bugs...

  • trac/ticket/roadmap.py

     
    6363        self.done_percent = 0 
    6464        self.done_count = 0 
    6565 
    66     def add_interval(self, title, count, qry_args, css_class, countsToProg=0): 
     66    def add_interval(self, title, count, qry_args, css_class, 
     67                     overall_completion=None, countsToProg=0): 
    6768        """Adds a division to this stats' group's progress bar. 
    6869 
    6970        `title` is the display name (eg 'closed', 'spent effort') of this 
     
    7273        `qry_args` is a dict of extra params that will yield the subset of 
    7374          tickets in this interval on a query. 
    7475        `css_class` is the css class that will be used to display the division. 
    75         `countsToProg` can be set to true to make this interval count towards 
    76           overall completion of this group of tickets. 
     76        `overall_completion` can be set to true to make this interval count 
     77          towards overall completion of this group of tickets. 
     78           
     79        (Warning: `countsToProg` argument will be removed in 0.12, use 
     80        `overall_completion` instead) 
    7781        """ 
     82        if overall_completion is None: 
     83            overall_completion = countsToProg 
    7884        self.intervals.append({ 
    7985            'title': title, 
    8086            'count': count, 
    8187            'qry_args': qry_args, 
    8288            'css_class': css_class, 
    8389            'percent': None, 
    84             'countsToProg': countsToProg 
     90            'countsToProg': overall_completion, 
     91            'overall_completion': overall_completion, 
    8592        }) 
    8693        self.count = self.count + count 
    8794 
     
    95102            interval['percent'] = round(float(interval['count'] /  
    96103                                        float(self.count) * 100)) 
    97104            total_percent = total_percent + interval['percent'] 
    98             if interval['countsToProg']: 
     105            if interval['overall_completion']: 
    99106                self.done_percent += interval['percent'] 
    100107                self.done_count += interval['count'] 
    101108 
    102109        if self.done_count and total_percent != 100: 
    103             fudge_int = [i for i in self.intervals if i['countsToProg']][0] 
     110            fudge_int = [i for i in self.intervals 
     111                         if i['overall_completion']][0] 
    104112            fudge_amt = 100 - total_percent 
    105113            fudge_int['percent'] += fudge_amt 
    106114            self.done_percent += fudge_amt 
    107115 
     116 
    108117class DefaultTicketGroupStatsProvider(Component): 
     118    """Configurable ticket group statistics provider. 
     119 
     120    Example configuration (which is also the default): 
     121 
     122    [milestone-groups] 
     123    closed = closed                      # a list of accepted status 
     124    closed.order = 0                     # sequence number in the progress bar 
     125    closed.args = group=resolution       # optional extra param for the query 
     126    closed.overall_completion = true     # count for overall completion 
     127 
     128    active = !closed                     # '!' for a list of rejected status 
     129    active.order = 1 
     130    active.css = open                    # css class for this interval 
     131    """ 
     132     
    109133    implements(ITicketGroupStatsProvider) 
    110134 
     135    def _get_ticket_groups(self): 
     136        if 'milestone-groups' in self.config: 
     137            groups = {} 
     138            order = 0 
     139            for option, value in self.config.options('milestone-groups'): 
     140                if '.' in option: 
     141                    name, qualifier = option.split('.', 1) 
     142                    group = groups.get(name) 
     143                    if group: 
     144                        group[qualifier] = value 
     145                else: 
     146                    groups[option] = {'name': option, 'status': value, 
     147                                      'order': order} 
     148                    order += 1 
     149            return [group for group in sorted(groups.values(), 
     150                                              key=lambda g: int(g['order']))] 
     151        else: 
     152            return [{'name': 'closed', 'status': 'closed', 
     153                     'args': 'group=resolution', 'overall_completion': 'true'}, 
     154                    {'name': 'active', 'status': '!closed', 'css': 'open'}] 
     155 
    111156    def get_ticket_group_stats(self, ticket_ids): 
    112157        total_cnt = len(ticket_ids) 
     158        status_cnt = {} 
     159        for s in TicketSystem(self.env).get_all_status(): 
     160            status_cnt[s] = 0 
    113161        if total_cnt: 
     162            active_cnt = ticket_cnt = 0 
    114163            cursor = self.env.get_db_cnx().cursor() 
    115             str_ids = [str(x) for x in sorted(ticket_ids)] 
    116             active_cnt = cursor.execute("SELECT count(1) FROM ticket " 
    117                                         "WHERE status <> 'closed' AND id IN " 
    118                                         "(%s)" % ",".join(str_ids)) 
    119             active_cnt = 0 
     164            cursor.execute("SELECT count(*) FROM ticket") 
    120165            for cnt, in cursor: 
    121                 active_cnt = cnt 
    122         else: 
    123             active_cnt = 0 
     166                ticket_cnt = cnt 
     167            if ticket_ids > ticket_cnt / 4: 
     168                # then it's probably faster to get the status for all tickets 
     169                ids = set(ticket_ids) 
     170                cursor.execute("SELECT id, status FROM ticket") 
     171                for id, s in cursor: 
     172                    if id in ids: 
     173                        status_cnt[s] = status_cnt.get(s, 0) + 1 
     174            else: 
     175                str_ids = [str(x) for x in sorted(ticket_ids)] 
     176                cursor.execute("SELECT status, count(status) FROM ticket " 
     177                               "WHERE id IN (%s) GROUP BY status" % 
     178                               ",".join(str_ids)) 
     179                for s, cnt in cursor: 
     180                    status_cnt[s] = cnt 
    124181 
    125         closed_cnt = total_cnt - active_cnt 
    126  
    127182        stat = TicketGroupStats('ticket status', 'ticket') 
    128         stat.add_interval('closed', closed_cnt, 
    129                           {'status': 'closed', 'group': 'resolution'}, 
    130                           'closed', True) 
    131         stat.add_interval('active', active_cnt, 
    132                           {'status': ['new', 'assigned', 'reopened']}, 
    133                           'open', False) 
     183        for group in self._get_ticket_groups(): 
     184            group_cnt = 0 
     185            accepted = [s.strip() for s in 
     186                        group['status'].replace('!', '').split(',')] 
     187            rejected = [] 
     188            if '!' in group['status']: 
     189                accepted, rejected = rejected, accepted 
     190            query_args = {} 
     191            for s, cnt in status_cnt.iteritems(): 
     192                if s in accepted or (rejected and s not in rejected): 
     193                    group_cnt += cnt 
     194                    query_args.setdefault('status', []).append(s) 
     195            for arg in [kv for kv in group.get('args', '').split(',') 
     196                        if '=' in kv]: 
     197                k, v = [a.strip() for a in arg.split('=', 1)] 
     198                query_args[k] = v 
     199            stat.add_interval(group['name'], group_cnt, query_args, 
     200                              group.get('css', group['name']), 
     201                              group.get('overall_completion', False)) 
    134202        stat.refresh_calcs() 
    135203        return stat 
    136204 
     
    631699 
    632700            for idx, gstat in enumerate(group_stats): 
    633701                gs_dict = milestone_groups[idx] 
    634                 gs_dict['percent_of_max_total'] = (float(gstat.count) / 
    635                                                    float(max_count) * 100) 
     702                percent = 1.0 
     703                if max_count: 
     704                    percent = float(gstat.count) / float(max_count) * 100 
     705                gs_dict['percent_of_max_total'] = percent 
    636706 
    637707        return 'milestone_view.html', data, None 
    638708 
  • trac/ticket/workflows/basic-workflow.ini

     
    2121reopen = closed -> reopened 
    2222reopen.permissions = TICKET_CREATE 
    2323reopen.operations = del_resolution 
     24 
     25[milestone-groups] 
     26closed = closed 
     27closed.order = 0 
     28closed.args = group=resolution 
     29closed.overall_completion = true 
     30 
     31active = assigned,accepted 
     32active.order = 1 
     33active.css = open 
     34 
     35new = new,reopened 
     36new.order = 2 
  • trac/htdocs/css/roadmap.css

     
    1919 text-decoration: none 
    2020} 
    2121table.progress td { background: #fff; padding: 0 } 
     22table.progress td.new { background: #f5f5b5 } 
    2223table.progress td.closed { background: #bae0ba } 
    2324table.progress td :hover { background: none } 
    2425p.percent { font-size: 10px; line-height: 2.4em; margin: 0.9em 0 0 } 
  • trac/templates/macros.html

     
    237237        <dd><a href="${interval_hrefs[idx]}">${interval.count}</a></dd> 
    238238      </py:for> 
    239239      <py:if test="stats_href"> 
    240         <dt>Total ${stats.unit}s:</dt> 
     240        <dt>/ Total ${stats.unit}s:</dt> 
    241241        <dd><a href="${stats_href}">${sum([x.count for x in stats.intervals], 0)}</a></dd> 
    242242      </py:if> 
    243243    </dl>