From 7f0ba708827dec5bb77222405009f1771e43655a Mon Sep 17 00:00:00 2001 From: "lpsolit%gmail.com" <> Date: Thu, 31 Jul 2008 02:47:25 +0000 Subject: Bug 313122: Implement Product->create, $product->update and $product->remove_from_db, and make editproducts.cgi use them - Patch by Frédéric Buclin r=mkanat a=LpSolit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Bugzilla/Bug.pm | 2 +- Bugzilla/Constants.pm | 4 + Bugzilla/Product.pm | 419 ++++++++++++++++++++++++++++++++++++++++++++++++-- Bugzilla/Series.pm | 1 - 4 files changed, 409 insertions(+), 17 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index ff0f2089f..99fd8742c 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -3088,7 +3088,7 @@ sub RemoveVotes { undef, ($votes, $id)); } # Now return the array containing emails to be sent. - return \@messages; + return @messages; } # If a user votes for a bug, or the number of votes required to diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index d8ddfcf66..b0dd0b6f6 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -146,6 +146,7 @@ use File::Basename; MAX_SMALLINT MAX_LEN_QUERY_NAME + MAX_PRODUCT_SIZE MAX_MILESTONE_SIZE MAX_COMPONENT_SIZE MAX_FREETEXT_LENGTH @@ -413,6 +414,9 @@ use constant MAX_SMALLINT => 32767; # The longest that a saved search name can be. use constant MAX_LEN_QUERY_NAME => 64; +# The longest product name allowed. +use constant MAX_PRODUCT_SIZE => 64; + # The longest milestone name allowed. use constant MAX_MILESTONE_SIZE => 20; 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 +# Frédéric Buclin 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; diff --git a/Bugzilla/Series.pm b/Bugzilla/Series.pm index a99c442fe..a4174c28d 100644 --- a/Bugzilla/Series.pm +++ b/Bugzilla/Series.pm @@ -33,7 +33,6 @@ package Bugzilla::Series; use Bugzilla::Error; use Bugzilla::Util; -use Bugzilla::User; sub new { my $invocant = shift; -- cgit v1.2.3-24-g4f1b