Component.pm 18 KB
Newer Older
1 2 3
# 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/.
4
#
5 6
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
7 8

package Bugzilla::Component;
9 10

use 5.10.1;
11
use strict;
12
use warnings;
13

14
use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
15

16
use Bugzilla::Constants;
17 18
use Bugzilla::Util;
use Bugzilla::Error;
19
use Bugzilla::User;
20
use Bugzilla::FlagType;
21
use Bugzilla::Series;
22

23 24
use Scalar::Util qw(blessed);

25 26 27 28
###############################
####    Initialization     ####
###############################

29
use constant DB_TABLE => 'components';
30

31 32
# This is mostly for the editfields.cgi case where ->get_all is called.
use constant LIST_ORDER => 'product_id, name';
33

34
use constant DB_COLUMNS => qw(
35 36 37 38 39 40 41
  id
  name
  product_id
  initialowner
  initialqacontact
  description
  isactive
42 43
);

44
use constant UPDATE_COLUMNS => qw(
45 46 47 48 49
  name
  initialowner
  initialqacontact
  description
  isactive
50 51
);

52
use constant REQUIRED_FIELD_MAP => {product_id => 'product',};
53

54
use constant VALIDATORS => {
55 56 57 58 59 60 61 62
  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,
63 64
};

65
use constant VALIDATOR_DEPENDENCIES => {name => ['product'],};
66

67 68 69
###############################

sub new {
70 71 72 73 74 75 76 77 78 79
  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"});
80
    }
81 82 83 84 85
    if (!defined $name) {
      ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"});
    }

    my $condition = 'product_id = ? AND name = ?';
86
    my @values    = ($product->id, $name);
87 88
    $param = {condition => $condition, values => \@values};
  }
89

90 91 92 93 94 95
  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;
96 97
}

98
sub create {
99 100
  my $class = shift;
  my $dbh   = Bugzilla->dbh;
101

102
  $dbh->bz_start_transaction();
103

104 105 106 107 108 109
  $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;
110

111 112
  my $component = $class->insert_create_data($params);
  $component->{product} = $product;
113

114 115
  # We still have to fill the component_cc table.
  $component->_update_cc_list($cc_list) if $cc_list;
116

117 118
  # Create series for the new component.
  $component->_create_series() if $create_series;
119

120 121
  $dbh->bz_commit_transaction();
  return $component;
122 123 124
}

sub update {
125 126 127 128 129 130 131 132 133
  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;
134 135 136
}

sub remove_from_db {
137 138
  my $self = shift;
  my $dbh  = Bugzilla->dbh;
139

140
  $self->_check_if_controller();    # From ChoiceInterface
141

142
  $dbh->bz_start_transaction();
143

144 145 146 147 148 149 150 151 152 153
  # 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}) {
154

155 156 157 158 159
        # 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();
      }
160
    }
161 162 163 164 165 166 167 168
    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();
169

170
  $dbh->bz_commit_transaction();
171 172 173 174 175 176 177
}

################################
# Validators
################################

sub _check_name {
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
  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;
194 195 196
}

sub _check_description {
197
  my ($invocant, $description) = @_;
198

199 200 201
  $description = trim($description);
  $description || ThrowUserError('component_blank_description');
  return $description;
202 203 204
}

sub _check_initialowner {
205
  my ($invocant, $owner) = @_;
206

207 208 209
  $owner || ThrowUserError('component_need_initialowner');
  my $owner_id = Bugzilla::User->check($owner)->id;
  return $owner_id;
210 211 212
}

sub _check_initialqacontact {
213 214 215 216 217 218 219 220 221 222
  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;
223 224 225
}

sub _check_product {
226 227 228 229 230
  my ($invocant, $product) = @_;
  $product
    || ThrowCodeError('param_required',
    {function => "$invocant->create", param => 'product'});
  return Bugzilla->user->check_can_admin_product($product->name);
231 232 233
}

sub _check_cc_list {
234 235 236 237 238 239 240 241
  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];
242 243 244 245 246 247 248
}

###############################
####       Methods         ####
###############################

sub _update_cc_list {
249 250
  my ($self, $cc_list) = @_;
  my $dbh = Bugzilla->dbh;
251

252 253 254 255
  my $old_cc_list = $dbh->selectcol_arrayref(
    'SELECT user_id FROM component_cc
                                WHERE component_id = ?', undef, $self->id
  );
256

257 258 259 260 261
  my ($removed, $added) = diff_arrays($old_cc_list, $cc_list);
  my $diff;
  if (scalar @$removed || scalar @$added) {
    $diff = [join(', ', @$removed), join(', ', @$added)];
  }
262

263
  $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
264

265 266 267 268 269
  my $sth = $dbh->prepare(
    'INSERT INTO component_cc
                             (user_id, component_id) VALUES (?, ?)'
  );
  $sth->execute($_, $self->id) foreach (@$cc_list);
270

271
  return $diff;
272 273 274
}

sub _create_series {
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
  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();
  }
301 302
}

303
sub set_name        { $_[0]->set('name',        $_[1]); }
304
sub set_description { $_[0]->set('description', $_[1]); }
305 306
sub set_is_active   { $_[0]->set('isactive',    $_[1]); }

307
sub set_default_assignee {
308 309 310
  my ($self, $owner) = @_;

  $self->set('initialowner', $owner);
311

312 313
  # Reset the default owner object.
  delete $self->{default_assignee};
314
}
315

316
sub set_default_qa_contact {
317 318 319
  my ($self, $qa_contact) = @_;

  $self->set('initialqacontact', $qa_contact);
320

321 322
  # Reset the default QA contact object.
  delete $self->{default_qa_contact};
323
}
324

325
sub set_cc_list {
326 327 328
  my ($self, $cc_list) = @_;

  $self->{cc_ids} = $self->_check_cc_list($cc_list);
329

330 331
  # Reset the list of CC user objects.
  delete $self->{initial_cc};
332 333
}

334
sub bug_count {
335 336
  my $self = shift;
  my $dbh  = Bugzilla->dbh;
337

338 339 340
  if (!defined $self->{'bug_count'}) {
    $self->{'bug_count'} = $dbh->selectrow_array(
      q{
341
            SELECT COUNT(*) FROM bugs
342 343 344 345
            WHERE component_id = ?}, undef, $self->id
    ) || 0;
  }
  return $self->{'bug_count'};
346 347 348
}

sub bug_ids {
349 350
  my $self = shift;
  my $dbh  = Bugzilla->dbh;
351

352 353 354
  if (!defined $self->{'bugs_ids'}) {
    $self->{'bugs_ids'} = $dbh->selectcol_arrayref(
      q{
355
            SELECT bug_id FROM bugs
356 357 358 359
            WHERE component_id = ?}, undef, $self->id
    );
  }
  return $self->{'bugs_ids'};
360 361 362
}

sub default_assignee {
363
  my $self = shift;
364

365 366
  return $self->{'default_assignee'}
    ||= new Bugzilla::User({id => $self->{'initialowner'}, cache => 1});
367 368 369
}

sub default_qa_contact {
370
  my $self = shift;
371

372 373 374
  return unless $self->{'initialqacontact'};
  return $self->{'default_qa_contact'}
    ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1});
375 376
}

377
sub flag_types {
378 379 380 381 382 383 384 385 386 387 388 389 390
  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'};
391 392
}

393
sub initial_cc {
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
  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'};
410 411
}

412
sub product {
413 414 415 416 417 418
  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'};
419 420
}

421 422 423 424
###############################
####      Accessors        ####
###############################

425
sub description { return $_[0]->{'description'}; }
426 427
sub product_id  { return $_[0]->{'product_id'}; }
sub is_active   { return $_[0]->{'isactive'}; }
428

429 430 431 432 433 434 435 436
##############################################
# Implement Bugzilla::Field::ChoiceInterface #
##############################################

use constant FIELD_NAME => 'component';
use constant is_default => 0;

sub is_set_on_bug {
437 438 439 440 441
  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;
442 443
}

444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
###############################
####      Subroutines      ####
###############################

1;

__END__

=head1 NAME

Bugzilla::Component - Bugzilla product component class.

=head1 SYNOPSIS

    use Bugzilla::Component;

460 461
    my $component = new Bugzilla::Component($comp_id);
    my $component = new Bugzilla::Component({ product => $product, name => $name });
462

463 464
    my $bug_count          = $component->bug_count();
    my $bug_ids            = $component->bug_ids();
465 466 467 468 469 470
    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;
471 472
    my $initial_cc         = $component->initial_cc;
    my $product            = $component->product;
473 474
    my $bug_flag_types     = $component->flag_types->{'bug'};
    my $attach_flag_types  = $component->flag_types->{'attachment'};
475

476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
    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;
493 494 495 496 497 498 499 500 501 502 503 504

=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
505 506
              by passing a component ID or a hash with the product
              object the component belongs to and the component name.
507 508

 Params:      $param - If you pass an integer, the integer is the
509
                       component ID from the database that we want to
510 511 512 513 514 515
                       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.
516 517 518

 Returns:     A Bugzilla::Component object.

519 520 521 522 523 524 525 526
=item C<bug_count()>

 Description: Returns the total of bugs that belong to the component.

 Params:      none.

 Returns:     Integer with the number of bugs.

527
=item C<bug_ids()>
528 529

 Description: Returns all bug IDs that belong to the component.
530

531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
 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.

551 552
 Returns:     A Bugzilla::User object if the default QA contact is defined for
              the component. Otherwise, returns undef.
553

554 555
=item C<initial_cc>

556 557 558 559 560 561
 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.
562

563 564
=item C<flag_types()>

565 566
 Description: Returns all bug and attachment flagtypes available for
              the component.
567

568
 Params:      none.
569

570
 Returns:     Two references to an array of flagtype objects.
571

572 573
=item C<product()>

574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
 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.
604

605
 Returns:     Nothing.
606

607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
=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.
642

643 644
=back

645
=head1 CLASS METHODS
646 647 648

=over

649
=item C<create(\%params)>
650

651
 Description: Create a new component for the given product.
652

653
 Params:      The hashref must have the following keys:
Simon Green's avatar
Simon Green committed
654 655 656 657 658 659
              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).
660
              The following keys are optional:
Simon Green's avatar
Simon Green committed
661 662 663 664
              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.
665

666
 Returns:     A Bugzilla::Component object.
667

668 669 670
=back

=cut
671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686

=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