Edgewall Software

Ticket #1005: trac-ticket-work-tracking.patch

File trac-ticket-work-tracking.patch, 16.6 KB (added by kisg, 4 years ago)

Trac ticket work tracking patch (for 0.8-stable, revision 1283)

  • wiki-default/TracTickets

     
    2929 * '''Status''' - What is the current status? 
    3030 * '''Summary''' - A brief description summarizing the problem or issue. 
    3131 * '''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. 
    3235 
     36 
     37 
    3338== Changing and Commenting Tickets == 
    3439 
    3540Once a ticket has been entered into Trac, you can at any time change the 
     
    3843 
    3944When viewing a ticket, this log of changes will appear below the main ticket area. 
    4045 
     46The '''Planned work''' and '''Actual work''' fields along with some custom reports can be used for simple project planning and project controlling. 
     47The '''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 
    4149''In the Trac project, we use ticket comments to discuss issues and 
    4250tasks. This makes understanding the motivation behind a design- or implementation choice easier, when returning to it later.'' 
    4351 
     
    7280'''Example:''' ''/trac/newticket?summary=Compile%20Error&version=1.0&component=gui'' 
    7381 
    7482 
    75 See also:  TracGuide, TracWiki, TracTicketsCustomFields, TracNotification 
    76  No newline at end of file 
     83See also:  TracGuide, TracWiki, TracTicketsCustomFields, TracNotification 
  • wiki-default/TracRoadmap

     
    55 
    66== The Roadmap View == 
    77 
    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. 
     8Basically, 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.  
    99 
     10A 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 
    1012== The Milestone View == 
    1113 
    1214It 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. 
     
    2628'''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. 
    2729 
    2830---- 
    29 See also: TracTickets, TracReports, TracQuery, TracGuide 
    30  No newline at end of file 
     31See also: TracTickets, TracReports, TracQuery, TracGuide 
  • trac/db_default.py

     
    9494        resolution      text, 
    9595        summary         text,           -- one-line summary 
    9696        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 
    98100); 
    99101CREATE TABLE ticket_change ( 
    100102        ticket          integer, 
     
    338340  WHERE status IN ('new', 'assigned', 'reopened')  
    339341AND p.name = t.priority AND p.type = 'priority' 
    340342  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""" 
     351SELECT 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) 
    342364 
    343365 
    344366## 
  • trac/Milestone.py

     
    3131def get_tickets_for_milestone(env, db, milestone, field='component'): 
    3232    custom = field not in Ticket.std_fields 
    3333    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, ' 
    3536    if custom: 
    3637        sql += 'ticket_custom.value AS %s ' \ 
    3738               'FROM ticket LEFT OUTER JOIN ticket_custom ON id = ticket ' \ 
     
    5051        ticket = { 
    5152            'id': int(row['id']), 
    5253            'status': row['status'], 
     54            'planned_work': row['planned_work'], 
     55            'actual_work': row['actual_work'], 
    5356            field: row[field] 
    5457        } 
    5558        tickets.append(ticket) 
     
    8184 
    8285def calc_ticket_stats(tickets): 
    8386    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 
    8696    closed_cnt = total_cnt - active_cnt 
    8797 
    8898    percent_complete = 0 
    8999    if total_cnt > 0: 
    90100        percent_complete = float(closed_cnt) / float(total_cnt) * 100 
    91101 
     102 
     103    work_percent_complete = 0 
     104    if planned_work > 0: 
     105        work_percent_complete = float(actual_work) / float(planned_work) * 100 
     106 
    92107    return { 
    93108        'total_tickets': total_cnt, 
    94109        'active_tickets': active_cnt, 
    95110        '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         
    97115    } 
    98116 
    99117 
     
    340358                percent_total = float(len(group_tickets)) / float(len(tickets)) 
    341359            self.req.hdf.setValue('%s.percent_total' % prefix, 
    342360                                  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) 
    345363            queries = get_query_links(self.env, milestone['name'], by, group) 
    346364            add_to_hdf(queries, self.req.hdf, '%s.queries' % prefix) 
    347365            group_no += 1 
  • trac/Ticket.py

     
    3737class Ticket(UserDict): 
    3838    std_fields = ['time', 'component', 'severity', 'priority', 'milestone', 
    3939                  'reporter', 'owner', 'cc', 'url', 'version', 'status', 'resolution', 
    40                   'keywords', 'summary', 'description'] 
     40                  'keywords', 'summary', 'description', 'planned_work', 'actual_work'] 
    4141 
    4242    def __init__(self, *args): 
    4343        UserDict.__init__(self) 
     
    7878        if rows: 
    7979            for r in rows: 
    8080                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         
    8187        self._forget_changes() 
    8288 
    8389    def populate(self, dict): 
     
    102108        now = int(time.time()) 
    103109        self['time'] = now 
    104110        self['changetime'] = now 
     111        self['actual_work'] = 0 
     112        if not self['planned_work']: 
     113            self['planned_work'] = 0             
    105114 
    106115        std_fields = filter(lambda n: n[:7] != 'custom_', self.keys()) 
    107116        custom_fields = filter(lambda n: n[:7] == 'custom_', self.keys()) 
     
    119128        self._forget_changes() 
    120129        return id 
    121130 
    122     def save_changes(self, db, author, comment, when = 0): 
     131    def save_changes(self, db, author, comment, spent_work, when = 0): 
    123132        """Store ticket changes in the database. 
    124133        The ticket must already exist in the database.""" 
    125134        assert self.has_key('id') 
     
    128137            when = int(time.time()) 
    129138        id = self['id'] 
    130139 
    131         if not self._old and not comment: return # Not modified 
     140        if not self._old and not comment and not spent_work: return # Not modified 
    132141 
    133142        # If the component is changed on a 'new' ticket then owner field 
    134143        # is updated accordingly. (#623). 
     
    155164                fname = name 
    156165                cursor.execute ('UPDATE ticket SET %s=%s WHERE id=%s', 
    157166                                fname, self[name], id) 
    158  
    159167            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]) 
    163171        if comment: 
    164172            cursor.execute ('INSERT INTO ticket_change ' 
    165173                            '(ticket,time,author,field,oldvalue,newvalue) ' 
    166174                            "VALUES (%s, %s, %s, 'comment', '', %s)", 
    167175                            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) 
    168185 
    169186        cursor.execute ('UPDATE ticket SET changetime=%s WHERE id=%s', when, id) 
    170187        db.commit() 
     
    372389        ticket.save_changes(self.db, 
    373390                            self.args.get('author', self.req.authname), 
    374391                            self.args.get('comment'), 
     392                            self.args.get('spent_work'), 
    375393                            when=now) 
    376394 
    377395        tn = TicketNotifyEmail(self.env) 
     
    454472            for field in Ticket.std_fields: 
    455473                if self.args.has_key(field) and field != 'reporter': 
    456474                    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 
    457483            self.req.hdf.setValue('ticket.action', action) 
    458484            reporter_id = self.args.get('author') 
    459485            comment = self.args.get('comment') 
  • templates/roadmap.cs

     
    2525    </p> 
    2626    <?cs with:stats = milestone.stats ?> 
    2727     <?cs if:#stats.total_tickets > #0 ?> 
     28      <h3> Ticket resolution progress </h3> 
    2829      <div class="progress"> 
    2930       <div style="width: <?cs var:#stats.percent_complete ?>%"></div> 
    3031      </div> 
     
    3839         var:stats.closed_tickets ?></a></dd> 
    3940      </dl> 
    4041     <?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 ?> 
    4155    <?cs /with ?> 
    4256   </div> 
    4357   <div class="descr"><?cs var:milestone.descr ?></div> 
  • templates/ticket.cs

     
    5151  call:ticketprop("Status", "status", ticket.status, 0) ?><?cs 
    5252  call:ticketprop("Version", "version", ticket.version, 0) ?><?cs 
    5353  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 
    5457  call:ticketprop("Milestone", "milestone", ticket.milestone, 0) ?><?cs 
    5558  set:last_prop = #1 ?><?cs 
    5659  call:ticketprop("Keywords", "keywords", ticket.keywords, 0) ?><?cs 
     
    118121   <li><strong>attachment</strong> added: <?cs var:change.new ?></li><?cs 
    119122  elif $change.field == "description" ?> 
    120123   <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 
    121126  elif $change.old == "" ?> 
    122127   <li><strong><?cs var:change.field ?></strong> set to <em><?cs var:change.new ?></em></li><?cs 
    123128  else ?> 
     
    191196   <label for="keywords">Keywords:</label> 
    192197   <input type="text" id="keywords" name="keywords" size="20" 
    193198       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) 
    194207  </div> 
    195208  <div class="col2"> 
    196209   <label for="priority">Priority:</label><?cs 
  • templates/newticket.cs

     
    5252   <label for="keywords">Keywords:</label> 
    5353   <input type="text" id="keywords" name="keywords" size="20" 
    5454       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) 
    5559  </div> 
    5660  <div class="col2"> 
    5761   <label for="priority">Priority:</label><?cs