summaryrefslogtreecommitdiffstats
path: root/Bugzilla/Product.pm
diff options
context:
space:
mode:
authorlpsolit%gmail.com <>2008-07-31 04:47:25 +0200
committerlpsolit%gmail.com <>2008-07-31 04:47:25 +0200
commit7f0ba708827dec5bb77222405009f1771e43655a (patch)
tree4b40eddbdcd172d211514d25dc9b2adcf78e9caf /Bugzilla/Product.pm
parent4fc0f4b92290a7fbf9b4340e9dd37c2626f524ea (diff)
downloadbugzilla-7f0ba708827dec5bb77222405009f1771e43655a.tar.gz
bugzilla-7f0ba708827dec5bb77222405009f1771e43655a.tar.xz
Bug 313122: Implement Product->create, $product->update and $product->remove_from_db, and make editproducts.cgi use them - Patch by Frédéric Buclin <LpSolit@gmail.com> r=mkanat a=LpSolit
Diffstat (limited to 'Bugzilla/Product.pm')
-rw-r--r--Bugzilla/Product.pm419
1 files changed, 404 insertions, 15 deletions
diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm
index edc621f67..f5c4fae6c 100644
--- a/Bugzilla/Product.pm
+++ b/Bugzilla/Product.pm
@@ -13,20 +13,23 @@
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Tiago R. Mello <timello@async.com.br>
+# Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Product;
-use Bugzilla::Version;
-use Bugzilla::Milestone;
-
use Bugzilla::Constants;
use Bugzilla::Util;
-use Bugzilla::Group;
use Bugzilla::Error;
-
+use Bugzilla::Group;
+use Bugzilla::Version;
+use Bugzilla::Milestone;
+use Bugzilla::Field;
+use Bugzilla::Status;
use Bugzilla::Install::Requirements;
+use Bugzilla::Mailer;
+use Bugzilla::Series;
use base qw(Bugzilla::Object);
@@ -39,22 +42,81 @@ use constant DEFAULT_CLASSIFICATION_ID => 1;
use constant DB_TABLE => 'products';
use constant DB_COLUMNS => qw(
- products.id
- products.name
- products.classification_id
- products.description
- products.milestoneurl
- products.disallownew
- products.votesperuser
- products.maxvotesperbug
- products.votestoconfirm
- products.defaultmilestone
+ id
+ name
+ classification_id
+ description
+ milestoneurl
+ disallownew
+ votesperuser
+ maxvotesperbug
+ votestoconfirm
+ defaultmilestone
);
+use constant REQUIRED_CREATE_FIELDS => qw(
+ name
+ description
+ version
+);
+
+use constant UPDATE_COLUMNS => qw(
+ name
+ description
+ defaultmilestone
+ milestoneurl
+ disallownew
+ votesperuser
+ maxvotesperbug
+ votestoconfirm
+);
+
+use constant VALIDATORS => {
+ classification => \&_check_classification,
+ name => \&_check_name,
+ description => \&_check_description,
+ version => \&_check_version,
+ defaultmilestone => \&_check_default_milestone,
+ milestoneurl => \&_check_milestone_url,
+ disallownew => \&Bugzilla::Object::check_boolean,
+ votesperuser => \&_check_votes_per_user,
+ maxvotesperbug => \&_check_votes_per_bug,
+ votestoconfirm => \&_check_votes_to_confirm,
+ create_series => \&Bugzilla::Object::check_boolean
+};
+
###############################
#### Constructors #####
###############################
+sub create {
+ my $class = shift;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
+
+ $class->check_required_create_fields(@_);
+
+ my $params = $class->run_create_validators(@_);
+ # Some fields do not exist in the DB as is.
+ $params->{classification_id} = delete $params->{classification};
+ my $version = delete $params->{version};
+ my $create_series = delete $params->{create_series};
+
+ my $product = $class->insert_create_data($params);
+
+ # Add the new version and milestone into the DB as valid values.
+ Bugzilla::Version::create($version, $product);
+ Bugzilla::Milestone->create({name => $params->{defaultmilestone}, product => $product});
+
+ # Create groups and series for the new product, if requested.
+ $product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'};
+ $product->_create_series() if $create_series;
+
+ $dbh->bz_commit_transaction();
+ return $product;
+}
+
# This is considerably faster than calling new_from_list three times
# for each product in the list, particularly with hundreds or thousands
# of products.
@@ -78,10 +140,337 @@ sub preload {
}
}
+sub update {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ # Don't update the DB if something goes wrong below -> transaction.
+ $dbh->bz_start_transaction();
+ my $changes = $self->SUPER::update(@_);
+
+ # We also have to fix votes.
+ my @msgs; # Will store emails to send to voters.
+ if ($changes->{maxvotesperbug} || $changes->{votesperuser} || $changes->{votestoconfirm}) {
+ # We cannot |use| these modules, due to dependency loops.
+ require Bugzilla::Bug;
+ import Bugzilla::Bug qw(RemoveVotes CheckIfVotedConfirmed);
+ require Bugzilla::User;
+ import Bugzilla::User qw(user_id_to_login);
+
+ # 1. too many votes for a single user on a single bug.
+ my @toomanyvotes_list = ();
+ if ($self->max_votes_per_bug < $self->votes_per_user) {
+ my $votes = $dbh->selectall_arrayref(
+ 'SELECT votes.who, votes.bug_id
+ FROM votes
+ INNER JOIN bugs
+ ON bugs.bug_id = votes.bug_id
+ WHERE bugs.product_id = ?
+ AND votes.vote_count > ?',
+ undef, ($self->id, $self->max_votes_per_bug));
+
+ foreach my $vote (@$votes) {
+ my ($who, $id) = (@$vote);
+ # If some votes are removed, RemoveVotes() returns a list
+ # of messages to send to voters.
+ push(@msgs, RemoveVotes($id, $who, 'votes_too_many_per_bug'));
+ my $name = user_id_to_login($who);
+
+ push(@toomanyvotes_list, {id => $id, name => $name});
+ }
+ }
+ $changes->{'too_many_votes'} = \@toomanyvotes_list;
+
+ # 2. too many total votes for a single user.
+ # This part doesn't work in the general case because RemoveVotes
+ # doesn't enforce votesperuser (except per-bug when it's less
+ # than maxvotesperbug). See Bugzilla::Bug::RemoveVotes().
+
+ my $votes = $dbh->selectall_arrayref(
+ 'SELECT votes.who, votes.vote_count
+ FROM votes
+ INNER JOIN bugs
+ ON bugs.bug_id = votes.bug_id
+ WHERE bugs.product_id = ?',
+ undef, $self->id);
+
+ my %counts;
+ foreach my $vote (@$votes) {
+ my ($who, $count) = @$vote;
+ if (!defined $counts{$who}) {
+ $counts{$who} = $count;
+ } else {
+ $counts{$who} += $count;
+ }
+ }
+ my @toomanytotalvotes_list = ();
+ foreach my $who (keys(%counts)) {
+ if ($counts{$who} > $self->votes_per_user) {
+ my $bug_ids = $dbh->selectcol_arrayref(
+ 'SELECT votes.bug_id
+ FROM votes
+ INNER JOIN bugs
+ ON bugs.bug_id = votes.bug_id
+ WHERE bugs.product_id = ?
+ AND votes.who = ?',
+ undef, ($self->id, $who));
+
+ foreach my $bug_id (@$bug_ids) {
+ # RemoveVotes() returns a list of messages to send
+ # in case some voters had too many votes.
+ push(@msgs, RemoveVotes($bug_id, $who, 'votes_too_many_per_user'));
+ my $name = user_id_to_login($who);
+
+ push(@toomanytotalvotes_list, {id => $bug_id, name => $name});
+ }
+ }
+ }
+ $changes->{'too_many_total_votes'} = \@toomanytotalvotes_list;
+
+ # 3. enough votes to confirm
+ my $bug_list =
+ $dbh->selectcol_arrayref('SELECT bug_id FROM bugs WHERE product_id = ?
+ AND bug_status = ? AND votes >= ?',
+ undef, ($self->id, 'UNCONFIRMED', $self->votes_to_confirm));
+
+ my @updated_bugs = ();
+ foreach my $bug_id (@$bug_list) {
+ my $confirmed = CheckIfVotedConfirmed($bug_id, $user->id);
+ push (@updated_bugs, $bug_id) if $confirmed;
+ }
+ $changes->{'confirmed_bugs'} = \@updated_bugs;
+ }
+ $dbh->bz_commit_transaction();
+
+ # Now that changes have been committed, we can send emails to voters.
+ foreach my $msg (@msgs) {
+ MessageToMTA($msg);
+ }
+
+ return $changes;
+}
+
+sub remove_from_db {
+ my $self = shift;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
+
+ if ($self->bug_count) {
+ if (Bugzilla->params->{'allowbugdeletion'}) {
+ foreach my $bug_id (@{$self->bug_ids}) {
+ # Note that we allow the user to delete bugs he can't see,
+ # which is okay, because he's deleting the whole Product.
+ my $bug = new Bugzilla::Bug($bug_id);
+ $bug->remove_from_db();
+ }
+ }
+ else {
+ ThrowUserError('product_has_bugs', { nb => $self->bug_count });
+ }
+ }
+
+ # XXX - This line can go away as soon as bug 427455 is fixed.
+ $dbh->do("DELETE FROM group_control_map WHERE product_id = ?", undef, $self->id);
+ $dbh->do("DELETE FROM products WHERE id = ?", undef, $self->id);
+
+ $dbh->bz_commit_transaction();
+
+ # We have to delete these internal variables, else we get
+ # the old lists of products and classifications again.
+ delete $user->{selectable_products};
+ delete $user->{selectable_classifications};
+
+}
+
+###############################
+#### Validators ####
+###############################
+
+sub _check_classification {
+ my ($invocant, $classification_name) = @_;
+
+ my $classification_id = 1;
+ if (Bugzilla->params->{'useclassification'}) {
+ my $classification =
+ Bugzilla::Classification::check_classification($classification_name);
+ $classification_id = $classification->id;
+ }
+ return $classification_id;
+}
+
+sub _check_name {
+ my ($invocant, $name) = @_;
+
+ $name = trim($name);
+ $name || ThrowUserError('product_blank_name');
+
+ if (length($name) > MAX_PRODUCT_SIZE) {
+ ThrowUserError('product_name_too_long', {'name' => $name});
+ }
+
+ my $product = new Bugzilla::Product({name => $name});
+ if ($product && (!ref $invocant || $product->id != $invocant->id)) {
+ # Check for exact case sensitive match:
+ if ($product->name eq $name) {
+ ThrowUserError('product_name_already_in_use', {'product' => $product->name});
+ }
+ else {
+ ThrowUserError('product_name_diff_in_case', {'product' => $name,
+ 'existing_product' => $product->name});
+ }
+ }
+ return $name;
+}
+
+sub _check_description {
+ my ($invocant, $description) = @_;
+
+ $description = trim($description);
+ $description || ThrowUserError('product_must_have_description');
+ return $description;
+}
+
+sub _check_version {
+ my ($invocant, $version) = @_;
+
+ $version = trim($version);
+ $version || ThrowUserError('product_must_have_version');
+ # We will check the version length when Bugzilla::Version->create will do it.
+ return $version;
+}
+
+sub _check_default_milestone {
+ my ($invocant, $milestone) = @_;
+
+ # Do nothing if target milestones are not in use.
+ unless (Bugzilla->params->{'usetargetmilestone'}) {
+ return (ref $invocant) ? $invocant->default_milestone : undef;
+ }
+
+ $milestone = trim($milestone);
+
+ if (ref $invocant) {
+ # The default milestone must be one of the existing milestones.
+ my $mil_obj = new Bugzilla::Milestone({name => $milestone, product => $invocant});
+
+ $mil_obj || ThrowUserError('product_must_define_defaultmilestone',
+ {product => $invocant->name,
+ milestone => $milestone});
+ }
+ else {
+ $milestone ||= '---';
+ }
+ return $milestone;
+}
+
+sub _check_milestone_url {
+ my ($invocant, $url) = @_;
+
+ # Do nothing if target milestones are not in use.
+ unless (Bugzilla->params->{'usetargetmilestone'}) {
+ return (ref $invocant) ? $invocant->milestone_url : undef;
+ }
+
+ $url = trim($url);
+ return $url;
+}
+
+sub _check_votes_per_user {
+ return _check_votes(@_, 0);
+}
+
+sub _check_votes_per_bug {
+ return _check_votes(@_, 10000);
+}
+
+sub _check_votes_to_confirm {
+ return _check_votes(@_, 0);
+}
+
+# This subroutine is only used internally by other _check_votes_* validators.
+sub _check_votes {
+ my ($invocant, $votes, $field, $default) = @_;
+
+ detaint_natural($votes);
+ # On product creation, if the number of votes is not a valid integer,
+ # we silently fall back to the given default value.
+ # If the product already exists and the change is illegal, we complain.
+ if (!defined $votes) {
+ if (ref $invocant) {
+ ThrowUserError('product_illegal_votes', {field => $field, votes => $_[1]});
+ }
+ else {
+ $votes = $default;
+ }
+ }
+ return $votes;
+}
+
###############################
#### Methods ####
###############################
+sub _create_bug_group {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $group_name = $self->name;
+ while (new Bugzilla::Group({name => $group_name})) {
+ $group_name .= '_';
+ }
+ my $group_description = get_text('bug_group_description', {product => $self});
+
+ my $group = Bugzilla::Group->create({name => $group_name,
+ description => $group_description,
+ isbuggroup => 1});
+
+ # Associate the new group and new product.
+ $dbh->do('INSERT INTO group_control_map
+ (group_id, product_id, entry, membercontrol, othercontrol, canedit)
+ VALUES (?, ?, ?, ?, ?, ?)',
+ undef, ($group->id, $self->id, Bugzilla->params->{'useentrygroupdefault'},
+ CONTROLMAPDEFAULT, CONTROLMAPNA, 0));
+}
+
+sub _create_series {
+ my $self = shift;
+
+ my @series;
+ # We do every status, every resolution, and an "opened" one as well.
+ foreach my $bug_status (@{get_legal_field_values('bug_status')}) {
+ push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]);
+ }
+
+ foreach my $resolution (@{get_legal_field_values('resolution')}) {
+ next if !$resolution;
+ push(@series, [$resolution, "resolution=" . url_quote($resolution)]);
+ }
+
+ my @openedstatuses = BUG_STATE_OPEN;
+ my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses);
+ push(@series, [get_text('series_all_open'), $query]);
+
+ foreach my $sdata (@series) {
+ my $series = new Bugzilla::Series(undef, $self->name,
+ get_text('series_subcategory'),
+ $sdata->[0], Bugzilla->user->id, 1,
+ $sdata->[1] . "&product=" . url_quote($self->name), 1);
+ $series->writeToDatabase();
+ }
+}
+
+sub set_name { $_[0]->set('name', $_[1]); }
+sub set_description { $_[0]->set('description', $_[1]); }
+sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); }
+sub set_milestone_url { $_[0]->set('milestoneurl', $_[1]); }
+sub set_disallow_new { $_[0]->set('disallownew', $_[1]); }
+sub set_votes_per_user { $_[0]->set('votesperuser', $_[1]); }
+sub set_votes_per_bug { $_[0]->set('maxvotesperbug', $_[1]); }
+sub set_votes_to_confirm { $_[0]->set('votestoconfirm', $_[1]); }
+
sub components {
my $self = shift;
my $dbh = Bugzilla->dbh;