Skip to content

Commit

Permalink
Bug 1151745: add ui to minimise steps required to move bugs between p…
Browse files Browse the repository at this point in the history
…roducts
  • Loading branch information
globau committed Apr 30, 2015
1 parent 0d15453 commit dac9873
Show file tree
Hide file tree
Showing 11 changed files with 591 additions and 82 deletions.
72 changes: 50 additions & 22 deletions Bugzilla/Bug.pm
Expand Up @@ -52,7 +52,7 @@ use Bugzilla::Comment;
use Bugzilla::BugUrl;
use Bugzilla::BugUserLastVisit;

use List::MoreUtils qw(firstidx uniq part);
use List::MoreUtils qw(firstidx uniq part any);
use List::Util qw(min max first);
use Storable qw(dclone);
use URI;
Expand Down Expand Up @@ -2689,7 +2689,32 @@ sub _set_product {
# other part of Bugzilla that checks $@.
undef $@;
Bugzilla->error_mode($old_error_mode);


my $invalid_groups;
my @idlist = ($self->id);
push(@idlist, map { $_->id } @{ $params->{other_bugs} })
if $params->{other_bugs};
@idlist = uniq @idlist;

# BMO - if everything is ok then we can skip the verfication page
if ($component_ok && $version_ok && $milestone_ok) {
$invalid_groups = $self->get_invalid_groups({ bug_ids => \@idlist, product => $product });
my $has_invalid_group = 0;
foreach my $group (@$invalid_groups) {
if (any { $_ eq $group->name } @{ $params->{groups}->{add} }) {
$has_invalid_group = 1;
last;
}
}
$params->{product_change_confirmed} =
# always check for invalid groups
!$has_invalid_group
# never skip verification when changing multiple bugs
&& scalar(@idlist) == 1
# ensure the user has seen the group ui for private bugs
&& (!@{ $self->groups_in } || Bugzilla->input_params->{group_verified});
}

my $verified = $params->{product_change_confirmed};
my %vars;
if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) {
Expand All @@ -2709,27 +2734,9 @@ sub _set_product {

if (!$verified) {
$vars{verify_bug_groups} = 1;
my $dbh = Bugzilla->dbh;
my @idlist = ($self->id);
push(@idlist, map {$_->id} @{ $params->{other_bugs} })
if $params->{other_bugs};
# Get the ID of groups which are no longer valid in the new product.
my $gids = $dbh->selectcol_arrayref(
'SELECT bgm.group_id
FROM bug_group_map AS bgm
WHERE bgm.bug_id IN (' . join(',', ('?') x @idlist) . ')
AND bgm.group_id NOT IN
(SELECT gcm.group_id
FROM group_control_map AS gcm
WHERE gcm.product_id = ?
AND ( (gcm.membercontrol != ?
AND gcm.group_id IN ('
. Bugzilla->user->groups_as_string . '))
OR gcm.othercontrol != ?) )',
undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA));
$vars{'old_groups'} = Bugzilla::Group->new_from_list($gids);
$vars{old_groups} = $invalid_groups || $self->get_invalid_groups({ bug_ids => \@idlist, product => $product });
}

if (%vars) {
$vars{product} = $product;
$vars{bug} = $self;
Expand Down Expand Up @@ -4301,6 +4308,27 @@ sub map_fields {
return \%field_values;
}

# Return the groups which are no longer valid in the specified product
sub get_invalid_groups {
my ($invocant, $params) = @_;
my @idlist = @{ $params->{bug_ids} };
my $product = $params->{product};
my $gids = Bugzilla->dbh->selectcol_arrayref(
'SELECT bgm.group_id
FROM bug_group_map AS bgm
WHERE bgm.bug_id IN (' . join(',', ('?') x @idlist) . ')
AND bgm.group_id NOT IN
(SELECT gcm.group_id
FROM group_control_map AS gcm
WHERE gcm.product_id = ?
AND ( (gcm.membercontrol != ?
AND gcm.group_id IN ('
. Bugzilla->user->groups_as_string . '))
OR gcm.othercontrol != ?) )',
undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA));
return Bugzilla::Group->new_from_list($gids);
}

################################################################################
# check_can_change_field() defines what users are allowed to change. You
# can add code here for site-specific policy changes, according to the
Expand Down
232 changes: 210 additions & 22 deletions extensions/BugModal/lib/WebService.pm
Expand Up @@ -15,9 +15,12 @@ use Bugzilla::Bug;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Field;
use Bugzilla::Group;
use Bugzilla::Keyword;
use Bugzilla::Milestone;
use Bugzilla::Product;
use Bugzilla::Version;
use List::MoreUtils qw(any first_value);

# these methods are much lighter than our public API calls

Expand All @@ -33,6 +36,7 @@ sub rest_resources {
},
},
},

# returns pre-formatted html, enabling reuse of the user template
qr{^/bug_modal/cc/(\d+)$}, {
GET => {
Expand All @@ -42,6 +46,18 @@ sub rest_resources {
},
},
},

# returns fields that require touching when the product is changed
qw{^/bug_modal/new_product/(\d+)$}, {
GET => {
method => 'new_product',
params => sub {
# products with slashes in their name means we have to grab
# the product from the query-string instead of the path
return { id => $_[0], product_name => Bugzilla->input_params->{product} }
},
},
},
]
}

Expand All @@ -59,15 +75,15 @@ sub edit {
unless (grep { $_->id == $bug->product_id } @products) {
unshift @products, $bug->product_obj;
}
$options{product} = [ map { { name => $_->name, description => $_->description } } @products ];
$options{product} = [ map { { name => $_->name } } @products ];

$options{component} = _name_desc($bug->component, $bug->product_obj->components);
$options{version} = _name($bug->version, $bug->product_obj->versions);
$options{target_milestone} = _name($bug->target_milestone, $bug->product_obj->milestones);
$options{priority} = _name($bug->priority, 'priority');
$options{bug_severity} = _name($bug->bug_severity, 'bug_severity');
$options{rep_platform} = _name($bug->rep_platform, 'rep_platform');
$options{op_sys} = _name($bug->op_sys, 'op_sys');
$options{component} = _name($bug->product_obj->components, $bug->component);
$options{version} = _name($bug->product_obj->versions, $bug->version);
$options{target_milestone} = _name($bug->product_obj->milestones, $bug->target_milestone);
$options{priority} = _name('priority', $bug->priority);
$options{bug_severity} = _name('bug_severity', $bug->bug_severity);
$options{rep_platform} = _name('rep_platform', $bug->rep_platform);
$options{op_sys} = _name('op_sys', $bug->op_sys);

# custom select fields
my @custom_fields =
Expand All @@ -93,27 +109,15 @@ sub edit {
}

sub _name {
my ($current, $values) = @_;
my ($values, $current) = @_;
# values can either be an array-ref of values, or a field name, which
# result in that field's legal-values being used.
if (!ref($values)) {
$values = Bugzilla::Field->new({ name => $values, cache => 1 })->legal_values;
}
return [
map { { name => $_->name } }
grep { $_->name eq $current || $_->is_active }
@$values
];
}

sub _name_desc {
my ($current, $values) = @_;
if (!ref($values)) {
$values = Bugzilla::Field->new({ name => $values, cache => 1 })->legal_values;
}
return [
map { { name => $_->name, description => $_->description } }
grep { $_->name eq $current || $_->is_active }
grep { (defined $current && $_->name eq $current) || $_->is_active }
@$values
];
}
Expand All @@ -135,4 +139,188 @@ sub cc {
return { html => $html };
}

sub new_product {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
my $bug = Bugzilla::Bug->check({ id => $params->{id} });
my $product = Bugzilla::Product->check({ name => $params->{product_name}, cache => 1 });
my $true = $self->type('boolean', 1);
my %result;

# components

my $components = _name($product->components);
my $current_component = $bug->component;
if (my $component = first_value { $_->{name} eq $current_component} @$components) {
# identical component in both products
$component->{selected} = $true;
}
else {
# default to a blank value
unshift @$components, {
name => '',
selected => $true,
};
}
$result{component} = $components;

# milestones

my $milestones = _name($product->milestones);
my $current_milestone = $bug->target_milestone;
if ($bug->check_can_change_field('target_milestone', 0, 1)
&& (my $milestone = first_value { $_->{name} eq $current_milestone} @$milestones))
{
# identical milestone in both products
$milestone->{selected} = $true;
}
else {
# use default milestone
my $default_milestone = $product->default_milestone;
my $milestone = first_value { $_->{name} eq $default_milestone } @$milestones;
$milestone->{selected} = $true;
}
$result{target_milestone} = $milestones;

# versions

my $versions = _name($product->versions);
my $current_version = $bug->version;
my $selected_version;
if (my $version = first_value { $_->{name} eq $current_version } @$versions) {
# identical version in both products
$version->{selected} = $true;
$selected_version = $version;
}
elsif (
$current_version =~ /^(\d+) Branch$/
|| $current_version =~ /^Firefox (\d+)$/
|| $current_version =~ /^(\d+)$/)
{
# firefox, with its three version naming schemes
my $branch = $1;
foreach my $test_version ("$branch Branch", "Firefox $branch", $branch) {
if (my $version = first_value { $_->{name} eq $test_version } @$versions) {
$version->{selected} = $true;
$selected_version = $version;
last;
}
}
}
if (!$selected_version) {
# "unspecified", "other"
foreach my $test_version ("unspecified", "other") {
if (my $version = first_value { lc($_->{name}) eq $test_version } @$versions) {
$version->{selected} = $true;
$selected_version = $version;
last;
}
}
}
if (!$selected_version) {
# default to a blank value
unshift @$versions, {
name => '',
selected => $true,
};
}
$result{version} = $versions;

# groups

my @groups;

# find invalid groups
push @groups,
map {{
type => 'invalid',
group => $_,
checked => 0,
}}
@{ Bugzilla::Bug->get_invalid_groups({ bug_ids => [ $bug->id ], product => $product }) };

# logic lifted from bug/process/verify-new-product.html.tmpl
my $current_groups = $bug->groups_in;
my $group_controls = $product->group_controls;
foreach my $group_id (keys %$group_controls) {
my $group_control = $group_controls->{$group_id};
if ($group_control->{membercontrol} == CONTROLMAPMANDATORY
|| ($group_control->{othercontrol} == CONTROLMAPMANDATORY && !$user->in_group($group_control->{name})))
{
# mandatory, always checked
push @groups, {
type => 'mandatory',
group => $group_control->{group},
checked => 1,
};
}
elsif (
($group_control->{membercontrol} != CONTROLMAPNA && $user->in_group($group_control->{name}))
|| $group_control->{othercontrol} != CONTROLMAPNA)
{
# optional, checked if..
my $group = $group_control->{group};
my $checked =
# same group as current product
(any { $_->id == $group->id } @$current_groups)
# member default
|| $group_control->{membercontrol} == CONTROLMAPDEFAULT && $user->in_group($group_control->{name})
# or other default
|| $group_control->{othercontrol} == CONTROLMAPDEFAULT && !$user->in_group($group_control->{name})
;
push @groups, {
type => 'optional',
group => $group_control->{group},
checked => $checked || 0,
};
}
}

my $default_group_name = $product->default_security_group;
if (my $default_group = first_value { $_->{group}->name eq $default_group_name } @groups) {
# because we always allow the default product group to be selected, it's never invalid
$default_group->{type} = 'optional' if $default_group->{type} eq 'invalid';
}
else {
# add the product's default group if it's missing
unshift @groups, {
type => 'optional',
group => $product->default_security_group_obj,
checked => 0,
};
}

# if the bug is currently in a group, ensure a group is checked by default
# by checking the product's default group if no other groups apply
if (@$current_groups && !any { $_->{checked} } @groups) {
foreach my $g (@groups) {
next unless $g->{group}->name eq $default_group_name;
$g->{checked} = 1;
last;
}
}

# group by type and flatten
my $vars = {
product => $product,
groups => { invalid => [], mandatory => [], optional => [] },
};
foreach my $g (@groups) {
push @{ $vars->{groups}->{$g->{type}} }, {
id => $g->{group}->id,
name => $g->{group}->name,
description => $g->{group}->description,
checked => $g->{checked},
};
}

# build group selection html
my $template = Bugzilla->template;
$template->process('bug_modal/new_product_groups.html.tmpl', $vars, \$result{groups})
|| ThrowTemplateError($template->error);

return \%result;
}

1;

0 comments on commit dac9873

Please sign in to comment.