Component.pm 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. # -*- Mode: perl; indent-tabs-mode: nil -*-
  2. #
  3. # The contents of this file are subject to the Mozilla Public
  4. # License Version 1.1 (the "License"); you may not use this file
  5. # except in compliance with the License. You may obtain a copy of
  6. # the License at http://www.mozilla.org/MPL/
  7. #
  8. # Software distributed under the License is distributed on an "AS
  9. # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
  10. # implied. See the License for the specific language governing
  11. # rights and limitations under the License.
  12. #
  13. # The Original Code is the Bugzilla Bug Tracking System.
  14. #
  15. # Contributor(s): Tiago R. Mello <timello@async.com.br>
  16. # Frédéric Buclin <LpSolit@gmail.com>
  17. # Max Kanat-Alexander <mkanat@bugzilla.org>
  18. # Akamai Technologies <bugzilla-dev@akamai.com>
  19. use strict;
  20. package Bugzilla::Component;
  21. use base qw(Bugzilla::Object);
  22. use Bugzilla::Constants;
  23. use Bugzilla::Util;
  24. use Bugzilla::Error;
  25. use Bugzilla::User;
  26. use Bugzilla::FlagType;
  27. use Bugzilla::Series;
  28. ###############################
  29. #### Initialization ####
  30. ###############################
  31. use constant DB_TABLE => 'components';
  32. use constant DB_COLUMNS => qw(
  33. id
  34. name
  35. product_id
  36. initialowner
  37. initialqacontact
  38. description
  39. );
  40. use constant REQUIRED_CREATE_FIELDS => qw(
  41. name
  42. product
  43. initialowner
  44. description
  45. );
  46. use constant UPDATE_COLUMNS => qw(
  47. name
  48. initialowner
  49. initialqacontact
  50. description
  51. );
  52. use constant VALIDATORS => {
  53. product => \&_check_product,
  54. initialowner => \&_check_initialowner,
  55. initialqacontact => \&_check_initialqacontact,
  56. description => \&_check_description,
  57. initial_cc => \&_check_cc_list,
  58. };
  59. use constant UPDATE_VALIDATORS => {
  60. name => \&_check_name,
  61. };
  62. ###############################
  63. sub new {
  64. my $class = shift;
  65. my $param = shift;
  66. my $dbh = Bugzilla->dbh;
  67. my $product;
  68. if (ref $param) {
  69. $product = $param->{product};
  70. my $name = $param->{name};
  71. if (!defined $product) {
  72. ThrowCodeError('bad_arg',
  73. {argument => 'product',
  74. function => "${class}::new"});
  75. }
  76. if (!defined $name) {
  77. ThrowCodeError('bad_arg',
  78. {argument => 'name',
  79. function => "${class}::new"});
  80. }
  81. my $condition = 'product_id = ? AND name = ?';
  82. my @values = ($product->id, $name);
  83. $param = { condition => $condition, values => \@values };
  84. }
  85. unshift @_, $param;
  86. my $component = $class->SUPER::new(@_);
  87. # Add the product object as attribute only if the component exists.
  88. $component->{product} = $product if ($component && $product);
  89. return $component;
  90. }
  91. sub create {
  92. my $class = shift;
  93. my $dbh = Bugzilla->dbh;
  94. $dbh->bz_start_transaction();
  95. $class->check_required_create_fields(@_);
  96. my $params = $class->run_create_validators(@_);
  97. my $cc_list = delete $params->{initial_cc};
  98. my $component = $class->insert_create_data($params);
  99. # We still have to fill the component_cc table.
  100. $component->_update_cc_list($cc_list);
  101. # Create series for the new component.
  102. $component->_create_series();
  103. $dbh->bz_commit_transaction();
  104. return $component;
  105. }
  106. sub run_create_validators {
  107. my $class = shift;
  108. my $params = $class->SUPER::run_create_validators(@_);
  109. my $product = delete $params->{product};
  110. $params->{product_id} = $product->id;
  111. $params->{name} = $class->_check_name($params->{name}, $product);
  112. return $params;
  113. }
  114. sub update {
  115. my $self = shift;
  116. my $changes = $self->SUPER::update(@_);
  117. # Update the component_cc table if necessary.
  118. if (defined $self->{cc_ids}) {
  119. my $diff = $self->_update_cc_list($self->{cc_ids});
  120. $changes->{cc_list} = $diff if defined $diff;
  121. }
  122. return $changes;
  123. }
  124. sub remove_from_db {
  125. my $self = shift;
  126. my $dbh = Bugzilla->dbh;
  127. $dbh->bz_start_transaction();
  128. if ($self->bug_count) {
  129. if (Bugzilla->params->{'allowbugdeletion'}) {
  130. require Bugzilla::Bug;
  131. foreach my $bug_id (@{$self->bug_ids}) {
  132. # Note: We allow admins to delete bugs even if they can't
  133. # see them, as long as they can see the product.
  134. my $bug = new Bugzilla::Bug($bug_id);
  135. $bug->remove_from_db();
  136. }
  137. } else {
  138. ThrowUserError('component_has_bugs', {nb => $self->bug_count});
  139. }
  140. }
  141. $dbh->do('DELETE FROM flaginclusions WHERE component_id = ?',
  142. undef, $self->id);
  143. $dbh->do('DELETE FROM flagexclusions WHERE component_id = ?',
  144. undef, $self->id);
  145. $dbh->do('DELETE FROM component_cc WHERE component_id = ?',
  146. undef, $self->id);
  147. $dbh->do('DELETE FROM components WHERE id = ?', undef, $self->id);
  148. $dbh->bz_commit_transaction();
  149. }
  150. ################################
  151. # Validators
  152. ################################
  153. sub _check_name {
  154. my ($invocant, $name, $product) = @_;
  155. $name = trim($name);
  156. $name || ThrowUserError('component_blank_name');
  157. if (length($name) > MAX_COMPONENT_SIZE) {
  158. ThrowUserError('component_name_too_long', {'name' => $name});
  159. }
  160. $product = $invocant->product if (ref $invocant);
  161. my $component = new Bugzilla::Component({product => $product, name => $name});
  162. if ($component && (!ref $invocant || $component->id != $invocant->id)) {
  163. ThrowUserError('component_already_exists', { name => $component->name,
  164. product => $product });
  165. }
  166. return $name;
  167. }
  168. sub _check_description {
  169. my ($invocant, $description) = @_;
  170. $description = trim($description);
  171. $description || ThrowUserError('component_blank_description');
  172. return $description;
  173. }
  174. sub _check_initialowner {
  175. my ($invocant, $owner) = @_;
  176. $owner || ThrowUserError('component_need_initialowner');
  177. my $owner_id = Bugzilla::User->check($owner)->id;
  178. return $owner_id;
  179. }
  180. sub _check_initialqacontact {
  181. my ($invocant, $qa_contact) = @_;
  182. my $qa_contact_id;
  183. if (Bugzilla->params->{'useqacontact'}) {
  184. $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact;
  185. }
  186. elsif (ref $invocant) {
  187. $qa_contact_id = $invocant->{initialqacontact};
  188. }
  189. return $qa_contact_id;
  190. }
  191. sub _check_product {
  192. my ($invocant, $product) = @_;
  193. return Bugzilla->user->check_can_admin_product($product->name);
  194. }
  195. sub _check_cc_list {
  196. my ($invocant, $cc_list) = @_;
  197. my %cc_ids;
  198. foreach my $cc (@$cc_list) {
  199. my $id = login_to_id($cc, THROW_ERROR);
  200. $cc_ids{$id} = 1;
  201. }
  202. return [keys %cc_ids];
  203. }
  204. ###############################
  205. #### Methods ####
  206. ###############################
  207. sub _update_cc_list {
  208. my ($self, $cc_list) = @_;
  209. my $dbh = Bugzilla->dbh;
  210. my $old_cc_list =
  211. $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
  212. WHERE component_id = ?', undef, $self->id);
  213. my ($removed, $added) = diff_arrays($old_cc_list, $cc_list);
  214. my $diff;
  215. if (scalar @$removed || scalar @$added) {
  216. $diff = [join(', ', @$removed), join(', ', @$added)];
  217. }
  218. $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
  219. my $sth = $dbh->prepare('INSERT INTO component_cc
  220. (user_id, component_id) VALUES (?, ?)');
  221. $sth->execute($_, $self->id) foreach (@$cc_list);
  222. return $diff;
  223. }
  224. sub _create_series {
  225. my $self = shift;
  226. # Insert default charting queries for this product.
  227. # If they aren't using charting, this won't do any harm.
  228. my $prodcomp = "&product=" . url_quote($self->product->name) .
  229. "&component=" . url_quote($self->name);
  230. my $open_query = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' .
  231. $prodcomp;
  232. my $nonopen_query = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' .
  233. $prodcomp;
  234. my @series = ([get_text('series_all_open'), $open_query],
  235. [get_text('series_all_closed'), $nonopen_query]);
  236. foreach my $sdata (@series) {
  237. my $series = new Bugzilla::Series(undef, $self->product->name,
  238. $self->name, $sdata->[0],
  239. Bugzilla->user->id, 1, $sdata->[1], 1);
  240. $series->writeToDatabase();
  241. }
  242. }
  243. sub set_name { $_[0]->set('name', $_[1]); }
  244. sub set_description { $_[0]->set('description', $_[1]); }
  245. sub set_default_assignee {
  246. my ($self, $owner) = @_;
  247. $self->set('initialowner', $owner);
  248. # Reset the default owner object.
  249. delete $self->{default_assignee};
  250. }
  251. sub set_default_qa_contact {
  252. my ($self, $qa_contact) = @_;
  253. $self->set('initialqacontact', $qa_contact);
  254. # Reset the default QA contact object.
  255. delete $self->{default_qa_contact};
  256. }
  257. sub set_cc_list {
  258. my ($self, $cc_list) = @_;
  259. $self->{cc_ids} = $self->_check_cc_list($cc_list);
  260. # Reset the list of CC user objects.
  261. delete $self->{initial_cc};
  262. }
  263. sub bug_count {
  264. my $self = shift;
  265. my $dbh = Bugzilla->dbh;
  266. if (!defined $self->{'bug_count'}) {
  267. $self->{'bug_count'} = $dbh->selectrow_array(q{
  268. SELECT COUNT(*) FROM bugs
  269. WHERE component_id = ?}, undef, $self->id) || 0;
  270. }
  271. return $self->{'bug_count'};
  272. }
  273. sub bug_ids {
  274. my $self = shift;
  275. my $dbh = Bugzilla->dbh;
  276. if (!defined $self->{'bugs_ids'}) {
  277. $self->{'bugs_ids'} = $dbh->selectcol_arrayref(q{
  278. SELECT bug_id FROM bugs
  279. WHERE component_id = ?}, undef, $self->id);
  280. }
  281. return $self->{'bugs_ids'};
  282. }
  283. sub default_assignee {
  284. my $self = shift;
  285. if (!defined $self->{'default_assignee'}) {
  286. $self->{'default_assignee'} =
  287. new Bugzilla::User($self->{'initialowner'});
  288. }
  289. return $self->{'default_assignee'};
  290. }
  291. sub default_qa_contact {
  292. my $self = shift;
  293. if (!defined $self->{'default_qa_contact'}) {
  294. $self->{'default_qa_contact'} =
  295. new Bugzilla::User($self->{'initialqacontact'});
  296. }
  297. return $self->{'default_qa_contact'};
  298. }
  299. sub flag_types {
  300. my $self = shift;
  301. if (!defined $self->{'flag_types'}) {
  302. $self->{'flag_types'} = {};
  303. $self->{'flag_types'}->{'bug'} =
  304. Bugzilla::FlagType::match({ 'target_type' => 'bug',
  305. 'product_id' => $self->product_id,
  306. 'component_id' => $self->id });
  307. $self->{'flag_types'}->{'attachment'} =
  308. Bugzilla::FlagType::match({ 'target_type' => 'attachment',
  309. 'product_id' => $self->product_id,
  310. 'component_id' => $self->id });
  311. }
  312. return $self->{'flag_types'};
  313. }
  314. sub initial_cc {
  315. my $self = shift;
  316. my $dbh = Bugzilla->dbh;
  317. if (!defined $self->{'initial_cc'}) {
  318. # If set_cc_list() has been called but data are not yet written
  319. # into the DB, we want the new values defined by it.
  320. my $cc_ids = $self->{cc_ids}
  321. || $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
  322. WHERE component_id = ?',
  323. undef, $self->id);
  324. $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids);
  325. }
  326. return $self->{'initial_cc'};
  327. }
  328. sub product {
  329. my $self = shift;
  330. if (!defined $self->{'product'}) {
  331. require Bugzilla::Product; # We cannot |use| it.
  332. $self->{'product'} = new Bugzilla::Product($self->product_id);
  333. }
  334. return $self->{'product'};
  335. }
  336. ###############################
  337. #### Accessors ####
  338. ###############################
  339. sub id { return $_[0]->{'id'}; }
  340. sub name { return $_[0]->{'name'}; }
  341. sub description { return $_[0]->{'description'}; }
  342. sub product_id { return $_[0]->{'product_id'}; }
  343. ###############################
  344. #### Subroutines ####
  345. ###############################
  346. 1;
  347. __END__
  348. =head1 NAME
  349. Bugzilla::Component - Bugzilla product component class.
  350. =head1 SYNOPSIS
  351. use Bugzilla::Component;
  352. my $component = new Bugzilla::Component($comp_id);
  353. my $component = new Bugzilla::Component({ product => $product, name => $name });
  354. my $bug_count = $component->bug_count();
  355. my $bug_ids = $component->bug_ids();
  356. my $id = $component->id;
  357. my $name = $component->name;
  358. my $description = $component->description;
  359. my $product_id = $component->product_id;
  360. my $default_assignee = $component->default_assignee;
  361. my $default_qa_contact = $component->default_qa_contact;
  362. my $initial_cc = $component->initial_cc;
  363. my $product = $component->product;
  364. my $bug_flag_types = $component->flag_types->{'bug'};
  365. my $attach_flag_types = $component->flag_types->{'attachment'};
  366. my $component = Bugzilla::Component->check({ product => $product, name => $name });
  367. my $component =
  368. Bugzilla::Component->create({ name => $name,
  369. product => $product,
  370. initialowner => $user_login1,
  371. initialqacontact => $user_login2,
  372. description => $description});
  373. $component->set_name($new_name);
  374. $component->set_description($new_description);
  375. $component->set_default_assignee($new_login_name);
  376. $component->set_default_qa_contact($new_login_name);
  377. $component->set_cc_list(\@new_login_names);
  378. $component->update();
  379. $component->remove_from_db;
  380. =head1 DESCRIPTION
  381. Component.pm represents a Product Component object.
  382. =head1 METHODS
  383. =over
  384. =item C<new($param)>
  385. Description: The constructor is used to load an existing component
  386. by passing a component ID or a hash with the product
  387. object the component belongs to and the component name.
  388. Params: $param - If you pass an integer, the integer is the
  389. component ID from the database that we want to
  390. read in. If you pass in a hash with the 'name'
  391. and 'product' keys, then the value of the name
  392. key is the name of a component being in the given
  393. product.
  394. Returns: A Bugzilla::Component object.
  395. =item C<bug_count()>
  396. Description: Returns the total of bugs that belong to the component.
  397. Params: none.
  398. Returns: Integer with the number of bugs.
  399. =item C<bugs_ids()>
  400. Description: Returns all bug IDs that belong to the component.
  401. Params: none.
  402. Returns: A reference to an array of bug IDs.
  403. =item C<default_assignee()>
  404. Description: Returns a user object that represents the default assignee for
  405. the component.
  406. Params: none.
  407. Returns: A Bugzilla::User object.
  408. =item C<default_qa_contact()>
  409. Description: Returns a user object that represents the default QA contact for
  410. the component.
  411. Params: none.
  412. Returns: A Bugzilla::User object.
  413. =item C<initial_cc>
  414. Description: Returns a list of user objects representing users being
  415. in the initial CC list.
  416. Params: none.
  417. Returns: An arrayref of L<Bugzilla::User> objects.
  418. =item C<flag_types()>
  419. Description: Returns all bug and attachment flagtypes available for
  420. the component.
  421. Params: none.
  422. Returns: Two references to an array of flagtype objects.
  423. =item C<product()>
  424. Description: Returns the product the component belongs to.
  425. Params: none.
  426. Returns: A Bugzilla::Product object.
  427. =item C<set_name($new_name)>
  428. Description: Changes the name of the component.
  429. Params: $new_name - new name of the component (string). This name
  430. must be unique within the product.
  431. Returns: Nothing.
  432. =item C<set_description($new_desc)>
  433. Description: Changes the description of the component.
  434. Params: $new_desc - new description of the component (string).
  435. Returns: Nothing.
  436. =item C<set_default_assignee($new_assignee)>
  437. Description: Changes the default assignee of the component.
  438. Params: $new_owner - login name of the new default assignee of
  439. the component (string). This user account
  440. must already exist.
  441. Returns: Nothing.
  442. =item C<set_default_qa_contact($new_qa_contact)>
  443. Description: Changes the default QA contact of the component.
  444. Params: $new_qa_contact - login name of the new QA contact of
  445. the component (string). This user
  446. account must already exist.
  447. Returns: Nothing.
  448. =item C<set_cc_list(\@cc_list)>
  449. Description: Changes the list of users being in the CC list by default.
  450. Params: \@cc_list - list of login names (string). All the user
  451. accounts must already exist.
  452. Returns: Nothing.
  453. =item C<update()>
  454. Description: Write changes made to the component into the DB.
  455. Params: none.
  456. Returns: A hashref with changes made to the component object.
  457. =item C<remove_from_db()>
  458. Description: Deletes the current component from the DB. The object itself
  459. is not destroyed.
  460. Params: none.
  461. Returns: Nothing.
  462. =back
  463. =head1 CLASS METHODS
  464. =over
  465. =item C<create(\%params)>
  466. Description: Create a new component for the given product.
  467. Params: The hashref must have the following keys:
  468. name - name of the new component (string). This name
  469. must be unique within the product.
  470. product - a Bugzilla::Product object to which
  471. the Component is being added.
  472. description - description of the new component (string).
  473. initialowner - login name of the default assignee (string).
  474. The following keys are optional:
  475. initiaqacontact - login name of the default QA contact (string),
  476. or an empty string to clear it.
  477. initial_cc - an arrayref of login names to add to the
  478. CC list by default.
  479. Returns: A Bugzilla::Component object.
  480. =back
  481. =cut