bugzilla-4intranet/extensions/testopia/lib/Testopia/TestPlan.pm

1515 lines
41 KiB
Perl

# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Test Runner System.
#
# The Initial Developer of the Original Code is Maciej Maczynski.
# Portions created by Maciej Maczynski are Copyright (C) 2001
# Maciej Maczynski. All Rights Reserved.
#
# Contributor(s): Greg Hendricks <ghendricks@novell.com>
package Testopia::TestPlan;
use strict;
use Bugzilla::User;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Config;
use Bugzilla::Constants;
use Bugzilla::Version;
use Bugzilla::Bug;
use Testopia::Constants;
use Testopia::Util;
use Testopia::TestTag;
use Testopia::Product;
use Testopia::Attachment;
use Text::Diff;
use JSON;
use base qw(Exporter Bugzilla::Object);
@Testopia::TestPlan::EXPORT = qw(lookup_type_by_name lookup_type);
###############################
#### Initialization ####
###############################
=head1 FIELDS
plan_id
product_id
author_id
type_id
default_product_version
name
creation_date
isactive
wiki
=cut
use constant DB_TABLE => "test_plans";
use constant NAME_FIELD => "name";
use constant ID_FIELD => "plan_id";
use constant DB_COLUMNS => qw(
plan_id
product_id
author_id
type_id
default_product_version
name
wiki
creation_date
isactive
);
use constant REQUIRED_CREATE_FIELDS => qw(product_id author_id type_id default_product_version name);
use constant UPDATE_COLUMNS => qw(product_id type_id default_product_version name isactive wiki);
use constant VALIDATORS => {
product_id => \&_check_product,
author_id => \&_check_author,
type_id => \&_check_type,
isactive => \&_check_isactive,
};
use constant NAME_MAX_LENGTH => 255;
sub report_columns {
my $self = shift;
my %columns;
# Changes here need to match Report.pm
$columns{'Type'} = "plan_type";
$columns{'Version'} = "default_product_version";
$columns{'Product'} = "product";
$columns{'Archived'} = "archived";
$columns{'Tags'} = "tags";
$columns{'Author'} = "author";
my @result;
push @result, {'name' => $_, 'id' => $columns{$_}} foreach (sort(keys %columns));
unshift @result, {'name' => '<none>', 'id'=> ''};
return \@result;
}
###############################
#### Validators ####
###############################
sub _check_product {
my ($invocant, $product_id) = @_;
if (ref $invocant){
ThrowUserError("plan-has-children")
if (($invocant->test_case_count || $invocant->test_run_count) && $invocant->product_id != $product_id);
}
$product_id = trim($product_id);
my $product;
if ($product_id !~ /^\d+$/ ){
$product = Bugzilla::Product::check_product($product_id);
$product = Testopia::Product->new($product->id);
}
else {
$product = Testopia::Product->new($product_id);
}
ThrowUserError("invalid-test-id-non-existent", {'id' => $product_id, 'type' => 'product'}) unless $product;
ThrowUserError("testopia-create-denied", {'object' => 'plan'}) unless $product->canedit;
if (ref $invocant){
$invocant->{'product'} = $product;
return $product->id;
}
return $product;
}
sub _check_author {
my ($invocant, $author) = @_;
$author = trim($author);
if ($author =~ /^\d+$/){
$author = Bugzilla::User->new($author);
}
else {
my $id = login_to_id($author, THROW_ERROR);
return $id;
}
return $author->id;
}
sub _check_type {
my ($invocant, $type_id) = @_;
$type_id = trim($type_id);
trick_taint($type_id);
if ($type_id !~ /^\d+$/){
$type_id = lookup_type_by_name($type_id) || $type_id;
}
Testopia::Util::validate_selection($type_id, 'type_id', 'test_plan_types');
return $type_id;
}
sub _check_product_version {
my ($invocant, $version, $product) = @_;
if (ref $invocant){
$product = $invocant->product;
}
$version = trim($version);
$version = Bugzilla::Version->check({product => $product, name => $version});
return $version->name;
}
sub _check_isactive {
my ($invocant, $isactive) = @_;
ThrowCodeError('bad_arg', {argument => 'isactive', function => 'set_isactive'}) unless ($isactive =~ /(1|0)/);
return $isactive;
}
###############################
#### Mutators ####
###############################
sub set_name { $_[0]->set('name', $_[1]); }
sub set_wiki { $_[0]->set('wiki', $_[1]); }
sub set_type { $_[0]->set('type_id', $_[1]); }
sub set_isactive { $_[0]->set('isactive', $_[1]); }
sub set_product_id { $_[0]->set('product_id', $_[1]); }
sub set_default_product_version {
my ($self, $value) = @_;
$value = $self->_check_product_version($value);
$self->set('default_product_version', $value);
}
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my $param = shift;
# We want to be able to supply an empty object to the templates for numerous
# lists etc. This is much cleaner than exporting a bunch of subroutines and
# adding them to $vars one by one. Probably just Laziness shining through.
if (ref $param eq 'HASH'){
if (keys %$param){
bless($param, $class);
return $param;
}
bless($param, $class);
return $param;
}
unshift @_, $param;
my $self = $class->SUPER::new(@_);
return $self;
}
sub run_create_validators {
my $class = shift;
my $params = $class->SUPER::run_create_validators(@_);
my $product = $params->{product_id};
$params->{default_product_version} = $class->_check_product_version($params->{default_product_version}, $product);
$params->{product_id} = $product->id;
return $params;
}
sub create {
my ($class, $params) = @_;
$class->SUPER::check_required_create_fields($params);
my $field_values = $class->run_create_validators($params);
$field_values->{creation_date} = Testopia::Util::get_time_stamp();
$field_values->{isactive} = 1;
#We have to handle the plan document text a bit differently since it has its own table.
my $plan_document = $field_values->{text};
my $tags = $field_values->{tags};
delete $field_values->{text};
delete $field_values->{tags};
my $self = $class->SUPER::insert_create_data($field_values);
$self->store_text($self->id, $field_values->{'author_id'}, $plan_document, $field_values->{creation_date});
$self->add_tag($tags);
# Add permissions for the plan
$self->add_tester($self->{'author_id'},15);
if (Bugzilla->params->{'testopia-default-plan-testers-regexp'}) {
$self->set_tester_regexp( Bugzilla->params->{"testopia-default-plan-testers-regexp"}, 3);
$self->derive_regexp_testers(Bugzilla->params->{'testopia-default-plan-testers-regexp'});
}
# Create default category
unless (scalar @{$self->product->categories}){
require Testopia::Category;
my $category = Testopia::Category->create(
{'name' => '--default--',
'description' => 'Default product category for test cases',
'product_id' => $self->product->id });
}
delete $self->{'product'};
return $self;
}
sub update {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $timestamp = Testopia::Util::get_time_stamp();
$dbh->bz_start_transaction();
my $changed = $self->SUPER::update();
foreach my $field (keys %$changed){
Testopia::Util::log_activity('plan', $self->id, $field, $timestamp, $changed->{$field}->[0], $changed->{$field}->[1]);
}
$dbh->bz_commit_transaction();
}
###############################
#### Methods ####
###############################
=head2 store_text
Stores the test plan document in the test_plan_texts
table. Used by both store and copy. Accepts the the test plan id,
author id, text, and a an optional timestamp.
=cut
sub store_text {
my $self = shift;
my $dbh = Bugzilla->dbh;
my ($key, $author, $text, $timestamp) = @_;
if (!defined $timestamp){
($timestamp) = Testopia::Util::get_time_stamp();
}
$text ||= '';
trick_taint($text);
my $version = $self->version || 0;
$dbh->do("INSERT INTO test_plan_texts
(plan_id, plan_text_version, who, creation_ts, plan_text)
VALUES(?,?,?,?,?)",
undef, $key, ++$version, $author,
$timestamp, $text);
$self->{'version'} = $version;
}
=head2 clone
Creates a copy of this test plan. Accepts the name of the new plan
and a boolean representing whether to copy the plan document as well.
=cut
sub clone {
my $self = shift;
my ($name, $author, $product_id, $version, $store_doc) = @_;
$store_doc = 1 unless defined($store_doc);
my $dbh = Bugzilla->dbh;
# Exclude the auto-incremented field from the column list.
my $columns = join(", ", grep {$_ ne 'plan_id'} DB_COLUMNS);
my ($timestamp) = Testopia::Util::get_time_stamp();
$dbh->do("INSERT INTO test_plans ($columns) VALUES (?,?,?,?,?,?,?)",
undef, ($product_id, $author,
$self->{'type_id'}, $version, $name,
$timestamp, 1));
my $key = $dbh->bz_last_key( 'test_plans', 'plan_id' );
my $text = $store_doc ? $self->text->{'plan_text'} : '';
$self->store_text($key, $self->{'author_id'}, $text, $timestamp);
return $key;
}
=head2 toggle_archive
Toggles the archive bit on the plan.
=cut
sub toggle_archive {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $oldvalue = $self->isactive;
my $newvalue = $oldvalue == 1 ? 0 : 1;
$self->set_isactive($newvalue);
$self->update;
}
=head2 add_tag
Associates a tag with this test plan. Takes the tag_id of the tag
to link.
=cut
sub add_tag {
my $self = shift;
my $dbh = Bugzilla->dbh;
my @tags;
foreach my $t (@_){
if (ref $t eq 'ARRAY'){
push @tags, $_ foreach @$t;
}
else{
push @tags, split(',', $t);
}
}
foreach my $name (@tags){
my $tag = Testopia::TestTag->create({'tag_name' => $name});
$tag->attach($self);
}
}
=head2 remove_tag
Removes a tag from this plan. Takes the tag_id of the tag to remove.
=cut
sub remove_tag {
my $self = shift;
my ($tag_name) = @_;
my $tag = Testopia::TestTag->check_tag($tag_name);
ThrowUserError('testopia-unknown-tag', {'name' => $tag}) unless $tag;
my $dbh = Bugzilla->dbh;
$dbh->do("DELETE FROM test_plan_tags
WHERE tag_id=? AND plan_id=?",
undef, ($tag->id, $self->{'plan_id'}));
}
sub get_used_categories {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $ref = $dbh->selectall_arrayref(
"SELECT DISTINCT test_case_categories.category_id AS id, name
FROM test_case_categories
JOIN test_cases ON test_cases.category_id = test_case_categories.category_id
JOIN test_case_plans ON test_cases.case_id = test_case_plans.case_id
WHERE plan_id = ?
ORDER BY name",
{'Slice'=>{}}, $self->id);
return $ref;
}
=head2 get_plan_types
Returns a list of types from the test_plan_types table
=cut
sub get_plan_types {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $types = $dbh->selectall_arrayref(
"SELECT type_id AS id, name
FROM test_plan_types
ORDER BY name",
{"Slice"=>{}});
return $types;
}
=head2 last_changed
Returns the date of the last change in the history table
=cut
sub last_changed {
my $self = shift;
my $dbh = Bugzilla->dbh;
my ($date) = $dbh->selectrow_array(
"SELECT MAX(changed)
FROM test_plan_activity
WHERE plan_id = ?",
undef, $self->id);
return $self->{'creation_date'} unless $date;
return $date;
}
=head2 plan_type_ref
Returns a type name matching the given type id
=cut
sub plan_type_ref {
my $self = shift;
my $type_id = shift;
my $dbh = Bugzilla->dbh;
my $type = $dbh->selectrow_hashref(
"SELECT type_id AS id, name, description
FROM test_plan_types
WHERE type_id = ?",
undef, $type_id);
return $type;
}
=head2 check_plan_type
Returns true if a type with the given name exists
=cut
sub check_plan_type {
my $self = shift;
my $name = shift;
my $dbh = Bugzilla->dbh;
my $type = $dbh->selectrow_hashref(
"SELECT 1
FROM test_plan_types
WHERE name = ?",
undef, $name);
return $type;
}
sub check_tester {
my $self = shift;
my $userid = shift;
my $dbh = Bugzilla->dbh;
my ($exists) = $dbh->selectrow_array(
"SELECT 1
FROM test_plan_permissions
WHERE userid = ? AND plan_id = ? AND grant_type = ?",
undef, ($userid, $self->id, GRANT_DIRECT));
return $exists;
}
=head2 update_plan_type
Update the given type
=cut
sub update_plan_type {
my $self = shift;
my ($type_id, $name, $desc) = @_;
my $dbh = Bugzilla->dbh;
my $type = $dbh->do(
"UPDATE test_plan_types
SET name = ?, description = ?
WHERE type_id = ?",
undef, ($name, $desc, $type_id));
}
=head2 add_plan_type
Add the given type
=cut
sub add_plan_type {
my $self = shift;
my ($name, $desc) = @_;
my $dbh = Bugzilla->dbh;
my $type = $dbh->do(
"INSERT INTO test_plan_types (name, description) VALUES(?, ?)",
undef, ($name, $desc));
}
=head2 get_fields
Returns a list of fields from the fielddefs table associated with
a plan
=cut
sub get_fields {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $types = $dbh->selectall_arrayref(
"SELECT fieldid AS id, description AS name
FROM test_fielddefs
WHERE table_name=?",
{"Slice"=>{}}, "test_plans");
unshift @$types, {id => 'text', name => 'Document'};
unshift @$types, {id => '[Creation]', name => '[Created]'};
return $types;
}
=head2 get_text_versions
Returns the list of versions of the plan document.
=cut
sub get_text_versions {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $versions = $dbh->selectall_arrayref(
"SELECT plan_text_version AS id, plan_text_version AS name
FROM test_plan_texts
WHERE plan_id = ?
ORDER BY plan_text_version",
{'Slice' =>{}}, $self->id);
return $versions;
}
=head2 diff_plan_doc
Returns either the diff of the latest version with a new text
or two numerical versions.
=cut
sub diff_plan_doc {
my $self = shift;
my ($new, $old) = @_;
$old ||= $self->version;
my $dbh = Bugzilla->dbh;
my $newdoc;
my $text = $new;
if (detaint_natural($new)){
# we are looking for a version
$newdoc = $dbh->selectrow_array(
"SELECT plan_text FROM test_plan_texts
WHERE plan_id = ? AND plan_text_version = ?",
undef, ($self->{'plan_id'}, $new));
}
else {
$newdoc = $text;
}
detaint_natural($old);
my $olddoc = $dbh->selectrow_array(
"SELECT plan_text FROM test_plan_texts
WHERE plan_id = ? AND plan_text_version = ?",
undef, ($self->{'plan_id'}, $old));
my $diff = diff(\$newdoc, \$olddoc);
return $diff
}
=head2 history
Returns a reference to a list of history entries from the
test_plan_activity table.
=cut
sub history {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $ref = $dbh->selectall_arrayref(
"SELECT defs.description AS what,
p.login_name AS who, a.changed, a.oldvalue, a.newvalue
FROM test_plan_activity AS a
JOIN test_fielddefs AS defs ON a.fieldid = defs.fieldid
JOIN profiles AS p ON a.who = p.userid
WHERE a.plan_id = ?",
{'Slice'=>{}}, $self->{'plan_id'});
foreach my $row (@$ref){
if ($row->{'what'} eq 'Product'){
$row->{'oldvalue'} = $self->lookup_product($row->{'oldvalue'});
$row->{'newvalue'} = $self->lookup_product($row->{'newvalue'});
}
elsif ($row->{'what'} eq 'Plan Type'){
$row->{'oldvalue'} = lookup_type($row->{'oldvalue'});
$row->{'newvalue'} = lookup_type($row->{'newvalue'});
}
$row->{'changed'} = format_time($row->{'changed'}, TIME_FORMAT);
}
return $ref;
}
sub copy_permissions {
my $self = shift;
my ($planid) = @_;
my $dbh = Bugzilla->dbh;
my ($regexp, $perms) = $dbh->selectrow_array(
"SELECT user_regexp, permissions
FROM test_plan_permissions_regexp
WHERE plan_id = ?",undef, $self->id);
$dbh->do("INSERT INTO test_plan_permissions_regexp (plan_id, user_regexp, permissions)
VALUES(?,?,?)", undef,($planid, $regexp, $perms)) if $regexp;
my $ref = $dbh->selectall_arrayref(
"SELECT userid, permissions
FROM test_plan_permissions
WHERE plan_id = ? AND grant_type = ?",
{'Slice' =>{}}, ($self->id, GRANT_DIRECT));
foreach my $row (@$ref){
$dbh->do("INSERT INTO test_plan_permissions (userid, plan_id, permissions, grant_type)
VALUES(?,?,?,?)", undef, ($row->{'userid'}, $planid, $row->{'permissions'}, GRANT_DIRECT));
}
}
=head2 lookup_type
Takes an ID of the type field and returns the value
=cut
sub lookup_type {
my ($id) = @_;
my $dbh = Bugzilla->dbh;
my ($value) = $dbh->selectrow_array(
"SELECT name
FROM test_plan_types
WHERE type_id = ?",
undef, $id);
return $value;
}
=head2 lookup_type_by_name
Returns the id of the type name passed.
=cut
sub lookup_type_by_name {
my ($name) = @_;
my $dbh = Bugzilla->dbh;
my ($value) = $dbh->selectrow_array(
"SELECT type_id
FROM test_plan_types
WHERE name = ?",
undef, $name);
return $value;
}
=head2 lookup_product
Takes an ID of the status field and returns the value
=cut
sub lookup_product {
my $self = shift;
my ($id) = @_;
my $dbh = Bugzilla->dbh;
my ($value) = $dbh->selectrow_array(
"SELECT name
FROM products
WHERE id = ?",
undef, $id);
return $value;
}
=head2 lookup_product_by_name
Returns the id of the product name passed.
=cut
sub lookup_product_by_name {
my ($name) = @_;
my $dbh = Bugzilla->dbh;
# TODO 2.22 use Product.pm
my ($value) = $dbh->selectrow_array(
"SELECT id
FROM products
WHERE name = ?",
undef, $name);
return $value;
}
sub set_tester_regexp {
my $self = shift;
my ($regexp, $permissions) = @_;
my $dbh = Bugzilla->dbh;
ThrowUserError("invalid_regexp") unless (eval {qr/$regexp/});
return unless $regexp;
my ($count) = $dbh->selectrow_array(
"SELECT COUNT(*)
FROM profiles
WHERE login_name REGEXP(?)",
undef, $regexp);
ThrowUserError("testopia-regexp-too-inclusive") if $count > Bugzilla->params->{'testopia-max-allowed-plan-testers'};
my ($is, $oldreg, $oldperms) = $dbh->selectrow_array(
"SELECT 1, user_regexp, permissions
FROM test_plan_permissions_regexp
WHERE plan_id = ?",undef, $self->id);
return unless ($oldreg ne $regexp || $oldperms != $permissions);
if ($is){
$dbh->do("UPDATE test_plan_permissions_regexp
SET user_regexp = ?, permissions = ?
WHERE plan_id = ?", undef, ($regexp, $permissions, $self->id));
}
else {
$dbh->do("INSERT INTO test_plan_permissions_regexp
(plan_id, user_regexp, permissions)
VALUES(?,?,?)",
undef, ($self->id, $regexp, $permissions));
}
$self->derive_regexp_testers($regexp);
}
sub derive_regexp_testers {
my $self = shift;
my $regexp = shift;
ThrowUserError("invalid_regexp") unless (eval {qr/$regexp/});
my $dbh = Bugzilla->dbh;
# Get the permissions of the regexp testers so we can set it later.
my ($permissions) = $dbh->selectrow_array(
"SELECT permissions
FROM test_plan_permissions_regexp
WHERE plan_id = ?", undef, $self->id);
my $sth = $dbh->prepare("SELECT profiles.userid, profiles.login_name, plan_id
FROM profiles
LEFT JOIN test_plan_permissions
ON test_plan_permissions.userid = profiles.userid
AND test_plan_permissions.plan_id = ?
AND grant_type = ?");
my $plan_add = $dbh->prepare("INSERT INTO test_plan_permissions
(userid, plan_id, permissions, grant_type)
VALUES (?,?,?,?)");
my $plan_update = $dbh->prepare("UPDATE test_plan_permissions
SET permissions = ?
WHERE userid = ? AND plan_id = ? AND grant_type = ?");
my $plan_del = $dbh->prepare("DELETE FROM test_plan_permissions
WHERE userid = ? AND plan_id = ?
AND grant_type = ?");
$sth->execute($self->id, GRANT_REGEXP);
while (my ($userid, $login, $present) = $sth->fetchrow_array()) {
if (($regexp =~ /\S+/) && ($login =~ m/$regexp/i)){
if ($present){
$plan_update->execute($permissions, $userid, $self->id, GRANT_REGEXP)
}
else {
$plan_add->execute($userid, $self->id, $permissions, GRANT_REGEXP)
}
}
else {
$plan_del->execute($userid, $self->id, GRANT_REGEXP) if $present;
}
}
}
sub remove_tester {
my $self = shift;
my ($userid) = @_;
my $dbh = Bugzilla->dbh;
$dbh->do("DELETE FROM test_plan_permissions
WHERE userid = ? AND plan_id = ? AND grant_type = ?",
undef, ($userid, $self->id, GRANT_DIRECT));
}
sub add_tester {
my $self = shift;
my ($userid, $perms) = @_;
my $dbh = Bugzilla->dbh;
my ($is) = $dbh->selectrow_array(
"SELECT userid
FROM test_plan_permissions
WHERE userid = ? AND plan_id = ? AND grant_type = ?",
undef, ($userid, $self->id, GRANT_DIRECT));
return if $is;
$dbh->do("INSERT INTO test_plan_permissions
(userid, plan_id, permissions, grant_type)
VALUES(?,?,?,?)",
undef, ($userid, $self->id, $perms, GRANT_DIRECT));
}
sub update_tester {
my $self = shift;
my ($userid, $perms) = @_;
my $dbh = Bugzilla->dbh;
$dbh->do("UPDATE test_plan_permissions SET permissions = ?
WHERE userid = ? AND plan_id = ? AND grant_type = ?",
undef, ($perms, $userid, $self->id, GRANT_DIRECT));
}
=head2 obliterate
Removes this plan and all things that reference it.
=cut
sub obliterate {
my $self = shift;
my ($cgi, $template) = @_;
my $vars;
my $dbh = Bugzilla->dbh;
my $progress_interval = 250;
my $i = 0;
my $total = scalar @{$self->test_cases} + scalar @{$self->test_runs};
foreach my $obj (@{$self->attachments}){
$obj->unlink_plan($self->id);
}
foreach my $obj (@{$self->test_runs}){
$obj->obliterate($cgi, $template);
}
foreach my $obj (@{$self->test_cases}){
$i++;
if ($cgi && $i % $progress_interval == 0){
print $cgi->multipart_end;
print $cgi->multipart_start;
$vars->{'complete'} = $i;
$vars->{'total'} = $total;
$vars->{'process'} = "Deleting test cases";
$template->process("testopia/progress.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
}
$obj->obliterate if (scalar @{$obj->plans} == 1);
}
$dbh->do("DELETE FROM test_plan_texts WHERE plan_id = ?", undef, $self->id);
$dbh->do("DELETE FROM test_plan_tags WHERE plan_id = ?", undef, $self->id);
$dbh->do("DELETE FROM test_plan_activity WHERE plan_id = ?", undef, $self->id);
$dbh->do("DELETE FROM test_plan_permissions WHERE plan_id = ?", undef, $self->id);
$dbh->do("DELETE FROM test_plan_permissions_regexp WHERE plan_id = ?", undef, $self->id);
$dbh->do("DELETE FROM test_case_plans WHERE plan_id = ?", undef, $self->id);
$dbh->do("DELETE FROM test_plans WHERE plan_id = ?", undef, $self->id);
return 1;
}
=head2 canview
Returns true if the logged in user has rights to view this plan
=cut
sub canview {
my $self = shift;
return 1 if Bugzilla->user->in_group('Testers');
return 1 if $self->get_user_rights(Bugzilla->user->id) & TR_READ;
return 0;
}
=head2 canedit
Returns true if the logged in user has rights to edit this plan
=cut
sub canedit {
my $self = shift;
return 1 if Bugzilla->user->in_group('Testers');
return 1 if $self->get_user_rights(Bugzilla->user->id) & TR_WRITE;
return 0;
}
=head2 candelete
Returns true if the logged in user has rights to delete this plan
=cut
sub candelete {
my $self = shift;
return 1 if Bugzilla->user->in_group('admin');
return 0 unless Bugzilla->params->{"allow-test-deletion"};
return 1 if Bugzilla->user->in_group('Testers') && Bugzilla->params->{"testopia-allow-group-member-deletes"};
return 1 if $self->get_user_rights(Bugzilla->user->id) & TR_DELETE;
return 0;
}
sub canadmin {
my $self = shift;
return 1 if Bugzilla->user->in_group("admin");
return 1 if ($self->get_user_rights(Bugzilla->user->id) & TR_ADMIN);
return 0;
}
sub get_user_rights {
my $self = shift;
my ($userid) = @_;
my $dbh = Bugzilla->dbh;
my ($perms) = $dbh->selectrow_array(
"SELECT permissions FROM test_plan_permissions
WHERE userid = ? AND plan_id = ?",
undef, ($userid, $self->id));
return $perms || 0;
}
sub get_case_tags {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $tags = $dbh->selectcol_arrayref(
"SELECT DISTINCT test_tags.tag_id FROM test_case_tags
INNER JOIN test_tags ON test_case_tags.tag_id = test_tags.tag_id
INNER JOIN test_case_plans on test_case_plans.case_id = test_case_tags.case_id
WHERE test_case_plans.plan_id = ?", undef, $self->id);
my @tags;
foreach my $id (@$tags){
push @tags, Testopia::TestTag->new($id);
}
return \@tags;
}
sub TO_JSON {
my $self = shift;
my $obj;
my $json = new JSON;
foreach my $field ($self->DB_COLUMNS){
$obj->{$field} = $self->{$field};
}
$obj->{'product_name'} = $self->product->name if $self->product;
$obj->{'run_count'} = $self->test_run_count;
$obj->{'case_count'} = $self->test_case_count;
$obj->{'author_name'} = $self->author->login;
$obj->{'plan_type'} = $self->plan_type;
$obj->{'canedit'} = $self->canedit;
$obj->{'canview'} = $self->canview;
$obj->{'candelete'} = $self->candelete;
$obj->{'link_url'} = 'tr_show_plan.cgi?plan_id=' . $self->id;
$obj->{'type'} = $self->type;
$obj->{'id'} = $self->id;
$obj->{'creation_date'} = format_time($self->{'creation_date'}, TIME_FORMAT);
return $json->encode($obj);
}
###############################
#### Accessors ####
###############################
=head1 ACCESSOR METHODS
=head2 id
Returns the ID for this object
=head2 creation_date
Returns the creation timestamp for this object
=head2 product_version
Returns the product version for this object
=head2 product_id
Returns the product id for this object
=head2 author
Returns a Bugzilla::User object representing the plan author
=head2 name
Returns the name of this plan
=head2 type_id
Returns the type id of this plan
=head2 isactive
Returns true if this plan is not archived
=cut
sub fields {
my $self = shift;
my @fields = qw();
return @fields;
}
sub id { return $_[0]->{'plan_id'}; }
sub creation_date { return $_[0]->{'creation_date'}; }
sub product_version { return $_[0]->{'default_product_version'}; }
sub product_id { return $_[0]->{'product_id'}; }
sub author { return Bugzilla::User->new($_[0]->{'author_id'}); }
sub name { return $_[0]->{'name'}; }
sub wiki { return $_[0]->{'wiki'}; }
sub type_id { return $_[0]->{'type_id'}; }
sub isactive { return $_[0]->{'isactive'}; }
=head2 type
Returns 'case'
=cut
sub type {
my $self = shift;
$self->{'type'} = 'plan';
return $self->{'type'};
}
sub tester_regexp {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my ($regexp) = $dbh->selectrow_array(
"SELECT user_regexp
FROM test_plan_permissions_regexp
WHERE plan_id = ?", undef, $self->id);
return $regexp;
}
sub tester_regexp_permissions {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my ($perms) = $dbh->selectrow_array(
"SELECT permissions
FROM test_plan_permissions_regexp
WHERE plan_id = ?", undef, $self->id);
my $p;
$p->{'read'} = $perms >= TR_READ;
$p->{'write'} = $perms >= TR_WRITE;
$p->{'delete'} = $perms >= TR_DELETE;
$p->{'admin'} = $perms >= TR_ADMIN;
return $p;
}
sub access_list {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my $ref = $dbh->selectall_arrayref(
"SELECT tpp.userid, permissions
FROM test_plan_permissions AS tpp
JOIN profiles ON profiles.userid = tpp.userid
WHERE plan_id = ? AND grant_type = ?
ORDER BY profiles.realname", {'Slice' =>{}}, ($self->id, GRANT_DIRECT));
my @rows;
foreach my $row (@$ref){
push @rows, {'user' => Bugzilla::User->new($row->{'userid'}),
'read' => $row->{'permissions'} >= TR_READ,
'write' => $row->{'permissions'} >= TR_WRITE,
'delete' => $row->{'permissions'} >= TR_DELETE,
'admin' => $row->{'permissions'} >= TR_ADMIN,
};
}
$self->{'access_list'} = \@rows;
return $self->{'access_list'};
}
sub has_admin {
my ($self, $deleted) = @_;
my $dbh = Bugzilla->dbh;
my $ref = $dbh->selectcol_arrayref(
"SELECT userid
FROM test_plan_permissions
WHERE plan_id = ?
AND userid != ?
AND permissions >= ?",undef,
$self->id, $deleted, TR_ADMIN);
return scalar @$ref;
}
=head2 attachments
Returns a reference to a list of attachments on this plan
=cut
sub attachments {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
return $self->{'attachments'} if exists $self->{'attachments'};
my $attachments = $dbh->selectcol_arrayref(
"SELECT attachment_id
FROM test_plan_attachments
WHERE plan_id = ?",
undef, $self->{'plan_id'});
my @attachments;
foreach my $attach (@{$attachments}){
push @attachments, Testopia::Attachment->new($attach);
}
$self->{'attachments'} = \@attachments;
return $self->{'attachments'};
}
=head2 bugs
Returns a reference to a list of Bugzilla::Bug objects associated
with this plan
=cut
sub bugs {
my $self = shift;
my $dbh = Bugzilla->dbh;
return $self->{'bugs'} if exists $self->{'bugs'};
my $ref = $dbh->selectcol_arrayref(
"SELECT DISTINCT bug_id
FROM test_case_bugs
JOIN test_cases ON test_case_bugs.case_id = test_cases.case_id
JOIN test_case_plans ON test_case_plans.case_id = test_cases.case_id
WHERE test_case_plans.plan_id = ?",
undef, $self->id);
my @bugs;
foreach my $id (@{$ref}){
push @bugs, Bugzilla::Bug->new($id, Bugzilla->user->id);
}
$self->{'bugs'} = \@bugs if @bugs;
$self->{'bug_list'} = join(',', @$ref);
return $self->{'bugs'};
}
=head2 product
Returns the product this plan is associated with
=cut
sub product {
my ($self) = @_;
return $self->{'product'} if exists $self->{'product'};
$self->{'product'} = Testopia::Product->new($self->product_id);
return $self->{'product'};
}
sub case_ids {
my $self = shift;
my $dbh = Bugzilla->dbh;
return $self->{'case_ids'} if exists $self->{'case_ids'};
my $ref = $dbh->selectcol_arrayref(
"SELECT case_id FROM test_case_plans
WHERE plan_id = ?", undef,
$self->id);
$self->{'case_ids'} = $ref;
return $self->{'case_ids'};
}
=head2 test_cases
Returns a reference to a list of Testopia::TestCase objects linked
to this plan
=cut
sub test_cases {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
return $self->{'test_cases'} if exists $self->{'test_cases'};
require Testopia::TestCase;
my $caseids = $dbh->selectcol_arrayref(
"SELECT case_id FROM test_case_plans
WHERE plan_id = ?",
undef, $self->{'plan_id'});
my @cases;
foreach my $id (@{$caseids}){
push @cases, Testopia::TestCase->new($id);
}
$self->{'case_list'} = join(',', @{$caseids});
$self->{'test_cases'} = \@cases;
return $self->{'test_cases'};
}
=head2 test_case_count
Returns a count of the test cases linked to this plan
=cut
sub test_case_count {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
return $self->{'test_case_count'} if exists $self->{'test_case_count'};
$self->{'test_case_count'} = $dbh->selectrow_array(
"SELECT COUNT(case_id) FROM test_case_plans
WHERE plan_id = ?",
undef, $self->{'plan_id'}) || 0;
return $self->{'test_case_count'};
}
=head2 test_runs
Returns a reference to a list of test runs in this plan
=cut
sub test_runs {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
return $self->{'test_runs'} if exists $self->{'test_runs'};
my $runids = $dbh->selectcol_arrayref("SELECT run_id FROM test_runs
WHERE plan_id = ?",
undef, $self->{'plan_id'});
my @runs;
foreach my $id (@{$runids}){
push @runs, Testopia::TestRun->new($id);
}
$self->{'test_runs'} = \@runs;
return $self->{'test_runs'};
}
=head2 test_run_count
Returns a count of the test cases linked to this plan
=cut
sub test_run_count {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
return $self->{'test_run_count'} if exists $self->{'test_run_count'};
$self->{'test_run_count'} = $dbh->selectrow_array(
"SELECT COUNT(run_id) FROM test_runs
WHERE plan_id = ?",
undef, $self->{'plan_id'}) || 0;
return $self->{'test_run_count'};
}
sub test_case_run_count {
my $self = shift;
my ($status_id) = @_;
my $dbh = Bugzilla->dbh;
my $query =
"SELECT count(case_run_id) FROM test_case_runs
INNER JOIN test_runs ON test_case_runs.run_id = test_runs.run_id
INNER JOIN test_plans ON test_runs.plan_id = test_plans.plan_id
WHERE test_case_runs.iscurrent = 1 AND test_plans.plan_id = ?";
$query .= " AND test_case_runs.case_run_status_id = ?" if $status_id;
my $count;
if ($status_id){
($count) = $dbh->selectrow_array($query,undef,($self->id,$status_id));
}
else {
($count) = $dbh->selectrow_array($query,undef,$self->id);
}
return $count;
}
sub builds_seen {
my $self = shift;
my ($status_id) = @_;
my $dbh = Bugzilla->dbh;
require Testopia::Build;
my $ref = $dbh->selectcol_arrayref(
"SELECT DISTINCT test_case_runs.build_id
FROM test_case_runs
INNER JOIN test_runs ON test_case_runs.run_id = test_runs.run_id
WHERE test_runs.plan_id = ? AND test_case_runs.iscurrent = 1",
undef,$self->id);
my @o;
foreach my $id (@$ref){
push @o, Testopia::Build->new($id);
}
return \@o;
}
sub environments_seen {
my $self = shift;
my ($status_id) = @_;
my $dbh = Bugzilla->dbh;
require Testopia::Environment;
my $ref = $dbh->selectcol_arrayref(
"SELECT DISTINCT test_case_runs.environment_id
FROM test_case_runs
INNER JOIN test_runs ON test_case_runs.run_id = test_runs.run_id
WHERE test_runs.plan_id = ? AND test_case_runs.iscurrent = 1",
undef,$self->id);
my @o;
foreach my $id (@$ref){
push @o, Testopia::Environment->new($id);
}
return \@o;
}
=head2 tags
Returns a reference to a list of Testopia::TestTag objects
associated with this plan
=cut
sub tags {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
return $self->{'tags'} if exists $self->{'tags'};
my $tagids = $dbh->selectcol_arrayref("SELECT test_plan_tags.tag_id
FROM test_plan_tags
INNER JOIN test_tags ON test_plan_tags.tag_id = test_tags.tag_id
WHERE plan_id = ?
ORDER BY test_tags.tag_name",
undef, $self->{'plan_id'});
my @plan_tags;
foreach my $t (@{$tagids}){
push @plan_tags, Testopia::TestTag->new($t);
}
$self->{'tags'} = \@plan_tags;
return $self->{'tags'};
}
=head2 text
Returns the text of the plan document from the latest version
in the test_plan_texts table
=cut
sub text {
my $self = shift;
my $dbh = Bugzilla->dbh;
my ($version) = @_;
trick_taint($version) if $version;
return $self->{'text'} if exists $self->{'text'} && !$version;
$version = $version || $self->version;
my $text = $dbh->selectrow_hashref(
"SELECT plan_text, profiles.realname AS author, plan_text_version AS version
FROM test_plan_texts AS tpt
INNER JOIN profiles ON tpt.who = profiles.userid
WHERE plan_id = ? AND plan_text_version = ?",
undef, ($self->{'plan_id'}, $version));
return $text if scalar @_;
$self->{'text'} = $text;
return $self->{'text'};
}
=head2 version
Returns the plan text version. This number is incremented any time
changes are made to the plan document.
=cut
sub version {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
return $self->{'version'} if exists $self->{'version'};
my ($ver) = $dbh->selectrow_array("SELECT MAX(plan_text_version)
FROM test_plan_texts
WHERE plan_id = ?",
undef, $self->{'plan_id'});
$self->{'version'} = $ver;
return $self->{'version'};
}
=head2 type
Returns the type of this plan
=cut
sub plan_type {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
return $self->{'plan_type'} if exists $self->{'plan_type'};
my ($type) = $dbh->selectrow_array("SELECT name
FROM test_plan_types
WHERE type_id = ?",
undef, $self->{'type_id'});
$self->{'plan_type'} = $type;
return $self->{'plan_type'};
}
=head1 TODO
Use Bugzilla::Product and Version in 2.22
=head1 SEE ALSO
Testopia::(TestRun, TestCase, Category, Build, Util)
=head1 AUTHOR
Greg Hendricks <ghendricks@novell.com>
=cut
1;
__END__
=head1 NAME
Testopia::TestPlan - Testopia Test Plan object
=head1 DESCRIPTION
This module represents a test plan in Testopia. The test plan
is the glue of testopia. Virtually all other objects associate
to a plan.
=head1 SYNOPSIS
use Testopia::TestPlan;
$plan = Testopia::TestPlan->new($plan_id);
$plan = Testopia::TestPlan->new({});
=cut
=head2 new
Instantiate a new test plan. This takes a single argument
either a test plan ID or a reference to a hash containing keys
identical to a test plan's fields and desired values.
=cut