Commit 620caf68 authored by mkanat%bugzilla.org's avatar mkanat%bugzilla.org

Bug 402791: Move status and resolution updating from process_bug.cgi into Bugzilla::Bug

Patch By Max Kanat-Alexander <mkanat@bugzilla.org> r=LpSolit, a=LpSolit
parent f39fc76d
......@@ -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()};
$old_status = $invocant->status;
my $comments = $invocant->{added_comments} || [];
$comment = $comments->[-1];
}
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;
}
# Check permissions for users filing new bugs.
if (!ref $invocant) {
my $default_status = Bugzilla::Status->can_change_to->[0];
if (!ref($invocant)) {
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);
delete $self->{'status'};
my $new_status = $self->status;
if ($new_status->is_open) {
# Check for the everconfirmed transition
$self->_set_everconfirmed(1) if (is_open_state($status) && $status ne 'UNCONFIRMED');
$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;
$self->set_status(Bugzilla->params->{'duplicate_or_move_bug_status'},
'DUPLICATE', $dupe_of);
}
# 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'});
}
}
}
}
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});
$self->set_status($action, $to_resolution);
}
else {
# Already closed bugs can only change their resolution
# using the change_resolution action.
$resolution = $self->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);
}
#####################################################################
......
......@@ -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.
......
......@@ -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');
# 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 {
$cgi->param('resolution', $cgi->param('resolution_knob_' . $bug_status->id));
$resolution = $cgi->param('resolution_knob_change_resolution');
}
}
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};
# Translate the knob values into new status and resolution values.
$b->process_knob($knob, $resolution, scalar $cgi->param('dup_id'));
}
}
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,15 +674,6 @@ 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 #
......@@ -779,7 +681,25 @@ foreach my $id (@idlist) {
$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);
......
......@@ -309,6 +309,10 @@
[% 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 %].
......
......@@ -252,14 +252,15 @@
[% 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" %]
......@@ -346,9 +347,10 @@
[% 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" %]
......@@ -679,8 +681,13 @@
[% ELSIF error == "illegal_bug_status_transition" %]
[% title = "Illegal $terms.Bug Status Change" %]
[% IF old.defined %]
You are not allowed to change the [% terms.bug %] status from
[%+ old FILTER html %] to [% new FILTER html %].
[%+ 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" %]
......@@ -940,7 +947,7 @@
[% 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.
......@@ -1379,6 +1386,10 @@
[% 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 %].
......@@ -1659,5 +1670,7 @@
version
[% ELSIF class == "Bugzilla::Milestone" %]
milestone
[% ELSIF class == "Bugzilla::Status" %]
status
[% END %]
[% END %]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment