Changeset 2552
- Timestamp:
- 08/07/07 19:42:12 (1 year ago)
- Files:
-
- worklogplugin/0.10/worklog/api.py (modified) (6 diffs)
- worklogplugin/0.10/worklog/dbhelper.py (deleted)
- worklogplugin/0.10/worklog/htdocs/worklogplugin.css (modified) (3 diffs)
- worklogplugin/0.10/worklog/manager.py (modified) (6 diffs)
- worklogplugin/0.10/worklog/templates/worklog.cs (modified) (2 diffs)
- worklogplugin/0.10/worklog/templates/worklog_webadminui.cs (modified) (2 diffs)
- worklogplugin/0.10/worklog/timeline_hook.py (modified) (4 diffs)
- worklogplugin/0.10/worklog/uihooks_ticket.py (modified) (3 diffs)
- worklogplugin/0.10/worklog/usermanual.py (modified) (1 diff)
- worklogplugin/0.10/worklog/webui.py (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
worklogplugin/0.10/worklog/api.py
r2458 r2552 1 1 import re 2 import dbhelper3 2 import time 4 3 from usermanual import * … … 17 16 class WorkLogSetupParticipant(Component): 18 17 implements(IEnvironmentSetupParticipant) 18 19 db_version_key = None 20 db_version = None 21 db_installed_version = None 19 22 20 23 """Extension point interface for components that need to participate in the … … 22 25 additional database tables.""" 23 26 def __init__(self): 24 pass 27 self.db_version_key = 'WorklogPlugin_Db_Version' 28 self.db_version = 2 29 self.db_installed_version = None 30 31 # Initialise database schema version tracking. 32 db = self.env.get_db_cnx() 33 cursor = db.cursor() 34 cursor.execute("SELECT value FROM system WHERE name=%s", (self.db_version_key,)) 35 try: 36 self.db_installed_version = int(cursor.fetchone()[0]) 37 except: 38 self.db_installed_version = 0 39 cursor.execute("INSERT INTO system (name,value) VALUES(%s,%s)", 40 (self.db_version_key, self.db_installed_version)) 41 db.commit() 42 db.close() 43 25 44 26 45 def environment_created(self): … … 29 48 self.upgrade_environment(None) 30 49 31 def db_needs_upgrade(self): 32 work_log = dbhelper.db_table_exists(self.env.get_db_cnx(), 'work_log'); 33 return not work_log 50 def system_needs_upgrade(self): 51 return self.db_installed_version < self.db_version 34 52 35 def db_do_upgrade(self): 36 work_log = dbhelper.db_table_exists(self.env.get_db_cnx(), 'work_log'); 37 if not work_log: 38 print "Creating work_log table" 39 sql = """ 40 CREATE TABLE work_log ( 41 user text, 42 ticket integer, 43 lastchange integer, 44 starttime integer, 45 endtime integer 46 ); 47 """ 48 dbhelper.execute_non_query(self.env.get_db_cnx(), sql) 53 def do_db_upgrade(self): 54 # Legacy support hack (supports upgrades from revisions r2495 or before) 55 if self.db_installed_version == 0: 56 db = self.env.get_db_cnx() 57 cursor = db.cursor() 58 try: 59 cursor.execute('SELECT * FROM work_log LIMIT 1') 60 # We've succeeded so we actually have version 1 61 self.db_installed_version = 1 62 except: 63 pass 64 db.close() 65 # End Legacy support hack 66 67 db = self.env.get_db_cnx() 68 cursor = db.cursor() 69 70 # Do the staged updates 71 try: 72 if self.db_installed_version < 1: 73 print 'Creating work_log table' 74 cursor.execute('CREATE TABLE work_log (' 75 'user TEXT,' 76 'ticket INTEGER,' 77 'lastchange INTEGER,' 78 'starttime INTEGER,' 79 'endtime INTEGER' 80 ')') 81 82 if self.db_installed_version < 2: 83 print 'Updating work_log table (v2)' 84 cursor.execute('ALTER TABLE work_log ' 85 'ADD COLUMN comment TEXT') 86 87 #if self.db_installed_version < 3: 88 # print 'Updating work_log table (v3)' 89 # cursor.execute('...') 90 91 # Updates complete, set the version 92 cursor.execute("UPDATE system SET value=%s WHERE name=%s", 93 (self.db_version, self.db_version_key)) 94 db.commit() 95 db.close() 96 except Exception, e: 97 self.log.error("WorklogPlugin Exception: %s" % (e,)); 98 db.rollback() 99 100 49 101 50 102 def needs_user_man(self): 51 maxversion = dbhelper.get_scalar(self.env.get_db_cnx(), 52 "SELECT MAX(version) FROM wiki WHERE name like %s", 0, 53 user_manual_wiki_title) 54 if (not maxversion) or maxversion < user_manual_version: 55 return True 56 return False 103 db = self.env.get_db_cnx() 104 cursor = db.cursor() 105 try: 106 cursor.execute('SELECT MAX(version) FROM wiki WHERE name=%s', (user_manual_wiki_title,)) 107 maxversion = int(cursor.fetchone()[0]) 108 except: 109 maxversion = 0 110 db.close() 111 112 return maxversion < user_manual_version 57 113 58 114 def do_user_man_update(self): 59 60 115 when = int(time.time()) 61 sql = """ 62 INSERT INTO wiki (name,version,time,author,ipnr,text,comment,readonly) 63 VALUES ( %s, %s, %s, 'Timing and Estimation Plugin', '127.0.0.1', %s,'',0) 64 """ 65 dbhelper.execute_non_query(self.env.get_db_cnx(),sql, 66 user_manual_wiki_title, 67 user_manual_version, 68 when, 69 user_manual_content) 70 116 db = self.env.get_db_cnx() 117 cursor = db.cursor() 118 cursor.execute('INSERT INTO wiki (name,version,time,author,ipnr,text,comment,readonly) ' 119 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)', 120 (user_manual_wiki_title, user_manual_version, when, 121 'WorkLog Plugin', '127.0.0.1', user_manual_content, 122 '', 0)) 123 db.commit() 124 db.close() 71 125 72 126 def environment_needs_upgrade(self, db): … … 77 131 78 132 """ 79 return (self. db_needs_upgrade() \133 return (self.system_needs_upgrade() \ 80 134 or self.needs_user_man()) 81 135 … … 91 145 return True 92 146 print "Worklog needs an upgrade" 93 if self. db_needs_upgrade():147 if self.system_needs_upgrade(): 94 148 p("Upgrading Database") 95 self.d b_do_upgrade()149 self.do_db_upgrade() 96 150 if self.needs_user_man(): 97 151 p("Upgrading usermanual") worklogplugin/0.10/worklog/htdocs/worklogplugin.css
r2425 r2552 1 #content.worklog2 {3 width: 100%;4 }5 1 #worklogmanual 6 2 { … … 8 4 float: right; 9 5 } 10 6 #worklog_comment 7 { 8 font-size: 70%; 9 color: green; 10 } 11 #worklog_time_delta 12 { 13 font-size: 70%; 14 color: green; 15 } 16 table#worklog_report 17 { 18 width: 99%; 19 clear: both; 20 } 11 21 #content.worklog table th 12 22 { … … 26 36 { 27 37 color: #999; 28 }29 #messages{30 38 } 31 39 .message worklogplugin/0.10/worklog/manager.py
r2495 r2552 129 129 self.stop_work() 130 130 self.explanation = '' 131 131 132 132 cursor = db.cursor() 133 133 cursor.execute('INSERT INTO work_log (user, ticket, lastchange, starttime, endtime) ' … … 137 137 138 138 139 def stop_work(self, stoptime=None ):139 def stop_work(self, stoptime=None, comment=''): 140 140 active = self.get_active_task() 141 141 if not active: … … 158 158 cursor = db.cursor() 159 159 cursor.execute('UPDATE work_log ' 160 'SET endtime=%s, lastchange=%s '160 'SET endtime=%s, lastchange=%s, comment=%s ' 161 161 'WHERE user=%s AND lastchange=%s AND endtime=0', 162 (stoptime, stoptime, self.authname, active['lastchange']))162 (stoptime, stoptime, comment, self.authname, active['lastchange'])) 163 163 164 164 message = '' 165 if self.config.getbool('worklog', 'comment'): 165 # Leave a comment if the user has configured this or if they have entered 166 # a work log comment. 167 if self.config.getbool('worklog', 'comment') or comment: 166 168 started = datetime.fromtimestamp(active['starttime']) 167 169 finished = datetime.fromtimestamp(stoptime) 168 message = '%s worked on this ticket for %s between %s %s and %s %s ' % \170 message = '%s worked on this ticket for %s between %s %s and %s %s.' % \ 169 171 (self.authname, pretty_timedelta(started, finished), \ 170 172 format_date(active['starttime']), format_time(active['starttime']), \ 171 173 format_date(stoptime), format_time(stoptime)) 174 if comment: 175 message += "\n[[BR]]\n" + comment 176 172 177 if self.config.getbool('worklog', 'timingandestimation') and \ 173 178 self.config.get('ticket-custom', 'hours'): … … 185 190 db = self.env.get_db_cnx() 186 191 tckt = Ticket(self.env, active['ticket'], db) 187 tckt['hours'] = str(float(delta) / 60) 192 # This hideous hack is here because I don't yet know how to do variable-DP rounding in python - sorry! 193 # It's meant to round to 2 DP, so please replace it if you know how. Many thanks, MK. 194 tckt['hours'] = str(float(int(100 * float(delta) / 60) / 100.0)) 188 195 self.save_ticket(tckt, db, message) 189 196 message = '' … … 225 232 226 233 task = {} 227 cursor.execute('SELECT wl.user, wl.ticket, t.summary, wl.lastchange, wl.starttime, wl.endtime '234 cursor.execute('SELECT wl.user, wl.ticket, t.summary, wl.lastchange, wl.starttime, wl.endtime, wl.comment ' 228 235 'FROM work_log wl ' 229 236 'LEFT JOIN ticket t ON wl.ticket=t.id ' 230 237 'WHERE wl.user=%s AND wl.lastchange=%s', (self.authname, lastchange)) 231 238 232 for user,ticket,summary,lastchange,starttime,endtime in cursor: 239 for user,ticket,summary,lastchange,starttime,endtime,comment in cursor: 240 if not comment: 241 comment = '' 242 233 243 task['user'] = user 234 244 task['ticket'] = ticket … … 237 247 task['starttime'] = float(starttime) 238 248 task['endtime'] = float(endtime) 249 task['comment'] = comment 239 250 return task 240 251 worklogplugin/0.10/worklog/templates/worklog.cs
r2452 r2552 11 11 </div> 12 12 13 <table border="0" cellspacing="0" cellpadding="0" >13 <table border="0" cellspacing="0" cellpadding="0" id="worklog_report"> 14 14 <tr> 15 15 <th>User</th> 16 16 <th>Activity</th> 17 <th>Last Change</th> 17 <th>Time</th> 18 <th>Comment</th> 18 19 </tr> 19 20 <?cs each:log = worklog.worklog ?> … … 23 24 <td><a href="<?cs var:log.ticket_url ?>">#<?cs var:log.ticket ?></a>: <?cs var:log.summary ?></td> 24 25 <?cs else ?> 25 <td><em>Idle</em> <small>(Last worked on: <a href="<?cs var:log.ticket_url ?>">#<?cs var:log.ticket ?></a>: <?cs var:log.summary ?>)</small></td> 26 <td><em>Idle</em> <small>(Last worked on: <a href="<?cs var:log.ticket_url ?>">#<?cs var:log.ticket ?></a>: <?cs var:log.summary ?>)</small></td> 26 27 <?cs /if ?> 27 <td><?cs var:log.delta ?></td> 28 <td><span id="worklog_time_delta"><?cs var:log.delta ?></span></td> 29 <td><span id="worklog_comment"><?cs var:log.comment ?></span></td> 28 30 </tr> 29 31 <?cs /each ?> worklogplugin/0.10/worklog/templates/worklog_webadminui.cs
r2494 r2552 10 10 <div class="field"> 11 11 <input type="checkbox" id="comment" name="comment" value="1" <?cs var:settings.comment ?>> 12 <label for="comment">Automatically add a comment when you stop work on a ticket?</label> 12 <label for="comment">Automatically add a comment when you stop work on a ticket?</label><br /> 13 <small>Please note that if you leave a comment when you stop work, then a comment is always added to the ticket to ensure all information is present on the ticket.</small> 13 14 </div> 14 15 <div class="field"> … … 30 31 <div class="field"> 31 32 <label for="roundup">Round up to nearest minute</label> 32 <input type="text" id="roundup" name="roundup" size="4" value="<?cs var:settings.roundup ?>"><br >33 <input type="text" id="roundup" name="roundup" size="4" value="<?cs var:settings.roundup ?>"><br /> 33 34 <small>This only applies when integrating with the 34 35 <a href="http://www.trac-hacks.org/wiki/TimingAndEstimationPlugin">Timing and Estimation Plugin</a></small> worklogplugin/0.10/worklog/timeline_hook.py
r2495 r2552 8 8 from trac.web.href import Href 9 9 from trac.Timeline import ITimelineEventProvider 10 from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner 10 11 11 12 class WorkLogTimelineAddon(Component): … … 28 29 cursor = db.cursor() 29 30 30 cursor.execute("""SELECT wl.user,wl.ticket,wl.time,wl.starttime,wl. kind,wl.humankind,t.summary31 cursor.execute("""SELECT wl.user,wl.ticket,wl.time,wl.starttime,wl.comment,wl.kind,wl.humankind,t.summary 31 32 FROM ( 32 33 33 SELECT user, ticket, starttime AS time, starttime, 'workstart' AS kind, 'started' AS humankind34 SELECT user, ticket, starttime AS time, starttime, comment, 'workstart' AS kind, 'started' AS humankind 34 35 FROM work_log 35 36 36 37 UNION 37 38 38 SELECT user, ticket, endtime AS time, starttime, 'workstop' AS kind, 'stopped' AS humankind39 SELECT user, ticket, endtime AS time, starttime, comment, 'workstop' AS kind, 'stopped' AS humankind 39 40 FROM work_log 40 41 … … 45 46 % (start, stop)) 46 47 previous_update = None 47 for user,ticket,time,starttime, kind,humankind,summary in cursor:48 for user,ticket,time,starttime,comment,kind,humankind,summary in cursor: 48 49 summary = Markup.escape(summary) 49 50 time = float(time) 50 51 starttime = float(starttime) 51 52 52 if format == 'rss': 53 title = Markup('%s %s working on Ticket #%s (%s) ' % \54 (user, humankind, ticket, summary ))53 title = Markup('%s %s working on Ticket #%s (%s): %s' % \ 54 (user, humankind, ticket, summary, comment)) 55 55 else: 56 56 title = Markup('%s %s working on Ticket <em title="%s">#%s</em>' % \ … … 60 60 started = datetime.fromtimestamp(starttime) 61 61 finished = datetime.fromtimestamp(time) 62 message = 'Time spent: ' + pretty_timedelta(started, finished) 62 if comment: 63 message = wiki_to_oneliner(comment, self.env, req, shorten=True) + Markup('<br />(Time spent: %s)' % pretty_timedelta(started, finished)) 64 else: 65 message = 'Time spent: %s' % pretty_timedelta(started, finished) 63 66 yield kind, href.ticket(ticket), title, time, user, message 64 67 worklogplugin/0.10/worklog/uihooks_ticket.py
r2494 r2552 71 71 f.setAttribute('action', '""" + req.href.worklog() + """') 72 72 f.setAttribute('class', 'inlinebuttons'); 73 74 var h_url = document.createElement('input'); 75 h_url.setAttribute('type', 'hidden'); 76 h_url.setAttribute('name', 'source_url'); 77 h_url.setAttribute('value', location.pathname); 78 f.appendChild(h_url); 79 73 80 var h = document.createElement('input'); 74 81 h.setAttribute('type', 'hidden'); … … 76 83 h.setAttribute('value', '""" + str(ticket) + """'); 77 84 f.appendChild(h); 85 78 86 var h2 = document.createElement('input'); 79 87 h2.setAttribute('type', 'hidden'); 80 88 h2.setAttribute('name', '__FORM_TOKEN'); 81 89 h2.setAttribute('value', '""" + str(req.incookie['trac_form_token'].value) + """'); 82 f.appendChild(h2); 90 f.appendChild(h2);""" 91 92 if stop: 93 script += """ 94 var s_comment_label = document.createElement('span'); 95 s_comment_label.id = 'worklog_comment'; 96 s_comment_label.innerHTML = 'Comment: '; 97 f.appendChild(s_comment_label); 98 99 var s_comment = document.createElement('input'); 100 s_comment.type = 'text'; 101 s_comment.size = '50'; 102 s_comment.name = 'comment'; 103 f.appendChild(s_comment);""" 104 105 script += """ 83 106 var s = document.createElement('input'); 84 107 s.setAttribute('type', 'submit');""" … … 156 179 button_js = self.get_button_js(req, ticket) 157 180 elif task and task['ticket'] == ticket: 158 # We are curr netly working on this, so display the stop button...181 # We are currently working on this, so display the stop button... 159 182 button_js = self.get_button_js(req, ticket, True) 160 183 worklogplugin/0.10/worklog/usermanual.py
r2380 r2552 1 1 user_manual_title = "Work Log Plugin User Manual" 2 user_manual_version = 12 user_manual_version = 2 3 3 user_manual_wiki_title = "WorkLogPluginUserManual" 4 4 user_manual_content = """ 5 = Timing and Estimation Plugin User Manual = 5 = !WorkLog Plugin User Manual = 6 This is a plugin that adds a Work Log capability to Trac. 6 7 7 This plugin provides a way to register which ticket you are currently working on. By doing this you keep the rest of your team in the loop so as to reduce the duplication of work.8 Basically, it allows you to register the fact you have started work on a ticket which effectively allows you to clock on and clock off. 8 9 9 It provides an extension to the XMLRPC plugin which allows the integration with Desktop applications which makes interaction with this plugin seemless 10 It uses javascript to add a button to the ticket page to allow you to start/stop working on a given ticket. 11 12 If the [http://trac-hacks.org/wiki/TimingAndEstimationPlugin TimingAndEstimationPlugin] is installed then when you clock off, the time spent on the ticket will be recorded. 13 14 If you visit the Work Log page (a new navigation entry), you will see a list of people (developers) and which tickets they are currently working on. Work log events are also logged to the Timeline for a historical view. 15 16 In the future it will also provide an extension to the XMLRPC plugin which allows the integration with Desktop applications which will make interaction with this plugin seemless. 10 17 """ worklogplugin/0.10/worklog/webui.py
r2495 r2552 14 14 INavigationContributor, ITemplateProvider 15 15 from trac.web.href import Href 16 16 from trac.wiki.formatter import wiki_to_html 17 from trac.util.text import CRLF 17 18 18 19 class WorkLogPage(Component): … … 40 41 db = self.env.get_db_cnx() 41 42 cursor = db.cursor() 42 cursor.execute('SELECT wl.user, s.value, wl.starttime, wl.endtime, wl.ticket, t.summary '43 cursor.execute('SELECT wl.user, s.value, wl.starttime, wl.endtime, wl.ticket, wl.comment, t.summary ' 43 44 'FROM (SELECT user,MAX(lastchange) lastchange FROM work_log GROUP BY user) wlt ' 44 45 'LEFT JOIN work_log wl ON wlt.user=wl.user AND wlt.lastchange=wl.lastchange ' … … 48 49 49 50 log = {} 50 for (user,name,starttime,endtime,ticket, summary) in cursor:51 for (user,name,starttime,endtime,ticket,comment,summary) in cursor: 51 52 starttime = float(starttime) 52 53 endtime = float(endtime) … … 57 58 if name: 58 59 dispname = '%s (%s)' % (name, user) 60 61 finished = '' 62 delta = '' 63 if not endtime == 0: 64 finished = datetime.fromtimestamp(endtime) 65 delta = 'Worked for %s (between %s %s and %s %s)' % \ 66 (pretty_timedelta(started, finished), 67 format_date(starttime), format_time(starttime), 68 format_date(endtime), format_time(endtime)) 69 else: 70 delta = 'Started %s ago (%s %s)' % \ 71 (pretty_timedelta(started), 72 format_date(starttime), format_time(starttime)) 73 74 75 minutes_elapsed = 0 76 if endtime == 0: 77 minutes_elapsed = int((int(time()) - starttime) / 60) 78 else: 79 minutes_elapsed = int((endtime - starttime) / 60) 80 59 81 log[user] = { "name": dispname, 60 82 "ticket": ticket, 61 83 "ticket_url": req.href.ticket(ticket), 84 "comment": wiki_to_html(comment, self.env, req), 62 85 "summary": summary, 63 86 "started": started, 64 "delta": 'Started ' + format_date(starttime) + " " + format_time(starttime) + \ 65 " (" + pretty_timedelta(started) + " ago)" 87 "delta": delta, 88 "finished": finished, 89 "minutes_elapsed": minutes_elapsed 66 90 } 91 92 return log 67 93 68 if not endtime == 0: 69 finished = datetime.fromtimestamp(endtime) 70 log[user]["finished"] = finished 71 log[user]["delta"] = 'Worked on from ' + format_date(starttime) + " " + format_time(starttime) + " - " + format_date(endtime) + " " + format_time(endtime) + \ 72 " (" + pretty_timedelta(started, finished) + ")" 94 def worklog_csv(self, req): 95 # Headers 96 req.write('user,full_name,starttime,endtime,ticket,ticket_summary,work_comment') 97 req.write(CRLF) 98 99 # Rows 100 db = self.env.get_db_cnx() 101 cursor = db.cursor() 102 cursor.execute('SELECT wl.user, s.value, wl.starttime, wl.endtime, wl.ticket, wl.comment, t.summary ' 103 'FROM work_log wl ' 104 'LEFT JOIN ticket t ON wl.ticket=t.id ' 105 'LEFT JOIN session_attribute s ON wl.user=s.sid AND s.name=\'name\' ' 106 'ORDER BY wl.lastchange DESC, wl.user') 107 for (user,name,starttime,endtime,ticket,comment,summary) in cursor: 108 if not comment: 109 comment = '' 110 111 req.write(user + ',') 112 req.write(name + ',') 113 req.write(str(starttime) + ',') 114 req.write(str(endtime) + ',') 115 req.write(str(ticket) + ',') 116 req.write(summary + ',') 117 req.write(comment) 118 req.write(CRLF) 119 73 120 74 121 75 return log76 77 122 # IRequestHandler methods 78 123 def match_request(self, req): … … 90 135 return None 91 136 137 if req.args.has_key('format') and req.args['format'] == 'csv': 138 req.send_header('Content-Type', 'text/plain;charset=utf-8') 139 self.worklog_csv(req) 140 return None 141 92 142 if req.method == 'POST': 93 143 mgr = WorkLogManager(self.env, self.config, req.authname) … … 97 147 else: 98 148 addMessage('You are now working on ticket #%s.' % (req.args['ticket'],)) 149 150 req.redirect(req.args['source_url']) 151 return None 152 99 153 elif req.args.has_key('stopwork'): 100 154 stoptime = None 101 155 if req.args.has_key('stoptime'): 102 156 stoptime = int(req.args['stoptime']) 103 if not mgr.stop_work(stoptime): 157 158 comment = '' 159 if req.args.has_key('comment'): 160 comment = str(req.args['comment']) 161 162 if not mgr.stop_work(stoptime, comment): 104 163 addMessage(mgr.get_explanation()) 105 164 else: 106 165 addMessage('You have stopped working.') 166 167 req.redirect(req.args['source_url']) 168 return None 107 169 170 # no POST, so they're just wanting a list of the worklog entries 108 171 req.hdf["worklog"] = {"messages": messages, 109 172 "worklog": self.get_worklog(req),
