diff options
-rwxr-xr-x | Bugzilla/Bug.pm | 8 | ||||
-rw-r--r-- | Bugzilla/Config/BugChange.pm | 22 | ||||
-rw-r--r-- | Bugzilla/Config/Common.pm | 12 | ||||
-rw-r--r-- | Bugzilla/Install/DB.pm | 153 | ||||
-rw-r--r-- | Bugzilla/Status.pm | 55 | ||||
-rwxr-xr-x | editparams.cgi | 4 | ||||
-rwxr-xr-x | editvalues.cgi | 14 | ||||
-rw-r--r-- | editworkflow.cgi | 5 | ||||
-rwxr-xr-x | process_bug.cgi | 5 | ||||
-rw-r--r-- | template/en/default/admin/params/bugchange.html.tmpl | 3 | ||||
-rw-r--r-- | template/en/default/admin/workflow/edit.html.tmpl | 19 | ||||
-rw-r--r-- | template/en/default/bug/knob.html.tmpl | 1 |
12 files changed, 217 insertions, 84 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 3ace277dd..da9e9a6b3 100755 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -1986,10 +1986,10 @@ sub get_new_status_and_resolution { # Leaving the status unchanged doesn't need more investigation. return ($self->bug_status, $self->resolution, $self->everconfirmed); } - elsif ($action eq 'duplicate') { - # Only alter the bug status if the bug is currently open. - $status = is_open_state($self->bug_status) ? 'RESOLVED' : $self->bug_status; - $resolution = 'DUPLICATE'; + 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 'change_resolution') { $status = $self->bug_status; diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm index 6941f0046..65b2aec96 100644 --- a/Bugzilla/Config/BugChange.pm +++ b/Bugzilla/Config/BugChange.pm @@ -34,13 +34,35 @@ package Bugzilla::Config::BugChange; use strict; use Bugzilla::Config::Common; +use Bugzilla::Status; $Bugzilla::Config::BugChange::sortkey = "03"; sub get_param_list { my $class = shift; + + # Hardcoded bug statuses which existed before Bugzilla 3.1. + my @closed_bug_statuses = ('RESOLVED', 'VERIFIED', 'CLOSED'); + + # If we are upgrading from 3.0 or older, bug statuses are not customisable + # and bug_status.is_open is not yet defined (hence the eval), so we use + # the bug statuses above as they are still hardcoded. + eval { + my @current_closed_states = map {$_->name} Bugzilla::Status::closed_bug_statuses(); + # If no closed state was found, use the default list above. + @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states); + }; + my @param_list = ( { + name => 'duplicate_or_move_bug_status', + type => 's', + choices => \@closed_bug_statuses, + default => $closed_bug_statuses[0], + checker => \&check_bug_status + }, + + { name => 'letsubmitterchoosepriority', type => 'b', default => 1 diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm index 0d6db5257..188ef0c90 100644 --- a/Bugzilla/Config/Common.pm +++ b/Bugzilla/Config/Common.pm @@ -40,6 +40,7 @@ use Bugzilla::Util; use Bugzilla::Constants; use Bugzilla::Field; use Bugzilla::Group; +use Bugzilla::Status; use base qw(Exporter); @Bugzilla::Config::Common::EXPORT = @@ -48,7 +49,7 @@ use base qw(Exporter); check_opsys check_shadowdb check_urlbase check_webdotbase check_netmask check_user_verify_class check_image_converter check_languages check_mail_delivery_method check_notification - check_timezone check_utf8 + check_timezone check_utf8 check_bug_status ); # Checking functions for the various values @@ -166,6 +167,15 @@ sub check_opsys { return ""; } +sub check_bug_status { + my $bug_status = shift; + my @closed_bug_statuses = map {$_->name} Bugzilla::Status::closed_bug_statuses(); + if (lsearch(\@closed_bug_statuses, $bug_status) < 0) { + return "Must be a valid closed status: one of " . join(', ', @closed_bug_statuses); + } + return ""; +} + sub check_group { my $group_name = shift; return "" unless $group_name; diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm index 3991cd453..4b4e7a068 100644 --- a/Bugzilla/Install/DB.pm +++ b/Bugzilla/Install/DB.pm @@ -2783,85 +2783,102 @@ sub _initialize_workflow { my $old_params = shift; my $dbh = Bugzilla->dbh; - if (!$dbh->bz_column_info('bug_status', 'is_open')) { - $dbh->bz_add_column('bug_status', 'is_open', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - # Till now, bug statuses were not customizable. Nevertheless, local - # changes are possible and so we will try to respect these changes. - # This means: get the status of bugs having a resolution different from '' - # and mark these statuses as 'closed', even if some of these statuses are - # expected to be open statuses. Bug statuses we have no information about - # are left as 'open'. - my @statuses = - @{$dbh->selectcol_arrayref('SELECT DISTINCT bug_status FROM bugs - WHERE resolution != ?', undef, '')}; + $dbh->bz_add_column('bug_status', 'is_open', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - # Append the default list of closed statuses. Duplicated statuses don't hurt. - @statuses = map {$dbh->quote($_)} (@statuses, qw(RESOLVED VERIFIED CLOSED)); + # Till now, bug statuses were not customizable. Nevertheless, local + # changes are possible and so we will try to respect these changes. + # This means: get the status of bugs having a resolution different from '' + # and mark these statuses as 'closed', even if some of these statuses are + # expected to be open statuses. Bug statuses we have no information about + # are left as 'open'. + my @closed_statuses = + @{$dbh->selectcol_arrayref('SELECT DISTINCT bug_status FROM bugs + WHERE resolution != ?', undef, '')}; + + # Append the default list of closed statuses *unless* we detect at least + # one closed state in the DB (i.e. with is_open = 0). This would mean that + # the DB has already been updated at least once and maybe the admin decided + # that e.g. 'RESOLVED' is now an open state, in which case we don't want to + # override this attribute. At least one bug status has to be a closed state + # anyway (due to the 'duplicate_or_move_bug_status' parameter) so it's safe + # to use this criteria. + my $num_closed_states = $dbh->selectrow_array('SELECT COUNT(*) FROM bug_status + WHERE is_open = 0'); + + if (!$num_closed_states) { + @closed_statuses = + map {$dbh->quote($_)} (@closed_statuses, qw(RESOLVED VERIFIED CLOSED)); print "Marking closed bug statuses as such...\n"; $dbh->do('UPDATE bug_status SET is_open = 0 WHERE value IN (' . - join(', ', @statuses) . ')'); + join(', ', @closed_statuses) . ')'); } # Populate the status_workflow table. We do nothing if the table already # has entries. If all bug status transitions have been deleted, the # workflow will be restored to its default schema. my $count = $dbh->selectrow_array('SELECT COUNT(*) FROM status_workflow'); - return if $count; - - my $create = $old_params->{'commentoncreate'}; - my $confirm = $old_params->{'commentonconfirm'}; - my $accept = $old_params->{'commentonaccept'}; - my $resolve = $old_params->{'commentonresolve'}; - my $verify = $old_params->{'commentonverify'}; - my $close = $old_params->{'commentonclose'}; - my $reopen = $old_params->{'commentonreopen'}; - # This was till recently the only way to get back to NEW for - # confirmed bugs, so we use this parameter here. - my $reassign = $old_params->{'commentonreassign'}; - - # This is the default workflow. - my @workflow = ([undef, 'UNCONFIRMED', $create], - [undef, 'NEW', $create], - [undef, 'ASSIGNED', $create], - ['UNCONFIRMED', 'NEW', $confirm], - ['UNCONFIRMED', 'ASSIGNED', $accept], - ['UNCONFIRMED', 'RESOLVED', $resolve], - ['NEW', 'ASSIGNED', $accept], - ['NEW', 'RESOLVED', $resolve], - ['ASSIGNED', 'NEW', $reassign], - ['ASSIGNED', 'RESOLVED', $resolve], - ['REOPENED', 'NEW', $reassign], - ['REOPENED', 'ASSIGNED', $accept], - ['REOPENED', 'RESOLVED', $resolve], - ['RESOLVED', 'UNCONFIRMED', $reopen], - ['RESOLVED', 'REOPENED', $reopen], - ['RESOLVED', 'VERIFIED', $verify], - ['RESOLVED', 'CLOSED', $close], - ['VERIFIED', 'UNCONFIRMED', $reopen], - ['VERIFIED', 'REOPENED', $reopen], - ['VERIFIED', 'CLOSED', $close], - ['CLOSED', 'UNCONFIRMED', $reopen], - ['CLOSED', 'REOPENED', $reopen]); - - print "Now filling the 'status_workflow' table with valid bug status transitions...\n"; - my $sth_select = $dbh->prepare('SELECT id FROM bug_status WHERE value = ?'); - my $sth = $dbh->prepare('INSERT INTO status_workflow (old_status, new_status, - require_comment) VALUES (?, ?, ?)'); - - foreach my $transition (@workflow) { - my ($from, $to); - # If it's an initial state, there is no "old" value. - $from = $dbh->selectrow_array($sth_select, undef, $transition->[0]) - if $transition->[0]; - $to = $dbh->selectrow_array($sth_select, undef, $transition->[1]); - # If one of the bug statuses doesn't exist, the transition is invalid. - next if (($transition->[0] && !$from) || !$to); - - $sth->execute($from, $to, $transition->[2] ? 1 : 0); + + if (!$count) { + # Make sure the variables below are defined as + # status_workflow.require_comment cannot be NULL. + my $create = $old_params->{'commentoncreate'} || 0; + my $confirm = $old_params->{'commentonconfirm'} || 0; + my $accept = $old_params->{'commentonaccept'} || 0; + my $resolve = $old_params->{'commentonresolve'} || 0; + my $verify = $old_params->{'commentonverify'} || 0; + my $close = $old_params->{'commentonclose'} || 0; + my $reopen = $old_params->{'commentonreopen'} || 0; + # This was till recently the only way to get back to NEW for + # confirmed bugs, so we use this parameter here. + my $reassign = $old_params->{'commentonreassign'} || 0; + + # This is the default workflow. + my @workflow = ([undef, 'UNCONFIRMED', $create], + [undef, 'NEW', $create], + [undef, 'ASSIGNED', $create], + ['UNCONFIRMED', 'NEW', $confirm], + ['UNCONFIRMED', 'ASSIGNED', $accept], + ['UNCONFIRMED', 'RESOLVED', $resolve], + ['NEW', 'ASSIGNED', $accept], + ['NEW', 'RESOLVED', $resolve], + ['ASSIGNED', 'NEW', $reassign], + ['ASSIGNED', 'RESOLVED', $resolve], + ['REOPENED', 'NEW', $reassign], + ['REOPENED', 'ASSIGNED', $accept], + ['REOPENED', 'RESOLVED', $resolve], + ['RESOLVED', 'UNCONFIRMED', $reopen], + ['RESOLVED', 'REOPENED', $reopen], + ['RESOLVED', 'VERIFIED', $verify], + ['RESOLVED', 'CLOSED', $close], + ['VERIFIED', 'UNCONFIRMED', $reopen], + ['VERIFIED', 'REOPENED', $reopen], + ['VERIFIED', 'CLOSED', $close], + ['CLOSED', 'UNCONFIRMED', $reopen], + ['CLOSED', 'REOPENED', $reopen]); + + print "Now filling the 'status_workflow' table with valid bug status transitions...\n"; + my $sth_select = $dbh->prepare('SELECT id FROM bug_status WHERE value = ?'); + my $sth = $dbh->prepare('INSERT INTO status_workflow (old_status, new_status, + require_comment) VALUES (?, ?, ?)'); + + foreach my $transition (@workflow) { + my ($from, $to); + # If it's an initial state, there is no "old" value. + $from = $dbh->selectrow_array($sth_select, undef, $transition->[0]) + if $transition->[0]; + $to = $dbh->selectrow_array($sth_select, undef, $transition->[1]); + # If one of the bug statuses doesn't exist, the transition is invalid. + next if (($transition->[0] && !$from) || !$to); + + $sth->execute($from, $to, $transition->[2] ? 1 : 0); + } } + + # Make sure the bug status used by the 'duplicate_or_move_bug_status' + # parameter has all the required transitions set. + Bugzilla::Status::add_missing_bug_status_transitions(); } 1; diff --git a/Bugzilla/Status.pm b/Bugzilla/Status.pm index e91f83871..cf8f98efa 100644 --- a/Bugzilla/Status.pm +++ b/Bugzilla/Status.pm @@ -54,15 +54,22 @@ sub is_open { return $_[0]->{'is_open'}; } ##### Methods #### ############################### +sub closed_bug_statuses { + my @bug_statuses = Bugzilla::Status->get_all; + @bug_statuses = grep { !$_->is_open } @bug_statuses; + return @bug_statuses; +} + sub can_change_to { my $self = shift; my $dbh = Bugzilla->dbh; if (!ref($self) || !defined $self->{'can_change_to'}) { - my ($cond, @args); + my ($cond, @args, $self_exists); if (ref($self)) { $cond = '= ?'; push(@args, $self->id); + $self_exists = 1; } else { $cond = 'IS NULL'; @@ -78,12 +85,37 @@ sub can_change_to { AND old_status $cond", undef, @args); + # Allow the bug status to remain unchanged. + push(@$new_status_ids, $self->id) if $self_exists; $self->{'can_change_to'} = Bugzilla::Status->new_from_list($new_status_ids); } return $self->{'can_change_to'}; } +sub add_missing_bug_status_transitions { + my $bug_status = shift || Bugzilla->params->{'duplicate_or_move_bug_status'}; + my $dbh = Bugzilla->dbh; + my $new_status = new Bugzilla::Status({name => $bug_status}); + # Silently discard invalid bug statuses. + $new_status || return; + + my $missing_statuses = $dbh->selectcol_arrayref('SELECT id + FROM bug_status + LEFT JOIN status_workflow + ON old_status = id + AND new_status = ? + WHERE old_status IS NULL', + undef, $new_status->id); + + my $sth = $dbh->prepare('INSERT INTO status_workflow + (old_status, new_status) VALUES (?, ?)'); + + foreach my $old_status_id (@$missing_statuses) { + next if ($old_status_id == $new_status->id); + $sth->execute($old_status_id, $new_status->id); + } +} 1; @@ -100,6 +132,10 @@ Bugzilla::Status - Bug status class. my $bug_status = new Bugzilla::Status({name => 'ASSIGNED'}); my $bug_status = new Bugzilla::Status(4); + my @closed_bug_statuses = Bugzilla::Status::closed_bug_statuses(); + + Bugzilla::Status::add_missing_bug_status_transitions($bug_status); + =head1 DESCRIPTION Status.pm represents a bug status object. It is an implementation @@ -113,6 +149,15 @@ below. =over +=item C<closed_bug_statuses> + + Description: Returns a list of C<Bugzilla::Status> objects which can have + a resolution associated with them ("closed" bug statuses). + + Params: none. + + Returns: A list of Bugzilla::Status objects. + =item C<can_change_to> Description: Returns the list of active statuses a bug can be changed to @@ -122,6 +167,14 @@ below. Returns: A list of Bugzilla::Status objects. +=item C<add_missing_bug_status_transitions> + + Description: Insert all missing transitions to a given bug status. + + Params: $bug_status - The value (name) of a bug status. + + Returns: nothing. + =back =cut diff --git a/editparams.cgi b/editparams.cgi index 7d280474b..38d486656 100755 --- a/editparams.cgi +++ b/editparams.cgi @@ -34,6 +34,7 @@ use Bugzilla::Error; use Bugzilla::Token; use Bugzilla::User; use Bugzilla::User::Setting; +use Bugzilla::Status; my $user = Bugzilla->login(LOGIN_REQUIRED); my $cgi = Bugzilla->cgi; @@ -137,6 +138,9 @@ if ($action eq 'save' && $current_module) { if ($name eq 'languages') { $update_lang_user_pref = 1; } + if ($name eq 'duplicate_or_move_bug_status') { + Bugzilla::Status::add_missing_bug_status_transitions($value); + } } } if ($update_lang_user_pref) { diff --git a/editvalues.cgi b/editvalues.cgi index 3974d45dc..951078389 100755 --- a/editvalues.cgi +++ b/editvalues.cgi @@ -29,6 +29,7 @@ use Bugzilla::Config qw(:admin); use Bugzilla::Token; use Bugzilla::Field; use Bugzilla::Bug; +use Bugzilla::Status; # List of different tables that contain the changeable field values # (the old "enums.") Keep them in alphabetical order by their @@ -136,7 +137,7 @@ $defaults{'bug_severity'} = 'defaultseverity'; # Alternatively, a list of non-editable values can be specified. # In this case, only the sortkey can be altered. my %static; -$static{'bug_status'} = ['UNCONFIRMED']; +$static{'bug_status'} = ['UNCONFIRMED', Bugzilla->params->{'duplicate_or_move_bug_status'}]; $static{'resolution'} = ['', 'FIXED', 'MOVED', 'DUPLICATE']; $static{$_->name} = ['---'] foreach (@custom_fields); @@ -234,9 +235,14 @@ if ($action eq 'new') { $dbh->do("INSERT INTO $field (value, sortkey) VALUES (?, ?)", undef, ($value, $sortkey)); - if ($field eq 'bug_status' && !$cgi->param('is_open')) { - # The bug status is a closed state, but they are open by default. - $dbh->do('UPDATE bug_status SET is_open = 0 WHERE value = ?', undef, $value); + if ($field eq 'bug_status') { + unless ($cgi->param('is_open')) { + # The bug status is a closed state, but they are open by default. + $dbh->do('UPDATE bug_status SET is_open = 0 WHERE value = ?', undef, $value); + } + # Allow the transition from this new bug status to the one used + # by the 'duplicate_or_move_bug_status' parameter. + Bugzilla::Status::add_missing_bug_status_transitions(); } delete_token($token); diff --git a/editworkflow.cgi b/editworkflow.cgi index ac914f76d..6aaed345e 100644 --- a/editworkflow.cgi +++ b/editworkflow.cgi @@ -100,7 +100,10 @@ elsif ($action eq 'update') { foreach my $new (@$statuses) { next if $old->id == $new->id; - if ($cgi->param('w_' . $old->id . '_' . $new->id)) { + # All transitions to 'duplicate_or_move_bug_status' must be valid. + if ($cgi->param('w_' . $old->id . '_' . $new->id) + || ($new->name eq Bugzilla->params->{'duplicate_or_move_bug_status'})) + { $sth_insert->execute($old->id, $new->id) unless defined $workflow->{$old->id}->{$new->id}; } diff --git a/process_bug.cgi b/process_bug.cgi index 6abbbb401..0faae9e7c 100755 --- a/process_bug.cgi +++ b/process_bug.cgi @@ -496,8 +496,9 @@ if ($action eq Bugzilla->params->{'move-button-text'}) { local $Storable::forgive_me = 1; my $bugs = dclone(\@bug_objects); foreach my $bug (@bug_objects) { - $bug->set_status('RESOLVED'); - $bug->set_resolution('MOVED'); + my ($status, $resolution) = $bug->get_new_status_and_resolution('move'); + $bug->set_status($status); + $bug->set_resolution($resolution); } $_->update() foreach @bug_objects; $dbh->bz_unlock_tables(); diff --git a/template/en/default/admin/params/bugchange.html.tmpl b/template/en/default/admin/params/bugchange.html.tmpl index 022e0cac1..0b2f42c6d 100644 --- a/template/en/default/admin/params/bugchange.html.tmpl +++ b/template/en/default/admin/params/bugchange.html.tmpl @@ -25,6 +25,9 @@ %] [% param_descs = { + duplicate_or_move_bug_status => "When $terms.abug is marked as a duplicate of another one " _ + "or is moved to another installation, use this $terms.bug status." + letsubmitterchoosepriority => "If this is on, then people submitting $terms.bugs can " _ "choose an initial priority for that ${terms.bug}. " _ "If off, then all $terms.bugs initially have the default " _ diff --git a/template/en/default/admin/workflow/edit.html.tmpl b/template/en/default/admin/workflow/edit.html.tmpl index d602171a1..68e16a022 100644 --- a/template/en/default/admin/workflow/edit.html.tmpl +++ b/template/en/default/admin/workflow/edit.html.tmpl @@ -69,12 +69,14 @@ [% FOREACH new_status = statuses %] [% IF status.id != new_status.id %] - <td align="center" class="checkbox-cell - [% " checked" IF workflow.${status.id}.${new_status.id}.defined %]" + [% checked = workflow.${status.id}.${new_status.id}.defined ? 1 : 0 %] + [% mandatory = (status.id && new_status.name == Param("duplicate_or_move_bug_status")) ? 1 : 0 %] + <td align="center" class="checkbox-cell[% " checked" IF checked || mandatory %]" title="From [% status.name FILTER html %] to [% new_status.name FILTER html %]"> <input type="checkbox" name="w_[% status.id %]_[% new_status.id %]" id="w_[% status.id %]_[% new_status.id %]" onclick="toggle_cell(this)" - [% " checked='checked'" IF workflow.${status.id}.${new_status.id}.defined %]> + [%+ "checked='checked'" IF checked || mandatory %] + [%+ "disabled='disabled'" IF mandatory %]> </td> [% ELSE %] <td class="checkbox-cell forbidden"> </td> @@ -84,6 +86,17 @@ [% END %] </table> +<p> + When [% terms.abug %] is marked as a duplicate of another one or is moved + to another installation, the [% terms.bug %] status is automatically set to + <b>[% Param("duplicate_or_move_bug_status") FILTER html %]</b>. All transitions to + this [% terms.bug %] status must then be valid (this is the reason why you cannot edit + them above).<br> + Note: you can change this setting by visiting the + <a href="editparams.cgi?section=bugchange#duplicate_or_move_bug_status">Parameters</a> + page and editing the <i>duplicate_or_move_bug_status</i> parameter. +</p> + <p align="center"> <input type="hidden" name="action" value="update"> <input type="hidden" name="token" value="[% token FILTER html %]"> diff --git a/template/en/default/bug/knob.html.tmpl b/template/en/default/bug/knob.html.tmpl index 99aed9c22..257ce4d94 100644 --- a/template/en/default/bug/knob.html.tmpl +++ b/template/en/default/bug/knob.html.tmpl @@ -39,6 +39,7 @@ [% NEXT IF !bug.isopened && (bug.everconfirmed && bug_status.name == "UNCONFIRMED" || !bug.everconfirmed && bug_status.name == "REOPENED") %] [% PROCESS initial_action %] + [% NEXT IF bug_status.name == bug.bug_status %] <input type="radio" id="knob_[% bug_status.id FILTER html %]" name="knob" value="[% bug_status.name FILTER html %]"> <label for="knob_[% bug_status.id FILTER html %]"> |