def get_ticket_group_stats(self, ticket_ids): # ticket_ids is a list of ticket id as number. total_count = len(ticket_ids) status_count = {'closed': 0, 'new': 0, 'reopened': 0} if total_count: db = self.env.get_db_cnx() cursor = db.cursor() str_ids = [str(x) for x in sorted(ticket_ids)] cursor.execute(""" SELECT status, count(status) FROM ticket WHERE id IN (%s) GROUP BY status""" % ",".join(str_ids)) for status, count in cursor.fetchall(): status_count[status] = count inprogress_cnt = total_count - (status_count['new'] + \ status_count['reopened'] + status_count['closed']) stat = TicketGroupStats('ticket status', 'ticket') stat.add_interval('closed', status_count['closed'], { 'status': 'closed', 'group': 'resolution' }, 'closed', True) stat.add_interval('inprogress', inprogress_cnt, {'status': ['accepted', 'assigned']}, 'inprogress', False) stat.add_interval('new', status_count['new'] + status_count['reopened'], {'status': ['new', 'reopened']}, 'new', False) stat.refresh_calcs() return stat
def get_ticket_group_stats(self, ticket_ids): stat = TicketGroupStats(self.drilldown_label, self.sum_label) for group in self._get_groups(ticket_ids): stat.add_interval( title=group.get('label', group['name']), count=group.get('total', 0), qry_args=group.get('query_args', {}), css_class=group.get('css_class', group['name']), overall_completion=as_bool(group.get('overall_completion'))) stat.refresh_calcs() return stat
def get_ticket_resolution_group_stats(self, ticket_ids): # ticket_ids is a list of ticket ids with type int stat = TicketGroupStats('ticket resolution', 'ticket') if len(ticket_ids): db = self.env.get_db_cnx() cursor = db.cursor() str_ids = [str(x) for x in sorted(ticket_ids)] cursor.execute(""" SELECT resolution, count(resolution) FROM ticket WHERE status='closed' AND id IN (%s)""" % ",".join(str_ids)) resolution_count = {} for resolution, count in cursor.fetchall(): resolution_count[resolution] = count for key, value in resolution_count.iteritems(): if key in ('fixed', 'completed'): # default ticket type 'defect' stat.add_interval(key, value, {'resolution': key}, 'value', True) else: stat.add_interval(key, value, {'resolution': key}, 'waste', False) stat.refresh_calcs() return stat
def get_ticket_resolution_group_stats(self, ticket_ids, pid): stat = TicketGroupStats(_('ticket resolution'), '') if len(ticket_ids): db = self.env.get_db_cnx() cursor = db.cursor() count_by_resolution = [] # list of dictionaries with keys name and count for resolution in model.Resolution.select(self.env, pid=pid, db=db): cursor.execute(''' SELECT COUNT(*) FROM ticket WHERE project_id=%s AND status = 'closed' AND resolution=%s AND id IN %s ''', (pid, resolution.name, tuple(ticket_ids))) count = cursor.fetchone()[0] if count > 0: count_by_resolution.append({'name': resolution.name, 'count': count}) for t in count_by_resolution: if t['name'] in self.done_resolutions: stat.add_interval(t['name'], t['count'], {'resolution': t['name']}, 'value', True) else: stat.add_interval(t['name'], t['count'], {'resolution': t['name']}, 'waste', False) stat.refresh_calcs() return stat
def _build_story_statistics(self, req, stats_provider, sprint_name): """ Assemble statistics for all stories in the given sprint_name so that the progress bar can be displayed. """ sprint_stats = dict() if not sprint_name: return sprint_stats cmd_get_stories = SprintController.ListTicketsHavingPropertiesCommand(self.env, sprint=sprint_name, properties=[Key.STORY_POINTS]) stories = self.controller.process_command(cmd_get_stories) type_dict = dict() closed_stories = list() inprogress_stories = list() for story in stories: if story[Key.STATUS] == Status.CLOSED: closed_stories.append(story) elif story[Key.STATUS] != Status.NEW: inprogress_stories.append(story) type_dict[story[Key.TYPE]] = True type_names = type_dict.keys() number_open_stories = len(stories) - (len(closed_stories) + \ len(inprogress_stories)) try: stat = TicketGroupStats('User Stories status', 'stories') stat.add_interval(_('Completed'), len(closed_stories), qry_args={Key.STATUS: Status.CLOSED}, css_class='closed', overall_completion=True) stat.add_interval(_('In Progress'), len(inprogress_stories), qry_args={Key.STATUS: Status.ACCEPTED}, css_class='inprogress') stat.add_interval(_('Open'), number_open_stories, qry_args={Key.STATUS: [Status.NEW, Status.REOPENED]}, css_class='open') stat.refresh_calcs() sprint_stats = self._build_sprint_stats_data(req, stat, sprint_name, type_names=type_names) except Exception, e: # The DB is closed? And we don't break for statistics error(stats_provider, "ERROR: %s" % to_unicode(e))
def build_stats_object_for_roadmap_macro(self, contingent, unit=None): """Return a TicketGroupStats instance loaded with the parameters for time used vs. time left.""" stats = TicketGroupStats('used contingent', unit) time_left = contingent.amount - contingent.actual stats.add_interval('used', contingent.actual, None, 'closed', overall_completion=True) stats.add_interval('left', time_left, None, None) stats.refresh_calcs() used_interval = stats.intervals[0] if stats.done_percent > 90: used_interval['css_class'] = 'critical' elif stats.done_percent > 70: used_interval['css_class'] = 'warning' return stats
def get_ticket_group_stats(self, ticket_ids): # ticket_ids is a list of ticket id as number. total_cnt = len(ticket_ids) if total_cnt: cursor = self.env.get_db_cnx().cursor() # get database connection str_ids = [str(x) for x in sorted(ticket_ids)] # create list of ticket id as string closed_cnt = cursor.execute("SELECT count(1) FROM ticket " "WHERE status = 'closed' AND id IN " "(%s)" % ",".join(str_ids)) # execute query and get cursor obj. closed_cnt = 0 for cnt, in cursor: closed_cnt = cnt active_cnt = cursor.execute("SELECT count(1) FROM ticket " "WHERE status IN ('new', 'reopened') " "AND id IN (%s)" % ",".join(str_ids)) # execute query and get cursor obj. active_cnt = 0 for cnt, in cursor: active_cnt = cnt else: closed_cnt = 0 active_cnt = 0 inprogress_cnt = total_cnt - ( active_cnt + closed_cnt) stat = TicketGroupStats('ticket status', 'ticket') stat.add_interval('closed', closed_cnt, {'status': 'closed', 'group': 'resolution'}, 'closed', True) stat.add_interval('inprogress', inprogress_cnt, {'status': ['accepted', 'assigned']}, 'inprogress', False) stat.add_interval('new', active_cnt, {'status': ['new', 'reopened']}, 'new', False) stat.refresh_calcs() return stat
def get_ticket_group_stats(self, ticket_ids, pid): total_cnt = len(ticket_ids) if total_cnt: db = self.env.get_db_cnx() cursor = db.cursor() closed_cnt = cursor.execute(''' SELECT COUNT(*) FROM ticket WHERE status = 'closed' AND project_id=%s AND id IN %s ''', (pid, tuple(ticket_ids))) closed_cnt = cursor.fetchone()[0] active_cnt = cursor.execute(''' SELECT COUNT(*) FROM ticket WHERE project_id=%s AND status IN %s AND id IN %s ''', (pid, tuple(self.new_statuses), tuple(ticket_ids))) active_cnt = cursor.fetchone()[0] else: closed_cnt = 0 active_cnt = 0 inprogress_cnt = total_cnt - ( active_cnt + closed_cnt) stat = TicketGroupStats(_('ticket status'), '') stat.add_interval(_('Closed'), closed_cnt, {'status': 'closed', 'group': 'resolution'}, 'closed', True) stat.add_interval(_('In progress'), inprogress_cnt, {'status': ['!closed']+['!'+st for st in self.new_statuses]}, 'inprogress', False) stat.add_interval(_('New'), active_cnt, {'status': self.new_statuses}, 'new', False) stat.refresh_calcs() return stat
def get_ticket_group_stats(self, ticket_ids): stat = TicketGroupStats(self.drilldown_label, self.sum_label) for group in self._get_groups(ticket_ids): stat.add_interval(title=group.get('label', group['name']), count=group.get('total', 0), qry_args=group.get('query_args', {}), css_class=group.get('css_class', group['name']), overall_completion=as_bool( group.get('overall_completion'))) stat.refresh_calcs() return stat
def get_ticket_group_stats(self, ticket_ids): # ticket_ids is a list of ticket id as number. total_count = len(ticket_ids) status_count = { 'closed': 0, 'new': 0, 'reopened': 0 } if total_count: db = self.env.get_db_cnx() cursor = db.cursor() str_ids = [str(x) for x in sorted(ticket_ids)] cursor.execute(""" SELECT status, count(status) FROM ticket WHERE id IN (%s) GROUP BY status""" % ",".join(str_ids)) for status, count in cursor.fetchall(): status_count[status] = count inprogress_cnt = total_count - (status_count['new'] + \ status_count['reopened'] + status_count['closed']) stat = TicketGroupStats('ticket status', 'ticket') stat.add_interval('closed', status_count['closed'], {'status': 'closed', 'group': 'resolution'}, 'closed', True) stat.add_interval('inprogress', inprogress_cnt, {'status': ['accepted', 'assigned']}, 'inprogress', False) stat.add_interval('new', status_count['new'] + status_count['reopened'], {'status': ['new', 'reopened']}, 'new', False) stat.refresh_calcs() return stat
def get_ticket_group_stats(self, ticket_ids): # ticket_ids is a list of ticket id as number. total_cnt = len(ticket_ids) if total_cnt: db = self.env.get_db_cnx() cursor = db.cursor() type_count = [] # list of dictionary with key name and count for type in model.Type.select(self.env): cursor.execute( "SELECT COUNT(*) FROM ticket " "WHERE type = %%s AND id IN (%s)" % (",".join(['%s'] * len(ticket_ids))), (type.name, ) + tuple(ticket_ids)) count = cursor.fetchone()[0] if count > 0: type_count.append({'name': type.name, 'count': count}) else: type_count = [] stat = TicketGroupStats('ticket type', 'ticket') for type in type_count: if type['name'] != 'defect': # default ticket type 'defect' stat.add_interval(type['name'], type['count'], {'type': type['name']}, 'value', True) else: stat.add_interval(type['name'], type['count'], {'type': type['name']}, 'waste', False) stat.refresh_calcs() return stat
def get_ticket_resolution_group_stats(self, ticket_ids): # ticket_ids is a list of ticket id as number. total_cnt = len(ticket_ids) if total_cnt: str_ids = [str(x) for x in sorted(ticket_ids)] # create list of ticket id as string cursor = self.env.get_db_cnx().cursor() # get database connection type_count = [] # list of dictionary with key name and count for type in model.Resolution.select(self.env): #Replace possible single-quotes in resolution strings with double single-quotes, #as required for SQLite queries (see th:#3684, http://www.sqlite.org/faq.html#q14) count = cursor.execute("SELECT count(1) FROM ticket " "WHERE status = 'closed' AND resolution = '%s' AND id IN " "(%s)" % (type.name.replace("'","''"), ",".join(str_ids))) count = 0 for cnt, in cursor: count = cnt if count > 0: type_count.append({'name':type.name,'count':count}) else: type_count = [] stat = TicketGroupStats('ticket resolution', 'ticket') for type in type_count: if type['name'] in ('fixed', 'completed'): # default ticket type 'defect' stat.add_interval(type['name'], type['count'], {'resolution': type['name']}, 'value', True) else: stat.add_interval(type['name'], type['count'], {'resolution': type['name']}, 'waste', False) stat.refresh_calcs() return stat
def get_ticket_group_stats(self, ticket_ids): # ticket_ids is a list of ticket id as number. total_cnt = len(ticket_ids) if total_cnt: str_ids = [str(x) for x in sorted(ticket_ids)] # create list of ticket id as string cursor = self.env.get_db_cnx().cursor() # get database connection type_count = [] # list of dictionary with key name and count for type in model.Type.select(self.env): count = cursor.execute("SELECT count(1) FROM ticket " "WHERE type = '%s' AND id IN " "(%s)" % (type.name, ",".join(str_ids))) # execute query and get cursor obj. count = 0 for cnt, in cursor: count = cnt if count > 0: type_count.append({'name':type.name,'count':count}) else: type_count = [] stat = TicketGroupStats('ticket type', 'ticket') for type in type_count: if type['name'] != 'defect': # default ticket type 'defect' stat.add_interval(type['name'], type['count'], {'type': type['name']}, 'value', True) else: stat.add_interval(type['name'], type['count'], {'type': type['name']}, 'waste', False) stat.refresh_calcs() return stat
def get_ticket_group_stats(self, ticket_ids): # ticket_ids is a list of ticket id as number. total_cnt = len(ticket_ids) if total_cnt: db = self.env.get_db_cnx() cursor = db.cursor() type_count = [] # list of dictionary with key name and count for type in model.Type.select(self.env): cursor.execute("SELECT COUNT(*) FROM ticket " "WHERE type = %%s AND id IN (%s)" % (",".join(['%s'] * len(ticket_ids))), (type.name,) + tuple(ticket_ids)) count = cursor.fetchone()[0] if count > 0: type_count.append({'name':type.name, 'count':count}) else: type_count = [] stat = TicketGroupStats('ticket type', 'ticket') for type in type_count: if type['name'] != 'defect': # default ticket type 'defect' stat.add_interval(type['name'], type['count'], {'type': type['name']}, 'value', True) else: stat.add_interval(type['name'], type['count'], {'type': type['name']}, 'waste', False) stat.refresh_calcs() return stat
def get_ticket_group_stats(self, ticket_ids, pid): total_cnt = len(ticket_ids) if total_cnt: db = self.env.get_db_cnx() cursor = db.cursor() type_count = [] # list of dictionary with key name and count for type in model.Type.select(self.env, pid=pid): cursor.execute(''' SELECT COUNT(*) FROM ticket WHERE project_id=%s AND type = %s AND id IN %s ''', (pid, type.name, tuple(ticket_ids))) count = cursor.fetchone()[0] if count > 0: type_count.append({'name':type.name, 'count':count}) else: type_count = [] stat = TicketGroupStats(_('ticket type'), '') for type in type_count: if type['name'] not in self.waste_types: stat.add_interval(type['name'], type['count'], {'type': type['name']}, 'value', True) else: stat.add_interval(type['name'], type['count'], {'type': type['name']}, 'waste', False) stat.refresh_calcs() return stat
def get_ticket_group_stats(self, ticket_ids): total_cnt = len(ticket_ids) stat = TicketGroupStats('ticket status', 'ticket') if total_cnt: cursor = self.env.get_db_cnx().cursor() id_list = ','.join([str(x) for x in ticket_ids]) for title, status, color, countsToProg in self.groups: status_list = ','.join(["'%s'" % s for s in status]) sql = ("SELECT count(1) FROM ticket" " WHERE status IN (%s)" " AND id IN (%s)" % (status_list, id_list)) cursor.execute(sql) count = 0 for cnt, in cursor: count = cnt stat.add_interval(title, int(count), {'status': status}, title, countsToProg) stat.refresh_calcs() return stat
def setUp(self): self.stats = TicketGroupStats('title', 'units')
class TicketGroupStatsTestCase(unittest.TestCase): def setUp(self): self.stats = TicketGroupStats('title', 'units') def test_init(self): self.assertEqual('title', self.stats.title, 'title incorrect') self.assertEqual('units', self.stats.unit, 'unit incorrect') self.assertEqual(0, self.stats.count, 'count not zero') self.assertEqual(0, len(self.stats.intervals), 'intervals not empty') def test_add_iterval(self): self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0) self.stats.refresh_calcs() self.assertEqual(3, self.stats.count, 'count not incremented') int = self.stats.intervals[0] self.assertEqual('intTitle', int['title'], 'title incorrect') self.assertEqual(3, int['count'], 'count incorrect') self.assertEqual({'k1': 'v1'}, int['qry_args'], 'query args incorrect') self.assertEqual('css', int['css_class'], 'css class incorrect') self.assertEqual(100, int['percent'], 'percent incorrect') self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0) self.stats.refresh_calcs() self.assertEqual(50, int['percent'], 'percent not being updated') def test_add_interval_no_prog(self): self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0) self.stats.add_interval('intTitle', 5, {'k1': 'v1'}, 'css', 0) self.stats.refresh_calcs() self.assertEqual(0, self.stats.done_count, 'count added for no prog') self.assertEqual(0, self.stats.done_percent, 'percent incremented') def test_add_interval_prog(self): self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0) self.stats.add_interval('intTitle', 1, {'k1': 'v1'}, 'css', 1) self.stats.refresh_calcs() self.assertEqual(4, self.stats.count, 'count not incremented') self.assertEqual(1, self.stats.done_count, 'count not added to prog') self.assertEqual(25, self.stats.done_percent, 'done percent not incr') def test_add_interval_fudging(self): self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0) self.stats.add_interval('intTitle', 5, {'k1': 'v1'}, 'css', 1) self.stats.refresh_calcs() self.assertEqual(8, self.stats.count, 'count not incremented') self.assertEqual(5, self.stats.done_count, 'count not added to prog') self.assertEqual(62, self.stats.done_percent, 'done percnt not fudged downward') self.assertEqual(62, self.stats.intervals[1]['percent'], 'interval percent not fudged downward') self.assertEqual(38, self.stats.intervals[0]['percent'], 'interval percent not fudged upward')
def get_ticket_group_stats(self, tickets, field_name=None): total_cnt = 0 ticket_ids= [] for ticket in tickets: try: ticket_ids.append(ticket['id']) total_cnt += int(ticket.get(field_name, 0)) except: pass if not field_name: return DefaultTicketGroupStatsProvider(self.env).get_ticket_group_stats(ticket_ids) all_statuses = set(TicketSystem(self.env).get_all_status()) status_cnt = {} for s in all_statuses: status_cnt[s] = 0 if total_cnt: cursor = self.env.get_db_cnx().cursor() str_ids = [str(x) for x in sorted(ticket_ids)] cursor.execute("SELECT status, sum(cast('0'||tc.value as int))"+\ " FROM ticket t LEFT OUTER JOIN ticket_custom tc ON t.id=tc.ticket AND tc.name=%s "+\ " WHERE id IN (%s) GROUP BY status" % ("%s,"*len(str_ids))[:-1], [field_name,]+str_ids) for s, cnt in cursor: status_cnt[s] = cnt stat = TicketGroupStats('ticket status', 'ticket') remaining_statuses = set(all_statuses) groups = DefaultTicketGroupStatsProvider(self.env)._get_ticket_groups() catch_all_group = None # we need to go through the groups twice, so that the catch up group # doesn't need to be the last one in the sequence for group in groups: status_str = group['status'].strip() if status_str == '*': if catch_all_group: raise TracError(_( "'%(group1)s' and '%(group2)s' milestone groups " "both are declared to be \"catch-all\" groups. " "Please check your configuration.", group1=group['name'], group2=catch_all_group['name'])) catch_all_group = group else: group_statuses = set([s.strip() for s in status_str.split(',')]) \ & all_statuses if group_statuses - remaining_statuses: raise TracError(_( "'%(groupname)s' milestone group reused status " "'%(status)s' already taken by other groups. " "Please check your configuration.", groupname=group['name'], status=', '.join(group_statuses - remaining_statuses))) else: remaining_statuses -= group_statuses group['statuses'] = group_statuses if catch_all_group: catch_all_group['statuses'] = remaining_statuses for group in groups: group_cnt = 0 query_args = {} for s, cnt in status_cnt.iteritems(): if s in group['statuses']: group_cnt += cnt or 0 query_args.setdefault('status', []).append(s) for arg in [kv for kv in group.get('query_args', '').split(',') if '=' in kv]: k, v = [a.strip() for a in arg.split('=', 1)] query_args[k] = v stat.add_interval(group.get('label', group['name']), group_cnt, query_args, group.get('css_class', group['name']), bool(group.get('overall_completion'))) stat.refresh_calcs() return stat
def get_ticket_group_stats(self, tickets, field_name=None): total_cnt = 0 ticket_ids = [] for ticket in tickets: try: ticket_ids.append(ticket['id']) total_cnt += int(ticket.get(field_name, 0)) except: pass if not field_name: return DefaultTicketGroupStatsProvider( self.env).get_ticket_group_stats(ticket_ids) all_statuses = set(TicketSystem(self.env).get_all_status()) status_cnt = {} for s in all_statuses: status_cnt[s] = 0 if total_cnt: cursor = self.env.get_db_cnx().cursor() str_ids = [str(x) for x in sorted(ticket_ids)] cursor.execute("SELECT status, sum(cast('0'||tc.value as int))"+\ " FROM ticket t LEFT OUTER JOIN ticket_custom tc ON t.id=tc.ticket AND tc.name=%s "+\ " WHERE id IN (%s) GROUP BY status" % ("%s,"*len(str_ids))[:-1], [field_name,]+str_ids) for s, cnt in cursor: status_cnt[s] = cnt stat = TicketGroupStats('ticket status', 'ticket') remaining_statuses = set(all_statuses) groups = DefaultTicketGroupStatsProvider(self.env)._get_ticket_groups() catch_all_group = None # we need to go through the groups twice, so that the catch up group # doesn't need to be the last one in the sequence for group in groups: status_str = group['status'].strip() if status_str == '*': if catch_all_group: raise TracError( _( "'%(group1)s' and '%(group2)s' milestone groups " "both are declared to be \"catch-all\" groups. " "Please check your configuration.", group1=group['name'], group2=catch_all_group['name'])) catch_all_group = group else: group_statuses = set([s.strip() for s in status_str.split(',')]) \ & all_statuses if group_statuses - remaining_statuses: raise TracError( _( "'%(groupname)s' milestone group reused status " "'%(status)s' already taken by other groups. " "Please check your configuration.", groupname=group['name'], status=', '.join(group_statuses - remaining_statuses))) else: remaining_statuses -= group_statuses group['statuses'] = group_statuses if catch_all_group: catch_all_group['statuses'] = remaining_statuses for group in groups: group_cnt = 0 query_args = {} for s, cnt in status_cnt.iteritems(): if s in group['statuses']: group_cnt += cnt or 0 query_args.setdefault('status', []).append(s) for arg in [ kv for kv in group.get('query_args', '').split(',') if '=' in kv ]: k, v = [a.strip() for a in arg.split('=', 1)] query_args[k] = v stat.add_interval(group.get('label', group['name']), group_cnt, query_args, group.get('css_class', group['name']), bool(group.get('overall_completion'))) stat.refresh_calcs() return stat