summaryrefslogtreecommitdiffstats
path: root/extensions/TrackingFlags/lib
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/TrackingFlags/lib')
-rw-r--r--extensions/TrackingFlags/lib/Admin.pm421
-rw-r--r--extensions/TrackingFlags/lib/Constants.pm45
-rw-r--r--extensions/TrackingFlags/lib/Flag.pm400
-rw-r--r--extensions/TrackingFlags/lib/Flag/Bug.pm171
-rw-r--r--extensions/TrackingFlags/lib/Flag/Value.pm130
-rw-r--r--extensions/TrackingFlags/lib/Flag/Visibility.pm172
6 files changed, 1339 insertions, 0 deletions
diff --git a/extensions/TrackingFlags/lib/Admin.pm b/extensions/TrackingFlags/lib/Admin.pm
new file mode 100644
index 000000000..9e8b33b43
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Admin.pm
@@ -0,0 +1,421 @@
+# 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::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->{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->{mode} = 'deleted';
+ $vars->{flag_name} = $flag->name;
+
+ } 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);
+ $vars->{can_delete} = !$flag_obj->has_values;
+
+ if ($vars->{mode} eq 'new') {
+ $vars->{message} = 'tracking_flags_created';
+ } else {
+ $vars->{message} = 'tracking_flags_updated';
+ }
+
+ } 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->has_values;
+
+ } 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);
+ # 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,
+ 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->{flag_id} || 0,
+ name => trim($input->{flag_name} || ''),
+ description => trim($input->{flag_desc} || ''),
+ sortkey => $input->{flag_sort} || 0,
+ type => trim($input->{flag_type} || ''),
+ is_active => $input->{flag_active} ? 1 : 0,
+ };
+ detaint_natural($flag->{id});
+ detaint_natural($flag->{sortkey});
+ 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},
+ 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} });
+ $value_obj->set_all($object_set);
+ $value_obj->update();
+ } 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..edeb1a62b
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Constants.pm
@@ -0,0 +1,45 @@
+# 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
+);
+
+use constant FLAG_TYPES => [
+ {
+ name => 'tracking',
+ description => 'Tracking Flags',
+ collapsed => 1,
+ },
+ {
+ name => 'project',
+ description => 'Project Flags',
+ collapsed => 0,
+ },
+ {
+ name => 'status',
+ description => 'Status Flags',
+ collapsed => 1,
+ },
+ {
+ name => 'blocking',
+ description => 'Blocking Flags',
+ collapsed => 1,
+ },
+ {
+ name => 'b2g',
+ description => 'B2G Flags',
+ collapsed => 1,
+ }
+];
+
+1;
diff --git a/extensions/TrackingFlags/lib/Flag.pm b/extensions/TrackingFlags/lib/Flag.pm
new file mode 100644
index 000000000..5daa47508
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Flag.pm
@@ -0,0 +1,400 @@
+# 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::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
+ is_active
+);
+
+use constant LIST_ORDER => 'sortkey';
+
+use constant UPDATE_COLUMNS => qw(
+ name
+ description
+ type
+ sortkey
+ is_active
+);
+
+use constant VALIDATORS => {
+ name => \&_check_name,
+ description => \&_check_description,
+ type => \&_check_type,
+ sortkey => \&_check_sortkey,
+ is_active => \&Bugzilla::Object::check_boolean,
+
+};
+
+use constant UPDATE_VALIDATORS => {
+ name => \&_check_name,
+ description => \&_check_description,
+ type => \&_check_type,
+ sortkey => \&_check_sortkey,
+ 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;
+
+ $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;
+
+ my $flag = $class->SUPER::create($params);
+
+ $dbh->bz_commit_transaction();
+
+ 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;
+ }
+
+ my $flags = $class->SUPER::match(@_);
+
+ preload_all_the_things($flags, { bug_id => $bug_id });
+
+ 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'} };
+}
+
+sub remove_from_db {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ $dbh->do('DELETE FROM fielddefs WHERE name = ?', undef, $self->name);
+ $self->SUPER::remove_from_db(@_);
+ $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};
+ }
+}
+
+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;
+
+ # 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_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 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 && $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 has_values {
+ my ($self) = @_;
+ return $self->{'has_values'} if defined $self->{'has_values'};
+ my $dbh = Bugzilla->dbh;
+ $self->{'has_values'} = scalar $dbh->selectrow_array("
+ SELECT 1
+ FROM tracking_flags_bugs
+ WHERE tracking_flag_id = ? " .
+ $dbh->sql_limit(1),
+ undef, $self->flag_id);
+ return $self->{'has_values'};
+}
+
+######################################
+# 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; }
+sub enter_bug { return 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..5e2886e66
--- /dev/null
+++ b/extensions/TrackingFlags/lib/Flag/Bug.pm
@@ -0,0 +1,171 @@
+# 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};
+ }
+}
+
+###############################
+#### 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;