diff options
Diffstat (limited to 'extensions/Releases/lib/Components.pm')
-rw-r--r-- | extensions/Releases/lib/Components.pm | 701 |
1 files changed, 701 insertions, 0 deletions
diff --git a/extensions/Releases/lib/Components.pm b/extensions/Releases/lib/Components.pm new file mode 100644 index 000000000..96e967838 --- /dev/null +++ b/extensions/Releases/lib/Components.pm @@ -0,0 +1,701 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +=pod + +=head1 NAME + +Bugzilla::Extension::Releases::Components - Interface to Red Hat Bugzilla Release Components + +=head1 SYNOPSIS + + # Create basic Release Components object + my $rc = Bugzilla::Extension::Releases::Components->new() + + # Get a list of current components + $rc->get_release_components({ release => $release }); + + # Set new values for the current list of components + $rc->set_release_components($data_ref); + + # Get full list of components visible for a release (flag) + my $components = $rc->get_visible_components($release); + +=head1 DESCRIPTION + +Bugzilla::Extension::Releases::Components contains a set of functions which help +maintain the release component lists used for RHEL process management. +The release components consists of a list of components that are +part of a approved component list as well as components that are +part of a capacity priority list. These lists help to determine +which bugs will be included in an upcoming update if the bugs are +reported against a component that has been approved. + +=cut + +package Bugzilla::Extension::Releases::Components; + +use strict; +use warnings; +use 5.10.1; + +############################################################################ +# Module Initialization +############################################################################ + +use Bugzilla::Constants; +use Bugzilla::Util; # trim +use Bugzilla::Error; +use Bugzilla::Extension::Releases::Release; +use Bugzilla::Extension::Releases::Util; # for log_activity + +use List::MoreUtils qw(any); + +=pod + +=head1 METHODS + +=over + +=item C<new> + +=item B<Description> + +Creates a new Release Component object. + +=item B<Params> + +=item B<Returns> + +=cut + +sub new { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $params = shift; + my $self = {}; + bless($self, $class); + + # Intialize some object data + $self->release($params) if ($params->{'release'} || $params->{'release_id'}); + + if ($params->{'preload'}) { + + # Preload the current data + $self->get_release_components($params); + $self->get_visible_components(); + } + + return $self; +} + +############################################################################ +# Functions/Methods +############################################################################ + +=item C<approved> + +=item B<Description> + +Return list of approved components + +=item B<Params> + +=item B<Returns> + +=cut + +sub approved { + my ($self, $params) = @_; + if (!$self->{'approved'}) { + $self->get_release_components($params); + } + return $self->{'approved'}; +} + +=item C<capacity> + +=item B<Description> + +Return list of capacity components + +=item B<Params> + +=item B<Returns> + +=cut + +sub capacity { + my ($self, $params) = @_; + if (!$self->{'capacity'}) { + $self->get_release_components($params); + } + return $self->{'capacity'}; +} + +=item C<ack> + +=item B<Description> + +Return list of acked components, This is normally same as the approved components. + +=item B<Params> + +=item B<Returns> + +=cut + +sub ack { + my ($self) = @_; + return $self->approved; +} + +=item C<nack> + +=item B<Description> + +Return list of nacked components, This is normally same as the approved components and +capacity components combined. + +=item B<Params> + +=item B<Returns> + +=cut + +sub nack { + my ($self, $params) = @_; + return [ + sort { $a->{'name'} cmp $b->{'name'} } @{$self->approved($params)}, + @{$self->capacity($params)} + ]; +} + +=item C<components> + +=item B<Description> + +Return list of all components possible for the given release. + +=item B<Params> + +=item B<Returns> + +=cut + +sub components { + my ($self, $params) = @_; + if (!$self->{'components'}) { + $self->get_visible_components($params); + } + return $self->{'components'}; +} + +=item C<release> + +=item B<Description> + +Returns a FlagType object representing the current release. + +=item B<Params> + +=item B<Returns> + +=cut + +sub release { + my ($self, $params) = @_; + + if ( !(defined($params->{'release'}) || defined($params->{'release_id'})) + && !defined($self->{'release'})) + { + ThrowCodeError('param_required', {param => 'release'}); + } + + if (ref($params->{'release'})) { + $self->{'release'} = $params->{'release'}; + } + elsif ($params->{'release'}) { + $self->{'release'} + = Bugzilla::Extension::Releases::Release->new( + {release => $params->{'release'}}) + || ThrowUserError('release_invalid_flag_name'); + } + elsif ($params->{'release_id'}) { + $self->{'release'} + = Bugzilla::Extension::Releases::Release->new( + {release_id => $params->{'release_id'}}) + || ThrowUserError('release_invalid_flag_name'); + } + + return $self->{'release'}; +} + +=item C<get_release_components> + +=item B<Description> + +=item B<Params> + +=item B<Returns> + +=cut + +sub get_release_components { + my ($self, $data_ref) = @_; + my $dbh = Bugzilla->dbh; + + my $sth; + if ($data_ref->{'names_only'}) { + $sth = $dbh->prepare(<<READ_NAME_ONLY); + SELECT DISTINCT components.name AS name + FROM components, + rh_release_components + WHERE components.id = rh_release_components.component_id + AND rh_release_components.release_id = ? + AND rh_release_components.component_type = ? + ORDER BY components.name +READ_NAME_ONLY + } + else { + $sth = $dbh->prepare(<<READ_ALL); + SELECT rh_release_components.id AS id, + rh_release_components.component_id AS component_id, + components.name AS name, + assigned_to.login_name AS initialowner, + qa_contact.login_name AS initialqacontact, + products.name AS product, + rh_release_components.component_type AS type + FROM components LEFT JOIN profiles qa_contact + ON components.initialqacontact = qa_contact.userid, + rh_release_components, + profiles assigned_to, + products + WHERE components.id = rh_release_components.component_id + AND components.initialowner = assigned_to.userid + AND components.product_id = products.id + AND rh_release_components.release_id = ? + AND rh_release_components.component_type = ? + ORDER BY components.name +READ_ALL + } + + my @approved_comps; + $sth->execute($self->release->id, 'approved'); + while (my $data_ref = $sth->fetchrow_hashref()) { + push(@approved_comps, $data_ref); + } + $self->{'approved'} = \@approved_comps; + + my @capacity_comps; + $sth->execute($self->release->id, 'capacity'); + while (my $data_ref = $sth->fetchrow_hashref()) { + push(@capacity_comps, $data_ref); + } + $self->{'capacity'} = \@capacity_comps; + + # Load optional component tags + if (!$data_ref->{names_only}) { + my $select_sth = $dbh->prepare( + "SELECT id, tag FROM rh_release_component_tags WHERE release_component_id = ?"); + foreach my $type ('approved', 'capacity') { + foreach my $component (@{$self->{$type}}) { + $select_sth->execute($component->{id}); + while (my ($id, $tag) = $select_sth->fetchrow_array()) { + $component->{tags} = [] if !$component->{tags}; + push(@{$component->{tags}}, $tag); + } + } + } + } + + return { + approved => $self->approved, + capacity => $self->capacity, + ack => $self->ack, + nack => $self->nack + }; +} + +=item C<set_release_components> + +=item B<Description> + +=item B<Params> + +=item B<Returns> + +=cut + +sub set_release_components { + my ($self, $data_ref) = @_; + my $dbh = Bugzilla->dbh; + + Bugzilla->user->can_edit_releases() + || ThrowUserError("auth_failure", + {group => 'program_management', action => "edit", object => "components"}); + + if (!defined $data_ref->{'approved'} && !defined $data_ref->{'capacity'}) { + ThrowCodeError('param_required', {param => 'approved'}); + } + + $self->release({release => $data_ref->{'release'}}) if $data_ref->{'release'}; + + my $allow_empty = $data_ref->{'allow_empty'} ? 1 : 0; + + # Prepared handles for inserting and deleting the release components + my $insert_sth = $dbh->prepare("INSERT INTO rh_release_components " + . "(release_id, component_id, component_type) VALUES (?, ?, ?)"); + my $delete_sth = $dbh->prepare("DELETE FROM rh_release_components " + . "WHERE release_id = ? AND component_type = ?"); + + my %new_approved_list; + my %new_capacity_list; + + if (defined $data_ref->{'approved'}) { + if ( $data_ref->{'approved_action'} ne 'add' + && $data_ref->{'approved_action'} ne 'remove' + && $data_ref->{'approved_action'} ne 'makeexact') + { + ThrowCodeError('param_required', {param => "approved_action"}); + } + + if ($data_ref->{'approved_action'} eq 'makeexact') { + foreach my $comp (@{$data_ref->{'approved'}}) { + $new_approved_list{$comp} = 1; + } + } + elsif ($data_ref->{'approved_action'} eq 'add' + || $data_ref->{'approved_action'} eq 'remove') + { + foreach my $comp (@{$self->approved}) { + $new_approved_list{$comp->{'name'}} = 1; + } + foreach my $comp (@{$data_ref->{'approved'}}) { + $new_approved_list{$comp} = 1 if $data_ref->{'approved_action'} eq 'add'; + delete $new_approved_list{$comp} if $data_ref->{'approved_action'} eq 'remove'; + } + } + } + + if (defined $data_ref->{'capacity'}) { + if ( $data_ref->{'capacity_action'} ne 'add' + && $data_ref->{'capacity_action'} ne 'remove' + && $data_ref->{'capacity_action'} ne 'makeexact') + { + ThrowCodeError('param_required', {param => "capacity_action"}); + } + + if ($data_ref->{'capacity_action'} eq 'makeexact') { + foreach my $comp (@{$data_ref->{'capacity'}}) { + $new_capacity_list{$comp} = 1; + } + } + elsif ($data_ref->{'capacity_action'} eq 'add' + || $data_ref->{'capacity_action'} eq 'remove') + { + foreach my $comp (@{$self->capacity}) { + $new_capacity_list{$comp->{'name'}} = 1; + } + foreach my $comp (@{$data_ref->{'capacity'}}) { + $new_capacity_list{$comp} = 1 if $data_ref->{'capacity_action'} eq 'add'; + delete $new_capacity_list{$comp} if $data_ref->{'capacity_action'} eq 'remove'; + } + } + } + + # Check that the new lists contain components on the list of allowed components + # and returns hash references with component names. + $self->sanity_check_lists(\%new_approved_list, \%new_capacity_list); + + # Throw confirmation screen if we are completely emptying either list. + if ( + ( + (defined $data_ref->{'approved'} && !%new_approved_list && @{$self->approved}) + || ( defined $data_ref->{'capacity'} + && !%new_capacity_list + && @{$self->capacity}) + ) + && !$allow_empty + ) + { + if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + print Bugzilla->cgi->header(); + Bugzilla->template->process( + "admin/releases/components/confirm_empty_list.html.tmpl", $data_ref) + || ThrowTemplateError(Bugzilla->template->error()); + exit; + } + else { + ThrowUserError('release_components_confirm_empty_list'); + } + } + + # Update the database + $dbh->bz_start_transaction(); + + # Get the timestamp for log_activity + my $timestamp = $dbh->selectrow_array('SELECT NOW()'); + + my %changes; + if (defined $data_ref->{'approved'} && (%new_approved_list || $allow_empty)) { + $delete_sth->execute($self->release->id, 'approved'); + + foreach my $comp_name (keys %new_approved_list) { + foreach my $comp_id (@{$self->components->{$comp_name}}) { + $insert_sth->execute($self->release->id, $comp_id, 'approved'); + } + } + + # Update the component tags table data + my $select_query + = "SELECT rh_release_components.id, components.name " + . " FROM rh_release_components, components " + . " WHERE rh_release_components.component_id = components.id " + . " AND rh_release_components.release_id = ? AND component_type = 'approved'"; + my $select_sth = $dbh->prepare($select_query); + $select_sth->execute($self->release->id); + while (my ($id, $name) = $select_sth->fetchrow_array()) { + $dbh->do("DELETE FROM rh_release_component_tags WHERE release_component_id = ?", + undef, $id); + if (defined $data_ref->{approved_tags}{$name} + && @{$data_ref->{approved_tags}{$name}}) + { + foreach my $tag (@{$data_ref->{approved_tags}{$name}}) { + trick_taint($tag); + $dbh->do( + "INSERT INTO rh_release_component_tags (release_component_id, tag) VALUES (?, ?)", + undef, $id, $tag + ); + } + } + } + + # Record changes to activity table + my %unique_approved = map { $_->{'name'} => 1 } @{$self->approved}; + my ($removed, $added) + = diff_arrays([keys %unique_approved], [keys %new_approved_list]); + if (@$added || @$removed) { + $changes{'approved'} = [$removed, $added]; + log_activity({ + release_id => $self->release->id, + act_type => 'component', + who => Bugzilla->user->id, + act_when => $timestamp, + what => 'approved', + removed => join(", ", @{$removed}), + added => join(", ", @{$added}) + }); + } + } + + if (defined $data_ref->{'capacity'} && (%new_capacity_list || $allow_empty)) { + $delete_sth->execute($self->release->id, 'capacity'); + + foreach my $comp_name (keys %new_capacity_list) { + foreach my $comp_id (@{$self->components->{$comp_name}}) { + $insert_sth->execute($self->release->id, $comp_id, 'capacity'); + } + } + + # Update the component tags table data + my $select_query + = "SELECT rh_release_components.id, components.name " + . " FROM rh_release_components, components " + . " WHERE rh_release_components.component_id = components.id " + . " AND rh_release_components.release_id = ? AND component_type = 'capacity'"; + my $select_sth = $dbh->prepare($select_query); + $select_sth->execute($self->release->id); + while (my ($id, $name) = $select_sth->fetchrow_array()) { + $dbh->do("DELETE FROM rh_release_component_tags WHERE release_component_id = ?", + undef, $id); + if (defined $data_ref->{capacity_tags}{$name} + && @{$data_ref->{capacity_tags}{$name}}) + { + foreach my $tag (@{$data_ref->{capacity_tags}{$name}}) { + trick_taint($tag); + $dbh->do( + "INSERT INTO rh_release_component_tags (release_component_id, tag) VALUES (?, ?)", + undef, $id, $tag + ); + } + } + } + + # Record changes to activity table + my %unique_capacity = map { $_->{'name'} => 1 } @{$self->capacity}; + my ($removed, $added) + = diff_arrays([keys %unique_capacity], [keys %new_capacity_list]); + if (@$added || @$removed) { + $changes{'capacity'} = [$removed, $added]; + log_activity({ + release_id => $self->release->id, + act_type => 'component', + who => Bugzilla->user->id, + act_when => $timestamp, + what => 'capacity', + removed => join(", ", @{$removed}), + added => join(", ", @{$added}) + }); + } + } + + $dbh->bz_commit_transaction(); + + # Refresh the lists + $self->get_release_components(); + + return \%changes; +} + +=item C<get_visible_components> + +=item B<Description> + +=item B<Params> + +=item B<Returns> + +=cut + +# Gather all possible components that this release flag is visible for +sub get_visible_components { + my ($self, $params) = shift; + my $dbh = Bugzilla->dbh; + + $self->release({release => $params->{'release'}}) if $params->{'release'}; + + my %components; + + # Included components + my $inclusions_sth = $dbh->prepare(<<INCLUSIONS); + SELECT components.name, components.id + FROM components, flaginclusions + WHERE components.product_id = flaginclusions.product_id + AND flaginclusions.component_id IS NULL + AND flaginclusions.type_id = ? +INCLUSIONS + $inclusions_sth->execute($self->release->flag_type->id); + while (my ($name, $id) = $inclusions_sth->fetchrow_array()) { + $components{$name} = [] if not exists $components{$name}; + push(@{$components{$name}}, $id); + } + + $inclusions_sth + = $dbh->prepare("SELECT components.name, components.id " + . "FROM components, flaginclusions " + . "WHERE components.id = flaginclusions.component_id " + . "AND flaginclusions.type_id = ?"); + $inclusions_sth->execute($self->release->flag_type->id); + while (my ($name, $id) = $inclusions_sth->fetchrow_array()) { + $components{$name} = [] if not exists $components{$name}; + push(@{$components{$name}}, $id); + } + + # Excluded components + my $exclusions_sth + = $dbh->prepare("SELECT components.name " + . "FROM components, flagexclusions " + . "WHERE components.product_id = flagexclusions.product_id " + . "AND flagexclusions.component_id IS NULL " + . "AND flagexclusions.type_id = ?"); + $exclusions_sth->execute($self->release->flag_type->id); + while (my ($name) = $exclusions_sth->fetchrow_array()) { + delete $components{$name}; + } + $exclusions_sth + = $dbh->prepare("SELECT components.name " + . "FROM components, flagexclusions " + . "WHERE components.id = flagexclusions.component_id " + . "AND flagexclusions.type_id = ?"); + $exclusions_sth->execute($self->release->flag_type->id); + while (my ($name) = $exclusions_sth->fetchrow_array()) { + delete $components{$name}; + } + + $self->{'components'} = \%components; + + return \%components; +} + +=item C<sanity_check_lists> + +=item B<Description> + +Sanity check the approved and capacity lists against the master component list. +If any bad components it will throw an error. Otherwise it simply returns. + +=item B<Params> + +$components_ref = Full list of possible components for the current release. + +$approved_ref = List of approved components + +$capacity_ref = List of capacity components + +=item B<Returns> + +None + +=cut + +sub sanity_check_lists { + my ($self, $approved_ref, $capacity_ref) = @_; + + # 1. approved comp name check + my @bad; + foreach my $comp (keys %{$approved_ref}) { + if (!$self->components->{$comp}) { + push(@bad, $comp); + } + } + if (@bad) { + ThrowUserError('release_components_illegal_component', + {comps => \@bad, type => 'approved'}); + } + + # 2. capacity comp name check + foreach my $comp (keys %{$capacity_ref}) { + if (!$self->components->{$comp}) { + push(@bad, $comp); + } + } + if (@bad) { + ThrowUserError('release_components_illegal_component', + {comps => \@bad, type => 'capacity'}); + } + + # 3. approved XOR capacity. component must be on only one of those lists. + my %approved = %{$approved_ref}; + %approved = map { $_->{name} => 1 } @{$self->approved} if !%approved; + my %capacity = %{$capacity_ref}; + %capacity = map { $_->{name} => 1 } @{$self->capacity} if !%capacity; + + foreach my $comp (sort keys %{$self->components}) { + + if (exists $approved{$comp} && exists $capacity{$comp}) { + push(@bad, $comp); + } + } + if (@bad) { + ThrowUserError('release_components_illegal_component', + {comps => \@bad, type => 'xor'}); + } + + return; +} + +=pod + +=back + +=head1 SEE ALSO + +L<Bugzilla|Bugzilla> + +=cut + +1; + + |