| # 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 5.10.1; |
| use strict; |
| use warnings; |
| |
| use parent 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. |
| my @components = @{ $self->product->components }; |
| if (scalar(@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}); |
| } |
| } |
| # Update the list of components in the product object. |
| $self->product->{components} = [grep { $_->id != $self->id } @components]; |
| $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; |
| |
| return $self->{'default_assignee'} |
| ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 }); |
| } |
| |
| sub default_qa_contact { |
| my $self = shift; |
| |
| return unless $self->{'initialqacontact'}; |
| return $self->{'default_qa_contact'} |
| ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1 }); |
| } |
| |
| 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 |
| |
| =head1 B<Methods in need of POD> |
| |
| =over |
| |
| =item is_set_on_bug |
| |
| =item product_id |
| |
| =item set_is_active |
| |
| =item description |
| |
| =item is_active |
| |
| =back |