diff options
Diffstat (limited to 'extensions/TrackingFlags/lib')
-rw-r--r-- | extensions/TrackingFlags/lib/Admin.pm | 442 | ||||
-rw-r--r-- | extensions/TrackingFlags/lib/Constants.pm | 47 | ||||
-rw-r--r-- | extensions/TrackingFlags/lib/Flag.pm | 467 | ||||
-rw-r--r-- | extensions/TrackingFlags/lib/Flag/Bug.pm | 187 | ||||
-rw-r--r-- | extensions/TrackingFlags/lib/Flag/Value.pm | 130 | ||||
-rw-r--r-- | extensions/TrackingFlags/lib/Flag/Visibility.pm | 172 |
6 files changed, 1445 insertions, 0 deletions
diff --git a/extensions/TrackingFlags/lib/Admin.pm b/extensions/TrackingFlags/lib/Admin.pm new file mode 100644 index 000000000..b72416819 --- /dev/null +++ b/extensions/TrackingFlags/lib/Admin.pm @@ -0,0 +1,442 @@ +# 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. + +package Bugzilla::Extension::TrackingFlags::Admin; + +use strict; +use warnings; + +use Bugzilla; +use Bugzilla::Component; +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::Product; +use Bugzilla::Util qw(trim detaint_natural); + +use Bugzilla::Extension::TrackingFlags::Constants; +use Bugzilla::Extension::TrackingFlags::Flag; +use Bugzilla::Extension::TrackingFlags::Flag::Bug; +use Bugzilla::Extension::TrackingFlags::Flag::Value; +use Bugzilla::Extension::TrackingFlags::Flag::Visibility; + +use JSON; +use Scalar::Util qw(blessed); + +use base qw(Exporter); +our @EXPORT = qw( + admin_list + admin_edit +); + +# +# page loading +# + +sub admin_list { + my ($vars) = @_; + $vars->{show_bug_counts} = Bugzilla->input_params->{show_bug_counts}; + $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; +} + +sub admin_edit { + my ($vars, $page) = @_; + my $input = Bugzilla->input_params; + + $vars->{groups} = _groups_to_json(); + $vars->{mode} = $input->{mode} || 'new'; + $vars->{flag_id} = $input->{flag_id} || 0; + $vars->{tracking_flag_types} = FLAG_TYPES; + + if ($input->{delete}) { + my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); + $flag->remove_from_db(); + + $vars->{message} = 'tracking_flag_deleted'; + $vars->{flag} = $flag; + $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; + + print Bugzilla->cgi->header; + my $template = Bugzilla->template; + $template->process('pages/tracking_flags_admin_list.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + + } elsif ($input->{save}) { + # save + + my ($flag, $values, $visibilities) = _load_from_input($input, $vars); + _validate($flag, $values, $visibilities); + my $flag_obj = _update_db($flag, $values, $visibilities); + + $vars->{flag} = $flag_obj; + $vars->{values} = _flag_values_to_json($values); + $vars->{visibility} = _flag_visibility_to_json($visibilities); + + if ($vars->{mode} eq 'new') { + $vars->{message} = 'tracking_flag_created'; + } else { + $vars->{message} = 'tracking_flag_updated'; + } + $vars->{mode} = 'edit'; + + } else { + # initial load + + if ($vars->{mode} eq 'edit') { + # edit - straight load + my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); + $vars->{flag} = $flag; + $vars->{values} = _flag_values_to_json($flag->values); + $vars->{visibility} = _flag_visibility_to_json($flag->visibility); + $vars->{can_delete} = !$flag->bug_count; + + } elsif ($vars->{mode} eq 'copy') { + # copy - load the source flag + $vars->{mode} = 'new'; + my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($input->{copy_from}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{copy_from} }); + + # increment the number at the end of the name and description + if ($flag->name =~ /^(\D+)(\d+)$/) { + $flag->set_name("$1" . ($2 + 1)); + } + if ($flag->description =~ /^(\D+)(\d+)$/) { + $flag->set_description("$1" . ($2 + 1)); + } + $flag->set_sortkey(_next_unique_sortkey($flag->sortkey)); + $flag->set_type($flag->flag_type); + $flag->set_enter_bug($flag->enter_bug); + # always default new flags as active, even when copying an inactive one + $flag->set_is_active(1); + + $vars->{flag} = $flag; + $vars->{values} = _flag_values_to_json($flag->values, 1); + $vars->{visibility} = _flag_visibility_to_json($flag->visibility, 1); + $vars->{can_delete} = 0; + + } else { + $vars->{mode} = 'new'; + $vars->{flag} = { + sortkey => 0, + enter_bug => 1, + is_active => 1, + }; + $vars->{values} = _flag_values_to_json([ + { + id => 0, + value => '---', + setter_group_id => '', + is_active => 1, + }, + ]); + $vars->{visibility} = ''; + $vars->{can_delete} = 0; + } + } +} + +sub _load_from_input { + my ($input, $vars) = @_; + + # flag + + my $flag = { + id => ($input->{mode} eq 'edit' ? $input->{flag_id} : 0), + name => trim($input->{flag_name} || ''), + description => trim($input->{flag_desc} || ''), + sortkey => $input->{flag_sort} || 0, + type => trim($input->{flag_type} || ''), + enter_bug => $input->{flag_enter_bug} ? 1 : 0, + is_active => $input->{flag_active} ? 1 : 0, + }; + detaint_natural($flag->{id}); + detaint_natural($flag->{sortkey}); + detaint_natural($flag->{enter_bug}); + detaint_natural($flag->{is_active}); + + # values + + my $values = decode_json($input->{values} || '[]'); + foreach my $value (@$values) { + $value->{value} = '' unless exists $value->{value} && defined $value->{value}; + $value->{setter_group_id} = '' unless $value->{setter_group_id}; + $value->{is_active} = $value->{is_active} ? 1 : 0; + } + + # vibility + + my $visibilities = decode_json($input->{visibility} || '[]'); + foreach my $visibility (@$visibilities) { + $visibility->{product} = '' unless exists $visibility->{product} && defined $visibility->{product}; + $visibility->{component} = '' unless exists $visibility->{component} && defined $visibility->{component}; + } + + return ($flag, $values, $visibilities); +} + +sub _next_unique_sortkey { + my ($sortkey) = @_; + + my %current; + foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all()) { + $current{$flag->sortkey} = 1; + } + + $sortkey += 5; + $sortkey += 5 while exists $current{$sortkey}; + return $sortkey; +} + +# +# validation +# + +sub _validate { + my ($flag, $values, $visibilities) = @_; + + # flag + + my @missing; + push @missing, 'Field Name' if $flag->{name} eq ''; + push @missing, 'Field Description' if $flag->{description} eq ''; + push @missing, 'Field Sort Key' if $flag->{sortkey} eq ''; + scalar(@missing) + && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); + + $flag->{name} =~ /^cf_/ + || ThrowUserError('tracking_flags_cf_prefix'); + + if ($flag->{id}) { + my $old_flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); + if ($flag->{name} ne $old_flag->name) { + Bugzilla::Field->new({ name => $flag->{name} }) + && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); + } + } else { + Bugzilla::Field->new({ name => $flag->{name} }) + && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); + } + + # values + + scalar(@$values) + || ThrowUserError('tracking_flags_missing_values'); + + my %seen; + foreach my $value (@$values) { + my $v = $value->{value}; + + $v eq '' + && ThrowUserError('tracking_flags_missing_value'); + + exists $seen{$v} + && ThrowUserError('tracking_flags_duplicate_value', { value => $v }); + $seen{$v} = 1; + + push @missing, "Setter for $v" if !$value->{setter_group_id}; + } + scalar(@missing) + && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); + + # visibility + + scalar(@$visibilities) + || ThrowUserError('tracking_flags_missing_visibility'); + + %seen = (); + foreach my $visibility (@$visibilities) { + my $name = $visibility->{product} . ':' . $visibility->{component}; + + exists $seen{$name} + && ThrowUserError('tracking_flags_duplicate_visibility', { name => $name }); + + $visibility->{product_obj} = Bugzilla::Product->new({ name => $visibility->{product} }) + || ThrowCodeError('tracking_flags_invalid_product', { product => $visibility->{product} }); + + if ($visibility->{component} ne '') { + $visibility->{component_obj} = Bugzilla::Component->new({ product => $visibility->{product_obj}, + name => $visibility->{component} }) + || ThrowCodeError('tracking_flags_invalid_component', { component => $visibility->{component} }); + } + } + +} + +# +# database updating +# + +sub _update_db { + my ($flag, $values, $visibilities) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $flag_obj = _update_db_flag($flag); + _update_db_values($flag_obj, $flag, $values); + _update_db_visibility($flag_obj, $flag, $visibilities); + $dbh->bz_commit_transaction(); + + return $flag_obj; +} + +sub _update_db_flag { + my ($flag) = @_; + + my $object_set = { + name => $flag->{name}, + description => $flag->{description}, + sortkey => $flag->{sortkey}, + type => $flag->{type}, + enter_bug => $flag->{enter_bug}, + is_active => $flag->{is_active}, + }; + + my $flag_obj; + if ($flag->{id}) { + # update existing flag + $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); + $flag_obj->set_all($object_set); + $flag_obj->update(); + + } else { + # create new flag + $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->create($object_set); + } + + return $flag_obj; +} + +sub _update_db_values { + my ($flag_obj, $flag, $values) = @_; + + # delete + foreach my $current_value (@{ $flag_obj->values }) { + if (!grep { $_->{id} == $current_value->id } @$values) { + $current_value->remove_from_db(); + } + } + + # add/update + my $sortkey = 0; + foreach my $value (@{ $values }) { + $sortkey += 10; + + my $object_set = { + value => $value->{value}, + setter_group_id => $value->{setter_group_id}, + is_active => $value->{is_active}, + sortkey => $sortkey, + }; + + if ($value->{id}) { + my $value_obj = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag value', id => $flag->{id} }); + my $old_value = $value_obj->value; + if ($object_set->{value} ne $old_value) { + $value_obj->set_all($object_set); + $value_obj->update(); + Bugzilla::Extension::TrackingFlags::Flag::Bug->update_all_values({ + value_obj => $value_obj, + old_value => $old_value, + new_value => $value_obj->value, + }); + } + } else { + $object_set->{tracking_flag_id} = $flag_obj->flag_id; + Bugzilla::Extension::TrackingFlags::Flag::Value->create($object_set); + } + } +} + +sub _update_db_visibility { + my ($flag_obj, $flag, $visibilities) = @_; + + # delete + foreach my $current_visibility (@{ $flag_obj->visibility }) { + if (!grep { $_->{id} == $current_visibility->id } @$visibilities) { + $current_visibility->remove_from_db(); + } + } + + # add + foreach my $visibility (@{ $visibilities }) { + next if $visibility->{id}; + Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ + tracking_flag_id => $flag_obj->flag_id, + product_id => $visibility->{product_obj}->id, + component_id => $visibility->{component} ? $visibility->{component_obj}->id : undef, + }); + } +} + +# +# serialisation +# + +sub _groups_to_json { + my @data; + foreach my $group (sort { $a->name cmp $b->name } Bugzilla::Group->get_all()) { + push @data, { + id => $group->id, + name => $group->name, + }; + } + return encode_json(\@data); +} + +sub _flag_values_to_json { + my ($values, $is_copy) = @_; + # setting is_copy will set the id's to zero, to force new values rather + # than editing existing ones + my @data; + foreach my $value (@$values) { + push @data, { + id => $is_copy ? 0 : $value->{id}, + value => $value->{value}, + setter_group_id => $value->{setter_group_id}, + is_active => $value->{is_active} ? JSON::true : JSON::false, + }; + } + return encode_json(\@data); +} + +sub _flag_visibility_to_json { + my ($visibilities, $is_copy) = @_; + # setting is_copy will set the id's to zero, to force new visibilites + # rather than editing existing ones + my @data; + + foreach my $visibility (@$visibilities) { + my $product = exists $visibility->{product_id} + ? $visibility->product->name + : $visibility->{product}; + my $component; + if (exists $visibility->{component_id} && $visibility->{component_id}) { + $component = $visibility->component->name; + } elsif (exists $visibility->{component}) { + $component = $visibility->{component}; + } else { + $component = undef; + } + push @data, { + id => $is_copy ? 0 : $visibility->{id}, + product => $product, + component => $component, + }; + } + @data = sort { + lc($a->{product}) cmp lc($b->{product}) + || lc($a->{component}) cmp lc($b->{component}) + } @data; + return encode_json(\@data); +} + +1; diff --git a/extensions/TrackingFlags/lib/Constants.pm b/extensions/TrackingFlags/lib/Constants.pm new file mode 100644 index 000000000..b6813c3c2 --- /dev/null +++ b/extensions/TrackingFlags/lib/Constants.pm @@ -0,0 +1,47 @@ +# 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. + +package Bugzilla::Extension::TrackingFlags::Constants; + +use strict; +use base qw(Exporter); + +our @EXPORT = qw( + FLAG_TYPES +); + +sub FLAG_TYPES { + my @flag_types = ( + { + name => 'project', + description => 'Project Flags', + collapsed => 0, + sortkey => 0 + }, + { + name => 'tracking', + description => 'Tracking Flags', + collapsed => 1, + sortkey => 1 + }, + { + name => 'blocking', + description => 'Blocking Flags', + collapsed => 1, + sortkey => 2 + }, + { + name => 'b2g', + description => 'B2G Flags', + collapsed => 1, + sortkey => 3 + }, + ); + return [ sort { $a->{'sortkey'} <=> $b->{'sortkey'} } @flag_types ]; +} + +1; diff --git a/extensions/TrackingFlags/lib/Flag.pm b/extensions/TrackingFlags/lib/Flag.pm new file mode 100644 index 000000000..3ae7a937e --- /dev/null +++ b/extensions/TrackingFlags/lib/Flag.pm @@ -0,0 +1,467 @@ +# 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. + +package Bugzilla::Extension::TrackingFlags::Flag; + +use base qw(Bugzilla::Object); + +use strict; +use warnings; + +use Bugzilla::Error; +use Bugzilla::Constants; +use Bugzilla::Util qw(detaint_natural trim); +use Bugzilla::Config qw(SetParam write_params); + +use Bugzilla::Extension::TrackingFlags::Constants; +use Bugzilla::Extension::TrackingFlags::Flag::Bug; +use Bugzilla::Extension::TrackingFlags::Flag::Value; +use Bugzilla::Extension::TrackingFlags::Flag::Visibility; + +############################### +#### Initialization #### +############################### + +use constant DB_TABLE => 'tracking_flags'; + +use constant DB_COLUMNS => qw( + id + field_id + name + description + type + sortkey + enter_bug + is_active +); + +use constant LIST_ORDER => 'sortkey'; + +use constant UPDATE_COLUMNS => qw( + name + description + type + sortkey + enter_bug + is_active +); + +use constant VALIDATORS => { + name => \&_check_name, + description => \&_check_description, + type => \&_check_type, + sortkey => \&_check_sortkey, + enter_bug => \&Bugzilla::Object::check_boolean, + is_active => \&Bugzilla::Object::check_boolean, +}; + +use constant UPDATE_VALIDATORS => { + name => \&_check_name, + description => \&_check_description, + type => \&_check_type, + sortkey => \&_check_sortkey, + enter_bug => \&Bugzilla::Object::check_boolean, + is_active => \&Bugzilla::Object::check_boolean, +}; + +############################### +#### Methods #### +############################### + +sub new { + my $class = shift; + my $param = shift; + my $cache = Bugzilla->request_cache; + + if (!ref $param + && exists $cache->{'tracking_flags'} + && exists $cache->{'tracking_flags'}->{$param}) + { + return $cache->{'tracking_flags'}->{$param}; + } + + return $class->SUPER::new($param); +} + +sub create { + my $class = shift; + my $params = shift; + my $dbh = Bugzilla->dbh; + my $flag; + + # Disable bug updates temporarily to avoid conflicts. + SetParam('disable_bug_updates', 1); + write_params(); + + eval { + $dbh->bz_start_transaction(); + + $params = $class->run_create_validators($params); + + # We have to create an entry for this new flag + # in the fielddefs table for use elsewhere. We cannot + # use Bugzilla::Field->create as it will create the + # additional tables needed by custom fields which we + # do not need. Also we do this so as not to add a + # another column to the bugs table. + # We will create the entry as a custom field with a + # type of FIELD_TYPE_EXTENSION so Bugzilla will skip + # these field types in certain parts of the core code. + $dbh->do("INSERT INTO fielddefs + (name, description, sortkey, type, custom, obsolete, buglist) + VALUES + (?, ?, ?, ?, ?, ?, ?)", + undef, + $params->{'name'}, + $params->{'description'}, + $params->{'sortkey'}, + FIELD_TYPE_EXTENSION, + 1, 0, 1); + $params->{'field_id'} = $dbh->bz_last_key; + + $flag = $class->SUPER::create($params); + + $dbh->bz_commit_transaction(); + }; + my $error = "$@"; + SetParam('disable_bug_updates', 0); + write_params(); + die $error if $error; + + return $flag; +} + +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + + my $old_self = $self->new($self->flag_id); + + # HACK! Bugzilla::Object::update uses hardcoded $self->id + # instead of $self->{ID_FIELD} so we need to reverse field_id + # and the real id temporarily + my $field_id = $self->id; + $self->{'field_id'} = $self->{'id'}; + + my $changes = $self->SUPER::update(@_); + + $self->{'field_id'} = $field_id; + + # Update the fielddefs entry + $dbh->do("UPDATE fielddefs SET name = ?, description = ? WHERE name = ?", + undef, + $self->name, $self->description, $old_self->name); + + # Update request_cache + my $cache = Bugzilla->request_cache; + if (exists $cache->{'tracking_flags'}) { + $cache->{'tracking_flags'}->{$self->flag_id} = $self; + } + + return $changes; +} + +sub match { + my $class = shift; + my ($params) = @_; + + # Use later for preload + my $bug_id = delete $params->{'bug_id'}; + + # Retrieve all flags relevant for the given product and component + if (!exists $params->{'id'} + && ($params->{'component'} || $params->{'component_id'} + || $params->{'product'} || $params->{'product_id'})) + { + my $visible_flags + = Bugzilla::Extension::TrackingFlags::Flag::Visibility->match(@_); + my @flag_ids = map { $_->tracking_flag_id } @$visible_flags; + + delete $params->{'component'} if exists $params->{'component'}; + delete $params->{'component_id'} if exists $params->{'component_id'}; + delete $params->{'product'} if exists $params->{'product'}; + delete $params->{'product_id'} if exists $params->{'product_id'}; + + $params->{'id'} = \@flag_ids; + } + + # We need to return inactive flags if a value has been set + my $is_active_filter = delete $params->{is_active}; + + my $flags = $class->SUPER::match($params); + preload_all_the_things($flags, { bug_id => $bug_id }); + + if ($is_active_filter) { + $flags = [ grep { $_->is_active || exists $_->{bug_flag} } @$flags ]; + } + return [ sort { $a->sortkey <=> $b->sortkey } @$flags ]; +} + +sub get_all { + my $self = shift; + my $cache = Bugzilla->request_cache; + if (!exists $cache->{'tracking_flags'}) { + my @tracking_flags = $self->SUPER::get_all(@_); + preload_all_the_things(\@tracking_flags); + my %tracking_flags_hash = map { $_->flag_id => $_ } @tracking_flags; + $cache->{'tracking_flags'} = \%tracking_flags_hash; + } + return sort { $a->flag_type cmp $b->flag_type || $a->sortkey <=> $b->sortkey } + values %{ $cache->{'tracking_flags'} }; +} + +# avoids the overhead of pre-loading if just the field names are required +sub get_all_names { + my $self = shift; + my $cache = Bugzilla->request_cache; + if (!exists $cache->{'tracking_flags_names'}) { + $cache->{'tracking_flags_names'} = + Bugzilla->dbh->selectcol_arrayref("SELECT name FROM tracking_flags ORDER BY name"); + } + return @{ $cache->{'tracking_flags_names'} }; +} + +sub remove_from_db { + my $self = shift; + my $dbh = Bugzilla->dbh; + + # Check to see if tracking_flags_bugs table has records + if ($self->bug_count) { + ThrowUserError('tracking_flag_has_contents', { flag => $self }); + } + + # Disable bug updates temporarily to avoid conflicts. + SetParam('disable_bug_updates', 1); + write_params(); + + eval { + $dbh->bz_start_transaction(); + + $dbh->do('DELETE FROM bugs_activity WHERE fieldid = ?', undef, $self->id); + $dbh->do('DELETE FROM fielddefs WHERE name = ?', undef, $self->name); + + $dbh->bz_commit_transaction(); + + # Remove from request cache + my $cache = Bugzilla->request_cache; + if (exists $cache->{'tracking_flags'}) { + delete $cache->{'tracking_flags'}->{$self->flag_id}; + } + }; + my $error = "$@"; + SetParam('disable_bug_updates', 0); + write_params(); + die $error if $error; +} + +sub preload_all_the_things { + my ($flags, $params) = @_; + + my %flag_hash = map { $_->flag_id => $_ } @$flags; + my @flag_ids = keys %flag_hash; + return unless @flag_ids; + + # Preload values + my $value_objects + = Bugzilla::Extension::TrackingFlags::Flag::Value->match({ tracking_flag_id => \@flag_ids }); + + # Now populate the tracking flags with this set of value objects. + foreach my $obj (@$value_objects) { + my $flag_id = $obj->tracking_flag_id; + + # Prepopulate the tracking flag object in the value object + $obj->{'tracking_flag'} = $flag_hash{$flag_id}; + + # Prepopulate the current value objects for this tracking flag + $flag_hash{$flag_id}->{'values'} ||= []; + push(@{$flag_hash{$flag_id}->{'values'}}, $obj); + } + + # Preload bug values if a bug_id is passed + if ($params && exists $params->{'bug_id'} && $params->{'bug_id'}) { + # We don't want to use @flag_ids here as we want all flags attached to this bug + # even if they are inactive. + my $bug_objects + = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $params->{'bug_id'} }); + # Now populate the tracking flags with this set of objects. + # Also we add them to the flag hash since we want them to be visible even if + # they are not longer applicable to this product/component. + foreach my $obj (@$bug_objects) { + my $flag_id = $obj->tracking_flag_id; + + # Load the flag object if it does not yet exist. + # This can happen if the bug value tracking flag + # is no longer visible for the product/component + $flag_hash{$flag_id} + ||= Bugzilla::Extension::TrackingFlags::Flag->new($flag_id); + + # Prepopulate the tracking flag object in the bug flag object + $obj->{'tracking_flag'} = $flag_hash{$flag_id}; + + # Prepopulate the the current bug flag object for the tracking flag + $flag_hash{$flag_id}->{'bug_flag'} = $obj; + } + } + + @$flags = values %flag_hash; +} + +############################### +#### Validators #### +############################### + +sub _check_name { + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowCodeError('param_required', { param => 'name' }); + return $name; +} + +sub _check_description { + my ($invocant, $description) = @_; + $description = trim($description); + $description || ThrowCodeError( 'param_required', { param => 'description' } ); + return $description; +} + +sub _check_type { + my ($invocant, $type) = @_; + $type = trim($type); + $type || ThrowCodeError( 'param_required', { param => 'type' } ); + grep($_->{name} eq $type, @{FLAG_TYPES()}) + || ThrowUserError('tracking_flags_invalid_flag_type', { type => $type }); + return $type; +} + +sub _check_sortkey { + my ($invocant, $sortkey) = @_; + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey }); + return $sortkey; +} + +############################### +#### Setters #### +############################### + +sub set_name { $_[0]->set('name', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_type { $_[0]->set('type', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } + +############################### +#### Accessors #### +############################### + +sub flag_id { return $_[0]->{'id'}; } +sub name { return $_[0]->{'name'}; } +sub description { return $_[0]->{'description'}; } +sub flag_type { return $_[0]->{'type'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub enter_bug { return $_[0]->{'enter_bug'}; } +sub is_active { return $_[0]->{'is_active'}; } + +sub values { + return $_[0]->{'values'} ||= Bugzilla::Extension::TrackingFlags::Flag::Value->match({ + tracking_flag_id => $_[0]->flag_id + }); +} + +sub visibility { + return $_[0]->{'visibility'} ||= Bugzilla::Extension::TrackingFlags::Flag::Visibility->match({ + tracking_flag_id => $_[0]->flag_id + }); +} + +sub can_set_value { + my ($self, $new_value, $user) = @_; + $user ||= Bugzilla->user; + my $new_value_obj; + foreach my $value (@{$self->values}) { + if ($value->value eq $new_value) { + $new_value_obj = $value; + last; + } + } + return $new_value_obj + && $new_value_obj->setter_group + && $user->in_group($new_value_obj->setter_group->name) + ? 1 + : 0; +} + +sub bug_flag { + my ($self, $bug_id) = @_; + # Return the current bug value object if defined unless the passed bug_id does + # not equal the current bug value objects id. + if (defined $self->{'bug_flag'} + && (!$bug_id || $self->{'bug_flag'}->bug->id == $bug_id)) + { + return $self->{'bug_flag'}; + } + + # Flag::Bug->new will return a default bug value object if $params undefined + my $params = !$bug_id + ? undef + : { condition => "tracking_flag_id = ? AND bug_id = ?", + values => [ $self->flag_id, $bug_id ] }; + return $self->{'bug_flag'} = Bugzilla::Extension::TrackingFlags::Flag::Bug->new($params); +} + +sub bug_count { + my ($self) = @_; + return $self->{'bug_count'} if defined $self->{'bug_count'}; + my $dbh = Bugzilla->dbh; + return $self->{'bug_count'} = scalar $dbh->selectrow_array(" + SELECT COUNT(bug_id) + FROM tracking_flags_bugs + WHERE tracking_flag_id = ?", + undef, $self->flag_id); +} + +sub activity_count { + my ($self) = @_; + return $self->{'activity_count'} if defined $self->{'activity_count'}; + my $dbh = Bugzilla->dbh; + return $self->{'activity_count'} = scalar $dbh->selectrow_array(" + SELECT COUNT(bug_id) + FROM bugs_activity + WHERE fieldid = ?", + undef, $self->id); +} + +###################################### +# Compatibility with Bugzilla::Field # +###################################### + +# Here we return 'field_id' instead of the real +# id as we want other Bugzilla code to treat this +# as a Bugzilla::Field object in certain places. +sub id { return $_[0]->{'field_id'}; } +sub type { return FIELD_TYPE_EXTENSION; } +sub legal_values { return $_[0]->values; } +sub custom { return 1; } +sub in_new_bugmail { return 1; } +sub obsolete { return $_[0]->is_active ? 0 : 1; } +sub buglist { return 1; } +sub is_select { return 1; } +sub is_abnormal { return 1; } +sub is_timetracking { return 0; } +sub visibility_field { return undef; } +sub visibility_values { return undef; } +sub controls_visibility_of { return undef; } +sub value_field { return undef; } +sub controls_values_of { return undef; } +sub is_visible_on_bug { return 1; } +sub is_relationship { return 0; } +sub reverse_desc { return ''; } +sub is_mandatory { return 0; } +sub is_numeric { return 0; } + +1; diff --git a/extensions/TrackingFlags/lib/Flag/Bug.pm b/extensions/TrackingFlags/lib/Flag/Bug.pm new file mode 100644 index 000000000..ea382a29d --- /dev/null +++ b/extensions/TrackingFlags/lib/Flag/Bug.pm @@ -0,0 +1,187 @@ +# 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. + +package Bugzilla::Extension::TrackingFlags::Flag::Bug; + +use base qw(Bugzilla::Object); + +use strict; +use warnings; + +use Bugzilla::Extension::TrackingFlags::Flag; + +use Bugzilla::Bug; +use Bugzilla::Error; + +use Scalar::Util qw(blessed); + +############################### +#### Initialization #### +############################### + +use constant DEFAULT_FLAG_BUG => { + 'id' => 0, + 'tracking_flag_id' => 0, + 'bug_id' => '', + 'value' => '---', +}; + +use constant DB_TABLE => 'tracking_flags_bugs'; + +use constant DB_COLUMNS => qw( + id + tracking_flag_id + bug_id + value +); + +use constant LIST_ORDER => 'id'; + +use constant UPDATE_COLUMNS => qw( + value +); + +use constant VALIDATORS => { + tracking_flag_id => \&_check_tracking_flag, + value => \&_check_value, +}; + +use constant AUDIT_CREATES => 0; +use constant AUDIT_UPDATES => 0; +use constant AUDIT_REMOVES => 0; + +############################### +#### Object Methods #### +############################### + +sub new { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($param) = @_; + + my $self; + if ($param) { + $self = $class->SUPER::new(@_); + if (!$self) { + $self = DEFAULT_FLAG_BUG; + bless($self, $class); + } + } + else { + $self = DEFAULT_FLAG_BUG; + bless($self, $class); + } + + return $self +} + +sub match { + my $class = shift; + my $bug_flags = $class->SUPER::match(@_); + preload_all_the_things($bug_flags); + return $bug_flags; +} + +sub remove_from_db { + my ($self) = @_; + $self->SUPER::remove_from_db(); + $self->{'id'} = $self->{'tracking_flag_id'} = $self->{'bug_id'} = 0; + $self->{'value'} = '---'; +} + +sub preload_all_the_things { + my ($bug_flags) = @_; + my $cache = Bugzilla->request_cache; + + # Preload tracking flag objects + my @tracking_flag_ids; + foreach my $bug_flag (@$bug_flags) { + if (exists $cache->{'tracking_flags'} + && $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}) + { + $bug_flag->{'tracking_flag'} + = $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}; + next; + } + push(@tracking_flag_ids, $bug_flag->tracking_flag_id); + } + + return unless @tracking_flag_ids; + + my $tracking_flags + = Bugzilla::Extension::TrackingFlags::Flag->match({ id => \@tracking_flag_ids }); + my %tracking_flag_hash = map { $_->flag_id => $_ } @$tracking_flags; + + foreach my $bug_flag (@$bug_flags) { + next if exists $bug_flag->{'tracking_flag'}; + $bug_flag->{'tracking_flag'} = $tracking_flag_hash{$bug_flag->tracking_flag_id}; + } +} + +############################## +#### Class Methods #### +############################## + +sub update_all_values { + my ($invocant, $params) = @_; + my $dbh = Bugzilla->dbh; + $dbh->do( + "UPDATE tracking_flags_bugs SET value=? WHERE tracking_flag_id=? AND value=?", + undef, + $params->{new_value}, + $params->{value_obj}->tracking_flag_id, + $params->{old_value}, + ); +} + +############################### +#### Validators #### +############################### + +sub _check_value { + my ($invocant, $value) = @_; + $value || ThrowCodeError('param_required', { param => 'value' }); + return $value; +} + +sub _check_tracking_flag { + my ($invocant, $flag) = @_; + if (blessed $flag) { + return $flag->flag_id; + } + $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 }) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + return $flag->flag_id; +} + +############################### +#### Setters #### +############################### + +sub set_value { $_[0]->set('value', $_[1]); } + +############################### +#### Accessors #### +############################### + +sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } +sub bug_id { return $_[0]->{'bug_id'}; } +sub value { return $_[0]->{'value'}; } + +sub bug { + return $_[0]->{'bug'} ||= Bugzilla::Bug->new({ + id => $_[0]->bug_id, cache => 1 + }); +} + +sub tracking_flag { + return $_[0]->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new({ + id => $_[0]->tracking_flag_id, cache => 1 + }); +} + +1; diff --git a/extensions/TrackingFlags/lib/Flag/Value.pm b/extensions/TrackingFlags/lib/Flag/Value.pm new file mode 100644 index 000000000..4023e191d --- /dev/null +++ b/extensions/TrackingFlags/lib/Flag/Value.pm @@ -0,0 +1,130 @@ +# 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. + +package Bugzilla::Extension::TrackingFlags::Flag::Value; + +use base qw(Bugzilla::Object); + +use strict; +use warnings; + +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::Util qw(detaint_natural); +use Scalar::Util qw(blessed); + +############################### +#### Initialization #### +############################### + +use constant DB_TABLE => 'tracking_flags_values'; + +use constant DB_COLUMNS => qw( + id + tracking_flag_id + setter_group_id + value + sortkey + is_active +); + +use constant LIST_ORDER => 'sortkey'; + +use constant UPDATE_COLUMNS => qw( + setter_group_id + value + sortkey + is_active +); + +use constant VALIDATORS => { + tracking_flag_id => \&_check_tracking_flag, + setter_group_id => \&_check_setter_group, + value => \&_check_value, + sortkey => \&_check_sortkey, + is_active => \&Bugzilla::Object::check_boolean, +}; + +############################### +#### Validators #### +############################### + +sub _check_value { + my ($invocant, $value) = @_; + defined $value || ThrowCodeError('param_required', { param => 'value' }); + return $value; +} + +sub _check_tracking_flag { + my ($invocant, $flag) = @_; + if (blessed $flag) { + return $flag->flag_id; + } + $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 }) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + return $flag->flag_id; +} + +sub _check_setter_group { + my ($invocant, $group) = @_; + if (blessed $group) { + return $group->id; + } + $group = Bugzilla::Group->new({ id => $group, cache => 1 }) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'setter_group_id', value => $group }); + return $group->id; +} + +sub _check_sortkey { + my ($invocant, $sortkey) = @_; + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey }); + return $sortkey; +} + +############################### +#### Setters #### +############################### + +sub set_setter_group_id { $_[0]->set('setter_group_id', $_[1]); } +sub set_value { $_[0]->set('value', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } + +############################### +#### Accessors #### +############################### + +sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } +sub setter_group_id { return $_[0]->{'setter_group_id'}; } +sub value { return $_[0]->{'value'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub is_active { return $_[0]->{'is_active'}; } + +sub tracking_flag { + return $_[0]->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new({ + id => $_[0]->tracking_flag_id, cache => 1 + }); +} + +sub setter_group { + if ($_[0]->setter_group_id) { + $_[0]->{'setter_group'} ||= Bugzilla::Group->new({ + id => $_[0]->setter_group_id, cache => 1 + }); + } + return $_[0]->{'setter_group'}; +} + +######################################## +## Compatibility with Bugzilla::Field ## +######################################## + +sub name { return $_[0]->{'value'}; } +sub is_visible_on_bug { return 1; } + +1; diff --git a/extensions/TrackingFlags/lib/Flag/Visibility.pm b/extensions/TrackingFlags/lib/Flag/Visibility.pm new file mode 100644 index 000000000..7600d71bd --- /dev/null +++ b/extensions/TrackingFlags/lib/Flag/Visibility.pm @@ -0,0 +1,172 @@ +# 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. + +package Bugzilla::Extension::TrackingFlags::Flag::Visibility; + +use base qw(Bugzilla::Object); + +use strict; +use warnings; + +use Bugzilla::Error; +use Bugzilla::Product; +use Bugzilla::Component; +use Scalar::Util qw(blessed); + +############################### +#### Initialization #### +############################### + +use constant DB_TABLE => 'tracking_flags_visibility'; + +use constant DB_COLUMNS => qw( + id + tracking_flag_id + product_id + component_id +); + +use constant LIST_ORDER => 'id'; + +use constant UPDATE_COLUMNS => (); # imutable + +use constant VALIDATORS => { + tracking_flag_id => \&_check_tracking_flag, + product_id => \&_check_product, + component_id => \&_check_component, +}; + +############################### +#### Methods #### +############################### + +sub match { + my $class= shift; + my ($params) = @_; + my $dbh = Bugzilla->dbh; + + # Allow matching component and product by name + # (in addition to matching by ID). + # Borrowed from Bugzilla::Bug::match + my %translate_fields = ( + product => 'Bugzilla::Product', + component => 'Bugzilla::Component', + ); + + foreach my $field (keys %translate_fields) { + my @ids; + # Convert names to ids. We use "exists" everywhere since people can + # legally specify "undef" to mean IS NULL + if (exists $params->{$field}) { + my $names = $params->{$field}; + my $type = $translate_fields{$field}; + my $objects = Bugzilla::Object::match($type, { name => $names }); + push(@ids, map { $_->id } @$objects); + } + # You can also specify ids directly as arguments to this function, + # so include them in the list if they have been specified. + if (exists $params->{"${field}_id"}) { + my $current_ids = $params->{"${field}_id"}; + my @id_array = ref $current_ids ? @$current_ids : ($current_ids); + push(@ids, @id_array); + } + # We do this "or" instead of a "scalar(@ids)" to handle the case + # when people passed only invalid object names. Otherwise we'd + # end up with a SUPER::match call with zero criteria (which dies). + if (exists $params->{$field} or exists $params->{"${field}_id"}) { + delete $params->{$field}; + $params->{"${field}_id"} = scalar(@ids) == 1 ? [ $ids[0] ] : \@ids; + } + } + + # If we aren't matching on the product, use the default matching code + if (!exists $params->{product_id}) { + return $class->SUPER::match(@_); + } + + my @criteria = ("1=1"); + + if ($params->{product_id}) { + push(@criteria, $dbh->sql_in('product_id', $params->{'product_id'})); + if ($params->{component_id}) { + my $component_id = $params->{component_id}; + push(@criteria, "(" . $dbh->sql_in('component_id', $params->{'component_id'}) . + " OR component_id IS NULL)"); + } + } + + my $where = join(' AND ', @criteria); + my $flag_ids = $dbh->selectcol_arrayref("SELECT id + FROM tracking_flags_visibility + WHERE $where"); + + return Bugzilla::Extension::TrackingFlags::Flag::Visibility->new_from_list($flag_ids); +} + +############################### +#### Validators #### +############################### + +sub _check_tracking_flag { + my ($invocant, $flag) = @_; + if (blessed $flag) { + return $flag->flag_id; + } + $flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + return $flag->flag_id; +} + +sub _check_product { + my ($invocant, $product) = @_; + if (blessed $product) { + return $product->id; + } + $product = Bugzilla::Product->new($product) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'product_id', value => $product }); + return $product->id; +} + +sub _check_component { + my ($invocant, $component) = @_; + return undef unless defined $component; + if (blessed $component) { + return $component->id; + } + $component = Bugzilla::Component->new($component) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'component_id', value => $component }); + return $component->id; +} + +############################### +#### Accessors #### +############################### + +sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } +sub product_id { return $_[0]->{'product_id'}; } +sub component_id { return $_[0]->{'component_id'}; } + +sub tracking_flag { + my ($self) = @_; + $self->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new($self->tracking_flag_id); + return $self->{'tracking_flag'}; +} + +sub product { + my ($self) = @_; + $self->{'product'} ||= Bugzilla::Product->new($self->product_id); + return $self->{'product'}; +} + +sub component { + my ($self) = @_; + return undef unless $self->component_id; + $self->{'component'} ||= Bugzilla::Component->new($self->component_id); + return $self->{'component'}; +} + +1; |