summaryrefslogtreecommitdiffstats
path: root/extensions/TrackingFlags
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/TrackingFlags')
-rw-r--r--extensions/TrackingFlags/Config.pm24
-rw-r--r--extensions/TrackingFlags/Extension.pm660
-rwxr-xr-xextensions/TrackingFlags/bin/bulk_flag_clear.pl137
-rwxr-xr-xextensions/TrackingFlags/bin/migrate_tracking_flags.pl316
-rw-r--r--extensions/TrackingFlags/lib/Admin.pm442
-rw-r--r--extensions/TrackingFlags/lib/Constants.pm47
-rw-r--r--extensions/TrackingFlags/lib/Flag.pm467
-rw-r--r--extensions/TrackingFlags/lib/Flag/Bug.pm187
-rw-r--r--extensions/TrackingFlags/lib/Flag/Value.pm130
-rw-r--r--extensions/TrackingFlags/lib/Flag/Visibility.pm172
-rw-r--r--extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl57
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags.html.tmpl29
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl63
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl33
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl54
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl38
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl9
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl10
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl26
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl18
-rw-r--r--extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl58
-rw-r--r--extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl197
-rw-r--r--extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl73
-rw-r--r--extensions/TrackingFlags/web/js/admin.js406
-rw-r--r--extensions/TrackingFlags/web/js/tracking_flags.js56
-rw-r--r--extensions/TrackingFlags/web/styles/admin.css107
-rw-r--r--extensions/TrackingFlags/web/styles/edit_bug.css18
30 files changed, 3888 insertions, 0 deletions
diff --git a/extensions/TrackingFlags/Config.pm b/extensions/TrackingFlags/Config.pm
new file mode 100644
index 000000000..1854cb9fd
--- /dev/null
+++ b/extensions/TrackingFlags/Config.pm
@@ -0,0 +1,24 @@
+# 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;
+use strict;
+
+use constant NAME => 'TrackingFlags';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'JSON-XS',
+ module => 'JSON::XS',
+ version => '2.0'
+ },
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/TrackingFlags/Extension.pm b/extensions/TrackingFlags/Extension.pm
new file mode 100644
index 000000000..7d8f8c401
--- /dev/null
+++ b/extensions/TrackingFlags/Extension.pm
@@ -0,0 +1,660 @@
+# 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;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Extension::TrackingFlags::Constants;
+use Bugzilla::Extension::TrackingFlags::Flag;
+use Bugzilla::Extension::TrackingFlags::Flag::Bug;
+use Bugzilla::Extension::TrackingFlags::Admin;
+
+use Bugzilla::Bug;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Extension::BMO::Data;
+use Bugzilla::Field;
+use Bugzilla::Install::Filesystem;
+use Bugzilla::Product;
+
+our $VERSION = '1';
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ if ($page eq 'tracking_flags_admin_list.html') {
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ { group => 'admin',
+ action => 'access',
+ object => 'administrative_pages' });
+ admin_list($vars);
+
+ } elsif ($page eq 'tracking_flags_admin_edit.html') {
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ { group => 'admin',
+ action => 'access',
+ object => 'administrative_pages' });
+ admin_edit($vars);
+ }
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ if ($file eq 'bug/create/create.html.tmpl'
+ || $file eq 'bug/create/create-winqual.html.tmpl')
+ {
+ $vars->{'tracking_flags'} = Bugzilla::Extension::TrackingFlags::Flag->match({
+ product => $vars->{'product'}->name,
+ enter_bug => 1,
+ is_active => 1,
+ });
+
+ $vars->{'tracking_flag_types'} = FLAG_TYPES;
+ }
+ elsif ($file eq 'bug/edit.html.tmpl'|| $file eq 'bug/show.xml.tmpl'
+ || $file eq 'email/bugmail.html.tmpl' || $file eq 'email/bugmail.txt.tmpl')
+ {
+ # note: bug/edit.html.tmpl doesn't support multiple bugs
+ my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'};
+
+ if ($bug && !$bug->{error}) {
+ $vars->{'tracking_flags'} = Bugzilla::Extension::TrackingFlags::Flag->match({
+ product => $bug->product,
+ component => $bug->component,
+ bug_id => $bug->id,
+ is_active => 1,
+ });
+ }
+
+ $vars->{'tracking_flag_types'} = FLAG_TYPES;
+ }
+ elsif ($file eq 'list/edit-multiple.html.tmpl' && $vars->{'one_product'}) {
+ $vars->{'tracking_flags'} = Bugzilla::Extension::TrackingFlags::Flag->match({
+ product => $vars->{'one_product'}->name,
+ is_active => 1
+ });
+ }
+}
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'tracking_flags'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ field_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'fielddefs',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'
+ }
+ },
+ name => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1,
+ },
+ description => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1,
+ },
+ type => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1,
+ },
+ sortkey => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ DEFAULT => '0',
+ },
+ enter_bug => {
+ TYPE => 'BOOLEAN',
+ NOTNULL => 1,
+ DEFAULT => 'TRUE',
+ },
+ is_active => {
+ TYPE => 'BOOLEAN',
+ NOTNULL => 1,
+ DEFAULT => 'TRUE',
+ },
+ ],
+ INDEXES => [
+ tracking_flags_idx => {
+ FIELDS => ['name'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'tracking_flags_values'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ tracking_flag_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'tracking_flags',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ },
+ },
+ setter_group_id => {
+ TYPE => 'INT3',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'groups',
+ COLUMN => 'id',
+ DELETE => 'SET NULL',
+ },
+ },
+ value => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1,
+ },
+ sortkey => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ DEFAULT => '0',
+ },
+ enter_bug => {
+ TYPE => 'BOOLEAN',
+ NOTNULL => 1,
+ DEFAULT => 'TRUE',
+ },
+ is_active => {
+ TYPE => 'BOOLEAN',
+ NOTNULL => 1,
+ DEFAULT => 'TRUE',
+ },
+ ],
+ INDEXES => [
+ tracking_flags_values_idx => {
+ FIELDS => ['tracking_flag_id', 'value'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'tracking_flags_bugs'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ tracking_flag_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'tracking_flags',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ },
+ },
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE',
+ },
+ },
+ value => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ tracking_flags_bugs_idx => {
+ FIELDS => ['tracking_flag_id', 'bug_id'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'tracking_flags_visibility'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ tracking_flag_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'tracking_flags',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ },
+ },
+ product_id => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'products',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ },
+ },
+ component_id => {
+ TYPE => 'INT2',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'components',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ },
+ },
+ ],
+ INDEXES => [
+ tracking_flags_visibility_idx => {
+ FIELDS => ['tracking_flag_id', 'product_id', 'component_id'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+
+ my $fk = $dbh->bz_fk_info('tracking_flags', 'field_id');
+ if ($fk and !defined $fk->{DELETE}) {
+ $fk->{DELETE} = 'CASCADE';
+ $dbh->bz_alter_fk('tracking_flags', 'field_id', $fk);
+ }
+
+ $dbh->bz_add_column(
+ 'tracking_flags',
+ 'enter_bug',
+ {
+ TYPE => 'BOOLEAN',
+ NOTNULL => 1,
+ DEFAULT => 'TRUE',
+ }
+ );
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{files};
+ my $extensions_dir = bz_locations()->{extensionsdir};
+ $files->{"$extensions_dir/TrackingFlags/bin/bulk_flag_clear.pl"} = {
+ perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE
+ };
+}
+
+sub active_custom_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{'fields'};
+ my $params = $args->{'params'};
+ my $product = $params->{'product'};
+ my $component = $params->{'component'};
+
+ # Create a hash of current fields based on field names
+ my %field_hash = map { $_->name => $_ } @$$fields;
+
+ my @tracking_flags;
+ if ($product) {
+ $params->{'product_id'} = $product->id;
+ $params->{'component_id'} = $component->id if $component;
+ $params->{'is_active'} = 1;
+ @tracking_flags = @{ Bugzilla::Extension::TrackingFlags::Flag->match($params) };
+ }
+ else {
+ @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ }
+
+ # Add tracking flags to fields hash replacing if already exists for our
+ # flag object instead of the usual Field.pm object
+ foreach my $flag (@tracking_flags) {
+ $field_hash{$flag->name} = $flag;
+ }
+
+ @$$fields = sort { $a->sortkey <=> $b->sortkey } values %field_hash;
+}
+
+sub buglist_columns {
+ my ($self, $args) = @_;
+ my $columns = $args->{columns};
+ my $dbh = Bugzilla->dbh;
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ foreach my $flag (@tracking_flags) {
+ $columns->{$flag->name} = {
+ name => "COALESCE(map_" . $flag->name . ".value, '---')",
+ title => $flag->description
+ };
+ }
+}
+
+sub buglist_column_joins {
+ my ($self, $args) = @_;
+ # if there are elements in the tracking_flags array, then they have been
+ # removed from the query, so we mustn't generate joins
+ return if scalar @{ $args->{search}->{tracking_flags} };
+
+ my $column_joins = $args->{'column_joins'};
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ foreach my $flag (@tracking_flags) {
+ $column_joins->{$flag->name} = {
+ as => 'map_' . $flag->name,
+ table => 'tracking_flags_bugs',
+ extra => [ 'map_' . $flag->name . '.tracking_flag_id = ' . $flag->flag_id ]
+ };
+ }
+}
+
+sub bug_create_cf_accessors {
+ my ($self, $args) = @_;
+ # Create the custom accessors for the flag values
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ foreach my $flag (@tracking_flags) {
+ my $flag_name = $flag->name;
+ if (!Bugzilla::Bug->can($flag_name)) {
+ my $accessor = sub {
+ my $self = shift;
+ return $self->{$flag_name} if defined $self->{$flag_name};
+ if (!exists $self->{'_tf_bug_values_preloaded'}) {
+ # preload all values currently set for this bug
+ my $bug_values
+ = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $self->id });
+ foreach my $value (@$bug_values) {
+ $self->{$value->tracking_flag->name} = $value->value;
+ }
+ $self->{'_tf_bug_values_preloaded'} = 1;
+ }
+ return $self->{$flag_name} ||= '---';
+ };
+ no strict 'refs';
+ *{"Bugzilla::Bug::$flag_name"} = $accessor;
+ }
+ if (!Bugzilla::Bug->can("set_$flag_name")) {
+ my $setter = sub {
+ my ($self, $value) = @_;
+ $value = ref($value) eq 'ARRAY'
+ ? $value->[0]
+ : $value;
+ $self->set($flag_name, $value);
+ };
+ no strict 'refs';
+ *{"Bugzilla::Bug::set_$flag_name"} = $setter;
+ }
+ }
+}
+
+sub bug_editable_bug_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{'fields'};
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ foreach my $flag (@tracking_flags) {
+ push(@$fields, $flag->name);
+ }
+}
+
+sub search_operator_field_override {
+ my ($self, $args) = @_;
+ my $operators = $args->{'operators'};
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ foreach my $flag (@tracking_flags) {
+ $operators->{$flag->name} = {
+ _non_changed => sub {
+ _tracking_flags_search_nonchanged($flag->flag_id, @_)
+ }
+ };
+ }
+}
+
+sub _tracking_flags_search_nonchanged {
+ my ($flag_id, $search, $args) = @_;
+ my ($bugs_table, $chart_id, $joins, $value, $operator) =
+ @$args{qw(bugs_table chart_id joins value operator)};
+ my $dbh = Bugzilla->dbh;
+
+ return if ($operator =~ m/^changed/);
+
+ my $bugs_alias = "tracking_flags_bugs_$chart_id";
+ my $flags_alias = "tracking_flags_$chart_id";
+
+ my $bugs_join = {
+ table => 'tracking_flags_bugs',
+ as => $bugs_alias,
+ from => $bugs_table . ".bug_id",
+ to => "bug_id",
+ extra => [$bugs_alias . ".tracking_flag_id = $flag_id"]
+ };
+
+ push(@$joins, $bugs_join);
+
+ $args->{'full_field'} = "COALESCE($bugs_alias.value, '---')";
+}
+
+sub bug_end_of_create {
+ my ($self, $args) = @_;
+ my $bug = $args->{'bug'};
+ my $timestamp = $args->{'timestamp'};
+ my $user = Bugzilla->user;
+
+ my $params = Bugzilla->request_cache->{tracking_flags_create_params};
+ return if !$params;
+
+ my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({
+ product => $bug->product,
+ component => $bug->component,
+ is_active => 1,
+ });
+
+ foreach my $flag (@$tracking_flags) {
+ next if !$params->{$flag->name};
+ foreach my $value (@{$flag->values}) {
+ next if $value->value ne $params->{$flag->name};
+ next if $value->value eq '---'; # do not insert if value is '---', same as empty
+ if (!$flag->can_set_value($value->value)) {
+ ThrowUserError('tracking_flags_change_denied',
+ { flag => $flag, value => $value });
+ }
+ Bugzilla::Extension::TrackingFlags::Flag::Bug->create({
+ tracking_flag_id => $flag->flag_id,
+ bug_id => $bug->id,
+ value => $value->value,
+ });
+ # Add the name/value pair to the bug object
+ $bug->{$flag->name} = $value->value;
+ }
+ }
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ my $params = $args->{params};
+
+ return unless $object->isa('Bugzilla::Bug');
+
+ # Do not filter by product/component as we may be changing those
+ my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({
+ bug_id => $object->id,
+ is_active => 1,
+ });
+
+ foreach my $flag (@$tracking_flags) {
+ my $flag_name = $flag->name;
+ if (exists $params->{$flag_name}) {
+ my $value = ref($params->{$flag_name}) eq 'ARRAY'
+ ? $params->{$flag_name}->[0]
+ : $params->{$flag_name};
+ $object->set($flag_name, $value);
+ }
+ }
+}
+
+sub bug_check_can_change_field {
+ my ($self, $args) = @_;
+ my ($bug, $field, $old_value, $new_value, $priv_results)
+ = @$args{qw(bug field old_value new_value priv_results)};
+
+ return if $field !~ /^cf_/ or $old_value eq $new_value;
+ return unless my $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ name => $field });
+
+ if ($flag->can_set_value($new_value)) {
+ push @$priv_results, PRIVILEGES_REQUIRED_NONE;
+ }
+ else {
+ push @$priv_results, PRIVILEGES_REQUIRED_EMPOWERED;
+ }
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+ my ($bug, $old_bug, $timestamp, $changes)
+ = @$args{qw(bug old_bug timestamp changes)};
+ my $user = Bugzilla->user;
+
+ # Do not filter by product/component as we may be changing those
+ my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({
+ bug_id => $bug->id,
+ is_active => 1,
+ });
+
+ my (@flag_changes);
+ foreach my $flag (@$tracking_flags) {
+ my $flag_name = $flag->name;
+ my $new_value = $bug->$flag_name;
+ my $old_value = $old_bug->$flag_name;
+
+ if ($new_value ne $old_value) {
+ # Do not allow if the user cannot set the old value or the new value
+ if (!$flag->can_set_value($new_value)) {
+ ThrowUserError('tracking_flags_change_denied',
+ { flag => $flag, value => $new_value });
+ }
+ push(@flag_changes, { flag => $flag,
+ added => $new_value,
+ removed => $old_value });
+ }
+ }
+
+ foreach my $change (@flag_changes) {
+ my $flag = $change->{'flag'};
+ my $added = $change->{'added'};
+ my $removed = $change->{'removed'};
+
+ if ($added eq '---') {
+ $flag->bug_flag->remove_from_db();
+ }
+ elsif ($removed eq '---') {
+ Bugzilla::Extension::TrackingFlags::Flag::Bug->create({
+ tracking_flag_id => $flag->flag_id,
+ bug_id => $bug->id,
+ value => $added,
+ });
+ }
+ else {
+ $flag->bug_flag->set_value($added);
+ $flag->bug_flag->update($timestamp);
+ }
+
+ $changes->{$flag->name} = [ $removed, $added ];
+ LogActivityEntry($bug->id, $flag->name, $removed, $added, $user->id, $timestamp);
+
+ # Update the name/value pair in the bug object
+ $bug->{$flag->name} = $added;
+ }
+}
+
+sub bug_end_of_create_validators {
+ my ($self, $args) = @_;
+ my $params = $args->{params};
+
+ # We need to stash away any params that are setting/updating tracking
+ # flags early on. Otherwise set_all or insert_create_data will complain.
+ my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all;
+ my $cache = Bugzilla->request_cache->{tracking_flags_create_params} ||= {};
+ foreach my $flag (@tracking_flags) {
+ my $flag_name = $flag->name;
+ if (defined $params->{$flag_name}) {
+ $cache->{$flag_name} = delete $params->{$flag_name};
+ }
+ }
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+ my $email = $args->{email};
+
+ # Add X-Bugzilla-Tracking header or add to it
+ # if already exists
+ if ($email->header('X-Bugzilla-ID')) {
+ my $bug_id = $email->header('X-Bugzilla-ID');
+
+ my $tracking_flags
+ = Bugzilla::Extension::TrackingFlags::Flag->match({ bug_id => $bug_id });
+
+ my @set_values = ();
+ foreach my $flag (@$tracking_flags) {
+ next if $flag->bug_flag->value eq '---';
+ push(@set_values, $flag->description . ":" . $flag->bug_flag->value);
+ }
+
+ if (@set_values) {
+ my $set_values_string = join(' ', @set_values);
+ if ($email->header('X-Bugzilla-Tracking')) {
+ $set_values_string = $email->header('X-Bugzilla-Tracking') .
+ " " . $set_values_string;
+ }
+ $email->header_set('X-Bugzilla-Tracking' => $set_values_string);
+ }
+ }
+}
+
+# Purpose: generically handle generating pretty blocking/status "flags" from
+# custom field names.
+sub quicksearch_map {
+ my ($self, $args) = @_;
+ my $map = $args->{'map'};
+
+ foreach my $name (keys %$map) {
+ if ($name =~ /^cf_(blocking|tracking|status)_([a-z]+)?(\d+)?$/) {
+ my $type = $1;
+ my $product = $2;
+ my $version = $3;
+
+ if ($version) {
+ $version = join('.', split(//, $version));
+ }
+
+ my $pretty_name = $type;
+ if ($product) {
+ $pretty_name .= "-" . $product;
+ }
+ if ($version) {
+ $pretty_name .= $version;
+ }
+
+ $map->{$pretty_name} = $name;
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/TrackingFlags/bin/bulk_flag_clear.pl b/extensions/TrackingFlags/bin/bulk_flag_clear.pl
new file mode 100755
index 000000000..1eff355fe
--- /dev/null
+++ b/extensions/TrackingFlags/bin/bulk_flag_clear.pl
@@ -0,0 +1,137 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../../..";
+use lib "$RealBin/../../../lib";
+use lib "$RealBin/../lib";
+
+BEGIN {
+ use Bugzilla;
+ Bugzilla->extensions;
+}
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::TrackingFlags::Flag;
+use Bugzilla::Extension::TrackingFlags::Flag::Bug;
+use Bugzilla::User;
+
+use Getopt::Long;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $config = {};
+GetOptions(
+ $config,
+ "trace=i",
+ "update_db",
+ "flag=s",
+ "modified_before=s",
+ "modified_after=s",
+ "value=s"
+) or exit;
+unless ($config->{flag}
+ && ($config->{modified_before}
+ || $config->{modified_after}
+ || $config->{value}))
+{
+ die <<EOF;
+$0
+ clears tracking flags matching the specified criteria.
+ the last-modified will be updated, however bugmail will not be generated.
+
+SYNTAX
+ $0 --flag <flag> (conditions) [--update_db]
+
+CONDITIONS
+ --modified_before <datetime> bug last-modified before <datetime>
+ --modified_after <datetime> bug last-modified after <datetime>
+ --value <flag value> flag = <flag value>
+
+OPTIONS
+ --update_db : by default only the impacted bugs will be listed. pass this
+ switch to update the database.
+EOF
+}
+
+# build sql
+
+my (@where, @values);
+
+my $flag = Bugzilla::Extension::TrackingFlags::Flag->check({ name => $config->{flag} });
+push @where, 'tracking_flags_bugs.tracking_flag_id = ?';
+push @values, $flag->flag_id;
+
+if ($config->{modified_before}) {
+ push @where, 'bugs.delta_ts < ?';
+ push @values, $config->{modified_before};
+}
+
+if ($config->{modified_after}) {
+ push @where, 'bugs.delta_ts > ?';
+ push @values, $config->{modified_after};
+}
+
+if ($config->{value}) {
+ push @where, 'tracking_flags_bugs.value = ?';
+ push @values, $config->{value};
+}
+
+my $sql = "
+ SELECT tracking_flags_bugs.bug_id
+ FROM tracking_flags_bugs
+ INNER JOIN bugs ON bugs.bug_id = tracking_flags_bugs.bug_id
+ WHERE (" . join(") AND (", @where) . ")
+ ORDER BY tracking_flags_bugs.bug_id
+";
+
+# execute query
+
+my $dbh = Bugzilla->dbh;
+$dbh->{TraceLevel} = $config->{trace} if $config->{trace};
+
+my $bug_ids = $dbh->selectcol_arrayref($sql, undef, @values);
+
+if (!@$bug_ids) {
+ die "no matching bugs found\n";
+}
+
+if (!$config->{update_db}) {
+ print "bugs found: ", scalar(@$bug_ids), "\n\n", join(',', @$bug_ids), "\n\n";
+ print "--update_db not provided, no changes made to the database\n";
+ exit;
+}
+
+# update bugs
+
+my $nobody = Bugzilla::User->check({ name => 'nobody@mozilla.org' });
+# put our nobody user into all groups to avoid permissions issues
+$nobody->{groups} = [Bugzilla::Group->get_all];
+Bugzilla->set_user($nobody);
+
+foreach my $bug_id (@$bug_ids) {
+ print "updating bug $bug_id\n";
+ $dbh->bz_start_transaction;
+
+ # update the bug
+ # this will deal with history for us but not send bugmail
+ my $bug = Bugzilla::Bug->check({ id => $bug_id });
+ $bug->set_all({ $flag->name => '---' });
+ $bug->update;
+
+ # update lastdiffed to skip bugmail for this change
+ $dbh->do(
+ "UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?",
+ undef,
+ $bug->id
+ );
+ $dbh->bz_commit_transaction;
+}
diff --git a/extensions/TrackingFlags/bin/migrate_tracking_flags.pl b/extensions/TrackingFlags/bin/migrate_tracking_flags.pl
new file mode 100755
index 000000000..06b3596c4
--- /dev/null
+++ b/extensions/TrackingFlags/bin/migrate_tracking_flags.pl
@@ -0,0 +1,316 @@
+#!/usr/bin/perl -w
+# 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.
+
+# Migrate old custom field based tracking flags to the new
+# table based tracking flags
+
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../../..";
+use lib "$RealBin/../../../lib";
+use lib "$RealBin/../lib";
+
+BEGIN {
+ use Bugzilla;
+ Bugzilla->extensions;
+}
+
+use Bugzilla::Constants;
+use Bugzilla::Field;
+use Bugzilla::Product;
+use Bugzilla::Component;
+use Bugzilla::Extension::BMO::Data;
+use Bugzilla::Install::Util qw(indicate_progress);
+
+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 Getopt::Long;
+use Data::Dumper;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my ($dry_run, $trace) = (0, 0);
+GetOptions(
+ "dry-run" => \$dry_run,
+ "trace" => \$trace,
+) or exit;
+
+my $dbh = Bugzilla->dbh;
+
+$dbh->{TraceLevel} = 1 if $trace;
+
+my %product_cache;
+my %component_cache;
+
+sub migrate_flag_visibility {
+ my ($new_flag, $products) = @_;
+
+ # Create product/component visibility
+ foreach my $prod_name (keys %$products) {
+ $product_cache{$prod_name} ||= Bugzilla::Product->new({ name => $prod_name });
+ if (!$product_cache{$prod_name}) {
+ warn "No such product $prod_name\n";
+ next;
+ }
+
+ # If no components specified then we do Product/__any__
+ # otherwise, we enter an entry for each Product/Component
+ my $components = $products->{$prod_name};
+ if (!@$components) {
+ Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({
+ tracking_flag_id => $new_flag->flag_id,
+ product_id => $product_cache{$prod_name}->id,
+ component_id => undef
+ });
+ }
+ else {
+ foreach my $comp_name (@$components) {
+ my $comp_matches = [];
+ # If the component is a regexp, we need to find all components
+ # matching the regex and insert each individually
+ if (ref $comp_name eq 'Regexp') {
+ my $comp_re = $comp_name;
+ $comp_re =~ s/\?\-xism://;
+ $comp_re =~ s/\(//;
+ $comp_re =~ s/\)//;
+ $comp_matches = $dbh->selectcol_arrayref(
+ 'SELECT components.name FROM components
+ WHERE components.product_id = ?
+ AND ' . $dbh->sql_regexp('components.name', $dbh->quote($comp_re)) . '
+ ORDER BY components.name',
+ undef,
+ $product_cache{$prod_name}->id);
+ }
+ else {
+ $comp_matches = [ $comp_name ];
+ }
+
+ foreach my $comp_match (@$comp_matches) {
+ $component_cache{"${prod_name}:${comp_match}"}
+ ||= Bugzilla::Component->new({ name => $comp_match,
+ product => $product_cache{$prod_name} });
+ if (!$component_cache{"${prod_name}:${comp_match}"}) {
+ warn "No such product $prod_name and component $comp_match\n";
+ next;
+ }
+
+ Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({
+ tracking_flag_id => $new_flag->flag_id,
+ product_id => $product_cache{$prod_name}->id,
+ component_id => $component_cache{"${prod_name}:${comp_match}"}->id,
+ });
+ }
+ }
+ }
+ }
+}
+
+sub migrate_flag_values {
+ my ($new_flag, $field) = @_;
+
+ print "Migrating flag values...";
+
+ my %blocking_trusted_requesters
+ = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_requesters};
+ my %blocking_trusted_setters
+ = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_setters};
+ my %status_trusted_wanters
+ = %{$Bugzilla::Extension::BMO::Data::status_trusted_wanters};
+ my %status_trusted_setters
+ = %{$Bugzilla::Extension::BMO::Data::status_trusted_setters};
+
+ my %group_cache;
+ foreach my $value (@{ $field->legal_values }) {
+ my $group_name = 'everyone';
+
+ if ($field->name =~ /^cf_(blocking|tracking)_/) {
+ if ($value->name ne '---' && $value->name !~ '\?$') {
+ $group_name = get_setter_group($field->name, \%blocking_trusted_setters);
+ }
+ if ($value->name eq '?') {
+ $group_name = get_setter_group($field->name, \%blocking_trusted_requesters);
+ }
+ } elsif ($field->name =~ /^cf_status_/) {
+ if ($value->name eq 'wanted') {
+ $group_name = get_setter_group($field->name, \%status_trusted_wanters);
+ } elsif ($value->name ne '---' && $value->name ne '?') {
+ $group_name = get_setter_group($field->name, \%status_trusted_setters);
+ }
+ }
+
+ $group_cache{$group_name} ||= Bugzilla::Group->new({ name => $group_name });
+ $group_cache{$group_name} || die "Setter group '$group_name' does not exist";
+
+ Bugzilla::Extension::TrackingFlags::Flag::Value->create({
+ tracking_flag_id => $new_flag->flag_id,
+ value => $value->name,
+ setter_group_id => $group_cache{$group_name}->id,
+ sortkey => $value->sortkey,
+ is_active => $value->is_active
+ });
+ }
+
+ print "done.\n";
+}
+
+sub get_setter_group {
+ my ($field, $trusted) = @_;
+ my $setter_group = $trusted->{'_default'} || "";
+ foreach my $dfield (keys %$trusted) {
+ if ($field =~ $dfield) {
+ $setter_group = $trusted->{$dfield};
+ }
+ }
+ return $setter_group;
+}
+
+sub migrate_flag_bugs {
+ my ($new_flag, $field) = @_;
+
+ print "Migrating bug values...";
+
+ my $bugs = $dbh->selectall_arrayref("SELECT bug_id, " . $field->name . "
+ FROM bugs
+ WHERE " . $field->name . " != '---'
+ ORDER BY bug_id");
+ local $| = 1;
+ my $count = 1;
+ my $total = scalar @$bugs;
+ foreach my $row (@$bugs) {
+ my ($id, $value) = @$row;
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ Bugzilla::Extension::TrackingFlags::Flag::Bug->create({
+ tracking_flag_id => $new_flag->flag_id,
+ bug_id => $id,
+ value => $value,
+
+ });
+ }
+
+ print "done.\n";
+}
+
+sub migrate_flag_activity {
+ my ($new_flag, $field) = @_;
+
+ print "Migating flag activity...";
+
+ my $new_field = Bugzilla::Field->new({ name => $new_flag->name });
+ $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE fieldid = ?",
+ undef, $new_field->id, $field->id);
+
+ print "done.\n";
+}
+
+sub do_migration {
+ my $bmo_tracking_flags = $Bugzilla::Extension::BMO::Data::cf_visible_in_products;
+ my $bmo_project_flags = $Bugzilla::Extension::BMO::Data::cf_project_flags;
+ my $bmo_disabled_flags = $Bugzilla::Extension::BMO::Data::cf_disabled_flags;
+
+ my $fields = Bugzilla::Field->match({ custom => 1,
+ type => FIELD_TYPE_SINGLE_SELECT });
+
+ my @drop_columns;
+ foreach my $field (@$fields) {
+ next if $field->name !~ /^cf_(blocking|tracking|status)_/;
+
+ foreach my $field_re (keys %$bmo_tracking_flags) {
+ next if $field->name !~ $field_re;
+
+ # Create the new tracking flag if not exists
+ my $new_flag
+ = Bugzilla::Extension::TrackingFlags::Flag->new({ name => $field->name });
+
+ next if $new_flag;
+
+ print "----------------------------------\n" .
+ "Migrating custom tracking field " . $field->name . "...\n";
+
+ my $new_flag_name = $field->name . "_new"; # Temporary name til we delete the old
+
+ my $type = grep($field->name =~ $_, @$bmo_project_flags)
+ ? 'project'
+ : 'tracking';
+
+ my $is_active = grep($_ eq $field->name, @$bmo_disabled_flags) ? 0 : 1;
+
+ $new_flag = Bugzilla::Extension::TrackingFlags::Flag->create({
+ name => $new_flag_name,
+ description => $field->description,
+ type => $type,
+ sortkey => $field->sortkey,
+ is_active => $is_active,
+ enter_bug => $field->enter_bug,
+ });
+
+ migrate_flag_visibility($new_flag, $bmo_tracking_flags->{$field_re});
+
+ migrate_flag_values($new_flag, $field);
+
+ migrate_flag_bugs($new_flag, $field);
+
+ migrate_flag_activity($new_flag, $field);
+
+ push(@drop_columns, $field->name);
+
+ # Remove the old flag entry from fielddefs
+ $dbh->do("DELETE FROM fielddefs WHERE name = ?",
+ undef, $field->name);
+
+ # Rename the new flag
+ $dbh->do("UPDATE fielddefs SET name = ? WHERE name = ?",
+ undef, $field->name, $new_flag_name);
+
+ $new_flag->set_name($field->name);
+ $new_flag->update;
+
+ # more than one regex could possibly match but we only want the first one
+ last;
+ }
+ }
+
+ # Drop each custom flag's value table and the column from the bz schema object
+ if (!$dry_run && @drop_columns) {
+ print "Dropping value tables and updating bz schema object...\n";
+
+ foreach my $column (@drop_columns) {
+ # Drop the values table
+ $dbh->bz_drop_table($column);
+
+ # Drop the bugs table column from the bz schema object
+ $dbh->_bz_real_schema->delete_column('bugs', $column);
+ $dbh->_bz_store_real_schema;
+ }
+
+ # Do the one alter table to drop all columns at once
+ $dbh->do("ALTER TABLE bugs DROP COLUMN " . join(", DROP COLUMN ", @drop_columns));
+ }
+}
+
+# Start Main
+
+eval {
+ if ($dry_run) {
+ print "** dry run : no changes to the database will be made **\n";
+ $dbh->bz_start_transaction();
+ }
+ print "Starting migration...\n";
+ do_migration();
+ $dbh->bz_rollback_transaction() if $dry_run;
+ print "All done!\n";
+};
+if ($@) {
+ $dbh->bz_rollback_transaction() if $dry_run;
+ die "$@" if $@;
+}
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;
diff --git a/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl b/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl
new file mode 100644
index 000000000..b2b6efca7
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl
@@ -0,0 +1,57 @@
+[%# 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.
+ #%]
+
+[% FOREACH flag = flag_list %]
+ [% SET bug_id = bug.defined ? bug.id : 0 %]
+ [% SET flag_bug_value = flag.bug_flag(bug_id).value %]
+ [% NEXT IF !new_bug && (!user.id && flag_bug_value == '---') %]
+ <tr id="row_[% flag.name FILTER html %]">
+ <td [% IF new_bug %]class="field_label"[% END %]>
+ <label for="[% flag.name FILTER html %]">
+ [% IF new_bug %]
+ <a
+ [% IF help_html.${flag.name}.defined %]
+ title="[% help_html.${flag.name} FILTER txt FILTER collapse FILTER html %]"
+ class="field_help_link"
+ [% END %]
+ href="page.cgi?id=fields.html#[% flag.name FILTER uri %]">
+ [% END %]
+ [% flag.description FILTER html %]
+ [% IF new_bug %]
+ </a>
+ [% END %]:</label>
+ </td>
+ <td>
+ [% IF user.id %]
+ <input type="hidden" id="[% flag.name FILTER html %]_dirty">
+ <select id="[% flag.name FILTER html %]"
+ name="[% flag.name FILTER html %]">
+ [% FOREACH value = flag.values %]
+ [% IF new_bug || value.name != flag_bug_value %]
+ [% NEXT IF !value.is_active || !flag.can_set_value(value.name) %]
+ [% END %]
+ <option value="[% value.name FILTER html %]"
+ id="v[% value.id FILTER html %]_[% flag.name FILTER html %]"
+ [% " selected" IF !new_bug && flag_bug_value == value.name %]>
+ [% value.name FILTER html %]</option>
+ [% END %]
+ </select>
+ <script type="text/javascript">
+ initHidingOptionsForIE('[% flag.name FILTER js %]');
+ </script>
+ [% IF !new_bug && user.id %]
+ <span id="ro_[% flag.name FILTER html %]" class="bz_default_hidden">
+ [% flag_bug_value FILTER html %]
+ </span>
+ [% END %]
+ [% ELSE %]
+ [% flag_bug_value FILTER html %]
+ [% END %]
+ </td>
+ </tr>
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl
new file mode 100644
index 000000000..4808da069
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl
@@ -0,0 +1,18 @@
+[%# 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.
+ #%]
+
+[% IF user.in_group('admin') %]
+ <dt id="push">
+ <a href="page.cgi?id=tracking_flags_admin_list.html">Release Tracking Flags</a>
+ </dt>
+ <dd>
+ Tracking flags are special multi-value fields used to aid tracking releases
+ of Firefox, Firefox OS, Thunderbird, and other projects.
+ </dd>
+[% END %]
+
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags.html.tmpl
new file mode 100644
index 000000000..b41e1619f
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags.html.tmpl
@@ -0,0 +1,29 @@
+[%# 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.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+<td>
+ <table class="tracking_flags">
+ [% FOREACH type = tracking_flag_types %]
+ [% flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% flag_list.push(flag) IF flag.flag_type == type.name %]
+ [% END %]
+ [% IF flag_list.size %]
+ <tr>
+ <th style="text-align:right">
+ [% type.description FILTER html %]:
+ </th>
+ </tr>
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
+ [% END %]
+ [% END %]
+ </table>
+</td>
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl
new file mode 100644
index 000000000..59fe1d0ec
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-form.html.tmpl
@@ -0,0 +1,63 @@
+[%# 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.
+ #%]
+
+[% IF tracking_flags.size %]
+ [% tracking_flag_names = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% tracking_flag_names.push(flag.name) %]
+ [% END %]
+
+ <script type="text/javascript">
+ [% js_filtered_names = [] %]
+ [% FOREACH flag = tracking_flag_names %]
+ [% js_filtered = flag FILTER js %]
+ [% js_filtered_names.push(js_filtered) %]
+ [% END %]
+ var tracking_flag_names = ['[% js_filtered_names.join("','") FILTER none %]'];
+ var tracking_flags = new Array([% product.components.size %]);
+
+ [% count = 0 %]
+ [% FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ [% tracking_flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% FOREACH v = flag.visibility %]
+ [% IF v.product_id == product.id
+ && (!v.component_id.defined || v.component_id == c.id) %]
+ [% tracking_flag_list.push(flag.name) %]
+ [% END %]
+ [% END %]
+ [% END %]
+ [% js_filtered_flags = [] %]
+ [% FOREACH flag = tracking_flag_list %]
+ [% js_filtered = flag FILTER js %]
+ [% js_filtered_flags.push(js_filtered) %]
+ [% END %]
+ tracking_flags[[% count %]] = ['[% js_filtered_flags.join("','") FILTER none %]'];
+ [% count = count + 1 %]
+ [% END %]
+
+ function update_tracking_flags () {
+ var component = document.getElementById('component');
+ // First, we disable all flags.
+ for (var i = 0; i < tracking_flag_names.length; i++) {
+ var flagField = document.getElementById(tracking_flag_names[i]);
+ flagField.disabled = true;
+ }
+ // Now enable flags available for the selected component.
+ var index = component.selectedIndex;
+ for (var i = 0; i < tracking_flags[index].length; i++) {
+ var flagField = document.getElementById(tracking_flags[index][i]);
+ flagField.disabled = false;
+ }
+ }
+
+ YAHOO.util.Event.onDOMReady(update_tracking_flags);
+ YAHOO.util.Event.addListener("component", "change", update_tracking_flags);
+ </script>
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl
new file mode 100644
index 000000000..2a90cbfe3
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-bug_flags_end.html.tmpl
@@ -0,0 +1,33 @@
+[%# 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.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% FOREACH type = tracking_flag_types %]
+ [% NEXT IF type.name == 'tracking' || type.name == 'project' %]
+ [% flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% flag_list.push(flag) IF flag.flag_type == type.name %]
+ [% END %]
+ [% IF flag_list.size %]
+ <tr>
+ <td>
+ <table class="tracking_flags">
+ <tr>
+ <th>
+ [% type.description FILTER html %]:
+ </th>
+ </tr>
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
+ </table>
+ </td>
+ </tr>
+ [% END %]
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl
new file mode 100644
index 000000000..662bc26ee
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-project_flags_end.html.tmpl
@@ -0,0 +1,18 @@
+[%# 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.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% flag_list = [] %]
+[% FOREACH flag = tracking_flags %]
+ [% NEXT IF flag.flag_type != 'project' %]
+ [% flag_list.push(flag) %]
+[% END %]
+[% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl
new file mode 100644
index 000000000..69827a87a
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-winqual-tracking_flags_end.html.tmpl
@@ -0,0 +1,18 @@
+[%# 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.
+ #%]
+
+[% RETURN IF NOT tracking_flags.size %]
+
+[% flag_list = [] %]
+[% FOREACH flag = tracking_flags %]
+ [% NEXT IF flag.flag_type != 'tracking' %]
+ [% flag_list.push(flag) %]
+[% END %]
+[% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list
+ new_bug = 1 %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..e0411b512
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,54 @@
+[%# 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.
+ #%]
+
+[% IF tracking_flags.size %]
+ [% FOREACH type = tracking_flag_types %]
+ [% flag_list = [] %]
+ [% FOREACH flag = tracking_flags %]
+ [% flag_list.push(flag) IF flag.flag_type == type.name %]
+ [% END %]
+ [% IF flag_list.size %]
+ <tr>
+ <td class="field_label">
+ <label>[% type.description FILTER html %]:</label>
+ </td>
+ <td>
+ [% IF bug.check_can_change_field('flagtypes.name', 0, 1) %]
+ [% IF user.id && type.collapsed %]
+ <span id="edit_[% type.name FILTER html %]_flags_action">
+ (<a href="#" name="[% type.name FILTER html %]" class="edit_tracking_flags_link">edit</a>)
+ </span>
+ [% END %]
+ <table class="tracking_flags">
+ [% INCLUDE bug/tracking_flags.html.tmpl
+ flag_list = flag_list %]
+ </table>
+ [% IF type.collapsed %]
+ <script type="text/javascript">
+ TrackingFlags.flags['[% type.name FILTER js %]'] = {};
+ [% FOREACH flag = flag_list %]
+ TrackingFlags.flags['[% type.name FILTER js %]']['[% flag.name FILTER js %]'] = '[% flag.bug_flag.value FILTER js %]';
+ [% END %]
+ TrackingFlags.types.push('[% type.name FILTER js %]');
+ </script>
+ [% END %]
+ [% ELSE %]
+ [% FOREACH flag = flag_list %]
+ [% NEXT IF flag.status == '---' %]
+ [% flag.description FILTER html %]: [% flag.bug_flag.value FILTER html %]<br>
+ [% END %]
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% END %]
+[% END %]
+
+<script type="text/javascript">
+ hide_tracking_flags();
+</script>
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl
new file mode 100644
index 000000000..f598609e8
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl
@@ -0,0 +1,38 @@
+[%# 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.
+ #%]
+
+<input type="hidden" id="[% field.name FILTER html %]_dirty">
+<select id="[% field.name FILTER html %]"
+ name="[% field.name FILTER html %]">
+ [% IF allow_dont_change %]
+ <option value="[% dontchange FILTER html %]"
+ [% ' selected="selected"' IF value == dontchange %]>
+ [% dontchange FILTER html %]
+ </option>
+ [% END %]
+ [% FOREACH legal_value = field.values %]
+ [% IF legal_value.name != value %]
+ [% NEXT IF !field.can_set_value(legal_value.name) %]
+ [% NEXT IF !legal_value.is_active %]
+ [% END %]
+ <option value="[% legal_value.name FILTER html %]"
+ id="v[% legal_value.id FILTER html %] [%- field.name FILTER html %]"
+ [% IF legal_value.name == value %]
+ selected="selected"
+ [% END %]>
+ [%- display_value(field.name, legal_value.name) FILTER html ~%]
+ </option>
+ [% END %]
+</select>
+<script type="text/javascript">
+<!--
+ initHidingOptionsForIE('[% field.name FILTER js %]');
+ [%+ INCLUDE "bug/field-events.js.tmpl"
+ field = field, product = bug.product_obj %]
+//-->
+</script>
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl
new file mode 100644
index 000000000..8fa1f1623
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl
@@ -0,0 +1,9 @@
+[%# 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.
+ #%]
+
+[% display_value(field.name, value) FILTER html %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..5e4ef2fcb
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,10 @@
+[%# 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.
+ #%]
+
+[% javascript_urls.push('extensions/TrackingFlags/web/js/tracking_flags.js') %]
+[% style_urls.push('extensions/TrackingFlags/web/styles/edit_bug.css') %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl
new file mode 100644
index 000000000..2d462ebaf
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl
@@ -0,0 +1,26 @@
+[%# 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.
+ #%]
+
+[% IF error == "tracking_flags_invalid_product" %]
+ [% title = "Invalid Product" %]
+ The product named '[% product FILTER html %]' does not exist.
+
+[% ELSIF error == "tracking_flags_invalid_component" %]
+ [% title = "Invalid Component" %]
+ The component named '[% component FILTER html %]' does not exist.
+
+[% ELSIF error == "tracking_flags_invalid_item_id" %]
+ [% title = "Invalid " _ item _ " ID" %]
+ Invalid [% item FILTER html %] ID ([% id FILTER html %]).
+
+[% ELSIF error == "tracking_flags_invalid_param" %]
+ [% title = "Invalid Parameter Provided" %]
+ An invalid parameter '[% value FILTER html %]'
+ for '[% name FILTER html %]' was provided.
+
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..ce254b8cc
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,18 @@
+[%# 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.
+ #%]
+
+[% IF message_tag == 'tracking_flag_created' %]
+ The tracking flag '[% flag.name FILTER html %]' has been created.
+
+[% ELSIF message_tag == 'tracking_flag_updated' %]
+ The tracking flag '[% flag.name FILTER html %]' has been updated.
+
+[% ELSIF message_tag == "tracking_flag_deleted" %]
+ The tracking flag '[% flag.name FILTER html %]' has been deleted.
+
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..8c067a5d1
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,58 @@
+[%# 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.
+ #%]
+
+[% IF error == "tracking_flags_change_denied" %]
+ [% title = "Tracking Flag Modification Denied" %]
+ You tried to update the status of the tracking flag '[% flag.name FILTER html %]'
+ [% IF value %] to '[% value FILTER html %]'[% END %].
+ Only a user with the required permissions may make this change.
+
+[% ELSIF error == "tracking_flags_missing_mandatory" %]
+ [% IF fields.size == 1 %]
+ [% title = "Missing mandatory field" %]
+ The field "[% fields.first FILTER html %]" is mandatory, and must be provided.
+ [% ELSE %]
+ [% title = "Missing mandatory fields" %]
+ The following fields are mandatory, and must be provided:
+ [%+ fields.join(', ') FILTER html %]
+ [% END %]
+
+[% ELSIF error == "tracking_flags_cf_prefix" %]
+ [% title = "Invalid flag name" %]
+ The flag name must start with 'cf_'.
+
+[% ELSIF error == "tracking_flags_missing_values" %]
+ [% title = "Missing values" %]
+ You must provide at least one value.
+
+[% ELSIF error == "tracking_flags_missing_value" %]
+ [% title = "Missing value" %]
+ You must provied the value for all values.
+
+[% ELSIF error == "tracking_flags_duplicate_value" %]
+ [% title = "Duplicate value" %]
+ The value "[% value FILTER html %]" has been provided more than once.
+
+[% ELSIF error == "tracking_flags_missing_visibility" %]
+ [% title = "Missing visibility" %]
+ You must provide at least one product for visibility.
+
+[% ELSIF error == "tracking_flags_duplicate_visibility" %]
+ [% title = "Duplicate visibility" %]
+ The visibility '[% name FILTER html %]' has been provided more than once.
+
+[% ELSIF error == "tracking_flags_invalid_flag_type" %]
+ [% title = "Invalid flag type" %]
+ The flag type '[% type FILTER html %]' is invalid.
+
+[% ELSIF error == "tracking_flag_has_contents" %]
+ [% title = "Tracking Flag Has Contents" %]
+ The tracking flag '[% flag.name FILTER html %]' cannot be deleted because
+ at least one [% terms.bug %] has a non empty value for this field.
+
+[% END %]
diff --git a/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl
new file mode 100644
index 000000000..12c8d2c3b
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl
@@ -0,0 +1,197 @@
+[%# 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.
+ #%]
+
+[% js_data = BLOCK %]
+var useclassification = false;
+var first_load = true;
+var last_sel = [];
+var cpts = new Array();
+[% n = 1 %]
+[% FOREACH p = user.get_selectable_products %]
+ cpts['[% n FILTER js %]'] = [
+ [%- FOREACH c = p.components %]'[% c.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% n = n+1 %]
+[% END %]
+var selected_components = [
+ [%- FOREACH c = input.component %]'[% c FILTER js %]'
+ [%- ',' UNLESS loop.last %] [%- END ~%] ];
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Release Tracking Flags"
+ javascript = js_data
+ javascript_urls = [ 'extensions/TrackingFlags/web/js/admin.js', 'js/productform.js' ]
+ style_urls = [ 'extensions/TrackingFlags/web/styles/admin.css' ]
+%]
+
+<script>
+ var groups = [% groups || '[]' FILTER none %];
+ var flag_values = [% values || '[]' FILTER none %];
+ var flag_visibility = [% visibility || '[]' FILTER none %];
+</script>
+
+<div id="edit_mode">
+ [% IF mode == 'edit' %]
+ Editing <b>[% flag.name FILTER html %]</b>.
+ [% ELSE %]
+ New flag
+ [% END %]
+</div>
+
+<form method="POST" action="page.cgi" onsubmit="return on_submit()">
+<input type="hidden" name="id" value="tracking_flags_admin_edit.html">
+<input type="hidden" name="mode" value="[% mode FILTER html %]">
+<input type="hidden" name="flag_id" value="[% flag ? flag.flag_id : 0 FILTER html %]">
+<input type="hidden" name="values" id="values" value="">
+<input type="hidden" name="visibility" id="visibility" value="">
+<input type="hidden" name="save" value="1">
+
+[%# name/desc/etc %]
+
+<table class="edit" cellspacing="0">
+
+<tr class="header">
+ <th colspan="3">Flag</th>
+</tr>
+
+<tr>
+ <th>Name</th>
+ <td><input name="flag_name" id="flag_name" value="[% flag.name FILTER html %]"></td>
+ <td class="help">database field name</td>
+</tr>
+
+<tr>
+ <th>Description</th>
+ <td><input name="flag_desc" id="flag_desc" value="[% flag.description FILTER html %]"></td>
+ <td class="help">visible name</td>
+</tr>
+
+<tr>
+ <th>Type</th>
+ <td>
+ <select name="flag_type" id="flag_type">
+ <option value=""></option>
+ [% FOREACH type = tracking_flag_types %]
+ <option value="[% type.name FILTER html %]"
+ [% 'selected="selected"' IF flag.flag_type == type.name %]>
+ [% type.name FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+ <td class="help">flag type used for grouping</td>
+</tr>
+
+<tr>
+ <th>Sort Key</th>
+ <td>
+ <input name="flag_sort" id="flag_sort" value="[% flag.sortkey FILTER html %]">
+ [
+ <a class="txt_icon" href="#" onclick="inc_field('flag_sort', 5);return false">+5</a>
+ | <a class="txt_icon" href="#" onclick="inc_field('flag_sort', -5);return false">-5</a>
+ ]
+ </td>
+</tr>
+
+<tr>
+ <th>Enter [% terms.Bug %]</th>
+ <td><input type="checkbox" name="flag_enter_bug" id="flag_enter_bug" value="1" [% "checked" IF flag.enter_bug %]></td>
+ <td class="help">can be set on [% terms.bug %] creation</td>
+</tr>
+
+<tr>
+ <th>Active</th>
+ <td><input type="checkbox" name="flag_active" id="flag_active" value="1" [% "checked" IF flag.is_active %]></td>
+</tr>
+
+[% IF mode == 'edit' %]
+ <tr>
+ <th>[% terms.Bug %] Count</th>
+ <td>[% flag.bug_count FILTER html %]</td>
+ </tr>
+[% END %]
+
+</table>
+
+[%# values %]
+
+<table id="flag_values" class="edit" cellspacing="0">
+
+<tr class="header">
+ <th colspan="4">Values</th>
+</tr>
+
+<tr>
+ <th>Value</th>
+ <th>Setter</th>
+ <th>Active</th>
+</tr>
+
+<tr>
+ <td colspan="4">
+ [ <a href="#" onclick="add_value();return false">New Value</a> ]
+ </td>
+</tr>
+
+</table>
+
+[%# visibility %]
+
+<table id="flag_visibility" class="edit" cellspacing="0">
+
+<tr class="header">
+ <th colspan="3">Visibility</th>
+</tr>
+
+<tr>
+ <th>Product</th>
+ <th>Component</th>
+</tr>
+
+<tr id="flag_visibility_add">
+ <td>
+ <select id="product" onChange="selectProduct(Dom.get('product'), Dom.get('component'), null, null, '-- Any --')">
+ <option value=""></option>
+ [% FOREACH p = user.get_selectable_products %]
+ <option value="[% p.name FILTER html %]"
+ [% " selected" IF input.product == p.name %]>
+ [% p.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+ <td>
+ <select id="component">
+ </select>
+ </td>
+ <td>
+ [ <a href="#" onclick="add_visibility();return false">Add</a> ]
+ <td>
+</tr>
+
+</table>
+
+
+[%# submit %]
+
+<div>
+ <input type="submit" name="submit" id="submit" value="[% mode == 'edit' ? 'Save Changes' : 'Add' %]">
+ [% IF mode == "edit" && !flag.bug_count %]
+ <input type="hidden" name="delete" id="delete" value="">
+ <input type="submit" value="Delete Flag [% IF flag.activity_count %] and Activity[% END %]"
+ onclick="return delete_confirm('[% flag.name FILTER js FILTER html %]')">
+ [% END %]
+</div>
+
+</form>
+
+<hr>
+<p>
+Return to the <a href="page.cgi?id=tracking_flags_admin_list.html">list of Tracking Flags</a>.
+</p>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl
new file mode 100644
index 000000000..5ea68dd98
--- /dev/null
+++ b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl
@@ -0,0 +1,73 @@
+[%# 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.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Release Tracking Flags"
+ style_urls = [ 'extensions/TrackingFlags/web/styles/admin.css' ]
+ javascript_urls = [ 'extensions/TrackingFlags/web/js/admin.js' ]
+%]
+
+<table id="flag_list" class="list" cellspacing="0">
+
+<tr>
+ <th>Name</th>
+ <th>Description</th>
+ <th>Type</th>
+ <th>Sort Key</th>
+ <th>Active</th>
+ [% IF show_bug_counts %]
+ <th>[% terms.Bugs %]</th>
+ [% END %]
+ <th>&nbsp;</th>
+</tr>
+
+[% FOREACH flag = flags %]
+ <tr class="flag_row
+ [% loop.count % 2 == 1 ? " odd_row" : " even_row" %]
+ [% " is_disabled" UNLESS flag.is_active %]">
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ <a href="page.cgi?id=tracking_flags_admin_edit.html&amp;mode=edit&amp;flag_id=[% flag.flag_id FILTER uri %]">
+ [% flag.name FILTER html %]
+ </a>
+ </td>
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ [% flag.description FILTER html %]
+ </td>
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ [% flag.flag_type FILTER html %]
+ </td>
+ <td [% 'class="disabled"' UNLESS flag.is_active %]>
+ [% flag.sortkey FILTER html %]
+ </td>
+ <td>
+ [% flag.is_active ? "Yes" : "No" %]
+ </td>
+ [% IF show_bug_counts %]
+ <td>
+ [% flag.bug_count FILTER html %]
+ </td>
+ [% END %]
+ <td>
+ <a href="page.cgi?id=tracking_flags_admin_edit.html&amp;mode=copy&amp;copy_from=[% flag.flag_id FILTER uri %]">Copy</a>
+ </td>
+ </tr>
+[% END %]
+
+</table>
+
+<div id="new_flag">
+ <a href="page.cgi?id=tracking_flags_admin_edit.html">Add Flag</a> |
+ [% IF !show_bug_counts %]
+ <a href="page.cgi?id=tracking_flags_admin_list.html&amp;show_bug_counts=1">
+ Show [% terms.bug %] counts (slower)</a> |
+ [% END %]
+ <input type="checkbox" onclick="filter_flag_list(this.checked)" id="filter">
+ <label for="filter">Show disabled flags</label>
+</div>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/TrackingFlags/web/js/admin.js b/extensions/TrackingFlags/web/js/admin.js
new file mode 100644
index 000000000..e3eb81714
--- /dev/null
+++ b/extensions/TrackingFlags/web/js/admin.js
@@ -0,0 +1,406 @@
+/* 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. */
+
+// init
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+
+Event.onDOMReady(function() {
+ try {
+ if (Dom.get('flag_list')) {
+ filter_flag_list(Dom.get('filter').checked);
+ }
+ else {
+ if (!JSON)
+ JSON = YAHOO.lang.JSON;
+ Event.addListener('flag_name', 'change', change_flag_name, Dom.get('flag_name'));
+ Event.addListener('flag_desc', 'change', change_string_value, Dom.get('flag_desc'));
+ Event.addListener('flag_type', 'change', change_select_value, Dom.get('flag_type'));
+ Event.addListener('flag_sort', 'change', change_int_value, Dom.get('flag_sort'));
+
+ Event.addListener('product', 'change', function() {
+ if (Dom.get('product').value == '')
+ Dom.get('component').options.length = 0;
+ });
+
+ update_flag_values();
+ update_flag_visibility();
+ tag_missing_values();
+ }
+ } catch(e) {
+ console.error(e);
+ }
+});
+
+// field
+
+function change_flag_name(e, o) {
+ change_string_value(e, o);
+ if (o.value == '')
+ return;
+ o.value = o.value.replace(/[^a-z0-9_]/g, '_');
+ if (!o.value.match(/^cf_/))
+ o.value = 'cf_' + o.value;
+ if (Dom.get('flag_desc').value == '') {
+ var desc = o.value;
+ desc = desc.replace(/^cf_/, '');
+ desc = desc.replace(/_/g, '-');
+ Dom.get('flag_desc').value = desc;
+ tag_missing_value(Dom.get('flag_desc'));
+ }
+}
+
+function inc_field(id, amount) {
+ var el = Dom.get(id);
+ el.value = el.value.match(/-?\d+/) * 1 + amount;
+ change_int_value(null, el);
+}
+
+// values
+
+function update_flag_values() {
+ // update the values table from the flag_values global
+
+ var tbl = Dom.get('flag_values');
+ if (!tbl)
+ return;
+
+ // remove current entries
+ while (tbl.rows.length > 3) {
+ tbl.deleteRow(2);
+ }
+
+ // add all entries
+
+ for (var i = 0, l = flag_values.length; i < l; i++) {
+ var value = flag_values[i];
+
+ var row = tbl.insertRow(2 + i);
+ var cell;
+
+ // value
+ cell = row.insertCell(0);
+ if (value.value == '---') {
+ cell.innerHTML = '---';
+ }
+ else {
+ var inputEl = document.createElement('input');
+ inputEl.id = 'value_' + i;
+ inputEl.type = 'text';
+ inputEl.className = 'option_value';
+ inputEl.value = value.value;
+ Event.addListener(inputEl, 'change', change_string_value, inputEl);
+ Event.addListener(inputEl, 'change', function(e, o) {
+ flag_values[o.id.match(/\d+$/)].value = o.value;
+ tag_invalid_values();
+ }, inputEl);
+ Event.addListener(inputEl, 'keyup', function(e, o) {
+ if ((e.key || e.keyCode) == 27 && o.value == '')
+ remove_value(o.id.match(/\d+$/));
+ }, inputEl);
+ cell.appendChild(inputEl);
+ }
+
+ // setter
+ cell = row.insertCell(1);
+ var selectEl = document.createElement('select');
+ selectEl.id = 'setter_' + i;
+ Event.addListener(selectEl, 'change', change_select_value, selectEl);
+ var optionEl = document.createElement('option');
+ optionEl.value = '';
+ selectEl.appendChild(optionEl);
+ for (var j = 0, m = groups.length; j < m; j++) {
+ var group = groups[j];
+ optionEl = document.createElement('option');
+ optionEl.value = group.id;
+ optionEl.innerHTML = YAHOO.lang.escapeHTML(group.name);
+ optionEl.selected = group.id == value.setter_group_id;
+ selectEl.appendChild(optionEl);
+ }
+ Event.addListener(selectEl, 'change', function(e, o) {
+ flag_values[o.id.match(/\d+$/)].setter_group_id = o.value;
+ tag_invalid_values();
+ }, selectEl);
+ cell.appendChild(selectEl);
+
+ // active
+ cell = row.insertCell(2);
+ if (value.value == '---') {
+ cell.innerHTML = 'Yes';
+ }
+ else {
+ var inputEl = document.createElement('input');
+ inputEl.type = 'checkbox';
+ inputEl.id = 'is_active_' + i;
+ inputEl.checked = value.is_active;
+ Event.addListener(inputEl, 'change', function(e, o) {
+ flag_values[o.id.match(/\d+$/)].is_active = o.checked;
+ }, inputEl);
+ cell.appendChild(inputEl);
+ }
+
+ // actions
+ cell = row.insertCell(3);
+ var html =
+ '[' +
+ (i == 0
+ ? '<span class="txt_icon">&nbsp;-&nbsp;</span>'
+ : '<a class="txt_icon" href="#" onclick="value_move_up(' + i + ');return false"> &Delta; </a>'
+ ) +
+ '|' +
+ (i == l - 1
+ ? '<span class="txt_icon">&nbsp;-&nbsp;</span>'
+ : '<a class="txt_icon" href="#" onclick="value_move_down(' + i + ');return false"> &nabla; </a>'
+ );
+ if (value.value != '---')
+ html += '| <a href="#" onclick="remove_value(' + i + ');return false">Remove</a>';
+ html += ']';
+ cell.innerHTML = html;
+ }
+
+ tag_invalid_values();
+}
+
+function tag_invalid_values() {
+ // reset
+ for (var i = 0, l = flag_values.length; i < l; i++) {
+ Dom.removeClass('value_' + i, 'admin_error');
+ }
+
+ for (var i = 0, l = flag_values.length; i < l; i++) {
+ // missing
+ if (flag_values[i].value == '')
+ Dom.addClass('value_' + i, 'admin_error');
+ if (!flag_values[i].setter_group_id)
+ Dom.addClass('setter_' + i, 'admin_error');
+
+ // duplicate values
+ for (var j = i; j < l; j++) {
+ if (i != j && flag_values[i].value == flag_values[j].value) {
+ Dom.addClass('value_' + i, 'admin_error');
+ Dom.addClass('value_' + j, 'admin_error');
+ }
+ }
+ }
+}
+
+function value_move_up(idx) {
+ if (idx == 0)
+ return;
+ var tmp = flag_values[idx];
+ flag_values[idx] = flag_values[idx - 1];
+ flag_values[idx - 1] = tmp;
+ update_flag_values();
+}
+
+function value_move_down(idx) {
+ if (idx == flag_values.length - 1)
+ return;
+ var tmp = flag_values[idx];
+ flag_values[idx] = flag_values[idx + 1];
+ flag_values[idx + 1] = tmp;
+ update_flag_values();
+}
+
+function add_value() {
+ var value = new Object();
+ value.id = 0;
+ value.value = '';
+ value.setter_group_id = '';
+ value.is_active = true;
+ var idx = flag_values.length;
+ flag_values[idx] = value;
+ update_flag_values();
+ Dom.get('value_' + idx).focus();
+}
+
+function remove_value(idx) {
+ flag_values.splice(idx, 1);
+ update_flag_values();
+}
+
+function update_value(e, o) {
+ var i = o.value.match(/\d+/);
+ flag_values[i].value = o.value;
+}
+
+// visibility
+
+function update_flag_visibility() {
+ // update the visibility table from the flag_visibility global
+
+ var tbl = Dom.get('flag_visibility');
+ if (!tbl)
+ return;
+
+ // remove current entries
+ while (tbl.rows.length > 3) {
+ tbl.deleteRow(2);
+ }
+
+ // show something if there aren't any components
+
+ if (!flag_visibility.length) {
+ var row = tbl.insertRow(2);
+ var cell = row.insertCell(0);
+ cell.innerHTML = '<i class="admin_error_text">missing</i>';
+ }
+
+ // add all entries
+
+ for (var i = 0, l = flag_visibility.length; i < l; i++) {
+ var visibility = flag_visibility[i];
+
+ var row = tbl.insertRow(2 + i);
+ var cell;
+
+ // product
+ cell = row.insertCell(0);
+ cell.innerHTML = visibility.product;
+
+ // component
+ cell = row.insertCell(1);
+ cell.innerHTML = visibility.component
+ ? visibility.component
+ : '<i>-- Any --</i>';
+
+ // actions
+ cell = row.insertCell(2);
+ cell.innerHTML = '[ <a href="#" onclick="remove_visibility(' + i + ');return false">Remove</a> ]';
+ }
+}
+
+function add_visibility() {
+ // validation
+ var product = Dom.get('product').value;
+ var component = Dom.get('component').value;
+ if (!product) {
+ alert('Please select a product.');
+ return;
+ }
+
+ // don't allow duplicates
+ for (var i = 0, l = flag_visibility.length; i < l; i++) {
+ if (flag_visibility[i].product == product && flag_visibility[i].component == component) {
+ Dom.get('product').value = '';
+ Dom.get('component').options.length = 0;
+ return;
+ }
+ }
+
+ if (component == '') {
+ // if we're adding an "any" component, remove non-any components
+ for (var i = 0; i < flag_visibility.length; i++) {
+ var visibility = flag_visibility[i];
+ if (visibility.product == product) {
+ flag_visibility.splice(i, 1);
+ i--;
+ }
+ }
+ }
+ else {
+ // don't add non-any components if an "any" component exists
+ for (var i = 0, l = flag_visibility.length; i < l; i++) {
+ var visibility = flag_visibility[i];
+ if (visibility.product == product && !visibility.component)
+ return;
+ }
+ }
+
+ // add to model
+ var visibility = new Object();
+ visibility.id = 0;
+ visibility.product = product;
+ visibility.component = component;
+ flag_visibility[flag_visibility.length] = visibility;
+
+ // update ui
+ update_flag_visibility();
+ Dom.get('product').value = '';
+ Dom.get('component').options.length = 0;
+}
+
+function remove_visibility(idx) {
+ flag_visibility.splice(idx, 1);
+ update_flag_visibility();
+}
+
+// validation and submission
+
+function tag_missing_values() {
+ var els = document.getElementsByTagName('input');
+ for (var i = 0, l = els.length; i < l; i++) {
+ var el = els[i];
+ if (el.id.match(/^(flag|value)_/))
+ tag_missing_value(el);
+ }
+ tag_missing_value(Dom.get('flag_type'));
+}
+
+function tag_missing_value(el) {
+ el.value == ''
+ ? Dom.addClass(el, 'admin_error')
+ : Dom.removeClass(el, 'admin_error');
+}
+
+function delete_confirm(flag) {
+ if (confirm('Are you sure you want to delete the flag ' + flag + ' ?')) {
+ Dom.get('delete').value = 1;
+ return true;
+ }
+ else {
+ return false;
+ }
+}
+
+function on_submit() {
+ if (Dom.get('delete') && Dom.get('delete').value)
+ return;
+ // let perl manage most validation errors, because they are clearly marked
+ // the exception is an empty visibility list, so catch that here as well
+ if (!flag_visibility.length) {
+ alert('You must provide at least one product for visibility.');
+ return false;
+ }
+
+ Dom.get('values').value = JSON.stringify(flag_values);
+ Dom.get('visibility').value = JSON.stringify(flag_visibility);
+ return true;
+}
+
+// flag list
+
+function filter_flag_list(show_disabled) {
+ var rows = Dom.getElementsByClassName('flag_row', 'tr', 'flag_list');
+ for (var i = 0, l = rows.length; i < l; i++) {
+ if (Dom.hasClass(rows[i], 'is_disabled')) {
+ if (show_disabled) {
+ Dom.removeClass(rows[i], 'bz_default_hidden');
+ }
+ else {
+ Dom.addClass(rows[i], 'bz_default_hidden');
+ }
+ }
+ }
+}
+
+// utils
+
+function change_string_value(e, o) {
+ o.value = YAHOO.lang.trim(o.value);
+ tag_missing_value(o);
+}
+
+function change_int_value(e, o) {
+ o.value = o.value.match(/-?\d+/);
+ tag_missing_value(o);
+}
+
+function change_select_value(e, o) {
+ tag_missing_value(o);
+}
diff --git a/extensions/TrackingFlags/web/js/tracking_flags.js b/extensions/TrackingFlags/web/js/tracking_flags.js
new file mode 100644
index 000000000..135b93dba
--- /dev/null
+++ b/extensions/TrackingFlags/web/js/tracking_flags.js
@@ -0,0 +1,56 @@
+/* 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.
+ */
+
+var Dom = YAHOO.util.Dom;
+
+var TrackingFlags = {
+ flags: {},
+ types: []
+};
+
+function hide_tracking_flags() {
+ for (var i = 0, l = TrackingFlags.types.length; i < l; i++) {
+ var flag_type = TrackingFlags.types[i];
+ for (var field in TrackingFlags.flags[flag_type]) {
+ var el = Dom.get(field);
+ var value = el ? el.value : TrackingFlags.flags[flag_type][field];
+ if (el && (value != TrackingFlags.flags[flag_type][field])) {
+ show_tracking_flags(flag_type);
+ return;
+ }
+ if (value == '---') {
+ Dom.addClass('row_' + field, 'bz_default_hidden');
+ } else {
+ Dom.addClass(field, 'bz_default_hidden');
+ Dom.removeClass('ro_' + field, 'bz_default_hidden');
+ }
+ }
+ }
+}
+
+function show_tracking_flags(flag_type) {
+ Dom.addClass('edit_' + flag_type + '_flags_action', 'bz_default_hidden');
+ for (var field in TrackingFlags.flags[flag_type]) {
+ if (Dom.get(field).value == '---') {
+ Dom.removeClass('row_' + field, 'bz_default_hidden');
+ } else {
+ Dom.removeClass(field, 'bz_default_hidden');
+ Dom.addClass('ro_' + field, 'bz_default_hidden');
+ }
+ }
+}
+
+YAHOO.util.Event.onDOMReady(function() {
+ var edit_tracking_links = Dom.getElementsByClassName('edit_tracking_flags_link');
+ for (var i = 0, l = edit_tracking_links.length; i < l; i++) {
+ YAHOO.util.Event.addListener(edit_tracking_links[i], 'click', function(e) {
+ e.preventDefault();
+ show_tracking_flags(this.name);
+ });
+ }
+});
diff --git a/extensions/TrackingFlags/web/styles/admin.css b/extensions/TrackingFlags/web/styles/admin.css
new file mode 100644
index 000000000..374409ce6
--- /dev/null
+++ b/extensions/TrackingFlags/web/styles/admin.css
@@ -0,0 +1,107 @@
+/* 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. */
+
+/* list */
+
+.list {
+ border: 1px solid #888888;
+}
+
+.list td, .list th {
+ padding: 3px 10px 3px 3px;
+ border: 1px solid #888888;
+}
+
+.list .odd_row {
+ background-color: #ffffff;
+ color: #000000;
+}
+
+.list .even_row {
+ background-color: #eeeeee;
+ color: #000000;
+}
+
+.list tr:hover {
+ background-color: #ccddee;
+}
+
+
+.list th {
+ text-align: left;
+ background: #dddddd;
+}
+
+.list .disabled {
+ color: #888888;
+ text-decoration: line-through;
+}
+
+#new_flag {
+ margin: 1em 0em;
+}
+
+/* edit */
+
+.edit {
+ margin-bottom: 2em;
+}
+
+.edit .header {
+ background: #dddddd;
+}
+
+.edit .help {
+ font-style: italic;
+}
+
+.edit td, .edit th {
+ padding: 1px 5px;
+}
+
+.edit th {
+ text-align: left;
+}
+
+#edit_mode {
+ margin: 1em 0em;
+}
+
+#flag_name {
+ width: 20em;
+}
+
+#flag_desc {
+ width: 20em;
+}
+
+#flag_sort {
+ width: 10em;
+}
+
+.option_value {
+ width: 10em;
+}
+
+.hidden {
+ display: none;
+}
+
+.txt_icon {
+ font-family: monospace;
+}
+
+.admin_error {
+ border: 1px solid red;
+ box-shadow: 0px 0px 4px #ff0000;
+ -webkit-box-shadow: 0px 0px 4px #ff0000;
+ -moz-box-shadow: 0px 0px 4px #ff0000;
+}
+
+.admin_error_text {
+ color: #cc0000;
+}
diff --git a/extensions/TrackingFlags/web/styles/edit_bug.css b/extensions/TrackingFlags/web/styles/edit_bug.css
new file mode 100644
index 000000000..132a6a1ca
--- /dev/null
+++ b/extensions/TrackingFlags/web/styles/edit_bug.css
@@ -0,0 +1,18 @@
+/* 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. */
+
+.tracking_flags {
+ width: auto !important;
+}
+
+.tracking_flags .field_label {
+ font-weight: normal !important;
+}
+
+#Create .tracking_flags th {
+ text-align: left;
+}