Ticket #2314: mstone_stat_ext_r2514.patch
| File mstone_stat_ext_r2514.patch, 31.3 KB (added by trac@…, 3 years ago) |
|---|
-
trac/ticket/tests/__init__.py
1 1 import unittest 2 2 3 from trac.ticket.tests import api, model, query 3 from trac.ticket.tests import api, model, query, roadmap 4 4 5 5 def suite(): 6 6 suite = unittest.TestSuite() 7 7 suite.addTest(api.suite()) 8 8 suite.addTest(model.suite()) 9 9 suite.addTest(query.suite()) 10 suite.addTest(roadmap.suite()) 10 11 return suite 11 12 12 13 if __name__ == '__main__': -
trac/ticket/tests/roadmap.py
1 from trac.config import Configuration 2 from trac.test import EnvironmentStub 3 from trac.ticket.roadmap import * 4 from trac.core import ComponentManager 5 6 import unittest 7 8 class 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 58 class 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 102 def in_tlist(ticket, list): 103 return len([t for t in list if t['id'] == ticket.id]) > 0 104 105 class 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 141 def 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 149 if __name__ == '__main__': 150 unittest.main(defaultTest='suite') 151 No newline at end of file -
trac/ticket/roadmap.py
23 23 from trac.util import escape, format_date, format_datetime, parse_date, \ 24 24 pretty_timedelta, shorten_line, CRLF 25 25 from trac.ticket import Milestone, Ticket, TicketSystem 26 from trac.ticket.query import Query 26 27 from trac.Timeline import ITimelineEventProvider 27 28 from trac.web import IRequestHandler 28 29 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 29 30 from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider 30 31 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 tickets45 32 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 33 class ITicketGroupStatsProvider(Interface): 34 def get_ticket_group_stats(self, tickets): 35 """ Gather statistics on a group of tickets. 63 36 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 """ 69 39 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 40 class TicketGroupStats(object): 41 """Encapsulates statistics on a group of tickets.""" 76 42 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. 84 45 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 101 def get_tickets_for_milestone(env, milestone_name, order='component'): 102 return Query(env, {'milestone': [milestone_name]}, order).execute() 103 104 def 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 111 class 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 85 127 def milestone_to_hdf(env, db, req, milestone): 86 128 safe_name = None 87 129 if milestone.exists: … … 102 144 hdf['completed_delta'] = pretty_timedelta(milestone.completed) 103 145 return hdf 104 146 147 def 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 105 178 def _get_groups(env, db, by='component'): 106 179 for field in TicketSystem(env).get_ticket_fields(): 107 180 if field['name'] == by: … … 118 191 class RoadmapModule(Component): 119 192 120 193 implements(INavigationContributor, IPermissionRequestor, IRequestHandler) 194 stats_providers = ExtensionPoint(ITicketGroupStatsProvider) 121 195 122 196 # INavigationContributor methods 123 197 … … 156 230 157 231 for idx,milestone in enumerate(milestones): 158 232 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 164 241 milestone['tickets'] = tickets # for the iCalendar view 165 242 166 243 if req.args.get('format') == 'ics': … … 280 357 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 281 358 ITimelineEventProvider, IWikiSyntaxProvider) 282 359 360 stats_providers = ExtensionPoint(ITicketGroupStatsProvider) 361 283 362 # INavigationContributor methods 284 363 285 364 def get_active_navigation_item(self, req): … … 456 535 'label': field['label']}) 457 536 if field['name'] == 'component': 458 537 component_group_available = True 459 req.hdf['milestone.stats.available_groups'] = available_groups460 538 461 539 if component_group_available: 462 540 by = req.args.get('by', 'component') 463 541 else: 464 542 by = req.args.get('by', available_groups[0]['name']) 465 req.hdf['milestone.stats.grouped_by'] = by466 543 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 472 554 473 555 groups = _get_groups(self.env, db, by) 474 556 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] 476 559 for group in groups: 477 560 group_tickets = [t for t in tickets if t[by] == group] 478 561 if not group_tickets: 479 562 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 493 577 group_no += 1 494 req.hdf['milestone.stats.max_percent_total'] = max_percent_total * 100 578 579 stat_no = 0 495 580 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 496 592 # IWikiSyntaxProvider methods 497 593 498 594 def get_wiki_syntax(self): … … 504 600 def _format_link(self, formatter, ns, name, label): 505 601 return '<a class="milestone" href="%s">%s</a>' \ 506 602 % (formatter.href.milestone(name), label) 603 -
templates/roadmap.cs
35 35 No date set<?cs 36 36 /if ?> 37 37 </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 ?> 62 61 </dl><?cs 63 62 /if ?><?cs 64 / with ?>63 /each ?> 65 64 </div> 66 65 <div class="description"><?cs var:milestone.description ?></div> 67 66 </li><?cs -
templates/milestone.cs
91 91 <select name="target" id="target"> 92 92 <option value="">None</option><?cs 93 93 each:other = milestones ?><?cs if:other != milestone.name ?> 94 <option><?cs var:other ?></option><?cs 94 <option><?cs var:other ?></option><?cs 95 95 /if ?><?cs /each ?> 96 96 </select> 97 97 <div class="buttons"> … … 115 115 No date set<?cs 116 116 /if ?> 117 117 </p><?cs 118 with:stats = milestone.stats ?><?cs119 if:#stats. total_tickets> #0 ?>120 <div class="progress"> 121 <a class="closed" href="<?cs122 var:milestone.queries.closed_tickets ?>" style="width:<?cs123 var: #stats.percent_closed ?>%" title="<?cs124 var:#stats.closed_tickets ?> of<?cs125 var:#stats.total_tickets ?> ticket<?cs126 if:#stats.total_tickets != #1 ?>s<?cs /if ?> closed"></a>127 <a class="open" href="<?cs128 var:milestone.queries.active_tickets ?>" style="width:<?cs129 var:# stats.percent_active - 1 ?>%" title="<?cs130 var:#stats. active_tickets ?> of<?cs131 var:#stats.total_tickets ?> ticket<?cs132 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 ?> 133 133 </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 ?> 142 141 </dl><?cs 143 142 /if ?><?cs 144 / with ?>143 /each ?> 145 144 </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>
