Ticket #5572: milestone_groups-r5842.diff
| File milestone_groups-r5842.diff, 13.1 KB (added by ecarter, 16 months ago) |
|---|
-
trac/ticket/tests/roadmap.py
74 74 75 75 tkt1 = Ticket(self.env) 76 76 tkt1.populate({'summary': 'Foo', 'milestone': 'Test', 'owner': 'foman', 77 'status': ' open'})77 'status': 'new'}) 78 78 tkt1.insert() 79 79 tkt2 = Ticket(self.env) 80 80 tkt2.populate({'summary': 'Bar', 'milestone': 'Test', … … 90 90 91 91 prov = DefaultTicketGroupStatsProvider(ComponentManager()) 92 92 prov.env = self.env 93 prov.config = self.env.config 93 94 self.stats = prov.get_ticket_group_stats([tkt1.id, tkt2.id, tkt3.id]) 94 95 95 96 def test_stats(self): … … 101 102 closed = self.stats.intervals[0] 102 103 self.assertEquals('closed', closed['title'], 'closed title incorrect') 103 104 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'}, 107 108 closed['qry_args'], 'qry_args incorrect') 108 109 self.assertEquals(1, closed['count'], 'closed count incorrect') 109 110 self.assertEquals(33, closed['percent'], 'closed percent incorrect') … … 112 113 open = self.stats.intervals[1] 113 114 self.assertEquals('active', open['title'], 'open title incorrect') 114 115 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']}, 118 120 open['qry_args'], 'qry_args incorrect') 119 121 self.assertEquals(2, open['count'], 'open count incorrect') 120 122 self.assertEquals(67, open['percent'], 'open percent incorrect') -
trac/ticket/roadmap.py
26 26 from trac.context import IContextProvider, Context 27 27 from trac.core import * 28 28 from trac.perm import IPermissionRequestor 29 from trac.util importsorted29 from trac.util.compat import set, sorted 30 30 from trac.util.datefmt import parse_date, utc, to_timestamp, \ 31 31 get_date_format_hint, get_datetime_format_hint 32 32 from trac.util.text import shorten_line, CRLF, to_unicode … … 64 64 self.done_percent = 0 65 65 self.done_count = 0 66 66 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): 68 69 """Adds a division to this stats' group's progress bar. 69 70 70 71 `title` is the display name (eg 'closed', 'spent effort') of this … … 73 74 `qry_args` is a dict of extra params that will yield the subset of 74 75 tickets in this interval on a query. 75 76 `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) 78 82 """ 83 if overall_completion is None: 84 overall_completion = countsToProg 79 85 self.intervals.append({ 80 86 'title': title, 81 87 'count': count, 82 88 'qry_args': qry_args, 83 89 'css_class': css_class, 84 90 'percent': None, 85 'countsToProg': countsToProg 91 'countsToProg': overall_completion, 92 'overall_completion': overall_completion, 86 93 }) 87 94 self.count = self.count + count 88 95 … … 96 103 interval['percent'] = round(float(interval['count'] / 97 104 float(self.count) * 100)) 98 105 total_percent = total_percent + interval['percent'] 99 if interval[' countsToProg']:106 if interval['overall_completion']: 100 107 self.done_percent += interval['percent'] 101 108 self.done_count += interval['count'] 102 109 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%. 103 113 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] 105 116 fudge_amt = 100 - total_percent 106 117 fudge_int['percent'] += fudge_amt 107 118 self.done_percent += fudge_amt 108 119 120 109 121 class 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 110 156 implements(ITicketGroupStatsProvider) 111 157 158 def _get_ticket_groups(self): 159 """Returns a dict describing the ticket groups used in milestone 160 progress bars. 161 """ 162 if 'milestone-groups' in self.config: 163 groups = {} 164 order = 0 165 for option, value in self.config.options('milestone-groups'): 166 if '.' in option: 167 name, qualifier = option.split('.', 1) 168 group = groups.get(name) 169 if group: 170 group[qualifier] = value 171 else: 172 raise TracError("%s milestone group qualified before " 173 " being defined" % name) 174 else: 175 groups[option] = {'name': option, 'status': value, 176 'order': order} 177 order += 1 178 return [group for group in sorted(groups.values(), 179 key=lambda g: int(g['order']))] 180 else: 181 return [{'name': 'closed', 'status': 'closed', 182 'args': 'group=resolution', 'overall_completion': 'true'}, 183 {'name': 'active', 'status': '*', 'css': 'open'}] 184 112 185 def get_ticket_group_stats(self, ticket_ids): 113 186 total_cnt = len(ticket_ids) 187 all_status = set(TicketSystem(self.env).get_all_status()) 188 status_cnt = {} 189 for s in all_status: 190 status_cnt[s] = 0 114 191 if total_cnt: 115 192 cursor = self.env.get_db_cnx().cursor() 116 193 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 194 cursor.execute("SELECT status, count(status) FROM ticket " 195 "WHERE id IN (%s) GROUP BY status" % 196 ",".join(str_ids)) 197 for s, cnt in cursor: 198 status_cnt[s] = cnt 125 199 126 closed_cnt = total_cnt - active_cnt127 128 200 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) 201 for group in self._get_ticket_groups(): 202 group_cnt = 0 203 status_list = group['status'].strip() 204 if status_list == '*': 205 accepted = all_status 206 all_status = set() 207 else: 208 invert = False 209 if status_list.startswith('!'): 210 status_list = status_list[1:] 211 invert = True 212 accepted = set([s.strip() for s in status_list.split(',')]) 213 if invert: 214 all_status = accepted 215 elif accepted - all_status: 216 raise TracError("%s milestone group reused status %s " 217 "already taken by other groups. " 218 "Please check your configuration." % 219 ', '.join(accepted - all_status)) 220 else: 221 all_status -= accepted 222 query_args = {} 223 for s, cnt in status_cnt.iteritems(): 224 if (s in accepted) ^ invert: 225 group_cnt += cnt 226 query_args.setdefault('status', []).append(s) 227 for arg in [kv for kv in group.get('args', '').split(',') 228 if '=' in kv]: 229 k, v = [a.strip() for a in arg.split('=', 1)] 230 query_args[k] = v 231 stat.add_interval(group['name'], group_cnt, query_args, 232 group.get('css', group['name']), 233 bool(group.get('overall_completion'))) 135 234 stat.refresh_calcs() 136 235 return stat 137 236 … … 636 735 637 736 for idx, gstat in enumerate(group_stats): 638 737 gs_dict = milestone_groups[idx] 639 gs_dict['percent_of_max_total'] = (float(gstat.count) / 640 float(max_count) * 100) 738 percent = 1.0 739 if max_count: 740 percent = float(gstat.count) / float(max_count) * 100 741 gs_dict['percent_of_max_total'] = percent 641 742 642 743 return 'milestone_view.html', data, None 643 744 -
trac/ticket/workflows/basic-workflow.ini
21 21 reopen = closed -> reopened 22 22 reopen.permissions = TICKET_CREATE 23 23 reopen.operations = del_resolution 24 25 [milestone-groups] 26 closed = closed 27 closed.order = 0 28 closed.args = group=resolution 29 closed.overall_completion = true 30 31 active = assigned,accepted 32 active.order = 1 33 active.css = open 34 35 new = new,reopened 36 new.order = 2 -
trac/htdocs/css/roadmap.css
19 19 text-decoration: none 20 20 } 21 21 table.progress td { background: #fff; padding: 0 } 22 table.progress td.new { background: #f5f5b5 } 22 23 table.progress td.closed { background: #bae0ba } 23 24 table.progress td :hover { background: none } 24 25 p.percent { font-size: 10px; line-height: 2.4em; margin: 0.9em 0 0 } -
trac/templates/macros.html
237 237 <dd><a href="${interval_hrefs[idx]}">${interval.count}</a></dd> 238 238 </py:for> 239 239 <py:if test="stats_href"> 240 <dt> Total ${stats.unit}s:</dt>240 <dt>/ Total ${stats.unit}s:</dt> 241 241 <dd><a href="${stats_href}">${sum([x.count for x in stats.intervals], 0)}</a></dd> 242 242 </py:if> 243 243 </dl>
