source: timingandestimationplugin/branches/trac1.0-Permissions/timingandestimationplugin/ticket_daemon.py

Last change on this file was 17107, checked in by Russ Tyndall, 5 years ago

1.5.9(b) - fixed Decimal cast in ticket_daemon re #13405

File size: 7.7 KB
Line 
1from trac.ticket import ITicketChangeListener, Ticket, ITicketManipulator
2from trac.perm import PermissionCache
3from trac.core import *
4import datetime
5import dbhelper
6
7def identity(x):
8    return x;
9
10def convertfloat(x):
11    "some european countries use , as the decimal separator"
12    if not x:
13        return 0.0
14    if isinstance(x, float) or isinstance(x, int):
15        return x
16    x = str(x).strip()
17    if len(x) > 0:
18        return float(x.replace(',','.'))
19    else: 
20        return 0.0
21
22try:
23    import trac.util.datefmt
24    to_timestamp = trac.util.datefmt.to_utimestamp
25except Exception:
26    to_timestamp = identity
27
28
29def save_custom_field_value( db, ticket_id, field, value ):
30    cursor = db.cursor();
31    cursor.execute("SELECT * FROM ticket_custom " 
32                   "WHERE ticket=%s and name=%s", (ticket_id, field))
33    if cursor.fetchone():
34        cursor.execute("UPDATE ticket_custom SET value=%s "
35                       "WHERE ticket=%s AND name=%s",
36                       (value, ticket_id, field))
37    else:
38        cursor.execute("INSERT INTO ticket_custom (ticket,name, "
39                       "value) VALUES(%s,%s,%s)",
40                       (ticket_id, field, value))
41
42def update_hours_to_floats(db, ticket_id):
43    cursor = db.cursor()
44    cursor.execute("SELECT time, newvalue FROM ticket_change"
45                   " WHERE newvalue like '%,%' AND  ticket=%s AND field='hours'", 
46                   (ticket_id,))
47    data = list(cursor.fetchall())
48    for (time, newvalue) in data:
49        cursor.execute("UPDATE ticket_change SET newvalue=%s "
50                       "WHERE ticket=%s AND time=%s AND field='hours'",
51                       (str(convertfloat(newvalue)), ticket_id, time))
52   
53def update_totalhours_custom( db, ticket_id):
54    cursor = db.cursor()
55    sumSql = """
56       (SELECT SUM( CASE WHEN newvalue = '' OR newvalue IS NULL THEN 0
57                         ELSE CAST( newvalue AS DECIMAL(10,2) ) END ) as total
58          FROM ticket_change
59         WHERE ticket=%s and field='hours')  """
60    cursor.execute("UPDATE ticket_custom SET value="+sumSql+
61                   "WHERE ticket=%s AND name='totalhours'",
62               (ticket_id,ticket_id))
63    if cursor.rowcount==0:
64        cursor.execute("INSERT INTO ticket_custom (name, value, ticket) "+
65                       "VALUES('totalhours',"+sumSql+",%s)",
66                       (ticket_id,ticket_id))
67
68def insert_totalhours_changes( db, ticket_id):
69    sql = """
70       INSERT INTO ticket_change (ticket, author, time, field, oldvalue, newvalue)
71       SELECT ticket, author, time, 'totalhours', 
72               (SELECT SUM( CASE WHEN newvalue = '' OR newvalue IS NULL THEN 0
73                           ELSE CAST( newvalue AS DECIMAL(10,2) ) END ) as total
74               FROM ticket_change as guts
75               WHERE guts.ticket = ticket_change.ticket AND guts.field='hours'
76                 AND guts.time < ticket_change.time
77              ) as oldvalue,
78              (SELECT SUM( CASE WHEN newvalue = '' OR newvalue IS NULL THEN 0
79                           ELSE CAST( newvalue AS DECIMAL(10,2) ) END ) as total
80               FROM ticket_change as guts
81               WHERE guts.ticket = ticket_change.ticket AND guts.field='hours'
82                 AND guts.time <= ticket_change.time
83              ) as newvalue
84          FROM ticket_change
85         WHERE ticket=%s and field='hours'
86           AND NOT EXISTS( SELECT ticket
87                             FROM ticket_change as guts
88                            WHERE guts.ticket=ticket_change.ticket
89                              AND guts.author=ticket_change.author
90                              AND guts.time=ticket_change.time
91                              AND field='totalhours')
92    """
93    cursor = db.cursor()
94    cursor.execute(sql, (ticket_id,))
95
96def delete_ticket_change( comp, ticket_id, change_time, field):
97    """ removes a ticket change from the database """
98    if isinstance(change_time, datetime.datetime):
99        change_time = to_timestamp(change_time)
100    sql = """DELETE FROM ticket_change 
101             WHERE ticket=%s and time=%s and field=%s""" 
102    dbhelper.execute_non_query(comp.env, sql, ticket_id, change_time, field)
103
104class TimeTrackingTicketObserver(Component):
105    implements(ITicketChangeListener)
106    def __init__(self):
107        pass
108
109    def watch_hours(self, ticket, author=None):
110        ticket_id = ticket.id
111        hours = convertfloat(ticket['hours'])
112        change_time = ticket['changetime']
113        # no hours, changed
114        if hours == 0:
115            return
116
117        # if we dont have an author just skip permissions check
118        # its probably not important as this is generated data
119        # and it was probably caused by someone with ticket admin modifying history
120        if author:
121            self.log.debug("Checking permissions")       
122            perm = PermissionCache(self.env, author)
123            if not perm or not perm.has_permission("TIME_RECORD"):
124                self.log.debug("Skipping recording because no permission to affect time")
125                tup = (ticket_id, change_time, "hours")
126                self.log.debug("deleting ticket change %s %s %s %s" % tup)
127                try:
128                    delete_ticket_change(self, ticket_id, change_time, "hours")
129                except Exception, e:
130                    self.log.exception("FAIL: %s" % e)
131                self.log.debug("hours change deleted")
132                return
133            self.log.debug("passed permissions check")
134        @self.env.with_transaction()
135        def fn(db):
136            update_hours_to_floats(db, ticket_id)
137            save_custom_field_value( db, ticket_id, "hours", '0')
138            insert_totalhours_changes( db, ticket_id )
139            update_totalhours_custom ( db, ticket_id )
140
141    def ticket_created(self, ticket):
142        """Called when a ticket is created."""
143        hours = convertfloat(ticket['hours'])
144        self.watch_hours(ticket, ticket['reporter']) # clears the hours
145        #makes the hours a ticket change like all the other hours records
146        t = Ticket (self.env, ticket.id)
147        if hours > 0:
148            t['hours']=str(hours);
149            t.save_changes(ticket['reporter'])
150
151    def ticket_changed(self, ticket, comment, author, old_values):
152        """Called when a ticket is modified.
153       
154        `old_values` is a dictionary containing the previous values of the
155        fields that have changed.
156        """
157        self.watch_hours(ticket, author)
158
159    def ticket_change_deleted(self, ticket, cdate, changes):
160        """called when a ticket change is deleted"""
161        self.watch_hours(ticket)
162
163    def ticket_deleted(self, ticket):
164        """Called when a ticket is deleted."""
165
166class TimeTrackingTicketValidator(Component):
167    implements(ITicketManipulator)
168
169    def __init__(self):
170        pass
171
172    def prepare_ticket(req, ticket, fields, actions):
173        """not currently called"""
174
175    def validate_ticket(self, req, ticket):
176        """Validate a ticket after it's been populated from user input.
177
178        Must return a list of `(field, message)` tuples, one for each problem
179        detected. `field` can be `None` to indicate an overall problem with the
180        ticket. Therefore, a return value of `[]` means everything is OK."""
181        errors = []
182        try:
183            convertfloat(ticket.values['hours'])
184        except KeyError:
185            self.log.exception("The hours field was not submitted")
186        except ValueError:
187            errors.append(('Add Hours to Ticket', 'Value must be a number'))
188        try:
189            convertfloat(ticket.values['estimatedhours'])
190        except KeyError:
191            self.log.exception("The estimatedhours field was not submitted")
192        except ValueError:
193            errors.append(('Estimated Number of Hours', 'Value must be a number'))
194        return errors
Note: See TracBrowser for help on using the repository browser.