summaryrefslogtreecommitdiffstats
path: root/extensions/BugModal/lib
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/BugModal/lib')
-rw-r--r--extensions/BugModal/lib/ActivityStream.pm284
-rw-r--r--extensions/BugModal/lib/MonkeyPatches.pm38
-rw-r--r--extensions/BugModal/lib/WebService.pm138
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;