diff options
Diffstat (limited to 'Bugzilla/Component.pm')
-rw-r--r-- | Bugzilla/Component.pm | 652 |
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 |