]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 24789 [E|A|R] Add Estimated, Actual, Remaining Time Fields
authorbugreport%peshkin.net <>
Sun, 13 Oct 2002 11:26:02 +0000 (11:26 +0000)
committerbugreport%peshkin.net <>
Sun, 13 Oct 2002 11:26:02 +0000 (11:26 +0000)
patch by jeff.hedlund@matrixsi.com
2xr=joel,justdave

22 files changed:
Bugzilla/Search.pm
CGI.pl
bug_form.pl
buglist.cgi
checksetup.pl
colchange.cgi
defparams.pl
globals.pl
long_list.cgi
post_bug.cgi
process_bug.cgi
processmail
query.cgi
template/en/default/bug/activity/table.html.tmpl
template/en/default/bug/comments.html.tmpl
template/en/default/bug/create/create.html.tmpl
template/en/default/bug/edit.html.tmpl
template/en/default/bug/show-multiple.html.tmpl
template/en/default/bug/time.html.tmpl [new file with mode: 0644]
template/en/default/global/user-error.html.tmpl
template/en/default/list/edit-multiple.html.tmpl
template/en/default/list/table.html.tmpl

index db97af3f2f4effe61dd7e80e19c22b9936392a88..36311d6c414947468a7d6e8ad10978e6c1809fc9 100644 (file)
@@ -149,6 +149,11 @@ sub init {
         push(@specialchart, ["keywords", $t, $F{'keywords'}]);
     }
 
+    if (lsearch($fieldsref, "(SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id)) AS actual_time") != -1) {
+        push(@supptables, "longdescs AS ldtime");
+        push(@wherepart, "ldtime.bug_id = bugs.bug_id");
+    }
+
     foreach my $id ("1", "2") {
         if (!defined ($F{"email$id"})) {
             next;
@@ -323,6 +328,62 @@ sub init {
              push(@wherepart, "$table.bug_id = bugs.bug_id");
              $f = "$table.thetext";
          },
+         "^work_time,changedby" => sub {
+             my $table = "longdescs_$chartid";
+             push(@supptables, "longdescs $table");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             my $id = &::DBNameToIdAndCheck($v);
+             $term = "(($table.who = $id";
+             $term .= ") AND ($table.work_time <> 0))";
+         },
+         "^work_time,changedbefore" => sub {
+             my $table = "longdescs_$chartid";
+             push(@supptables, "longdescs $table");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             $term = "(($table.bug_when < " . &::SqlQuote(SqlifyDate($v));
+             $term .= ") AND ($table.work_time <> 0))";
+         },
+         "^work_time,changedafter" => sub {
+             my $table = "longdescs_$chartid";
+             push(@supptables, "longdescs $table");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             $term = "(($table.bug_when > " . &::SqlQuote(SqlifyDate($v));
+             $term .= ") AND ($table.work_time <> 0))";
+         },
+         "^work_time," => sub {
+             my $table = "longdescs_$chartid";
+             push(@supptables, "longdescs $table");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             $f = "$table.work_time";
+         },
+         "^percentage_complete," => sub {
+             my $oper;
+             if ($t eq "equals") {
+                 $oper = "=";
+             } elsif ($t eq "greaterthan") {
+                 $oper = ">";
+             } elsif ($t eq "lessthan") {
+                 $oper = "<";
+             } elsif ($t eq "notequal") {
+                 $oper = "<>";
+             } elsif ($t eq "regexp") {
+                 $oper = "REGEXP";
+             } elsif ($t eq "notregexp") {
+                 $oper = "NOT REGEXP";
+             } else {
+                 $oper = "noop";
+             }
+             if ($oper ne "noop") {
+                 my $table = "longdescs_$chartid";
+                 push(@supptables, "longdescs $table");
+                 push(@wherepart, "$table.bug_id = bugs.bug_id");
+                 my $field = "(100*((SUM($table.work_time)*COUNT(DISTINCT $table.bug_when)/COUNT(bugs.bug_id))/((SUM($table.work_time)*COUNT(DISTINCT $table.bug_when)/COUNT(bugs.bug_id))+bugs.remaining_time))) AS percentage_complete_$table";
+                 push(@fields, $field);
+                 push(@having, 
+                      "percentage_complete_$table $oper " . &::SqlQuote($v));
+             }
+             $term = "0=0";
+         },
          "^bug_group,(?!changed)" => sub {
             push(@supptables, "LEFT JOIN bug_group_map bug_group_map_$chartid ON bugs.bug_id = bug_group_map_$chartid.bug_id");
 
diff --git a/CGI.pl b/CGI.pl
index 20f257d6038f558a6f0a8a3e5ca22233d74920b6..cb2d3d76df04af6e798f4feda4cddb0d6a2e9cf0 100644 (file)
--- a/CGI.pl
+++ b/CGI.pl
@@ -956,6 +956,7 @@ sub GetBugActivity {
     
     my $query = "
         SELECT IFNULL(fielddefs.description, bugs_activity.fieldid),
+                fielddefs.name,
                 bugs_activity.attach_id,
                 bugs_activity.bug_when,
                 bugs_activity.removed, bugs_activity.added,
@@ -974,41 +975,59 @@ sub GetBugActivity {
     my $changes = [];
     my $incomplete_data = 0;
     
-    while (my ($field, $attachid, $when, $removed, $added, $who) 
+    while (my ($field, $fieldname, $attachid, $when, $removed, $added, $who) 
                                                                = FetchSQLData())
     {
         my %change;
+        my $activity_visible = 1;
         
-        # This gets replaced with a hyperlink in the template.
-        $field =~ s/^Attachment// if $attachid;
+        # check if the user should see this field's activity
+        if ($fieldname eq 'remaining_time' ||
+            $fieldname eq 'estimated_time' ||
+            $fieldname eq 'work_time') {
 
-        # Check for the results of an old Bugzilla data corruption bug
-        $incomplete_data = 1 if ($added =~ /^\?/ || $removed =~ /^\?/);
+            if (!UserInGroup(Param('timetrackinggroup'))) {
+                $activity_visible = 0;
+            } else {
+                $activity_visible = 1;
+            }
+        } else {
+            $activity_visible = 1;
+        }
+                
+        if ($activity_visible) {
+            # This gets replaced with a hyperlink in the template.
+            $field =~ s/^Attachment// if $attachid;
+
+            # Check for the results of an old Bugzilla data corruption bug
+            $incomplete_data = 1 if ($added =~ /^\?/ || $removed =~ /^\?/);
         
-        # An operation, done by 'who' at time 'when', has a number of
-        # 'changes' associated with it.
-        # If this is the start of a new operation, store the data from the
-        # previous one, and set up the new one.
-        if ($operation->{'who'} 
-            && ($who ne $operation->{'who'} 
-                || $when ne $operation->{'when'})) 
-        {
-            $operation->{'changes'} = $changes;
-            push (@operations, $operation);
+            # An operation, done by 'who' at time 'when', has a number of
+            # 'changes' associated with it.
+            # If this is the start of a new operation, store the data from the
+            # previous one, and set up the new one.
+            if ($operation->{'who'} 
+                && ($who ne $operation->{'who'} 
+                    || $when ne $operation->{'when'})) 
+            {
+                $operation->{'changes'} = $changes;
+                push (@operations, $operation);
             
-            # Create new empty anonymous data structures.
-            $operation = {};
-            $changes = [];
-        }  
+                # Create new empty anonymous data structures.
+                $operation = {};
+                $changes = [];
+            }  
         
-        $operation->{'who'} = $who;
-        $operation->{'when'} = $when;            
+            $operation->{'who'} = $who;
+            $operation->{'when'} = $when;            
         
-        $change{'field'} = $field;
-        $change{'attachid'} = $attachid;
-        $change{'removed'} = $removed;
-        $change{'added'} = $added;
-        push (@$changes, \%change);
+            $change{'field'} = $field;
+            $change{'fieldname'} = $fieldname;
+            $change{'attachid'} = $attachid;
+            $change{'removed'} = $removed;
+            $change{'added'} = $added;
+            push (@$changes, \%change);
+        }   
     }
     
     if ($operation->{'who'}) {
index c664f2de1f4abe7ddca54eacd5c764149c6c4660..f0ad8e377e90d5e782141d18356543da9007e6dd 100644 (file)
@@ -86,7 +86,8 @@ sub show_bug {
         reporter, bug_file_loc, short_desc, target_milestone, 
         qa_contact, status_whiteboard, 
         date_format(creation_ts,'%Y-%m-%d %H:%i'),
-        delta_ts, sum(votes.count), delta_ts calc_disp_date
+        delta_ts, sum(votes.count), delta_ts calc_disp_date,
+        estimated_time, remaining_time
     FROM bugs LEFT JOIN votes USING(bug_id), products, components
     WHERE bugs.bug_id = $id
         AND bugs.product_id = products.id
@@ -110,7 +111,8 @@ sub show_bug {
                        "priority", "bug_severity", "component_id", "component", 
                        "assigned_to", "reporter", "bug_file_loc", "short_desc", 
                        "target_milestone", "qa_contact", "status_whiteboard", 
-                       "creation_ts", "delta_ts", "votes", "calc_disp_date") 
+                       "creation_ts", "delta_ts", "votes", "calc_disp_date", 
+                       "estimated_time", "remaining_time")
     {
         $value = shift(@row);
         if ($field eq "calc_disp_date") {
@@ -233,6 +235,14 @@ sub show_bug {
         push(@list, $i);
     }
 
+    if (UserInGroup(Param("timetrackinggroup"))) {
+
+        SendSQL("SELECT SUM(work_time) 
+                 FROM longdescs WHERE longdescs.bug_id=$id");
+        $bug{'actual_time'} = FetchSQLData();
+
+    }
+
     $bug{'dependson'} = \@list;
 
     my @list2;
index 9f00e15aeab1098a8895dea821e9db28780814bf..e71db0618f79a24ae09d52a2d4487e61f8071038 100755 (executable)
@@ -382,8 +382,10 @@ DefineColumn("os"                , "bugs.op_sys"                , "OS"
 DefineColumn("target_milestone"  , "bugs.target_milestone"      , "Target Milestone" );
 DefineColumn("votes"             , "bugs.votes"                 , "Votes"            );
 DefineColumn("keywords"          , "bugs.keywords"              , "Keywords"         );
-
-
+DefineColumn("estimated_time"   , "bugs.estimated_time"       , "Estimated Hours"  );
+DefineColumn("remaining_time"   , "bugs.remaining_time"       , "Remaining Hours"  );
+DefineColumn("actual_time"      , "(SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id)) AS actual_time", "Actual Hours");
+DefineColumn("percentage_complete","(100*((SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id))/((SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id))+bugs.remaining_time))) AS percentage_complete", "% Complete"); 
 ################################################################################
 # Display Column Determination
 ################################################################################
@@ -430,6 +432,14 @@ if (trim($::FORM{'votes'}) && !grep($_ eq 'votes', @displaycolumns)) {
     push(@displaycolumns, 'votes');
 }
 
+# Remove the timetracking columns if they are not a part of the group
+# (happens if a user had access to time tracking and it was revoked/disabled)
+if (!UserInGroup(Param("timetrackinggroup"))) {
+   @displaycolumns = grep($_ ne 'estimated_time', @displaycolumns);
+   @displaycolumns = grep($_ ne 'remaining_time', @displaycolumns);
+   @displaycolumns = grep($_ ne 'actual_time', @displaycolumns);
+   @displaycolumns = grep($_ ne 'percentage_complete', @displaycolumns);
+}
 
 ################################################################################
 # Select Column Determination
@@ -440,6 +450,12 @@ if (trim($::FORM{'votes'}) && !grep($_ eq 'votes', @displaycolumns)) {
 # The bug ID is always selected because bug IDs are always displayed 
 my @selectcolumns = ("id");
 
+# remaining and actual_time are required for precentage_complete calculation:
+if (lsearch(\@displaycolumns, "percentage_complete")) {
+    push (@selectcolumns, "remaining_time");
+    push (@selectcolumns, "actual_time");
+}
+
 # Display columns are selected because otherwise we could not display them.
 push (@selectcolumns, @displaycolumns);
 
@@ -459,6 +475,10 @@ if ($dotweak) {
 # Convert the list of columns being selected into a list of column names.
 my @selectnames = map($columns->{$_}->{'name'}, @selectcolumns);
 
+# Remove columns with no names, such as percentage_complete
+#  (or a removed *_time column due to permissions)
+@selectnames = grep($_ ne '', @selectnames);
+
 # Generate the basic SQL query that will be used to generate the bug list.
 my $search = new Bugzilla::Search('fields' => \@selectnames, 
                                   'url' => $::buffer);
@@ -538,6 +558,15 @@ if ($order) {
     # sort order was given
     $db_order =~ s/bugs.votes\s*(,|$)/bugs.votes desc$1/i;
 
+    # the 'actual_time' field is defined as an aggregate function, but 
+    # for order we just need the column name 'actual_time'
+    my $aggregate_search = quotemeta($columns->{'actual_time'}->{'name'});
+    $db_order =~ s/$aggregate_search/actual_time/g;
+
+    # the 'percentage_complete' field is defined as an aggregate too
+    $aggregate_search = quotemeta($columns->{'percentage_complete'}->{'name'});
+    $db_order =~ s/$aggregate_search/percentage_complete/g;
+
     $query .= " ORDER BY $db_order ";
 }
 
index 83cc06a5f5ab80c039b1bc48f5eee51ba51f509c..c44c032357b93e8b6ec70938d57f6d4c17e7f254 100755 (executable)
@@ -1438,6 +1438,8 @@ $table{bugs} =
     everconfirmed tinyint not null,
     reporter_accessible tinyint not null default 1,
     cclist_accessible tinyint not null default 1,
+    estimated_time decimal(5,2) not null default 0,
+    remaining_time decimal(5,2) not null default 0,
     alias varchar(20),
     
     index (assigned_to),
@@ -1478,6 +1480,7 @@ $table{longdescs} =
    'bug_id mediumint not null,
     who mediumint not null,
     bug_when datetime not null,
+    work_time decimal(5,2) not null default 0,
     thetext mediumtext,
     isprivate tinyint not null default 0,
     index(bug_id),
@@ -1853,6 +1856,8 @@ AddFDef("everconfirmed", "Ever Confirmed", 0);
 AddFDef("reporter_accessible", "Reporter Accessible", 0);
 AddFDef("cclist_accessible", "CC Accessible", 0);
 AddFDef("bug_group", "Group", 0);
+AddFDef("estimated_time", "Estimated Hours", 1);
+AddFDef("remaining_time", "Remaining Hours", 0);
 
 # Oops. Bug 163299
 $dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'");
@@ -1860,6 +1865,8 @@ $dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'");
 AddFDef("flagtypes.name", "Flag", 0);
 AddFDef("requesters.login_name", "Flag Requester", 0);
 AddFDef("setters.login_name", "Flag Setter", 0);
+AddFDef("work_time", "Hours Worked", 0);
+AddFDef("percentage_complete", "Percentage Complete", 0);
 
 ###########################################################################
 # Detect changed local settings
@@ -2903,6 +2910,11 @@ if (GetFieldDef("bugs","qacontact_accessible")) {
     DropField("bugs", "assignee_accessible");
 }
 
+# 2002-02-20 jeff.hedlund@matrixsi.com - bug 24789 time tracking
+AddField("longdescs", "work_time", "decimal(5,2) not null default 0");
+AddField("bugs", "estimated_time", "decimal(5,2) not null default 0");
+AddField("bugs", "remaining_time", "decimal(5,2) not null default 0");
+
 # 2002-03-15 bbaetz@student.usyd.edu.au - bug 129466
 # 2002-05-13 preed@sigkill.com - bug 129446 patch backported to the 
 #  BUGZILLA-2_14_1-BRANCH as a security blocker for the 2.14.2 release
index 2ef7c7d102a2debfb502ac97d725111ad41e5b0e..3d1ea04761aa6ae5ad486302e5f092830be7ff09 100755 (executable)
@@ -58,6 +58,11 @@ if (@::legal_keywords) {
     push(@masterlist, "keywords");
 }
 
+if (UserInGroup(Param("timetrackinggroup"))) {
+    push(@masterlist, ("estimated_time", "remaining_time", "actual_time",
+                       "percentage_complete")); 
+}
+
 push(@masterlist, ("summary", "summaryfull"));
 
 $vars->{'masterlist'} = \@masterlist;
index 232b6c346569b2f801657d13ec4d6a8444afffff..bb5d43df72102ec1a270344714e0610d7509ac39 100644 (file)
@@ -861,6 +861,14 @@ Reason: %reason%
    default => ''
   },
 
+  {
+   name => 'timetrackinggroup',
+   desc => 'The name of the group of users who can see/change time tracking ' .
+           'information.',
+   type => 't',
+   default => ''
+  },
+  
   {
    name => 'loginnetmask',
    desc => 'The number of bits for the netmask used if a user chooses to ' .
index 167d0918a93790045f435f4d1c153e0bdf6f9822..ad96eaf51ab279e1b674103607688799727215c7 100644 (file)
@@ -296,7 +296,8 @@ sub FetchOneColumn {
                           "status", "resolution", "summary");
 
 sub AppendComment {
-    my ($bugid, $who, $comment, $isprivate, $timestamp) = @_;
+    my ($bugid, $who, $comment, $isprivate, $timestamp, $work_time) = @_;
+    $work_time ||= 0;
     
     # Use the date/time we were given if possible (allowing calling code
     # to synchronize the comment's timestamp with those of other records).
@@ -304,15 +305,26 @@ sub AppendComment {
     
     $comment =~ s/\r\n/\n/g;     # Get rid of windows-style line endings.
     $comment =~ s/\r/\n/g;       # Get rid of mac-style line endings.
-    if ($comment =~ /^\s*$/) {  # Nothin' but whitespace.
+
+    # allowing negatives though so people can back out errors in time reporting
+    if (defined $work_time) {
+       # regexp verifies one or more digits, optionally followed by a period and
+       # zero or more digits, OR we have a period followed by one or more digits
+       if ($work_time !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/) { 
+          ThrowUserError("need_numeric_value");
+          return;
+       }
+    } else { $work_time = 0 };
+
+    if ($comment =~ /^\s*$/) {  # Nothin' but whitespace
         return;
     }
 
     my $whoid = DBNameToIdAndCheck($who);
     my $privacyval = $isprivate ? 1 : 0 ;
-    SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext, isprivate) " .
+    SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext, isprivate, work_time) " .
         "VALUES($bugid, $whoid, $timestamp, " . SqlQuote($comment) . ", " . 
-        $privacyval . ")");
+        $privacyval . ", " . SqlQuote($work_time) . ")");
 
     SendSQL("UPDATE bugs SET delta_ts = now() WHERE bug_id = $bugid");
 }
@@ -1104,7 +1116,7 @@ sub GetLongDescriptionAsText {
     $query .= "ORDER BY longdescs.bug_when";
     SendSQL($query);
     while (MoreSQLData()) {
-        my ($who, $when, $text, $isprivate) = (FetchSQLData());
+        my ($who, $when, $text, $isprivate, $work_time) = (FetchSQLData());
         if ($count) {
             $result .= "\n\n------- Additional Comments From $who".Param('emailsuffix')."  ".
                 time2str("%Y-%m-%d %H:%M", str2time($when)) . " -------\n";
@@ -1124,7 +1136,7 @@ sub GetComments {
     my @comments;
     SendSQL("SELECT  profiles.realname, profiles.login_name, 
                      date_format(longdescs.bug_when,'%Y-%m-%d %H:%i'), 
-                     longdescs.thetext,
+                     longdescs.thetext, longdescs.work_time,
                      isprivate,
                      date_format(longdescs.bug_when,'%Y%m%d%H%i%s') 
             FROM     longdescs, profiles
@@ -1134,7 +1146,8 @@ sub GetComments {
              
     while (MoreSQLData()) {
         my %comment;
-        ($comment{'name'}, $comment{'email'}, $comment{'time'}, $comment{'body'},
+        ($comment{'name'}, $comment{'email'}, $comment{'time'}, 
+        $comment{'body'}, $comment{'work_time'},
         $comment{'isprivate'}, $comment{'when'}) = FetchSQLData();
         
         $comment{'email'} .= Param('emailsuffix');
@@ -1490,6 +1503,20 @@ sub PerformSubsts {
     return $str;
 }
 
+sub FormatTimeUnit {
+    # Returns a number with 2 digit precision, unless the last digit is a 0
+    # then it returns only 1 digit precision
+    my ($time) = (@_);
+    my $newtime = sprintf("%.2f", $time);
+
+    if ($newtime =~ /0\Z/) {
+        $newtime = sprintf("%.1f", $time);
+    }
+
+    return $newtime;
+    
+}
 ###############################################################################
 # Global Templatization Code
 
index 6df8a8bad998144308880aff9a2bc2dbb22c0a80..5047d271ea3c74148325aab9b2922a3e186f825d 100755 (executable)
@@ -56,7 +56,9 @@ my $generic_query = "
     bugs.target_milestone,
     bugs.qa_contact, 
     bugs.status_whiteboard, 
-    bugs.keywords
+    bugs.keywords,
+    bugs.estimated_time,
+    bugs.remaining_time
   FROM bugs,profiles assign,profiles report, products, components
   WHERE assign.userid = bugs.assigned_to AND report.userid = bugs.reporter
     AND bugs.product_id=products.id AND bugs.component_id=components.id";
@@ -79,7 +81,8 @@ foreach my $bug_id (split(/[:,]/, $buglist)) {
                        "op_sys", "bug_status", "resolution", "priority",
                        "bug_severity", "component", "assigned_to", "reporter",
                        "bug_file_loc", "short_desc", "target_milestone",
-                       "qa_contact", "status_whiteboard", "keywords") 
+                       "qa_contact", "status_whiteboard", "keywords", 
+                       "estimated_time", "remaining_time") 
     {
         $bug{$field} = shift @row;
     }
@@ -91,6 +94,12 @@ foreach my $bug_id (split(/[:,]/, $buglist)) {
 
         push (@bugs, \%bug);
     }
+
+    if (UserInGroup(Param("timetrackinggroup"))) {
+        SendSQL("SELECT SUM(work_time) FROM longdescs WHERE bug_id=$bug_id");
+
+        $bug{'actual_time'} = FetchSQLData();
+    }
 }
 
 # Add the list of bug hashes to the variables
index c07d07d99ef768e008b946236f7dc9de56c00905..d519a484c47176b815e7d5e4776cf8bfdea6038f 100755 (executable)
@@ -233,7 +233,8 @@ if ($::FORM{'keywords'} && UserInGroup("editbugs")) {
 
 # Build up SQL string to add bug.
 my $sql = "INSERT INTO bugs " . 
-  "(" . join(",", @used_fields) . ", reporter, creation_ts) " . 
+  "(" . join(",", @used_fields) . ", reporter, creation_ts, " .
+  "estimated_time, remaining_time) " .
   "VALUES (";
 
 foreach my $field (@used_fields) {
@@ -246,7 +247,23 @@ $comment = trim($comment);
 # OK except for the fact that it causes e-mail to be suppressed.
 $comment = $comment ? $comment : " ";
 
-$sql .= "$::userid, now() )";
+$sql .= "$::userid, now(), ";
+
+# Time Tracking
+if (UserInGroup(Param("timetrackinggroup")) &&
+    defined $::FORM{'estimated_time'}) {
+
+    my $est_time = $::FORM{'estimated_time'};
+    if ($est_time =~ /^(?:\d+(?:\.\d*)?|\.\d+)$/) {
+        $sql .= SqlQuote($est_time) . "," . SqlQuote($est_time);
+    } else {
+        $vars->{'field'} = "estimated_time";
+        ThrowUserError("need_positive_number");
+    }
+} else {
+    $sql .= "0, 0";
+}
+$sql .= ")";
 
 # Groups
 my @groupstoadd = ();
index f529f13ea9358cf045778d31f582b46bb34e8c59..439587178b89e60bb6cc350970d294604b026960 100755 (executable)
@@ -703,6 +703,25 @@ if (defined $::FORM{'qa_contact'}) {
     }
 }
 
+# jeff.hedlund@matrixsi.com time tracking data processing:
+foreach my $field ("estimated_time", "remaining_time") {
+
+    if (defined $::FORM{$field}) {
+        my $er_time = trim($::FORM{$field});
+        if ($er_time ne $::FORM{'dontchange'}) {
+            if ($er_time > 99999.99) {
+                ThrowUserError("value_out_of_range", {variable => $field});
+            }
+            if ($er_time =~ /^(?:\d+(?:\.\d*)?|\.\d+)$/) {
+                DoComma();
+                $::query .= "$field = " . SqlQuote($er_time);
+            } else {
+                $vars->{'field'} = $field;
+                ThrowUserError("need_positive_number");
+            }
+        }
+    }
+}
 
 # If the user is submitting changes from show_bug.cgi for a single bug,
 # and that bug is restricted to a group, process the checkboxes that
@@ -808,6 +827,12 @@ SWITCH: for ($::FORM{'knob'}) {
         last SWITCH;
     };
     /^resolve$/ && CheckonComment( "resolve" ) && do {
+        if (UserInGroup(Param('timetrackinggroup'))) {
+            if (defined $::FORM{'remaining_time'} &&
+                $::FORM{'remaining_time'} > 0) {
+                ThrowUserError("resolving_remaining_time");
+            }
+        }
         # Check here, because its the only place we require the resolution
         CheckFormField(\%::FORM, 'resolution', \@::settable_resolution);
         ChangeStatus('RESOLVED');
@@ -1170,6 +1195,26 @@ foreach my $id (@idlist) {
         }
     }
 
+    SendSQL("select now()");
+    $timestamp = FetchOneColumn();
+
+    if ($::FORM{'work_time'} > 99999.99) {
+        ThrowUserError("value_out_of_range", {variable => 'work_time'});
+    }
+    if (defined $::FORM{'comment'} || defined $::FORM{'work_time'}) {
+        if ($::FORM{'work_time'} != 0 && 
+            (!defined $::FORM{'comment'} || $::FORM{'comment'} =~ /^\s*$/)) {
+        
+            ThrowUserError('comment_required');
+        } else {
+            AppendComment($id, $::COOKIE{'Bugzilla_login'}, $::FORM{'comment'},
+                $::FORM{'commentprivacy'}, $timestamp, $::FORM{'work_time'});
+            if ($::FORM{'work_time'} != 0) {
+                LogActivityEntry($id, "work_time", "", $::FORM{'work_time'});
+            }
+        }
+    }
+
     if (@::legal_keywords) {
         # There are three kinds of "keywordsaction": makeexact, add, delete.
         # For makeexact, we delete everything, and then add our things.
@@ -1229,17 +1274,11 @@ foreach my $id (@idlist) {
         SendSQL("DELETE FROM bug_group_map 
                  WHERE bug_id = $id AND group_id = $grouptodel");
     }
-    SendSQL("select now()");
-    $timestamp = FetchOneColumn();
 
     my $groupDelNames = join(',', @groupDelNames);
     my $groupAddNames = join(',', @groupAddNames);
 
     LogActivityEntry($id, "bug_group", $groupDelNames, $groupAddNames); 
-    if (defined $::FORM{'comment'}) {
-        AppendComment($id, $::COOKIE{'Bugzilla_login'}, $::FORM{'comment'},
-            $::FORM{'commentprivacy'}, $timestamp);
-    }
     
     my $removedCcString = "";
     if (defined $::FORM{newcc} || defined $::FORM{removecc} || defined $::FORM{masscc}) {
index a472975976167cb8f9dff4acafdb2b6336d93e9a..3b421e922ceba1ed74f9b1b5472937ddc025907c 100755 (executable)
@@ -129,12 +129,13 @@ sub ProcessOneBug {
     if ($values{'qa_contact'}) {
         $values{'qa_contact'} = DBID_to_name($values{'qa_contact'});
     }
+    $values{'estimated_time'} = FormatTimeUnit($values{'estimated_time'});
 
     my @diffs;
 
 
     SendSQL("SELECT profiles.login_name, fielddefs.description, " .
-            "       bug_when, removed, added, attach_id " .
+            "       bug_when, removed, added, attach_id, fielddefs.name " .
             "FROM bugs_activity, fielddefs, profiles " .
             "WHERE bug_id = $id " .
             "  AND fielddefs.fieldid = bugs_activity.fieldid " .
@@ -150,21 +151,32 @@ sub ProcessOneBug {
     }
 
     my $difftext = "";
+    my $diffheader = "";
+    my $diffpart = {};
+    my @diffparts;
     my $lastwho = "";
     foreach my $ref (@diffs) {
-        my ($who, $what, $when, $old, $new, $attachid) = (@$ref);
+        my ($who, $what, $when, $old, $new, $attachid, $fieldname) = (@$ref);
+        $diffpart = {};
         if ($who ne $lastwho) {
             $lastwho = $who;
-            $difftext .= "\n$who" . Param('emailsuffix') . " changed:\n\n";
-            $difftext .= FormatTriple("What    ", "Removed", "Added");
-            $difftext .= ('-' x 76) . "\n";
+            $diffheader = "\n$who" . Param('emailsuffix') . " changed:\n\n";
+            $diffheader .= FormatTriple("What    ", "Removed", "Added");
+            $diffheader .= ('-' x 76) . "\n";
         }
         $what =~ s/^Attachment/Attachment #$attachid/ if $attachid;
-        $difftext .= FormatTriple($what, $old, $new);
+        if( $fieldname eq 'estimated_time' ||
+            $fieldname eq 'remaining_time' ) {
+            $old = FormatTimeUnit($old);
+            $new = FormatTimeUnit($new);
+        }
+        $difftext = FormatTriple($what, $old, $new);
+        $diffpart->{'header'} = $diffheader;
+        $diffpart->{'fieldname'} = $fieldname;
+        $diffpart->{'text'} = $difftext;
+        push(@diffparts, $diffpart);
     }
 
-    $difftext = trim($difftext);
-
 
     my $deptext = "";
 
@@ -220,7 +232,9 @@ sub ProcessOneBug {
     $deptext = trim($deptext);
 
     if ($deptext) {
-        $difftext = trim($difftext . "\n\n" . $deptext);
+        #$difftext = trim($difftext . "\n\n" . $deptext);
+        $diffpart->{'text'} = trim("\n\n" . $deptext);
+        push(@diffparts, $diffpart);
     }
 
 
@@ -301,9 +315,9 @@ sub ProcessOneBug {
         if ( !defined(NewProcessOnePerson($person, $count, \@headerlist,
                                           \@reasons, \%values,
                                           \%defmailhead, 
-                                          \%fielddescription, $difftext, 
-                                          $newcomments, $anyprivate, 
-                                          $start, $id, 
+                                          \%fielddescription, \@diffparts,
+                                          $newcomments, 
+                                          $anyprivate, $start, $id, 
                                           \@depbugs))) 
         {
 
@@ -613,14 +627,16 @@ sub filterEmailGroup ($$$) {
 }
 
 sub NewProcessOnePerson ($$$$$$$$$$$$$) {
-    my ($person, $count, $hlRef, $reasonsRef, $valueRef, $dmhRef, $fdRef, $difftext, 
-        $newcomments, $anyprivate, $start, $id, $depbugsRef) = @_;
+    my ($person, $count, $hlRef, $reasonsRef, $valueRef, $dmhRef, $fdRef,  
+        $diffRef, $newcomments, $anyprivate, $start, 
+        $id, $depbugsRef) = @_;
 
     my %values = %$valueRef;
     my @headerlist = @$hlRef;
     my @reasons = @$reasonsRef;
     my %defmailhead = %$dmhRef;
     my %fielddescription = %$fdRef;
+    my @diffparts = @$diffRef;
     my @depbugs = @$depbugsRef;
     
     if ($seen{$person}) {
@@ -680,10 +696,41 @@ sub NewProcessOnePerson ($$$$$$$$$$$$$) {
         if (! $value) {
           next;
         }
-        my $desc = $fielddescription{$f};
-        $head .= FormatDouble($desc, $value);
+        # Don't send estimated_time if user not in the group, or not enabled
+        if ($f ne 'estimated_time' ||
+            UserInGroup(Param('timetrackinggroup'), $userid)) { 
+             
+            my $desc = $fielddescription{$f};
+            $head .= FormatDouble($desc, $value);
+        }
       }
     }
+
+    # Build difftext (the actions) by verifying the user should see them
+    my $difftext = "";
+    my $diffheader = "";
+    my $add_diff;
+    foreach my $diff (@diffparts) {
+        
+        $add_diff = 0;
+        
+        if ($diff->{'fieldname'} eq 'estimated_time' ||
+            $diff->{'fieldname'} eq 'remaining_time' ||
+            $diff->{'fieldname'} eq 'work_time') {
+            if (UserInGroup(Param("timetrackinggroup"), $userid)) {
+                $add_diff = 1;
+            }
+        } else {
+            $add_diff = 1;
+        }
+        if ($add_diff) {
+            if ($diffheader ne $diff->{'header'}) {
+                $diffheader = $diff->{'header'};
+                $difftext .= $diffheader;
+            }
+            $difftext .= $diff->{'text'};
+        }
+    }
     
     if ($difftext eq "" && $newcomments eq "") {
       # Whoops, no differences!
index 1aa17b723eff905cdd28d470b5f23888a2fdbcbd..973d1fdbc03e8ebf7679da2a2c16cd3e033a4c9d 100755 (executable)
--- a/query.cgi
+++ b/query.cgi
@@ -281,7 +281,16 @@ shift @::legal_resolution;
       # Another hack - this array contains "" for some reason. See bug 106589.
 $vars->{'resolution'} = \@::legal_resolution;
 
-$vars->{'chfield'} = ["[Bug creation]", @::log_columns];
+my @chfields = @::log_columns;
+push @chfields, "[Bug creation]";
+if (UserInGroup(Param('timetrackinggroup'))) {
+    push @chfields, "work_time";
+} else {
+    @chfields = grep($_ ne "estimated_time", @chfields);
+    @chfields = grep($_ ne "remaining_time", @chfields);
+}
+@chfields = (sort(@chfields));
+$vars->{'chfield'} = \@chfields;
 $vars->{'bug_status'} = \@::legal_bug_status;
 $vars->{'rep_platform'} = \@::legal_platform;
 $vars->{'op_sys'} = \@::legal_opsys;
@@ -295,6 +304,13 @@ push(@fields, { name => "noop", description => "---" });
 SendSQL("SELECT name, description FROM fielddefs ORDER BY sortkey");
 while (MoreSQLData()) {
     my ($name, $description) = FetchSQLData();
+    if (($name eq "estimated_time" ||
+         $name eq "remaining_time" ||
+         $name eq "work_time" ||
+         $name eq "percentage_complete" ) &&
+        (!UserInGroup(Param('timetrackinggroup')))) {
+        next;
+    }
     push(@fields, { name => $name, description => $description });
 }
 
index 43529bd23ea4331b66db7cbc50387ce9b6b843e2..45c8e43802510c73c6297fda0095e3e0e78717e3 100644 (file)
@@ -32,6 +32,8 @@
   # incomplete_data: boolean. True if some of the data is incomplete (because
   #                  it was affected by an old Bugzilla bug.)
   #%]
+
+[% PROCESS bug/time.html.tmpl %]
  
 [% IF incomplete_data %]
   <p>
             </td>
             <td>
               [% IF change.removed %]
-                [% change.removed FILTER html %]
+                [% IF change.fieldname == 'estimated_time' ||
+                      change.fieldname == 'remaining_time' ||
+                      change.fieldname == 'work_time' %]
+                  [% PROCESS formattimeunit time_unit=change.removed %]
+                [% ELSE %]
+                  [% change.removed FILTER html %]
+                [% END %]
               [% ELSE %]
                 &nbsp;
               [% END %]
             </td>
             <td>
               [% IF change.added %]
-                [% change.added FILTER html %]
+                [% IF change.fieldname == 'estimated_time' ||
+                      change.fieldname == 'remaining_time' ||
+                      change.fieldname == 'work_time' %]
+                  [% PROCESS formattimeunit time_unit=change.added %]
+                [% ELSE %]
+                  [% change.added FILTER html %]
+                [% END %]
               [% ELSE %]
                 &nbsp;
               [% END %]
index 7a8ae73db8b5423e57ef70dec63797e6f27b2f2e..f5880a811dc068526805a2fb0358a4fffcb57513 100644 (file)
@@ -30,6 +30,7 @@
   [% count = count + 1 %]
 [% END %]
 
+[% PROCESS bug/time.html.tmpl %]
 
 [%############################################################################%]
 [%# Block for individual comments                                            #%]
         <i>------- Additional Comment
         <a name="c[% count %]" href="#c[% count %]">#[% count %]</a> From 
         <a href="mailto:[% comment.email FILTER html %]">[% comment.name FILTER html %]</a>
-        [%+ comment.time %] -------
+        [%+ comment.time %] 
+        -------
         </i>
       [% END %]
+        
       [% IF mode == "edit" && isinsider %]
         <i>
           <input type=hidden name="oisprivate-[% count %]" 
           [% " checked=\"checked\"" IF comment.isprivate %]> Private
         </i>
       [% END %]
-    
+      [% IF UserInGroup(Param('timetrackinggroup')) &&
+            (comment.work_time > 0 || comment.work_time < 0) %]
+         <br>
+         Additional hours worked: 
+         [% PROCESS formattimeunit time_unit=comment.work_time %]
+      [% END %]
 [%# Don't indent the <pre> block, since then the spaces are displayed in the
   # generated HTML
   #%]
index 066c11b63adcc4465a91a9a2a531c04f8de79f14..354dd990c678216d153856fe757650cdffdbf3b4 100644 (file)
     <td colspan="3"></td>
   </tr>
 
+[% IF UserInGroup(Param('timetrackinggroup')) %]
+  <tr>
+    <td align="right"><strong>Estimated Hours:</strong></td>
+    <td colspan="3">
+      <input name="estimated_time" size="6" maxlength="6" value="0.0"/>
+    </td>
+  </tr>
+
+  <tr>
+    <td>&nbsp;</td>
+    <td colspan="3"></td>
+  </tr>
+[% END %]
+
   <tr>
     <td align="right"><strong>URL:</strong></td>
     <td colspan="3">
index 15285216952a49fe1f31fa62ddeedab27a65184e..453b4aa65d058133a317af369c9f8fe510ccd971 100644 (file)
 [% END %]
 
 [% PROCESS bug/navigate.html.tmpl %]
+[% PROCESS bug/time.html.tmpl %]
+
+<script type="text/javascript" language="JavaScript">
+<!--
+var fRemainingTime = [% bug.remaining_time %]; // holds the original value
+function adjustRemainingTime() {
+   // subtracts time spent from remaining time
+   var new_time;
+
+   new_time =
+      fRemainingTime - document.changeform.work_time.value;
+   // get upto 2 decimal places
+   document.changeform.remaining_time.value = 
+        Math.round(new_time * 100)/100;
+}
+
+function updateRemainingTime() {
+   // if the remaining time is changed manually, update fRemainingTime
+   fRemainingTime = document.changeform.remaining_time.value;
+}
+
+//-->
+</script>
 
 <hr>
 
     </tr> 
   [% END %]
   </table>
+
+  [% IF UserInGroup(Param('timetrackinggroup')) %]
+    <br>
+    <table cellpadding=0 cellspacing=0 border=1>
+      <tr>
+        <th width="16.6%" align="center" bgcolor="#cccccc">
+          Orig. Est.
+        </th>
+        <th width="16.6%" align="center" bgcolor="#cccccc">
+          Current Est.
+        </th>
+        <th width="16.6%" align="center" bgcolor="#cccccc">
+          Hours Worked
+        </th>
+        <th width="16.6%" align="center" bgcolor="#cccccc">
+          Hours Left
+        </th>
+        <th width="16.6%" align="center" bgcolor="#cccccc">
+          %Complete
+        </th>
+        <th width="16.6%" align="center" bgcolor="#cccccc">
+          Gain
+        </th>
+      </tr>
+      <tr>
+        <td align="center">
+          <input name="estimated_time"
+                 value="[% PROCESS formattimeunit 
+                                   time_unit=bug.estimated_time %]"
+                 size="6" maxlength="6">
+        </td>
+        <td align="center">
+          [% PROCESS formattimeunit 
+                     time_unit=(bug.actual_time + bug.remaining_time) %]
+        </td>
+        <td align="center">
+          [% PROCESS formattimeunit time_unit=bug.actual_time %] + 
+          <input name="work_time" value="0" size="3" maxlength="6"
+                 onChange="adjustRemainingTime();">
+        </td>
+        <td align="center">
+          <input name="remaining_time"
+                 value="[% PROCESS formattimeunit
+                                   time_unit=bug.remaining_time %]"
+                 size="6" maxlength="6" onChange="updateRemainingTime();">
+        </td>
+        <td align="center">
+          [% PROCESS calculatepercentage act=bug.actual_time
+                                         rem=bug.remaining_time %]
+        </td>
+        <td align="center">
+          [% PROCESS formattimeunit time_unit=bug.estimated_time - (bug.actual_time + bug.remaining_time) %]
+        </td>
+      </tr>
+    </table>
+  [% END %]
   
 [%# *** Attachments *** %]
 
index 0c089e9c5f2f247b3918cf80e1a89b5280096afa..d7e2fcf09e40d2a73c0d3530673cc3ead7e716c9 100644 (file)
@@ -24,6 +24,7 @@
   title = "Full Text Bug Listing"
   style_urls = [ "css/show_multiple.css" ]
 %]
+[% PROCESS bug/time.html.tmpl %]
 [% IF bugs.first %]
   [% FOREACH bug = bugs %]
     [% PROCESS bug_display %]
@@ -34,6 +35,7 @@
   </p>
 [% END %]
 
+
 [% PROCESS global/footer.html.tmpl %]
 
 
       </tr>
     [% END %]
 
+    [% IF UserInGroup(Param("timetrackinggroup")) %]
+      <tr>
+        <td colspan="4">
+          <b>Orig. Est.:</b>&nbsp;
+          [% PROCESS formattimeunit time_unit=bug.estimated_time %]
+          &nbsp;
+          <b>Current Est.:</b>&nbsp;
+          [% PROCESS formattimeunit 
+                     time_unit=(bug.remaining_time + bug.actual_time) %]
+          &nbsp;
+          <b>Hours Worked:</b>&nbsp;
+          [% PROCESS formattimeunit time_unit=bug.actual_time %]&nbsp;
+          <b>Hours Left:</b>&nbsp;
+          [% PROCESS formattimeunit time_unit=bug.remaining_time %]
+          &nbsp;
+          <b>Percentage Complete:</b>&nbsp;
+          [% PROCESS calculatepercentage act=bug.actual_time 
+                                         rem=bug.remaining_time %]&nbsp;
+          <b>Gain</b>&nbsp;
+          [% PROCESS formattimeunit 
+                     time_unit=bug.estimated_time - (bug.actual_time + bug.remaining_time) %]
+          &nbsp;
+        </td>
+      </tr>
+    [% END %]
+
     <tr>
       <td colspan="4">
         <b>Description:</b>
diff --git a/template/en/default/bug/time.html.tmpl b/template/en/default/bug/time.html.tmpl
new file mode 100644 (file)
index 0000000..af69669
--- /dev/null
@@ -0,0 +1,48 @@
+<!-- 1.0@bugzilla.org -->
+[%# The contents of this file are subject to the Mozilla Public
+  # License Version 1.1 (the "License"); you may not use this file
+  # except in compliance with the License. You may obtain a copy of
+  # the License at http://www.mozilla.org/MPL/
+  #
+  # Software distributed under the License is distributed on an "AS
+  # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+  # implied. See the License for the specific language governing
+  # rights and limitations under the License.
+  #
+  # The Original Code is the Bugzilla Bug Tracking System.
+  #
+  # The Initial Developer of the Original Code is Netscape Communications
+  # Corporation. Portions created by Netscape are
+  # Copyright (C) 1998 Netscape Communications Corporation. All
+  # Rights Reserved.
+  #
+  # Contributor(s): Jeff Hedlund <jeff.hedlund@matrixsi.com>
+  #
+  #%]
+                                      
+[% BLOCK formattimeunit %]
+  [%# INTERFACE:
+    # time_unit:  the number converting, converts to 2 decimal places
+    #             unless the last character is a 0, then it truncates to 
+    #             1 decimal place
+    #%]
+  [% time_unit = time_unit FILTER format('%.2f') %]
+  [% IF time_unit.match('0\Z') %]
+    [% time_unit FILTER format('%.1f') %]
+  [% ELSE %]
+    [% time_unit FILTER format('%.2f') %]
+  [% END %]
+[% END %]
+
+[% BLOCK calculatepercentage %]
+  [%# INTERFACE:
+    # act:   actual time  
+    # rem:   remaining time
+    # %]
+  [% IF (act + rem) > 0 %]
+    [% (act / (act + rem)) * 100 
+       FILTER format("%d") %]
+  [% ELSE %]
+    0
+  [% END %]
+[% END %]
index f487067dd5ab2e42a6e155b41b6a19c27a551cba..29cb3c90141496cf514537b6d3e0839361acd2d4 100644 (file)
         
   [% ELSIF error == "need_component" %]
     [% title = "Component Required" %]
-    You must specify a component to help determine the new owner of these bugs.
-                  
+    You must specify a component to help determine the new owner of these bugs.                            
+
+  [% ELSIF error == "need_numeric_value" %]
+    [% title = "Numeric Value Required" %]
+    Hours requires a numeric value.
+
+  [% ELSIF error == "need_positive_number" %]
+    [% title = "Positive Number Required" %]
+    [% field %] requires a positive number.
+
   [% ELSIF error == "need_product" %]
     [% title = "Product Required" %]
     You must specify a product to help determine the new owner of these bugs.
   [% ELSIF error == "report_access_denied" %]
     [% title = "Access Denied" %]
     You do not have the permissions necessary to view reports for this product.
-     
+
   [% ELSIF error == "requestee_too_short" %]
     [% title = "Requestee Name Too Short" %]
     One or two characters match too many users, so please enter at least 
   [% ELSIF error == "require_summary" %]
     [% title = "Summary Needed" %]
     You must enter a summary for this bug.
+  
+  [% ELSIF error == "resolving_remaining_time" %]
+    [% title = "Trying to Resolve with Hours Remaining" %]
+    You cannot resolve a bug with hours still remaining.  Set 
+    Remaining Hours to zero if you want to resolve the bug.
 
   [% ELSIF error == "sanity_check_access_denied" %]
     [% title = "Access Denied" %]
     [% title = "Wrong Token" %]
     That token cannot be used to change your email address.
 
+  [% ELSIF error == "value_out_of_range" %]
+    [% title = "Value Out Of Range" %]
+    Value is out of range for field [% variable %].
+
   [% ELSIF error == "z_axis_defined_with_no_x_axis" %]
     [% title = "Nonsensical Options" %]
     You've defined a field for multiple tables without having defined
index 4121d029252ac7f2e90bd6f15cc6921b8eecf95f..4d769c5fafff478bf6d444ab6f87d678fc6685aa 100644 (file)
 
   </tr>
 
+  [% IF UserInGroup(Param("timetrackinggroup")) %]
+    <tr>
+      <th><label for="estimated_time">Estimated Hours:</label></th>
+      <td>
+        <input id="estimated_time"
+               name="estimated_time"
+               value="[% dontchange FILTER html %]"
+               size="6">
+      </td>
+      <th><label for="remaining_time">Remaining Hours:</label></th>
+      <td>
+        <input id="remaining_time"
+               name="remaining_time"
+               value="[% dontchange FILTER html %]"
+               size="6">
+      </td>
+    </tr>
+  [% END %]
+
   [% IF Param("useqacontact") %]
     <tr>
       <th><label for="qa_contact">QA Contact:</label></th>
index 6d5ee0d6cc11397f13761b5cb86e53a3aa9ab521..eb130896135f99ddb10a45af17f9da27af5f6a05 100644 (file)
     "version"           => { maxlength => 5 , title => "Vers" } , 
     "os"                => { maxlength => 4 } , 
     "target_milestone"  => { title => "TargetM" } , 
+    "percentage_complete" => { format_value => "%d %%" } , 
   }
 %]
 
 [% qorder = order FILTER url_quote IF order %]
 
+[% PROCESS bug/time.html.tmpl %]
+
 [%############################################################################%]
 [%# Table Header                                                             #%]
 [%############################################################################%]
     [% FOREACH column = displaycolumns %]
     <td>
       [% '<nobr>' IF NOT abbrev.$column.wrap %]
-      [%- bug.$column.truncate(abbrev.$column.maxlength, abbrev.$column.ellipsis) FILTER html -%]
+      [% IF abbrev.$column.format_value %] 
+        [%- bug.$column FILTER format(abbrev.$column.format_value) FILTER html -%] 
+      [% ELSIF column == 'actual_time' ||
+               column == 'remaining_time' ||
+               column == 'estimated_time' %]
+        [% PROCESS formattimeunit time_unit=bug.$column %] 
+      [% ELSE %]
+        [%- bug.$column.truncate(abbrev.$column.maxlength, abbrev.$column.ellipsis) FILTER html -%]
+      [% END %]
       [%- '</nobr>' IF NOT abbrev.$column.wrap %]
     </td>
     [% END %]