]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 402791: Move status and resolution updating from process_bug.cgi into Bugzilla...
authormkanat%bugzilla.org <>
Fri, 18 Jan 2008 02:41:44 +0000 (02:41 +0000)
committermkanat%bugzilla.org <>
Fri, 18 Jan 2008 02:41:44 +0000 (02:41 +0000)
Patch By Max Kanat-Alexander <mkanat@bugzilla.org> r=LpSolit, a=LpSolit

Bugzilla/Bug.pm
Bugzilla/Status.pm
process_bug.cgi
template/en/default/global/code-error.html.tmpl
template/en/default/global/user-error.html.tmpl

index 28752da46679bc4923de10136aed602e1e8d9dcf..20e87c25c5bc3eff82180d41efcd300457919ce1 100755 (executable)
@@ -164,6 +164,7 @@ use constant UPDATE_VALIDATORS => {
     assigned_to         => \&_check_assigned_to,
     bug_status          => \&_check_bug_status,
     cclist_accessible   => \&Bugzilla::Object::check_boolean,
+    dup_id              => \&_check_dup_id,
     qa_contact          => \&_check_qa_contact,
     reporter_accessible => \&Bugzilla::Object::check_boolean,
     resolution          => \&_check_resolution,
@@ -178,14 +179,14 @@ sub UPDATE_COLUMNS {
     my @columns = qw(
         alias
         assigned_to
+        bug_file_loc
+        bug_severity
+        bug_status
         cclist_accessible
         component_id
         deadline
         estimated_time
         everconfirmed
-        bug_file_loc
-        bug_severity
-        bug_status
         op_sys
         priority
         product_id
@@ -437,12 +438,8 @@ sub run_create_validators {
     delete $params->{product};
 
     ($params->{bug_status}, $params->{everconfirmed})
-        = $class->_check_bug_status($params->{bug_status}, $product);
-
-    # Check whether a comment is required on bug creation.
-    my $vars = {};
-    $vars->{comment_exists} = ($params->{comment} =~ /\S+/) ? 1 : 0;
-    Bugzilla::Bug->check_status_change_triggers($params->{bug_status}, [], $vars);
+        = $class->_check_bug_status($params->{bug_status}, $product,
+                                    $params->{comment});
 
     $params->{target_milestone} = $class->_check_target_milestone(
         $params->{target_milestone}, $product);
@@ -582,14 +579,19 @@ sub update {
                          $delta_ts);
     }
 
-    # If this bug is no longer a duplicate, it no longer belongs in the
-    # dup table.
-    if (exists $changes->{'resolution'} 
-        && $changes->{'resolution'}->[0] eq 'DUPLICATE') 
-    {
-        my $dup_id = $self->dup_id;
+    # Check if we have to update the duplicates table and the other bug.
+    my ($old_dup, $cur_dup) = ($old_bug->dup_id || 0, $self->dup_id || 0);
+    if ($old_dup != $cur_dup) {
         $dbh->do("DELETE FROM duplicates WHERE dupe = ?", undef, $self->id);
-        $changes->{'dupe_of'} = [$dup_id, undef];
+        if ($cur_dup) {
+            $dbh->do('INSERT INTO duplicates (dupe, dupe_of) VALUES (?,?)',
+                     undef, $self->id, $cur_dup);
+            if (my $update_dup = delete $self->{_dup_for_update}) {
+                $update_dup->update();
+            }
+        }
+        
+        $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef];
     }
 
     # If any change occurred, refresh the timestamp of the bug.
@@ -893,41 +895,62 @@ sub _check_bug_severity {
 }
 
 sub _check_bug_status {
-    my ($invocant, $status, $product) = @_;
+    my ($invocant, $status, $product, $comment) = @_;
     my $user = Bugzilla->user;
 
-    my @valid_statuses;
+    # Make sure this is a valid status.
+    my $new_status = ref $status ? $status : Bugzilla::Status->check($status);
+    
+    my $old_status; # Note that this is undef for new bugs.
     if (ref $invocant) {
         $product = $invocant->product_obj;
-        @valid_statuses = map { $_->name } @{$invocant->status->can_change_to};
-    }
-    else {
-        @valid_statuses = map { $_->name } @{Bugzilla::Status->can_change_to()};
-    }
-
-    if (!$product->votes_to_confirm) {
-        # UNCONFIRMED becomes an invalid status if votes_to_confirm is 0,
-        # even if you are in editbugs.
-        @valid_statuses = grep {$_ ne 'UNCONFIRMED'} @valid_statuses;
+        $old_status = $invocant->status;
+        my $comments = $invocant->{added_comments} || [];
+        $comment = $comments->[-1];
     }
-
-    if (!ref($invocant)) {
+    
+    # Check permissions for users filing new bugs.
+    if (!ref $invocant) {
+        my $default_status = Bugzilla::Status->can_change_to->[0];
+        
         if ($user->in_group('editbugs', $product->id)
             || $user->in_group('canconfirm', $product->id)) {
            # If the user with privs hasn't selected another status,
            # select the first one of the list.
-           $status ||= $valid_statuses[0];
+           $new_status ||= $default_status;
         }
         else {
             # A user with no privs cannot choose the initial status.
-            $status = $valid_statuses[0];
+            $new_status = $default_status;
         }
     }
 
-    # This check already takes the workflow into account.
-    check_field('bug_status', $status, \@valid_statuses);
+    # Make sure this is a valid transition.
+    if (!$new_status->allow_change_from($old_status, $product)) {
+         ThrowUserError('illegal_bug_status_transition',
+                        { old => $old_status, new => $new_status });
+    }
 
-    return $status if ref $invocant;
+    # Check if a comment is required for this change.
+    if ($new_status->comment_required_on_change_from($old_status) && !$comment)
+    {
+        ThrowUserError('comment_required', { old => $old_status,
+                                             new => $new_status });
+        
+    }
+    
+    if (ref $invocant && $new_status->name eq 'ASSIGNED'
+        && Bugzilla->params->{"usetargetmilestone"}
+        && Bugzilla->params->{"musthavemilestoneonaccept"}
+        # musthavemilestoneonaccept applies only if at least two
+        # target milestones are defined for the product.
+        && scalar(@{ $product->milestones }) > 1
+        && $invocant->target_milestone eq $product->default_milestone)
+    {
+        ThrowUserError("milestone_required", { bug => $invocant });
+    }
+
+    return $new_status->name if ref $invocant;
     return ($status, $status eq 'UNCONFIRMED' ? 0 : 1);
 }
 
@@ -1073,6 +1096,85 @@ sub _check_dependencies {
     return ($deps{'dependson'}, $deps{'blocked'});
 }
 
+sub _check_dup_id {
+    my ($self, $dupe_of) = @_;
+    my $dbh = Bugzilla->dbh;
+    
+    $dupe_of = trim($dupe_of);
+    $dupe_of || ThrowCodeError('undefined_field', { field => 'dup_id' });
+    # Make sure we can change the original bug (issue A on bug 96085)
+    ValidateBugID($dupe_of, 'dup_id');
+    
+    # Make sure a loop isn't created when marking this bug
+    # as duplicate.
+    my %dupes;
+    my $this_dup = $dupe_of;
+    my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?');
+
+    while ($this_dup) {
+        if ($this_dup == $self->id) {
+            ThrowUserError('dupe_loop_detected', { bug_id  => $self->id,
+                                                   dupe_of => $dupe_of });
+        }
+        # If $dupes{$this_dup} is already set to 1, then a loop
+        # already exists which does not involve this bug.
+        # As the user is not responsible for this loop, do not
+        # prevent him from marking this bug as a duplicate.
+        last if exists $dupes{$this_dup};
+        $dupes{$this_dup} = 1;
+        $this_dup = $dbh->selectrow_array($sth, undef, $this_dup);
+    }
+
+    my $cur_dup = $self->dup_id || 0;
+    if ($cur_dup != $dupe_of && Bugzilla->params->{'commentonduplicate'}
+        && !$self->{added_comments})
+    {
+        ThrowUserError('comment_required');
+    }
+
+    # Should we add the reporter to the CC list of the new bug?
+    # If he can see the bug...
+    if ($self->reporter->can_see_bug($dupe_of)) {
+        my $dupe_of_bug = new Bugzilla::Bug($dupe_of);
+        # We only add him if he's not the reporter of the other bug.
+        $self->{_add_dup_cc} = 1
+            if $dupe_of_bug->reporter->id != $self->reporter->id;
+    }
+    # What if the reporter currently can't see the new bug? In the browser 
+    # interface, we prompt the user. In other interfaces, we default to 
+    # not adding the user, as the safest option.
+    elsif (Bugzilla->params->usage_mode == USAGE_MODE_BROWSER) {
+        # If we've already confirmed whether the user should be added...
+        my $cgi = Bugzilla->cgi;
+        my $add_confirmed = $cgi->param('confirm_add_duplicate');
+        if (defined $add_confirmed) {
+            $self->{_add_dup_cc} = $add_confirmed;
+        }
+        else {
+            # Note that here we don't check if he user is already the reporter
+            # of the dupe_of bug, since we already checked if he can *see*
+            # the bug, above. People might have reporter_accessible turned
+            # off, but cclist_accessible turned on, so they might want to
+            # add the reporter even though he's already the reporter of the
+            # dup_of bug.
+            my $vars = {};
+            my $template = Bugzilla->template;
+            # Ask the user what they want to do about the reporter.
+            $vars->{'cclist_accessible'} = $dbh->selectrow_array(
+                q{SELECT cclist_accessible FROM bugs WHERE bug_id = ?},
+                undef, $dupe_of);
+            $vars->{'original_bug_id'} = $dupe_of;
+            $vars->{'duplicate_bug_id'} = $self->id;
+            print $cgi->header();
+            $template->process("bug/process/confirm-duplicate.html.tmpl", $vars)
+              || ThrowTemplateError($template->error());
+            exit;
+        }
+    }
+
+    return $dupe_of;
+}
+
 sub _check_estimated_time {
     return $_[0]->_check_time($_[1], 'estimated_time');
 }
@@ -1221,9 +1323,45 @@ sub _check_rep_platform {
 }
 
 sub _check_resolution {
-    my ($invocant, $resolution) = @_;
+    my ($self, $resolution) = @_;
     $resolution = trim($resolution);
+    
+    # Throw a special error for resolving bugs without a resolution
+    # (or trying to change the resolution to '' on a closed bug without
+    # using clear_resolution).
+    ThrowUserError('missing_resolution', { status => $self->status->name })
+        if !$resolution && !$self->status->is_open;
+    
+    # Make sure this is a valid resolution.
     check_field('resolution', $resolution);
+
+    # The moving code doesn't use set_resolution. This check prevents
+    # people from hacking the URL variables (or using some other interface)
+    # and setting a bug to MOVED without moving it.
+    ThrowCodeError('no_manual_moved') if $resolution eq 'MOVED';
+    
+    # Don't allow open bugs to have resolutions.
+    ThrowUserError('resolution_not_allowed') if $self->status->is_open;
+    
+    # Check noresolveonopenblockers.
+    if (Bugzilla->params->{"noresolveonopenblockers"} && $resolution eq 'FIXED')
+    {
+        my @dependencies = CountOpenDependencies($self->id);
+        if (@dependencies) {
+            ThrowUserError("still_unresolved_bugs",
+                           { dependencies     => \@dependencies,
+                             dependency_count => scalar @dependencies });
+        }
+    }
+
+    # Check if they're changing the resolution and need to comment.
+    if (Bugzilla->params->{'commentonchange_resolution'} 
+        && $self->resolution && $resolution ne $self->resolution 
+        && !$self->{added_comments})
+    {
+        ThrowUserError('comment_required');
+    }
+    
     return $resolution;
 }
 
@@ -1476,6 +1614,11 @@ sub set_assigned_to {
 }
 sub reset_assigned_to {
     my $self = shift;
+    if (Bugzilla->params->{'commentonreassignbycomponent'} 
+        && !$self->{added_comments})
+    {
+        ThrowUserError('comment_required');
+    }
     my $comp = $self->component_obj;
     $self->set_assigned_to($comp->default_assignee);
 }
@@ -1528,6 +1671,38 @@ sub set_dependencies {
     $self->{'dependson'} = $dependson;
     $self->{'blocked'}   = $blocked;
 }
+sub _clear_dup_id { $_[0]->{dup_id} = undef; }
+sub set_dup_id {
+    my ($self, $dup_id) = @_;
+    my $old = $self->dup_id || 0;
+    $self->set('dup_id', $dup_id);
+    my $new = $self->dup_id || 0;
+    return if $old == $new;
+    
+    # Update the other bug.
+    my $dupe_of = new Bugzilla::Bug($self->dup_id);
+    if (delete $self->{_add_dup_cc}) {
+        $dupe_of->add_cc($self->reporter);
+    }
+    $dupe_of->add_comment("", { type       => CMT_HAS_DUPE,
+                                extra_data => $self->id });
+    $self->{_dup_for_update} = $dupe_of;
+    
+    # Now make sure that we add a duplicate comment on *this* bug.
+    # (Change an existing comment into a dup comment, if there is one,
+    # or add an empty dup comment.)
+    if ($self->{added_comments}) {
+        my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL }
+                          @{ $self->{added_comments} };
+        # Turn the last one into a dup comment.
+        $normal[-1]->{type} = CMT_DUPE_OF;
+        $normal[-1]->{extra_data} = $self->dup_id;
+    }
+    else {
+        $self->add_comment('', { type       => CMT_DUPE_OF,
+                                 extra_data => $self->dup_id });
+    }
+}
 sub set_estimated_time { $_[0]->set('estimated_time', $_[1]); }
 sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); }
 sub set_op_sys         { $_[0]->set('op_sys',        $_[1]); }
@@ -1665,6 +1840,11 @@ sub set_qa_contact {
 }
 sub reset_qa_contact {
     my $self = shift;
+    if (Bugzilla->params->{'commentonreassignbycomponent'}
+        && !$self->{added_comments})
+    {
+        ThrowUserError('comment_required');
+    }
     my $comp = $self->component_obj;
     $self->set_qa_contact($comp->default_qa_contact);
 }
@@ -1672,14 +1852,73 @@ sub set_remaining_time { $_[0]->set('remaining_time', $_[1]); }
 # Used only when closing a bug or moving between closed states.
 sub _zero_remaining_time { $_[0]->{'remaining_time'} = 0; }
 sub set_reporter_accessible { $_[0]->set('reporter_accessible', $_[1]); }
-sub set_resolution     { $_[0]->set('resolution',    $_[1]); }
-sub clear_resolution   { $_[0]->{'resolution'} = '' }
+sub set_resolution {
+    my ($self, $value, $dupe_of) = @_;
+    
+    my $old_res = $self->resolution;
+    $self->set('resolution', $value);
+    my $new_res = $self->resolution;
+    
+    if ($new_res ne $old_res) {
+        # Clear the dup_id if we're leaving the dup resolution.
+        if ($old_res eq 'DUPLICATE') {
+            $self->_clear_dup_id();
+        }
+        # Duplicates should have no remaining time left.
+        elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) {
+            $self->_zero_remaining_time();
+        }
+    }
+    
+    # We don't check if we're entering or leaving the dup resolution here,
+    # because we could be moving from being a dup of one bug to being a dup
+    # of another, theoretically. Note that this code block will also run
+    # when going between different closed states.
+    if ($self->resolution eq 'DUPLICATE') {
+        if ($dupe_of) {
+            $self->set_dup_id($dupe_of);
+        }
+        elsif (!$self->dup_id) {
+            ThrowUserError('dupe_id_required');
+        }
+    }
+}
+sub clear_resolution {
+    my $self = shift;
+    if (!$self->status->is_open) {
+        ThrowUserError('resolution_cant_clear', { bug_id => $self->id });
+    }
+    if (Bugzilla->params->{'commentonclearresolution'}
+        && $self->resolution && !$self->{added_comments})
+    {
+        ThrowUserError('comment_required');
+    }
+    $self->{'resolution'} = ''; 
+    $self->_clear_dup_id; 
+}
 sub set_severity       { $_[0]->set('bug_severity',  $_[1]); }
 sub set_status {
-    my ($self, $status) = @_;
+    my ($self, $status, $resolution, $dupe_of) = @_;
+    my $old_status = $self->status;
     $self->set('bug_status', $status);
-    # Check for the everconfirmed transition
-    $self->_set_everconfirmed(1) if (is_open_state($status) && $status ne 'UNCONFIRMED');
+    delete $self->{'status'};
+    my $new_status = $self->status;
+    
+    if ($new_status->is_open) {
+        # Check for the everconfirmed transition
+        $self->_set_everconfirmed(1) if $new_status->name ne 'UNCONFIRMED';
+        $self->clear_resolution();
+    }
+    else {
+        # We do this here so that we can make sure closed statuses have
+        # resolutions.
+        $self->set_resolution($resolution || $self->resolution, $dupe_of);
+        
+        # Changing between closed statuses zeros the remaining time.
+        if ($new_status->id != $old_status->id && $self->remaining_time != 0) {
+            $self->_zero_remaining_time();
+        }
+    }
 }
 sub set_status_whiteboard { $_[0]->set('status_whiteboard', $_[1]); }
 sub set_summary           { $_[0]->set('short_desc',        $_[1]); }
@@ -2389,217 +2628,29 @@ sub bug_alias_to_id {
 # Workflow Control routines
 #####################################################################
 
-# Make sure that the new status is allowed by the status workflow.
-sub check_status_transition {
-    my ($self, $new_status) = @_;
-
-    if (!grep { $_->name eq $self->bug_status } @{$new_status->can_change_from}) {
-        ThrowUserError('illegal_bug_status_transition', {old => $self->bug_status,
-                                                         new => $new_status->name})
-    }
-}
-
-# Make sure all checks triggered by the workflow are successful.
-# Some are hardcoded and come from older versions of Bugzilla.
-sub check_status_change_triggers {
-    my ($self, $action, $bugs, $vars) = @_;
+sub process_knob {
+    my ($self, $action, $to_resolution, $dupe_of) = @_;
     my $dbh = Bugzilla->dbh;
-    $vars ||= {};
-
-    my @bug_ids = map {$_->id} @$bugs;
-    # First, make sure no comment is required if there is none.
-    # If a comment is given, then this check is useless.
-    if (!$vars->{comment_exists}) {
-        if (grep { $action eq $_ } SPECIAL_STATUS_WORKFLOW_ACTIONS) {
-            # 'commentonnone' doesn't exist, so this is safe.
-            ThrowUserError('comment_required') if Bugzilla->params->{"commenton$action"};
-        }
-        elsif (!scalar @bug_ids) {
-            # The bug is being created; that's why @bug_ids is undefined.
-            my $comment_required =
-              $dbh->selectrow_array('SELECT require_comment
-                                       FROM status_workflow
-                                 INNER JOIN bug_status
-                                         ON id = new_status
-                                      WHERE old_status IS NULL
-                                        AND value = ?',
-                                      undef, $action);
-
-            ThrowUserError('description_required') if $comment_required;
-        }
-        else {
-            my $required_for_transitions =
-              $dbh->selectcol_arrayref('SELECT DISTINCT bug_status.value
-                                          FROM bug_status
-                                    INNER JOIN bugs
-                                            ON bugs.bug_status = bug_status.value
-                                    INNER JOIN status_workflow
-                                            ON bug_status.id = old_status
-                                    INNER JOIN bug_status b_s
-                                            ON b_s.id = new_status
-                                         WHERE bug_id IN (' . join (',', @bug_ids). ')
-                                           AND b_s.value = ?
-                                           AND require_comment = 1',
-                                         undef, $action);
-
-            if (scalar(@$required_for_transitions)) {
-                ThrowUserError('comment_required', {old => $required_for_transitions,
-                                                    new => $action});
-            }
-        }
-    }
-
-    # Now run hardcoded checks.
-    # There is no checks for these actions.
-    return if ($action eq 'none' || $action eq 'clearresolution');
-
-    # Also leave now if we are creating a new bug (we only want to check
-    # if a comment is required on bug creation).
-    return unless scalar @bug_ids;
 
+    return if $action eq 'none';
+    
     if ($action eq 'duplicate') {
-        # You cannot mark bugs as duplicates when changing
-        # several bugs at once.
-        $vars->{bug_id} || ThrowUserError('dupe_not_allowed');
-
-        # Make sure we can change the original bug (issue A on bug 96085)
-        $vars->{dup_id} || ThrowCodeError('undefined_field', { field => 'dup_id' });
-        ValidateBugID($vars->{dup_id}, 'dup_id');
-
-        # Make sure a loop isn't created when marking this bug
-        # as duplicate.
-        my %dupes;
-        my $dupe_of = $vars->{dup_id};
-        my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates
-                                 WHERE dupe = ?');
-
-        while ($dupe_of) {
-            if ($dupe_of == $vars->{bug_id}) {
-                ThrowUserError('dupe_loop_detected', { bug_id  => $vars->{bug_id},
-                                                       dupe_of => $vars->{dup_id} });
-            }
-            # If $dupes{$dupe_of} is already set to 1, then a loop
-            # already exists which does not involve this bug.
-            # As the user is not responsible for this loop, do not
-            # prevent him from marking this bug as a duplicate.
-            last if exists $dupes{"$dupe_of"};
-            $dupes{"$dupe_of"} = 1;
-            $sth->execute($dupe_of);
-            $dupe_of = $sth->fetchrow_array;
-        }
-
-        # Also, let's see if the reporter has authorization to see
-        # the bug to which we are duping. If not we need to prompt.
-        $vars->{DuplicateUserConfirm} = 1;
-
-        # DUPLICATE bugs should have no time remaining.
-        foreach my $bug (@$bugs) {
-            # Note that 0.00 is *true* for Perl!
-            next unless ($bug->remaining_time > 0);
-            $bug->_zero_remaining_time;
-            $vars->{'message'} = "remaining_time_zeroed"
-              if Bugzilla->user->in_group(Bugzilla->params->{'timetrackinggroup'});
-        }
-    }
-    elsif ($action eq 'change_resolution' || !is_open_state($action)) {
-        # don't resolve as fixed while still unresolved blocking bugs
-        if (Bugzilla->params->{"noresolveonopenblockers"}
-            && $vars->{resolution} eq 'FIXED')
-        {
-            my @dependencies = Bugzilla::Bug::CountOpenDependencies(@bug_ids);
-            if (scalar @dependencies > 0) {
-                ThrowUserError("still_unresolved_bugs",
-                               { dependencies     => \@dependencies,
-                                 dependency_count => scalar @dependencies });
-            }
-        }
-
-        # You cannot use change_resolution if there is at least one open bug
-        # nor can you close open bugs if no resolution is given.
-        my $open_states = join(',', map {$dbh->quote($_)} BUG_STATE_OPEN);
-        my $idlist = join(',', @bug_ids);
-        my $is_open =
-          $dbh->selectrow_array("SELECT 1 FROM bugs WHERE bug_id IN ($idlist)
-                                 AND bug_status IN ($open_states)");
-
-        if ($is_open) {
-            ThrowUserError('resolution_not_allowed') if ($action eq 'change_resolution');
-            ThrowUserError('missing_resolution', {status => $action}) if !$vars->{resolution};
-        }
-        # Now is good time to validate the resolution, if any.
-        check_field('resolution', $vars->{resolution},
-                    Bugzilla::Bug->settable_resolutions) if $vars->{resolution};
-
-        if ($action ne 'change_resolution') {
-            foreach my $b (@$bugs) {
-                if ($b->bug_status ne $action) {
-                    # Note that 0.00 is *true* for Perl!
-                    next unless ($b->remaining_time > 0);
-                    $b->_zero_remaining_time;
-                    $vars->{'message'} = "remaining_time_zeroed"
-                      if Bugzilla->user->in_group(Bugzilla->params->{'timetrackinggroup'});
-                }
-            }
-        }
+        $self->set_status(Bugzilla->params->{'duplicate_or_move_bug_status'},
+                          'DUPLICATE', $dupe_of);
     }
-    elsif ($action eq 'ASSIGNED'
-           && Bugzilla->params->{"usetargetmilestone"}
-           && Bugzilla->params->{"musthavemilestoneonaccept"})
-    {
-        $vars->{requiremilestone} = 1;
-    }
-}
-
-sub get_new_status_and_resolution {
-    my ($self, $action, $resolution) = @_;
-    my $dbh = Bugzilla->dbh;
-
-    my $status;
-    my $everconfirmed = $self->everconfirmed;
-    if ($action eq 'none') {
-        # Leaving the status unchanged doesn't need more investigation.
-        return ($self->bug_status, $self->resolution, $self->everconfirmed);
-    }
-    elsif ($action eq 'duplicate' || $action eq 'move') {
-        # Always change the bug status, even if the bug was already "closed".
-        $status = Bugzilla->params->{'duplicate_or_move_bug_status'};
-        $resolution = ($action eq 'duplicate') ? 'DUPLICATE' : 'MOVED';
+    elsif ($action eq 'move') {
+        $self->set_status(Bugzilla->params->{'duplicate_or_move_bug_status'},
+                          'MOVED');
     }
     elsif ($action eq 'change_resolution') {
-        $status = $self->bug_status;
-        # You cannot change the resolution of an open bug.
-        ThrowUserError('resolution_not_allowed') if is_open_state($status);
-        $resolution || ThrowUserError('missing_resolution', {status => $status});
+        $self->set_resolution($to_resolution);
     }
     elsif ($action eq 'clearresolution') {
-        $status = $self->bug_status;
-        is_open_state($status) || ThrowUserError('missing_resolution', {status => $status});
-        $resolution = '';
+        $self->clear_resolution();
     }
     else {
-        $status = $action;
-        if (is_open_state($status)) {
-            # Open bugs have no resolution.
-            $resolution = '';
-            $everconfirmed = ($status eq 'UNCONFIRMED') ? 0 : 1;
-        }
-        elsif (is_open_state($self->bug_status)) {
-            # A resolution is required to close bugs.
-            $resolution || ThrowUserError('missing_resolution', {status => $status});
-        }
-        else {
-            # Already closed bugs can only change their resolution
-            # using the change_resolution action.
-            $resolution = $self->resolution
-        }
+        $self->set_status($action, $to_resolution);
     }
-    # Now it's time to validate the bug resolution.
-    # Bug resolutions have no workflow specific rules, so any valid
-    # resolution will do it.
-    check_field('resolution', $resolution) if ($resolution ne '');
-    trick_taint($resolution);
-
-    return ($status, $resolution, $everconfirmed);
 }
 
 #####################################################################
index 65baf04b8a815deab3f3b06d2e050dd0be9b50a0..5573fa8363e949d3ecf6326484ab491d28a67271 100644 (file)
@@ -106,6 +106,24 @@ sub can_change_to {
     return $self->{'can_change_to'};
 }
 
+sub allow_change_from {
+    my ($self, $old_status, $product) = @_;
+
+    # Always allow transitions from a status to itself.
+    return 1 if ($old_status && $old_status->id == $self->id);
+
+    if ($self->name eq 'UNCONFIRMED' && !$product->votes_to_confirm) {
+        # UNCONFIRMED is an invalid status transition if votes_to_confirm is 0
+        # in this product.
+        return 0;
+    }
+
+    my ($cond, $values) = $self->_status_condition($old_status);
+    my ($transition_allowed) = Bugzilla->dbh->selectrow_array(
+        "SELECT 1 FROM status_workflow WHERE $cond", undef, @$values);
+    return $transition_allowed ? 1 : 0;
+}
+
 sub can_change_from {
     my $self = shift;
     my $dbh = Bugzilla->dbh;
@@ -128,6 +146,32 @@ sub can_change_from {
     return $self->{'can_change_from'};
 }
 
+sub comment_required_on_change_from {
+    my ($self, $old_status) = @_;
+    my ($cond, $values) = $self->_status_condition($old_status);
+    
+    my ($require_comment) = Bugzilla->dbh->selectrow_array(
+        "SELECT require_comment FROM status_workflow
+          WHERE $cond", undef, @$values);
+    return $require_comment;
+}
+
+# Used as a helper for various functions that have to deal with old_status
+# sometimes being NULL and sometimes having a value.
+sub _status_condition {
+    my ($self, $old_status) = @_;
+    my @values;
+    my $cond = 'old_status IS NULL';
+    # For newly-filed bugs
+    if ($old_status) {
+        $cond = 'old_status = ?';
+        push(@values, $old_status->id);
+    }
+    $cond .= " AND new_status = ?";
+    push(@values, $self->id);
+    return ($cond, \@values);
+}
+
 sub add_missing_bug_status_transitions {
     my $bug_status = shift || Bugzilla->params->{'duplicate_or_move_bug_status'};
     my $dbh = Bugzilla->dbh;
@@ -215,6 +259,60 @@ below.
 
  Returns:     A list of Bugzilla::Status objects.
 
+=item C<allow_change_from>
+
+=over
+
+=item B<Description>
+
+Tells you whether or not a change to this status from another status is
+allowed.
+
+=item B<Params>
+
+=over
+
+=item C<$old_status> - The Bugzilla::Status you're changing from.
+
+=item C<$product> - A L<Bugzilla::Product> representing the product of
+the bug you're changing. Needed to check product-specific workflow
+issues (such as whether or not the C<UNCONFIRMED> status is enabled
+in this product).
+
+=back
+
+=item B<Returns>
+
+C<1> if you are allowed to change to this status from that status, or
+C<0> if you aren't allowed.
+
+Note that changing from a status to itself is always allowed.
+
+=back
+
+=item C<comment_required_on_change_from>
+
+=over
+
+=item B<Description>
+
+Checks if a comment is required to change to this status from another
+status, according to the current settings in the workflow.
+
+Note that this doesn't implement the checks enforced by the various
+C<commenton> parameters--those are checked by internal checks in
+L<Bugzilla::Bug>.
+
+=item B<Params>
+
+C<$old_status> - The status you're changing from.
+
+=item B<Returns>
+
+C<1> if a comment is required on this change, C<0> if not.
+
+=back
+
 =item C<add_missing_bug_status_transitions>
 
  Description: Insert all missing transitions to a given bug status.
index dcea4ffba510d544d15920f7851195a96695713d..71be091685aa33cefe126975ab1f92a611185627 100755 (executable)
@@ -75,7 +75,6 @@ $vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
 
 my @editable_bug_fields = editable_bug_fields();
 
-my $requiremilestone = 0;
 local our $PrivilegesRequired = 0;
 
 ######################################################################
@@ -268,48 +267,9 @@ if (should_set('product')) {
     }
 }
 
-# Confirm that the reporter of the current bug can access the bug we are duping to.
-sub DuplicateUserConfirm {
-    my ($dupe, $original) = @_;
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
-    my $template = Bugzilla->template;
-
-    # if we've already been through here, then exit
-    if (defined $cgi->param('confirm_add_duplicate')) {
-        return;
-    }
-
-    if ($dupe->reporter->can_see_bug($original)) {
-        $cgi->param('confirm_add_duplicate', '1');
-        return;
-    }
-    elsif (Bugzilla->usage_mode == USAGE_MODE_EMAIL) {
-        # The email interface defaults to the safe alternative, which is
-        # not CC'ing the user.
-        $cgi->param('confirm_add_duplicate', 0);
-        return;
-    }
-
-    $vars->{'cclist_accessible'} = $dbh->selectrow_array(
-        q{SELECT cclist_accessible FROM bugs WHERE bug_id = ?},
-        undef, $original);
-    
-    # Once in this part of the subroutine, the user has not been auto-validated
-    # and the duper has not chosen whether or not to add to CC list, so let's
-    # ask the duper what he/she wants to do.
-    
-    $vars->{'original_bug_id'} = $original;
-    $vars->{'duplicate_bug_id'} = $dupe->bug_id;
-    
-    # Confirm whether or not to add the reporter to the cc: list
-    # of the original bug (the one this bug is being duped against).
-    print Bugzilla->cgi->header();
-    $template->process("bug/process/confirm-duplicate.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
-}
-
+# Component, target_milestone, and version are in here just in case
+# the 'product' field wasn't defined in the CGI. It doesn't hurt to set
+# them twice.
 my @set_fields = qw(op_sys rep_platform priority bug_severity
                     component target_milestone version
                     bug_file_loc status_whiteboard short_desc
@@ -324,9 +284,13 @@ my %methods = (
     bug_file_loc => 'set_url',
 );
 foreach my $b (@bug_objects) {
-    # Component, target_milestone, and version are in here just in case
-    # the 'product' field wasn't defined in the CGI. It doesn't hurt to set
-    # them twice.
+    if (should_set('comment') || $cgi->param('work_time')) {
+        # Add a comment as needed to each bug. This is done early because
+        # there are lots of things that want to check if we added a comment.
+        $b->add_comment(scalar($cgi->param('comment')),
+            { isprivate => scalar $cgi->param('commentprivacy'),
+              work_time => scalar $cgi->param('work_time') });
+    }
     foreach my $field_name (@set_fields) {
         if (should_set($field_name)) {
             my $method = $methods{$field_name};
@@ -480,7 +444,13 @@ if ($action eq Bugzilla->params->{'move-button-text'}) {
     foreach my $bug (@bug_objects) {
         my ($status, $resolution) = $bug->get_new_status_and_resolution('move');
         $bug->set_status($status);
-        $bug->set_resolution($resolution);
+        # We don't use set_resolution here because the MOVED resolution is
+        # special and is normally rejected by set_resolution.
+        $bug->{resolution} = $resolution;
+        # That means that we need to clear dups manually. Eventually this
+        # bug-moving code will all be inside Bugzilla::Bug, so it's OK
+        # to call an internal function here.
+        $bug->_clear_dup_id;
     }
     $_->update() foreach @bug_objects;
     $dbh->bz_unlock_tables();
@@ -527,56 +497,34 @@ if ($action eq Bugzilla->params->{'move-button-text'}) {
 }
 
 
-if (($cgi->param('set_default_assignee') || $cgi->param('set_default_qa_contact'))
-    && Bugzilla->params->{'commentonreassignbycomponent'} && !comment_exists())
-{
-        ThrowUserError('comment_required');
+# You cannot mark bugs as duplicates when changing several bugs at once
+# (because currently there is no way to check for duplicate loops in that
+# situation).
+if (!$cgi->param('id') && $cgi->param('dup_id')) {
+    ThrowUserError('dupe_not_allowed');
 }
 
-my $duplicate; # It will store the ID of the bug we are pointing to, if any.
-
-# Make sure the bug status transition is legal for all bugs.
-my $knob = scalar $cgi->param('knob');
-# Special actions (duplicate, change_resolution and clearresolution) are outside
-# the workflow.
-if (!grep { $knob eq $_ } SPECIAL_STATUS_WORKFLOW_ACTIONS) {
-    # Make sure the bug status exists and is active.
-    check_field('bug_status', $knob);
-    my $bug_status = new Bugzilla::Status({name => $knob});
-    $_->check_status_transition($bug_status) foreach @bug_objects;
-
-    # Fill the resolution field with the correct value (e.g. in case the
-    # workflow allows several open -> closed transitions).
-    if ($bug_status->is_open) {
-        $cgi->delete('resolution');
-    }
-    else {
-        $cgi->param('resolution', $cgi->param('resolution_knob_' . $bug_status->id));
+# Set the status, resolution, and dupe_of (if needed). This has to be done
+# down here, because the validity of status changes depends on other fields,
+# such as Target Milestone.
+foreach my $b (@bug_objects) {
+    if (should_set('knob')) {
+        # First, get the correct resolution <select>, in case there is more
+        # than one open -> closed transition allowed.
+        my $knob = $cgi->param('knob');
+        my $status = new Bugzilla::Status({name => $knob});
+        my $resolution;
+        if ($status) {
+            $resolution = $cgi->param('resolution_knob_' . $status->id);
+        }
+        else {
+            $resolution = $cgi->param('resolution_knob_change_resolution');
+        }
+        
+        # Translate the knob values into new status and resolution values.
+        $b->process_knob($knob, $resolution, scalar $cgi->param('dup_id'));
     }
 }
-elsif ($knob eq 'change_resolution') {
-    # Fill the resolution field with the correct value.
-    $cgi->param('resolution', $cgi->param('resolution_knob_change_resolution'));
-}
-else {
-    # The resolution field is not in use.
-    $cgi->delete('resolution');
-}
-
-# The action is a valid one.
-trick_taint($knob);
-# Some information is required for checks.
-$vars->{comment_exists} = comment_exists();
-$vars->{bug_id} = $cgi->param('id');
-$vars->{dup_id} = $cgi->param('dup_id');
-$vars->{resolution} = $cgi->param('resolution') || '';
-Bugzilla::Bug->check_status_change_triggers($knob, \@bug_objects, $vars);
-
-# Some triggers require extra actions.
-$duplicate = $vars->{dup_id} if ($knob eq 'duplicate');
-$requiremilestone = $vars->{requiremilestone};
-# $vars->{DuplicateUserConfirm} is true only if a single bug is being edited.
-DuplicateUserConfirm($bug, $duplicate) if $vars->{DuplicateUserConfirm};
 
 my $any_keyword_changes;
 if (defined $cgi->param('keywords')) {
@@ -627,32 +575,6 @@ foreach my $id (@idlist) {
     my $comma = $::comma;
     my $old_bug_obj = new Bugzilla::Bug($id);
 
-    my ($status, $everconfirmed);
-    my $resolution = $old_bug_obj->resolution;
-    # We only care about the resolution field if the user explicitly edits it
-    # or if he closes the bug.
-    if ($knob eq 'change_resolution' || $cgi->param('resolution')) {
-        $resolution = $cgi->param('resolution');
-    }
-    ($status, $resolution, $everconfirmed) =
-      $old_bug_obj->get_new_status_and_resolution($knob, $resolution);
-
-    if ($status ne $old_bug_obj->bug_status) {
-        $query .= "$comma bug_status = ?";
-        push(@bug_values, $status);
-        $comma = ',';
-    }
-    if ($resolution ne $old_bug_obj->resolution) {
-        $query .= "$comma resolution = ?";
-        push(@bug_values, $resolution);
-        $comma = ',';
-    }
-    if ($everconfirmed ne $old_bug_obj->everconfirmed) {
-        $query .= "$comma everconfirmed = ?";
-        push(@bug_values, $everconfirmed);
-        $comma = ',';
-    }
-
     my $bug_changed = 0;
     my $write = "WRITE";        # Might want to make a param to control
                                 # whether we do LOW_PRIORITY ...
@@ -695,9 +617,6 @@ foreach my $id (@idlist) {
         $formhash{$col} = $cgi->param($col) if defined $cgi->param($col);
         $i++;
     }
-    # The status and resolution are defined by the workflow.
-    $formhash{'bug_status'} = $status;
-    $formhash{'resolution'} = $resolution;
 
     # This hash is required by Bug::check_can_change_field().
     my $cgi_hash = {'dontchange' => scalar $cgi->param('dontchange')};
@@ -731,15 +650,7 @@ foreach my $id (@idlist) {
     }
 
     my $new_product = $bug_objects{$id}->product_obj;
-    # musthavemilestoneonaccept applies only if at least two
-    # target milestones are defined for the product.
-    if ($requiremilestone
-        && scalar(@{ $new_product->milestones }) > 1
-        && $bug_objects{$id}->target_milestone
-           eq $new_product->default_milestone)
-    {
-        ThrowUserError("milestone_required", { bug_id => $id });
-    }
+
     if (defined $cgi->param('delta_ts') && $cgi->param('delta_ts') ne $delta_ts)
     {
         ($vars->{'operations'}) =
@@ -763,23 +674,32 @@ foreach my $id (@idlist) {
         exit;
     }
 
-    if ($cgi->param('comment') || $cgi->param('work_time') || $duplicate) {
-        my $type = $duplicate ? CMT_DUPE_OF : CMT_NORMAL;
-
-        $bug_objects{$id}->add_comment(scalar($cgi->param('comment')),
-            { isprivate => scalar($cgi->param('commentprivacy')),
-              work_time => scalar $cgi->param('work_time'), type => $type, 
-              extra_data => $duplicate});
-        $bug_changed = 1;
-    }
-    
+   
     #################################
     # Start Actual Database Updates #
     #################################
     
     $timestamp = $dbh->selectrow_array(q{SELECT NOW()});
 
-    $bug_objects{$id}->update($timestamp);
+    my $changes = $bug_objects{$id}->update($timestamp);
+
+    my %notify_deps;
+    if ($changes->{'bug_status'}) {
+        my ($old_status, $new_status) = @{ $changes->{'bug_status'} };
+        
+        # If this bug has changed from opened to closed or vice-versa,
+        # then all of the bugs we block need to be notified.
+        if (is_open_state($old_status) ne is_open_state($new_status)) {
+            $notify_deps{$_} = 1 foreach (@{$bug_objects{$id}->blocked});
+        }
+        
+        # We may have zeroed the remaining time, if we moved into a closed
+        # status, so we should inform the user about that.
+        if (!is_open_state($new_status) && $changes->{'remaining_time'}) {
+            $vars->{'message'} = "remaining_time_zeroed"
+              if Bugzilla->user->in_group(Bugzilla->params->{'timetrackinggroup'});
+        }
+    }
 
     $bug_objects{$id}->update_keywords($timestamp);
     
@@ -790,11 +710,6 @@ foreach my $id (@idlist) {
         $dbh->do($query, undef, @bug_values);
     }
 
-    # Check for duplicates if the bug is [re]open or its resolution is changed.
-    if ($resolution ne 'DUPLICATE') {
-        $dbh->do(q{DELETE FROM duplicates WHERE dupe = ?}, undef, $id);
-    }
-
     my ($cc_removed) = $bug_objects{$id}->update_cc($timestamp);
     $cc_removed = [map {$_->login} @$cc_removed];
 
@@ -828,7 +743,6 @@ foreach my $id (@idlist) {
 
     # $msgs will store emails which have to be sent to voters, if any.
     my $msgs;
-    my %notify_deps;
     
     foreach my $c (@editable_bug_fields) {
         my $col = $c;           # We modify it, don't want to modify array
@@ -844,6 +758,7 @@ foreach my $id (@idlist) {
                                         bug_severity short_desc alias
                                         deadline estimated_time remaining_time
                                         reporter_accessible cclist_accessible
+                                        bug_status resolution
                                         status_whiteboard bug_file_loc),
                                      Bugzilla->custom_field_names);
 
@@ -857,14 +772,6 @@ foreach my $id (@idlist) {
                 CheckIfVotedConfirmed($id, $whoid);
             }
 
-            # If this bug has changed from opened to closed or vice-versa,
-            # then all of the bugs we block need to be notified.
-            if ($col eq 'bug_status' 
-                && is_open_state($old) ne is_open_state($new))
-            {
-                $notify_deps{$_} = 1 foreach (@{$bug_objects{$id}->blocked});
-            }
-
             LogActivityEntry($id,$col,$old,$new,$whoid,$timestamp);
             $bug_changed = 1;
         }
@@ -883,35 +790,6 @@ foreach my $id (@idlist) {
         MessageToMTA($msg);
     }
 
-    if ($duplicate) {
-        # If the bug was already marked as a duplicate, remove
-        # the existing entry.
-        $dbh->do('DELETE FROM duplicates WHERE dupe = ?',
-                  undef, $cgi->param('id'));
-
-        my $dup = new Bugzilla::Bug($duplicate);
-        my $reporter = $new_bug_obj->reporter;
-        my $isoncc = $dbh->selectrow_array(q{SELECT who FROM cc
-                                           WHERE bug_id = ? AND who = ?},
-                                           undef, $duplicate, $reporter->id);
-        unless (($reporter->id == $dup->reporter->id) || $isoncc
-                || !$cgi->param('confirm_add_duplicate')) {
-            # The reporter is oblivious to the existence of the original bug
-            # and is permitted access. Add him to the cc (and record activity).
-            LogActivityEntry($duplicate,"cc","",$reporter->name,
-                             $whoid,$timestamp);
-            $dbh->do(q{INSERT INTO cc (who, bug_id) VALUES (?, ?)},
-                     undef, $reporter->id, $duplicate);
-        }
-        # Bug 171639 - Duplicate notifications do not need to be private.
-        $dup->add_comment("", { type => CMT_HAS_DUPE,
-                                extra_data => $new_bug_obj->bug_id });
-        $dup->update($timestamp);
-
-        $dbh->do(q{INSERT INTO duplicates VALUES (?, ?)}, undef,
-                 $duplicate, $cgi->param('id'));
-    }
-
     # Now all changes to the DB have been made. It's time to email
     # all concerned users, including the bug itself, but also the
     # duplicated bug and dependent bugs, if any.
@@ -930,15 +808,18 @@ foreach my $id (@idlist) {
     # receive email about the change.
     send_results($id, $vars);
  
-    if ($duplicate) {
+    # If the bug was marked as a duplicate, we need to notify users on the
+    # other bug of any changes to that bug.
+    my $new_dup_id = $changes->{'dup_id'} ? $changes->{'dup_id'}->[1] : undef;
+    if ($new_dup_id) {
         $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login }; 
 
-        $vars->{'id'} = $duplicate;
+        $vars->{'id'} = $new_dup_id;
         $vars->{'type'} = "dupe";
         
         # Let the user know a duplication notation was added to the 
         # original bug.
-        send_results($duplicate, $vars);
+        send_results($new_dup_id, $vars);
     }
 
     my %all_dep_changes = (%notify_deps, %changed_deps);
index dce7947d61ffa075091539ea1a0edd84acacf814..1e5581e1337fd8fa193d92fcb54a3307cde77177 100644 (file)
   [% ELSIF error == "need_quipid" %]
     A valid quipid is needed.
 
+  [% ELSIF error == "no_manual_moved" %]
+    You cannot set the resolution of [% terms.abug %] to MOVED without
+    moving the [% terms.bug %].
+
   [% ELSIF error == "param_must_be_numeric" %]
     [% title = "Invalid Parameter" %]
     Invalid parameter passed to [% function FILTER html %].
index 7bce4ad98a167c71e735cbc84ed7495f4f4ae930..ca9bf0cdb50dfbef998593612dc0d49b284cb4ee 100644 (file)
 
   [% ELSIF error == "comment_required" %]
     [% title = "Comment Required" %]
-    You have to specify a <b>comment</b>
-    [% IF old.size && new %]
-      to change the [% terms.bug %] status from [% old.join(", ") FILTER html %]
-      to [% new FILTER html %].
+    You have to specify a
+    [% IF old && new %]
+      <b>comment</b> when changing the status of [% terms.abug %] from
+      [%+ old.name FILTER html %] to [% new.name FILTER html %].
+    [% ELSIF new %]
+      description for this [% terms.bug %].
     [% ELSE %]
-      on this change.
+      <b>comment</b> on this change.
     [% END %]
-    Please explain your change.
 
   [% ELSIF error == "comment_too_long" %]
     [% title = "Comment Too Long" %]
     [% title = "Dependency Loop Detected" %]
     You can't make [% terms.abug %] block itself or depend on itself.
 
-  [% ELSIF error == "description_required" %]
-    [% title = "Description Required" %]
-    You must provide a description of the [% terms.bug %].
+  [% ELSIF error == "dupe_id_required" %]
+    [% title = "Duplicate Bug Id Required" %]
+    You must specify [% terms.abug %] id to mark this [% terms.bug %]
+    as a duplicate of.
 
   [% ELSIF error == "dupe_not_allowed" %]
     [% title = "Cannot mark $terms.bugs as duplicates" %]
 
   [% ELSIF error == "illegal_bug_status_transition" %]
     [% title = "Illegal $terms.Bug Status Change" %]
-    You are not allowed to change the [% terms.bug %] status from
-    [%+ old FILTER html %] to [% new FILTER html %].
+    [% IF old.defined %]
+      You are not allowed to change the [% terms.bug %] status from
+      [%+ old.name FILTER html %] to [% new.name FILTER html %].
+    [% ELSE %]
+      You are not allowed to file new [% terms.bugs %] with the
+      [%+ new.name FILTER html %] status. 
+    [% END %]
 
   [% ELSIF error == "illegal_change" %]
     [% title = "Not allowed" %]
   [% ELSIF error == "milestone_required" %]
     [% title = "Milestone Required" %]
     You must determine a target milestone for [% terms.bug %] 
-    [%+ bug_id FILTER html %]
+    [%+ bug.id FILTER html %]
     if you are going to accept it.  Part of accepting 
     [%+ terms.abug %] is giving an estimate of when it will be fixed.
 
     [% title = "Summary Needed" %]
     You must enter a summary for this [% terms.bug %].
 
+  [% ELSIF error == "resolution_cant_clear" %]
+    [% terms.Bug %] [% bug_id FILTER bug_link(bug_id) FILTER none %] is 
+    closed, so you cannot clear its resolution.
+
   [% ELSIF error == "resolution_not_allowed" %]
     [% title = "Resolution Not Allowed" %]
     You cannot set a resolution for open [% terms.bugs %].
     version
   [% ELSIF class == "Bugzilla::Milestone" %]
     milestone
+  [% ELSIF class == "Bugzilla::Status" %]
+    status
   [% END %]
 [% END %]