source: analyzeplugin/0.12/analyze/analyses/queue.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: 11.1 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 queue-related analyses and fixes
11# TODO: fix sql injection strings
12
13import copy
14
15def get_dependency_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    position_field = args['col1_field1']
22    position = int(args['col1_value1'])
23
24    # iterate over the fields in same order every time
25    standard = [field for field in args['standard_fields'].keys()]
26    custom = [field for field in args['custom_fields'].keys()]
27
28    # first check that the dependent ticket query fields are correct
29    if standard or custom:
30        issue = "#%s's dependent tickets are not in the correct queue." % id
31        ticket,ids = _get_query_field_violators(db, args, standard, custom, id)
32        if ids:
33            data = []
34            for tid in ids:
35                changes = {'ticket':tid,position_field:str(position-1)}
36                changes.update(ticket)
37                data.append(changes)
38            # solution 1: move dependent tickets to query fields and position-1
39            tix = ', '.join(["#%s" % tid for tid in ids])
40            fields = ', '.join(['%s %s' % (k,v) for k,v in ticket.items()])
41            fields += ', and above position %d' % position
42            solutions.append({
43              'name': 'Move %s to %s' % (tix,fields),
44              'data': data,
45            })
46            return issue,solutions
47
48    # queue fields are all good, so how's their order?
49    issue = "#%s's dependent tickets are out of order." % id
50
51    sql  = "SELECT t.id, c.value FROM ticket t"
52    sql += " JOIN ticket_custom c ON t.id = c.ticket"
53    sql += " AND c.name = '%s' " % position_field
54    sql += _get_join(custom)
55    sql += "WHERE t.id IN "
56    sql += " (SELECT source FROM mastertickets WHERE dest = %s)" % id
57    sql += " AND t.status != 'closed'"
58    sql += _get_filter_and(standard, custom, args)
59    sql += " AND (c.value = '' OR CAST(c.value AS INTEGER) > %s)" % position
60    cursor = db.cursor()
61    cursor.execute(sql)
62    result = [(tid,pos) for tid,pos in cursor]
63    if not result:
64        return '',[] # no dependent tickets!  skip
65    ids,positions = zip(*result)
66
67    # solution 1: move dependent tickets above position
68    tix = ', '.join(["#%s" % tid for tid in ids])
69    solutions.append({
70      'name': 'Move %s before position %d' % (tix,position),
71      'data': [{'ticket':tid,position_field:str(position-1)} for tid in ids],
72    })
73
74    # solution 2: move this ticket below lowest position
75    lowest = max([0] + [int(pos) for pos in positions if pos.strip()])
76    if lowest:
77        solutions.append({
78          'name': 'Move #%s after position %d' % (id,lowest),
79          'data': {'ticket':id,position_field:str(lowest+1)},
80        })
81
82    return issue,solutions
83
84
85def get_project_solutions(db, args):
86    """For each blockedby ticket of two ids, check that the first id's
87    queue positions are <= all of the second id's queue positions.
88    """
89    solutions = []
90    id1 = args['id1']
91    id2 = args['id2']
92    position_field = args['col1_field1']
93    project_type = args['project_type']
94
95    # iterate over the fields in same order every time
96    standard = [field for field in args['standard_fields'].keys()]
97    custom = [field for field in args['custom_fields'].keys()]
98
99    # first check that the dependent ticket query fields are correct
100    if standard or custom:
101        for tid in (id1,id2):
102            issue = "#%s's dependent tickets are not in the correct queue."%tid
103            ticket,ids = _get_query_field_violators(db,args,standard,custom,tid)
104            if ids:
105                data = []
106                for tid in ids:
107                    changes = {'ticket':tid}
108                    changes.update(ticket)
109                    data.append(changes)
110                # solution 1: move dependent tickets to query fields
111                tix = ', '.join(["#%s" % tid for tid in ids])
112                fields = ', '.join(['%s %s' % (k,v) for k,v in ticket.items()])
113                solutions.append({
114                  'name': 'Move %s to %s' % (tix,fields),
115                  'data': data,
116                })
117                return issue,solutions
118
119    # second do a multi-parent analysis - else can cause havoc!
120    cursor = db.cursor()
121    for tid in (id1,id2):
122        # get all (unfiltered) children from this parent
123        issue = "#%s's dependent tickets are in multiple %ss." % \
124                    (tid,project_type)
125
126        # for each child, determine if it has multiple parents
127        sql  = "SELECT t.id, b.value FROM ticket t "
128        sql += " JOIN ticket_custom b ON t.id=b.ticket AND b.name='blocking'"
129        sql += _get_join(custom)
130        sql += " WHERE t.id IN"
131        sql += "     (SELECT source FROM mastertickets WHERE dest=%s)" % tid
132        sql += " AND t.status != 'closed'"
133        sql += _get_filter_and(standard, custom, args) + ";"
134        cursor.execute(sql)
135        children = [(cid,[b.strip() for b in blocking.split(',')]) \
136                    for (cid,blocking) in cursor]
137        for cid,blocking in children:
138            # for each child, determine if it has multiple parents
139            sql  = "SELECT p.id FROM ticket p WHERE p.id IN"
140            sql += " (SELECT dest FROM mastertickets WHERE source=%s)" % cid
141            sql += "AND p.status != 'closed' "
142            sql += "AND p.type = '%s';" % project_type
143            cursor.execute(sql)
144            ids = [pid for (pid,) in cursor]
145            if len(ids) >= 2:
146                for pid in ids:
147                    rest = [str(i) for i in ids if i != pid]
148                    block = [b for b in blocking if b not in rest]
149                    tix = ', '.join(["#%s" % i for i in rest])
150                    name = "%s%s" % (project_type,len(rest) > 1 and 's' or '')
151                    solutions.append({
152                      'name': 'Remove #%s from %s %s' % (cid,name,tix),
153                      'data': {'ticket':cid,'blocking':', '.join(block)},
154                    })
155                return issue,solutions
156
157    # queue fields are all good, so how's their childrens' order?
158    issue = "#%s and #%s's dependent tickets are out of order." % (id1,id2)
159
160    stats = [{'id':id1,'fn':'max','op':'>','label':'before'},
161             {'id':id2,'fn':'min','op':'<','label':'after'}]
162    for stat in stats:
163        sql  = "SELECT %s(CAST(c.value AS INTEGER)) FROM ticket t" % stat['fn']
164        sql += " JOIN ticket_custom c ON t.id = c.ticket"
165        sql += " AND c.name = '%s' " % position_field
166        sql += _get_join(custom)
167        sql += "WHERE t.id IN "
168        sql += " (SELECT source FROM mastertickets WHERE dest=%s)" % stat['id']
169        sql += " AND t.status != 'closed'"
170        sql += _get_filter_and(standard, custom, args) + ";"
171        cursor.execute(sql)
172        result = cursor.fetchone()
173        stat['result'] = result and result[0] and int(result[0]) or -9999
174
175    for i in range(len(stats)):
176        stat = stats[i]
177        j = (i+1)%2 # the other stat
178        sql  = "SELECT t.id FROM ticket t"
179        sql += " JOIN ticket_custom c ON t.id = c.ticket"
180        sql += _get_join(custom)
181        sql += " AND c.name = '%s' " % position_field
182        sql += "WHERE t.id IN "
183        sql += " (SELECT source FROM mastertickets WHERE dest=%s)" % stat['id']
184        sql += " AND t.status != 'closed'"
185        sql += _get_filter_and(standard, custom, args)
186        sql += " AND (c.value = '' OR CAST(c.value AS INTEGER)"
187        sql += %s %s)" % (stat['op'],stats[j]['result'])
188        cursor = db.cursor()
189        cursor.execute(sql)
190        ids = [tid for (tid,) in cursor]
191        pos = stats[j]['result']
192        if ids and pos != -9999:
193            # solution n: move project i's tickets before project j's
194            #             highest/lowest position
195            tix = ', '.join(["#%s" % tid for tid in ids])
196            new_pos = str(pos + (i or -1)) # either -1 or +1
197            solutions.append({
198              'name': 'Move %s %s position %d' % (tix,stat['label'],pos),
199              'data': [{'ticket':tid,position_field:new_pos} for tid in ids],
200            })
201
202    return issue,solutions
203
204
205# common functions
206
207def _is_filter_field(name, standard, custom, args):
208    """Evaluates to True if the given name is a filter field."""
209    return (name in standard and args['standard_fields'][name] or \
210            name in custom and args['custom_fields'][name])
211
212def _get_join(custom, t='t'):
213    """Get JOIN sql query part for custom fields."""
214    sql = ' '
215    for i in range(len(custom)):
216        name = custom[i]
217        sql += "JOIN ticket_custom c%s ON %s.id = c%d.ticket " % (i,t,i)
218        sql += "AND c%d.name = '%s' " % (i,name)
219    return sql + ' '
220
221def _get_filter_and(standard, custom, args):
222    """Get AND sql query part for filtered standard and custom fields."""
223    sql = ' '
224    for name in standard:
225        vals = copy.copy(args['standard_fields'][name])
226        if not vals:
227            continue
228        not_ = vals.pop() and 'NOT IN' or 'IN'
229        in_ = ','.join(["'%s'" % v for v in vals])
230        sql += " AND t.%s %s (%s)" % (name,not_,in_)
231    for i in range(len(custom)):
232        name = custom[i]
233        vals = copy.copy(args['custom_fields'][name])
234        if not vals:
235            continue
236        not_ = vals.pop() and 'NOT IN' or 'IN'
237        in_ = ','.join(["'%s'" % v for v in vals])
238        sql += " AND c%d.value %s (%s)" % (i,not_,in_)
239    return sql + ' '
240
241def _get_query_field_violators(db, args, standard, custom, id):
242    """Return ticket id's queue fields and any children tickets that do
243    not have these fields (i.e., are in the wrong queue)."""
244    cursor = db.cursor()
245
246    # build field selectors
247    keys = standard + custom
248    fields = ["t."+name for name in standard]
249    fields += ["c%d.value" % i for i in range(len(custom))]
250
251    # build "from" part of query
252    from_  = " FROM ticket t"
253    from_ += " LEFT OUTER JOIN milestone m ON t.milestone = m.name "
254    from_ += _get_join(custom)
255
256    # get this ticket's queue field values
257    sql = "SELECT " + ', '.join(fields) + from_ + "WHERE t.id = %s" % id
258    cursor.execute(sql)
259    result = cursor.fetchone()
260    if not result:
261        return {},[]
262    ticket = {}
263    for i in range(len(keys)):
264        name = keys[i]
265        if (not _is_filter_field(name, standard, custom, args)):
266            ticket[name] = result[i]
267
268    # find open dependent tickets that don't match queue fields
269    sql = "SELECT t.id " + from_
270    sql += " WHERE t.id IN"
271    sql += " (SELECT source FROM mastertickets WHERE dest = %s)" % id
272    sql += " AND t.status != 'closed'"
273
274    # add queue fields
275    sql += " AND ("
276    or_ = []
277    for name in standard:
278        if args['standard_fields'][name]:
279            continue
280        or_ += ["t.%s != '%s' " % (name,ticket[name])]
281    for i in range(len(custom)):
282        name = custom[i]
283        if args['custom_fields'][name]:
284            continue
285        or_ += ["c%d.value != '%s' " % (i,ticket[name])]
286    sql += ' OR '.join(or_) + ') '
287    cursor.execute(sql)
288    return ticket,[id for (id,) in cursor]
Note: See TracBrowser for help on using the repository browser.