summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/Releases/lib/Components.pm')
-rw-r--r--extensions/Releases/lib/Components.pm701
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;
+
+