summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/AgileTools/lib/Pool.pm')
-rw-r--r--extensions/AgileTools/lib/Pool.pm521
1 files changed, 521 insertions, 0 deletions
diff --git a/extensions/AgileTools/lib/Pool.pm b/extensions/AgileTools/lib/Pool.pm
new file mode 100644
index 000000000..541083b25
--- /dev/null
+++ b/extensions/AgileTools/lib/Pool.pm
@@ -0,0 +1,521 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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/.
+#
+# Copyright (C) 2012 Jolla Ltd.
+# Contact: Pami Ketolainen <pami.ketolainen@jollamobile.com>
+
+=head1 NAME
+
+Bugzilla::Extension::AgileTools::Pool - Bug pool Object class
+
+=head1 SYNOPSIS
+
+ use Bugzilla::Extension::AgileTools::Pool;
+
+ my $pool = Bugzilla::Extension::AgileTools::Pool->new(1);
+ $pool->add_bug(1, 1);
+ $pool->remove_bug(2);
+
+=head1 DESCRIPTION
+
+Pool object presents an ordered set of bugs.
+
+=cut
+
+use strict;
+use warnings;
+use 5.10.1;
+
+package Bugzilla::Extension::AgileTools::Pool;
+
+use Bugzilla::Error;
+use Bugzilla::Hook;
+use Bugzilla::Util qw(detaint_natural trim);
+use Bugzilla::Bug qw(LogActivityEntry);
+
+use Scalar::Util qw(blessed);
+use List::MoreUtils qw(any);
+
+use base qw(Bugzilla::Object);
+
+
+use constant DB_TABLE => 'agile_pool';
+
+use constant DB_COLUMNS => qw(
+ id
+ name
+ is_active
+);
+
+use constant NUMERIC_COLUMNS => qw(
+ id
+ is_active
+);
+
+use constant UPDATE_COLUMNS => qw(
+ name
+ is_active
+);
+
+use constant VALIDATORS => {name => \&_check_name,};
+
+# Mutators
+##########
+
+sub set_name { $_[0]->set('name', $_[1]); return; }
+sub set_is_active { $_[0]->set('is_active', $_[1]); return; }
+
+# Accessors
+###########
+
+sub is_active { return $_[0]->{is_active}; }
+
+# Validatord
+####
+sub _check_name {
+ my ($invocant, $value) = @_;
+ my $class = blessed($invocant) || $invocant;
+ my $name = trim($value);
+ ThrowUserError('invalid_parameter',
+ {name => 'name', err => 'Name must not be empty'})
+ unless $name;
+
+ if (!blessed($invocant) || lc($invocant->name) ne lc($name)) {
+ ThrowUserError('invalid_parameter',
+ {name => 'name', err => "Pool with name '$name' already exists"})
+ if defined Bugzilla::Extension::AgileTools::Pool->new({name => $name});
+ }
+ return $name;
+}
+
+
+=head1 METHODS
+
+=over
+
+=item C<bugs()>
+
+ Description: Returns a list of bugs in this pool
+ Returns: Array ref of Bug objects
+
+=cut
+
+sub bugs {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ if (!defined $self->{bugs}) {
+ my $pool_bugs = $dbh->selectall_arrayref(
+ "SELECT bug_id, pool_order FROM bug_agile_pool
+ WHERE pool_id = ?", undef, $self->id
+ );
+ my %bug_order = map { ("$_->[0]" => $_->[1]) } @$pool_bugs;
+ $self->{bugs} = Bugzilla::Bug->new_from_list([keys %bug_order]);
+
+ # Set pool and pool_order in bug objects so that they are not fetched again
+ foreach my $bug (@{$self->{bugs}}) {
+ $bug->{pool_order} = $bug_order{$bug->id};
+ $bug->{pool_id} = $self->id;
+ $bug->{pool} = $self;
+ }
+ }
+ return $self->{bugs};
+}
+
+=item C<add_bug($bug, $order)>
+
+ Description: Inserts new bug into the pool
+ Params: $bug - Bug object or ID to be added to this pool
+ $order - (optional) Order of the new bug in this pool,
+ goes last if not given
+ Returns: New order value of the bug in the new pool
+ Note: Bug can be only in one pool at the time and it will be removed
+ from any previous pool.
+
+=cut
+
+sub add_bug {
+ my $self = shift;
+ my ($bug, $order) = @_;
+ if (!defined $bug) {
+ ThrowCodeError("param_required",
+ {param => 'bug', function => 'Pool->remove_bug'});
+ }
+ $bug = Bugzilla::Bug->new($bug) unless ref $bug;
+ if (!defined $bug) {
+ ThrowCodeError("param_invalid",
+ {param => blessed($bug), function => 'Pool->remove_bug',});
+ }
+ ThrowCodeError("param_must_be_numeric",
+ {param => 'order', function => 'Pool->add_bug'})
+ unless (!defined $order || detaint_natural($order));
+
+ my ($old_pool, $old_order);
+ ($order, $old_pool, $old_order) = _adjust_order($bug->id, $self->id, $order);
+ if ($old_pool == $self->id && $old_order == $order) {
+ return 0;
+ }
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ if ($old_pool) {
+
+ # Update old entry
+ $dbh->do(
+ "UPDATE bug_agile_pool
+ SET pool_id = ?, pool_order = ?
+ WHERE bug_id = ?", undef, ($self->id, $order, $bug->id)
+ );
+
+ # Shift other bugs up in old pool
+ $dbh->do(
+ "UPDATE bug_agile_pool
+ SET pool_order = pool_order - 1
+ WHERE pool_id = ? AND pool_order > ? AND bug_id != ?", undef,
+ ($old_pool, $old_order, $bug->id)
+ );
+ }
+ else {
+ # Insert new entry
+ $dbh->do(
+ "INSERT INTO bug_agile_pool (bug_id, pool_id, pool_order)
+ VALUES (?, ?, ?)", undef, ($bug->id, $self->id, $order)
+ );
+ }
+
+ # Note: If the bug is moved inside this pool, the other bugs will probably
+ # get shifted back and forth, but the performance gain from more detailed
+ # queries is probably not worth the introduced complexity...
+
+ # Shift other bugs down in this pool
+ $dbh->do(
+ "UPDATE bug_agile_pool
+ SET pool_order = pool_order + 1
+ WHERE pool_id = ? AND pool_order >= ? AND bug_id != ?", undef,
+ ($self->id, $order, $bug->id)
+ );
+ $dbh->bz_commit_transaction();
+
+ # Update references in the objects
+ if (defined $self->{bugs} && $self->id != $old_pool) {
+ push(@{$self->{bugs}}, $bug);
+ }
+ $bug->{pool} = $self;
+ $bug->{pool_id} = $self->id;
+ $bug->{pool_order} = $order;
+
+ return 1;
+}
+
+# Helper to figure out suitable postion for the bug being added to the pool
+sub _adjust_order {
+ my ($bug_id, $pool_id, $order) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my ($old_pool, $old_order) = $dbh->selectrow_array(
+ "SELECT pool_id, pool_order
+ FROM bug_agile_pool
+ WHERE bug_id = ?", undef, $bug_id
+ );
+ $old_pool //= 0;
+ $old_order //= 0;
+ my $max = $dbh->selectrow_array(
+ "SELECT COUNT(*)
+ FROM bug_agile_pool
+ WHERE pool_id = ?", undef, $pool_id
+ );
+ $max += 1 if ($pool_id != $old_pool);
+
+ if (!defined $order) {
+
+ # See if we can set the order based on a parent...
+ my $parent_at = $dbh->selectrow_array(
+ 'SELECT bug_agile_pool.pool_order
+ FROM bug_agile_pool
+ LEFT JOIN dependencies
+ ON bug_agile_pool.bug_id = dependencies.blocked
+ AND dependencies.dependson = ?
+ WHERE bug_agile_pool.pool_id = ? AND dependson IS NOT NULL '
+ . $dbh->sql_group_by('bug_agile_pool.bug_id')
+ . ' ORDER BY bug_agile_pool.pool_order LIMIT 1', undef, $bug_id, $pool_id
+ );
+ if (defined $parent_at) {
+
+ # If bug is moved inside this pool, (in which case the order is
+ # usually given, but just in case) we need to note that the parent
+ # might move up. so either its parent order or parent order + 1
+ $order
+ = ($old_pool == $pool_id && $old_order && $old_order < $parent_at)
+ ? $parent_at
+ : $parent_at + 1;
+ }
+ else {
+ # ...or just add it to the end
+ $order = $max;
+ }
+ }
+
+ # And make sure it's in acceptaple limits
+ $order = $max if ($order > $max);
+ $order = 1 if ($order <= 0);
+ return ($order, $old_pool, $old_order);
+}
+
+=item C<remove_bug($bug)>
+
+ Description: Remove bug from pool
+ Params: $bug - Bug object or ID to be removed from this pool
+
+=cut
+
+sub remove_bug {
+ my ($self, $bug) = @_;
+ if (!defined $bug) {
+ ThrowCodeError("param_required",
+ {param => 'bug', function => 'Pool->remove_bug'});
+ }
+ $bug = Bugzilla::Bug->new($bug) unless ref $bug;
+ if (!defined $bug) {
+ ThrowCodeError("param_invalid",
+ {param => blessed($bug), function => 'Pool->remove_bug',});
+ }
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+
+ # order in this pool
+ my $order = $dbh->selectrow_array(
+ "SELECT pool_order
+ FROM bug_agile_pool
+ WHERE pool_id = ? AND bug_id = ?", undef, ($self->id, $bug->id)
+ );
+
+ if (defined $order) {
+
+ # Delete old entry
+ $dbh->do(
+ "DELETE FROM bug_agile_pool
+ WHERE pool_id = ? AND bug_id = ?", undef, ($self->id, $bug->id)
+ );
+
+ # Shift bugs in old pool
+ $dbh->do(
+ "UPDATE bug_agile_pool
+ SET pool_order = pool_order - 1
+ WHERE pool_id = ? AND pool_order > ?", undef, ($self->id, $order)
+ );
+ delete $self->{bugs};
+ delete $bug->{pool};
+ delete $bug->{pool_id};
+ delete $bug->{pool_order};
+ }
+ $dbh->bz_commit_transaction();
+
+ return;
+}
+
+=item C<is_sprint()>
+
+ Description: Check if pool is a sprint
+
+=cut
+
+sub is_sprint {
+ my $id = shift;
+
+ # If called as Pool method or with a pool object
+ $id = $id->id if ref($id);
+ detaint_natural($id);
+ my $cache = Bugzilla->request_cache->{sprint_pools} ||= {};
+ if (!defined $cache->{$id}) {
+ $cache->{$id}
+ = Bugzilla->dbh->selectrow_array(
+ "SELECT COUNT(id) FROM agile_sprint WHERE id = ?",
+ undef, $id);
+ }
+ return $cache->{$id};
+}
+
+=item C<is_backlog()>
+
+ Description: Check if pool is a backlog
+
+=cut
+
+sub is_backlog {
+ my $id = shift;
+
+ # If called as Pool method or with a pool object
+ $id = $id->id if ref($id);
+ detaint_natural($id);
+ my $cache = Bugzilla->request_cache->{backlog_pools} ||= {};
+ if (!defined $cache->{$id}) {
+ $cache->{$id}
+ = Bugzilla->dbh->selectrow_array(
+ "SELECT COUNT(pool_id) FROM agile_backlog WHERE pool_id = ?",
+ undef, $id);
+ }
+ return $cache->{$id};
+}
+
+## RED HAT EXTENSION START 1323078
+
+=item C<team_id>
+
+ Description: Get ID for team pool belongs to
+
+=cut
+
+sub team_id {
+ my $self = shift;
+
+ my $handle;
+ if ($self->is_sprint) {
+ $handle = Bugzilla::Extension::AgileTools::Sprint->new($self->id);
+ }
+ elsif ($self->is_backlog) {
+ $handle = Bugzilla::Extension::AgileTools::Backlog->new($self->id);
+ }
+
+ return ($handle->team_id) if ($handle && $handle->team_id);
+ return;
+}
+
+=item C<team>
+
+ Description: Get team pool belongs to
+
+=cut
+
+sub team {
+ my $self = shift;
+
+ if (!defined $self->{team}) {
+ my $handle;
+ if ($self->is_sprint) {
+ $handle = Bugzilla::Extension::AgileTools::Sprint->new($self->id);
+ }
+ elsif ($self->is_backlog) {
+ $handle = Bugzilla::Extension::AgileTools::Backlog->new($self->id);
+ }
+ $self->{team} = $handle->team if ($handle->team_id);
+ }
+
+ return $self->{team};
+}
+
+## RED HAT EXTENSION END 1323078
+
+=back
+
+=head1 RELATED METHODS
+
+=head2 Bugzilla::Bug object methods
+
+The L<Bugzilla::Bug> object is extended to provide easy access to pool
+
+=over
+
+=item C<Bugzilla::Bug::pool_order>
+
+ Description: Returns the pool order of bug or undef if bug is not in a pool
+
+=cut
+
+BEGIN {
+ *Bugzilla::Bug::pool_order = sub {
+ my $self = shift;
+ if (!exists $self->{pool_order}) {
+ my ($pool_id, $pool_order) = Bugzilla->dbh->selectrow_array("
+ SELECT pool_id, pool_order FROM bug_agile_pool
+ WHERE bug_id = ?", undef, $self->id);
+
+ $self->{pool_id} = $pool_id || 0;
+ $self->{pool_order} = $pool_order || 0;
+ }
+ return $self->{pool_order};
+ };
+
+=item C<Bugzilla::Bug::pool_id>
+
+ Description: Returns the pool id of bug or undef if bug is not in
+ a pool
+
+=cut
+
+ *Bugzilla::Bug::pool_id = sub {
+ my $self = shift;
+ if (!exists $self->{pool_id}) {
+ my ($pool_id, $pool_order) = Bugzilla->dbh->selectrow_array("
+ SELECT pool_id, pool_order FROM bug_agile_pool
+ WHERE bug_id = ?", undef, $self->id);
+
+ $self->{pool_id} = $pool_id || 0;
+ $self->{pool_order} = $pool_order || 0;
+ }
+ return $self->{pool_id};
+ };
+
+=item C<Bugzilla::Bug::pool>
+
+ Description: Returns the Pool object of the bug or undef if bug is not in
+ a pool
+
+=cut
+
+ *Bugzilla::Bug::pool = sub {
+ my $self = shift;
+ if (!exists $self->{pool}) {
+ $self->{pool}
+ = $self->pool_id
+ ? new Bugzilla::Extension::AgileTools::Pool($self->pool_id)
+ : undef;
+ }
+ return $self->{pool};
+ };
+
+ *Bugzilla::Bug::set_pool_id = sub {
+ my ($self, $pool_id) = @_;
+ delete $self->{pool};
+ $self->{pool_id} = $pool_id;
+ };
+
+ *Bugzilla::Bug::set_pool_order = sub {
+ my ($self, $order) = @_;
+ $self->{pool_order} = $order;
+ $self->{pool_order_set} = 1;
+ };
+
+ *Bugzilla::Bug::in_pool_team = sub {
+ my $self = shift;
+ my $user = shift;
+ if ($user && $self->pool_id) {
+ if ($self->pool->is_sprint) {
+ my $sprint = Bugzilla::Extension::AgileTools::Sprint->new($self->pool_id);
+ return any { $sprint->team_id == $_->id } @{$user->agile_teams};
+ }
+ elsif ($self->pool->is_backlog) {
+ my $bl = Bugzilla::Extension::AgileTools::Backlog->new($self->pool_id);
+ return any { $bl->team_id == $_->id } @{$user->agile_teams};
+ }
+ }
+
+ return;
+ };
+
+} # END BEGIN
+
+1;
+
+__END__
+
+=back
+
+=head1 SEE ALSO
+
+L<Bugzilla::Object>
+
+