source: worklogplugin/trunk/worklog/manager.py

Last change on this file was 16797, checked in by Ryan J Ollos, 6 years ago

WorkLog 1.0dev: Refactor and remove debug print statement

Refs #12627.

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