diff options
Diffstat (limited to 'extensions/TrackingFlags/lib/Admin.pm')
-rw-r--r-- | extensions/TrackingFlags/lib/Admin.pm | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/extensions/TrackingFlags/lib/Admin.pm b/extensions/TrackingFlags/lib/Admin.pm new file mode 100644 index 000000000..1bae18ef8 --- /dev/null +++ b/extensions/TrackingFlags/lib/Admin.pm @@ -0,0 +1,446 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::TrackingFlags::Admin; + +use strict; +use warnings; + +use Bugzilla; +use Bugzilla::Component; +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::Product; +use Bugzilla::Util qw(trim detaint_natural); + +use Bugzilla::Extension::TrackingFlags::Constants; +use Bugzilla::Extension::TrackingFlags::Flag; +use Bugzilla::Extension::TrackingFlags::Flag::Bug; +use Bugzilla::Extension::TrackingFlags::Flag::Value; +use Bugzilla::Extension::TrackingFlags::Flag::Visibility; + +use JSON; +use Scalar::Util qw(blessed); + +use base qw(Exporter); +our @EXPORT = qw( + admin_list + admin_edit +); + +# +# page loading +# + +sub admin_list { + my ($vars) = @_; + $vars->{show_bug_counts} = Bugzilla->input_params->{show_bug_counts}; + $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; +} + +sub admin_edit { + my ($vars, $page) = @_; + my $input = Bugzilla->input_params; + + $vars->{groups} = _groups_to_json(); + $vars->{mode} = $input->{mode} || 'new'; + $vars->{flag_id} = $input->{flag_id} || 0; + $vars->{tracking_flag_types} = FLAG_TYPES; + + if ($input->{delete}) { + my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); + $flag->remove_from_db(); + + $vars->{message} = 'tracking_flag_deleted'; + $vars->{flag} = $flag; + $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; + + print Bugzilla->cgi->header; + my $template = Bugzilla->template; + $template->process('pages/tracking_flags_admin_list.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + + } elsif ($input->{save}) { + # save + + my ($flag, $values, $visibilities) = _load_from_input($input, $vars); + _validate($flag, $values, $visibilities); + my $flag_obj = _update_db($flag, $values, $visibilities); + + $vars->{flag} = $flag_obj; + $vars->{values} = _flag_values_to_json($values); + $vars->{visibility} = _flag_visibility_to_json($visibilities); + + if ($vars->{mode} eq 'new') { + $vars->{message} = 'tracking_flag_created'; + } else { + $vars->{message} = 'tracking_flag_updated'; + } + $vars->{mode} = 'edit'; + + } else { + # initial load + + if ($vars->{mode} eq 'edit') { + # edit - straight load + my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); + $vars->{flag} = $flag; + $vars->{values} = _flag_values_to_json($flag->values); + $vars->{visibility} = _flag_visibility_to_json($flag->visibility); + $vars->{can_delete} = !$flag->bug_count; + + } elsif ($vars->{mode} eq 'copy') { + # copy - load the source flag + $vars->{mode} = 'new'; + my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($input->{copy_from}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{copy_from} }); + + # increment the number at the end of the name and description + if ($flag->name =~ /^(\D+)(\d+)$/) { + $flag->set_name("$1" . ($2 + 1)); + } + if ($flag->description =~ /^(\D+)(\d+)$/) { + $flag->set_description("$1" . ($2 + 1)); + } + $flag->set_sortkey(_next_unique_sortkey($flag->sortkey)); + $flag->set_type($flag->flag_type); + $flag->set_enter_bug($flag->enter_bug); + # always default new flags as active, even when copying an inactive one + $flag->set_is_active(1); + + $vars->{flag} = $flag; + $vars->{values} = _flag_values_to_json($flag->values, 1); + $vars->{visibility} = _flag_visibility_to_json($flag->visibility, 1); + $vars->{can_delete} = 0; + + } else { + $vars->{mode} = 'new'; + $vars->{flag} = { + sortkey => 0, + enter_bug => 1, + is_active => 1, + }; + $vars->{values} = _flag_values_to_json([ + { + id => 0, + value => '---', + setter_group_id => '', + is_active => 1, + comment => '', + }, + ]); + $vars->{visibility} = ''; + $vars->{can_delete} = 0; + } + } +} + +sub _load_from_input { + my ($input, $vars) = @_; + + # flag + + my $flag = { + id => ($input->{mode} eq 'edit' ? $input->{flag_id} : 0), + name => trim($input->{flag_name} || ''), + description => trim($input->{flag_desc} || ''), + sortkey => $input->{flag_sort} || 0, + type => trim($input->{flag_type} || ''), + enter_bug => $input->{flag_enter_bug} ? 1 : 0, + is_active => $input->{flag_active} ? 1 : 0, + }; + detaint_natural($flag->{id}); + detaint_natural($flag->{sortkey}); + detaint_natural($flag->{enter_bug}); + detaint_natural($flag->{is_active}); + + # values + + my $values = decode_json($input->{values} || '[]'); + foreach my $value (@$values) { + $value->{value} = '' unless exists $value->{value} && defined $value->{value}; + $value->{setter_group_id} = '' unless $value->{setter_group_id}; + $value->{is_active} = $value->{is_active} ? 1 : 0; + } + + # vibility + + my $visibilities = decode_json($input->{visibility} || '[]'); + foreach my $visibility (@$visibilities) { + $visibility->{product} = '' unless exists $visibility->{product} && defined $visibility->{product}; + $visibility->{component} = '' unless exists $visibility->{component} && defined $visibility->{component}; + } + + return ($flag, $values, $visibilities); +} + +sub _next_unique_sortkey { + my ($sortkey) = @_; + + my %current; + foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all()) { + $current{$flag->sortkey} = 1; + } + + $sortkey += 5; + $sortkey += 5 while exists $current{$sortkey}; + return $sortkey; +} + +# +# validation +# + +sub _validate { + my ($flag, $values, $visibilities) = @_; + + # flag + + my @missing; + push @missing, 'Field Name' if $flag->{name} eq ''; + push @missing, 'Field Description' if $flag->{description} eq ''; + push @missing, 'Field Sort Key' if $flag->{sortkey} eq ''; + scalar(@missing) + && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); + + $flag->{name} =~ /^cf_/ + || ThrowUserError('tracking_flags_cf_prefix'); + + if ($flag->{id}) { + my $old_flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); + if ($flag->{name} ne $old_flag->name) { + Bugzilla::Field->new({ name => $flag->{name} }) + && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); + } + } else { + Bugzilla::Field->new({ name => $flag->{name} }) + && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); + } + + # values + + scalar(@$values) + || ThrowUserError('tracking_flags_missing_values'); + + my %seen; + foreach my $value (@$values) { + my $v = $value->{value}; + + $v eq '' + && ThrowUserError('tracking_flags_missing_value'); + + exists $seen{$v} + && ThrowUserError('tracking_flags_duplicate_value', { value => $v }); + $seen{$v} = 1; + + push @missing, "Setter for $v" if !$value->{setter_group_id}; + } + scalar(@missing) + && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); + + # visibility + + scalar(@$visibilities) + || ThrowUserError('tracking_flags_missing_visibility'); + + %seen = (); + foreach my $visibility (@$visibilities) { + my $name = $visibility->{product} . ':' . $visibility->{component}; + + exists $seen{$name} + && ThrowUserError('tracking_flags_duplicate_visibility', { name => $name }); + + $visibility->{product_obj} = Bugzilla::Product->new({ name => $visibility->{product} }) + || ThrowCodeError('tracking_flags_invalid_product', { product => $visibility->{product} }); + + if ($visibility->{component} ne '') { + $visibility->{component_obj} = Bugzilla::Component->new({ product => $visibility->{product_obj}, + name => $visibility->{component} }) + || ThrowCodeError('tracking_flags_invalid_component', { + product => $visibility->{product}, + component_name => $visibility->{component}, + }); + } + } + +} + +# +# database updating +# + +sub _update_db { + my ($flag, $values, $visibilities) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $flag_obj = _update_db_flag($flag); + _update_db_values($flag_obj, $flag, $values); + _update_db_visibility($flag_obj, $flag, $visibilities); + $dbh->bz_commit_transaction(); + + return $flag_obj; +} + +sub _update_db_flag { + my ($flag) = @_; + + my $object_set = { + name => $flag->{name}, + description => $flag->{description}, + sortkey => $flag->{sortkey}, + type => $flag->{type}, + enter_bug => $flag->{enter_bug}, + is_active => $flag->{is_active}, + }; + + my $flag_obj; + if ($flag->{id}) { + # update existing flag + $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); + $flag_obj->set_all($object_set); + $flag_obj->update(); + + } else { + # create new flag + $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->create($object_set); + } + + return $flag_obj; +} + +sub _update_db_values { + my ($flag_obj, $flag, $values) = @_; + + # delete + foreach my $current_value (@{ $flag_obj->values }) { + if (!grep { $_->{id} == $current_value->id } @$values) { + $current_value->remove_from_db(); + } + } + + # add/update + my $sortkey = 0; + foreach my $value (@{ $values }) { + $sortkey += 10; + + my $object_set = { + value => $value->{value}, + setter_group_id => $value->{setter_group_id}, + is_active => $value->{is_active}, + sortkey => $sortkey, + comment => $value->{comment}, + }; + + if ($value->{id}) { + my $value_obj = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag value', id => $flag->{id} }); + my $old_value = $value_obj->value; + $value_obj->set_all($object_set); + $value_obj->update(); + Bugzilla::Extension::TrackingFlags::Flag::Bug->update_all_values({ + value_obj => $value_obj, + old_value => $old_value, + new_value => $value_obj->value, + }); + } else { + $object_set->{tracking_flag_id} = $flag_obj->flag_id; + Bugzilla::Extension::TrackingFlags::Flag::Value->create($object_set); + } + } +} + +sub _update_db_visibility { + my ($flag_obj, $flag, $visibilities) = @_; + + # delete + foreach my $current_visibility (@{ $flag_obj->visibility }) { + if (!grep { $_->{id} == $current_visibility->id } @$visibilities) { + $current_visibility->remove_from_db(); + } + } + + # add + foreach my $visibility (@{ $visibilities }) { + next if $visibility->{id}; + Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ + tracking_flag_id => $flag_obj->flag_id, + product_id => $visibility->{product_obj}->id, + component_id => $visibility->{component} ? $visibility->{component_obj}->id : undef, + }); + } +} + +# +# serialisation +# + +sub _groups_to_json { + my @data; + foreach my $group (sort { $a->name cmp $b->name } Bugzilla::Group->get_all()) { + push @data, { + id => $group->id, + name => $group->name, + }; + } + return encode_json(\@data); +} + +sub _flag_values_to_json { + my ($values, $is_copy) = @_; + # setting is_copy will set the id's to zero, to force new values rather + # than editing existing ones + my @data; + foreach my $value (@$values) { + push @data, { + id => $is_copy ? 0 : $value->{id}, + value => $value->{value}, + setter_group_id => $value->{setter_group_id}, + is_active => $value->{is_active} ? JSON::true : JSON::false, + comment => $value->{comment} // '', + }; + } + return encode_json(\@data); +} + +sub _flag_visibility_to_json { + my ($visibilities, $is_copy) = @_; + # setting is_copy will set the id's to zero, to force new visibilites + # rather than editing existing ones + my @data; + + foreach my $visibility (@$visibilities) { + my $product = exists $visibility->{product_id} + ? $visibility->product->name + : $visibility->{product}; + my $component; + if (exists $visibility->{component_id} && $visibility->{component_id}) { + $component = $visibility->component->name; + } elsif (exists $visibility->{component}) { + $component = $visibility->{component}; + } else { + $component = undef; + } + push @data, { + id => $is_copy ? 0 : $visibility->{id}, + product => $product, + component => $component, + }; + } + @data = sort { + lc($a->{product}) cmp lc($b->{product}) + || lc($a->{component}) cmp lc($b->{component}) + } @data; + return encode_json(\@data); +} + +1; |