diff options
author | Byron Jones <glob@mozilla.com> | 2015-03-24 06:45:44 +0100 |
---|---|---|
committer | Byron Jones <glob@mozilla.com> | 2015-03-24 06:45:44 +0100 |
commit | 3ac701266452d3509776fe58f9e1b2b8e9f33c1e (patch) | |
tree | 88124baaadb529b1c9809f6b3fa20384c1870780 /extensions/BugModal/lib | |
parent | 11bd061970f8b9c98e6af43a4c8c7ca4bfff9eb3 (diff) | |
download | bugzilla-3ac701266452d3509776fe58f9e1b2b8e9f33c1e.tar.gz bugzilla-3ac701266452d3509776fe58f9e1b2b8e9f33c1e.tar.xz |
Bug 1096798: prototype modal show_bug view
Diffstat (limited to 'extensions/BugModal/lib')
-rw-r--r-- | extensions/BugModal/lib/ActivityStream.pm | 284 | ||||
-rw-r--r-- | extensions/BugModal/lib/MonkeyPatches.pm | 38 | ||||
-rw-r--r-- | extensions/BugModal/lib/WebService.pm | 138 |
3 files changed, 460 insertions, 0 deletions
diff --git a/extensions/BugModal/lib/ActivityStream.pm b/extensions/BugModal/lib/ActivityStream.pm new file mode 100644 index 000000000..dae6b8ba2 --- /dev/null +++ b/extensions/BugModal/lib/ActivityStream.pm @@ -0,0 +1,284 @@ +# 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::BugModal::ActivityStream; +1; + +package Bugzilla::Bug; +use strict; +use warnings; + +use Bugzilla::User; +use Bugzilla::Constants; +use Time::Local; + +# returns an arrayref containing all changes to the bug - comments, field +# changes, and duplicates +# [ +# { +# time => $unix_timestamp, +# user_id => actor user-id +# comment => optional, comment added +# id => unique identifier for this change-set +# activty => [ +# { +# who => user object +# when => time (string) +# changes => [ +# { +# fieldname => field name :) +# added => string +# removed => string +# } +# ... +# ] +# } +# ... +# ] +# }, +# ... +# ] + +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 = _sql_date_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}; + $change_set->{activity} = [ + sort { $a->{fieldname} cmp $b->{fieldname} } + @{ $change_set->{activity} } + ]; + } + $self->{activity_stream} = [ sort { $a->{time} <=> $b->{time} } @$stream ]; + } + return $self->{activity_stream}; +} + +# 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) = @_; + push @$stream, { + time => $time, + user_id => $user_id, + comment => $comment, + activity => [], + }; +} + +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; + push @{ $entry->{activity} }, $data; + return; + } + push @$stream, { + time => $time, + user_id => $user_id, + comment => undef, + activity => [ $data ], + }; +} + +sub _add_comments_to_stream { + my ($bug, $stream) = @_; + my $user = Bugzilla->user; + + my $raw_comments = $bug->comments(); + foreach my $comment (@$raw_comments) { + next if $comment->type == CMT_HAS_DUPE; + next if $comment->is_private && !($user->is_insider || $user->id == $comment->author->id); + next if $comment->body eq '' && ($comment->work_time - 0) != 0 && !$user->is_timetracker; + _add_comment_to_stream($stream, _sql_date_to_time($comment->creation_ts), $comment->author->id, $comment); + } +} + +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) { + # until we can toggle their visibility, skip CC changes + $operation->{changes} = [ grep { $_->{fieldname} ne 'cc' } @{ $operation->{changes} } ]; + next unless @{ $operation->{changes} }; + + # make operation.who an object + $operation->{who} = Bugzilla::User->new({ name => $operation->{who}, cache => 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 = correct_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; + } + } + + # split multiple flag changes (must be processed last) + if ($change->{fieldname} eq 'flagtypes.name') { + my @added = split(/, /, $change->{added}); + my @removed = split(/, /, $change->{removed}); + next if scalar(@added) <= 1 && scalar(@removed) <= 1; + # remove current change + splice(@{$operation->{changes}}, $i, 1); + # restructure into added/removed for each flag + my %flags; + foreach my $added (@added) { + my ($value, $name) = $added =~ /^((.+).)$/; + $flags{$name}{added} = $value; + $flags{$name}{removed} |= ''; + } + foreach my $removed (@removed) { + my ($value, $name) = $removed =~ /^((.+).)$/; + $flags{$name}{added} |= ''; + $flags{$name}{removed} = $value; + } + # 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}; + splice(@{$operation->{changes}}, $i, 0, $flag_change); + } + $i--; + } + } + + _add_activity_to_stream($stream, _sql_date_to_time($operation->{when}), $operation->{who}->id, $operation); + } + + # prime the visible-bugs cache + $user->visible_bugs([keys %visible_bug_ids]); +} + +# 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 $sth = $dbh->prepare(" + SELECT longdescs.who, + UNIX_TIMESTAMP(bug_when), " . + $dbh->sql_date_format('bug_when') . ", + extra_data + FROM longdescs + INNER JOIN profiles ON profiles.userid = longdescs.who + WHERE bug_id = ? AND type = ? + ORDER BY bug_when + "); + $sth->execute($bug->id, CMT_HAS_DUPE); + + while (my($who, $time, $when, $dupe_id) = $sth->fetchrow_array) { + _add_activity_to_stream($stream, $time, $who, { + who => Bugzilla::User->new({ id => $who, cache => 1 }), + when => $when, + changes => [{ + fieldname => 'duplicate', + added => $dupe_id, + buglist => 1, + }], + }); + } +} + +sub _sql_date_to_time { + my ($date) = @_; + $date =~ /^(\d{4})[\.\-](\d{2})[\.\-](\d{2}) (\d{2}):(\d{2}):(\d{2})$/ + or die "internal error: invalid date '$date'"; + return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); +} + +1; diff --git a/extensions/BugModal/lib/MonkeyPatches.pm b/extensions/BugModal/lib/MonkeyPatches.pm new file mode 100644 index 000000000..1d902b2a9 --- /dev/null +++ b/extensions/BugModal/lib/MonkeyPatches.pm @@ -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. + +package Bugzilla::Extension::BugModal::MonkeyPatches; +1; + +package Bugzilla::Bug; +use strict; +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 }); +} + +1; + +package Bugzilla::User; +use strict; +use warnings; + +sub moz_nick { + my ($self) = @_; + return $1 if $self->name =~ /:(.+?)\b/; + return $self->name if $self->name; + $self->login =~ /^([^\@]+)\@/; + return $1; +} + +1; diff --git a/extensions/BugModal/lib/WebService.pm b/extensions/BugModal/lib/WebService.pm new file mode 100644 index 000000000..4c8b6b001 --- /dev/null +++ b/extensions/BugModal/lib/WebService.pm @@ -0,0 +1,138 @@ +# 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::BugModal::WebService; +use strict; +use warnings; + +use base qw(Bugzilla::WebService); + +use Bugzilla::Bug; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Field; +use Bugzilla::Keyword; +use Bugzilla::Milestone; +use Bugzilla::Version; + +# these methods are much lighter than our public API calls + +sub rest_resources { + return [ + # 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] } + }, + }, + }, + ] +} + +# 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, description => $_->description } } @products ]; + + $options{component} = _name_desc($bug->component, $bug->product_obj->components); + $options{version} = _name($bug->version, $bug->product_obj->versions); + $options{target_milestone} = _name($bug->target_milestone, $bug->product_obj->milestones); + $options{priority} = _name($bug->priority, 'priority'); + $options{bug_severity} = _name($bug->bug_severity, 'bug_severity'); + $options{rep_platform} = _name($bug->rep_platform, 'rep_platform'); + $options{op_sys} = _name($bug->op_sys, '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; + $options{$field_name} = [ + map { { name => $_->name } } + grep { $bug->$field_name eq $_->name || $_->is_active } + @{ $field->legal_values } + ]; + } + + # keywords + my @keywords = Bugzilla::Keyword->get_all(); + + # results + return { + options => \%options, + keywords => [ map { $_->name } @keywords ], + }; +} + +sub _name { + my ($current, $values) = @_; + # 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 { $_->name eq $current || $_->is_active } + @$values + ]; +} + +sub _name_desc { + my ($current, $values) = @_; + if (!ref($values)) { + $values = Bugzilla::Field->new({ name => $values, cache => 1 })->legal_values; + } + return [ + map { { name => $_->name, description => $_->description } } + grep { $_->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 = { + cc_list => [ + sort { lc($a->moz_nick) cmp lc($b->moz_nick) } + @{ $bug->cc_users } + ] + }; + + my $html = ''; + $template->process('bug_modal/cc_list.html.tmpl', $vars, \$html) + || ThrowTemplateError($template->error); + return { html => $html }; +} + +1; |