source: analyzeplugin/0.12/analyze/analyses/rollup.py

Last change on this file was 13992, checked in by Ryan J Ollos, 9 years ago

Changed to 3-Clause BSD license with permission of author. Refs #11832.

  • Property svn:executable set to *
File size: 7.0 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2011-2014 Rob Guttman <guttman@alum.mit.edu>
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution.
8#
9
10# several rollup-related analyses and fixes
11# TODO: fix sql injection strings
12
13import copy
14
15def get_solutions(db, args):
16    """For each blockedby ticket of this id, check that it's queue
17    position is <= this ticket's position.
18    """
19    solutions = []
20    id = args['id1']
21
22    # iterate over the fields in same order every time
23    standard = [field for field in args['standard_fields'].keys()]
24    custom = [field for field in args['custom_fields'].keys()]
25
26    # queue fields are all good, so how's their order?
27    old,tickets = _get_rollup_fields(db, args, standard, custom, id)
28    if not old or not tickets:
29        return '',[] # no rollup tickets!  skip
30
31    changes = []
32    new = {'ticket':id}
33    for field in standard + custom:
34        if field in args['standard_fields']:
35            rollup = args['standard_fields'][field]
36        else:
37            rollup = args['custom_fields'][field]
38
39        # compare old and new values
40        old_value = _get_value(old[field], rollup)
41        new_value = _get_stat_rollup(field, rollup, tickets)
42        if old_value != new_value:
43            new[field] = str(new_value)
44            if new_value == '':
45                new_value = '(empty)'
46            changes.append("%s to %s" % (field,new_value))
47    if len(new) == 1:
48        return '',[] # no changes
49
50    # solution 1: update rollup field changes
51    issue = "#%s has rollup field changes." % id
52    solutions.append({
53      'name': 'Update %s' % (', '.join(changes)),
54      'data': new,
55    })
56    return issue,solutions
57
58
59# common functions
60
61def _get_value(value, rollup):
62    if not value:
63        return value
64    if rollup['numeric']:
65        try:
66            return int(value)
67        except (ValueError, TypeError):
68            return float(value)
69    return value
70
71def _get_stat_rollup(field, rollup, tickets):
72    options = rollup['options']
73    if rollup['numeric']:
74        vals = [_get_value(ticket[field], rollup) for ticket in tickets \
75                if ticket[field]] # guard against empty data
76    else:
77        vals = [options.index(ticket[field]) for ticket in tickets \
78                if ticket[field] in options] # guard against bad data
79
80    if rollup['stat'] == 'sum':
81        return sum(vals)
82    if rollup['stat'] == 'min':
83        if rollup['numeric']:
84            return min(vals)
85        else:
86            return options[min(vals)]
87    if rollup['stat'] == 'max':
88        if rollup['numeric']:
89            return max(vals)
90        else:
91            return options[max(vals)]
92    if rollup['stat'] in ('avg','mean'):
93        avg = sum(vals)/len(vals)
94        if rollup['numeric']:
95            return avg # TODO: handle precision of floats
96        else:
97            return options[avg]
98    if rollup['stat'] == 'median':
99        vals.sort()
100        mid = len(vals)/2
101        if rollup['numeric']:
102            return vals[mid]
103        else:
104            return options[vals[mid]]
105    if rollup['stat'] == 'mode':
106        vals.sort()
107        counts = {}
108        hi_count = 0
109        hi_value = 0
110        for val in vals:
111            counts[val] = counts.get(val,0)+1
112            if counts[val] > hi_count:
113                hi_count = counts[val]
114                hi_value = val
115        if rollup['numeric']:
116            return hi_value
117        else:
118            return options[hi_value]
119
120    # assume stat is the pivot field - the pivot algorithm is as follows:
121    #
122    #  * if all values are < the pivot index, then select their max index
123    #  * else if all are > the pivot index, then select their min index
124    #  * else select the pivot index
125    index = options.index(rollup['stat'])
126    max_index = max(vals)
127    if max_index < index:
128        return options[max_index]
129    min_index = min(vals)
130    if min_index > index:
131        return options[min_index]
132    return options[index]
133
134def _get_join(custom, t='t'):
135    """Get JOIN sql query part for custom fields."""
136    sql = ' '
137    for i in range(len(custom)):
138        name = custom[i]
139        sql += "JOIN ticket_custom c%s ON %s.id = c%d.ticket " % (i,t,i)
140        sql += "AND c%d.name = '%s' " % (i,name)
141    return sql + ' '
142
143def _get_filter_and(standard, custom, args):
144    """Get AND sql query part for filtered standard and custom fields."""
145    sql = ' '
146    for name in standard:
147        vals = copy.copy(args['standard_fields'][name])
148        if not vals:
149            continue
150        not_ = vals.pop() and 'NOT IN' or 'IN'
151        in_ = ','.join(["'%s'" % v for v in vals])
152        sql += " AND t.%s %s (%s)" % (name,not_,in_)
153    for i in range(len(custom)):
154        name = custom[i]
155        vals = copy.copy(args['custom_fields'][name])
156        if not vals:
157            continue
158        not_ = vals.pop() and 'NOT IN' or 'IN'
159        in_ = ','.join(["'%s'" % v for v in vals])
160        sql += " AND c%d.value %s (%s)" % (i,not_,in_)
161    return sql + ' '
162
163def _get_rollup_fields(db, args, standard, custom, id):
164    """Return ticket id's rollup fields and all of its children tickets
165    recursively."""
166    cursor = db.cursor()
167    project_type = args['project_type']
168
169    # build field selectors
170    keys = standard + custom
171    fields = ["t."+name for name in standard]
172    fields += ["c%d.value" % i for i in range(len(custom))]
173
174    # build "from" part of query
175    from_  = " FROM ticket t"
176    from_ += _get_join(custom)
177
178    # get this ticket's rollup field values
179    sql = "SELECT " + ', '.join(fields) + from_ + "WHERE t.id = %s" % id
180    cursor.execute(sql)
181    result = cursor.fetchone()
182    if not result:
183        return {},[]
184    ticket = {'ticket':id}
185    for i in range(len(keys)):
186        name = keys[i]
187        ticket[name] = result[i]
188
189    sql = "SELECT t.id, " + ', '.join(fields) + from_ + "WHERE t.id IN"
190    sql += " (SELECT source FROM mastertickets WHERE dest = %s)" # % id
191    sql += " AND t.status != 'closed' AND t.type != '%s'" % project_type
192    sql += " AND t.id NOT IN (%s);" # skip visited tickets
193    visited = [id]
194    recurse = args['recurse']
195    tickets = _get_rollup_children(cursor, sql, id, keys, visited, recurse)
196    return ticket,tickets
197
198
199def _get_rollup_children(cursor, sql, id, keys, visited, recurse):
200    """Recursively return all children tickets of the given id that have
201    not yet been visited."""
202    # find open dependent tickets that haven't been seen yet
203    tickets = []
204    cursor.execute(sql % (id,','.join(visited)))
205    for result in cursor:
206        cid = str(result[0])
207        visited.append(cid) # add ticket id to visited
208        ticket = {'id':cid}
209        for i in range(len(keys)):
210            name = keys[i]
211            ticket[name] = result[i+1]
212        tickets.append(ticket)
213
214    # now get children tickets (separate loop to avoid confusing cursor)
215    children = []
216    if recurse:
217        for ticket in tickets:
218            id = ticket['id']
219            children += _get_rollup_children(cursor,sql,id,keys,visited,recurse)
220
221    return tickets + children
Note: See TracBrowser for help on using the repository browser.