diff options
Diffstat (limited to 'extensions/TrackingFlags')
30 files changed, 3317 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..5a4b24611 --- /dev/null +++ b/extensions/TrackingFlags/Extension.pm @@ -0,0 +1,517 @@ +# 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::Constants; +use Bugzilla::Field; +use Bugzilla::Product; +use Bugzilla::Component; +use Bugzilla::Error; +use Bugzilla::Extension::BMO::Data; + +our $VERSION = '1'; + +sub page_before_template { + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + if ($page eq 'tracking_flags_admin_list.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + { group => 'admin', + action => 'access', + object => 'administrative_pages' }); + admin_list($vars); + + } elsif ($page eq 'tracking_flags_admin_edit.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + { group => 'admin', + action => 'access', + object => 'administrative_pages' }); + admin_edit($vars); + } +} + +sub template_before_process { + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + if ($file eq 'bug/create/create.html.tmpl' + || $file eq 'bug/create/create-winqual.html.tmpl') + { + $vars->{'tracking_flags'} = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $vars->{'product'}->name, + is_active => 1, + }); + + $vars->{'tracking_flag_types'} = FLAG_TYPES; + } + elsif ($file eq 'bug/edit.html.tmpl'|| $file eq 'bug/show.xml.tmpl') { + # note: bug/edit.html.tmpl doesn't support multiple bugs + my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; + + if ($bug) { + $vars->{'tracking_flags'} = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $bug->product, + component => $bug->component, + bug_id => $bug->id, + is_active => 1, + }); + } + + $vars->{'tracking_flag_types'} = FLAG_TYPES; + } + elsif ($file eq 'list/edit-multiple.html.tmpl' && $vars->{'one_product'}) { + $vars->{'tracking_flags'} = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $vars->{'one_product'}->name, + is_active => 1 + }); + } +} + +sub db_schema_abstract_schema { + my ($self, $args) = @_; + $args->{'schema'}->{'tracking_flags'} = { + FIELDS => [ + id => { + TYPE => 'MEDIUMSERIAL', + NOTNULL => 1, + PRIMARYKEY => 1, + }, + field_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'fielddefs', + COLUMN => 'id' + } + }, + 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', + }, + 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', + }, + is_active => { + TYPE => 'BOOLEAN', + NOTNULL => 1, + DEFAULT => 'TRUE', + }, + ], + INDEXES => [ + tracking_flags_values_idx => { + FIELDS => ['tracking_flag_id', 'value'], + TYPE => 'UNIQUE', + }, + ], + }; + $args->{'schema'}->{'tracking_flags_bugs'} = { + FIELDS => [ + id => { + TYPE => 'MEDIUMSERIAL', + NOTNULL => 1, + PRIMARYKEY => 1, + }, + tracking_flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'tracking_flags', + COLUMN => 'id', + DELETE => 'CASCADE', + }, + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'bugs', + COLUMN => 'bug_id', + DELETE => 'CASCADE', + }, + }, + value => { + TYPE => 'varchar(64)', + NOTNULL => 1, + }, + ], + INDEXES => [ + tracking_flags_bugs_idx => { + FIELDS => ['tracking_flag_id', 'bug_id'], + TYPE => 'UNIQUE', + }, + ], + }; + $args->{'schema'}->{'tracking_flags_visibility'} = { + FIELDS => [ + id => { + TYPE => 'MEDIUMSERIAL', + NOTNULL => 1, + PRIMARYKEY => 1, + }, + tracking_flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'tracking_flags', + COLUMN => 'id', + DELETE => 'CASCADE', + }, + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => { + TABLE => 'products', + COLUMN => 'id', + DELETE => 'CASCADE', + }, + }, + component_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => { + TABLE => 'components', + COLUMN => 'id', + DELETE => 'CASCADE', + }, + }, + ], + INDEXES => [ + tracking_flags_visibility_idx => { + FIELDS => ['tracking_flag_id', 'product_id', 'component_id'], + TYPE => 'UNIQUE', + }, + ], + }; +} + +sub 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) { + my $params = { product_id => $product->id }; + $params->{'component_id'} = $component->id if $component; + @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) = @_; + 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; + 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} ||= '---'; + }; + my $name = "Bugzilla::Bug::$flag_name"; + if (!Bugzilla::Bug->can($flag_name)) { + no strict 'refs'; + *{$name} = $accessor; + } + } +} + +sub bug_editable_bug_fields { + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + push(@$fields, $flag->name); + } +} + +sub search_operator_field_override { + my ($self, $args) = @_; + my $operators = $args->{'operators'}; + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + $operators->{$flag->name} = { + _non_changed => sub { + _tracking_flags_search_nonchanged($flag->flag_id, @_) + } + }; + } +} + +sub _tracking_flags_search_nonchanged { + my ($flag_id, $search, $args) = @_; + my ($bugs_table, $chart_id, $joins, $value, $operator) = + @$args{qw(bugs_table chart_id joins value operator)}; + my $dbh = Bugzilla->dbh; + + return if ($operator =~ m/^changed/); + + my $bugs_alias = "tracking_flags_bugs_$chart_id"; + my $flags_alias = "tracking_flags_$chart_id"; + + my $bugs_join = { + table => 'tracking_flags_bugs', + as => $bugs_alias, + from => $bugs_table . ".bug_id", + to => "bug_id", + extra => [$bugs_alias . ".tracking_flag_id = $flag_id"] + }; + + push(@$joins, $bugs_join); + + $args->{'full_field'} = "$bugs_alias.value"; +} + +sub bug_end_of_create { + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $timestamp = $args->{'timestamp'}; + my $params = Bugzilla->input_params; + my $user = Bugzilla->user; + + 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 bug_end_of_update { + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $timestamp = $args->{'timestamp'}; + my $changes = $args->{'changes'}; + my $params = Bugzilla->input_params; + 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 $new_value = $params->{$flag->name} || '---'; + my $old_value = $flag->bug_flag->value; + + next if $new_value eq $old_value; + + 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 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); + } + } +} + +__PACKAGE__->NAME; diff --git a/extensions/TrackingFlags/lib/Admin.pm b/extensions/TrackingFlags/lib/Admin.pm new file mode 100644 index 000000000..9e8b33b43 --- /dev/null +++ b/extensions/TrackingFlags/lib/Admin.pm @@ -0,0 +1,421 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::TrackingFlags::Admin; + +use strict; +use warnings; + +use Bugzilla; +use Bugzilla::Component; +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::Product; +use Bugzilla::Util qw(trim detaint_natural); + +use Bugzilla::Extension::TrackingFlags::Constants; +use Bugzilla::Extension::TrackingFlags::Flag; +use Bugzilla::Extension::TrackingFlags::Flag::Value; +use Bugzilla::Extension::TrackingFlags::Flag::Visibility; + +use JSON; +use Scalar::Util qw(blessed); + +use base qw(Exporter); +our @EXPORT = qw( + admin_list + admin_edit +); + +# +# page loading +# + +sub admin_list { + my ($vars) = @_; + + $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; +} + +sub admin_edit { + my ($vars, $page) = @_; + my $input = Bugzilla->input_params; + + $vars->{groups} = _groups_to_json(); + $vars->{mode} = $input->{mode} || 'new'; + $vars->{flag_id} = $input->{flag_id} || 0; + $vars->{tracking_flag_types} = FLAG_TYPES; + + if ($input->{delete}) { + my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); + $flag->remove_from_db(); + + $vars->{mode} = 'deleted'; + $vars->{flag_name} = $flag->name; + + } elsif ($input->{save}) { + # save + + my ($flag, $values, $visibilities) = _load_from_input($input, $vars); + _validate($flag, $values, $visibilities); + my $flag_obj = _update_db($flag, $values, $visibilities); + + $vars->{flag} = $flag_obj; + $vars->{values} = _flag_values_to_json($values); + $vars->{visibility} = _flag_visibility_to_json($visibilities); + $vars->{can_delete} = !$flag_obj->has_values; + + if ($vars->{mode} eq 'new') { + $vars->{message} = 'tracking_flags_created'; + } else { + $vars->{message} = 'tracking_flags_updated'; + } + + } else { + # initial load + + if ($vars->{mode} eq 'edit') { + # edit - straight load + my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); + $vars->{flag} = $flag; + $vars->{values} = _flag_values_to_json($flag->values); + $vars->{visibility} = _flag_visibility_to_json($flag->visibility); + $vars->{can_delete} = !$flag->has_values; + + } elsif ($vars->{mode} eq 'copy') { + # copy - load the source flag + $vars->{mode} = 'new'; + my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($input->{copy_from}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{copy_from} }); + + # increment the number at the end of the name and description + if ($flag->name =~ /^(\D+)(\d+)$/) { + $flag->set_name("$1" . ($2 + 1)); + } + if ($flag->description =~ /^(\D+)(\d+)$/) { + $flag->set_description("$1" . ($2 + 1)); + } + $flag->set_sortkey(_next_unique_sortkey($flag->sortkey)); + $flag->set_type($flag->flag_type); + # always default new flags as active, even when copying an inactive one + $flag->set_is_active(1); + + $vars->{flag} = $flag; + $vars->{values} = _flag_values_to_json($flag->values, 1); + $vars->{visibility} = _flag_visibility_to_json($flag->visibility, 1); + $vars->{can_delete} = 0; + + } else { + $vars->{mode} = 'new'; + $vars->{flag} = { + sortkey => 0, + is_active => 1, + }; + $vars->{values} = _flag_values_to_json([ + { + id => 0, + value => '---', + setter_group_id => '', + is_active => 1, + }, + ]); + $vars->{visibility} = ''; + $vars->{can_delete} = 0; + } + } +} + +sub _load_from_input { + my ($input, $vars) = @_; + + # flag + + my $flag = { + id => $input->{flag_id} || 0, + name => trim($input->{flag_name} || ''), + description => trim($input->{flag_desc} || ''), + sortkey => $input->{flag_sort} || 0, + type => trim($input->{flag_type} || ''), + is_active => $input->{flag_active} ? 1 : 0, + }; + detaint_natural($flag->{id}); + detaint_natural($flag->{sortkey}); + detaint_natural($flag->{is_active}); + + # values + + my $values = decode_json($input->{values} || '[]'); + foreach my $value (@$values) { + $value->{value} = '' unless exists $value->{value} && defined $value->{value}; + $value->{setter_group_id} = '' unless $value->{setter_group_id}; + $value->{is_active} = $value->{is_active} ? 1 : 0; + } + + # vibility + + my $visibilities = decode_json($input->{visibility} || '[]'); + foreach my $visibility (@$visibilities) { + $visibility->{product} = '' unless exists $visibility->{product} && defined $visibility->{product}; + $visibility->{component} = '' unless exists $visibility->{component} && defined $visibility->{component}; + } + + return ($flag, $values, $visibilities); +} + +sub _next_unique_sortkey { + my ($sortkey) = @_; + + my %current; + foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all()) { + $current{$flag->sortkey} = 1; + } + + $sortkey += 5; + $sortkey += 5 while exists $current{$sortkey}; + return $sortkey; +} + +# +# validation +# + +sub _validate { + my ($flag, $values, $visibilities) = @_; + + # flag + + my @missing; + push @missing, 'Field Name' if $flag->{name} eq ''; + push @missing, 'Field Description' if $flag->{description} eq ''; + push @missing, 'Field Sort Key' if $flag->{sortkey} eq ''; + scalar(@missing) + && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); + + $flag->{name} =~ /^cf_/ + || ThrowUserError('tracking_flags_cf_prefix'); + + if ($flag->{id}) { + my $old_flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); + if ($flag->{name} ne $old_flag->name) { + Bugzilla::Field->new({ name => $flag->{name} }) + && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); + } + } else { + Bugzilla::Field->new({ name => $flag->{name} }) + && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); + } + + # values + + scalar(@$values) + || ThrowUserError('tracking_flags_missing_values'); + + my %seen; + foreach my $value (@$values) { + my $v = $value->{value}; + + $v eq '' + && ThrowUserError('tracking_flags_missing_value'); + + exists $seen{$v} + && ThrowUserError('tracking_flags_duplicate_value', { value => $v }); + $seen{$v} = 1; + + push @missing, "Setter for $v" if !$value->{setter_group_id}; + } + scalar(@missing) + && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); + + # visibility + + scalar(@$visibilities) + || ThrowUserError('tracking_flags_missing_visibility'); + + %seen = (); + foreach my $visibility (@$visibilities) { + my $name = $visibility->{product} . ':' . $visibility->{component}; + + exists $seen{$name} + && ThrowUserError('tracking_flags_duplicate_visibility', { name => $name }); + + $visibility->{product_obj} = Bugzilla::Product->new({ name => $visibility->{product} }) + || ThrowCodeError('tracking_flags_invalid_product', { product => $visibility->{product} }); + + if ($visibility->{component} ne '') { + $visibility->{component_obj} = Bugzilla::Component->new({ product => $visibility->{product_obj}, + name => $visibility->{component} }) + || ThrowCodeError('tracking_flags_invalid_component', { component => $visibility->{component} }); + } + } + +} + +# +# database updating +# + +sub _update_db { + my ($flag, $values, $visibilities) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $flag_obj = _update_db_flag($flag); + _update_db_values($flag_obj, $flag, $values); + _update_db_visibility($flag_obj, $flag, $visibilities); + $dbh->bz_commit_transaction(); + + return $flag_obj; +} + +sub _update_db_flag { + my ($flag) = @_; + + my $object_set = { + name => $flag->{name}, + description => $flag->{description}, + sortkey => $flag->{sortkey}, + type => $flag->{type}, + is_active => $flag->{is_active}, + }; + + my $flag_obj; + if ($flag->{id}) { + # update existing flag + $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); + $flag_obj->set_all($object_set); + $flag_obj->update(); + + } else { + # create new flag + $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->create($object_set); + } + + return $flag_obj; +} + +sub _update_db_values { + my ($flag_obj, $flag, $values) = @_; + + # delete + foreach my $current_value (@{ $flag_obj->values }) { + if (!grep { $_->{id} == $current_value->id } @$values) { + $current_value->remove_from_db(); + } + } + + # add/update + my $sortkey = 0; + foreach my $value (@{ $values }) { + $sortkey += 10; + + my $object_set = { + value => $value->{value}, + setter_group_id => $value->{setter_group_id}, + is_active => $value->{is_active}, + sortkey => $sortkey, + }; + + if ($value->{id}) { + my $value_obj = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag value', id => $flag->{id} }); + $value_obj->set_all($object_set); + $value_obj->update(); + } else { + $object_set->{tracking_flag_id} = $flag_obj->flag_id; + Bugzilla::Extension::TrackingFlags::Flag::Value->create($object_set); + } + } +} + +sub _update_db_visibility { + my ($flag_obj, $flag, $visibilities) = @_; + + # delete + foreach my $current_visibility (@{ $flag_obj->visibility }) { + if (!grep { $_->{id} == $current_visibility->id } @$visibilities) { + $current_visibility->remove_from_db(); + } + } + + # add + foreach my $visibility (@{ $visibilities }) { + next if $visibility->{id}; + Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ + tracking_flag_id => $flag_obj->flag_id, + product_id => $visibility->{product_obj}->id, + component_id => $visibility->{component} ? $visibility->{component_obj}->id : undef, + }); + } +} + +# +# serialisation +# + +sub _groups_to_json { + my @data; + foreach my $group (sort { $a->name cmp $b->name } Bugzilla::Group->get_all()) { + push @data, { + id => $group->id, + name => $group->name, + }; + } + return encode_json(\@data); +} + +sub _flag_values_to_json { + my ($values, $is_copy) = @_; + # setting is_copy will set the id's to zero, to force new values rather + # than editing existing ones + my @data; + foreach my $value (@$values) { + push @data, { + id => $is_copy ? 0 : $value->{id}, + value => $value->{value}, + setter_group_id => $value->{setter_group_id}, + is_active => $value->{is_active} ? JSON::true : JSON::false, + }; + } + return encode_json(\@data); +} + +sub _flag_visibility_to_json { + my ($visibilities, $is_copy) = @_; + # setting is_copy will set the id's to zero, to force new visibilites + # rather than editing existing ones + my @data; + + foreach my $visibility (@$visibilities) { + my $product = exists $visibility->{product_id} + ? $visibility->product->name + : $visibility->{product}; + my $component; + if (exists $visibility->{component_id} && $visibility->{component_id}) { + $component = $visibility->component->name; + } elsif (exists $visibility->{component}) { + $component = $visibility->{component}; + } else { + $component = undef; + } + push @data, { + id => $is_copy ? 0 : $visibility->{id}, + product => $product, + component => $component, + }; + } + @data = sort { + lc($a->{product}) cmp lc($b->{product}) + || lc($a->{component}) cmp lc($b->{component}) + } @data; + return encode_json(\@data); +} + +1; diff --git a/extensions/TrackingFlags/lib/Constants.pm b/extensions/TrackingFlags/lib/Constants.pm new file mode 100644 index 000000000..edeb1a62b --- /dev/null +++ b/extensions/TrackingFlags/lib/Constants.pm @@ -0,0 +1,45 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::TrackingFlags::Constants; + +use strict; +use base qw(Exporter); + +our @EXPORT = qw( + FLAG_TYPES +); + +use constant FLAG_TYPES => [ + { + name => 'tracking', + description => 'Tracking Flags', + collapsed => 1, + }, + { + name => 'project', + description => 'Project Flags', + collapsed => 0, + }, + { + name => 'status', + description => 'Status Flags', + collapsed => 1, + }, + { + name => 'blocking', + description => 'Blocking Flags', + collapsed => 1, + }, + { + name => 'b2g', + description => 'B2G Flags', + collapsed => 1, + } +]; + +1; diff --git a/extensions/TrackingFlags/lib/Flag.pm b/extensions/TrackingFlags/lib/Flag.pm new file mode 100644 index 000000000..5daa47508 --- /dev/null +++ b/extensions/TrackingFlags/lib/Flag.pm @@ -0,0 +1,400 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::TrackingFlags::Flag; + +use base qw(Bugzilla::Object); + +use strict; +use warnings; + +use Bugzilla::Error; +use Bugzilla::Constants; +use Bugzilla::Util qw(detaint_natural trim); + +use Bugzilla::Extension::TrackingFlags::Constants; +use Bugzilla::Extension::TrackingFlags::Flag::Bug; +use Bugzilla::Extension::TrackingFlags::Flag::Value; +use Bugzilla::Extension::TrackingFlags::Flag::Visibility; + +############################### +#### Initialization #### +############################### + +use constant DB_TABLE => 'tracking_flags'; + +use constant DB_COLUMNS => qw( + id + field_id + name + description + type + sortkey + is_active +); + +use constant LIST_ORDER => 'sortkey'; + +use constant UPDATE_COLUMNS => qw( + name + description + type + sortkey + is_active +); + +use constant VALIDATORS => { + name => \&_check_name, + description => \&_check_description, + type => \&_check_type, + sortkey => \&_check_sortkey, + is_active => \&Bugzilla::Object::check_boolean, + +}; + +use constant UPDATE_VALIDATORS => { + name => \&_check_name, + description => \&_check_description, + type => \&_check_type, + sortkey => \&_check_sortkey, + is_active => \&Bugzilla::Object::check_boolean, +}; + +############################### +#### Methods #### +############################### + +sub new { + my $class = shift; + my $param = shift; + my $cache = Bugzilla->request_cache; + + if (!ref $param + && exists $cache->{'tracking_flags'} + && exists $cache->{'tracking_flags'}->{$param}) + { + return $cache->{'tracking_flags'}->{$param}; + } + + return $class->SUPER::new($param); +} + +sub create { + my $class = shift; + my $params = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + $params = $class->run_create_validators($params); + + # We have to create an entry for this new flag + # in the fielddefs table for use elsewhere. We cannot + # use Bugzilla::Field->create as it will create the + # additional tables needed by custom fields which we + # do not need. Also we do this so as not to add a + # another column to the bugs table. + # We will create the entry as a custom field with a + # type of FIELD_TYPE_EXTENSION so Bugzilla will skip + # these field types in certain parts of the core code. + $dbh->do("INSERT INTO fielddefs + (name, description, sortkey, type, custom, obsolete, buglist) + VALUES + (?, ?, ?, ?, ?, ?, ?)", + undef, + $params->{'name'}, + $params->{'description'}, + $params->{'sortkey'}, + FIELD_TYPE_EXTENSION, + 1, 0, 1); + $params->{'field_id'} = $dbh->bz_last_key; + + my $flag = $class->SUPER::create($params); + + $dbh->bz_commit_transaction(); + + return $flag; +} + +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + + my $old_self = $self->new($self->flag_id); + + # HACK! Bugzilla::Object::update uses hardcoded $self->id + # instead of $self->{ID_FIELD} so we need to reverse field_id + # and the real id temporarily + my $field_id = $self->id; + $self->{'field_id'} = $self->{'id'}; + + my $changes = $self->SUPER::update(@_); + + $self->{'field_id'} = $field_id; + + # Update the fielddefs entry + $dbh->do("UPDATE fielddefs SET name = ?, description = ? WHERE name = ?", + undef, + $self->name, $self->description, $old_self->name); + + # Update request_cache + my $cache = Bugzilla->request_cache; + if (exists $cache->{'tracking_flags'}) { + $cache->{'tracking_flags'}->{$self->flag_id} = $self; + } + + return $changes; +} + +sub match { + my $class = shift; + my ($params) = @_; + + # Use later for preload + my $bug_id = delete $params->{'bug_id'}; + + # Retrieve all flags relevant for the given product and component + if (!exists $params->{'id'} + && ($params->{'component'} || $params->{'component_id'} + || $params->{'product'} || $params->{'product_id'})) + { + my $visible_flags + = Bugzilla::Extension::TrackingFlags::Flag::Visibility->match(@_); + my @flag_ids = map { $_->tracking_flag_id } @$visible_flags; + + delete $params->{'component'} if exists $params->{'component'}; + delete $params->{'component_id'} if exists $params->{'component_id'}; + delete $params->{'product'} if exists $params->{'product'}; + delete $params->{'product_id'} if exists $params->{'product_id'}; + + $params->{'id'} = \@flag_ids; + } + + my $flags = $class->SUPER::match(@_); + + preload_all_the_things($flags, { bug_id => $bug_id }); + + return [ sort { $a->sortkey <=> $b->sortkey } @$flags ]; +} + +sub get_all { + my $self = shift; + my $cache = Bugzilla->request_cache; + if (!exists $cache->{'tracking_flags'}) { + my @tracking_flags = $self->SUPER::get_all(@_); + preload_all_the_things(\@tracking_flags); + my %tracking_flags_hash = map { $_->flag_id => $_ } @tracking_flags; + $cache->{'tracking_flags'} = \%tracking_flags_hash; + } + return sort { $a->flag_type cmp $b->flag_type || $a->sortkey <=> $b->sortkey } + values %{ $cache->{'tracking_flags'} }; +} + +sub remove_from_db { + my $self = shift; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + $dbh->do('DELETE FROM fielddefs WHERE name = ?', undef, $self->name); + $self->SUPER::remove_from_db(@_); + $dbh->bz_commit_transaction(); + + # Remove from request cache + my $cache = Bugzilla->request_cache; + if (exists $cache->{'tracking_flags'}) { + delete $cache->{'tracking_flags'}->{$self->flag_id}; + } +} + +sub preload_all_the_things { + my ($flags, $params) = @_; + + my %flag_hash = map { $_->flag_id => $_ } @$flags; + my @flag_ids = keys %flag_hash; + return unless @flag_ids; + + # Preload values + my $value_objects + = Bugzilla::Extension::TrackingFlags::Flag::Value->match({ tracking_flag_id => \@flag_ids }); + + # Now populate the tracking flags with this set of value objects. + foreach my $obj (@$value_objects) { + my $flag_id = $obj->tracking_flag_id; + + # Prepopulate the tracking flag object in the value object + $obj->{'tracking_flag'} = $flag_hash{$flag_id}; + + # Prepopulate the current value objects for this tracking flag + $flag_hash{$flag_id}->{'values'} ||= []; + push(@{$flag_hash{$flag_id}->{'values'}}, $obj); + } + + # Preload bug values if a bug_id is passed + if ($params && exists $params->{'bug_id'} && $params->{'bug_id'}) { + # We don't want to use @flag_ids here as we want all flags attached to this bug + # even if they are inactive. + my $bug_objects + = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $params->{'bug_id'} }); + # Now populate the tracking flags with this set of objects. + # Also we add them to the flag hash since we want them to be visible even if + # they are not longer applicable to this product/component. + foreach my $obj (@$bug_objects) { + my $flag_id = $obj->tracking_flag_id; + + # Prepopulate the tracking flag object in the bug flag object + $obj->{'tracking_flag'} = $flag_hash{$flag_id}; + + # Prepopulate the the current bug flag object for the tracking flag + $flag_hash{$flag_id}->{'bug_flag'} = $obj; + } + } + + @$flags = values %flag_hash; +} + +############################### +#### Validators #### +############################### + +sub _check_name { + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowCodeError('param_required', { param => 'name' }); + return $name; +} + +sub _check_description { + my ($invocant, $description) = @_; + $description = trim($description); + $description || ThrowCodeError( 'param_required', { param => 'description' } ); + return $description; +} + +sub _check_type { + my ($invocant, $type) = @_; + $type = trim($type); + $type || ThrowCodeError( 'param_required', { param => 'type' } ); + grep($_->{name} eq $type, @{FLAG_TYPES()}) + || ThrowUserError('tracking_flags_invalid_flag_type', { type => $type }); + return $type; +} + +sub _check_sortkey { + my ($invocant, $sortkey) = @_; + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey }); + return $sortkey; +} + +############################### +#### Setters #### +############################### + +sub set_name { $_[0]->set('name', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_type { $_[0]->set('type', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } + +############################### +#### Accessors #### +############################### + +sub flag_id { return $_[0]->{'id'}; } +sub name { return $_[0]->{'name'}; } +sub description { return $_[0]->{'description'}; } +sub flag_type { return $_[0]->{'type'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub is_active { return $_[0]->{'is_active'}; } + +sub values { + return $_[0]->{'values'} ||= Bugzilla::Extension::TrackingFlags::Flag::Value->match({ + tracking_flag_id => $_[0]->flag_id + }); +} + +sub visibility { + return $_[0]->{'visibility'} ||= Bugzilla::Extension::TrackingFlags::Flag::Visibility->match({ + tracking_flag_id => $_[0]->flag_id + }); +} + +sub can_set_value { + my ($self, $new_value, $user) = @_; + $user ||= Bugzilla->user; + my $new_value_obj; + foreach my $value (@{$self->values}) { + if ($value->value eq $new_value) { + $new_value_obj = $value; + last; + } + } + return $new_value_obj && $user->in_group($new_value_obj->setter_group->name) + ? 1 + : 0; +} + +sub bug_flag { + my ($self, $bug_id) = @_; + # Return the current bug value object if defined unless the passed bug_id does + # not equal the current bug value objects id. + if (defined $self->{'bug_flag'} + && (!$bug_id || $self->{'bug_flag'}->bug->id == $bug_id)) + { + return $self->{'bug_flag'}; + } + + # Flag::Bug->new will return a default bug value object if $params undefined + my $params = !$bug_id + ? undef + : { condition => "tracking_flag_id = ? AND bug_id = ?", + values => [ $self->flag_id, $bug_id ] }; + return $self->{'bug_flag'} = Bugzilla::Extension::TrackingFlags::Flag::Bug->new($params); +} + +sub has_values { + my ($self) = @_; + return $self->{'has_values'} if defined $self->{'has_values'}; + my $dbh = Bugzilla->dbh; + $self->{'has_values'} = scalar $dbh->selectrow_array(" + SELECT 1 + FROM tracking_flags_bugs + WHERE tracking_flag_id = ? " . + $dbh->sql_limit(1), + undef, $self->flag_id); + return $self->{'has_values'}; +} + +###################################### +# Compatibility with Bugzilla::Field # +###################################### + +# Here we return 'field_id' instead of the real +# id as we want other Bugzilla code to treat this +# as a Bugzilla::Field object in certain places. +sub id { return $_[0]->{'field_id'}; } +sub type { return FIELD_TYPE_EXTENSION; } +sub legal_values { return $_[0]->values; } +sub custom { return 1; } +sub in_new_bugmail { return 1; } +sub obsolete { return 0; } +sub enter_bug { return 1; } +sub buglist { return 1; } +sub is_select { return 1; } +sub is_abnormal { return 1; } +sub is_timetracking { return 0; } +sub visibility_field { return undef; } +sub visibility_values { return undef; } +sub controls_visibility_of { return undef; } +sub value_field { return undef; } +sub controls_values_of { return undef; } +sub is_visible_on_bug { return 1; } +sub is_relationship { return 0; } +sub reverse_desc { return ''; } +sub is_mandatory { return 0; } +sub is_numeric { return 0; } + +1; diff --git a/extensions/TrackingFlags/lib/Flag/Bug.pm b/extensions/TrackingFlags/lib/Flag/Bug.pm new file mode 100644 index 000000000..5e2886e66 --- /dev/null +++ b/extensions/TrackingFlags/lib/Flag/Bug.pm @@ -0,0 +1,171 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::TrackingFlags::Flag::Bug; + +use base qw(Bugzilla::Object); + +use strict; +use warnings; + +use Bugzilla::Extension::TrackingFlags::Flag; + +use Bugzilla::Bug; +use Bugzilla::Error; + +use Scalar::Util qw(blessed); + +############################### +#### Initialization #### +############################### + +use constant DEFAULT_FLAG_BUG => { + 'id' => 0, + 'tracking_flag_id' => 0, + 'bug_id' => '', + 'value' => '---', +}; + +use constant DB_TABLE => 'tracking_flags_bugs'; + +use constant DB_COLUMNS => qw( + id + tracking_flag_id + bug_id + value +); + +use constant LIST_ORDER => 'id'; + +use constant UPDATE_COLUMNS => qw( + value +); + +use constant VALIDATORS => { + tracking_flag_id => \&_check_tracking_flag, + value => \&_check_value, +}; + +use constant AUDIT_CREATES => 0; +use constant AUDIT_UPDATES => 0; +use constant AUDIT_REMOVES => 0; + +############################### +#### Object Methods #### +############################### + +sub new { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($param) = @_; + + my $self; + if ($param) { + $self = $class->SUPER::new(@_); + if (!$self) { + $self = DEFAULT_FLAG_BUG; + bless($self, $class); + } + } + else { + $self = DEFAULT_FLAG_BUG; + bless($self, $class); + } + + return $self +} + +sub match { + my $class = shift; + my $bug_flags = $class->SUPER::match(@_); + preload_all_the_things($bug_flags); + return $bug_flags; +} + +sub remove_from_db { + my ($self) = @_; + $self->SUPER::remove_from_db(); + $self->{'id'} = $self->{'tracking_flag_id'} = $self->{'bug_id'} = 0; + $self->{'value'} = '---'; +} + +sub preload_all_the_things { + my ($bug_flags) = @_; + my $cache = Bugzilla->request_cache; + + # Preload tracking flag objects + my @tracking_flag_ids; + foreach my $bug_flag (@$bug_flags) { + if (exists $cache->{'tracking_flags'} + && $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}) + { + $bug_flag->{'tracking_flag'} + = $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}; + next; + } + push(@tracking_flag_ids, $bug_flag->tracking_flag_id); + } + + return unless @tracking_flag_ids; + + my $tracking_flags + = Bugzilla::Extension::TrackingFlags::Flag->match({ id => \@tracking_flag_ids }); + my %tracking_flag_hash = map { $_->flag_id => $_ } @$tracking_flags; + + foreach my $bug_flag (@$bug_flags) { + next if exists $bug_flag->{'tracking_flag'}; + $bug_flag->{'tracking_flag'} = $tracking_flag_hash{$bug_flag->tracking_flag_id}; + } +} + +############################### +#### Validators #### +############################### + +sub _check_value { + my ($invocant, $value) = @_; + $value || ThrowCodeError('param_required', { param => 'value' }); + return $value; +} + +sub _check_tracking_flag { + my ($invocant, $flag) = @_; + if (blessed $flag) { + return $flag->flag_id; + } + $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 }) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + return $flag->flag_id; +} + +############################### +#### Setters #### +############################### + +sub set_value { $_[0]->set('value', $_[1]); } + +############################### +#### Accessors #### +############################### + +sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } +sub bug_id { return $_[0]->{'bug_id'}; } +sub value { return $_[0]->{'value'}; } + +sub bug { + return $_[0]->{'bug'} ||= Bugzilla::Bug->new({ + id => $_[0]->bug_id, cache => 1 + }); +} + +sub tracking_flag { + return $_[0]->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new({ + id => $_[0]->tracking_flag_id, cache => 1 + }); +} + +1; diff --git a/extensions/TrackingFlags/lib/Flag/Value.pm b/extensions/TrackingFlags/lib/Flag/Value.pm new file mode 100644 index 000000000..4023e191d --- /dev/null +++ b/extensions/TrackingFlags/lib/Flag/Value.pm @@ -0,0 +1,130 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::TrackingFlags::Flag::Value; + +use base qw(Bugzilla::Object); + +use strict; +use warnings; + +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::Util qw(detaint_natural); +use Scalar::Util qw(blessed); + +############################### +#### Initialization #### +############################### + +use constant DB_TABLE => 'tracking_flags_values'; + +use constant DB_COLUMNS => qw( + id + tracking_flag_id + setter_group_id + value + sortkey + is_active +); + +use constant LIST_ORDER => 'sortkey'; + +use constant UPDATE_COLUMNS => qw( + setter_group_id + value + sortkey + is_active +); + +use constant VALIDATORS => { + tracking_flag_id => \&_check_tracking_flag, + setter_group_id => \&_check_setter_group, + value => \&_check_value, + sortkey => \&_check_sortkey, + is_active => \&Bugzilla::Object::check_boolean, +}; + +############################### +#### Validators #### +############################### + +sub _check_value { + my ($invocant, $value) = @_; + defined $value || ThrowCodeError('param_required', { param => 'value' }); + return $value; +} + +sub _check_tracking_flag { + my ($invocant, $flag) = @_; + if (blessed $flag) { + return $flag->flag_id; + } + $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 }) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + return $flag->flag_id; +} + +sub _check_setter_group { + my ($invocant, $group) = @_; + if (blessed $group) { + return $group->id; + } + $group = Bugzilla::Group->new({ id => $group, cache => 1 }) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'setter_group_id', value => $group }); + return $group->id; +} + +sub _check_sortkey { + my ($invocant, $sortkey) = @_; + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey }); + return $sortkey; +} + +############################### +#### Setters #### +############################### + +sub set_setter_group_id { $_[0]->set('setter_group_id', $_[1]); } +sub set_value { $_[0]->set('value', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } + +############################### +#### Accessors #### +############################### + +sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } +sub setter_group_id { return $_[0]->{'setter_group_id'}; } +sub value { return $_[0]->{'value'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub is_active { return $_[0]->{'is_active'}; } + +sub tracking_flag { + return $_[0]->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new({ + id => $_[0]->tracking_flag_id, cache => 1 + }); +} + +sub setter_group { + if ($_[0]->setter_group_id) { + $_[0]->{'setter_group'} ||= Bugzilla::Group->new({ + id => $_[0]->setter_group_id, cache => 1 + }); + } + return $_[0]->{'setter_group'}; +} + +######################################## +## Compatibility with Bugzilla::Field ## +######################################## + +sub name { return $_[0]->{'value'}; } +sub is_visible_on_bug { return 1; } + +1; diff --git a/extensions/TrackingFlags/lib/Flag/Visibility.pm b/extensions/TrackingFlags/lib/Flag/Visibility.pm new file mode 100644 index 000000000..7600d71bd --- /dev/null +++ b/extensions/TrackingFlags/lib/Flag/Visibility.pm @@ -0,0 +1,172 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::TrackingFlags::Flag::Visibility; + +use base qw(Bugzilla::Object); + +use strict; +use warnings; + +use Bugzilla::Error; +use Bugzilla::Product; +use Bugzilla::Component; +use Scalar::Util qw(blessed); + +############################### +#### Initialization #### +############################### + +use constant DB_TABLE => 'tracking_flags_visibility'; + +use constant DB_COLUMNS => qw( + id + tracking_flag_id + product_id + component_id +); + +use constant LIST_ORDER => 'id'; + +use constant UPDATE_COLUMNS => (); # imutable + +use constant VALIDATORS => { + tracking_flag_id => \&_check_tracking_flag, + product_id => \&_check_product, + component_id => \&_check_component, +}; + +############################### +#### Methods #### +############################### + +sub match { + my $class= shift; + my ($params) = @_; + my $dbh = Bugzilla->dbh; + + # Allow matching component and product by name + # (in addition to matching by ID). + # Borrowed from Bugzilla::Bug::match + my %translate_fields = ( + product => 'Bugzilla::Product', + component => 'Bugzilla::Component', + ); + + foreach my $field (keys %translate_fields) { + my @ids; + # Convert names to ids. We use "exists" everywhere since people can + # legally specify "undef" to mean IS NULL + if (exists $params->{$field}) { + my $names = $params->{$field}; + my $type = $translate_fields{$field}; + my $objects = Bugzilla::Object::match($type, { name => $names }); + push(@ids, map { $_->id } @$objects); + } + # You can also specify ids directly as arguments to this function, + # so include them in the list if they have been specified. + if (exists $params->{"${field}_id"}) { + my $current_ids = $params->{"${field}_id"}; + my @id_array = ref $current_ids ? @$current_ids : ($current_ids); + push(@ids, @id_array); + } + # We do this "or" instead of a "scalar(@ids)" to handle the case + # when people passed only invalid object names. Otherwise we'd + # end up with a SUPER::match call with zero criteria (which dies). + if (exists $params->{$field} or exists $params->{"${field}_id"}) { + delete $params->{$field}; + $params->{"${field}_id"} = scalar(@ids) == 1 ? [ $ids[0] ] : \@ids; + } + } + + # If we aren't matching on the product, use the default matching code + if (!exists $params->{product_id}) { + return $class->SUPER::match(@_); + } + + my @criteria = ("1=1"); + + if ($params->{product_id}) { + push(@criteria, $dbh->sql_in('product_id', $params->{'product_id'})); + if ($params->{component_id}) { + my $component_id = $params->{component_id}; + push(@criteria, "(" . $dbh->sql_in('component_id', $params->{'component_id'}) . + " OR component_id IS NULL)"); + } + } + + my $where = join(' AND ', @criteria); + my $flag_ids = $dbh->selectcol_arrayref("SELECT id + FROM tracking_flags_visibility + WHERE $where"); + + return Bugzilla::Extension::TrackingFlags::Flag::Visibility->new_from_list($flag_ids); +} + +############################### +#### Validators #### +############################### + +sub _check_tracking_flag { + my ($invocant, $flag) = @_; + if (blessed $flag) { + return $flag->flag_id; + } + $flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + return $flag->flag_id; +} + +sub _check_product { + my ($invocant, $product) = @_; + if (blessed $product) { + return $product->id; + } + $product = Bugzilla::Product->new($product) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'product_id', value => $product }); + return $product->id; +} + +sub _check_component { + my ($invocant, $component) = @_; + return undef unless defined $component; + if (blessed $component) { + return $component->id; + } + $component = Bugzilla::Component->new($component) + || ThrowCodeError('tracking_flags_invalid_param', { name => 'component_id', value => $component }); + return $component->id; +} + +############################### +#### Accessors #### +############################### + +sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } +sub product_id { return $_[0]->{'product_id'}; } +sub component_id { return $_[0]->{'component_id'}; } + +sub tracking_flag { + my ($self) = @_; + $self->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new($self->tracking_flag_id); + return $self->{'tracking_flag'}; +} + +sub product { + my ($self) = @_; + $self->{'product'} ||= Bugzilla::Product->new($self->product_id); + return $self->{'product'}; +} + +sub component { + my ($self) = @_; + return undef unless $self->component_id; + $self->{'component'} ||= Bugzilla::Component->new($self->component_id); + return $self->{'component'}; +} + +1; diff --git a/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl b/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl new file mode 100644 index 000000000..b2b6efca7 --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/bug/tracking_flags.html.tmpl @@ -0,0 +1,57 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% FOREACH flag = flag_list %] + [% SET bug_id = bug.defined ? bug.id : 0 %] + [% SET flag_bug_value = flag.bug_flag(bug_id).value %] + [% NEXT IF !new_bug && (!user.id && flag_bug_value == '---') %] + <tr id="row_[% flag.name FILTER html %]"> + <td [% IF new_bug %]class="field_label"[% END %]> + <label for="[% flag.name FILTER html %]"> + [% IF new_bug %] + <a + [% IF help_html.${flag.name}.defined %] + title="[% help_html.${flag.name} FILTER txt FILTER collapse FILTER html %]" + class="field_help_link" + [% END %] + href="page.cgi?id=fields.html#[% flag.name FILTER uri %]"> + [% END %] + [% flag.description FILTER html %] + [% IF new_bug %] + </a> + [% END %]:</label> + </td> + <td> + [% IF user.id %] + <input type="hidden" id="[% flag.name FILTER html %]_dirty"> + <select id="[% flag.name FILTER html %]" + name="[% flag.name FILTER html %]"> + [% FOREACH value = flag.values %] + [% IF new_bug || value.name != flag_bug_value %] + [% NEXT IF !value.is_active || !flag.can_set_value(value.name) %] + [% END %] + <option value="[% value.name FILTER html %]" + id="v[% value.id FILTER html %]_[% flag.name FILTER html %]" + [% " selected" IF !new_bug && flag_bug_value == value.name %]> + [% value.name FILTER html %]</option> + [% END %] + </select> + <script type="text/javascript"> + initHidingOptionsForIE('[% flag.name FILTER js %]'); + </script> + [% IF !new_bug && user.id %] + <span id="ro_[% flag.name FILTER html %]" class="bz_default_hidden"> + [% flag_bug_value FILTER html %] + </span> + [% END %] + [% ELSE %] + [% flag_bug_value FILTER html %] + [% END %] + </td> + </tr> +[% END %] diff --git a/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl new file mode 100644 index 000000000..4808da069 --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/admin/admin-end_links_right.html.tmpl @@ -0,0 +1,18 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% IF user.in_group('admin') %] + <dt id="push"> + <a href="page.cgi?id=tracking_flags_admin_list.html">Release Tracking Flags</a> + </dt> + <dd> + Tracking flags are special multi-value fields used to aid tracking releases + of Firefox, Firefox OS, Thunderbird, and other projects. + </dd> +[% END %] + diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-bug_flags_end.html.tmpl new file mode 100644 index 000000000..2a90cbfe3 --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-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-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-project_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-project_flags_end.html.tmpl new file mode 100644 index 000000000..662bc26ee --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-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-tracking_flags_end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-tracking_flags_end.html.tmpl new file mode 100644 index 000000000..69827a87a --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/bug/create/create-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/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..697db75ce --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl @@ -0,0 +1,191 @@ +[%# 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. + #%] + +[%# Old style custom field based tracking flags %] +[% old_tracking_flags = [] %] +[% old_project_flags = [] %] +[% FOREACH field = Bugzilla.active_custom_fields(product=>bug.product_obj,component=>bug.component_obj,type=>2) %] + [% NEXT IF field.type == constants.FIELD_TYPE_EXTENSION %] + [% NEXT IF NOT user.id AND bug.${field.name} == "---" %] + [% NEXT IF cf_flag_disabled(field.name, bug) %] + [% IF cf_is_project_flag(field.name) %] + [% old_project_flags.push(field) %] + [% ELSE %] + [% old_tracking_flags.push(field) %] + [% END %] +[% END %] + +[%# Add in the new tracking flags that are type tracking or project %] +[% new_tracking_flags = [] %] +[% new_project_flags = [] %] +[% IF tracking_flags.size %] + [% FOREACH flag = tracking_flags %] + [% IF flag.flag_type == 'tracking' %] + [% new_tracking_flags.push(flag) %] + [% END %] + [% IF flag.flag_type == 'project' %] + [% new_project_flags.push(flag) %] + [% END %] + [% END %] +[% END %] + +[% IF old_project_flags.size || new_project_flags.size %] + <tr> + <td class="field_label"> + <label>Project Flags:</label> + </td> + <td> + [% IF bug.check_can_change_field('flagtypes.name', 0, 1) %] + <table class="tracking_flags"> + [% FOREACH field = old_project_flags %] + [% NEXT IF NOT user.id AND field.value == "---" %] + <tr id="row_[% field.name FILTER js %]"> + <td> + <label for="[% field.name FILTER html %]"> + [% field_descs.${field.name} FILTER html %]: + </label> + </td> + <td> + [% PROCESS bug/field.html.tmpl value = bug.${field.name} + editable = user.id + no_tds = 1 %] + [% IF user.id %] + <span id="ro_[% field.name FILTER html %]" class="bz_default_hidden"> + [% bug.${field.name} FILTER html %] + </span> + [% END %] + </td> + </tr> + [% END %] + [% INCLUDE bug/tracking_flags.html.tmpl + flag_list = new_project_flags %] + </table> + [% ELSE %] + [% FOREACH field = old_project_flags %] + [% NEXT IF bug.${field.name} == "---" %] + [% field_descs.${field.name} FILTER html %]: [% bug.${field.name} FILTER html %]<br> + [% END %] + [% FOREACH flag = project_flags %] + [% NEXT IF flag.bug_flag.value == '---' %] + [% flag.description FILTER html %]: [% flag.bug_flag.value FILTER html %]<br> + [% END %] + [% END %] + </td> + </tr> +[% END %] + +[% IF old_tracking_flags.size || new_tracking_flags.size %] + <tr> + <td class="field_label"> + <label>Tracking Flags:</label> + </td> + <td> + [% IF bug.check_can_change_field('flagtypes.name', 0, 1) %] + [% IF user.id %] + <span id="edit_tracking_flags_action"> + (<a href="#" name="tracking" class="edit_tracking_flags_link">edit</a>) + </span> + [% END %] + <table class="tracking_flags"> + [% FOREACH field = old_tracking_flags %] + [% NEXT IF NOT user.id AND field.value == "---" %] + <tr id="row_[% field.name FILTER js %]"> + <td> + <label for="[% field.name FILTER html %]"> + [% field_descs.${field.name} FILTER html %]: + </label> + </td> + <td> + [% PROCESS bug/field.html.tmpl + value = bug.${field.name} + editable = user.id + no_tds = 1 + %] + [% IF user.id %] + <span id="ro_[% field.name FILTER html %]" class="bz_default_hidden"> + [% bug.${field.name} FILTER html %] + </span> + [% END %] + </td> + </tr> + [% END %] + [% INCLUDE bug/tracking_flags.html.tmpl + flag_list = new_tracking_flags %] + </table> + [% ELSE %] + [% FOREACH field = old_tracking_flags %] + [% NEXT IF bug.${field.name} == "---" %] + [% field_descs.${field.name} FILTER html %]: [% bug.${field.name} FILTER html %]<br> + [% END %] + [% FOREACH flag = new_tracking_flags %] + [% NEXT IF flag.status == '---' %] + [% flag.description FILTER html %]: [% flag.bug_flag.value FILTER html %]<br> + [% END %] + [% END %] + </td> + </tr> + <script type="text/javascript"> + TrackingFlags.flags['tracking'] = {}; + [% FOREACH field = old_tracking_flags %] + TrackingFlags.flags['tracking']['[% field.name FILTER js %]'] = '[% bug.${field.name} FILTER js %]'; + [% END %] + [% FOREACH flag = new_tracking_flags %] + TrackingFlags.flags['tracking']['[% flag.name FILTER js %]'] = '[% flag.bug_flag.value FILTER js %]'; + [% END %] + TrackingFlags.types.push('tracking'); + </script> +[% END %] + +[%# Last, display any new style flags that are not type tracking or project %] +[% IF 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 class="field_label"> + <label>[% type.description FILTER html %]:</label> + </td> + <td> + [% IF bug.check_can_change_field('flagtypes.name', 0, 1) %] + [% IF user.id && type.collapsed %] + <span id="edit_[% type.name FILTER html %]_flags_action"> + (<a href="#" name="[% type.name FILTER html %]" class="edit_tracking_flags_link">edit</a>) + </span> + [% END %] + <table class="tracking_flags"> + [% INCLUDE bug/tracking_flags.html.tmpl + flag_list = flag_list %] + </table> + [% IF type.collapsed %] + <script type="text/javascript"> + TrackingFlags.flags['[% type.name FILTER js %]'] = {}; + [% FOREACH flag = flag_list %] + TrackingFlags.flags['[% type.name FILTER js %]']['[% flag.name FILTER js %]'] = '[% flag.bug_flag.value FILTER js %]'; + [% END %] + TrackingFlags.types.push('[% type.name FILTER js %]'); + </script> + [% END %] + [% ELSE %] + [% FOREACH flag = flag_list %] + [% NEXT IF flag.status == '---' %] + [% flag.description FILTER html %]: [% flag.bug_flag.value FILTER html %]<br> + [% END %] + [% END %] + </td> + </tr> + [% END %] + [% END %] +[% END %] + +<script type="text/javascript"> + hide_tracking_flags(); +</script> diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl new file mode 100644 index 000000000..f598609e8 --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/bug/field-editable.html.tmpl @@ -0,0 +1,38 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +<input type="hidden" id="[% field.name FILTER html %]_dirty"> +<select id="[% field.name FILTER html %]" + name="[% field.name FILTER html %]"> + [% IF allow_dont_change %] + <option value="[% dontchange FILTER html %]" + [% ' selected="selected"' IF value == dontchange %]> + [% dontchange FILTER html %] + </option> + [% END %] + [% FOREACH legal_value = field.values %] + [% IF legal_value.name != value %] + [% NEXT IF !field.can_set_value(legal_value.name) %] + [% NEXT IF !legal_value.is_active %] + [% END %] + <option value="[% legal_value.name FILTER html %]" + id="v[% legal_value.id FILTER html %] [%- field.name FILTER html %]" + [% IF legal_value.name == value %] + selected="selected" + [% END %]> + [%- display_value(field.name, legal_value.name) FILTER html ~%] + </option> + [% END %] +</select> +<script type="text/javascript"> +<!-- + initHidingOptionsForIE('[% field.name FILTER js %]'); + [%+ INCLUDE "bug/field-events.js.tmpl" + field = field, product = bug.product_obj %] +//--> +</script> diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl new file mode 100644 index 000000000..8fa1f1623 --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/bug/field-non_editable.html.tmpl @@ -0,0 +1,9 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% display_value(field.name, value) FILTER html %] diff --git a/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl new file mode 100644 index 000000000..5e4ef2fcb --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/bug/show-header-end.html.tmpl @@ -0,0 +1,10 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% javascript_urls.push('extensions/TrackingFlags/web/js/tracking_flags.js') %] +[% style_urls.push('extensions/TrackingFlags/web/styles/edit_bug.css') %] diff --git a/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl new file mode 100644 index 000000000..2d462ebaf --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/global/code-error-errors.html.tmpl @@ -0,0 +1,26 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% IF error == "tracking_flags_invalid_product" %] + [% title = "Invalid Product" %] + The product named '[% product FILTER html %]' does not exist. + +[% ELSIF error == "tracking_flags_invalid_component" %] + [% title = "Invalid Component" %] + The component named '[% component FILTER html %]' does not exist. + +[% ELSIF error == "tracking_flags_invalid_item_id" %] + [% title = "Invalid " _ item _ " ID" %] + Invalid [% item FILTER html %] ID ([% id FILTER html %]). + +[% ELSIF error == "tracking_flags_invalid_param" %] + [% title = "Invalid Parameter Provided" %] + An invalid parameter '[% value FILTER html %]' + for '[% name FILTER html %]' was provided. + +[% END %] diff --git a/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl new file mode 100644 index 000000000..45bb7332c --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/global/messages-messages.html.tmpl @@ -0,0 +1,15 @@ +[%# 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_flags_created' %] + The tracking flag has been created. + +[% ELSIF message_tag == 'tracking_flags_updated' %] + The tracking flag has been updated. + +[% 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..f17df1bdf --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,57 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% 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.name 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_duplicate_field" %] + [% title = "Duplicate field" %] + A tracking flag field named "[% name FILTER html %]" already exists. + +[% 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. + +[% 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..d9f11fa2a --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_edit.html.tmpl @@ -0,0 +1,194 @@ +[%# 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' ] +%] + +[% IF mode == 'deleted' %] + + <p>The tracking flag [% flag_name FILTER html %] has been deleted.</p> + <hr> + <p>Return to the <a href="page.cgi?id=tracking_flags_admin_list.html">list of Tracking Flags</a>.</p> + + [% RETURN %] + +[% END %] + +<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="[% mode == 'edit' ? flag.flag_id : 0 FILTER html %]"> +<input type="hidden" name="values" id="values" value=""> +<input type="hidden" name="visibility" id="visibility" value=""> +<input type="hidden" name="save" value="1"> + +[%# name/desc/etc %] + +<table class="edit" cellspacing="0"> + +<tr class="header"> + <th colspan="3">Flag</th> +</tr> + +<tr> + <th>Name</th> + <td><input name="flag_name" id="flag_name" value="[% flag.name FILTER html %]"></td> + <td class="help">database field name</td> +</tr> + +<tr> + <th>Description</th> + <td><input name="flag_desc" id="flag_desc" value="[% flag.description FILTER html %]"></td> + <td class="help">visible name</td> +</tr> + +<tr> + <th>Type</th> + <td> + <select name="flag_type" id="flag_type"> + <option value=""></option> + [% FOREACH type = tracking_flag_types %] + <option value="[% type.name FILTER html %]" + [% 'selected="selected"' IF flag.flag_type == type.name %]> + [% type.name FILTER html %]</option> + [% END %] + </select> + </td> + <td class="help">flag type used for grouping</td> +</tr> + +<tr> + <th>Sort Key</th> + <td> + <input name="flag_sort" id="flag_sort" value="[% flag.sortkey FILTER html %]"> + [ + <a class="txt_icon" href="#" onclick="inc_field('flag_sort', 5);return false">+5</a> + | <a class="txt_icon" href="#" onclick="inc_field('flag_sort', -5);return false">-5</a> + ] + </td> +</tr> + +<tr> + <th>Active</th> + <td><input type="checkbox" name="flag_active" id="flag_active" value="1" [% "checked" IF flag.is_active %]></td> +</tr> + +</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 can_delete %] + <input type="hidden" name="delete" id="delete" value=""> + <input type="submit" value="Delete" + 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..6c2ecc915 --- /dev/null +++ b/extensions/TrackingFlags/template/en/default/pages/tracking_flags_admin_list.html.tmpl @@ -0,0 +1,56 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% PROCESS global/header.html.tmpl + title = "Release Tracking Flags" + style_urls = [ 'extensions/TrackingFlags/web/styles/admin.css' ] +%] + +<table class="list" cellspacing="0"> + +<tr> + <th>Name</th> + <th>Description</th> + <th>Type</th> + <th>Sort Key</th> + <th>Active</th> + <th> </th> +</tr> + +[% FOREACH flag = flags %] + <tr class="[% loop.count % 2 == 1 ? "odd_row" : "even_row" %]"> + <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> + <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> +</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..461ef7c63 --- /dev/null +++ b/extensions/TrackingFlags/web/js/admin.js @@ -0,0 +1,384 @@ +/* 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 (!JSON) + JSON = YAHOO.lang.JSON; + + Event.addListener('flag_name', 'change', change_flag_name, Dom.get('flag_name')); + Event.addListener('flag_desc', 'change', change_string_value, Dom.get('flag_desc')); + Event.addListener('flag_type', 'change', change_select_value, Dom.get('flag_type')); + Event.addListener('flag_sort', 'change', change_int_value, Dom.get('flag_sort')); + + Event.addListener('product', 'change', function() { + if (Dom.get('product').value == '') + Dom.get('component').options.length = 0; + }); + + update_flag_values(); + update_flag_visibility(); + tag_missing_values(); + } catch(e) { + console.error(e); + } +}); + +// field + +function change_flag_name(e, o) { + change_string_value(e, o); + if (o.value == '') + return; + o.value = o.value.replace(/[^a-z0-9_]/g, '_'); + if (!o.value.match(/^cf_/)) + o.value = 'cf_' + o.value; + if (Dom.get('flag_desc').value == '') { + var desc = o.value; + desc = desc.replace(/^cf_/, ''); + desc = desc.replace(/_/g, '-'); + Dom.get('flag_desc').value = desc; + tag_missing_value(Dom.get('flag_desc')); + } +} + +function inc_field(id, amount) { + var el = Dom.get(id); + el.value = el.value.match(/-?\d+/) * 1 + amount; + change_int_value(null, el); +} + +// values + +function update_flag_values() { + // update the values table from the flag_values global + + var tbl = Dom.get('flag_values'); + if (!tbl) + return; + + // remove current entries + while (tbl.rows.length > 3) { + tbl.deleteRow(2); + } + + // add all entries + + for (var i = 0, l = flag_values.length; i < l; i++) { + var value = flag_values[i]; + + var row = tbl.insertRow(2 + i); + var cell; + + // value + cell = row.insertCell(0); + if (value.value == '---') { + cell.innerHTML = '---'; + } else { + var inputEl = document.createElement('input'); + inputEl.id = 'value_' + i; + inputEl.type = 'text'; + inputEl.className = 'option_value'; + inputEl.value = value.value; + Event.addListener(inputEl, 'change', change_string_value, inputEl); + Event.addListener(inputEl, 'change', function(e, o) { + flag_values[o.id.match(/\d+$/)].value = o.value; + tag_invalid_values(); + }, inputEl); + Event.addListener(inputEl, 'keyup', function(e, o) { + if ((e.key || e.keyCode) == 27 && o.value == '') + remove_value(o.id.match(/\d+$/)); + }, inputEl); + cell.appendChild(inputEl); + } + + // setter + cell = row.insertCell(1); + var selectEl = document.createElement('select'); + selectEl.id = 'setter_' + i; + Event.addListener(selectEl, 'change', change_select_value, selectEl); + var optionEl = document.createElement('option'); + optionEl.value = ''; + selectEl.appendChild(optionEl); + for (var j = 0, m = groups.length; j < m; j++) { + var group = groups[j]; + optionEl = document.createElement('option'); + optionEl.value = group.id; + optionEl.innerHTML = YAHOO.lang.escapeHTML(group.name); + optionEl.selected = group.id == value.setter_group_id; + selectEl.appendChild(optionEl); + } + Event.addListener(selectEl, 'change', function(e, o) { + flag_values[o.id.match(/\d+$/)].setter_group_id = o.value; + tag_invalid_values(); + }, selectEl); + cell.appendChild(selectEl); + + // active + cell = row.insertCell(2); + if (value.value == '---') { + cell.innerHTML = 'Yes'; + } else { + var inputEl = document.createElement('input'); + inputEl.type = 'checkbox'; + inputEl.id = 'is_active_' + i; + inputEl.checked = value.is_active; + Event.addListener(inputEl, 'change', function(e, o) { + flag_values[o.id.match(/\d+$/)].is_active = o.checked; + }, inputEl); + cell.appendChild(inputEl); + } + + // actions + cell = row.insertCell(3); + var html = + '[' + + (i == 0 + ? '<span class="txt_icon"> - </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 != '---') + html += '| <a href="#" onclick="remove_value(' + i + ');return false">Remove</a>'; + html += ']'; + cell.innerHTML = html; + } + + tag_invalid_values(); +} + +function tag_invalid_values() { + // reset + for (var i = 0, l = flag_values.length; i < l; i++) { + Dom.removeClass('value_' + i, 'admin_error'); + } + + for (var i = 0, l = flag_values.length; i < l; i++) { + // missing + if (flag_values[i].value == '') + Dom.addClass('value_' + i, 'admin_error'); + if (!flag_values[i].setter_group_id) + Dom.addClass('setter_' + i, 'admin_error'); + + // duplicate values + for (var j = i; j < l; j++) { + if (i != j && flag_values[i].value == flag_values[j].value) { + Dom.addClass('value_' + i, 'admin_error'); + Dom.addClass('value_' + j, 'admin_error'); + } + } + } +} + +function value_move_up(idx) { + if (idx == 0) + return; + var tmp = flag_values[idx]; + flag_values[idx] = flag_values[idx - 1]; + flag_values[idx - 1] = tmp; + update_flag_values(); +} + +function value_move_down(idx) { + if (idx == flag_values.length - 1) + return; + var tmp = flag_values[idx]; + flag_values[idx] = flag_values[idx + 1]; + flag_values[idx + 1] = tmp; + update_flag_values(); +} + +function add_value() { + var value = new Object(); + value.id = 0; + value.value = ''; + value.setter_group_id = ''; + value.is_active = true; + var idx = flag_values.length; + flag_values[idx] = value; + update_flag_values(); + Dom.get('value_' + idx).focus(); +} + +function remove_value(idx) { + flag_values.splice(idx, 1); + update_flag_values(); +} + +function update_value(e, o) { + var i = o.value.match(/\d+/); + flag_values[i].value = o.value; +} + +// visibility + +function update_flag_visibility() { + // update the visibility table from the flag_visibility global + + var tbl = Dom.get('flag_visibility'); + if (!tbl) + return; + + // remove current entries + while (tbl.rows.length > 3) { + tbl.deleteRow(2); + } + + // show something if there aren't any components + + if (!flag_visibility.length) { + var row = tbl.insertRow(2); + var cell = row.insertCell(0); + cell.innerHTML = '<i class="admin_error_text">missing</i>'; + } + + // add all entries + + for (var i = 0, l = flag_visibility.length; i < l; i++) { + var visibility = flag_visibility[i]; + + var row = tbl.insertRow(2 + i); + var cell; + + // product + cell = row.insertCell(0); + cell.innerHTML = visibility.product; + + // component + cell = row.insertCell(1); + cell.innerHTML = visibility.component + ? visibility.component + : '<i>-- Any --</i>'; + + // actions + cell = row.insertCell(2); + cell.innerHTML = '[ <a href="#" onclick="remove_visibility(' + i + ');return false">Remove</a> ]'; + } +} + +function add_visibility() { + // validation + var product = Dom.get('product').value; + var component = Dom.get('component').value; + if (!product) { + alert('Please select a product.'); + return; + } + + // don't allow duplicates + for (var i = 0, l = flag_visibility.length; i < l; i++) { + if (flag_visibility[i].product == product && flag_visibility[i].component == component) { + Dom.get('product').value = ''; + Dom.get('component').options.length = 0; + return; + } + } + + if (component == '') { + // if we're adding an "any" component, remove non-any components + for (var i = 0; i < flag_visibility.length; i++) { + var visibility = flag_visibility[i]; + if (visibility.product == product) { + flag_visibility.splice(i, 1); + i--; + } + } + } else { + // don't add non-any components if an "any" component exists + for (var i = 0, l = flag_visibility.length; i < l; i++) { + var visibility = flag_visibility[i]; + if (visibility.product == product && !visibility.component) { + return; + } + } + } + + // add to model + var visibility = new Object(); + visibility.id = 0; + visibility.product = product; + visibility.component = component; + flag_visibility[flag_visibility.length] = visibility; + + // update ui + update_flag_visibility(); + Dom.get('product').value = ''; + Dom.get('component').options.length = 0; +} + +function remove_visibility(idx) { + flag_visibility.splice(idx, 1); + update_flag_visibility(); +} + +// validation and submission + +function tag_missing_values() { + var els = document.getElementsByTagName('input'); + for (var i = 0, l = els.length; i < l; i++) { + var el = els[i]; + if (el.id.match(/^(flag|value)_/)) + tag_missing_value(el); + } + tag_missing_value(Dom.get('flag_type')); +} + +function tag_missing_value(el) { + el.value == '' + ? Dom.addClass(el, 'admin_error') + : Dom.removeClass(el, 'admin_error'); +} + +function delete_confirm(flag) { + if (confirm('Are you sure you want to delete the flag ' + flag + ' ?')) { + Dom.get('delete').value = 1; + return true; + } else { + return false; + } +} + +function on_submit() { + if (Dom.get('delete') && Dom.get('delete').value) + return; + // let perl manage most validation errors, because they are clearly marked + // the exception is an empty visibility list, so catch that here as well + if (!flag_visibility.length) { + alert('You must provide at least one product for visibility.'); + return false; + } + + Dom.get('values').value = JSON.stringify(flag_values); + Dom.get('visibility').value = JSON.stringify(flag_visibility); + return true; +} + +// utils + +function change_string_value(e, o) { + o.value = YAHOO.lang.trim(o.value); + tag_missing_value(o); +} + +function change_int_value(e, o) { + o.value = o.value.match(/-?\d+/); + tag_missing_value(o); +} + +function change_select_value(e, o) { + tag_missing_value(o); +} + diff --git a/extensions/TrackingFlags/web/js/tracking_flags.js b/extensions/TrackingFlags/web/js/tracking_flags.js new file mode 100644 index 000000000..135b93dba --- /dev/null +++ b/extensions/TrackingFlags/web/js/tracking_flags.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. + */ + +var Dom = YAHOO.util.Dom; + +var TrackingFlags = { + flags: {}, + types: [] +}; + +function hide_tracking_flags() { + for (var i = 0, l = TrackingFlags.types.length; i < l; i++) { + var flag_type = TrackingFlags.types[i]; + for (var field in TrackingFlags.flags[flag_type]) { + var el = Dom.get(field); + var value = el ? el.value : TrackingFlags.flags[flag_type][field]; + if (el && (value != TrackingFlags.flags[flag_type][field])) { + show_tracking_flags(flag_type); + return; + } + if (value == '---') { + Dom.addClass('row_' + field, 'bz_default_hidden'); + } else { + Dom.addClass(field, 'bz_default_hidden'); + Dom.removeClass('ro_' + field, 'bz_default_hidden'); + } + } + } +} + +function show_tracking_flags(flag_type) { + Dom.addClass('edit_' + flag_type + '_flags_action', 'bz_default_hidden'); + for (var field in TrackingFlags.flags[flag_type]) { + if (Dom.get(field).value == '---') { + Dom.removeClass('row_' + field, 'bz_default_hidden'); + } else { + Dom.removeClass(field, 'bz_default_hidden'); + Dom.addClass('ro_' + field, 'bz_default_hidden'); + } + } +} + +YAHOO.util.Event.onDOMReady(function() { + var edit_tracking_links = Dom.getElementsByClassName('edit_tracking_flags_link'); + for (var i = 0, l = edit_tracking_links.length; i < l; i++) { + YAHOO.util.Event.addListener(edit_tracking_links[i], 'click', function(e) { + e.preventDefault(); + show_tracking_flags(this.name); + }); + } +}); diff --git a/extensions/TrackingFlags/web/styles/admin.css b/extensions/TrackingFlags/web/styles/admin.css new file mode 100644 index 000000000..374409ce6 --- /dev/null +++ b/extensions/TrackingFlags/web/styles/admin.css @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. */ + +/* list */ + +.list { + border: 1px solid #888888; +} + +.list td, .list th { + padding: 3px 10px 3px 3px; + border: 1px solid #888888; +} + +.list .odd_row { + background-color: #ffffff; + color: #000000; +} + +.list .even_row { + background-color: #eeeeee; + color: #000000; +} + +.list tr:hover { + background-color: #ccddee; +} + + +.list th { + text-align: left; + background: #dddddd; +} + +.list .disabled { + color: #888888; + text-decoration: line-through; +} + +#new_flag { + margin: 1em 0em; +} + +/* edit */ + +.edit { + margin-bottom: 2em; +} + +.edit .header { + background: #dddddd; +} + +.edit .help { + font-style: italic; +} + +.edit td, .edit th { + padding: 1px 5px; +} + +.edit th { + text-align: left; +} + +#edit_mode { + margin: 1em 0em; +} + +#flag_name { + width: 20em; +} + +#flag_desc { + width: 20em; +} + +#flag_sort { + width: 10em; +} + +.option_value { + width: 10em; +} + +.hidden { + display: none; +} + +.txt_icon { + font-family: monospace; +} + +.admin_error { + border: 1px solid red; + box-shadow: 0px 0px 4px #ff0000; + -webkit-box-shadow: 0px 0px 4px #ff0000; + -moz-box-shadow: 0px 0px 4px #ff0000; +} + +.admin_error_text { + color: #cc0000; +} diff --git a/extensions/TrackingFlags/web/styles/edit_bug.css b/extensions/TrackingFlags/web/styles/edit_bug.css new file mode 100644 index 000000000..132a6a1ca --- /dev/null +++ b/extensions/TrackingFlags/web/styles/edit_bug.css @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. */ + +.tracking_flags { + width: auto !important; +} + +.tracking_flags .field_label { + font-weight: normal !important; +} + +#Create .tracking_flags th { + text-align: left; +} |