diff options
Diffstat (limited to 'extensions/BugModal')
-rw-r--r-- | extensions/BugModal/Config.pm | 6 | ||||
-rw-r--r-- | extensions/BugModal/Extension.pm | 551 | ||||
-rw-r--r-- | extensions/BugModal/lib/ActivityStream.pm | 567 | ||||
-rw-r--r-- | extensions/BugModal/lib/MonkeyPatches.pm | 21 | ||||
-rw-r--r-- | extensions/BugModal/lib/Util.pm | 28 | ||||
-rw-r--r-- | extensions/BugModal/lib/WebService.pm | 618 |
6 files changed, 907 insertions, 884 deletions
diff --git a/extensions/BugModal/Config.pm b/extensions/BugModal/Config.pm index 25a864e6e..bd15c075c 100644 --- a/extensions/BugModal/Config.pm +++ b/extensions/BugModal/Config.pm @@ -10,8 +10,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'BugModal'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'BugModal'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/BugModal/Extension.pm b/extensions/BugModal/Extension.pm index ef9c93a37..8b72bb757 100644 --- a/extensions/BugModal/Extension.pm +++ b/extensions/BugModal/Extension.pm @@ -26,322 +26,321 @@ use JSON::XS qw(encode_json); our $VERSION = '1'; use constant READABLE_BUG_STATUS_PRODUCTS => ( - 'Core', - 'Toolkit', - 'Firefox', - 'Firefox for Android', - 'Firefox for iOS', - 'Bugzilla', - 'bugzilla.mozilla.org' + 'Core', 'Toolkit', + 'Firefox', 'Firefox for Android', + 'Firefox for iOS', 'Bugzilla', + 'bugzilla.mozilla.org' ); sub show_bug_format { - my ($self, $args) = @_; - $args->{format} = _alternative_show_bug_format(); + my ($self, $args) = @_; + $args->{format} = _alternative_show_bug_format(); } sub edit_bug_format { - my ($self, $args) = @_; - $args->{format} = _alternative_show_bug_format(); + my ($self, $args) = @_; + $args->{format} = _alternative_show_bug_format(); } sub _alternative_show_bug_format { - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - if (my $ctype = $cgi->param('ctype')) { - return '' if $ctype ne 'html'; - } - if (my $format = $cgi->param('format')) { - return ($format eq '__default__' || $format eq 'default') ? '' : $format; - } - return $user->setting('ui_experiments') eq 'on' ? 'modal' : ''; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + if (my $ctype = $cgi->param('ctype')) { + return '' if $ctype ne 'html'; + } + if (my $format = $cgi->param('format')) { + return ($format eq '__default__' || $format eq 'default') ? '' : $format; + } + return $user->setting('ui_experiments') eq 'on' ? 'modal' : ''; } sub template_after_create { - my ($self, $args) = @_; - my $context = $args->{template}->context; - - # wrapper around time_ago() - $context->define_filter( - time_duration => sub { - my ($context) = @_; - return sub { - my ($timestamp) = @_; - my $datetime = datetime_from($timestamp) - // return $timestamp; - return time_ago($datetime); - }; - }, 1 - ); - - # morph a string into one which is suitable to use as an element's id - $context->define_filter( - id => sub { - my ($context) = @_; - return sub { - my ($id) = @_; - $id //= ''; - $id = lc($id); - while ($id ne '' && $id !~ /^[a-z]/) { - $id = substr($id, 1); - } - $id =~ tr/ /-/; - $id =~ s/[^a-z\d\-_:\.]/_/g; - return $id; - }; - }, 1 - ); - - # parse date string and output epoch - $context->define_filter( - epoch => sub { - my ($context) = @_; - return sub { - my ($date_str) = @_; - return date_str_to_time($date_str); - }; - }, 1 - ); - - # flatten a list of hashrefs to a list of values - # eg. logins = users.pluck("login") - $context->define_vmethod( - list => pluck => sub { - my ($list, $field) = @_; - return [ map { $_->$field } @$list ]; - } - ); - - # returns array where the value in $field does not equal $value - # opposite of "only" - # eg. not_byron = users.skip("name", "Byron") - $context->define_vmethod( - list => skip => sub { - my ($list, $field, $value) = @_; - return [ grep { $_->$field ne $value } @$list ]; - } - ); - - # returns array where the value in $field equals $value - # opposite of "skip" - # eg. byrons_only = users.only("name", "Byron") - $context->define_vmethod( - list => only => sub { - my ($list, $field, $value) = @_; - return [ grep { $_->$field eq $value } @$list ]; - } - ); - - # returns boolean indicating if the value exists in the list - # eg. has_byron = user_names.exists("byron") - $context->define_vmethod( - list => exists => sub { - my ($list, $value) = @_; - return any { $_ eq $value } @$list; - } - ); - - # ucfirst is only available in new template::toolkit versions - $context->define_vmethod( - item => ucfirst => sub { - my ($text) = @_; - return ucfirst($text); - } - ); -} - -sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{file}; - my $vars = $args->{vars}; - - if ($file eq 'bug/process/header.html.tmpl' - || $file eq 'bug/create/created.html.tmpl' - || $file eq 'attachment/created.html.tmpl' - || $file eq 'attachment/updated.html.tmpl') - { - if (_alternative_show_bug_format() eq 'modal') { - $vars->{alt_ui_header} = 'bug_modal/header.html.tmpl'; - $vars->{alt_ui_show} = 'bug/show-modal.html.tmpl'; - $vars->{alt_ui_edit} = 'bug_modal/edit.html.tmpl'; + my ($self, $args) = @_; + my $context = $args->{template}->context; + + # wrapper around time_ago() + $context->define_filter( + time_duration => sub { + my ($context) = @_; + return sub { + my ($timestamp) = @_; + my $datetime = datetime_from($timestamp) // return $timestamp; + return time_ago($datetime); + }; + }, + 1 + ); + + # morph a string into one which is suitable to use as an element's id + $context->define_filter( + id => sub { + my ($context) = @_; + return sub { + my ($id) = @_; + $id //= ''; + $id = lc($id); + while ($id ne '' && $id !~ /^[a-z]/) { + $id = substr($id, 1); } - return; + $id =~ tr/ /-/; + $id =~ s/[^a-z\d\-_:\.]/_/g; + return $id; + }; + }, + 1 + ); + + # parse date string and output epoch + $context->define_filter( + epoch => sub { + my ($context) = @_; + return sub { + my ($date_str) = @_; + return date_str_to_time($date_str); + }; + }, + 1 + ); + + # flatten a list of hashrefs to a list of values + # eg. logins = users.pluck("login") + $context->define_vmethod( + list => pluck => sub { + my ($list, $field) = @_; + return [map { $_->$field } @$list]; } - - if ($file =~ m#^bug/show-([^\.]+)\.html\.tmpl$#) { - my $format = $1; - return unless _alternative_show_bug_format() eq $format; + ); + + # returns array where the value in $field does not equal $value + # opposite of "only" + # eg. not_byron = users.skip("name", "Byron") + $context->define_vmethod( + list => skip => sub { + my ($list, $field, $value) = @_; + return [grep { $_->$field ne $value } @$list]; } - elsif ($file ne 'bug_modal/edit.html.tmpl') { - return; + ); + + # returns array where the value in $field equals $value + # opposite of "skip" + # eg. byrons_only = users.only("name", "Byron") + $context->define_vmethod( + list => only => sub { + my ($list, $field, $value) = @_; + return [grep { $_->$field eq $value } @$list]; } - - if ($vars->{bug} && !$vars->{bugs}) { - $vars->{bugs} = [$vars->{bug}]; + ); + + # returns boolean indicating if the value exists in the list + # eg. has_byron = user_names.exists("byron") + $context->define_vmethod( + list => exists => sub { + my ($list, $value) = @_; + return any { $_ eq $value } @$list; } + ); - return unless - $vars->{bugs} - && ref($vars->{bugs}) eq 'ARRAY' - && scalar(@{ $vars->{bugs} }) == 1; - my $bug = $vars->{bugs}->[0]; - return if exists $bug->{error}; - - # trigger loading of tracking flags - if (Bugzilla->has_extension('TrackingFlags')) { - Bugzilla::Extension::TrackingFlags->template_before_process({ - file => 'bug/edit.html.tmpl', - vars => $vars, - }); + # ucfirst is only available in new template::toolkit versions + $context->define_vmethod( + item => ucfirst => sub { + my ($text) = @_; + return ucfirst($text); } + ); +} - if (any { $bug->product eq $_ } READABLE_BUG_STATUS_PRODUCTS) { - my @flags = map { { name => $_->name, status => $_->status } } @{$bug->flags}; - $vars->{readable_bug_status_json} = encode_json({ - dupe_of => $bug->dup_id, - id => $bug->id, - keywords => [ map { $_->name } @{$bug->keyword_objects} ], - priority => $bug->priority, - resolution => $bug->resolution, - status => $bug->bug_status, - flags => \@flags, - target_milestone => $bug->target_milestone, - map { $_->name => $_->bug_flag($bug->id)->value } @{$vars->{tracking_flags}}, - }); - # HTML4 attributes cannot be longer than this, so just skip it in this case. - if (length($vars->{readable_bug_status_json}) > 65536) { - delete $vars->{readable_bug_status_json}; - } +sub template_before_process { + my ($self, $args) = @_; + my $file = $args->{file}; + my $vars = $args->{vars}; + + if ( $file eq 'bug/process/header.html.tmpl' + || $file eq 'bug/create/created.html.tmpl' + || $file eq 'attachment/created.html.tmpl' + || $file eq 'attachment/updated.html.tmpl') + { + if (_alternative_show_bug_format() eq 'modal') { + $vars->{alt_ui_header} = 'bug_modal/header.html.tmpl'; + $vars->{alt_ui_show} = 'bug/show-modal.html.tmpl'; + $vars->{alt_ui_edit} = 'bug_modal/edit.html.tmpl'; } + return; + } + + if ($file =~ m#^bug/show-([^\.]+)\.html\.tmpl$#) { + my $format = $1; + return unless _alternative_show_bug_format() eq $format; + } + elsif ($file ne 'bug_modal/edit.html.tmpl') { + return; + } + + if ($vars->{bug} && !$vars->{bugs}) { + $vars->{bugs} = [$vars->{bug}]; + } + + return + unless $vars->{bugs} + && ref($vars->{bugs}) eq 'ARRAY' + && scalar(@{$vars->{bugs}}) == 1; + my $bug = $vars->{bugs}->[0]; + return if exists $bug->{error}; + + # trigger loading of tracking flags + if (Bugzilla->has_extension('TrackingFlags')) { + Bugzilla::Extension::TrackingFlags->template_before_process({ + file => 'bug/edit.html.tmpl', vars => $vars, + }); + } + + if (any { $bug->product eq $_ } READABLE_BUG_STATUS_PRODUCTS) { + my @flags = map { {name => $_->name, status => $_->status} } @{$bug->flags}; + $vars->{readable_bug_status_json} = encode_json({ + dupe_of => $bug->dup_id, + id => $bug->id, + keywords => [map { $_->name } @{$bug->keyword_objects}], + priority => $bug->priority, + resolution => $bug->resolution, + status => $bug->bug_status, + flags => \@flags, + target_milestone => $bug->target_milestone, + map { $_->name => $_->bug_flag($bug->id)->value } @{$vars->{tracking_flags}}, + }); - # bug->choices loads a lot of data that we want to lazy-load - # just load the status and resolutions and perform extra checks here - # upstream does these checks in the bug/fields template - my $perms = $bug->user; - my @resolutions; - foreach my $r (@{ Bugzilla::Field->new({ name => 'resolution', cache => 1 })->legal_values }) { - my $resolution = $r->name; - next unless $resolution; - - # always allow the current value - if ($resolution eq $bug->resolution) { - push @resolutions, $r; - next; - } - - # never allow inactive values - next unless $r->is_active; + # HTML4 attributes cannot be longer than this, so just skip it in this case. + if (length($vars->{readable_bug_status_json}) > 65536) { + delete $vars->{readable_bug_status_json}; + } + } + + # bug->choices loads a lot of data that we want to lazy-load + # just load the status and resolutions and perform extra checks here + # upstream does these checks in the bug/fields template + my $perms = $bug->user; + my @resolutions; + foreach my $r ( + @{Bugzilla::Field->new({name => 'resolution', cache => 1})->legal_values}) + { + my $resolution = $r->name; + next unless $resolution; + + # always allow the current value + if ($resolution eq $bug->resolution) { + push @resolutions, $r; + next; + } - # ensure the user has basic rights to change this field - next unless $bug->check_can_change_field('resolution', '---', $resolution); + # never allow inactive values + next unless $r->is_active; - # canconfirm users can only set the resolution to WFM, INCOMPLETE or DUPE - if ($perms->{canconfirm} - && !($perms->{canedit} || $perms->{isreporter})) - { - next if - $resolution ne 'WORKSFORME' - && $resolution ne 'INCOMPLETE' - && $resolution ne 'DUPLICATE'; - } + # ensure the user has basic rights to change this field + next unless $bug->check_can_change_field('resolution', '---', $resolution); - # reporters can set it to anything, except INCOMPLETE - if ($perms->{isreporter} - && !($perms->{canconfirm} || $perms->{canedit})) - { - next if $resolution eq 'INCOMPLETE'; - } + # canconfirm users can only set the resolution to WFM, INCOMPLETE or DUPE + if ($perms->{canconfirm} && !($perms->{canedit} || $perms->{isreporter})) { + next + if $resolution ne 'WORKSFORME' + && $resolution ne 'INCOMPLETE' + && $resolution ne 'DUPLICATE'; + } - # expired has, uh, expired - next if $resolution eq 'EXPIRED'; + # reporters can set it to anything, except INCOMPLETE + if ($perms->{isreporter} && !($perms->{canconfirm} || $perms->{canedit})) { + next if $resolution eq 'INCOMPLETE'; + } - push @resolutions, $r; + # expired has, uh, expired + next if $resolution eq 'EXPIRED'; + + push @resolutions, $r; + } + $bug->{choices} = { + bug_status => [ + grep { $_->is_active || $_->name eq $bug->bug_status } + @{$bug->statuses_available} + ], + resolution => \@resolutions, + }; + + # group tracking flags by version to allow for a better tabular output + my @tracking_table; + my $tracking_flags = $vars->{tracking_flags}; + foreach my $flag (@$tracking_flags) { + my $flag_type = $flag->flag_type; + my $type = 'status'; + my $name = $flag->description; + if ($flag_type eq 'tracking' && $name =~ /^(tracking|status)-(.+)/) { + ($type, $name) = ($1, $2); } - $bug->{choices} = { - bug_status => [ - grep { $_->is_active || $_->name eq $bug->bug_status } - @{ $bug->statuses_available } - ], - resolution => \@resolutions, - }; - - # group tracking flags by version to allow for a better tabular output - my @tracking_table; - my $tracking_flags = $vars->{tracking_flags}; - foreach my $flag (@$tracking_flags) { - my $flag_type = $flag->flag_type; - my $type = 'status'; - my $name = $flag->description; - if ($flag_type eq 'tracking' && $name =~ /^(tracking|status)-(.+)/) { - ($type, $name) = ($1, $2); - } - my ($existing) = grep { $_->{type} eq $flag_type && $_->{name} eq $name } @tracking_table; - if ($existing) { - $existing->{$type} = $flag; - } - else { - push @tracking_table, { - $type => $flag, - name => $name, - type => $flag_type, - }; - } + my ($existing) + = grep { $_->{type} eq $flag_type && $_->{name} eq $name } @tracking_table; + if ($existing) { + $existing->{$type} = $flag; } - $vars->{tracking_flags_table} = \@tracking_table; - - # for the "view -> hide treeherder comments" menu item - my $treeherder_id = Bugzilla->treeherder_user->id; - foreach my $change_set (@{ $bug->activity_stream }) { - if ($change_set->{comment} && $change_set->{comment}->author->id == $treeherder_id) { - $vars->{treeherder} = Bugzilla->treeherder_user; - last; - } + else { + push @tracking_table, {$type => $flag, name => $name, type => $flag_type,}; + } + } + $vars->{tracking_flags_table} = \@tracking_table; + + # for the "view -> hide treeherder comments" menu item + my $treeherder_id = Bugzilla->treeherder_user->id; + foreach my $change_set (@{$bug->activity_stream}) { + if ( $change_set->{comment} + && $change_set->{comment}->author->id == $treeherder_id) + { + $vars->{treeherder} = Bugzilla->treeherder_user; + last; } + } } sub bug_start_of_set_all { - my ($self, $args) = @_; - my $bug = $args->{bug}; - my $params = $args->{params}; - - # reset to the component defaults if not supplied - if (exists $params->{assigned_to} && (!defined $params->{assigned_to} || $params->{assigned_to} eq '')) { - $params->{assigned_to} = $bug->component_obj->default_assignee->login; - } - if (exists $params->{qa_contact} && (!defined $params->{qa_contact} || $params->{qa_contact} eq '') - && $bug->component_obj->default_qa_contact->id) - { - $params->{qa_contact} = $bug->component_obj->default_qa_contact->login; - } + my ($self, $args) = @_; + my $bug = $args->{bug}; + my $params = $args->{params}; + + # reset to the component defaults if not supplied + if (exists $params->{assigned_to} + && (!defined $params->{assigned_to} || $params->{assigned_to} eq '')) + { + $params->{assigned_to} = $bug->component_obj->default_assignee->login; + } + if ( exists $params->{qa_contact} + && (!defined $params->{qa_contact} || $params->{qa_contact} eq '') + && $bug->component_obj->default_qa_contact->id) + { + $params->{qa_contact} = $bug->component_obj->default_qa_contact->login; + } } sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{bug_modal} = 'Bugzilla::Extension::BugModal::WebService'; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{bug_modal} = 'Bugzilla::Extension::BugModal::WebService'; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'ui_experiments', - options => ['on', 'off'], - default => 'on', - category => 'User Interface' - }); - add_setting({ - name => 'ui_remember_collapsed', - options => ['on', 'off'], - default => 'off', - category => 'User Interface' - }); - add_setting({ - name => 'ui_use_absolute_time', - options => ['on', 'off'], - default => 'off', - category => 'User Interface', - }); + my ($self, $args) = @_; + add_setting({ + name => 'ui_experiments', + options => ['on', 'off'], + default => 'on', + category => 'User Interface' + }); + add_setting({ + name => 'ui_remember_collapsed', + options => ['on', 'off'], + default => 'off', + category => 'User Interface' + }); + add_setting({ + name => 'ui_use_absolute_time', + options => ['on', 'off'], + default => 'off', + category => 'User Interface', + }); } __PACKAGE__->NAME; diff --git a/extensions/BugModal/lib/ActivityStream.pm b/extensions/BugModal/lib/ActivityStream.pm index 098c5df33..a7983e85c 100644 --- a/extensions/BugModal/lib/ActivityStream.pm +++ b/extensions/BugModal/lib/ActivityStream.pm @@ -49,310 +49,324 @@ use Bugzilla::Constants; # ] sub activity_stream { - my ($self) = @_; - if (!$self->{activity_stream}) { - my $stream = []; - _add_comments_to_stream($self, $stream); - _add_activities_to_stream($self, $stream); - _add_duplicates_to_stream($self, $stream); - - my $base_time = date_str_to_time($self->creation_ts); - foreach my $change_set (@$stream) { - $change_set->{id} = $change_set->{comment} - ? 'c' . $change_set->{comment}->count - : 'a' . ($change_set->{time} - $base_time) . '_' . $change_set->{user_id}; - foreach my $activity (@{ $change_set->{activity} }) { - $activity->{changes} = [ - sort { $a->{fieldname} cmp $b->{fieldname} } - @{ $activity->{changes} } - ]; - } - } - my $order = Bugzilla->user->setting('comment_sort_order'); - if ($order eq 'oldest_to_newest') { - $self->{activity_stream} = [ sort { $a->{time} <=> $b->{time} } @$stream ]; - } - elsif ($order eq 'newest_to_oldest') { - $self->{activity_stream} = [ sort { $b->{time} <=> $a->{time} } @$stream ]; - } - elsif ($order eq 'newest_to_oldest_desc_first') { - my $desc = shift @$stream; - $self->{activity_stream} = [ $desc, sort { $b->{time} <=> $a->{time} } @$stream ]; - } + my ($self) = @_; + if (!$self->{activity_stream}) { + my $stream = []; + _add_comments_to_stream($self, $stream); + _add_activities_to_stream($self, $stream); + _add_duplicates_to_stream($self, $stream); + + my $base_time = date_str_to_time($self->creation_ts); + foreach my $change_set (@$stream) { + $change_set->{id} + = $change_set->{comment} + ? 'c' . $change_set->{comment}->count + : 'a' . ($change_set->{time} - $base_time) . '_' . $change_set->{user_id}; + foreach my $activity (@{$change_set->{activity}}) { + $activity->{changes} + = [sort { $a->{fieldname} cmp $b->{fieldname} } @{$activity->{changes}}]; + } + } + my $order = Bugzilla->user->setting('comment_sort_order'); + if ($order eq 'oldest_to_newest') { + $self->{activity_stream} = [sort { $a->{time} <=> $b->{time} } @$stream]; } - return $self->{activity_stream}; + elsif ($order eq 'newest_to_oldest') { + $self->{activity_stream} = [sort { $b->{time} <=> $a->{time} } @$stream]; + } + elsif ($order eq 'newest_to_oldest_desc_first') { + my $desc = shift @$stream; + $self->{activity_stream} = [$desc, sort { $b->{time} <=> $a->{time} } @$stream]; + } + } + return $self->{activity_stream}; } sub find_activity_id_for_attachment { - my ($self, $attachment) = @_; - my $attach_id = $attachment->id; - my $stream = $self->activity_stream; - foreach my $change_set (@$stream) { - next unless exists $change_set->{attach_id}; - return $change_set->{id} if $change_set->{attach_id} == $attach_id; - } - return undef; + my ($self, $attachment) = @_; + my $attach_id = $attachment->id; + my $stream = $self->activity_stream; + foreach my $change_set (@$stream) { + next unless exists $change_set->{attach_id}; + return $change_set->{id} if $change_set->{attach_id} == $attach_id; + } + return undef; } sub find_activity_id_for_flag { - my ($self, $flag) = @_; - my $flagtype_name = $flag->type->name; - my $date = $flag->modification_date; - my $setter_id = $flag->setter->id; - my $stream = $self->activity_stream; - - # unfortunately bugs_activity treats all flag changes as the same field, so - # we don't have an object_id to match on - - if (!exists $self->{activity_cache}->{flag}->{$flag->id}) { - foreach my $change_set (reverse @$stream) { - foreach my $activity (@{ $change_set->{activity} }) { - # match by user, timestamp, and flag-type name - next unless - $activity->{who}->id == $setter_id - && $activity->{when} eq $date; - foreach my $change (@{ $activity->{changes} }) { - next unless - $change->{fieldname} eq 'flagtypes.name' - && $change->{flagtype_name} eq $flagtype_name; - $self->{activity_cache}->{flag}->{$flag->id} = $change_set->{id}; - return $change_set->{id}; - } - } + my ($self, $flag) = @_; + my $flagtype_name = $flag->type->name; + my $date = $flag->modification_date; + my $setter_id = $flag->setter->id; + my $stream = $self->activity_stream; + + # unfortunately bugs_activity treats all flag changes as the same field, so + # we don't have an object_id to match on + + if (!exists $self->{activity_cache}->{flag}->{$flag->id}) { + foreach my $change_set (reverse @$stream) { + foreach my $activity (@{$change_set->{activity}}) { + + # match by user, timestamp, and flag-type name + next unless $activity->{who}->id == $setter_id && $activity->{when} eq $date; + foreach my $change (@{$activity->{changes}}) { + next + unless $change->{fieldname} eq 'flagtypes.name' + && $change->{flagtype_name} eq $flagtype_name; + $self->{activity_cache}->{flag}->{$flag->id} = $change_set->{id}; + return $change_set->{id}; } - # if we couldn't find the flag in bugs_activity it means it was set - # during bug creation - $self->{activity_cache}->{flag}->{$flag->id} = 'c0'; + } } - return $self->{activity_cache}->{flag}->{$flag->id}; + + # if we couldn't find the flag in bugs_activity it means it was set + # during bug creation + $self->{activity_cache}->{flag}->{$flag->id} = 'c0'; + } + return $self->{activity_cache}->{flag}->{$flag->id}; } # comments are processed first, so there's no need to merge into existing entries sub _add_comment_to_stream { - my ($stream, $time, $user_id, $comment) = @_; - my $rh = { - time => $time, - user_id => $user_id, - comment => $comment, - activity => [], - }; - if ($comment->type == CMT_ATTACHMENT_CREATED || $comment->type == CMT_ATTACHMENT_UPDATED) { - $rh->{attach_id} = $comment->extra_data; - } - push @$stream, $rh; + my ($stream, $time, $user_id, $comment) = @_; + my $rh + = {time => $time, user_id => $user_id, comment => $comment, activity => [],}; + if ( $comment->type == CMT_ATTACHMENT_CREATED + || $comment->type == CMT_ATTACHMENT_UPDATED) + { + $rh->{attach_id} = $comment->extra_data; + } + push @$stream, $rh; } sub _add_activity_to_stream { - my ($stream, $time, $user_id, $data) = @_; - foreach my $entry (@$stream) { - next unless $entry->{time} == $time && $entry->{user_id} == $user_id; - $entry->{cc_only} = $entry->{cc_only} && $data->{cc_only}; - push @{ $entry->{activity} }, $data; - return; - } - push @$stream, { - time => $time, - user_id => $user_id, - comment => undef, - cc_only => $data->{cc_only}, - activity => [ $data ], + my ($stream, $time, $user_id, $data) = @_; + foreach my $entry (@$stream) { + next unless $entry->{time} == $time && $entry->{user_id} == $user_id; + $entry->{cc_only} = $entry->{cc_only} && $data->{cc_only}; + push @{$entry->{activity}}, $data; + return; + } + push @$stream, + { + time => $time, + user_id => $user_id, + comment => undef, + cc_only => $data->{cc_only}, + activity => [$data], }; } sub _add_comments_to_stream { - my ($bug, $stream) = @_; - my $user = Bugzilla->user; - my $treeherder_id = Bugzilla->treeherder_user->id; - - my $raw_comments = $bug->comments(); - foreach my $comment (@$raw_comments) { - next if $comment->type == CMT_HAS_DUPE; - my $author_id = $comment->author->id; - next if $comment->is_private && !($user->is_insider || $user->id == $author_id); - next if $comment->body eq '' && ($comment->work_time - 0) != 0 && $user->is_timetracker; - - # treeherder is so spammy we hide its comments by default - if ($author_id == $treeherder_id) { - $comment->{collapsed} = 1; - $comment->{collapsed_reason} = $comment->author->name; - } - if ($comment->type != CMT_ATTACHMENT_CREATED && $comment->count == 0 && length($comment->body) == 0) { - $comment->{collapsed} = 1; - $comment->{collapsed_reason} = 'empty'; - } - # If comment type is resolved as duplicate, do not add '...marked as duplicate...' string to comment body - if ($comment->type == CMT_DUPE_OF) { - $comment->set_type(0); - # Skip if user did not supply comment also - next if $comment->body eq ''; - } - - _add_comment_to_stream($stream, date_str_to_time($comment->creation_ts), $comment->author->id, $comment); + my ($bug, $stream) = @_; + my $user = Bugzilla->user; + my $treeherder_id = Bugzilla->treeherder_user->id; + + my $raw_comments = $bug->comments(); + foreach my $comment (@$raw_comments) { + next if $comment->type == CMT_HAS_DUPE; + my $author_id = $comment->author->id; + next if $comment->is_private && !($user->is_insider || $user->id == $author_id); + next + if $comment->body eq '' + && ($comment->work_time - 0) != 0 + && $user->is_timetracker; + + # treeherder is so spammy we hide its comments by default + if ($author_id == $treeherder_id) { + $comment->{collapsed} = 1; + $comment->{collapsed_reason} = $comment->author->name; } -} - -sub _add_activities_to_stream { - my ($bug, $stream) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # build bug activity - my ($raw_activity) = $bug->can('get_activity') - ? $bug->get_activity() - : Bugzilla::Bug::GetBugActivity($bug->id); - - # allow other extensions to alter history - Bugzilla::Hook::process('inline_history_activitiy', { activity => $raw_activity }); - - my %attachment_cache; - foreach my $attachment (@{$bug->attachments}) { - $attachment_cache{$attachment->id} = $attachment; + if ( $comment->type != CMT_ATTACHMENT_CREATED + && $comment->count == 0 + && length($comment->body) == 0) + { + $comment->{collapsed} = 1; + $comment->{collapsed_reason} = 'empty'; } - # build a list of bugs we need to check visibility of, so we can check with a single query - my %visible_bug_ids; +# If comment type is resolved as duplicate, do not add '...marked as duplicate...' string to comment body + if ($comment->type == CMT_DUPE_OF) { + $comment->set_type(0); - # envelope, augment and tweak - foreach my $operation (@$raw_activity) { - - # make operation.who an object - $operation->{who} = Bugzilla::User->new({ name => $operation->{who}, cache => 1 }); - - # we need to track operations which are just cc changes - $operation->{cc_only} = 1; - - for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { - my $change = $operation->{changes}->[$i]; - - # make an attachment object - if ($change->{attachid}) { - $change->{attach} = $attachment_cache{$change->{attachid}}; - } - - # empty resolutions are displayed as --- by default - # make it explicit here to enable correct display of the change - if ($change->{fieldname} eq 'resolution') { - $change->{removed} = '---' if $change->{removed} eq ''; - $change->{added} = '---' if $change->{added} eq ''; - } - - # make boolean fields true/false instead of 1/0 - my ($table, $field) = ('bugs', $change->{fieldname}); - if ($field =~ /^([^\.]+)\.(.+)$/) { - ($table, $field) = ($1, $2); - } - my $column = $dbh->bz_column_info($table, $field); - if ($column && $column->{TYPE} eq 'BOOLEAN') { - $change->{removed} = ''; - $change->{added} = $change->{added} ? 'true' : 'false'; - } - - # load field object (only required for custom fields), and set the - # field type for custom fields - my $field_obj; - if ($change->{fieldname} =~ /^cf_/) { - $field_obj = Bugzilla::Field->new({ name => $change->{fieldname}, cache => 1 }); - $change->{fieldtype} = $field_obj->type; - } + # Skip if user did not supply comment also + next if $comment->body eq ''; + } - # identify buglist changes - if ($change->{fieldname} eq 'blocked' || - $change->{fieldname} eq 'dependson' || - $change->{fieldname} eq 'dupe' || - ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID) - ) { - $change->{buglist} = 1; - foreach my $what (qw(removed added)) { - my @buglist = split(/[\s,]+/, $change->{$what}); - foreach my $id (@buglist) { - if ($id && $id =~ /^\d+$/) { - $visible_bug_ids{$id} = 1; - } - } - } - } + _add_comment_to_stream($stream, date_str_to_time($comment->creation_ts), + $comment->author->id, $comment); + } +} - # split see-also - if ($change->{fieldname} eq 'see_also') { - my $url_base = Bugzilla->localconfig->{urlbase}; - foreach my $f (qw( added removed )) { - my @values; - foreach my $value (split(/, /, $change->{$f})) { - my ($bug_id) = substr($value, 0, length($url_base)) eq $url_base - ? $value =~ /id=(\d+)$/ - : undef; - push @values, { - url => $value, - bug_id => $bug_id, - }; - } - $change->{$f} = \@values; - } +sub _add_activities_to_stream { + my ($bug, $stream) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # build bug activity + my ($raw_activity) + = $bug->can('get_activity') + ? $bug->get_activity() + : Bugzilla::Bug::GetBugActivity($bug->id); + + # allow other extensions to alter history + Bugzilla::Hook::process('inline_history_activitiy', + {activity => $raw_activity}); + + my %attachment_cache; + foreach my $attachment (@{$bug->attachments}) { + $attachment_cache{$attachment->id} = $attachment; + } + +# build a list of bugs we need to check visibility of, so we can check with a single query + my %visible_bug_ids; + + # envelope, augment and tweak + foreach my $operation (@$raw_activity) { + + # make operation.who an object + $operation->{who} + = Bugzilla::User->new({name => $operation->{who}, cache => 1}); + + # we need to track operations which are just cc changes + $operation->{cc_only} = 1; + + for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { + my $change = $operation->{changes}->[$i]; + + # make an attachment object + if ($change->{attachid}) { + $change->{attach} = $attachment_cache{$change->{attachid}}; + } + + # empty resolutions are displayed as --- by default + # make it explicit here to enable correct display of the change + if ($change->{fieldname} eq 'resolution') { + $change->{removed} = '---' if $change->{removed} eq ''; + $change->{added} = '---' if $change->{added} eq ''; + } + + # make boolean fields true/false instead of 1/0 + my ($table, $field) = ('bugs', $change->{fieldname}); + if ($field =~ /^([^\.]+)\.(.+)$/) { + ($table, $field) = ($1, $2); + } + my $column = $dbh->bz_column_info($table, $field); + if ($column && $column->{TYPE} eq 'BOOLEAN') { + $change->{removed} = ''; + $change->{added} = $change->{added} ? 'true' : 'false'; + } + + # load field object (only required for custom fields), and set the + # field type for custom fields + my $field_obj; + if ($change->{fieldname} =~ /^cf_/) { + $field_obj = Bugzilla::Field->new({name => $change->{fieldname}, cache => 1}); + $change->{fieldtype} = $field_obj->type; + } + + # identify buglist changes + if ( $change->{fieldname} eq 'blocked' + || $change->{fieldname} eq 'dependson' + || $change->{fieldname} eq 'dupe' + || ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID)) + { + $change->{buglist} = 1; + foreach my $what (qw(removed added)) { + my @buglist = split(/[\s,]+/, $change->{$what}); + foreach my $id (@buglist) { + if ($id && $id =~ /^\d+$/) { + $visible_bug_ids{$id} = 1; } + } + } + } + + # split see-also + if ($change->{fieldname} eq 'see_also') { + my $url_base = Bugzilla->localconfig->{urlbase}; + foreach my $f (qw( added removed )) { + my @values; + foreach my $value (split(/, /, $change->{$f})) { + my ($bug_id) + = substr($value, 0, length($url_base)) eq $url_base + ? $value =~ /id=(\d+)$/ + : undef; + push @values, {url => $value, bug_id => $bug_id,}; + } + $change->{$f} = \@values; + } + } + + # track cc-only + if ($change->{fieldname} ne 'cc') { + $operation->{cc_only} = 0; + } + + # split multiple flag changes (must be processed last) + # set $change->{flagtype_name} to make searching the activity + # stream for flag changes easier and quicker + if ($change->{fieldname} eq 'flagtypes.name') { + my @added = split(/, /, $change->{added}); + my @removed = split(/, /, $change->{removed}); + if (scalar(@added) <= 1 && scalar(@removed) <= 1) { + $change->{flagtype_name} = _extract_flagtype($added[0] || $removed[0]); + next; + } - # track cc-only - if ($change->{fieldname} ne 'cc') { - $operation->{cc_only} = 0; - } + # remove current change + splice(@{$operation->{changes}}, $i, 1); - # split multiple flag changes (must be processed last) - # set $change->{flagtype_name} to make searching the activity - # stream for flag changes easier and quicker - if ($change->{fieldname} eq 'flagtypes.name') { - my @added = split(/, /, $change->{added}); - my @removed = split(/, /, $change->{removed}); - if (scalar(@added) <= 1 && scalar(@removed) <= 1) { - $change->{flagtype_name} = _extract_flagtype($added[0] || $removed[0]); - next; - } - # remove current change - splice(@{$operation->{changes}}, $i, 1); - # restructure into added/removed for each flag - my %flags; - foreach my $flag (@added) { - $flags{$flag}{added} = $flag; - $flags{$flag}{removed} = ''; - } - foreach my $flag (@removed) { - $flags{$flag}{added} = ''; - $flags{$flag}{removed} = $flag; - } - # clone current change, modify and insert - foreach my $flag (sort keys %flags) { - my $flag_change = {}; - foreach my $key (keys %$change) { - $flag_change->{$key} = $change->{$key}; - } - $flag_change->{removed} = $flags{$flag}{removed}; - $flag_change->{added} = $flags{$flag}{added}; - $flag_change->{flagtype_name} = _extract_flagtype($flag); - splice(@{$operation->{changes}}, $i, 0, $flag_change); - } - $i--; - } + # restructure into added/removed for each flag + my %flags; + foreach my $flag (@added) { + $flags{$flag}{added} = $flag; + $flags{$flag}{removed} = ''; + } + foreach my $flag (@removed) { + $flags{$flag}{added} = ''; + $flags{$flag}{removed} = $flag; } - _add_activity_to_stream($stream, date_str_to_time($operation->{when}), $operation->{who}->id, $operation); + # clone current change, modify and insert + foreach my $flag (sort keys %flags) { + my $flag_change = {}; + foreach my $key (keys %$change) { + $flag_change->{$key} = $change->{$key}; + } + $flag_change->{removed} = $flags{$flag}{removed}; + $flag_change->{added} = $flags{$flag}{added}; + $flag_change->{flagtype_name} = _extract_flagtype($flag); + splice(@{$operation->{changes}}, $i, 0, $flag_change); + } + $i--; + } } - # prime the visible-bugs cache - $user->visible_bugs([keys %visible_bug_ids]); + _add_activity_to_stream( + $stream, + date_str_to_time($operation->{when}), + $operation->{who}->id, $operation + ); + } + + # prime the visible-bugs cache + $user->visible_bugs([keys %visible_bug_ids]); } sub _extract_flagtype { - my ($value) = @_; - return $value =~ /^(.+)[\?\-\+]/ ? $1 : undef; + my ($value) = @_; + return $value =~ /^(.+)[\?\-\+]/ ? $1 : undef; } # display 'duplicate of this bug' as an activity entry, not a comment sub _add_duplicates_to_stream { - my ($bug, $stream) = @_; - my $dbh = Bugzilla->dbh; + my ($bug, $stream) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT longdescs.who, - UNIX_TIMESTAMP(bug_when), " . - $dbh->sql_date_format('bug_when') . ", + UNIX_TIMESTAMP(bug_when), " . $dbh->sql_date_format('bug_when') . ", type, extra_data FROM longdescs @@ -360,19 +374,22 @@ sub _add_duplicates_to_stream { WHERE bug_id = ? AND (type = ? OR type = ?) ORDER BY bug_when "); - $sth->execute($bug->id, CMT_HAS_DUPE, CMT_DUPE_OF); - - while (my($who, $time, $when, $type, $dupe_id) = $sth->fetchrow_array) { - _add_activity_to_stream($stream, $time, $who, { - who => Bugzilla::User->new({ id => $who, cache => 1 }), - when => $when, - changes => [{ - fieldname => ($type == CMT_HAS_DUPE ? 'has_dupe' : 'dupe_of'), - added => $dupe_id, - buglist => 1, - }], - }); - } + $sth->execute($bug->id, CMT_HAS_DUPE, CMT_DUPE_OF); + + while (my ($who, $time, $when, $type, $dupe_id) = $sth->fetchrow_array) { + _add_activity_to_stream( + $stream, $time, $who, + { + who => Bugzilla::User->new({id => $who, cache => 1}), + when => $when, + changes => [{ + fieldname => ($type == CMT_HAS_DUPE ? 'has_dupe' : 'dupe_of'), + added => $dupe_id, + buglist => 1, + }], + } + ); + } } 1; diff --git a/extensions/BugModal/lib/MonkeyPatches.pm b/extensions/BugModal/lib/MonkeyPatches.pm index 54bd6e560..042dabc38 100644 --- a/extensions/BugModal/lib/MonkeyPatches.pm +++ b/extensions/BugModal/lib/MonkeyPatches.pm @@ -17,10 +17,10 @@ use warnings; use Bugzilla::User; sub treeherder_user { - return Bugzilla->process_cache->{treeherder_user} //= - Bugzilla::User->new({ name => 'tbplbot@gmail.com', cache => 1 }) - || Bugzilla::User->new({ name => 'orangefactor@bots.tld', cache => 1 }) - || Bugzilla::User->new(); + return Bugzilla->process_cache->{treeherder_user} + //= Bugzilla::User->new({name => 'tbplbot@gmail.com', cache => 1}) + || Bugzilla::User->new({name => 'orangefactor@bots.tld', cache => 1}) + || Bugzilla::User->new(); } package Bugzilla::Bug; @@ -32,10 +32,11 @@ use warnings; use Bugzilla::Attachment; sub active_attachments { - my ($self) = @_; - return [] if $self->{error}; - return $self->{active_attachments} //= Bugzilla::Attachment->get_attachments_by_bug( - $self, { exclude_obsolete => 1, preload => 1 }); + my ($self) = @_; + return [] if $self->{error}; + return $self->{active_attachments} + //= Bugzilla::Attachment->get_attachments_by_bug($self, + {exclude_obsolete => 1, preload => 1}); } 1; @@ -47,8 +48,8 @@ use strict; use warnings; sub is_image { - my ($self) = @_; - return substr($self->contenttype, 0, 6) eq 'image/'; + my ($self) = @_; + return substr($self->contenttype, 0, 6) eq 'image/'; } 1; diff --git a/extensions/BugModal/lib/Util.pm b/extensions/BugModal/lib/Util.pm index 6a453159e..b1d7068d8 100644 --- a/extensions/BugModal/lib/Util.pm +++ b/extensions/BugModal/lib/Util.pm @@ -21,19 +21,21 @@ use DateTime::TimeZone; use Time::Local qw(timelocal); sub date_str_to_time { - my ($date) = @_; - # avoid creating a DateTime object - if ($date =~ /^(\d{4})[\.\-](\d{2})[\.\-](\d{2}) (\d{2}):(\d{2}):(\d{2})$/) { - return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); - } - state $tz //= DateTime::TimeZone->new( name => 'local' ); - my $dt = datetime_from($date, $tz); - if (!$dt) { - # this should never happen - warn("invalid datetime '$date'"); - return undef; - } - return $dt->epoch; + my ($date) = @_; + + # avoid creating a DateTime object + if ($date =~ /^(\d{4})[\.\-](\d{2})[\.\-](\d{2}) (\d{2}):(\d{2}):(\d{2})$/) { + return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); + } + state $tz //= DateTime::TimeZone->new(name => 'local'); + my $dt = datetime_from($date, $tz); + if (!$dt) { + + # this should never happen + warn("invalid datetime '$date'"); + return undef; + } + return $dt->epoch; } 1; diff --git a/extensions/BugModal/lib/WebService.pm b/extensions/BugModal/lib/WebService.pm index b69d609dd..5f3308327 100644 --- a/extensions/BugModal/lib/WebService.pm +++ b/extensions/BugModal/lib/WebService.pm @@ -27,353 +27,357 @@ use Taint::Util qw(untaint); # these methods are much lighter than our public API calls sub rest_resources { - return [ - # return all the products accessible by the user. - # required by new-bug - qr{^/bug_modal/initial_field_values}, { - GET => { - method => 'initial_field_values' - }, + return [ + # return all the products accessible by the user. + # required by new-bug + qr{^/bug_modal/initial_field_values}, + {GET => {method => 'initial_field_values'},}, + + # return all the components pertaining to the product. + # required by new-bug + qr{^/bug_modal/product_info}, + { + GET => { + method => 'product_info', + params => sub { + return {product_name => Bugzilla->input_params->{product}}; }, + }, + }, - # return all the components pertaining to the product. - # required by new-bug - qr{^/bug_modal/product_info}, { - GET => { - method => 'product_info', - params => sub { - return { product_name => Bugzilla->input_params->{product} } - }, - }, + # return all the lazy-loaded data; kept in sync with the UI's + # requirements. + qr{^/bug_modal/edit/(\d+)$}, + { + GET => { + method => 'edit', + params => sub { + return {id => $_[0]}; }, + }, + }, - # return all the lazy-loaded data; kept in sync with the UI's - # requirements. - qr{^/bug_modal/edit/(\d+)$}, { - GET => { - method => 'edit', - params => sub { - return { id => $_[0] } - }, - }, + # returns pre-formatted html, enabling reuse of the user template + qr{^/bug_modal/cc/(\d+)$}, + { + GET => { + method => 'cc', + params => sub { + return {id => $_[0]}; }, + }, + }, - # returns pre-formatted html, enabling reuse of the user template - qr{^/bug_modal/cc/(\d+)$}, { - GET => { - method => 'cc', - params => sub { - return { id => $_[0] } - }, - }, - }, + # returns fields that require touching when the product is changed + qw{^/bug_modal/new_product/(\d+)$}, + { + GET => { + method => 'new_product', + params => sub { - # returns fields that require touching when the product is changed - qw{^/bug_modal/new_product/(\d+)$}, { - GET => { - method => 'new_product', - params => sub { - # products with slashes in their name means we have to grab - # the product from the query-string instead of the path - return { id => $_[0], product_name => Bugzilla->input_params->{product} } - }, - }, + # products with slashes in their name means we have to grab + # the product from the query-string instead of the path + return {id => $_[0], product_name => Bugzilla->input_params->{product}}; }, - ] + }, + }, + ]; } sub initial_field_values { - my $user = Bugzilla->user; - return { - products => _name($user->get_enterable_products), - keywords => _name([Bugzilla::Keyword->get_all()]), - }; + my $user = Bugzilla->user; + return { + products => _name($user->get_enterable_products), + keywords => _name([Bugzilla::Keyword->get_all()]), + }; } sub product_info { - my ( $self, $params ) = @_; - if ( !ref $params->{product_name} ) { - untaint( $params->{product_name} ); - } - else { - ThrowCodeError( 'params_required', { function => 'BugModal.components', params => ['product'] } ); - } - my $product = Bugzilla::Product->check( { name => $params->{product_name}, cache => 1 } ); - $product = Bugzilla->user->can_enter_product( $product, 1 ); - my @components = map { - { - name => $_->name, - description => $_->description, - } - } @{ $product->components }; - return { - components => \@components, - versions => _name($product->versions), - }; + my ($self, $params) = @_; + if (!ref $params->{product_name}) { + untaint($params->{product_name}); + } + else { + ThrowCodeError('params_required', + {function => 'BugModal.components', params => ['product']}); + } + my $product + = Bugzilla::Product->check({name => $params->{product_name}, cache => 1}); + $product = Bugzilla->user->can_enter_product($product, 1); + my @components = map { {name => $_->name, description => $_->description,} } + @{$product->components}; + return {components => \@components, versions => _name($product->versions),}; } # everything we need for edit mode in a single call, returning just the fields # that the ui requires. sub edit { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $bug = Bugzilla::Bug->check({ id => $params->{id} }); - - # the keys of the options hash must match the field id in the ui - my %options; - - my @products = @{ $user->get_enterable_products }; - unless (grep { $_->id == $bug->product_id } @products) { - unshift @products, $bug->product_obj; - } - $options{product} = [ map { { name => $_->name } } @products ]; - - $options{component} = _name($bug->product_obj->components, $bug->component); - $options{version} = _name($bug->product_obj->versions, $bug->version); - $options{target_milestone} = _name($bug->product_obj->milestones, $bug->target_milestone); - $options{priority} = _name('priority', $bug->priority); - $options{bug_severity} = _name('bug_severity', $bug->bug_severity); - $options{rep_platform} = _name('rep_platform', $bug->rep_platform); - $options{op_sys} = _name('op_sys', $bug->op_sys); - - # custom select fields - my @custom_fields = - grep { $_->type == FIELD_TYPE_SINGLE_SELECT || $_->type == FIELD_TYPE_MULTI_SELECT } - Bugzilla->active_custom_fields({ product => $bug->product_obj, component => $bug->component_obj }); - foreach my $field (@custom_fields) { - my $field_name = $field->name; - my @values = map { { name => $_->name } } - grep { $bug->$field_name eq $_->name - || ($_->is_active - && $bug->check_can_change_field($field_name, $bug->$field_name, $_->name)) } - @{ $field->legal_values }; - $options{$field_name} = \@values; - } - - # keywords - my @keywords = grep { $_->is_active } Bugzilla::Keyword->get_all(); - - # results - return { - options => \%options, - keywords => [ map { $_->name } @keywords ], - }; + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $bug = Bugzilla::Bug->check({id => $params->{id}}); + + # the keys of the options hash must match the field id in the ui + my %options; + + my @products = @{$user->get_enterable_products}; + unless (grep { $_->id == $bug->product_id } @products) { + unshift @products, $bug->product_obj; + } + $options{product} = [map { {name => $_->name} } @products]; + + $options{component} = _name($bug->product_obj->components, $bug->component); + $options{version} = _name($bug->product_obj->versions, $bug->version); + $options{target_milestone} + = _name($bug->product_obj->milestones, $bug->target_milestone); + $options{priority} = _name('priority', $bug->priority); + $options{bug_severity} = _name('bug_severity', $bug->bug_severity); + $options{rep_platform} = _name('rep_platform', $bug->rep_platform); + $options{op_sys} = _name('op_sys', $bug->op_sys); + + # custom select fields + my @custom_fields = grep { + $_->type == FIELD_TYPE_SINGLE_SELECT + || $_->type == FIELD_TYPE_MULTI_SELECT + } Bugzilla->active_custom_fields( + {product => $bug->product_obj, component => $bug->component_obj}); + foreach my $field (@custom_fields) { + my $field_name = $field->name; + my @values = map { {name => $_->name} } grep { + $bug->$field_name eq $_->name + || ($_->is_active + && $bug->check_can_change_field($field_name, $bug->$field_name, $_->name)) + } @{$field->legal_values}; + $options{$field_name} = \@values; + } + + # keywords + my @keywords = grep { $_->is_active } Bugzilla::Keyword->get_all(); + + # results + return {options => \%options, keywords => [map { $_->name } @keywords],}; } sub _name { - my ($values, $current) = @_; - # values can either be an array-ref of values, or a field name, which - # result in that field's legal-values being used. - if (!ref($values)) { - $values = Bugzilla::Field->new({ name => $values, cache => 1 })->legal_values; - } - return [ - map { { name => $_->name } } - grep { (defined $current && $_->name eq $current) || $_->is_active } - @$values - ]; + my ($values, $current) = @_; + + # values can either be an array-ref of values, or a field name, which + # result in that field's legal-values being used. + if (!ref($values)) { + $values = Bugzilla::Field->new({name => $values, cache => 1})->legal_values; + } + return [map { {name => $_->name} } + grep { (defined $current && $_->name eq $current) || $_->is_active } + @$values]; } sub cc { - my ($self, $params) = @_; - my $template = Bugzilla->template; - my $bug = Bugzilla::Bug->check({ id => $params->{id} }); - my $vars = { - bug => $bug, - cc_list => [ - sort { lc($a->identity) cmp lc($b->identity) } - @{ $bug->cc_users } - ] - }; - - my $html = ''; - $template->process('bug_modal/cc_list.html.tmpl', $vars, \$html) - || ThrowTemplateError($template->error); - return { html => $html }; + my ($self, $params) = @_; + my $template = Bugzilla->template; + my $bug = Bugzilla::Bug->check({id => $params->{id}}); + my $vars = { + bug => $bug, + cc_list => [sort { lc($a->identity) cmp lc($b->identity) } @{$bug->cc_users}] + }; + + my $html = ''; + $template->process('bug_modal/cc_list.html.tmpl', $vars, \$html) + || ThrowTemplateError($template->error); + return {html => $html}; } sub new_product { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $bug = Bugzilla::Bug->check({ id => $params->{id} }); - my $product = Bugzilla::Product->check({ name => $params->{product_name}, cache => 1 }); - my $true = $self->type('boolean', 1); - my %result; - - # components - - my $components = _name($product->components); - my $current_component = $bug->component; - if (my $component = first_value { $_->{name} eq $current_component} @$components) { - # identical component in both products - $component->{selected} = $true; - } - else { - # default to a blank value - unshift @$components, { - name => '', - selected => $true, - }; - } - $result{component} = $components; - - # milestones - - my $milestones = _name($product->milestones); - my $current_milestone = $bug->target_milestone; - if ($bug->check_can_change_field('target_milestone', 0, 1) - && (my $milestone = first_value { $_->{name} eq $current_milestone} @$milestones)) - { - # identical milestone in both products - $milestone->{selected} = $true; - } - else { - # use default milestone - my $default_milestone = $product->default_milestone; - my $milestone = first_value { $_->{name} eq $default_milestone } @$milestones; - $milestone->{selected} = $true; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $bug = Bugzilla::Bug->check({id => $params->{id}}); + my $product + = Bugzilla::Product->check({name => $params->{product_name}, cache => 1}); + my $true = $self->type('boolean', 1); + my %result; + + # components + + my $components = _name($product->components); + my $current_component = $bug->component; + if (my $component + = first_value { $_->{name} eq $current_component } @$components) + { + # identical component in both products + $component->{selected} = $true; + } + else { + # default to a blank value + unshift @$components, {name => '', selected => $true,}; + } + $result{component} = $components; + + # milestones + + my $milestones = _name($product->milestones); + my $current_milestone = $bug->target_milestone; + if ($bug->check_can_change_field('target_milestone', 0, 1) + && (my $milestone + = first_value { $_->{name} eq $current_milestone } @$milestones)) + { + # identical milestone in both products + $milestone->{selected} = $true; + } + else { + # use default milestone + my $default_milestone = $product->default_milestone; + my $milestone = first_value { $_->{name} eq $default_milestone } @$milestones; + $milestone->{selected} = $true; + } + $result{target_milestone} = $milestones; + + # versions + + my $versions = _name($product->versions); + my $current_version = $bug->version; + my $selected_version; + if (my $version = first_value { $_->{name} eq $current_version } @$versions) { + + # identical version in both products + $version->{selected} = $true; + $selected_version = $version; + } + elsif ($current_version =~ /^(\d+) Branch$/ + || $current_version =~ /^Firefox (\d+)$/ + || $current_version =~ /^(\d+)$/) + { + # firefox, with its three version naming schemes + my $branch = $1; + foreach my $test_version ("$branch Branch", "Firefox $branch", $branch) { + if (my $version = first_value { $_->{name} eq $test_version } @$versions) { + $version->{selected} = $true; + $selected_version = $version; + last; + } } - $result{target_milestone} = $milestones; - - # versions + } + if (!$selected_version) { - my $versions = _name($product->versions); - my $current_version = $bug->version; - my $selected_version; - if (my $version = first_value { $_->{name} eq $current_version } @$versions) { - # identical version in both products + # "unspecified", "other" + foreach my $test_version ("unspecified", "other") { + if (my $version = first_value { lc($_->{name}) eq $test_version } @$versions) { $version->{selected} = $true; $selected_version = $version; + last; + } } - elsif ( - $current_version =~ /^(\d+) Branch$/ - || $current_version =~ /^Firefox (\d+)$/ - || $current_version =~ /^(\d+)$/) + } + if (!$selected_version) { + + # default to a blank value + unshift @$versions, {name => '', selected => $true,}; + } + $result{version} = $versions; + + # groups + + my @groups; + + # find invalid groups + push @groups, + map { {type => 'invalid', group => $_, checked => 0,} } + @{Bugzilla::Bug->get_invalid_groups( + {bug_ids => [$bug->id], product => $product})}; + + # logic lifted from bug/process/verify-new-product.html.tmpl + my $current_groups = $bug->groups_in; + my $group_controls = $product->group_controls; + foreach my $group_id (keys %$group_controls) { + my $group_control = $group_controls->{$group_id}; + if ( + $group_control->{membercontrol} == CONTROLMAPMANDATORY + || ($group_control->{othercontrol} == CONTROLMAPMANDATORY + && !$user->in_group($group_control->{name})) + ) { - # firefox, with its three version naming schemes - my $branch = $1; - foreach my $test_version ("$branch Branch", "Firefox $branch", $branch) { - if (my $version = first_value { $_->{name} eq $test_version } @$versions) { - $version->{selected} = $true; - $selected_version = $version; - last; - } - } - } - if (!$selected_version) { - # "unspecified", "other" - foreach my $test_version ("unspecified", "other") { - if (my $version = first_value { lc($_->{name}) eq $test_version } @$versions) { - $version->{selected} = $true; - $selected_version = $version; - last; - } - } - } - if (!$selected_version) { - # default to a blank value - unshift @$versions, { - name => '', - selected => $true, - }; - } - $result{version} = $versions; - - # groups - - my @groups; - - # find invalid groups - push @groups, - map {{ - type => 'invalid', - group => $_, - checked => 0, - }} - @{ Bugzilla::Bug->get_invalid_groups({ bug_ids => [ $bug->id ], product => $product }) }; - - # logic lifted from bug/process/verify-new-product.html.tmpl - my $current_groups = $bug->groups_in; - my $group_controls = $product->group_controls; - foreach my $group_id (keys %$group_controls) { - my $group_control = $group_controls->{$group_id}; - if ($group_control->{membercontrol} == CONTROLMAPMANDATORY - || ($group_control->{othercontrol} == CONTROLMAPMANDATORY && !$user->in_group($group_control->{name}))) - { - # mandatory, always checked - push @groups, { - type => 'mandatory', - group => $group_control->{group}, - checked => 1, - }; - } - elsif ( - ($group_control->{membercontrol} != CONTROLMAPNA && $user->in_group($group_control->{name})) - || $group_control->{othercontrol} != CONTROLMAPNA) - { - # optional, checked if.. - my $group = $group_control->{group}; - my $checked = - # same group as current product - (any { $_->id == $group->id } @$current_groups) - # member default - || $group_control->{membercontrol} == CONTROLMAPDEFAULT && $user->in_group($group_control->{name}) - # or other default - || $group_control->{othercontrol} == CONTROLMAPDEFAULT && !$user->in_group($group_control->{name}) - ; - push @groups, { - type => 'optional', - group => $group_control->{group}, - checked => $checked || 0, - }; - } + # mandatory, always checked + push @groups, + {type => 'mandatory', group => $group_control->{group}, checked => 1,}; } + elsif ( + ( + $group_control->{membercontrol} != CONTROLMAPNA + && $user->in_group($group_control->{name}) + ) + || $group_control->{othercontrol} != CONTROLMAPNA + ) + { + # optional, checked if.. + my $group = $group_control->{group}; + my $checked = - my $default_group_name = $product->default_security_group; - if (my $default_group = first_value { $_->{group}->name eq $default_group_name } @groups) { - # because we always allow the default product group to be selected, it's never invalid - $default_group->{type} = 'optional' if $default_group->{type} eq 'invalid'; - } - else { - # add the product's default group if it's missing - unshift @groups, { - type => 'optional', - group => $product->default_security_group_obj, - checked => 0, - }; - } + # same group as current product + (any { $_->id == $group->id } @$current_groups) - # if the bug is currently in a group, ensure a group is checked by default - # by checking the product's default group if no other groups apply - if (@$current_groups && !any { $_->{checked} } @groups) { - foreach my $g (@groups) { - next unless $g->{group}->name eq $default_group_name; - $g->{checked} = 1; - last; - } - } + # member default + || $group_control->{membercontrol} == CONTROLMAPDEFAULT + && $user->in_group($group_control->{name}) - # group by type and flatten - my $vars = { - product => $product, - groups => { invalid => [], mandatory => [], optional => [] }, - }; - foreach my $g (@groups) { - push @{ $vars->{groups}->{$g->{type}} }, { - id => $g->{group}->id, - name => $g->{group}->name, - description => $g->{group}->description, - checked => $g->{checked}, + # or other default + || $group_control->{othercontrol} == CONTROLMAPDEFAULT + && !$user->in_group($group_control->{name}); + push @groups, + { + type => 'optional', + group => $group_control->{group}, + checked => $checked || 0, }; } - - # build group selection html - my $template = Bugzilla->template; - $template->process('bug_modal/new_product_groups.html.tmpl', $vars, \$result{groups}) - || ThrowTemplateError($template->error); - - return \%result; + } + + my $default_group_name = $product->default_security_group; + if (my $default_group + = first_value { $_->{group}->name eq $default_group_name } @groups) + { +# because we always allow the default product group to be selected, it's never invalid + $default_group->{type} = 'optional' if $default_group->{type} eq 'invalid'; + } + else { + # add the product's default group if it's missing + unshift @groups, + { + type => 'optional', + group => $product->default_security_group_obj, + checked => 0, + }; + } + + # if the bug is currently in a group, ensure a group is checked by default + # by checking the product's default group if no other groups apply + if (@$current_groups && !any { $_->{checked} } @groups) { + foreach my $g (@groups) { + next unless $g->{group}->name eq $default_group_name; + $g->{checked} = 1; + last; + } + } + + # group by type and flatten + my $vars = { + product => $product, + groups => {invalid => [], mandatory => [], optional => []}, + }; + foreach my $g (@groups) { + push @{$vars->{groups}->{$g->{type}}}, + { + id => $g->{group}->id, + name => $g->{group}->name, + description => $g->{group}->description, + checked => $g->{checked}, + }; + } + + # build group selection html + my $template = Bugzilla->template; + $template->process('bug_modal/new_product_groups.html.tmpl', + $vars, \$result{groups}) + || ThrowTemplateError($template->error); + + return \%result; } 1; |