Aklapper has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/76105


Change subject: Add bmo extension InlineHistory
......................................................................

Add bmo extension InlineHistory

Displays bug report information shown on show_activity.cgi inline
as part of show_bug.cgi. This makes it easier to see which action
has been taken when by who (e.g. changes to priority, assignee,
or status).

Bug: 47256
Change-Id: I6efc6bd485e32b65b99f652074c325d2d623bfb9
---
A extensions/InlineHistory/Config.pm
A extensions/InlineHistory/Extension.pm
A extensions/InlineHistory/README
A 
extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl
A 
extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl
A 
extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl
A 
extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl
A extensions/InlineHistory/web/inline-history.js
A extensions/InlineHistory/web/style.css
9 files changed, 837 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/wikimedia/bugzilla/modifications 
refs/changes/05/76105/1

diff --git a/extensions/InlineHistory/Config.pm 
b/extensions/InlineHistory/Config.pm
new file mode 100644
index 0000000..3834bd8
--- /dev/null
+++ b/extensions/InlineHistory/Config.pm
@@ -0,0 +1,13 @@
+# 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::InlineHistory;
+use strict;
+
+use constant NAME => 'InlineHistory';
+
+__PACKAGE__->NAME;
diff --git a/extensions/InlineHistory/Extension.pm 
b/extensions/InlineHistory/Extension.pm
new file mode 100644
index 0000000..f761a9f
--- /dev/null
+++ b/extensions/InlineHistory/Extension.pm
@@ -0,0 +1,206 @@
+# 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::InlineHistory;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::User::Setting;
+use Bugzilla::Constants;
+use Bugzilla::Attachment;
+
+our $VERSION = '1.5';
+
+# don't show inline history for bugs with lots of changes
+use constant MAXIMUM_ACTIVITY_COUNT => 500;
+
+sub template_before_process {
+    my ($self, $args) = @_;
+    my $file = $args->{'file'};
+    my $vars = $args->{'vars'};
+
+    return if $file ne 'bug/edit.html.tmpl';
+
+    my $user = Bugzilla->user;
+    my $dbh = Bugzilla->dbh;
+    return unless $user->id && $user->settings->{'inline_history'}->{'value'} 
eq 'on';
+
+    # note: bug/edit.html.tmpl doesn't support multiple bugs
+    my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'};
+    my $bug_id = $bug->id;
+
+    # build bug activity
+    my ($activity) = Bugzilla::Bug::GetBugActivity($bug_id);
+    $activity = _add_duplicates($bug_id, $activity);
+
+    if (scalar @$activity > MAXIMUM_ACTIVITY_COUNT) {
+        $activity = [];
+        $vars->{'ih_activity'} = 0;
+        $vars->{'ih_activity_max'} = 1;
+        return;
+    }
+
+    # prime caches with objects already loaded
+    my %user_cache;
+    foreach my $comment (@{$bug->comments}) {
+        $user_cache{$comment->{author}->login} = $comment->{author};
+    }
+
+    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;
+
+    # augment and tweak
+    foreach my $operation (@$activity) {
+        # make operation.who an object
+        $user_cache{$operation->{who}} ||= Bugzilla::User->new({ name => 
$operation->{who} });
+        $operation->{who} = $user_cache{$operation->{who}};
+
+        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';
+            }
+
+            my $field_obj;
+            if ($change->{fieldname} =~ /^cf_/) {
+                $field_obj = Bugzilla::Field->new({ name => 
$change->{fieldname}, cache => 1 });
+            }
+
+            # 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 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--;
+            }
+        }
+    }
+
+    $user->visible_bugs([keys %visible_bug_ids]);
+
+    $vars->{'ih_activity'} = $activity;
+}
+
+sub _add_duplicates {
+    # insert 'is a dupe of this bug' comment to allow js to display
+    # as activity
+
+    my ($bug_id, $activity) = @_;
+
+    my $dbh = Bugzilla->dbh;
+    my $sth = $dbh->prepare("
+        SELECT profiles.login_name, " .
+               $dbh->sql_date_format('bug_when', '%Y.%m.%d %H:%i:%s') . ",
+               extra_data,
+               thetext
+          FROM longdescs
+               INNER JOIN profiles ON profiles.userid = longdescs.who
+         WHERE bug_id = ?
+               AND (
+                 type = ?
+                 OR thetext LIKE '%has been marked as a duplicate of this%'
+               )
+         ORDER BY bug_when
+    ");
+    $sth->execute($bug_id, CMT_HAS_DUPE);
+
+    while (my($who, $when, $dupe_id, $the_text) = $sth->fetchrow_array) {
+        if (!$dupe_id) {
+            next unless $the_text =~ / (\d+) has been marked as a duplicate of 
this/;
+            $dupe_id = $1;
+        }
+        my $entry = {
+            'when' => $when,
+            'who' => $who,
+            'changes' => [
+                {
+                    'removed' => '',
+                    'added' => $dupe_id,
+                    'attachid' => undef,
+                    'fieldname' => 'dupe',
+                    'dupe' => 1,
+                }
+            ],
+        };
+        push @$activity, $entry;
+    }
+
+    return [ sort { $a->{when} cmp $b->{when} } @$activity ];
+}
+
+sub install_before_final_checks {
+    my ($self, $args) = @_;
+    add_setting('inline_history', ['on', 'off'], 'off');
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/InlineHistory/README b/extensions/InlineHistory/README
new file mode 100644
index 0000000..f5aaf16
--- /dev/null
+++ b/extensions/InlineHistory/README
@@ -0,0 +1,10 @@
+InlineHistory inserts bug activity inline with the comments when viewing a bug.
+It was derived from the Bugzilla Tweaks Addon by Ehasn Akhgari.
+
+For technical and performance reasons it is only available to logged in users,
+and is enabled by a User Preference.
+
+It works with an unmodified install of Bugzilla 4.0 and 4.2.
+
+If you have modified your show_bug template, the javascript in
+web/inline-history.js may need to be updated to suit your installation.
diff --git 
a/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl
 
b/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl
new file mode 100644
index 0000000..1c47fd2
--- /dev/null
+++ 
b/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl
@@ -0,0 +1,152 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
+
+[% RETURN UNLESS ih_activity %]
+[%# this div exists to allow bugzilla-tweaks to detect when we're active %]
+<div id="inline-history-ext"></div>
+
+<script>
+  var ih_activity = new Array();
+  var ih_activity_flags = new Array();
+  var ih_activity_sort_order = '[% user.settings.comment_sort_order.value 
FILTER js %]';
+  [% FOREACH operation = ih_activity %]
+    var html = '';
+    [% has_cc = 0 %]
+    [% has_flag = 0 %]
+    [% changer_identity = operation.who.identity %]
+    [% changer_login = operation.who.login %]
+    [% change_date = operation.when FILTER time %]
+
+    [% FOREACH change = operation.changes %]
+      [%# track flag changes %]
+      [% IF change.fieldname == 'flagtypes.name' && change.added != '' %]
+        var item = new Array(5);
+        item[0] = '[% changer_login FILTER js %]';
+        item[1] = '[% change_date FILTER js %]';
+        item[2] = '[% change.attachid FILTER js %]';
+        item[3] = '[% change.added FILTER js %]';
+        item[4] = '[% changer_identity FILTER js %]';
+        ih_activity_flags.push(item);
+        [% has_flag = 1 %]
+      [% END %]
+
+      [%# wrap CC changes in a span for toggling visibility %]
+      [% IF change.fieldname == 'cc' %]
+        html += '<span class="ih_cc">';
+        [% has_cc = 1 %]
+      [% END %]
+
+      [%# make attachment changes better %]
+      [% IF change.attachid %]
+        html += '<a '
+                + 'href="attachment.cgi?id=[% change.attachid FILTER none 
%]&amp;action=edit" '
+                + 'title="[% change.attach.description FILTER html FILTER js 
%]" '
+                + 'class="[% "bz_obsolete" IF change.attach.isobsolete %]"'
+                + '>Attachment #[% change.attachid FILTER none %]</a> - ';
+      [% END %]
+
+      [%# buglists need to be displayed differently, as we shouldn't use 
strike-out %]
+      [% IF change.buglist %]
+        [% IF change.dupe %]
+          [% label = 'Duplicate of this ' _ terms.bug %]
+        [% ELSE %]
+          [% label = field_descs.${change.fieldname} %]
+        [% END %]
+        [% IF change.added != '' %]
+          html += '[% label FILTER js %]: ';
+          [% PROCESS add_change value = change.added %]
+        [% END %]
+        [% IF change.removed != '' %]
+          [% "html += '<br>';" IF change.added != '' %]
+          html += 'No longer [% label FILTER lcfirst FILTER js %]: ';
+          [% PROCESS add_change value = change.removed %]
+        [% END %]
+      [% ELSE %]
+        [% IF change.fieldname == 'longdescs.isprivate' %]
+          [%# reference the comment that was made private/public in the field 
label %]
+          html += '<a href="#c[% change.comment.count FILTER js %]">'
+                  + 'Comment [% change.comment.count FILTER js %]</a> is 
private: ';
+        [% ELSE %]
+          [%# normal label %]
+          html += '[% field_descs.${change.fieldname} FILTER js %]: ';
+        [% END %]
+        [% IF change.removed != '' %]
+          [% IF change.added == '' %]
+            html += '<span class="ih_deleted">';
+          [% END %]
+          [% PROCESS add_change value = change.removed %]
+          [% IF change.added == '' %]
+            html += '</span>';
+          [% ELSE %]
+            html += ' &rarr; ';
+          [% END %]
+        [% END %]
+        [% PROCESS add_change value = change.added %]
+      [% END %]
+      [% "html += '<br>';" UNLESS loop.last %]
+
+      [% IF change.fieldname == 'cc' %]
+        html += '</span>';
+      [% END %]
+    [% END %]
+
+    [% changer_id = operation.who.id %]
+    [% UNLESS user_cache.$changer_id %]
+      [% user_cache.$changer_id = BLOCK %]
+        [% INCLUDE global/user.html.tmpl who = operation.who %]
+      [% END %]
+    [% END %]
+
+    var item = new Array(7);
+    item[0] = '[% changer_login FILTER js %]';
+    item[1] = '[% change_date FILTER js %]';
+    item[2] = html;
+    item[3] = '<div class="bz_comment_head">'
+              + '<span class="bz_comment_user">'
+              + '[% user_cache.$changer_id FILTER js %]'
+              + '</span>'
+              + '<span class="bz_comment_time"> ' + item[1] + ' </span>'
+              + '</div>';
+    item[4] = [% IF has_cc && (operation.changes.size == 1) %]true[% ELSE 
%]false[% END %];
+    item[5] = [% IF change.dupe %][% change.added FILTER js %][% ELSE %]0[% 
END %];
+    item[6] = [% IF has_flag %]true[% ELSE %]false[% END %];
+    ih_activity[[% loop.index %]] = item;
+  [% END %]
+  inline_history.init();
+</script>
+
+[% BLOCK add_change %]
+  html += '[%~%]
+  [% IF change.fieldname == 'estimated_time' ||
+        change.fieldname == 'remaining_time' ||
+        change.fieldname == 'work_time' %]
+    [% PROCESS formattimeunit time_unit = value FILTER html FILTER js %]
+  [% ELSIF change.buglist %]
+    [% value FILTER bug_list_link FILTER js %]
+  [% ELSIF change.fieldname == 'bug_file_loc' %]
+    [%~%]<a href="[% value FILTER html FILTER js %]" target="_blank"
+      [%~ ' onclick="return inline_history.confirmUnsafeUrl(this.href)"'
+          UNLESS is_safe_url(value) %]>
+    [%~%][% value FILTER html FILTER js %]</a>
+  [% ELSIF change.fieldname == 'see_also' %]
+    [% FOREACH see_also = value.split(', ') %]
+      [%~%]<a href="[% see_also FILTER html FILTER js %]" target="_blank">
+      [%~%][% see_also FILTER html FILTER js %]</a>
+      [%- ", " IF NOT loop.last %]
+    [% END %]
+  [% ELSIF change.fieldname == 'assigned_to' ||
+           change.fieldname == 'reporter' ||
+           change.fieldname == 'qa_contact' ||
+           change.fieldname == 'cc' ||
+           change.fieldname == 'flagtypes.name' %]
+    [% value FILTER email FILTER js %]
+  [% ELSE %]
+    [% value FILTER html FILTER js %]
+  [% END %]
+  [%~ %]';
+[% END %]
diff --git 
a/extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl
 
b/extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl
new file mode 100644
index 0000000..133005f
--- /dev/null
+++ 
b/extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
+
+[% IF ih_activity_max %]
+<p>
+  <i>This [% terms.bug %] contains too many changes to be displayed inline.</i>
+</p>
+[% END %]
diff --git 
a/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl
 
b/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 0000000..7e54b83
--- /dev/null
+++ 
b/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
+
+[% IF user.id && user.settings.inline_history.value == "on" %]
+  [% style_urls.push('extensions/InlineHistory/web/style.css') %]
+  [% javascript_urls.push('extensions/InlineHistory/web/inline-history.js') %]
+[% END %]
diff --git 
a/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl
 
b/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 0000000..e1ff4c0
--- /dev/null
+++ 
b/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl
@@ -0,0 +1,11 @@
+[%# 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.
+  #%]
+
+[%
+  setting_descs.inline_history = "When viewing a $terms.bug, show all 
$terms.bug activity",
+%]
diff --git a/extensions/InlineHistory/web/inline-history.js 
b/extensions/InlineHistory/web/inline-history.js
new file mode 100644
index 0000000..0d38edf
--- /dev/null
+++ b/extensions/InlineHistory/web/inline-history.js
@@ -0,0 +1,385 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var inline_history = {
+  _ccDivs: null,
+  _hasAttachmentFlags: false,
+  _hasBugFlags: false,
+
+  init: function() {
+    Dom = YAHOO.util.Dom;
+
+    // remove 'has been marked as a duplicate of this bug' comments
+    var reDuplicate = /^\*\*\* \S+ \d+ has been marked as a duplicate of this/;
+    var reBugId = /show_bug\.cgi\?id=(\d+)/;
+    var comments = Dom.getElementsByClassName("bz_comment", 'div', 'comments');
+    for (var i = 1, il = comments.length; i < il; i++) {
+      var textDiv = Dom.getElementsByClassName('bz_comment_text', 'pre', 
comments[i]);
+      if (textDiv) {
+        var match = reDuplicate.exec(textDiv[0].textContent || 
textDiv[0].innerText);
+        if (match) {
+          // grab the comment and bug number from the element
+          var comment = comments[i];
+          var number = comment.id.substr(1);
+          var time = this.trim(Dom.getElementsByClassName('bz_comment_time', 
'span', comment)[0].innerHTML);
+          var dupeId = 0;
+          match = reBugId.exec(Dom.get('comment_text_' + number).innerHTML);
+          if (match)
+            dupeId = match[1];
+          // remove the element
+          comment.parentNode.removeChild(comment);
+          // update the html for the history item to include the comment number
+          if (dupeId == 0)
+            continue;
+          for (var j = 0, jl = ih_activity.length; j < jl; j++) {
+            var item = ih_activity[j];
+            if (item[5] == dupeId && item[1] == time) {
+              // insert comment number and link into the header
+              item[3] = item[3].substr(0, item[3].length - 6) // remove 
trailing </div>
+                // add comment number
+                + '<span class="bz_comment_number" id="c' + number + '">'
+                + '<a href="#c' + number + '">Comment ' + number + '</a>'
+                + '</span>'
+                + '</div>';
+              break;
+            }
+          }
+        }
+      }
+    }
+
+    // ensure new items are placed immediately after the last comment
+    var commentDivs = Dom.getElementsByClassName('bz_comment', 'div', 
'comments');
+    if (!commentDivs.length) return;
+    var lastCommentDiv = commentDivs[commentDivs.length - 1];
+
+    // insert activity into the correct location
+    var commentTimes = Dom.getElementsByClassName('bz_comment_time', 'span', 
'comments');
+    for (var i = 0, il = ih_activity.length; i < il; i++) {
+      var item = ih_activity[i];
+      // item[0] : who
+      // item[1] : when
+      // item[2] : change html
+      // item[3] : header html
+      // item[4] : bool; cc-only
+      // item[5] : int; dupe bug id (or 0)
+      // item[6] : bool; is flag
+      var user = item[0];
+      var time = item[1];
+
+      var reachedEnd = false;
+      var start_index = ih_activity_sort_order == 
'newest_to_oldest_desc_first' ? 1 : 0;
+      for (var j = start_index, jl = commentTimes.length; j < jl; j++) {
+        var commentHead = commentTimes[j].parentNode;
+        var mainUser = Dom.getElementsByClassName('email', 'a', 
commentHead)[0].href.substr(7);
+        var text = commentTimes[j].textContent || commentTimes[j].innerText;
+        var mainTime = this.trim(text);
+
+        if (ih_activity_sort_order == 'oldest_to_newest' ? time > mainTime : 
time < mainTime) {
+          if (j < commentTimes.length - 1) {
+            continue;
+          } else {
+            reachedEnd = true;
+          }
+        }
+
+        var inline = (mainUser == user && time == mainTime);
+        var currentDiv = document.createElement("div");
+
+        // place ih_cc class on parent container if it's the only child
+        var containerClass = '';
+        if (item[4]) {
+          item[2] = item[2].replace('"ih_cc"', '""');
+          containerClass = 'ih_cc';
+        }
+
+        if (inline) {
+          // assume that the change was made by the same user
+          commentHead.parentNode.appendChild(currentDiv);
+          currentDiv.innerHTML = item[2];
+          Dom.addClass(currentDiv, 'ih_inlinehistory');
+          Dom.addClass(currentDiv, containerClass);
+          if (item[6])
+            this.setFlagChangeID(item, commentHead.parentNode.id);
+
+        } else {
+          // the change was made by another user
+          if (!reachedEnd) {
+            var parentDiv = commentHead.parentNode;
+            var previous = this.previousElementSibling(parentDiv);
+            if (previous && previous.className.indexOf("ih_history") >= 0) {
+              currentDiv = this.previousElementSibling(parentDiv);
+            } else {
+              parentDiv.parentNode.insertBefore(currentDiv, parentDiv);
+            }
+          } else {
+            var parentDiv = commentHead.parentNode;
+            var next = this.nextElementSibling(parentDiv);
+            if (next && next.className.indexOf("ih_history") >= 0) {
+              currentDiv = this.nextElementSibling(parentDiv);
+            } else {
+              lastCommentDiv.parentNode.insertBefore(currentDiv, 
lastCommentDiv.nextSibling);
+            }
+          }
+
+          var itemHtml =  '<div class="ih_history_item ' + containerClass + '" 
'
+                          + 'id="h' + i + '">'
+                          + item[3] + item[2]
+                          + '</div>';
+
+          if (ih_activity_sort_order == 'oldest_to_newest') {
+            currentDiv.innerHTML = currentDiv.innerHTML + itemHtml;
+          } else {
+            currentDiv.innerHTML = itemHtml + currentDiv.innerHTML;
+          }
+          currentDiv.setAttribute("class", "bz_comment ih_history");
+          if (item[6])
+            this.setFlagChangeID(item, 'h' + i);
+        }
+        break;
+      }
+    }
+
+    // find comment blocks which only contain cc changes, shift the ih_cc
+    var historyDivs = Dom.getElementsByClassName('ih_history', 'div', 
'comments');
+    for (var i = 0, il = historyDivs.length; i < il; i++) {
+      var historyDiv = historyDivs[i];
+      var itemDivs = Dom.getElementsByClassName('ih_history_item', 'div', 
historyDiv);
+      var ccOnly = true;
+      for (var j = 0, jl = itemDivs.length; j < jl; j++) {
+        if (!Dom.hasClass(itemDivs[j], 'ih_cc')) {
+          ccOnly = false;
+          break;
+        }
+      }
+      if (ccOnly) {
+        for (var j = 0, jl = itemDivs.length; j < jl; j++) {
+          Dom.removeClass(itemDivs[j], 'ih_cc');
+        }
+        Dom.addClass(historyDiv, 'ih_cc');
+      }
+    }
+
+    if (this._hasAttachmentFlags)
+      this.linkAttachmentFlags();
+    if (this._hasBugFlags)
+      this.linkBugFlags();
+
+    ih_activity = undefined;
+    ih_activity_flags = undefined;
+
+    this._ccDivs = Dom.getElementsByClassName('ih_cc', '', 'comments');
+    this.hideCC();
+    YAHOO.util.Event.onDOMReady(this.addCCtoggler);
+  },
+
+  setFlagChangeID: function(changeItem, id) {
+    // put the ID for the change into ih_activity_flags
+    for (var i = 0, il = ih_activity_flags.length; i < il; i++) {
+      var flagItem = ih_activity_flags[i];
+      // flagItem[0] : who.login
+      // flagItem[1] : when
+      // flagItem[2] : attach id
+      // flagItem[3] : flag
+      // flagItem[4] : who.identity
+      // flagItem[5] : change div id
+      if (flagItem[0] == changeItem[0] && flagItem[1] == changeItem[1]) {
+        // store the div
+        flagItem[5] = id;
+        // tag that we have flags to process
+        if (flagItem[2]) {
+          this._hasAttachmentFlags = true;
+        } else {
+          this._hasBugFlags = true;
+        }
+        // don't break as there may be multiple flag changes at once
+      }
+    }
+  },
+
+  linkAttachmentFlags: function() {
+    var rows = Dom.get('attachment_table').getElementsByTagName('tr');
+    for (var i = 0, il = rows.length; i < il; i++) {
+
+      // deal with attachments with flags only
+      var tr = rows[i];
+      if (!tr.id || tr.id == 'a0')
+        continue;
+      var attachFlagTd = Dom.getElementsByClassName('bz_attach_flags', 'td', 
tr);
+      if (attachFlagTd.length == 0)
+        continue;
+      attachFlagTd = attachFlagTd[0];
+
+      // get the attachment id
+      var attachId = 0;
+      var anchors = tr.getElementsByTagName('a');
+      for (var j = 0, jl = anchors.length; j < jl; j++) {
+        var match = anchors[j].href.match(/attachment\.cgi\?id=(\d+)/);
+        if (match) {
+          attachId = match[1];
+          break;
+        }
+      }
+      if (!attachId)
+        continue;
+
+      var html = '';
+
+      // there may be multiple flags, split by <br>
+      var attachFlags = attachFlagTd.innerHTML.split('<br>');
+      for (var j = 0, jl = attachFlags.length; j < jl; j++) {
+        var match = 
attachFlags[j].match(/^\s*(<span.+\/span>):([^\?\-\+]+[\?\-\+])([\s\S]*)/);
+        if (!match) continue;
+        var setterSpan = match[1];
+        var flag = this.trim(match[2].replace('\u2011', '-', 'g'));
+        var requestee = this.trim(match[3]);
+        var requesteeLogin = '';
+
+        match = setterSpan.match(/title="([^"]+)"/);
+        if (!match) continue;
+        var setterIdentity = this.htmlDecode(match[1]);
+
+        if (requestee) {
+          match = requestee.match(/title="([^"]+)"/);
+          if (!match) continue;
+          requesteeLogin = this.htmlDecode(match[1]);
+          match = requesteeLogin.match(/<([^>]+)>/);
+          if (match)
+            requesteeLogin = match[1];
+        }
+
+        var flagValue = requestee ? flag + '(' + requesteeLogin + ')' : flag;
+        // find the id for this change
+        var found = false;
+        for (var k = 0, kl = ih_activity_flags.length; k < kl; k++) {
+          flagItem = ih_activity_flags[k];
+          if (
+            flagItem[2] == attachId
+            && flagItem[3] == flagValue
+            && flagItem[4] == setterIdentity
+          ) {
+            html +=
+              setterSpan + ': '
+              + '<a href="#' + flagItem[5] + '">' + flag + '</a> '
+              + requestee + '<br>';
+            found = true;
+            break;
+          }
+        }
+        if (!found) {
+          // something went wrong, insert the flag unlinked
+          html += attachFlags[j] + '<br>';
+        }
+      }
+
+      if (html)
+        attachFlagTd.innerHTML = html;
+    }
+  },
+
+  linkBugFlags: function() {
+    var flags = Dom.get('flags');
+    if (!flags) return;
+    var rows = flags.getElementsByTagName('tr');
+    for (var i = 0, il = rows.length; i < il; i++) {
+      var cells = rows[i].getElementsByTagName('td');
+      if (!cells[1]) continue;
+
+      var match = cells[0].innerHTML.match(/title="([^"]+)"/);
+      if (!match) continue;
+      var setterIdentity = this.htmlDecode(match[1]);
+
+      var flagValue = cells[2].getElementsByTagName('select');
+      if (!flagValue.length) continue;
+      flagValue = flagValue[0].value;
+
+      var flagLabel = cells[1].getElementsByTagName('label');
+      if (!flagLabel.length) continue;
+      flagLabel = flagLabel[0];
+      var flagName = this.trim(flagLabel.innerHTML).replace('\u2011', '-', 
'g');
+
+      for (var j = 0, jl = ih_activity_flags.length; j < jl; j++) {
+        flagItem = ih_activity_flags[j];
+        if (
+          !flagItem[2]
+          && flagItem[3] == flagName + flagValue
+          && flagItem[4] == setterIdentity
+        ) {
+          flagLabel.innerHTML = 
+            '<a href="#' + flagItem[5] + '">' + flagName + '</a>';
+          break;
+        }
+      }
+    }
+  },
+
+  hideCC: function() {
+    Dom.addClass(this._ccDivs, 'ih_hidden');
+  },
+
+  showCC: function() {
+    Dom.removeClass(this._ccDivs, 'ih_hidden');
+  },
+
+  addCCtoggler: function() {
+    var ul = Dom.getElementsByClassName('bz_collapse_expand_comments');
+    if (ul.length == 0)
+      return;
+    ul = ul[0];
+    var a = document.createElement('a');
+    a.href = 'javascript:void(0)';
+    a.id = 'ih_toggle_cc';
+    YAHOO.util.Event.addListener(a, 'click', function(e) {
+      if (Dom.get('ih_toggle_cc').innerHTML == 'Show CC Changes') {
+        a.innerHTML = 'Hide CC Changes';
+        inline_history.showCC();
+      } else {
+        a.innerHTML = 'Show CC Changes';
+        inline_history.hideCC();
+      }
+    });
+    a.innerHTML = 'Show CC Changes';
+    var li = document.createElement('li');
+    li.appendChild(a);
+    ul.appendChild(li);
+  },
+
+  confirmUnsafeUrl: function(url) {
+    return confirm(
+      'This is considered an unsafe URL and could possibly be harmful.\n'
+      + 'The full URL is:\n\n' + url + '\n\nContinue?');
+  },
+
+  previousElementSibling: function(el) {
+    if (el.previousElementSibling)
+      return el.previousElementSibling;
+    while (el = el.previousSibling) {
+      if (el.nodeType == 1)
+        return el;
+    }
+  },
+
+  nextElementSibling: function(el) {
+    if (el.nextElementSibling)
+      return el.nextElementSibling;
+    while (el = el.nextSibling) {
+      if (el.nodeType == 1)
+        return el;
+    }
+  },
+
+  htmlDecode: function(v) {
+    if (!v.match(/&/)) return v;
+    var e = document.createElement('textarea');
+    e.innerHTML = v;
+    return e.value;
+  },
+
+  trim: function(s) {
+    return s.replace(/^\s+|\s+$/g, '');
+  }
+}
diff --git a/extensions/InlineHistory/web/style.css 
b/extensions/InlineHistory/web/style.css
new file mode 100644
index 0000000..af76eba
--- /dev/null
+++ b/extensions/InlineHistory/web/style.css
@@ -0,0 +1,35 @@
+/* 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. */
+
+.ih_history {
+    background: none !important;
+    color: #444;
+}
+
+.ih_inlinehistory {
+    font-weight: normal;
+    font-size: small;
+    color: #444;
+    border-top: 1px dotted #C8C8BA;
+    padding-top: 5px;
+}
+
+.bz_comment.ih_history {
+    padding: 5px 5px 0px 5px
+}
+
+.ih_history_item {
+    margin-bottom: 5px;
+}
+
+.ih_hidden {
+    display: none;
+}
+
+.ih_deleted {
+    text-decoration: line-through;
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/76105
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I6efc6bd485e32b65b99f652074c325d2d623bfb9
Gerrit-PatchSet: 1
Gerrit-Project: wikimedia/bugzilla/modifications
Gerrit-Branch: master
Gerrit-Owner: Aklapper <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to