diff options
Diffstat (limited to 'extensions/TrackingFlags')
32 files changed, 4136 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..a1b5a0ef6 --- /dev/null +++ b/extensions/TrackingFlags/Extension.pm @@ -0,0 +1,789 @@ +# 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; + +use JSON; + +our $VERSION = '1'; + +BEGIN { + *Bugzilla::tracking_flags = \&_tracking_flags; + *Bugzilla::tracking_flag_names = \&_tracking_flag_names; +} + +sub _tracking_flags { + return Bugzilla::Extension::TrackingFlags::Flag->get_all(); +} + +sub _tracking_flag_names { + return Bugzilla::Extension::TrackingFlags::Flag->get_all_names(); +} + +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') + { + my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $vars->{'product'}->name, + enter_bug => 1, + is_active => 1, + }); + + $vars->{tracking_flags} = $flags; + $vars->{tracking_flags_json} = _flags_to_json($flags); + $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}) { + my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $bug->product, + component => $bug->component, + bug_id => $bug->id, + is_active => 1, + }); + + $vars->{tracking_flags} = $flags; + $vars->{tracking_flags_json} = _flags_to_json($flags); + } + + $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 _flags_to_json { + my ($flags) = @_; + + my $json = { + flags => {}, + types => [], + comments => {}, + }; + + my %type_map = map { $_->{name} => $_ } @{ FLAG_TYPES() }; + foreach my $flag (@$flags) { + my $flag_type = $flag->flag_type; + + $json->{flags}->{$flag_type}->{$flag->name} = $flag->bug_flag->value; + + if ($type_map{$flag_type}->{collapsed} + && !grep { $_ eq $flag_type } @{ $json->{types} }) + { + push @{ $json->{types} }, $flag_type; + } + + foreach my $value (@{ $flag->values }) { + if (defined($value->comment) && $value->comment ne '') { + $json->{comments}->{$flag->name}->{$value->value} = $value->comment; + } + } + } + + return encode_json($json); +} + +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', + }, + comment => { + TYPE => 'TEXT', + NOTNULL => 0, + }, + ], + 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', + } + ); + $dbh->bz_add_column( + 'tracking_flags_values', + 'comment', + { + TYPE => 'TEXT', + NOTNULL => 0, + }, + ); +} + +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); + + if ($operator eq 'isempty' or $operator eq 'isnotempty') { + $args->{'full_field'} = "$bugs_alias.value"; + } + else { + $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; + } + } +} + +sub reorg_move_component { + my ($self, $args) = @_; + my $new_product = $args->{new_product}; + my $component = $args->{component}; + + Bugzilla->dbh->do( + "UPDATE tracking_flags_visibility SET product_id=? WHERE component_id=?", + undef, + $new_product->id, $component->id, + ); +} + +sub sanitycheck_check { + my ($self, $args) = @_; + my $status = $args->{status}; + + $status->('tracking_flags_check'); + + my ($count) = Bugzilla->dbh->selectrow_array(" + SELECT COUNT(*) + FROM tracking_flags_visibility + INNER JOIN components ON components.id = tracking_flags_visibility.component_id + WHERE tracking_flags_visibility.product_id <> components.product_id + "); + if ($count) { + $status->('tracking_flags_alert', undef, 'alert'); + $status->('tracking_flags_repair'); + } +} + +sub sanitycheck_repair { + my ($self, $args) = @_; + return unless Bugzilla->cgi->param('tracking_flags_repair'); + + my $status = $args->{'status'}; + my $dbh = Bugzilla->dbh; + $status->('tracking_flags_repairing'); + + my $rows = $dbh->selectall_arrayref(" + SELECT DISTINCT tracking_flags_visibility.product_id AS bad_product_id, + components.product_id AS good_product_id, + tracking_flags_visibility.component_id + FROM tracking_flags_visibility + INNER JOIN components ON components.id = tracking_flags_visibility.component_id + WHERE tracking_flags_visibility.product_id <> components.product_id + ", + { Slice => {} } + ); + foreach my $row (@$rows) { + $dbh->do(" + UPDATE tracking_flags_visibility + SET product_id=? + WHERE product_id=? AND component_id=? + ", undef, + $row->{good_product_id}, + $row->{bad_product_id}, + $row->{component_id}, + ); + } +} + +__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..1bae18ef8 --- /dev/null +++ b/extensions/TrackingFlags/lib/Admin.pm @@ -0,0 +1,446 @@ +# 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, + comment => '', + }, + ]); + $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', { + product => $visibility->{product}, + component_name => $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, + comment => $value->{comment}, + }; + + 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; + $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, + comment => $value->{comment} // '', + }; + } + 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..0b1ae3a1a --- /dev/null +++ b/extensions/TrackingFlags/lib/Constants.pm @@ -0,0 +1,41 @@ +# 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 + }, + ); + 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..964d76810 --- /dev/null +++ b/extensions/TrackingFlags/lib/Flag/Value.pm @@ -0,0 +1,142 @@ +# 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 trim); +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 + comment +); + +use constant LIST_ORDER => 'sortkey'; + +use constant UPDATE_COLUMNS => qw( + setter_group_id + value + sortkey + is_active + comment +); + +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, + comment => \&_check_comment, +}; + +############################### +#### 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; +} + +sub _check_comment { + my ($invocant, $value) = @_; + return undef unless defined $value; + $value = trim($value); + return $value eq '' ? undef : $value; +} + +############################### +#### 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]); } +sub set_comment { $_[0]->set('comment', $_[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 comment { return $_[0]->{'comment'}; } + +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..4e2c97dfa --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl @@ -0,0 +1,62 @@ +[%# 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 %]" + onchange="tracking_flag_change(this)"> + [% 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 %] + +<script type="text/javascript"> + TrackingFlags = [% tracking_flags_json FILTER none %]; +</script> 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/admin/sanitycheck/messages-statuses.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl new file mode 100644 index 000000000..71ef63c11 --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl @@ -0,0 +1,23 @@ +[%# 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 san_tag == "tracking_flags_repair" %] + <a href="sanitycheck.cgi?tracking_flags_repair=1&token= + [%- issue_hash_token(['sanitycheck']) FILTER uri %]" + >Repair invalid product_id values in the tracking_flags_visibility table</a> + +[% ELSIF san_tag == "tracking_flags_check" %] + Checking tracking_flags_visibility table for bad values of product_id. + +[% ELSIF san_tag == "tracking_flags_alert" %] + Bad values for product_id found in the tracking_flags_visibility table. + +[% ELSIF san_tag == "tracking_flags_repairing" %] + OK, now fixing bad product_id values in the tracking_flags_visibility table. + +[% 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..b66bd3df4 --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl @@ -0,0 +1,46 @@ +[%# 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 UNLESS 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> + [% 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 %] + +<script type="text/javascript"> + TrackingFlags = [% tracking_flags_json FILTER none %]; + 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..d656aac92 --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl @@ -0,0 +1,27 @@ +[%# 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_name FILTER html %]' does not exist in the + product '[% product FILTER html %]'. + +[% 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/header-start.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/header-start.html.tmpl new file mode 100644 index 000000000..2bf1c75c3 --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/global/header-start.html.tmpl @@ -0,0 +1,11 @@ +[%# 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 template.name == "bug/create/create.html.tmpl" && tracking_flags.size %] + [% javascript_urls.push('extensions/TrackingFlags/web/js/tracking_flags.js') %] +[% 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..60406490f --- /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" 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> </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&mode=edit&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&mode=copy&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&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..58bdd294f --- /dev/null +++ b/extensions/TrackingFlags/web/js/admin.js @@ -0,0 +1,440 @@ +/* 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 * 2)); + 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"> - </span>' + : '<a class="txt_icon" href="#" onclick="value_move_up(' + i + ');return false"> Δ </a>' + ) + + '|' + + (i == l - 1 + ? '<span class="txt_icon"> - </span>' + : '<a class="txt_icon" href="#" onclick="value_move_down(' + i + ');return false"> ∇ </a>' + ); + if (value.value != '---') { + var lbl = value.comment == '' ? 'Set Comment' : 'Edit Comment'; + html += + '|<a href="#" onclick="remove_value(' + i + ');return false">Remove</a>' + + '|<a href="#" onclick="toggle_value_comment(this, ' + i + ');return false">' + lbl + '</a>' + + } + html += ' ]'; + cell.innerHTML = html; + + row = tbl.insertRow(3 + (i * 2)); + row.className = 'bz_default_hidden'; + row.id = 'comment_row_' + i; + cell = row.insertCell(0); + cell = row.insertCell(1); + cell.colSpan = 3; + var ta = document.createElement('textarea'); + ta.className = 'value_comment'; + ta.id = 'value_comment_' + i; + ta.rows = 5; + ta.value = value.comment; + cell.appendChild(ta); + Event.addListener(ta, 'blur', function(e, idx) { + flag_values[idx].comment = e.target.value; + }, i); + } + + 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; +} + +function toggle_value_comment(btn, idx) { + var row = Dom.get('comment_row_' + idx); + if (Dom.hasClass(row, 'bz_default_hidden')) { + Dom.removeClass(row, 'bz_default_hidden'); + btn.innerHTML = 'Hide Comment'; + Dom.get('value_comment_' + idx).select(); + Dom.get('value_comment_' + idx).focus(); + } else { + Dom.addClass(row, 'bz_default_hidden'); + btn.innerHTML = flag_values[idx].comment == '' ? 'Set Comment' : 'Edit Comment'; + } +} + +// 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..041ae43f5 --- /dev/null +++ b/extensions/TrackingFlags/web/js/tracking_flags.js @@ -0,0 +1,95 @@ +/* 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; + +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'); + } + } +} + +function tracking_flag_change(e) { + var value = e.value; + var prefill; + if (TrackingFlags.comments[e.name]) + prefill = TrackingFlags.comments[e.name][e.value]; + if (!prefill) { + var cr = document.getElementById('cr_' + e.id); + if (cr) + cr.parentElement.removeChild(cr); + return; + } + if (!document.getElementById('cr_' + e.id)) { + // create "comment required" + var span = document.createElement('span'); + span.id = 'cr_' + e.id; + span.appendChild(document.createTextNode('(')); + var a = document.createElement('a'); + a.appendChild(document.createTextNode('comment required')); + a.href = '#'; + a.onclick = function() { + var c = document.getElementById('comment'); + c.focus(); + c.select(); + document.getElementById('add_comment').scrollIntoView(); + return false; + }; + span.appendChild(a); + span.appendChild(document.createTextNode(')')); + e.parentNode.appendChild(span); + } + // prefill comment + var commentEl = document.getElementById('comment'); + if (!commentEl) + return; + var value = commentEl.value; + if (value == prefill) + return; + if (value == '') { + commentEl.value = prefill; + } else { + commentEl.value = prefill + "\n\n" + value; + } +} + +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..51c6ab966 --- /dev/null +++ b/extensions/TrackingFlags/web/styles/admin.css @@ -0,0 +1,111 @@ +/* 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; +} + +.value_comment { + width: 100%; +} + +.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; +} |