diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 6dbcffe348..78bb1dff48 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -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; @@ -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) { @@ -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; @@ -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 diff --git a/extensions/BugModal/lib/WebService.pm b/extensions/BugModal/lib/WebService.pm index 4c8b6b001b..5a1ec15c0e 100644 --- a/extensions/BugModal/lib/WebService.pm +++ b/extensions/BugModal/lib/WebService.pm @@ -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 @@ -33,6 +36,7 @@ sub rest_resources { }, }, }, + # returns pre-formatted html, enabling reuse of the user template qr{^/bug_modal/cc/(\d+)$}, { GET => { @@ -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} } + }, + }, + }, ] } @@ -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 = @@ -93,7 +109,7 @@ 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)) { @@ -101,19 +117,7 @@ sub _name { } 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 ]; } @@ -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; diff --git a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl index da7f2f2946..63bd72dc67 100644 --- a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl +++ b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl @@ -233,7 +233,7 @@ Fetching - + [% END %]
@@ -265,26 +265,50 @@ [% WRAPPER fields_lhs %] [%# product %] + [% can_edit_product = bug.check_can_change_field("product", 0, 1) %] [% WRAPPER bug_modal/field.html.tmpl field = bug_fields.product field_type = constants.FIELD_TYPE_SINGLE_SELECT + hide_on_edit = can_edit_product %] +
- [% bug.product FILTER html %]
[% END %] + [% WRAPPER bug_modal/field.html.tmpl + field = bug_fields.product + field_type = constants.FIELD_TYPE_SINGLE_SELECT + hide_on_view = 1 + hide_on_edit = !can_edit_product + append_content = 1 + %] + + [% PROCESS prodcompsearch/form.html.tmpl + id = "pcs" + custom_select = 1 + hidden = 1 + throbber = "product-throbber" + %] + + + + + + [% END %] [%# component %] [% WRAPPER bug_modal/field.html.tmpl field = bug_fields.component field_type = constants.FIELD_TYPE_SINGLE_SELECT %] +
- [% bug.component FILTER html %]