diff options
Diffstat (limited to 'Bugzilla/Milestone.pm')
-rw-r--r-- | Bugzilla/Milestone.pm | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/Bugzilla/Milestone.pm b/Bugzilla/Milestone.pm new file mode 100644 index 0000000..b4ddaea --- /dev/null +++ b/Bugzilla/Milestone.pm @@ -0,0 +1,378 @@ +# 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. + +use strict; + +package Bugzilla::Milestone; + +use base qw(Bugzilla::Object); + +use Bugzilla::Constants; +use Bugzilla::Util; +use Bugzilla::Error; + +use Scalar::Util qw(blessed); + +################################ +##### Initialization ##### +################################ + +use constant DEFAULT_SORTKEY => 0; + +use constant DB_TABLE => 'milestones'; +use constant NAME_FIELD => 'value'; +use constant LIST_ORDER => 'sortkey, value'; + +use constant DB_COLUMNS => qw( + id + value + product_id + sortkey + isactive +); + +use constant REQUIRED_FIELD_MAP => { + product_id => 'product', +}; + +use constant UPDATE_COLUMNS => qw( + value + sortkey + isactive +); + +use constant VALIDATORS => { + product => \&_check_product, + sortkey => \&_check_sortkey, + value => \&_check_value, + isactive => \&Bugzilla::Object::check_boolean, +}; + +use constant VALIDATOR_DEPENDENCIES => { + value => ['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 value = ?'; + my @values = ($product->id, $name); + $param = { condition => $condition, values => \@values }; + } + + unshift @_, $param; + return $class->SUPER::new(@_); +} + +sub run_create_validators { + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + return $params; +} + +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $changes = $self->SUPER::update(@_); + + if (exists $changes->{value}) { + # The milestone value is stored in the bugs table instead of its ID. + $dbh->do('UPDATE bugs SET target_milestone = ? + WHERE target_milestone = ? AND product_id = ?', + undef, ($self->name, $changes->{value}->[0], $self->product_id)); + + # The default milestone also stores the value instead of the ID. + $dbh->do('UPDATE products SET defaultmilestone = ? + WHERE id = ? AND defaultmilestone = ?', + undef, ($self->name, $self->product_id, $changes->{value}->[0])); + } + $dbh->bz_commit_transaction(); + + return $changes; +} + +sub remove_from_db { + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + # The default milestone cannot be deleted. + if ($self->name eq $self->product->default_milestone) { + ThrowUserError('milestone_is_default', { milestone => $self }); + } + + if ($self->bug_count) { + # We don't want to delete bugs when deleting a milestone. + # Bugs concerned are reassigned to the default milestone. + my $bug_ids = + $dbh->selectcol_arrayref('SELECT bug_id FROM bugs + WHERE product_id = ? AND target_milestone = ?', + undef, ($self->product->id, $self->name)); + + my $timestamp = $dbh->selectrow_array('SELECT NOW()'); + + $dbh->do('UPDATE bugs SET target_milestone = ?, delta_ts = ? + WHERE ' . $dbh->sql_in('bug_id', $bug_ids), + undef, ($self->product->default_milestone, $timestamp)); + + require Bugzilla::Bug; + import Bugzilla::Bug qw(LogActivityEntry); + foreach my $bug_id (@$bug_ids) { + LogActivityEntry($bug_id, 'target_milestone', + $self->name, + $self->product->default_milestone, + Bugzilla->user->id, $timestamp); + } + } + $self->SUPER::remove_from_db(); + + $dbh->bz_commit_transaction(); +} + +################################ +# Validators +################################ + +sub _check_value { + my ($invocant, $name, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product : $params->{product}; + + $name = trim($name); + $name || ThrowUserError('milestone_blank_name'); + if (length($name) > MAX_MILESTONE_SIZE) { + ThrowUserError('milestone_name_too_long', {name => $name}); + } + + my $milestone = new Bugzilla::Milestone({product => $product, name => $name}); + if ($milestone && (!ref $invocant || $milestone->id != $invocant->id)) { + ThrowUserError('milestone_already_exists', { name => $milestone->name, + product => $product->name }); + } + return $name; +} + +sub _check_sortkey { + my ($invocant, $sortkey) = @_; + + # Keep a copy in case detaint_signed() clears the sortkey + my $stored_sortkey = $sortkey; + + if (!detaint_signed($sortkey) || $sortkey < MIN_SMALLINT || $sortkey > MAX_SMALLINT) { + ThrowUserError('milestone_sortkey_invalid', {sortkey => $stored_sortkey}); + } + return $sortkey; +} + +sub _check_product { + my ($invocant, $product) = @_; + $product || ThrowCodeError('param_required', + { function => "$invocant->create", param => "product" }); + return Bugzilla->user->check_can_admin_product($product->name); +} + +################################ +# Methods +################################ + +sub set_name { $_[0]->set('value', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_is_active { $_[0]->set('isactive', $_[1]); } + +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 product_id = ? AND target_milestone = ?}, + undef, $self->product_id, $self->name) || 0; + } + return $self->{'bug_count'}; +} + +################################ +##### Accessors ###### +################################ + +sub name { return $_[0]->{'value'}; } +sub product_id { return $_[0]->{'product_id'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub is_active { return $_[0]->{'isactive'}; } + +sub product { + my $self = shift; + + require Bugzilla::Product; + $self->{'product'} ||= new Bugzilla::Product($self->product_id); + return $self->{'product'}; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Milestone - Bugzilla product milestone class. + +=head1 SYNOPSIS + + use Bugzilla::Milestone; + + my $milestone = new Bugzilla::Milestone({ name => $name, product => $product_obj }); + my $milestone = Bugzilla::Milestone->check({ name => $name, product => $product_obj }); + my $milestone = Bugzilla::Milestone->check({ id => $id }); + + my $name = $milestone->name; + my $product_id = $milestone->product_id; + my $product = $milestone->product; + my $sortkey = $milestone->sortkey; + + my $milestone = Bugzilla::Milestone->create( + { value => $name, product => $product, sortkey => $sortkey }); + + $milestone->set_name($new_name); + $milestone->set_sortkey($new_sortkey); + $milestone->update(); + + $milestone->remove_from_db; + +=head1 DESCRIPTION + +Milestone.pm represents a Product Milestone object. + +=head1 METHODS + +=over + +=item C<< new({name => $name, product => $product}) >> + + Description: The constructor is used to load an existing milestone + by passing a product object and a milestone name. + + Params: $product - a Bugzilla::Product object. + $name - the name of a milestone (string). + + Returns: A Bugzilla::Milestone object. + +=item C<name()> + + Description: Name (value) of the milestone. + + Params: none. + + Returns: The name of the milestone. + +=item C<product_id()> + + Description: ID of the product the milestone belongs to. + + Params: none. + + Returns: The ID of a product. + +=item C<product()> + + Description: The product object of the product the milestone belongs to. + + Params: none. + + Returns: A Bugzilla::Product object. + +=item C<sortkey()> + + Description: Sortkey of the milestone. + + Params: none. + + Returns: The sortkey of the milestone. + +=item C<bug_count()> + + Description: Returns the total of bugs that belong to the milestone. + + Params: none. + + Returns: Integer with the number of bugs. + +=item C<set_name($new_name)> + + Description: Changes the name of the milestone. + + Params: $new_name - new name of the milestone (string). This name + must be unique within the product. + + Returns: Nothing. + +=item C<set_sortkey($new_sortkey)> + + Description: Changes the sortkey of the milestone. + + Params: $new_sortkey - new sortkey of the milestone (signed integer). + + Returns: Nothing. + +=item C<update()> + + Description: Writes the new name and/or the new sortkey into the DB. + + Params: none. + + Returns: A hashref with changes made to the milestone object. + +=item C<remove_from_db()> + + Description: Deletes the current milestone from the DB. The object itself + is not destroyed. + + Params: none. + + Returns: Nothing. + +=back + +=head1 CLASS METHODS + +=over + +=item C<< create({value => $value, product => $product, sortkey => $sortkey}) >> + + Description: Create a new milestone for the given product. + + Params: $value - name of the new milestone (string). This name + must be unique within the product. + $product - a Bugzilla::Product object. + $sortkey - the sortkey of the new milestone (signed integer) + + Returns: A Bugzilla::Milestone object. + +=back |