Commit 2bd57ce8 authored by lpsolit%gmail.com's avatar lpsolit%gmail.com

Bug 344965: Fix process_bug.cgi and bug/* templates to work with custom bug…

Bug 344965: Fix process_bug.cgi and bug/* templates to work with custom bug status workflow - Patch by Fré©ric Buclin <LpSolit@gmail.com> r=mkanat a=LpSolit
parent 9e81bb03
......@@ -120,8 +120,6 @@ use File::Basename;
FIELD_TYPE_FREETEXT
FIELD_TYPE_SINGLE_SELECT
BUG_STATE_OPEN
USAGE_MODE_BROWSER
USAGE_MODE_CMDLINE
USAGE_MODE_WEBSERVICE
......@@ -351,10 +349,6 @@ use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https',
'irc', 'mid', 'news', 'nntp', 'prospero', 'telnet',
'view-source', 'wais');
# States that are considered to be "open" for bugs.
use constant BUG_STATE_OPEN => ('NEW', 'REOPENED', 'ASSIGNED',
'UNCONFIRMED');
# Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode.
use constant USAGE_MODE_BROWSER => 0;
use constant USAGE_MODE_CMDLINE => 1;
......
......@@ -22,7 +22,7 @@ package Bugzilla::Install::DB;
use strict;
use Bugzilla::Bug qw(is_open_state);
use Bugzilla::Bug qw(BUG_STATE_OPEN is_open_state);
use Bugzilla::Constants;
use Bugzilla::Hook;
use Bugzilla::Install::Util qw(indicate_progress);
......
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Frédéric Buclin.
# Portions created by Frédéric Buclin are Copyright (C) 2007
# Frédéric Buclin. All Rights Reserved.
#
# Contributor(s): Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Status;
use base qw(Bugzilla::Object);
################################
##### Initialization #####
################################
use constant DB_TABLE => 'bug_status';
use constant DB_COLUMNS => qw(
id
value
sortkey
isactive
is_open
);
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'sortkey, value';
###############################
##### Accessors ####
###############################
sub name { return $_[0]->{'value'}; }
sub sortkey { return $_[0]->{'sortkey'}; }
sub is_active { return $_[0]->{'isactive'}; }
sub is_open { return $_[0]->{'is_open'}; }
###############################
##### Methods ####
###############################
sub can_change_to {
my $self = shift;
my $dbh = Bugzilla->dbh;
if (!defined $self->{'can_change_to'}) {
my $new_status_ids = $dbh->selectcol_arrayref('SELECT new_status
FROM status_workflow
INNER JOIN bug_status
ON id = new_status
WHERE isactive = 1
AND old_status = ?',
undef, $self->id);
$self->{'can_change_to'} = Bugzilla::Status->new_from_list($new_status_ids);
}
return $self->{'can_change_to'};
}
1;
__END__
=head1 NAME
Bugzilla::Status - Bug status class.
=head1 SYNOPSIS
use Bugzilla::Status;
my $bug_status = new Bugzilla::Status({name => 'ASSIGNED'});
my $bug_status = new Bugzilla::Status(4);
=head1 DESCRIPTION
Status.pm represents a bug status object. It is an implementation
of L<Bugzilla::Object>, and thus provides all methods that
L<Bugzilla::Object> provides.
The methods that are specific to C<Bugzilla::Status> are listed
below.
=head1 METHODS
=over
=item C<can_change_to>
Description: Returns the list of active statuses a bug can be changed to
given the current bug status.
Params: none.
Returns: A list of Bugzilla::Status objects.
=back
=cut
......@@ -1139,8 +1139,26 @@ if ($dotweak) {
$vars->{'unconfirmedstate'} = 'UNCONFIRMED';
$vars->{'bugstatuses'} = [ keys %$bugstatuses ];
# Convert bug statuses to their ID.
my @bug_statuses = map {$dbh->quote($_)} keys %$bugstatuses;
my $bug_status_ids =
$dbh->selectcol_arrayref('SELECT id FROM bug_status
WHERE value IN (' . join(', ', @bug_statuses) .')');
# This query collects new statuses which are common to all current bug statuses.
# It also accepts transitions where the bug status doesn't change.
$bug_status_ids =
$dbh->selectcol_arrayref('SELECT DISTINCT new_status
FROM status_workflow sw1
WHERE NOT EXISTS (SELECT * FROM status_workflow sw2
WHERE sw2.old_status != sw1.new_status
AND sw2.old_status IN (' . join(', ', @$bug_status_ids) . ')
AND NOT EXISTS (SELECT * FROM status_workflow sw3
WHERE sw3.new_status = sw1.new_status
AND sw3.old_status = sw2.old_status))');
$vars->{'current_bug_statuses'} = [keys %$bugstatuses];
$vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids);
# The groups to which the user belongs.
$vars->{'groups'} = GetGroups();
......
......@@ -27,6 +27,7 @@ use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Token;
use Bugzilla::Status;
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
......@@ -42,12 +43,6 @@ $user->in_group('admin')
my $action = $cgi->param('action') || 'edit';
my $token = $cgi->param('token');
sub get_statuses {
my $statuses = $dbh->selectall_arrayref('SELECT id, value, is_open FROM bug_status
ORDER BY sortkey, value', { Slice => {} });
return $statuses;
}
sub get_workflow {
my $workflow = $dbh->selectall_arrayref('SELECT old_status, new_status, require_comment
FROM status_workflow');
......@@ -64,7 +59,7 @@ sub load_template {
my $template = Bugzilla->template;
my $vars = {};
$vars->{'statuses'} = get_statuses();
$vars->{'statuses'} = [Bugzilla::Status->get_all];
$vars->{'workflow'} = get_workflow();
$vars->{'token'} = issue_session_token("workflow_$filename");
$vars->{'message'} = $message;
......@@ -79,9 +74,8 @@ if ($action eq 'edit') {
}
elsif ($action eq 'update') {
check_token_data($token, 'workflow_edit');
my $statuses = get_statuses;
my $statuses = [Bugzilla::Status->get_all];
my $workflow = get_workflow();
my $initial_state = {id => 0};
my $sth_insert = $dbh->prepare('INSERT INTO status_workflow (old_status, new_status)
VALUES (?, ?)');
......@@ -90,22 +84,28 @@ elsif ($action eq 'update') {
my $sth_delnul = $dbh->prepare('DELETE FROM status_workflow
WHERE old_status IS NULL AND new_status = ?');
foreach my $old ($initial_state, @$statuses) {
# Hashes cannot have undef as a key, so we use 0. But the DB
# must store undef, for referential integrity.
my $old_id_for_db = $old->{'id'} || undef;
# Part 1: Initial bug statuses.
foreach my $new (@$statuses) {
if ($cgi->param('w_0_' . $new->id)) {
$sth_insert->execute(undef, $new->id)
unless defined $workflow->{0}->{$new->id};
}
else {
$sth_delnul->execute($new->id);
}
}
# Part 2: Bug status changes.
foreach my $old (@$statuses) {
foreach my $new (@$statuses) {
next if $old->{'id'} == $new->{'id'};
next if $old->id == $new->id;
if ($cgi->param('w_' . $old->{'id'} . '_' . $new->{'id'})) {
$sth_insert->execute($old_id_for_db, $new->{'id'})
unless defined $workflow->{$old->{'id'}}->{$new->{'id'}};
}
elsif ($old_id_for_db) {
$sth_delete->execute($old_id_for_db, $new->{'id'});
if ($cgi->param('w_' . $old->id . '_' . $new->id)) {
$sth_insert->execute($old->id, $new->id)
unless defined $workflow->{$old->id}->{$new->id};
}
else {
$sth_delnul->execute($new->{'id'});
$sth_delete->execute($old->id, $new->id);
}
}
}
......
......@@ -43,6 +43,7 @@ use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Bug;
eval "use GD";
$@ && ThrowCodeError("gd_not_installed");
......
......@@ -32,6 +32,7 @@ use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Bug;
###########################################################################
# General subs
......
......@@ -50,24 +50,24 @@
<th>&nbsp;</th>
[% FOREACH status = statuses %]
<th class="col-header[% status.is_open ? " open-status" : " closed-status" %]">
[% status.value FILTER html %]
[% status.name FILTER html %]
</th>
[% END %]
</tr>
[%# This defines the entry point in the workflow %]
[% p = [{id => 0, value => "{Start}", is_open => 1}] %]
[% p = [{id => 0, name => "{Start}", is_open => 1}] %]
[% FOREACH status = p.merge(statuses) %]
<tr class="highlight">
<th align="right" class="[% status.is_open ? "open-status" : "closed-status" %]">
[% status.value FILTER html %]
[% status.name FILTER html %]
</th>
[% FOREACH new_status = statuses %]
[% IF workflow.${status.id}.${new_status.id}.defined %]
<td align="center" class="checkbox-cell
[% " checked" IF workflow.${status.id}.${new_status.id} %]"
title="From [% status.value FILTER html %] to [% new_status.value FILTER html %]">
title="From [% status.name FILTER html %] to [% new_status.name FILTER html %]">
<input type="checkbox" name="c_[% status.id %]_[% new_status.id %]"
id="c_[% status.id %]_[% new_status.id %]" onclick="toggle_cell(this)"
[% " checked='checked'" IF workflow.${status.id}.${new_status.id} %]>
......
......@@ -34,8 +34,12 @@
</script>
<p>
This page allows you to define which status transitions are valid
in your workflow.
This page allows you to define which status transitions are valid in your workflow.
For compatibility with older versions of [% terms.Bugzilla %], reopening [% terms.abug %]
will only display either UNCONFIRMED or REOPENED (if allowed by your workflow) but not
both. The decision depends on whether the [% terms.bug %] has ever been confirmed or not.
So it is a good idea to allow both transitions and let [% terms.Bugzilla %] select the
correct one.
</p>
<form id="workflow_form" method="POST" action="editworkflow.cgi">
......@@ -50,24 +54,24 @@
<th>&nbsp;</th>
[% FOREACH status = statuses %]
<th class="col-header[% status.is_open ? " open-status" : " closed-status" %]">
[% status.value FILTER html %]
[% status.name FILTER html %]
</th>
[% END %]
</tr>
[%# This defines the entry point in the workflow %]
[% p = [{id => 0, value => "{Start}", is_open => 1}] %]
[% p = [{id => 0, name => "{Start}", is_open => 1}] %]
[% FOREACH status = p.merge(statuses) %]
<tr class="highlight">
<th align="right" class="[% status.is_open ? "open-status" : "closed-status" %]">
[% status.value FILTER html %]
[% status.name FILTER html %]
</th>
[% 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 %]"
title="From [% status.value FILTER html %] to [% new_status.value FILTER html %]">
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 %]>
......
......@@ -595,7 +595,7 @@
<td align="right">
<b><a href="page.cgi?id=fields.html#status">Status</a></b>:
</td>
<td>[% status_descs.${bug.bug_status} FILTER html %]</td>
<td>[% get_status(bug.bug_status) FILTER html %]</td>
</tr>
<tr>
......
......@@ -18,42 +18,39 @@
#
# Contributor(s): Gervase Markham <gerv@gerv.net>
# Vaskin Kissoyan <vkissoyan@yahoo.com>
# Frédéric Buclin <LpSolit@gmail.com>
#%]
[% PROCESS global/variables.none.tmpl %]
[%# *** Knob *** %]
<br>
<div id="knob">
<div id="knob-options">
[% knum = 1 %]
[% initial_action_shown = 0 %]
[% IF bug.isunconfirmed && bug.user.canconfirm %]
[% PROCESS initial_action %]
<input type="radio" id="knob-confirm" name="knob" value="confirm">
<label for="knob-confirm">
Confirm [% terms.bug %] (change status to <b>[% get_status("NEW") FILTER html %]</b>)
</label>
<br>
[% knum = knum + 1 %]
[% END %]
[% IF bug.isopened && bug.bug_status != "ASSIGNED" && bug.user.canedit
&& (!bug.isunconfirmed || bug.user.canconfirm) %]
[%# These actions are based on the current custom workflow. %]
[% FOREACH bug_status = bug.status.can_change_to %]
[% NEXT IF bug.isunconfirmed && bug_status.is_open && !bug.user.canconfirm %]
[% NEXT IF bug.isopened && !bug.isunconfirmed && bug_status.is_open && !bug.user.canedit %]
[% NEXT IF !bug_status.is_open && !bug.user.canedit && !bug.user.isreporter %]
[% NEXT IF !bug_status.is_open && bug_status.is_open && !bug.user.canedit && !bug.user.isreporter %]
[%# Special hack to only display UNCO or REOP when reopening, but not both;
# for compatibility with older versions. %]
[% NEXT IF !bug.isopened && (bug.everconfirmed && bug_status.name == "UNCONFIRMED"
|| !bug.everconfirmed && bug_status.name == "REOPENED") %]
[% PROCESS initial_action %]
<input type="radio" id="knob-accept" name="knob" value="accept">
<label for="knob-accept">
Accept [% terms.bug %] (
[% IF bug.isunconfirmed %]confirm [% terms.bug %], and [% END %]change
status to <b>[% get_status("ASSIGNED") FILTER html %]</b>)
<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 %]">
Change status to <b>[% get_status(bug_status.name) FILTER html %]</b>
</label>
[% IF bug.isopened && !bug_status.is_open %]
and set the resolution to [% PROCESS select_resolution field = "knob_${bug_status.id}" %]
[% END %]
<br>
[% knum = knum + 1 %]
[% END %]
[%# These actions are special and are independent of the workflow. %]
[% IF bug.user.canedit || bug.user.isreporter %]
[% IF bug.isopened %]
[% IF bug.resolution %]
......@@ -64,65 +61,27 @@
<b>[% get_resolution(bug.resolution) FILTER html %]</b>)
</label>
<br>
[% knum = knum + 1 %]
[% END %]
[% PROCESS initial_action %]
<input type="radio" id="knob-resolve" name="knob" value="resolve">
<label for="knob-resolve">
Resolve [% terms.bug %], changing
<a href="page.cgi?id=fields.html#resolution">resolution</a> to
</label>
[% PROCESS select_resolution %]
[% PROCESS duplicate %]
[% ELSE %]
[% IF bug.resolution != "MOVED" ||
(bug.resolution == "MOVED" && bug.user.canmove) %]
[% IF bug.resolution != "MOVED" || bug.user.canmove %]
[% PROCESS initial_action %]
<input type="radio" id="knob-change-resolution" name="knob" value="change_resolution">
<label for="knob-change-resolution">
<input type="radio" id="knob_change_resolution" name="knob" value="change_resolution">
<label for="knob_change_resolution">
Change <a href="page.cgi?id=fields.html#resolution">resolution</a> to
</label>
[% PROCESS select_resolution %]
[% PROCESS duplicate %]
<input type="radio" id="knob-reopen" name="knob" value="reopen">
<label for="knob-reopen">
Reopen [% terms.bug %]
</label>
<br>
[% knum = knum + 1 %]
[% END %]
[% IF bug.bug_status == "RESOLVED" %]
[% PROCESS initial_action %]
<input type="radio" id="knob-verify" name="knob" value="verify">
<label for="knob-verify">
Mark [% terms.bug %] as <b>[% get_status("VERIFIED") FILTER html %]</b>
</label>
[% PROCESS select_resolution field = "knob_change_resolution" %]
<br>
[% knum = knum + 1 %]
[% END %]
[% IF bug.bug_status != "CLOSED" %]
[% PROCESS initial_action %]
<input type="radio" id="knob-close" name="knob" value="close">
<label for="knob-close">
Mark [% terms.bug %] as <b>[% get_status("CLOSED") FILTER html %]</b>
</label>
<br>
[% knum = knum + 1 %]
[% END %]
[% END %]
[% PROCESS duplicate %]
[% END %]
</div>
<div id="knob-buttons">
<input type="submit" value="Commit" id="commit">
<input type="submit" value="Commit" id="commit">
[% IF bug.user.canmove %]
&nbsp; <font size="+1"><b> | </b></font> &nbsp;
<input type="submit" name="action" id="action"
value="[% Param("move-button-text") %]">
<input type="submit" name="action" id="action" value="[% Param("move-button-text") %]">
[% END %]
</div>
</div>
......@@ -143,23 +102,20 @@
[% END %]
[% BLOCK select_resolution %]
<select name="resolution"
onchange="document.changeform.knob[[% knum %]].checked=true">
<select name="resolution_[% field FILTER html %]"
onchange="document.forms['changeform'].[% field FILTER html %].checked=true">
[% FOREACH r = bug.choices.resolution %]
<option value="[% r FILTER html %]">[% get_resolution(r) FILTER html %]</option>
[% END %]
</select>
<br>
[% knum = knum + 1 %]
[% END %]
[% BLOCK duplicate %]
<input type="radio" id="knob-duplicate" name="knob" value="duplicate">
<label for="knob-duplicate">
<input type="radio" id="knob_duplicate" name="knob" value="duplicate">
<label for="knob_duplicate">
Mark the [% terms.bug %] as duplicate of [% terms.bug %] #
</label>
<input name="dup_id" size="6"
onchange="if (this.value != '') {document.changeform.knob[[% knum %]].checked=true}">
onchange="if (this.value != '') {document.forms['changeform'].knob_duplicate.checked=true}">
<br>
[% knum = knum + 1 %]
[% END %]
......@@ -191,8 +191,7 @@
'list/edit-multiple.html.tmpl' => [
'group.id',
'knum',
'menuname',
'menuname',
],
'list/list.rdf.tmpl' => [
......@@ -319,10 +318,6 @@
'flag.status',
],
'bug/knob.html.tmpl' => [
'knum',
],
'bug/navigate.html.tmpl' => [
'bug_list.first',
'bug_list.last',
......
......@@ -243,7 +243,13 @@
[% ELSIF error == "comment_required" %]
[% title = "Comment Required" %]
You have to specify a <b>comment</b> on this change.
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 %].
[% ELSE %]
on this change.
[% END %]
Please explain your change.
[% ELSIF error == "comment_too_long" %]
......@@ -633,7 +639,12 @@
[% title = "Your Search Makes No Sense" %]
The only legal values for the <em>Attachment is patch</em> field are
0 and 1.
[% ELSIF error == "illegal_bug_status_transition" %]
[% title = "Illegal $terms.Bug Status Change" %]
You are not allowed to change the [% terms.bug %] status from
[%+ old.join(", ") FILTER html %] to [%+ new FILTER html %].
[% ELSIF error == "illegal_change" %]
[% title = "Not allowed" %]
You tried to change the
......
......@@ -18,6 +18,7 @@
#
# Contributor(s): Myk Melez <myk@mozilla.org>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Frédéric Buclin <LpSolit@gmail.com>
#%]
[% PROCESS global/variables.none.tmpl %]
......@@ -301,66 +302,25 @@
[% knum = 0 %]
<input id="knob-none" type="radio" name="knob" value="none" checked="checked">
<label for="knob-none">Do nothing else</label><br>
[% IF bugstatuses.size == 1 && bugstatuses.0 == unconfirmedstate %]
[% knum = knum + 1 %]
<input id="knob-confirm" type="radio" name="knob" value="confirm">
<label for="knob-confirm">
Confirm [% terms.bugs %] (change status to <b>[% get_status("NEW") FILTER html %]</b>)
</label><br>
[% FOREACH bug_status = new_bug_statuses %]
<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 %]">
Change status to <b>[% get_status(bug_status.name) FILTER html %]</b>
</label>
[% IF !bug_status.is_open %]
and set the resolution to [% PROCESS select_resolution field = "knob_${bug_status.id}" %]
[% END %]
<br>
[% END %]
[%# If all the bugs being changed are open, allow the user to accept them,
clear their resolution or resolve them. %]
[% IF !bugstatuses.containsany(closedstates) %]
[% knum = knum + 1 %]
<input id="knob-accept" type="radio" name="knob" value="accept">
<label for="knob-accept">
Accept [% terms.bugs %] (change status to <b>[% get_status("ASSIGNED") FILTER html %]</b>)
</label><br>
[% knum = knum + 1 %]
[%# If all the bugs being changed are open, allow the user to clear their resolution. %]
[% IF !current_bug_statuses.containsany(closedstates) %]
<input id="knob-clearresolution" type="radio" name="knob" value="clearresolution">
<label for="knob-clearresolution">Clear the resolution</label><br>
[% knum = knum + 1 %]
<input id="knob-resolve" type="radio" name="knob" value="resolve">
<label for="knob-resolve">
Resolve [% terms.bugs %], changing <a href="page.cgi?id=fields.html#resolution">resolution</a> to
</label>
<select name="resolution" onchange="document.forms.changeform.knob[[% knum %]].checked=true">
[% FOREACH resolution = resolutions %]
[% NEXT IF !resolution %]
<option value="[% resolution FILTER html %]">
[% get_resolution(resolution) FILTER html %]
</option>
[% END %]
</select><br>
[% END %]
[%# If all the bugs are closed, allow the user to reopen them. %]
[% IF !bugstatuses.containsany(openstates) %]
[% knum = knum + 1 %]
<input id="knob-reopen" type="radio" name="knob" value="reopen">
<label for="knob-reopen">Reopen [% terms.bugs %]</label><br>
[% END %]
[% IF bugstatuses.size == 1 %]
[% IF bugstatuses.contains('RESOLVED') %]
[% knum = knum + 1 %]
<input id="knob-verify" type="radio" name="knob" value="verify">
<label for="knob-verify">Mark [% terms.bugs %] as <b>[% get_status("VERIFIED") FILTER html %]</b></label><br>
[% END %]
[% END %]
[% IF !bugstatuses.containsany(openstates) AND !bugstatuses.contains('CLOSED') %]
[% knum = knum + 1 %]
<input id="knob-close" type="radio" name="knob" value="close">
<label for="knob-close">Mark [% terms.bugs %] as <b>[% get_status("CLOSED") FILTER html %]</b></label><br>
[% END %]
<input type="submit" id="commit" value="Commit">
......@@ -384,3 +344,13 @@
[% END %]
</select>
[% END %]
[% BLOCK select_resolution %]
<select name="resolution"
onchange="document.forms['changeform'].[% field FILTER html %].checked=true">
[% FOREACH r = resolutions %]
[% NEXT IF !r %]
<option value="[% r FILTER html %]">[% get_resolution(r) FILTER html %]</option>
[% END %]
</select>
[% 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