Edgewall Software

Ticket #5572: milestone_groups-r5946.diff

File milestone_groups-r5946.diff, 13.3 KB (added by cboos, 15 months ago)

updated patch, don't depend on the order of entries in trac.in and a few other fixes

  • trac/ticket/tests/roadmap.py

     
    7474 
    7575        tkt1 = Ticket(self.env) 
    7676        tkt1.populate({'summary': 'Foo', 'milestone': 'Test', 'owner': 'foman', 
    77                         'status': 'open'}) 
     77                        'status': 'new'}) 
    7878        tkt1.insert() 
    7979        tkt2 = Ticket(self.env) 
    8080        tkt2.populate({'summary': 'Bar', 'milestone': 'Test', 
     
    9090         
    9191        prov = DefaultTicketGroupStatsProvider(ComponentManager()) 
    9292        prov.env = self.env 
     93        prov.config = self.env.config 
    9394        self.stats = prov.get_ticket_group_stats([tkt1.id, tkt2.id, tkt3.id]) 
    9495 
    9596    def test_stats(self): 
     
    101102        closed = self.stats.intervals[0] 
    102103        self.assertEquals('closed', closed['title'], 'closed title incorrect') 
    103104        self.assertEquals('closed', closed['css_class'], 'closed class incorrect') 
    104         self.assertEquals(True, closed['countsToProg'], 
    105                           'closed not count to prog') 
    106         self.assertEquals({'status': 'closed', 'group': 'resolution'}, 
     105        self.assertEquals(True, closed['overall_completion'], 
     106                          'closed should contribute to overall completion') 
     107        self.assertEquals({'status': ['closed'], 'group': 'resolution'}, 
    107108                          closed['qry_args'], 'qry_args incorrect') 
    108109        self.assertEquals(1, closed['count'], 'closed count incorrect') 
    109110        self.assertEquals(33, closed['percent'], 'closed percent incorrect') 
     
    112113        open = self.stats.intervals[1] 
    113114        self.assertEquals('active', open['title'], 'open title incorrect') 
    114115        self.assertEquals('open', open['css_class'], 'open class incorrect') 
    115         self.assertEquals(False, open['countsToProg'], 
    116                           'open not count to prog') 
    117         self.assertEquals({'status': ['!closed']}, 
     116        self.assertEquals(False, open['overall_completion'], 
     117                          "open shouldn't contribute to overall completion") 
     118        self.assertEquals({'status': 
     119                           [u'assigned', u'new', u'accepted', u'reopened']}, 
    118120                          open['qry_args'], 'qry_args incorrect') 
    119121        self.assertEquals(2, open['count'], 'open count incorrect') 
    120122        self.assertEquals(67, open['percent'], 'open percent incorrect') 
  • trac/ticket/roadmap.py

     
    2626from trac.context import IContextProvider, Context 
    2727from trac.core import * 
    2828from trac.perm import IPermissionRequestor 
    29 from trac.util import sorted 
     29from trac.util.compat import set, sorted 
    3030from trac.util.datefmt import parse_date, utc, to_timestamp, \ 
    3131                              get_date_format_hint, get_datetime_format_hint 
    3232from trac.util.text import shorten_line, CRLF, to_unicode 
     
    6464        self.done_percent = 0 
    6565        self.done_count = 0 
    6666 
    67     def add_interval(self, title, count, qry_args, css_class, countsToProg=0): 
     67    def add_interval(self, title, count, qry_args, css_class, 
     68                     overall_completion=None, countsToProg=0): 
    6869        """Adds a division to this stats' group's progress bar. 
    6970 
    7071        `title` is the display name (eg 'closed', 'spent effort') of this 
     
    7374        `qry_args` is a dict of extra params that will yield the subset of 
    7475          tickets in this interval on a query. 
    7576        `css_class` is the css class that will be used to display the division. 
    76         `countsToProg` can be set to true to make this interval count towards 
    77           overall completion of this group of tickets. 
     77        `overall_completion` can be set to true to make this interval count 
     78          towards overall completion of this group of tickets. 
     79           
     80        (Warning: `countsToProg` argument will be removed in 0.12, use 
     81        `overall_completion` instead) 
    7882        """ 
     83        if overall_completion is None: 
     84            overall_completion = countsToProg 
    7985        self.intervals.append({ 
    8086            'title': title, 
    8187            'count': count, 
    8288            'qry_args': qry_args, 
    8389            'css_class': css_class, 
    8490            'percent': None, 
    85             'countsToProg': countsToProg 
     91            'countsToProg': overall_completion, 
     92            'overall_completion': overall_completion, 
    8693        }) 
    8794        self.count = self.count + count 
    8895 
     
    96103            interval['percent'] = round(float(interval['count'] /  
    97104                                        float(self.count) * 100)) 
    98105            total_percent = total_percent + interval['percent'] 
    99             if interval['countsToProg']: 
     106            if interval['overall_completion']: 
    100107                self.done_percent += interval['percent'] 
    101108                self.done_count += interval['count'] 
    102109 
     110        # We want the percentages to add up to 100%.  To do that, we fudge the 
     111        # first interval that counts as "completed".  That interval is adjusted 
     112        # by enough to make the intervals sum to 100%. 
    103113        if self.done_count and total_percent != 100: 
    104             fudge_int = [i for i in self.intervals if i['countsToProg']][0] 
     114            fudge_int = [i for i in self.intervals 
     115                         if i['overall_completion']][0] 
    105116            fudge_amt = 100 - total_percent 
    106117            fudge_int['percent'] += fudge_amt 
    107118            self.done_percent += fudge_amt 
    108119 
     120 
    109121class DefaultTicketGroupStatsProvider(Component): 
     122    """Configurable ticket group statistics provider. 
     123 
     124    Example configuration (which is also the default): 
     125    {{{ 
     126    [milestone-groups] 
     127 
     128    # Definition of a 'closed' group: 
     129     
     130    closed = closed 
     131 
     132    # The definition consists in a comma-separated list of accepted status. 
     133    # The list could be prefixed by '!', meaning it's a list  of rejected 
     134    # status instead. 
     135    # Also, '*' means any status and could be used for the last group as a 
     136    # catch-all expression. 
     137 
     138    # Qualifiers for the above group (the group must have been defined first): 
     139     
     140    closed.order = 0                     # sequence number in the progress bar 
     141    closed.args = group=resolution       # optional extra param for the query 
     142    closed.overall_completion = true     # count for overall completion 
     143 
     144    # Definition of an 'active' group: 
     145 
     146    active = * 
     147    active.order = 1 
     148    active.css = open                    # CSS class for this interval 
     149 
     150    # The CSS class can be one of: new (yellow), open (no color) or 
     151    # closed (green). New styles can easily be added using the following 
     152    # selector:  `table.progress td.<class>` 
     153    }}} 
     154    """ 
     155 
    110156    implements(ITicketGroupStatsProvider) 
    111157 
     158    default_milestone_groups =  [ 
     159        {'name': 'closed', 'status': 'closed', 
     160         'args': 'group=resolution', 'overall_completion': 'true'}, 
     161        {'name': 'active', 'status': '*', 'css': 'open'} 
     162        ] 
     163 
     164    def _get_ticket_groups(self): 
     165        """Returns a dict describing the ticket groups used in milestone 
     166        progress bars. 
     167        """ 
     168        if 'milestone-groups' in self.config: 
     169            groups = {} 
     170            order = 0 
     171            for groupname, value in self.config.options('milestone-groups'): 
     172                qualifier = 'status' 
     173                if '.' in groupname: 
     174                    groupname, qualifier = groupname.split('.', 1) 
     175                group = groups.setdefault(groupname, {'name': groupname, 
     176                                                      'order': order}) 
     177                group[qualifier] = value 
     178                order = max(order, int(group['order'])) + 1 
     179            return [group for group in sorted(groups.values(), 
     180                                              key=lambda g: int(g['order']))] 
     181        else: 
     182            return self.default_milestone_groups 
     183 
    112184    def get_ticket_group_stats(self, ticket_ids): 
    113185        total_cnt = len(ticket_ids) 
     186        all_status = set(TicketSystem(self.env).get_all_status()) 
     187        status_cnt = {} 
     188        for s in all_status: 
     189            status_cnt[s] = 0 
    114190        if total_cnt: 
    115191            cursor = self.env.get_db_cnx().cursor() 
    116192            str_ids = [str(x) for x in sorted(ticket_ids)] 
    117             active_cnt = cursor.execute("SELECT count(1) FROM ticket " 
    118                                         "WHERE status <> 'closed' AND id IN " 
    119                                         "(%s)" % ",".join(str_ids)) 
    120             active_cnt = 0 
    121             for cnt, in cursor: 
    122                 active_cnt = cnt 
    123         else: 
    124             active_cnt = 0 
     193            cursor.execute("SELECT status, count(status) FROM ticket " 
     194                           "WHERE id IN (%s) GROUP BY status" % 
     195                           ",".join(str_ids)) 
     196            for s, cnt in cursor: 
     197                status_cnt[s] = cnt 
    125198 
    126         closed_cnt = total_cnt - active_cnt 
    127  
    128199        stat = TicketGroupStats('ticket status', 'ticket') 
    129         stat.add_interval('closed', closed_cnt, 
    130                           {'status': 'closed', 'group': 'resolution'}, 
    131                           'closed', True) 
    132         stat.add_interval('active', active_cnt, 
    133                           {'status': ['!closed']}, 
    134                           'open', False) 
     200        remaining_status = set(all_status) 
     201        for group in self._get_ticket_groups(): 
     202            group_cnt = 0 
     203            status_str = group['status'].strip() 
     204            invert = False 
     205            if status_str == '*': 
     206                group_status = remaining_status 
     207                remaining_status = set() 
     208            else: 
     209                if status_str.startswith('!'): 
     210                    status_str = status_str[1:] 
     211                    invert = True 
     212                group_status = set([s.strip() for s in status_str.split(',')])\ 
     213                               & all_status 
     214                if invert: 
     215                    remaining_status = group_status # see XOR trick below 
     216                elif group_status - remaining_status: 
     217                    raise TracError(_( 
     218                        "'%(groupname)s' milestone group reused status " 
     219                        "'%(status)s' already taken by other groups. " 
     220                        "Please check your configuration.", 
     221                        groupname=group['name'], 
     222                        status=', '.join(group_status - remaining_status))) 
     223                else: 
     224                    remaining_status -= group_status 
     225            query_args = {} 
     226            for s, cnt in status_cnt.iteritems(): 
     227                if (s in group_status) ^ invert: 
     228                    group_cnt += cnt 
     229                    query_args.setdefault('status', []).append(s) 
     230            for arg in [kv for kv in group.get('args', '').split(',') 
     231                        if '=' in kv]: 
     232                k, v = [a.strip() for a in arg.split('=', 1)] 
     233                query_args[k] = v 
     234            stat.add_interval(group['name'], group_cnt, query_args, 
     235                              group.get('css', group['name']), 
     236                              bool(group.get('overall_completion'))) 
    135237        stat.refresh_calcs() 
    136238        return stat 
    137239 
     
    636738 
    637739            for idx, gstat in enumerate(group_stats): 
    638740                gs_dict = milestone_groups[idx] 
    639                 gs_dict['percent_of_max_total'] = (float(gstat.count) / 
    640                                                    float(max_count) * 100) 
     741                percent = 1.0 
     742                if max_count: 
     743                    percent = float(gstat.count) / float(max_count) * 100 
     744                gs_dict['percent_of_max_total'] = percent 
    641745 
    642746        return 'milestone_view.html', data, None 
    643747 
  • trac/ticket/workflows/basic-workflow.ini

     
    3030reopen = closed -> reopened 
    3131reopen.permissions = TICKET_CREATE 
    3232reopen.operations = del_resolution 
     33 
     34[milestone-groups] 
     35closed = closed 
     36closed.order = 0 
     37closed.args = group=resolution 
     38closed.overall_completion = true 
     39 
     40active = assigned,accepted 
     41active.order = 1 
     42active.css = open 
     43 
     44new = new,reopened 
     45new.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>