Ticket #1005: trac-ticket-work-tracking.patch
| File trac-ticket-work-tracking.patch, 16.6 KB (added by kisg, 4 years ago) |
|---|
-
wiki-default/TracTickets
29 29 * '''Status''' - What is the current status? 30 30 * '''Summary''' - A brief description summarizing the problem or issue. 31 31 * '''Description''' - The body of the ticket. A good description should be '''specific, descriptive and to the point'''. 32 * '''Planned work''' - The planned work to resolve the ticket in man hours. This field is optional. 33 * '''Actual work''' - The work amount already spent on the resolution of the ticket in man hours. 34 * '''Remaining work''' - The remaining work amount planned for the ticket. This value is computed automatically from the '''Planned work''' and '''Actual work''' fields. 32 35 36 37 33 38 == Changing and Commenting Tickets == 34 39 35 40 Once a ticket has been entered into Trac, you can at any time change the … … 38 43 39 44 When viewing a ticket, this log of changes will appear below the main ticket area. 40 45 46 The '''Planned work''' and '''Actual work''' fields along with some custom reports can be used for simple project planning and project controlling. 47 The '''Actual work''' field cannot be updated directly. In the '''Properties''' block a '''Spent work''' field is shown. The value entered into this field (in man hours) will be added to the value of the '''Actual work''' field. In the ticket changelog the original value is saved. This way the work spent on every ticket change can be tracked. This feature can be used for simple work hour reporting and project metrics. 48 41 49 ''In the Trac project, we use ticket comments to discuss issues and 42 50 tasks. This makes understanding the motivation behind a design- or implementation choice easier, when returning to it later.'' 43 51 … … 72 80 '''Example:''' ''/trac/newticket?summary=Compile%20Error&version=1.0&component=gui'' 73 81 74 82 75 See also: TracGuide, TracWiki, TracTicketsCustomFields, TracNotification 76 No newline at end of file 83 See also: TracGuide, TracWiki, TracTicketsCustomFields, TracNotification -
wiki-default/TracRoadmap
5 5 6 6 == The Roadmap View == 7 7 8 Basically, the roadmap is just a list of future milestones. You can add a description to milestones (using WikiFormatting) describing main objectives, for example. In addition, tickets targeted for a milestone are aggregated, and the ratio between active and resolved tickets is displayed as a milestone progress bar. 8 Basically, the roadmap is just a list of future milestones. You can add a description to milestones (using WikiFormatting) describing main objectives, for example. In addition, tickets targeted for a milestone are aggregated, and the ratio between active and resolved tickets is displayed as a milestone progress bar. 9 9 10 A second progress bar is shown if the tickets targeted for the milestone have their '''Planned work''' and '''Actual work''' properties filled out. This bar shows the total planned work and the already spent work for the tickets. Using this feature you get a more sophisticated overview of your projects. 11 10 12 == The Milestone View == 11 13 12 14 It is possible to drill down into this simple statistic by viewing the individual milestone pages. By default, the active/resolved ratio will be grouped and displayed by component. You can also regroup the status by other criteria, such as ticket owner or severity. Ticket numbers are linked to [wiki:TracQuery custom queries] listing corresponding tickets. … … 26 28 '''Note:''' For tickets to be included in the calendar (as TO-DO items), you need to be authenticated when copying the link. You will only see tickets assigned to yourself, and associated with a milestone. 27 29 28 30 ---- 29 See also: TracTickets, TracReports, TracQuery, TracGuide 30 No newline at end of file 31 See also: TracTickets, TracReports, TracQuery, TracGuide -
trac/db_default.py
94 94 resolution text, 95 95 summary text, -- one-line summary 96 96 description text, -- problem description (long) 97 keywords text 97 keywords text, 98 planned_work text, -- planned work for the ticket in man hours 99 actual_work text -- actual work spent on the ticket 98 100 ); 99 101 CREATE TABLE ticket_change ( 100 102 ticket integer, … … 338 340 WHERE status IN ('new', 'assigned', 'reopened') 339 341 AND p.name = t.priority AND p.type = 'priority' 340 342 ORDER BY (owner = '$USER') DESC, p.value, milestone, severity, time 341 """)) 343 """), 344 ('Active Tickets over budget', 345 """ 346 * List all active tickets that are over budget. (In non-manager talk: \'\'\'Actual work\'\'\' is more than \'\'\'Planned work\'\'\') 347 * Color each row based on priority. 348 * If a ticket has been accepted, a '*' is appended after the owner's name 349 """, 350 """ 351 SELECT p.value AS __color__, 352 id AS ticket, summary, component, version, milestone, severity, 353 (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner, 354 time AS created, 355 changetime AS _changetime, description AS _description, 356 reporter AS _reporter 357 FROM ticket t, enum p 358 WHERE status IN ('new', 'assigned', 'reopened') 359 AND (actual_work + 0) > (planned_work + 0) 360 AND p.name = t.priority AND p.type = 'priority' 361 ORDER BY p.value, milestone, severity, time 362 """) 363 ) 342 364 343 365 344 366 ## -
trac/Milestone.py
31 31 def get_tickets_for_milestone(env, db, milestone, field='component'): 32 32 custom = field not in Ticket.std_fields 33 33 cursor = db.cursor() 34 sql = 'SELECT ticket.id AS id, ticket.status AS status, ' 34 sql = 'SELECT ticket.id AS id, ticket.status AS status, ' \ 35 'ticket.planned_work AS planned_work, ticket.actual_work AS actual_work, ' 35 36 if custom: 36 37 sql += 'ticket_custom.value AS %s ' \ 37 38 'FROM ticket LEFT OUTER JOIN ticket_custom ON id = ticket ' \ … … 50 51 ticket = { 51 52 'id': int(row['id']), 52 53 'status': row['status'], 54 'planned_work': row['planned_work'], 55 'actual_work': row['actual_work'], 53 56 field: row[field] 54 57 } 55 58 tickets.append(ticket) … … 81 84 82 85 def calc_ticket_stats(tickets): 83 86 total_cnt = len(tickets) 84 active = [ticket for ticket in tickets if ticket['status'] != 'closed'] 85 active_cnt = len(active) 87 planned_work = 0.0 88 actual_work = 0.0 89 active_cnt = 0 90 for ticket in tickets: 91 planned_work += float(ticket['planned_work']) 92 actual_work += float(ticket['actual_work']) 93 if ticket['status'] != 'closed': 94 active_cnt += 1 95 86 96 closed_cnt = total_cnt - active_cnt 87 97 88 98 percent_complete = 0 89 99 if total_cnt > 0: 90 100 percent_complete = float(closed_cnt) / float(total_cnt) * 100 91 101 102 103 work_percent_complete = 0 104 if planned_work > 0: 105 work_percent_complete = float(actual_work) / float(planned_work) * 100 106 92 107 return { 93 108 'total_tickets': total_cnt, 94 109 'active_tickets': active_cnt, 95 110 'closed_tickets': closed_cnt, 96 'percent_complete': percent_complete 111 'percent_complete': percent_complete, 112 'planned_work': planned_work, 113 'actual_work' : actual_work, 114 'work_percent_complete': work_percent_complete 97 115 } 98 116 99 117 … … 340 358 percent_total = float(len(group_tickets)) / float(len(tickets)) 341 359 self.req.hdf.setValue('%s.percent_total' % prefix, 342 360 str(percent_total * 100)) 343 stats = calc_ticket_stats(group_tickets)344 add_to_hdf( stats, self.req.hdf, prefix)361 group_stats = calc_ticket_stats(group_tickets) 362 add_to_hdf(group_stats, self.req.hdf, prefix) 345 363 queries = get_query_links(self.env, milestone['name'], by, group) 346 364 add_to_hdf(queries, self.req.hdf, '%s.queries' % prefix) 347 365 group_no += 1 -
trac/Ticket.py
37 37 class Ticket(UserDict): 38 38 std_fields = ['time', 'component', 'severity', 'priority', 'milestone', 39 39 'reporter', 'owner', 'cc', 'url', 'version', 'status', 'resolution', 40 'keywords', 'summary', 'description' ]40 'keywords', 'summary', 'description', 'planned_work', 'actual_work'] 41 41 42 42 def __init__(self, *args): 43 43 UserDict.__init__(self) … … 78 78 if rows: 79 79 for r in rows: 80 80 self['custom_' + r[0]] = r[1] 81 82 # Compute the remaining work 83 if self['planned_work']: 84 remaining_work = float(self['planned_work']) - float(self['actual_work']) 85 self['remaining_work'] = remaining_work 86 81 87 self._forget_changes() 82 88 83 89 def populate(self, dict): … … 102 108 now = int(time.time()) 103 109 self['time'] = now 104 110 self['changetime'] = now 111 self['actual_work'] = 0 112 if not self['planned_work']: 113 self['planned_work'] = 0 105 114 106 115 std_fields = filter(lambda n: n[:7] != 'custom_', self.keys()) 107 116 custom_fields = filter(lambda n: n[:7] == 'custom_', self.keys()) … … 119 128 self._forget_changes() 120 129 return id 121 130 122 def save_changes(self, db, author, comment, when = 0):131 def save_changes(self, db, author, comment, spent_work, when = 0): 123 132 """Store ticket changes in the database. 124 133 The ticket must already exist in the database.""" 125 134 assert self.has_key('id') … … 128 137 when = int(time.time()) 129 138 id = self['id'] 130 139 131 if not self._old and not comment : return # Not modified140 if not self._old and not comment and not spent_work: return # Not modified 132 141 133 142 # If the component is changed on a 'new' ticket then owner field 134 143 # is updated accordingly. (#623). … … 155 164 fname = name 156 165 cursor.execute ('UPDATE ticket SET %s=%s WHERE id=%s', 157 166 fname, self[name], id) 158 159 167 cursor.execute ('INSERT INTO ticket_change ' 160 '(ticket, time, author, field, oldvalue, newvalue) '161 'VALUES (%s, %s, %s, %s, %s, %s)',162 id, when, author, fname, self._old[name], self[name])168 '(ticket, time, author, field, oldvalue, newvalue) ' 169 'VALUES (%s, %s, %s, %s, %s, %s)', 170 id, when, author, fname, self._old[name], self[name]) 163 171 if comment: 164 172 cursor.execute ('INSERT INTO ticket_change ' 165 173 '(ticket,time,author,field,oldvalue,newvalue) ' 166 174 "VALUES (%s, %s, %s, 'comment', '', %s)", 167 175 id, when, author, comment) 176 if spent_work: 177 fname = 'actual_work' 178 actual_work = float(self[fname]) + float(spent_work) 179 cursor.execute ('UPDATE ticket SET %s=%s WHERE id=%s', 180 fname, actual_work, id) 181 cursor.execute ('INSERT INTO ticket_change ' 182 '(ticket, time, author, field, newvalue) ' 183 'VALUES (%s, %s, %s, %s, %s)', 184 id, when, author, 'spent_work', spent_work) 168 185 169 186 cursor.execute ('UPDATE ticket SET changetime=%s WHERE id=%s', when, id) 170 187 db.commit() … … 372 389 ticket.save_changes(self.db, 373 390 self.args.get('author', self.req.authname), 374 391 self.args.get('comment'), 392 self.args.get('spent_work'), 375 393 when=now) 376 394 377 395 tn = TicketNotifyEmail(self.env) … … 454 472 for field in Ticket.std_fields: 455 473 if self.args.has_key(field) and field != 'reporter': 456 474 ticket[field] = self.args.get(field) 475 476 # Compute new actual work and remaining work 477 spent_work = float(self.args.get('spent_work')) 478 if spent_work > 0: 479 ticket['actual_work'] = float(ticket['actual_work']) + spent_work 480 ticket['remaining_work'] = float(ticket['planned_work']) - float(ticket['actual_work']) 481 ticket['spent_work'] = spent_work 482 457 483 self.req.hdf.setValue('ticket.action', action) 458 484 reporter_id = self.args.get('author') 459 485 comment = self.args.get('comment') -
templates/roadmap.cs
25 25 </p> 26 26 <?cs with:stats = milestone.stats ?> 27 27 <?cs if:#stats.total_tickets > #0 ?> 28 <h3> Ticket resolution progress </h3> 28 29 <div class="progress"> 29 30 <div style="width: <?cs var:#stats.percent_complete ?>%"></div> 30 31 </div> … … 38 39 var:stats.closed_tickets ?></a></dd> 39 40 </dl> 40 41 <?cs /if ?> 42 <?cs if:#stats.planned_work > #0 ?> 43 <h3> Work progress </h3> 44 <div class="progress"> 45 <div style="width: <?cs var:#stats.work_percent_complete ?>%"></div> 46 </div> 47 <p class="percent"><?cs var:#stats.work_percent_complete ?>%</p> 48 <dl> 49 <dt>Planned work:</dt> 50 <dd><?cs var:stats.planned_work ?> (hrs)</dd> 51 <dt>Actual work:</dt> 52 <dd><?cs var:stats.actual_work ?> (hrs)</dd> 53 </dl> 54 <?cs /if ?> 41 55 <?cs /with ?> 42 56 </div> 43 57 <div class="descr"><?cs var:milestone.descr ?></div> -
templates/ticket.cs
51 51 call:ticketprop("Status", "status", ticket.status, 0) ?><?cs 52 52 call:ticketprop("Version", "version", ticket.version, 0) ?><?cs 53 53 call:ticketprop("Resolution", "resolution", ticket.resolution, 0) ?><?cs 54 call:ticketprop("Planned work (hrs)", "planned_work", ticket.planned_work, 0) ?><?cs 55 call:ticketprop("Actual work (hrs)", "actual_work", ticket.actual_work, 0) ?><?cs 56 call:ticketprop("Remaining work (hrs)", "remaining_work", ticket.remaining_work, 0) ?><?cs 54 57 call:ticketprop("Milestone", "milestone", ticket.milestone, 0) ?><?cs 55 58 set:last_prop = #1 ?><?cs 56 59 call:ticketprop("Keywords", "keywords", ticket.keywords, 0) ?><?cs … … 118 121 <li><strong>attachment</strong> added: <?cs var:change.new ?></li><?cs 119 122 elif $change.field == "description" ?> 120 123 <li><strong><?cs var:change.field ?></strong> changed.</li><?cs 124 elif $change.field == "spent_work" ?> 125 <li><strong>Work spent on change</strong>: <?cs var:change.new ?> hrs</li><?cs 121 126 elif $change.old == "" ?> 122 127 <li><strong><?cs var:change.field ?></strong> set to <em><?cs var:change.new ?></em></li><?cs 123 128 else ?> … … 191 196 <label for="keywords">Keywords:</label> 192 197 <input type="text" id="keywords" name="keywords" size="20" 193 198 value="<?cs var:ticket.keywords ?>" /> 199 <br /> 200 <label for="planned_work">Planned work:</label> 201 <input type="text" id="planned_work" name="planned_work" size="5" 202 value="<?cs var:ticket.planned_work ?>" /> (hrs) 203 <br /> 204 <label for="spent_work">Spent work:</label> 205 <input type="text" id="spent_work" name="spent_work" size="5" 206 value="<?cs var:ticket.spent_work ?>" /> (hrs) 194 207 </div> 195 208 <div class="col2"> 196 209 <label for="priority">Priority:</label><?cs -
templates/newticket.cs
52 52 <label for="keywords">Keywords:</label> 53 53 <input type="text" id="keywords" name="keywords" size="20" 54 54 value="<?cs var:newticket.keywords ?>" /> 55 <br /> 56 <label for="planned_work">Planned work:</label> 57 <input type="text" id="planned_work" name="planned_work" size="5" 58 value="<?cs var:newticket.planned_work ?>" /> (hrs) 55 59 </div> 56 60 <div class="col2"> 57 61 <label for="priority">Priority:</label><?cs
