root/worklogplugin/0.11/worklog/manager.py

Revision 3075, 13.4 kB (checked in by coling, 1 year ago)

Fixes #2431. Thanks for the report. If there are any more postgres issues please let me know.

  • Property svn:mime-type set to text/x-python
  • Property svn:eol-style set to native
Line 
1 from time import time
2 from datetime import tzinfo, timedelta, datetime
3 from util import pretty_timedelta
4 from trac.ticket.notification import TicketNotifyEmail
5 from trac.ticket import Ticket
6 from trac.ticket.web_ui import TicketModule
7 from trac.util.datefmt import format_date, format_time, to_datetime
8
9 class WorkLogManager:
10     env = None
11     config = None
12     authname = None
13     explanation = None
14     now = None
15    
16     def __init__(self, env, config, authname='anonymous'):
17         self.env = env
18         self.config = config
19         self.authname = authname
20         self.explanation = ""
21         self.now = int(time()) - 1
22
23     def get_explanation(self):
24         return self.explanation
25    
26     def can_work_on(self, ticket):
27         # Need to check several things.
28         # 1. Is some other user working on this ticket?
29         # 2. a) Is the autostopstart setting true? or
30         #    b) Is the user working on a ticket already?
31         # 3. a) Is the autoreassignaccept setting true? or
32         #    b) Is the ticket assigned to the user?
33
34         # 0. Are you logged in?
35         if self.authname == 'anonymous':
36             self.explanation = 'You need to be logged in to work on tickets.'
37             return False
38        
39         # 1. Other user working on it?
40         who,since = self.who_is_working_on(ticket)
41         if who:
42             if who != self.authname:
43                 self.explanation = 'Another user (%s) has been working on ticket #%s since %s' % (who, ticket, since)
44             else:
45                 self.explanation = 'You are already working on ticket #%s' % (ticket,)
46             return False
47
48         # 2. a) Is the autostopstart setting true? or
49         #    b) Is the user working on a ticket already?
50         if not self.config.getbool('worklog', 'autostopstart'):
51             active = self.get_active_task()
52             if active:
53                 self.explanation = 'You cannot work on ticket #%s as you are currently working on ticket #%s. You have to chill out.' % (ticket, active['ticket'])
54                 return False
55        
56         # 3. a) Is the autoreassignaccept setting true? or
57         #    b) Is the ticket assigned to the user?
58         if not self.config.getbool('worklog', 'autoreassignaccept'):
59             tckt = Ticket(self.env, ticket)
60             if self.authname != tckt['owner']:
61                 self.explanation = 'You cannot work on ticket #%s as you are not the owner. You should speak to %s.' % (ticket, tckt['owner'])
62                 return False
63
64         # If we get here then we know we can start work :)
65         return True
66
67     def save_ticket(self, tckt, db, msg):
68         # determine sequence number...
69         cnum = 0
70         tm = TicketModule(self.env)
71         for change in tm.grouped_changelog_entries(tckt, db):
72             if change['permanent']:
73                 cnum += 1
74         nowdt = self.now
75         nowdt = to_datetime(nowdt)
76         tckt.save_changes(self.authname, msg, nowdt, db, cnum+1)
77         ## Often the time overlaps and causes a db error,
78         ## especially when the trac integration post-commit hook is used.
79         ## NOTE TO SELF. I DON'T THINK THIS IS NECESSARY RIGHT NOW...
80         #count = 0
81         #while count < 10:
82         #    try:
83         #        tckt.save_changes(self.authname, msg, self.now, db, cnum+1)
84         #        count = 42
85         #    except Exception, e:
86         #        self.now += 1
87         #        count += 1
88         db.commit()
89        
90         tn = TicketNotifyEmail(self.env)
91         tn.notify(tckt, newticket=0, modtime=nowdt)
92         # We fudge time as it has to be unique
93         self.now += 1
94        
95
96     def start_work(self, ticket):
97
98         if not self.can_work_on(ticket):
99             return False
100
101         # We could just horse all the fields of the ticket to the right values
102         # bit it seems more correct to follow the in-build state-machine for
103         # ticket modification.
104
105         # If the ticket is closed, we need to reopen it.
106         db = self.env.get_db_cnx()
107         tckt = Ticket(self.env, ticket, db)
108
109         if 'closed' == tckt['status']:
110             tckt['status'] = 'reopened'
111             tckt['resolution'] = ''
112             self.save_ticket(tckt, db, 'Automatically reopening in order to start work.')
113
114             # Reinitialise for next test
115             db = self.env.get_db_cnx()
116             tckt = Ticket(self.env, ticket, db)
117
118            
119         if self.authname != tckt['owner']:
120             tckt['owner'] = self.authname
121             if 'new' == tckt['status']:
122                 tckt['status'] = 'assigned'
123             else:
124                 tckt['status'] = 'new'
125             self.save_ticket(tckt, db, 'Automatically reassigning in order to start work.')
126
127             # Reinitialise for next test
128             db = self.env.get_db_cnx()
129             tckt = Ticket(self.env, ticket, db)
130
131
132         if 'assigned' != tckt['status']:
133             tckt['status'] = 'assigned'
134             self.save_ticket(tckt, db, 'Automatically accepting in order to start work.')
135
136         # There is a chance the user may be working on another ticket at the moment
137         # depending on config options
138         if self.config.getbool('worklog', 'autostopstart'):
139             # Don't care if this fails, as with these arguments the only failure
140             # point is if there is no active task... which is the desired scenario :)
141             self.stop_work()
142             self.explanation = ''
143  
144         cursor = db.cursor()
145         cursor.execute('INSERT INTO work_log (worker, ticket, lastchange, starttime, endtime) '
146                        'VALUES (%s, %s, %s, %s, %s)',
147                        (self.authname, ticket, self.now, self.now, 0))
148         db.commit()
149         return True
150
151    
152     def stop_work(self, stoptime=None, comment=''):
153         active = self.get_active_task()
154         if not active:
155             self.explanation = 'You cannot stop working as you appear to be a complete slacker already!'
156             return False
157
158         if stoptime:
159             if stoptime <= active['starttime']:
160                 self.explanation = 'You cannot set your stop time to that value as it is before the start time!'
161                 return False
162             elif stoptime >= self.now:
163                 self.explanation = 'You cannot set your stop time to that value as it is in the future!'
164                 return False
165         else:
166             stoptime = self.now - 1
167
168         stoptime = float(stoptime)
169        
170         db = self.env.get_db_cnx();
171         cursor = db.cursor()
172         cursor.execute('UPDATE work_log '
173                        'SET endtime=%s, lastchange=%s, comment=%s '
174                        'WHERE worker=%s AND lastchange=%s AND endtime=0',
175                        (stoptime, stoptime, comment, self.authname, active['lastchange']))
176         db.commit()
177
178         message = ''
179         # Leave a comment if the user has configured this or if they have entered
180         # a work log comment.
181         if self.config.getbool('worklog', 'comment') or comment:
182             started = datetime.fromtimestamp(active['starttime'])
183             finished = datetime.fromtimestamp(stoptime)
184             message = '%s worked on this ticket for %s between %s %s and %s %s.' % \
185                       (self.authname, pretty_timedelta(started, finished), \
186                        format_date(active['starttime']), format_time(active['starttime']), \
187                        format_date(stoptime), format_time(stoptime))
188             if comment:
189                 message += "\n[[BR]]\n" + comment
190            
191         if self.config.getbool('worklog', 'timingandestimation') and \
192                self.config.get('ticket-custom', 'hours'):
193             if not message:
194                 message = 'Hours recorded automatically by the worklog plugin.'
195
196             round_delta = float(self.config.getint('worklog', 'roundup') or 1)
197            
198             # Get the delta in minutes
199             delta = float(int(stoptime) - int(active['starttime'])) / float(60)
200            
201             # Round up if needed
202             delta = int(round((delta / round_delta) + float(0.5))) * int(round_delta)
203            
204             db = self.env.get_db_cnx()
205             tckt = Ticket(self.env, active['ticket'], db)
206            
207             # This hideous hack is here because I don't yet know how to do variable-DP rounding in python - sorry!
208             # It's meant to round to 2 DP, so please replace it if you know how.  Many thanks, MK.
209             tckt['hours'] = str(float(int(100 * float(delta) / 60) / 100.0))
210             self.save_ticket(tckt, db, message)
211             message = ''
212
213         if message:
214             db = self.env.get_db_cnx()
215             tckt = Ticket(self.env, active['ticket'], db)
216             self.save_ticket(tckt, db, message)
217        
218         return True
219
220
221     def who_is_working_on(self, ticket):
222         db = self.env.get_db_cnx()
223         cursor = db.cursor()
224         cursor.execute('SELECT worker,starttime FROM work_log WHERE ticket=%s AND endtime=0', (ticket,))
225         try:
226             who,since = cursor.fetchone()
227             return who,float(since)
228         except:
229             pass
230         return None,None
231
232     def who_last_worked_on(self, ticket):
233         return "Not implemented"
234
235     def get_latest_task(self):
236         if self.authname == 'anonymous':
237             return None
238
239         db = self.env.get_db_cnx()
240         cursor = db.cursor()
241         cursor.execute('SELECT MAX(lastchange) FROM work_log WHERE worker=%s', (self.authname,))
242         row = cursor.fetchone()
243         if not row or not row[0]:
244             return None
245    
246         lastchange = row[0]
247    
248         task = {}
249         cursor.execute('SELECT wl.worker, wl.ticket, t.summary, wl.lastchange, wl.starttime, wl.endtime, wl.comment '
250                        'FROM work_log wl '
251                        'LEFT JOIN ticket t ON wl.ticket=t.id '
252                        'WHERE wl.worker=%s AND wl.lastchange=%s', (self.authname, lastchange))
253
254         for user,ticket,summary,lastchange,starttime,endtime,comment in cursor:
255             if not comment:
256                 comment = ''
257            
258             task['user'] = user
259             task['ticket'] = ticket
260             task['summary'] = summary
261             task['lastchange'] = float(lastchange)
262             task['starttime'] = float(starttime)
263             task['endtime'] = float(endtime)
264             task['comment'] = comment
265         return task
266    
267     def get_active_task(self):
268         task = self.get_latest_task()
269         if not task:
270             return None
271         if not task.has_key('endtime'):
272             return None
273
274         if task['endtime'] > 0:
275             return None
276
277         return task
278
279     def get_work_log(self, mode='all'):
280         db = self.env.get_db_cnx()
281         cursor = db.cursor()
282         if mode == 'user':
283             cursor.execute('SELECT wl.worker, s.value, wl.starttime, wl.endtime, wl.ticket, t.summary, t.status, wl.comment '
284                            'FROM work_log wl '
285                            'INNER JOIN ticket t ON wl.ticket=t.id '
286                            'LEFT JOIN session_attribute s ON wl.worker=s.sid AND s.name=\'name\' '
287                            'WHERE wl.worker=%s '
288                            'ORDER BY wl.lastchange DESC', (self.authname,))
289         elif mode == 'summary':
290             cursor.execute('SELECT wl.worker, s.value, wl.starttime, wl.endtime, wl.ticket, t.summary, t.status, wl.comment '
291                            'FROM (SELECT worker,MAX(lastchange) AS lastchange FROM work_log GROUP BY worker) wlt '
292                            'INNER JOIN work_log wl ON wlt.worker=wl.worker AND wlt.lastchange=wl.lastchange '
293                            'INNER JOIN ticket t ON wl.ticket=t.id '
294                            'LEFT JOIN session_attribute s ON wl.worker=s.sid AND s.name=\'name\' '
295                            'ORDER BY wl.lastchange DESC, wl.worker')
296         else:
297             cursor.execute('SELECT wl.worker, s.value, wl.starttime, wl.endtime, wl.ticket, t.summary, t.status, wl.comment '
298                            'FROM work_log wl '
299                            'INNER JOIN ticket t ON wl.ticket=t.id '
300                            'LEFT JOIN session_attribute s ON wl.worker=s.sid AND s.name=\'name\' '
301                            'ORDER BY wl.lastchange DESC, wl.worker')
302        
303         rv = []
304         for user,name,starttime,endtime,ticket,summary,status,comment  in cursor:
305             starttime = float(starttime)
306             endtime = float(endtime)
307            
308             started = datetime.fromtimestamp(starttime)
309            
310             dispname = user
311             if name:
312                 dispname = '%s (%s)' % (name, user)
313            
314             if not endtime == 0:
315                 finished = datetime.fromtimestamp(endtime)
316                 delta = 'Worked for %s (between %s %s and %s %s)' % \
317                         (pretty_timedelta(started, finished),
318                          format_date(starttime), format_time(starttime),
319                          format_date(endtime), format_time(endtime))
320             else:
321                 delta = 'Started %s ago (%s %s)' % \
322                         (pretty_timedelta(started),
323                          format_date(starttime), format_time(starttime))
324
325             rv.append({'user': user,
326                        'name': name,
327                        'dispname': dispname,
328                        'starttime': int(starttime),
329                        'endtime': int(endtime),
330                        'delta': delta,
331                        'ticket': ticket,
332                        'summary': summary,
333                        'status': status,
334                        'comment': comment})
335         return rv
336        
Note: See TracBrowser for help on using the browser.