Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 96 additions & 1 deletion Bugzilla/WebService/Bug.pm
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ sub comments {
}

my %bugs;
my @bug_comments;
foreach my $bug_id (@$bug_ids) {
my $bug;

Expand All @@ -356,6 +357,15 @@ sub comments {
my $comments
= $bug->comments({order => 'oldest_to_newest', after => $params->{new_since}
});
push(@bug_comments, [$bug, $comments]);
}

# Preload every requested bug's comments in one query, otherwise a bulk
# request would run one aggregation query per bug.
$self->_preload_comment_edit_info([map { @{$_->[1]} } @bug_comments]);

foreach my $bug_comment (@bug_comments) {
my ($bug, $comments) = @$bug_comment;
my @result;
foreach my $comment (@$comments) {
next if $comment->is_private && !$user->is_insider;
Expand All @@ -381,6 +391,8 @@ sub comments {
my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data;
Bugzilla::Bug->check($_) foreach (keys %got_bug_ids);

$self->_preload_comment_edit_info($comment_data);

foreach my $comment (@$comment_data) {
if ($comment->is_private && !$user->is_insider) {
ThrowUserError('comment_is_private', {id => $comment->id});
Expand Down Expand Up @@ -411,6 +423,58 @@ sub render_comment {
return {html => $html};
}

# Helper for every method that returns comments. Fetches the revision metadata
# (number of edits and the timestamp of the most recent edit) for a set of
# comments using a single query, and stashes it on each comment object for
# _translate_comment.
#
# The data is only exposed to users who are allowed to edit other people's
# comments; everyone else gets no edit_count/last_change_time keys at all.
# Revisions hidden by an edit-comments admin are only counted for admins.
#
# Comments that were already preloaded are skipped, so callers that batch every
# comment of a request up front turn the later per-bug calls into no-ops.
sub _preload_comment_edit_info {
my ($self, $comments) = @_;
my $user = Bugzilla->user;

# can_edit_comments is injected by the EditComments extension; without it
# there is no longdescs_activity table to query.
return unless $user->can('can_edit_comments') && $user->can_edit_comments;

my @todo = grep { !exists $_->{edit_info} } @$comments;
return unless @todo;

my $dbh = Bugzilla->dbh;
my @ids = map { $_->id } @todo;

# Admins can see hidden revisions, so they are counted for them only.
my $hidden_clause = $user->is_edit_comments_admin ? '' : 'AND is_hidden = 0';

my $rows = $dbh->selectall_hashref(
'SELECT comment_id, COUNT(*) AS edit_count,
MAX(change_when) AS last_change_time
FROM longdescs_activity
WHERE ' . $dbh->sql_in('comment_id', \@ids) . " $hidden_clause
GROUP BY comment_id", 'comment_id'
);

foreach my $comment (@todo) {
$comment->{edit_info} = $rows->{$comment->id}
|| {edit_count => 0, last_change_time => undef};
}
}

# Batch-preloads the comment edit info for every bug about to be passed through
# _bug_to_hash(), so that a multi-bug request runs one aggregation query rather
# than one per bug.
sub _preload_bugs_comment_edit_info {
my ($self, $bugs, $params) = @_;

return unless filter_wants $params, 'comments', ['extra'];
$self->_preload_comment_edit_info([map { @{$_->comments} } @$bugs]);
}

# Helper for Bug.comments
sub _translate_comment {
my ($self, $comment, $filters, $types, $prefix) = @_;
Expand All @@ -430,6 +494,13 @@ sub _translate_comment {
count => $self->type('int', $comment->count),
};

# Only set by _preload_comment_edit_info when the user may edit others' comments
if (my $edit_info = $comment->{edit_info}) {
$comment_hash->{edit_count} = $self->type('int', $edit_info->{edit_count});
$comment_hash->{last_change_time}
= $self->type('dateTime', $edit_info->{last_change_time});
}

if (Bugzilla->params->{use_comment_reactions}) {
$comment_hash->{reactions} = {};

Expand Down Expand Up @@ -481,9 +552,11 @@ sub get {
$bug = Bugzilla::Bug->check($bug_id);
}
push(@bugs, $bug);
push(@hashes, $self->_bug_to_hash($bug, $params));
}

$self->_preload_bugs_comment_edit_info(\@bugs, $params);
@hashes = map { $self->_bug_to_hash($_, $params) } @bugs;

# Set the ETag before inserting the update tokens
# since the tokens will always be unique even if
# the data has not changed.
Expand Down Expand Up @@ -712,6 +785,7 @@ sub search {
my %bug_objects
= map { $_->id => $_ } @{Bugzilla::Bug->new_from_list(\@bug_ids)};
my @bugs = map { $bug_objects{$_} } @bug_ids;
$self->_preload_bugs_comment_edit_info(\@bugs, $params);
@bugs = map { $self->_bug_to_hash($_, $params) } @bugs;

return {bugs => \@bugs};
Expand Down Expand Up @@ -765,6 +839,7 @@ sub possible_duplicates {
@$possible_dupes = grep { $_->id != $params->{id} } @$possible_dupes;
}

$self->_preload_bugs_comment_edit_info($possible_dupes, $params);
my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
$self->_add_update_tokens($params, $possible_dupes, \@hashes);
return {bugs => \@hashes};
Expand Down Expand Up @@ -1600,6 +1675,7 @@ sub _bug_to_hash {
my @result;
my $comments
= $bug->comments({order => 'oldest_to_newest', after => $params->{new_since}});
$self->_preload_comment_edit_info($comments);
foreach my $comment (@$comments) {
next if $comment->is_private && !$user->is_insider;
push(@result,
Expand Down Expand Up @@ -2782,6 +2858,23 @@ may be deprecated and removed in a future release.
C<boolean> True if this comment is private (only visible to a certain
group called the "insidergroup"), False otherwise.

=item edit_count

C<int> The number of times this comment has been edited. C<0> if the comment
has never been edited.

This key is only present for users who are allowed to edit other people's
comments. Revisions that have been hidden by an edit-comments admin are only
counted for members of the edit-comments admins group.

=item last_change_time

C<dateTime> The time (in Bugzilla's timezone) of the most recent edit to this
comment, or null if the comment has never been edited.

This key is only present for users who are allowed to edit other people's
comments, and follows the same rules as C<edit_count> for hidden revisions.

=back

=item B<Errors>
Expand Down Expand Up @@ -2824,6 +2917,8 @@ C<creator>.

=item C<raw_text> was added in Bugzilla B<6.0>.

=item C<edit_count> and C<last_change_time> were added in Bugzilla B<6.0>.

=back


Expand Down
85 changes: 51 additions & 34 deletions docs/en/rst/api/core/v1/comment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,40 +82,57 @@ and the value is the comment. (The format of comments is described below.)

A "comment" as described above is a object that contains the following items:

============= ======== ========================================================
name type description
============= ======== ========================================================
id int The globally unique ID for the comment.
bug_id int The ID of the bug that this comment is on.
attachment_id int If the comment was made on an attachment, this will be
the ID of that attachment. Otherwise it will be null.
count int The number of the comment local to the bug. The
Description is 0, comments start with 1.
text string The body of the comment, including any special text
(such as "this bug was marked as a duplicate of...").
raw_text string The body of the comment without any special additional
text.
creator string The login name of the comment's author.
time datetime The time (in Bugzilla's timezone) that the comment was
added.
creation_time datetime This is exactly same as the ``time`` key. Use this
field instead of ``time`` for consistency with other
methods including :ref:`rest_single_bug` and
:ref:`rest_attachments`.

For compatibility, ``time`` is still usable. However,
please note that ``time`` may be deprecated and removed
in a future release.

is_private boolean ``true`` if this comment is private (only visible to a
certain group called the "insidergroup"), ``false``
otherwise.
is_markdown boolean ``true`` if this comment is markdown. ``false`` if this
comment is plaintext.
reactions object An object containing reacted emoji names and
corresponding counts. To retrieve reacted users, use
:ref:`rest_get_comment_reactions`.
============= ======== ========================================================
================ ======== =====================================================
name type description
================ ======== =====================================================
id int The globally unique ID for the comment.
bug_id int The ID of the bug that this comment is on.
attachment_id int If the comment was made on an attachment, this will
be the ID of that attachment. Otherwise it will be
null.
count int The number of the comment local to the bug. The
Description is 0, comments start with 1.
text string The body of the comment, including any special text
(such as "this bug was marked as a duplicate of...").
raw_text string The body of the comment without any special
additional text.
creator string The login name of the comment's author.
time datetime The time (in Bugzilla's timezone) that the comment
was added.
creation_time datetime This is exactly same as the ``time`` key. Use this
field instead of ``time`` for consistency with other
methods including :ref:`rest_single_bug` and
:ref:`rest_attachments`.

For compatibility, ``time`` is still usable.
However, please note that ``time`` may be deprecated
and removed in a future release.

is_private boolean ``true`` if this comment is private (only visible to
a certain group called the "insidergroup"),
``false`` otherwise.
is_markdown boolean ``true`` if this comment is markdown. ``false`` if
this comment is plaintext.
edit_count int The number of times this comment has been edited.
``0`` if the comment has never been edited.

Only present for users who are allowed to edit other
people's comments. Revisions hidden by an
edit-comments admin are only counted for members of
the edit-comments admins group.

last_change_time datetime The time (in Bugzilla's timezone) of the most recent
edit to this comment, or null if the comment has
never been edited.

Only present for users who are allowed to edit other
people's comments, and follows the same rules as
``edit_count`` for hidden revisions.

reactions object An object containing reacted emoji names and
corresponding counts. To retrieve reacted users, use
:ref:`rest_get_comment_reactions`.
================ ======== =====================================================

**Errors**

Expand Down
Loading
Loading