# 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) { $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 $changes = $self->SUPER::update(@_); if (exists $changes->{value}) { my $dbh = Bugzilla->dbh; # 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])); } return $changes; } sub remove_from_db { my $self = shift; my $dbh = Bugzilla->dbh; # 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(); } ################################ # 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 }); 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 $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 Description: Name (value) of the milestone. Params: none. Returns: The name of the milestone. =item C Description: ID of the product the milestone belongs to. Params: none. Returns: The ID of a product. =item C Description: The product object of the product the milestone belongs to. Params: none. Returns: A Bugzilla::Product object. =item C Description: Sortkey of the milestone. Params: none. Returns: The sortkey of the milestone. =item C Description: Returns the total of bugs that belong to the milestone. Params: none. Returns: Integer with the number of bugs. =item C 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 Description: Changes the sortkey of the milestone. Params: $new_sortkey - new sortkey of the milestone (signed integer). Returns: Nothing. =item C 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 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 $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