aboutsummaryrefslogtreecommitdiff
path: root/Bugzilla/Component.pm
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/Component.pm')
-rw-r--r--Bugzilla/Component.pm652
1 files changed, 652 insertions, 0 deletions
diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm
new file mode 100644
index 0000000..1ce4e02
--- /dev/null
+++ b/Bugzilla/Component.pm
@@ -0,0 +1,652 @@
+# 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/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Component;
+use strict;
+use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
+
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::Error;
+use Bugzilla::User;
+use Bugzilla::FlagType;
+use Bugzilla::Series;
+
+use Scalar::Util qw(blessed);
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DB_TABLE => 'components';
+# This is mostly for the editfields.cgi case where ->get_all is called.
+use constant LIST_ORDER => 'product_id, name';
+
+use constant DB_COLUMNS => qw(
+ id
+ name
+ product_id
+ initialowner
+ initialqacontact
+ description
+ isactive
+);
+
+use constant UPDATE_COLUMNS => qw(
+ name
+ initialowner
+ initialqacontact
+ description
+ isactive
+);
+
+use constant REQUIRED_FIELD_MAP => {
+ product_id => 'product',
+};
+
+use constant VALIDATORS => {
+ create_series => \&Bugzilla::Object::check_boolean,
+ product => \&_check_product,
+ initialowner => \&_check_initialowner,
+ initialqacontact => \&_check_initialqacontact,
+ description => \&_check_description,
+ initial_cc => \&_check_cc_list,
+ name => \&_check_name,
+ isactive => \&Bugzilla::Object::check_boolean,
+};
+
+use constant VALIDATOR_DEPENDENCIES => {
+ name => ['product'],
+};
+
+###############################
+
+sub new {
+ my $class = shift;
+ my $param = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $product;
+ if (ref $param and !defined $param->{id}) {
+ $product = $param->{product};
+ my $name = $param->{name};
+ if (!defined $product) {
+ ThrowCodeError('bad_arg',
+ {argument => 'product',
+ function => "${class}::new"});
+ }
+ if (!defined $name) {
+ ThrowCodeError('bad_arg',
+ {argument => 'name',
+ function => "${class}::new"});
+ }
+
+ my $condition = 'product_id = ? AND name = ?';
+ my @values = ($product->id, $name);
+ $param = { condition => $condition, values => \@values };
+ }
+
+ unshift @_, $param;
+ my $component = $class->SUPER::new(@_);
+ # Add the product object as attribute only if the component exists.
+ $component->{product} = $product if ($component && $product);
+ return $component;
+}
+
+sub create {
+ my $class = shift;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
+
+ $class->check_required_create_fields(@_);
+ my $params = $class->run_create_validators(@_);
+ my $cc_list = delete $params->{initial_cc};
+ my $create_series = delete $params->{create_series};
+ my $product = delete $params->{product};
+ $params->{product_id} = $product->id;
+
+ my $component = $class->insert_create_data($params);
+ $component->{product} = $product;
+
+ # We still have to fill the component_cc table.
+ $component->_update_cc_list($cc_list) if $cc_list;
+
+ # Create series for the new component.
+ $component->_create_series() if $create_series;
+
+ $dbh->bz_commit_transaction();
+ return $component;
+}
+
+sub update {
+ my $self = shift;
+ my $changes = $self->SUPER::update(@_);
+
+ # Update the component_cc table if necessary.
+ if (defined $self->{cc_ids}) {
+ my $diff = $self->_update_cc_list($self->{cc_ids});
+ $changes->{cc_list} = $diff if defined $diff;
+ }
+ return $changes;
+}
+
+sub remove_from_db {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ $self->_check_if_controller(); # From ChoiceInterface
+
+ $dbh->bz_start_transaction();
+
+ # Products must have at least one component.
+ if (scalar(@{$self->product->components}) == 1) {
+ ThrowUserError('component_is_last', { comp => $self });
+ }
+
+ if ($self->bug_count) {
+ if (Bugzilla->params->{'allowbugdeletion'}) {
+ require Bugzilla::Bug;
+ foreach my $bug_id (@{$self->bug_ids}) {
+ # Note: We allow admins to delete bugs even if they can't
+ # see them, as long as they can see the product.
+ my $bug = new Bugzilla::Bug($bug_id);
+ $bug->remove_from_db();
+ }
+ } else {
+ ThrowUserError('component_has_bugs', {nb => $self->bug_count});
+ }
+ }
+ $self->SUPER::remove_from_db();
+
+ $dbh->bz_commit_transaction();
+}
+
+################################
+# Validators
+################################
+
+sub _check_name {
+ my ($invocant, $name, undef, $params) = @_;
+ my $product = blessed($invocant) ? $invocant->product : $params->{product};
+
+ $name = trim($name);
+ $name || ThrowUserError('component_blank_name');
+
+ if (length($name) > MAX_COMPONENT_SIZE) {
+ ThrowUserError('component_name_too_long', {'name' => $name});
+ }
+
+ my $component = new Bugzilla::Component({product => $product, name => $name});
+ if ($component && (!ref $invocant || $component->id != $invocant->id)) {
+ ThrowUserError('component_already_exists', { name => $component->name,
+ product => $product });
+ }
+ return $name;
+}
+
+sub _check_description {
+ my ($invocant, $description) = @_;
+
+ $description = trim($description);
+ $description || ThrowUserError('component_blank_description');
+ return $description;
+}
+
+sub _check_initialowner {
+ my ($invocant, $owner) = @_;
+
+ $owner || ThrowUserError('component_need_initialowner');
+ my $owner_id = Bugzilla::User->check($owner)->id;
+ return $owner_id;
+}
+
+sub _check_initialqacontact {
+ my ($invocant, $qa_contact) = @_;
+
+ my $qa_contact_id;
+ if (Bugzilla->params->{'useqacontact'}) {
+ $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact;
+ }
+ elsif (ref $invocant) {
+ $qa_contact_id = $invocant->{initialqacontact};
+ }
+ return $qa_contact_id;
+}
+
+sub _check_product {
+ my ($invocant, $product) = @_;
+ $product || ThrowCodeError('param_required',
+ { function => "$invocant->create", param => 'product' });
+ return Bugzilla->user->check_can_admin_product($product->name);
+}
+
+sub _check_cc_list {
+ my ($invocant, $cc_list) = @_;
+
+ my %cc_ids;
+ foreach my $cc (@$cc_list) {
+ my $id = login_to_id($cc, THROW_ERROR);
+ $cc_ids{$id} = 1;
+ }
+ return [keys %cc_ids];
+}
+
+###############################
+#### Methods ####
+###############################
+
+sub _update_cc_list {
+ my ($self, $cc_list) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $old_cc_list =
+ $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
+ WHERE component_id = ?', undef, $self->id);
+
+ my ($removed, $added) = diff_arrays($old_cc_list, $cc_list);
+ my $diff;
+ if (scalar @$removed || scalar @$added) {
+ $diff = [join(', ', @$removed), join(', ', @$added)];
+ }
+
+ $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
+
+ my $sth = $dbh->prepare('INSERT INTO component_cc
+ (user_id, component_id) VALUES (?, ?)');
+ $sth->execute($_, $self->id) foreach (@$cc_list);
+
+ return $diff;
+}
+
+sub _create_series {
+ my $self = shift;
+
+ # Insert default charting queries for this product.
+ # If they aren't using charting, this won't do any harm.
+ my $prodcomp = "&product=" . url_quote($self->product->name) .
+ "&component=" . url_quote($self->name);
+
+ my $open_query = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' .
+ $prodcomp;
+ my $nonopen_query = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' .
+ $prodcomp;
+
+ my @series = ([get_text('series_all_open'), $open_query],
+ [get_text('series_all_closed'), $nonopen_query]);
+
+ foreach my $sdata (@series) {
+ my $series = new Bugzilla::Series(undef, $self->product->name,
+ $self->name, $sdata->[0],
+ Bugzilla->user->id, 1, $sdata->[1], 1);
+ $series->writeToDatabase();
+ }
+}
+
+sub set_name { $_[0]->set('name', $_[1]); }
+sub set_description { $_[0]->set('description', $_[1]); }
+sub set_is_active { $_[0]->set('isactive', $_[1]); }
+sub set_default_assignee {
+ my ($self, $owner) = @_;
+
+ $self->set('initialowner', $owner);
+ # Reset the default owner object.
+ delete $self->{default_assignee};
+}
+sub set_default_qa_contact {
+ my ($self, $qa_contact) = @_;
+
+ $self->set('initialqacontact', $qa_contact);
+ # Reset the default QA contact object.
+ delete $self->{default_qa_contact};
+}
+sub set_cc_list {
+ my ($self, $cc_list) = @_;
+
+ $self->{cc_ids} = $self->_check_cc_list($cc_list);
+ # Reset the list of CC user objects.
+ delete $self->{initial_cc};
+}
+
+sub bug_count {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ if (!defined $self->{'bug_count'}) {
+ $self->{'bug_count'} = $dbh->selectrow_array(q{
+ SELECT COUNT(*) FROM bugs
+ WHERE component_id = ?}, undef, $self->id) || 0;
+ }
+ return $self->{'bug_count'};
+}
+
+sub bug_ids {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ if (!defined $self->{'bugs_ids'}) {
+ $self->{'bugs_ids'} = $dbh->selectcol_arrayref(q{
+ SELECT bug_id FROM bugs
+ WHERE component_id = ?}, undef, $self->id);
+ }
+ return $self->{'bugs_ids'};
+}
+
+sub default_assignee {
+ my $self = shift;
+
+ if (!defined $self->{'default_assignee'}) {
+ $self->{'default_assignee'} =
+ new Bugzilla::User($self->{'initialowner'});
+ }
+ return $self->{'default_assignee'};
+}
+
+sub default_qa_contact {
+ my $self = shift;
+
+ return if !$self->{'initialqacontact'};
+
+ if (!defined $self->{'default_qa_contact'}) {
+ $self->{'default_qa_contact'} =
+ new Bugzilla::User($self->{'initialqacontact'});
+ }
+ return $self->{'default_qa_contact'};
+}
+
+sub flag_types {
+ my $self = shift;
+
+ if (!defined $self->{'flag_types'}) {
+ my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id,
+ component_id => $self->id });
+
+ $self->{'flag_types'} = {};
+ $self->{'flag_types'}->{'bug'} =
+ [grep { $_->target_type eq 'bug' } @$flagtypes];
+ $self->{'flag_types'}->{'attachment'} =
+ [grep { $_->target_type eq 'attachment' } @$flagtypes];
+ }
+ return $self->{'flag_types'};
+}
+
+sub initial_cc {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ if (!defined $self->{'initial_cc'}) {
+ # If set_cc_list() has been called but data are not yet written
+ # into the DB, we want the new values defined by it.
+ my $cc_ids = $self->{cc_ids}
+ || $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
+ WHERE component_id = ?',
+ undef, $self->id);
+
+ $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids);
+ }
+ return $self->{'initial_cc'};
+}
+
+sub product {
+ my $self = shift;
+ if (!defined $self->{'product'}) {
+ require Bugzilla::Product; # We cannot |use| it.
+ $self->{'product'} = new Bugzilla::Product($self->product_id);
+ }
+ return $self->{'product'};
+}
+
+###############################
+#### Accessors ####
+###############################
+
+sub description { return $_[0]->{'description'}; }
+sub product_id { return $_[0]->{'product_id'}; }
+sub is_active { return $_[0]->{'isactive'}; }
+
+##############################################
+# Implement Bugzilla::Field::ChoiceInterface #
+##############################################
+
+use constant FIELD_NAME => 'component';
+use constant is_default => 0;
+
+sub is_set_on_bug {
+ my ($self, $bug) = @_;
+ my $value = blessed($bug) ? $bug->component_id : $bug->{component};
+ $value = $value->id if blessed($value);
+ return 0 unless $value;
+ return $value == $self->id ? 1 : 0;
+}
+
+###############################
+#### Subroutines ####
+###############################
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Component - Bugzilla product component class.
+
+=head1 SYNOPSIS
+
+ use Bugzilla::Component;
+
+ my $component = new Bugzilla::Component($comp_id);
+ my $component = new Bugzilla::Component({ product => $product, name => $name });
+
+ my $bug_count = $component->bug_count();
+ my $bug_ids = $component->bug_ids();
+ my $id = $component->id;
+ my $name = $component->name;
+ my $description = $component->description;
+ my $product_id = $component->product_id;
+ my $default_assignee = $component->default_assignee;
+ my $default_qa_contact = $component->default_qa_contact;
+ my $initial_cc = $component->initial_cc;
+ my $product = $component->product;
+ my $bug_flag_types = $component->flag_types->{'bug'};
+ my $attach_flag_types = $component->flag_types->{'attachment'};
+
+ my $component = Bugzilla::Component->check({ product => $product, name => $name });
+
+ my $component =
+ Bugzilla::Component->create({ name => $name,
+ product => $product,
+ initialowner => $user_login1,
+ initialqacontact => $user_login2,
+ description => $description});
+
+ $component->set_name($new_name);
+ $component->set_description($new_description);
+ $component->set_default_assignee($new_login_name);
+ $component->set_default_qa_contact($new_login_name);
+ $component->set_cc_list(\@new_login_names);
+ $component->update();
+
+ $component->remove_from_db;
+
+=head1 DESCRIPTION
+
+Component.pm represents a Product Component object.
+
+=head1 METHODS
+
+=over
+
+=item C<new($param)>
+
+ Description: The constructor is used to load an existing component
+ by passing a component ID or a hash with the product
+ object the component belongs to and the component name.
+
+ Params: $param - If you pass an integer, the integer is the
+ component ID from the database that we want to
+ read in.
+ However, If you pass in a hash, it must contain
+ two keys:
+ name (string): the name of the component
+ product (object): an object of Bugzilla::Product
+ representing the product that the component belongs to.
+
+ Returns: A Bugzilla::Component object.
+
+=item C<bug_count()>
+
+ Description: Returns the total of bugs that belong to the component.
+
+ Params: none.
+
+ Returns: Integer with the number of bugs.
+
+=item C<bug_ids()>
+
+ Description: Returns all bug IDs that belong to the component.
+
+ Params: none.
+
+ Returns: A reference to an array of bug IDs.
+
+=item C<default_assignee()>
+
+ Description: Returns a user object that represents the default assignee for
+ the component.
+
+ Params: none.
+
+ Returns: A Bugzilla::User object.
+
+=item C<default_qa_contact()>
+
+ Description: Returns a user object that represents the default QA contact for
+ the component.
+
+ Params: none.
+
+ Returns: A Bugzilla::User object if the default QA contact is defined for
+ the component. Otherwise, returns undef.
+
+=item C<initial_cc>
+
+ Description: Returns a list of user objects representing users being
+ in the initial CC list.
+
+ Params: none.
+
+ Returns: An arrayref of L<Bugzilla::User> objects.
+
+=item C<flag_types()>
+
+ Description: Returns all bug and attachment flagtypes available for
+ the component.
+
+ Params: none.
+
+ Returns: Two references to an array of flagtype objects.
+
+=item C<product()>
+
+ Description: Returns the product the component belongs to.
+
+ Params: none.
+
+ Returns: A Bugzilla::Product object.
+
+=item C<set_name($new_name)>
+
+ Description: Changes the name of the component.
+
+ Params: $new_name - new name of the component (string). This name
+ must be unique within the product.
+
+ Returns: Nothing.
+
+=item C<set_description($new_desc)>
+
+ Description: Changes the description of the component.
+
+ Params: $new_desc - new description of the component (string).
+
+ Returns: Nothing.
+
+=item C<set_default_assignee($new_assignee)>
+
+ Description: Changes the default assignee of the component.
+
+ Params: $new_owner - login name of the new default assignee of
+ the component (string). This user account
+ must already exist.
+
+ Returns: Nothing.
+
+=item C<set_default_qa_contact($new_qa_contact)>
+
+ Description: Changes the default QA contact of the component.
+
+ Params: $new_qa_contact - login name of the new QA contact of
+ the component (string). This user
+ account must already exist.
+
+ Returns: Nothing.
+
+=item C<set_cc_list(\@cc_list)>
+
+ Description: Changes the list of users being in the CC list by default.
+
+ Params: \@cc_list - list of login names (string). All the user
+ accounts must already exist.
+
+ Returns: Nothing.
+
+=item C<update()>
+
+ Description: Write changes made to the component into the DB.
+
+ Params: none.
+
+ Returns: A hashref with changes made to the component object.
+
+=item C<remove_from_db()>
+
+ Description: Deletes the current component from the DB. The object itself
+ is not destroyed.
+
+ Params: none.
+
+ Returns: Nothing.
+
+=back
+
+=head1 CLASS METHODS
+
+=over
+
+=item C<create(\%params)>
+
+ Description: Create a new component for the given product.
+
+ Params: The hashref must have the following keys:
+ name - name of the new component (string). This name
+ must be unique within the product.
+ product - a Bugzilla::Product object to which
+ the Component is being added.
+ description - description of the new component (string).
+ initialowner - login name of the default assignee (string).
+ The following keys are optional:
+ initialqacontact - login name of the default QA contact (string),
+ or an empty string to clear it.
+ initial_cc - an arrayref of login names to add to the
+ CC list by default.
+
+ Returns: A Bugzilla::Component object.
+
+=back
+
+=cut