Edgewall Software

Ticket #2314: mstone_stat_ext_r2514.patch

File mstone_stat_ext_r2514.patch, 31.3 KB (added by trac@…, 3 years ago)

patch implementing this feature

  • trac/ticket/tests/__init__.py

     
    11import unittest 
    22 
    3 from trac.ticket.tests import api, model, query 
     3from trac.ticket.tests import api, model, query, roadmap 
    44 
    55def suite(): 
    66    suite = unittest.TestSuite() 
    77    suite.addTest(api.suite()) 
    88    suite.addTest(model.suite()) 
    99    suite.addTest(query.suite()) 
     10    suite.addTest(roadmap.suite()) 
    1011    return suite 
    1112 
    1213if __name__ == '__main__': 
  • trac/ticket/tests/roadmap.py

     
     1from trac.config import Configuration 
     2from trac.test import EnvironmentStub 
     3from trac.ticket.roadmap import * 
     4from trac.core import ComponentManager 
     5 
     6import unittest 
     7 
     8class TicketGroupStatsTestCase(unittest.TestCase): 
     9 
     10    def setUp(self): 
     11        self.stats = TicketGroupStats('title', 'unit') 
     12 
     13    def test_init(self): 
     14        self.assertEquals('title', self.stats.title, 'title incorrect') 
     15        self.assertEquals('unit', self.stats.unit, 'unit incorrect') 
     16        self.assertEquals(0, self.stats.count, 'count not zero') 
     17        self.assertEquals(0, len(self.stats.intervals), 'intervals not empty') 
     18 
     19    def test_add_iterval(self): 
     20        self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0) 
     21        self.assertEquals(3, self.stats.count, 'count not incremented') 
     22        int = self.stats.intervals[0] 
     23        self.assertEquals('intTitle', int['title'], 'title incorrect') 
     24        self.assertEquals(3, int['count'], 'count incorrect') 
     25        self.assertEquals({'k1': 'v1'}, int['qry_args'], 'query args incorrect') 
     26        self.assertEquals('css', int['class'], 'css class incorrect') 
     27        self.assertEquals(100, int['percent'], 'percent incorrect') 
     28        self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0) 
     29        self.assertEquals(50, int['percent'], 'percent not being updated') 
     30 
     31    def test_add_interval_no_prog(self): 
     32        self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0) 
     33        self.stats.add_interval('intTitle', 5, {'k1': 'v1'}, 'css', 0) 
     34        int = self.stats.intervals[1] 
     35        self.assertEquals(0, self.stats.done_count, 'count added for no prog') 
     36        self.assertEquals(0, self.stats.done_percent, 'percent incremented') 
     37 
     38    def test_add_interval_prog(self): 
     39        self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0) 
     40        self.stats.add_interval('intTitle', 1, {'k1': 'v1'}, 'css', 1) 
     41        self.assertEquals(4, self.stats.count, 'count not incremented') 
     42        self.assertEquals(1, self.stats.done_count, 'count not added to prog') 
     43        self.assertEquals(25, self.stats.done_percent, 'done percent not incr') 
     44 
     45    def test_add_interval_fudging(self): 
     46        self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0) 
     47        self.stats.add_interval('intTitle', 5, {'k1': 'v1'}, 'css', 1) 
     48        self.assertEquals(8, self.stats.count, 'count not incremented') 
     49        self.assertEquals(5, self.stats.done_count, 'count not added to prog') 
     50        self.assertEquals(62, self.stats.done_percent, 
     51                          'done percnt not fudged downward') 
     52        self.assertEquals(62, self.stats.intervals[1]['percent'], 
     53                          'interval percent not fudged downward') 
     54        self.assertEquals(38, self.stats.intervals[0]['percent'], 
     55                          'interval percent not fudged upward') 
     56 
     57 
     58class DefaultTicketGroupStatsProviderTestCase(unittest.TestCase): 
     59 
     60    def setUp(self): 
     61        self.env = EnvironmentStub(default_data=True) 
     62        tkt1 = Ticket(self.env) 
     63        tkt1.populate({'summary': 'Foo', 'milestone': 'Test', 'owner': 'foman', 
     64                        'status': 'open'}) 
     65        tkt2 = Ticket(self.env) 
     66        tkt2.populate({'summary': 'Bar', 'milestone': 'Test', 
     67                        'status': 'closed', 'owner': 'barman'}) 
     68        tkt3 = Ticket(self.env) 
     69        tkt3.populate({'summary': 'Sum', 'milestone': 'Test', 'owner': 'suman', 
     70                        'status': 'reopened'}) 
     71        prov = DefaultTicketGroupStatsProvider(ComponentManager()) 
     72        self.stats = prov.get_ticket_group_stats([tkt1, tkt2, tkt3]) 
     73 
     74    def test_stats(self): 
     75        self.assertEquals(self.stats.title, 'ticket status', 'title incorrect') 
     76        self.assertEquals(self.stats.unit, 'ticket', 'unit incorrect') 
     77        self.assertEquals(2, len(self.stats.intervals), 'more than 2 intervals') 
     78 
     79    def test_closed_interval(self): 
     80        closed = self.stats.intervals[0] 
     81        self.assertEquals('closed', closed['title'], 'closed title incorrect') 
     82        self.assertEquals('closed', closed['class'], 'closed class incorrect') 
     83        self.assertEquals(True, closed['countsToProg'], 
     84                          'closed not count to prog') 
     85        self.assertEquals({'status': 'closed'}, closed['qry_args'], 
     86                          'qry_args incorrect') 
     87        self.assertEquals(1, closed['count'], 'closed count incorrect') 
     88        self.assertEquals(33, closed['percent'], 'closed percent incorrect') 
     89 
     90    def test_open_interval(self): 
     91        open = self.stats.intervals[1] 
     92        self.assertEquals('active', open['title'], 'open title incorrect') 
     93        self.assertEquals('open', open['class'], 'open class incorrect') 
     94        self.assertEquals(False, open['countsToProg'], 
     95                          'open not count to prog') 
     96        self.assertEquals({'status': ['new', 'assigned', 'reopened']}, 
     97                          open['qry_args'], 'qry_args incorrect') 
     98        self.assertEquals(2, open['count'], 'open count incorrect') 
     99        self.assertEquals(67, open['percent'], 'open percent incorrect') 
     100 
     101 
     102def in_tlist(ticket, list): 
     103    return len([t for t in list if t['id'] == ticket.id]) > 0 
     104 
     105class RoadmapModuleTestCase(unittest.TestCase): 
     106 
     107    def setUp(self): 
     108        self.env = EnvironmentStub(default_data=True) 
     109 
     110        self.milestone1 = Milestone(self.env) 
     111        self.milestone1.name = 'Test' 
     112        self.milestone1.insert() 
     113        self.milestone2 = Milestone(self.env) 
     114        self.milestone2.name = 'Test2' 
     115        self.milestone2.insert() 
     116 
     117        tkt1 = Ticket(self.env) 
     118        tkt1.populate({'summary': 'Foo', 'milestone': 'Test', 'owner': 'foman'}) 
     119        tkt1.insert() 
     120        tkt2 = Ticket(self.env) 
     121        tkt2.populate({'summary': 'Bar', 'milestone': 'Test2', 
     122                        'status': 'closed', 'owner': 'barman'}) 
     123        tkt2.insert() 
     124        tkt3 = Ticket(self.env) 
     125        tkt3.populate({'summary': 'Sum', 'milestone': 'Test', 'owner': 'suman'}) 
     126        tkt3.insert() 
     127        self.tkt1 = tkt1 
     128        self.tkt2 = tkt2 
     129        self.tkt3 = tkt3 
     130 
     131    def test_get_tickets_for_milestone(self): 
     132        tkts1 = get_tickets_for_milestone(self.env, self.milestone1.name) 
     133        tkts2 = get_tickets_for_milestone(self.env, self.milestone2.name) 
     134        self.assertTrue(in_tlist(self.tkt1, tkts1) and in_tlist(self.tkt3, tkts1), 
     135                         'tickets that should be returned were not') 
     136        self.assertFalse(in_tlist(self.tkt2, tkts1), 
     137                         'tickets that should not have been returned were') 
     138        self.assertTrue(in_tlist(self.tkt2, tkts2), 'multiple milestones wrong') 
     139 
     140 
     141def suite(): 
     142    suite = unittest.TestSuite() 
     143    suite.addTest(unittest.makeSuite(TicketGroupStatsTestCase, 'test')) 
     144    suite.addTest(unittest.makeSuite(DefaultTicketGroupStatsProviderTestCase, 
     145                                      'test')) 
     146    suite.addTest(unittest.makeSuite(RoadmapModuleTestCase, 'test')) 
     147    return suite 
     148 
     149if __name__ == '__main__': 
     150    unittest.main(defaultTest='suite') 
     151 No newline at end of file 
  • trac/ticket/roadmap.py

     
    2323from trac.util import escape, format_date, format_datetime, parse_date, \ 
    2424                      pretty_timedelta, shorten_line, CRLF 
    2525from trac.ticket import Milestone, Ticket, TicketSystem 
     26from trac.ticket.query import Query 
    2627from trac.Timeline import ITimelineEventProvider 
    2728from trac.web import IRequestHandler 
    2829from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 
    2930from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider 
    3031 
    31 def get_tickets_for_milestone(env, db, milestone, field='component'): 
    32     cursor = db.cursor() 
    33     fields = TicketSystem(env).get_ticket_fields() 
    34     if field in [f['name'] for f in fields if not f.get('custom')]: 
    35         cursor.execute("SELECT id,status,%s FROM ticket WHERE milestone=%%s " 
    36                        "ORDER BY %s" % (field, field), (milestone,)) 
    37     else: 
    38         cursor.execute("SELECT id,status,value FROM ticket LEFT OUTER " 
    39                        "JOIN ticket_custom ON (id=ticket AND name=%s) " 
    40                        "WHERE milestone=%s ORDER BY value", (field, milestone)) 
    41     tickets = [] 
    42     for tkt_id, status, fieldval in cursor: 
    43         tickets.append({'id': tkt_id, 'status': status, field: fieldval}) 
    44     return tickets 
    4532 
    46 def get_query_links(env, milestone, grouped_by='component', group=None): 
    47     q = {} 
    48     if not group: 
    49         q['all_tickets'] = env.href.query(milestone=milestone) 
    50         q['active_tickets'] = env.href.query(milestone=milestone, 
    51                                              status=('new', 'assigned', 'reopened')) 
    52         q['closed_tickets'] = env.href.query(milestone=milestone, status='closed') 
    53     else: 
    54         q['all_tickets'] = env.href.query({grouped_by: group}, 
    55                                           milestone=milestone) 
    56         q['active_tickets'] = env.href.query({grouped_by: group}, 
    57                                              milestone=milestone, 
    58                                              status=('new', 'assigned', 'reopened')) 
    59         q['closed_tickets'] = env.href.query({grouped_by: group}, 
    60                                              milestone=milestone, 
    61                                              status='closed') 
    62     return q 
     33class ITicketGroupStatsProvider(Interface): 
     34    def get_ticket_group_stats(self, tickets): 
     35        """ Gather statistics on a group of tickets. 
    6336 
    64 def calc_ticket_stats(tickets): 
    65     total_cnt = len(tickets) 
    66     active = [ticket for ticket in tickets if ticket['status'] != 'closed'] 
    67     active_cnt = len(active) 
    68     closed_cnt = total_cnt - active_cnt 
     37        This method returns a valid TicketGroupStats object. 
     38        """ 
    6939 
    70     percent_active, percent_closed = 0, 0 
    71     if total_cnt > 0: 
    72         percent_active = round(float(active_cnt) / float(total_cnt) * 100) 
    73         percent_closed = round(float(closed_cnt) / float(total_cnt) * 100) 
    74         if percent_active + percent_closed > 100: 
    75             percent_closed -= 1 
     40class TicketGroupStats(object): 
     41    """Encapsulates statistics on a group of tickets.""" 
    7642 
    77     return { 
    78         'total_tickets': total_cnt, 
    79         'active_tickets': active_cnt, 
    80         'percent_active': percent_active, 
    81         'closed_tickets': closed_cnt, 
    82         'percent_closed': percent_closed 
    83     } 
     43    def __init__(self, title, unit): 
     44        """Creates a new TicketGroupStats object. 
    8445 
     46        title is the display name of this group of stats (eg 'ticket status'). 
     47        unit is the display name of the units for these stats (eg 'hour'). 
     48        """ 
     49        self.title = title 
     50        self.unit = unit 
     51        self.count = 0 
     52        self.qry_args = {} 
     53        self.intervals = [] 
     54        self.done_percent = 0 
     55        self.done_count = 0 
     56 
     57    def add_interval(self, title, count, qry_args, css_class, countsToProg=0): 
     58        """Adds a division to this stats' group's progress bar. 
     59 
     60        title is the display name (eg 'closed', 'spent effort') of this interval 
     61          that will be displayed in front of the unit name. 
     62        count is the number of units in the interval. 
     63        qry_args is a dict of extra params that will yield the subset of 
     64          tickets in this interval on a query. 
     65        css_class is the css class that will be used to display the division. 
     66        countsToProg can be set to true to make this interval count towards 
     67          overall completion of this group of tickets. 
     68        """ 
     69        self.intervals.append({ 
     70            'title': title, 
     71            'count': count, 
     72            'qry_args': qry_args, 
     73            'class': css_class, 
     74            'percent': None, 
     75            'countsToProg': countsToProg 
     76        }) 
     77        self.count = self.count + count 
     78        self._refresh_calcs() 
     79 
     80    def _refresh_calcs(self): 
     81        if self.count < 1: 
     82            return 
     83        total_percent = 0 
     84        self.done_percent = 0 
     85        self.done_count = 0 
     86        for interval in self.intervals: 
     87            interval['percent'] = round(float(interval['count'] /  
     88                                        float(self.count) * 100)) 
     89            total_percent = total_percent + interval['percent'] 
     90            if interval['countsToProg']: 
     91                self.done_percent += interval['percent'] 
     92                self.done_count += interval['count'] 
     93 
     94        if self.done_count and total_percent != 100: 
     95            fudge_int = [i for i in self.intervals if i['countsToProg']][0] 
     96            fudge_amt = 100 - total_percent 
     97            fudge_int['percent'] += fudge_amt 
     98            self.done_percent += fudge_amt 
     99 
     100 
     101def get_tickets_for_milestone(env, milestone_name, order='component'): 
     102    return Query(env, {'milestone': [milestone_name]}, order).execute() 
     103 
     104def get_ticket_stats(providers, tickets): 
     105    stats = [] 
     106    for provider in providers: 
     107        stat = provider.get_ticket_group_stats(tickets) 
     108        stats.append(stat) 
     109    return stats 
     110 
     111class DefaultTicketGroupStatsProvider(Component): 
     112    implements(ITicketGroupStatsProvider) 
     113 
     114    def get_ticket_group_stats(self, tickets): 
     115        total_cnt = len(tickets) 
     116        active = [ticket for ticket in tickets if ticket['status'] != 'closed'] 
     117        active_cnt = len(active) 
     118        closed_cnt = total_cnt - active_cnt 
     119 
     120        stat = TicketGroupStats('ticket status', 'ticket') 
     121        stat.add_interval('closed', closed_cnt, {'status': 'closed'}, 
     122                          'closed', True) 
     123        stat.add_interval('active', active_cnt, 
     124                     {'status': ['new', 'assigned', 'reopened']}, 'open', False) 
     125        return stat 
     126 
    85127def milestone_to_hdf(env, db, req, milestone): 
    86128    safe_name = None 
    87129    if milestone.exists: 
     
    102144        hdf['completed_delta'] = pretty_timedelta(milestone.completed) 
    103145    return hdf 
    104146 
     147def milestone_stat_to_hdf(env, stat, name, grouped_by='component', group=None): 
     148    def merge_cp(dict1, dict2): 
     149        cp = dict1.copy() 
     150        cp.update(dict2) 
     151        return cp 
     152 
     153    hdf = {} 
     154    base_args = {'milestone': name, grouped_by: group} 
     155    hdf['title'] = stat.title 
     156    hdf['caps_title'] = stat.title.capitalize() 
     157    hdf['count'] = stat.count 
     158    hdf['unit']= stat.unit 
     159    hdf['href'] = env.href.query(merge_cp(base_args, stat.qry_args)) 
     160    hdf['done_percent'] = stat.done_percent 
     161    hdf['done_count'] = stat.done_count 
     162    hdf['intervals'] = [] 
     163    i = 0 
     164    for i in range(len(stat.intervals)): 
     165        interval = stat.intervals[i] 
     166        int_hdf = {} 
     167        for (key,val) in interval.items(): 
     168            if key != 'qry_args': 
     169                int_hdf[key] = val 
     170        int_hdf['href'] = env.href.query( 
     171                                    merge_cp(base_args, interval['qry_args'])) 
     172        int_hdf['caps_title'] = interval['title'].capitalize() 
     173        int_hdf['last'] = i == len(stat.intervals) - 1 
     174        hdf['intervals'].append(int_hdf) 
     175        i += 1 
     176    return hdf 
     177 
    105178def _get_groups(env, db, by='component'): 
    106179    for field in TicketSystem(env).get_ticket_fields(): 
    107180        if field['name'] == by: 
     
    118191class RoadmapModule(Component): 
    119192 
    120193    implements(INavigationContributor, IPermissionRequestor, IRequestHandler) 
     194    stats_providers = ExtensionPoint(ITicketGroupStatsProvider) 
    121195 
    122196    # INavigationContributor methods 
    123197 
     
    156230 
    157231        for idx,milestone in enumerate(milestones): 
    158232            prefix = 'roadmap.milestones.%d.' % idx 
    159             tickets = get_tickets_for_milestone(self.env, db, milestone['name'], 
    160                                                 'owner') 
    161             req.hdf[prefix + 'stats'] = calc_ticket_stats(tickets) 
    162             for k, v in get_query_links(self.env, milestone['name']).items(): 
    163                 req.hdf[prefix + 'queries.' + k] = escape(v) 
     233            tickets = get_tickets_for_milestone(self.env, milestone['name'], 
     234                                                 'owner') 
     235            stats = get_ticket_stats(self.stats_providers, tickets) 
     236            stat_no = 0 
     237            for stat in stats: 
     238                req.hdf['%sstats.%s' % (prefix, stat_no)] = \ 
     239                       milestone_stat_to_hdf(self.env, stat, milestone['name']) 
     240                stat_no += 1 
    164241            milestone['tickets'] = tickets # for the iCalendar view 
    165242 
    166243        if req.args.get('format') == 'ics': 
     
    280357    implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 
    281358               ITimelineEventProvider, IWikiSyntaxProvider) 
    282359 
     360    stats_providers = ExtensionPoint(ITicketGroupStatsProvider) 
     361 
    283362    # INavigationContributor methods 
    284363 
    285364    def get_active_navigation_item(self, req): 
     
    456535                                         'label': field['label']}) 
    457536                if field['name'] == 'component': 
    458537                    component_group_available = True 
    459         req.hdf['milestone.stats.available_groups'] = available_groups 
    460538 
    461539        if component_group_available: 
    462540            by = req.args.get('by', 'component') 
    463541        else: 
    464542            by = req.args.get('by', available_groups[0]['name']) 
    465         req.hdf['milestone.stats.grouped_by'] = by 
    466543 
    467         tickets = get_tickets_for_milestone(self.env, db, milestone.name, by) 
    468         stats = calc_ticket_stats(tickets) 
    469         req.hdf['milestone.stats'] = stats 
    470         for key, value in get_query_links(self.env, milestone.name).items(): 
    471             req.hdf['milestone.queries.' + key] = escape(value) 
     544        tickets = get_tickets_for_milestone(self.env, milestone.name, by) 
     545        stats = get_ticket_stats(self.stats_providers, tickets) 
     546        stat_no = 0 
     547        for stat in stats: 
     548            prefix = 'milestone.stats.%s' % stat_no 
     549            req.hdf['%s.available_groups' % prefix] = available_groups 
     550            req.hdf['%s.grouped_by' % prefix] = by 
     551            req.hdf[prefix] = milestone_stat_to_hdf(self.env, stat, 
     552                                                      milestone.name) 
     553            stat_no += 1 
    472554 
    473555        groups = _get_groups(self.env, db, by) 
    474556        group_no = 0 
    475         max_percent_total = 0 
     557        max_counts = [0 for prov in self.stats_providers] 
     558        group_stats = [[] for prov in self.stats_providers] 
    476559        for group in groups: 
    477560            group_tickets = [t for t in tickets if t[by] == group] 
    478561            if not group_tickets: 
    479562                continue 
    480             prefix = 'milestone.stats.groups.%s' % group_no 
    481             req.hdf['%s.name' % prefix] = group 
    482             percent_total = 0 
    483             if len(tickets) > 0: 
    484                 percent_total = float(len(group_tickets)) / float(len(tickets)) 
    485                 if percent_total > max_percent_total: 
    486                     max_percent_total = percent_total 
    487             req.hdf['%s.percent_total' % prefix] = percent_total * 100 
    488             stats = calc_ticket_stats(group_tickets) 
    489             req.hdf[prefix] = stats 
    490             for key, value in get_query_links(self.env, milestone.name, 
    491                                               by, group).items(): 
    492                 req.hdf['%s.queries.%s' % (prefix, key)] = escape(value) 
     563 
     564            gstats = get_ticket_stats(self.stats_providers, group_tickets) 
     565            stat_no = 0 
     566            for gstat in gstats: 
     567                prefix = 'milestone.stats.%s.groups.%s' % (stat_no, group_no) 
     568                req.hdf['%s.name' % prefix] = group 
     569                req.hdf[prefix] = milestone_stat_to_hdf(self.env, gstat, 
     570                                                     milestone.name, by, group) 
     571                if gstat.count > max_counts[stat_no]:                     
     572                    max_counts[stat_no] = gstat.count 
     573                     
     574                group_stats[stat_no].append(gstat) 
     575                stat_no += 1 
     576 
    493577            group_no += 1 
    494         req.hdf['milestone.stats.max_percent_total'] = max_percent_total * 100 
     578         
     579        stat_no = 0 
    495580 
     581        for stat in stats: 
     582            prefix = 'milestone.stats.%s' % stat_no             
     583            grp_no = 0 
     584            for gstat in group_stats[stat_no]: 
     585                req.hdf['%s.groups.%s.percent_of_max_total' % (prefix, grp_no)]\ 
     586                                     = round((float(gstat.count) / 
     587                                              float(max_counts[stat_no]) * 100)) 
     588                grp_no +=1 
     589 
     590            stat_no += 1 
     591 
    496592    # IWikiSyntaxProvider methods 
    497593 
    498594    def get_wiki_syntax(self): 
     
    504600    def _format_link(self, formatter, ns, name, label): 
    505601        return '<a class="milestone" href="%s">%s</a>' \ 
    506602               % (formatter.href.milestone(name), label) 
     603 
  • templates/roadmap.cs

     
    3535      No date set<?cs 
    3636     /if ?> 
    3737    </p><?cs 
    38     with:stats = milestone.stats ?><?cs 
    39      if:#stats.total_tickets > #0 ?> 
    40       <div class="progress"> 
    41        <a class="closed" href="<?cs 
    42          var:milestone.queries.closed_tickets ?>" style="width: <?cs 
    43          var:#stats.percent_closed ?>%" title="<?cs 
    44          var:#stats.closed_tickets ?> of <?cs 
    45          var:#stats.total_tickets ?> ticket<?cs 
    46          if:#stats.total_tickets != #1 ?>s<?cs /if ?> closed"></a> 
    47        <a class="open" href="<?cs 
    48          var:milestone.queries.active_tickets ?>" style="width: <?cs 
    49          var:#stats.percent_active - 1 ?>%" title="<?cs 
    50          var:#stats.active_tickets ?> of <?cs 
    51          var:#stats.total_tickets ?> ticket<?cs 
    52          if:#stats.total_tickets != #1 ?>s<?cs /if ?> active"></a> 
    53       </div> 
    54       <p class="percent"><?cs var:#stats.percent_closed ?>%</p> 
    55       <dl> 
    56        <dt>Closed tickets:</dt> 
    57        <dd><a href="<?cs var:milestone.queries.closed_tickets ?>"><?cs 
    58          var:stats.closed_tickets ?></a></dd> 
    59        <dt>Active tickets:</dt> 
    60        <dd><a href="<?cs var:milestone.queries.active_tickets ?>"><?cs 
    61          var:stats.active_tickets ?></a></dd> 
     38    each:stats = milestone.stats ?><?cs 
     39     if:#stats.count > #0 ?> 
     40      <div class="progress"><?cs  
     41       each:interval = stats.intervals ?> 
     42        <a class="<?cs var:interval.class ?>" href="<?cs 
     43         var:interval.href?>" style="width: <?cs 
     44         if: interval.last ?><?cs #IE fix ?><?cs 
     45          var:interval.percent - 1 ?><?cs 
     46         else ?><?cs 
     47          var:interval.percent ?><?cs 
     48         /if ?>%" title="<?cs 
     49         var:#interval.count ?> of <?cs 
     50         var:#stats.count ?> <?cs var:stats.unit ?><?cs 
     51         if:#stats.count != #1 ?>s<?cs /if ?> <?cs var:interval.title ?>"></a><?cs 
     52       /each ?> 
     53      </div>      
     54      <p class="percent"><?cs var:#stats.done_percent ?>%</p> 
     55      <dl><?cs 
     56       each:interval = stats.intervals ?> 
     57       <dt><?cs var:interval.caps_title ?> <?cs var:stats.unit ?>s:</dt> 
     58       <dd><a href="<?cs var:interval.href ?>"><?cs 
     59         var:interval.count ?></a></dd><?cs  
     60       /each ?> 
    6261      </dl><?cs 
    6362     /if ?><?cs 
    64     /with ?> 
     63    /each ?> 
    6564   </div> 
    6665   <div class="description"><?cs var:milestone.description ?></div> 
    6766  </li><?cs 
  • templates/milestone.cs

     
    9191   <select name="target" id="target"> 
    9292    <option value="">None</option><?cs 
    9393     each:other = milestones ?><?cs if:other != milestone.name ?> 
    94       <option><?cs var:other ?></option><?cs  
     94      <option><?cs var:other ?></option><?cs 
    9595     /if ?><?cs /each ?> 
    9696   </select> 
    9797   <div class="buttons"> 
     
    115115     No date set<?cs 
    116116    /if ?> 
    117117   </p><?cs 
    118    with:stats = milestone.stats ?><?cs 
    119     if:#stats.total_tickets > #0 ?> 
    120      <div class="progress"> 
    121       <a class="closed" href="<?cs 
    122         var:milestone.queries.closed_tickets ?>" style="width: <?cs 
    123         var:#stats.percent_closed ?>%" title="<?cs 
    124         var:#stats.closed_tickets ?> of <?cs 
    125         var:#stats.total_tickets ?> ticket<?cs 
    126         if:#stats.total_tickets != #1 ?>s<?cs /if ?> closed"></a> 
    127       <a class="open" href="<?cs 
    128         var:milestone.queries.active_tickets ?>" style="width: <?cs 
    129         var:#stats.percent_active - 1 ?>%" title="<?cs 
    130         var:#stats.active_tickets ?> of <?cs 
    131         var:#stats.total_tickets ?> ticket<?cs 
    132         if:#stats.total_tickets != #1 ?>s<?cs /if ?> active"></a> 
     118   each:stats = milestone.stats ?><?cs 
     119    if:#stats.count > #0 ?> 
     120     <div class="progress"><?cs 
     121      each:interval = stats.intervals ?> 
     122       <a class="<?cs var:interval.class ?>" href="<?cs 
     123        var:interval.href?>" style="width: <?cs 
     124        if: interval.last ?><?cs #IE fix ?><?cs 
     125         var:interval.percent - 1 ?><?cs 
     126        else ?><?cs 
     127         var:interval.percent ?><?cs 
     128        /if ?>%" title="<?cs 
     129        var:#interval.count ?> of <?cs 
     130        var:#stats.count ?> <?cs var:stats.unit ?><?cs 
     131        if:#stats.count != #1 ?>s<?cs /if ?> <?cs var:interval.title ?>"></a><?cs 
     132      /each ?> 
    133133     </div> 
    134      <p class="percent"><?cs var:#stats.percent_closed ?>%</p> 
    135      <dl> 
    136       <dt>Closed tickets:</dt> 
    137       <dd><a href="<?cs var:milestone.queries.closed_tickets ?>"><?cs 
    138         var:stats.closed_tickets ?></a></dd> 
    139       <dt>Active tickets:</dt> 
    140       <dd><a href="<?cs var:milestone.queries.active_tickets ?>"><?cs 
    141         var:stats.active_tickets ?></a></dd> 
     134     <p class="percent"><?cs var:#stats.done_percent ?>%</p> 
     135     <dl><?cs 
     136      each:interval = stats.intervals ?> 
     137      <dt><?cs var:interval.caps_title ?> <?cs var:stats.unit ?>s:</dt> 
     138      <dd><a href="<?cs var:interval.href ?>"><?cs 
     139        var:interval.count ?></a></dd><?cs 
     140      /each ?> 
    142141     </dl><?cs 
    143142    /if ?><?cs 
    144    /with ?> 
     143   /each ?> 
    145144  </div> 
    146   <form id="stats" action="" method="get"> 
    147    <fieldset> 
    148     <legend> 
    149      <label for="by">Ticket status by</label> 
    150      <select id="by" name="by" onchange="this.form.submit()"><?cs 
    151      each:group = milestone.stats.available_groups ?> 
    152       <option value="<?cs var:group.name ?>" <?cs 
    153         if:milestone.stats.grouped_by == group.name ?> selected="selected"<?cs 
    154         /if ?>><?cs var:group.label ?></option><?cs 
    155      /each ?></select> 
    156      <noscript><input type="submit" value="Update" /></noscript> 
    157     </legend> 
    158     <table summary="Shows the milestone completion status grouped by <?cs 
    159       var:milestone.stats.grouped_by ?>"><?cs 
    160      each:group = milestone.stats.groups ?> 
    161       <tr> 
    162        <th scope="row"><a href="<?cs 
    163          var:group.queries.all_tickets ?>"><?cs var:group.name ?></a></th> 
    164        <td style="white-space: nowrap"><?cs if:#group.total_tickets ?> 
    165         <div class="progress" style="width: <?cs 
    166           var:#group.percent_total * #80 / #milestone.stats.max_percent_total ?>%"> 
    167          <a class="closed" href="<?cs 
    168            var:group.queries.closed_tickets ?>" style="width: <?cs 
    169            var:#group.percent_closed ?>%" title="<?cs 
    170           var:group.closed_tickets ?> of <?cs 
    171           var:group.total_tickets ?> ticket<?cs 
    172           if:group.total_tickets != #1 ?>s<?cs /if ?> closed"></a> 
    173          <a class="open" href="<?cs 
    174            var:group.queries.active_tickets ?>" style="width: <?cs 
    175            var:#group.percent_active - 1 ?>%" title="<?cs 
    176           var:group.active_tickets ?> of <?cs 
    177           var:group.total_tickets ?> ticket<?cs 
    178           if:group.total_tickets != 1 ?>s<?cs /if ?> active"></a> 
    179         </div> 
    180         <p class="percent"><?cs var:group.closed_tickets ?>/<?cs 
    181          var:group.total_tickets ?></p> 
    182        <?cs /if ?></td> 
    183       </tr><?cs 
    184      /each ?> 
    185     </table><?cs /if ?> 
    186    </fieldset> 
    187   </form> 
     145  <div id="group_stats"><?cs 
     146  each:stats = milestone.stats ?> 
     147   <form id="stats" action="" method="get"> 
     148    <fieldset> 
     149     <legend> 
     150      <label for="by"><?cs var:stats.caps_title ?> by</label> 
     151      <select id="by" name="by" onchange="this.form.submit()"><?cs 
     152      each:group = stats.available_groups ?> 
     153       <option value="<?cs var:group.name ?>" <?cs 
     154         if:stats.grouped_by == group.name ?> selected="selected"<?cs 
     155         /if ?>><?cs var:group.label ?></option><?cs 
     156      /each ?></select> 
     157      <noscript><input type="submit" value="Update" /></noscript>