Commit ba0765bf authored by Byron Jones's avatar Byron Jones

Bug 793963: add the ability to tag comments with arbitrary tags

r=dkl, a=glob
parent 0aade93c
......@@ -3822,7 +3822,7 @@ sub _bugs_in_order {
# Get the activity of a bug, starting from $starttime (if given).
# This routine assumes Bugzilla::Bug->check has been previously called.
sub get_activity {
my ($self, $attach_id, $starttime) = @_;
my ($self, $attach_id, $starttime, $include_comment_tags) = @_;
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
......@@ -3834,7 +3834,7 @@ sub get_activity {
if (defined $starttime) {
trick_taint($starttime);
push (@args, $starttime);
$datepart = "AND bugs_activity.bug_when > ?";
$datepart = "AND bug_when > ?";
}
my $attachpart = "";
......@@ -3854,7 +3854,7 @@ sub get_activity {
my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " .
$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') .
", bugs_activity.removed, bugs_activity.added, profiles.login_name,
" AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name,
bugs_activity.comment_id
FROM bugs_activity
$suppjoins
......@@ -3865,8 +3865,31 @@ sub get_activity {
WHERE bugs_activity.bug_id = ?
$datepart
$attachpart
$suppwhere
ORDER BY bugs_activity.bug_when, bugs_activity.id";
$suppwhere ";
if (Bugzilla->params->{'comment_taggers_group'}
&& $include_comment_tags
&& !$attach_id)
{
$query .= "
UNION ALL
SELECT 'comment_tag' AS name,
NULL AS attach_id," .
$dbh->sql_date_format('longdescs_tags_activity.bug_when', '%Y.%m.%d %H:%i:%s') . " AS bug_when,
longdescs_tags_activity.removed,
longdescs_tags_activity.added,
profiles.login_name,
longdescs_tags_activity.comment_id as comment_id
FROM longdescs_tags_activity
INNER JOIN profiles ON profiles.userid = longdescs_tags_activity.who
WHERE longdescs_tags_activity.bug_id = ?
$datepart
";
push @args, $self->id;
push @args, $starttime if defined $starttime;
}
$query .= "ORDER BY bug_when, comment_id";
my $list = $dbh->selectall_arrayref($query, undef, @args);
......
......@@ -13,11 +13,13 @@ use strict;
use parent qw(Bugzilla::Object);
use Bugzilla::Attachment;
use Bugzilla::Comment::TagWeights;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;
use List::Util qw(first);
use Scalar::Util qw(blessed);
###############################
......@@ -79,21 +81,90 @@ use constant VALIDATOR_DEPENDENCIES => {
sub update {
my $self = shift;
my $changes = $self->SUPER::update(@_);
$self->bug->_sync_fulltext( update_comments => 1);
my ($changes, $old_comment) = $self->SUPER::update(@_);
if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) {
$self->bug->_sync_fulltext( update_comments => 1);
}
my @old_tags = @{ $old_comment->tags };
my @new_tags = @{ $self->tags };
my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags);
if (@$removed_tags || @$added_tags) {
my $dbh = Bugzilla->dbh;
my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
my $sth_delete = $dbh->prepare(
"DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?"
);
my $sth_insert = $dbh->prepare(
"INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)"
);
my $sth_activity = $dbh->prepare(
"INSERT INTO longdescs_tags_activity
(bug_id, comment_id, who, bug_when, added, removed)
VALUES (?, ?, ?, ?, ?, ?)"
);
foreach my $tag (@$removed_tags) {
my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
if ($weighted) {
if ($weighted->weight == 1) {
$weighted->remove_from_db();
} else {
$weighted->set_weight($weighted->weight - 1);
$weighted->update();
}
}
trick_taint($tag);
$sth_delete->execute($self->id, $tag);
$sth_activity->execute(
$self->bug_id, $self->id, Bugzilla->user->id, $when, '', $tag);
}
foreach my $tag (@$added_tags) {
my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
if ($weighted) {
$weighted->set_weight($weighted->weight + 1);
$weighted->update();
} else {
Bugzilla::Comment::TagWeights->create({ tag => $tag, weight => 1 });
}
trick_taint($tag);
$sth_insert->execute($self->id, $tag);
$sth_activity->execute(
$self->bug_id, $self->id, Bugzilla->user->id, $when, $tag, '');
}
}
return $changes;
}
# Speeds up displays of comment lists by loading all ->author objects
# at once for a whole list.
# Speeds up displays of comment lists by loading all author objects and tags at
# once for a whole list.
sub preload {
my ($class, $comments) = @_;
# Author
my %user_ids = map { $_->{who} => 1 } @$comments;
my $users = Bugzilla::User->new_from_list([keys %user_ids]);
my %user_map = map { $_->id => $_ } @$users;
foreach my $comment (@$comments) {
$comment->{author} = $user_map{$comment->{who}};
}
# Tags
if (Bugzilla->params->{'comment_taggers_group'}) {
my $dbh = Bugzilla->dbh;
my @comment_ids = map { $_->id } @$comments;
my %comment_map = map { $_->id => $_ } @$comments;
my $rows = $dbh->selectall_arrayref(
"SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
FROM longdescs_tags
WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . "
GROUP BY comment_id");
foreach my $row (@$rows) {
$comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ];
}
}
}
###############################
......@@ -113,6 +184,39 @@ sub work_time {
sub type { return $_[0]->{'type'}; }
sub extra_data { return $_[0]->{'extra_data'} }
sub tags {
my ($self) = @_;
return [] unless Bugzilla->params->{'comment_taggers_group'};
$self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
"SELECT tag
FROM longdescs_tags
WHERE comment_id = ?
ORDER BY tag",
undef, $self->id);
return $self->{'tags'};
}
sub collapsed {
my ($self) = @_;
return 0 unless Bugzilla->params->{'comment_taggers_group'};
return $self->{collapsed} if exists $self->{collapsed};
$self->{collapsed} = 0;
Bugzilla->request_cache->{comment_tags_collapsed}
||= [ split(/\s*,\s*/, Bugzilla->params->{'collapsed_comment_tags'}) ];
my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} };
foreach my $my_tag (@{ $self->tags }) {
$my_tag = lc($my_tag);
foreach my $collapsed_tag (@collapsed_tags) {
if ($my_tag eq lc($collapsed_tag)) {
$self->{collapsed} = 1;
last;
}
}
last if $self->{collapsed};
}
return $self->{collapsed};
}
sub bug {
my $self = shift;
require Bugzilla::Bug;
......@@ -170,6 +274,26 @@ sub set_is_private { $_[0]->set('isprivate', $_[1]); }
sub set_type { $_[0]->set('type', $_[1]); }
sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
sub add_tag {
my ($self, $tag) = @_;
$tag = $self->_check_tag($tag);
my $tags = $self->tags;
return if grep { lc($tag) eq lc($_) } @$tags;
push @$tags, $tag;
$self->{'tags'} = [ sort @$tags ];
}
sub remove_tag {
my ($self, $tag) = @_;
$tag = $self->_check_tag($tag);
my $tags = $self->tags;
my $index = first { lc($tags->[$_]) eq lc($tag) } 0..scalar(@$tags) - 1;
return unless defined $index;
splice(@$tags, $index, 1);
}
##############
# Validators #
##############
......@@ -312,6 +436,17 @@ sub _check_isprivate {
return $isprivate ? 1 : 0;
}
sub _check_tag {
my ($invocant, $tag) = @_;
length($tag) < MIN_COMMENT_TAG_LENGTH
and ThrowUserError('comment_tag_too_short', { tag => $tag });
length($tag) > MAX_COMMENT_TAG_LENGTH
and ThrowUserError('comment_tag_too_long', { tag => $tag });
$tag =~ /^[\w\d\._-]+$/
or ThrowUserError('comment_tag_invalid', { tag => $tag });
return $tag;
}
sub count {
my ($self) = @_;
......@@ -326,7 +461,7 @@ sub count {
undef, $self->bug_id, $self->creation_ts);
return --$self->{'count'};
}
}
1;
......@@ -372,7 +507,7 @@ C<string> Time spent as related to this comment.
=item C<is_private>
C<boolean> Comment is marked as private
C<boolean> Comment is marked as private.
=item C<already_wrapped>
......@@ -387,6 +522,54 @@ L<Bugzilla::User> who created the comment.
C<int> The position this comment is located in the full list of comments for a bug starting from 0.
=item C<collapsed>
C<boolean> Comment should be displayed as collapsed by default.
=item C<tags>
C<array of strings> The tags attached to the comment.
=item C<add_tag>
=over
=item B<Description>
Attaches the specified tag to the comment.
=item B<Params>
=over
=item C<tag>
C<string> The tag to attach.
=back
=back
=item C<remove_tag>
=over
=item B<Description>
Detaches the specified tag from the comment.
=item B<Params>
=over
=item C<tag>
C<string> The tag to detach.
=back
=back
=item C<body_full>
=over
......
# 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::Comment::TagWeights;
use 5.10.1;
use strict;
use parent qw(Bugzilla::Object);
use Bugzilla::Constants;
# No auditing required
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant AUDIT_REMOVES => 0;
use constant DB_COLUMNS => qw(
id
tag
weight
);
use constant UPDATE_COLUMNS => qw(
weight
);
use constant DB_TABLE => 'longdescs_tags_weights';
use constant ID_FIELD => 'id';
use constant NAME_FIELD => 'tag';
use constant LIST_ORDER => 'weight DESC';
use constant VALIDATORS => { };
sub tag { return $_[0]->{'tag'} }
sub weight { return $_[0]->{'weight'} }
sub set_weight { $_[0]->set('weight', $_[1]); }
1;
=head1 NAME
Comment::TagWeights - Bugzilla comment weighting class.
=head1 DESCRIPTION
TagWeights.pm represents a Comment::TagWeight object. It is an implementation
of L<Bugzilla::Object>, and thus provides all methods that L<Bugzilla::Object>
provides.
TagWeights is used to quickly find tags and order by their usage count.
=head1 PROPERTIES
=over
=item C<tag>
C<getter string> The tag
=item C<weight>
C<getter int> The tag's weight. The value returned corresponds to the number of
comments with this tag attached.
=item C<set_weight>
C<setter int> Set the tag's weight.
=back
......@@ -84,7 +84,13 @@ sub get_param_list {
choices => ['', @legal_OS],
default => '',
checker => \&check_opsys
} );
},
{
name => 'collapsed_comment_tags',
type => 't',
default => 'obsolete, spam',
});
return @param_list;
}
......
......@@ -28,6 +28,7 @@ use parent qw(Exporter);
check_mail_delivery_method check_notification check_utf8
check_bug_status check_smtp_auth check_theschwartz_available
check_maxattachmentsize check_email check_smtp_ssl
check_comment_taggers_group
);
# Checking functions for the various values
......@@ -367,6 +368,14 @@ sub check_theschwartz_available {
return "";
}
sub check_comment_taggers_group {
my $group_name = shift;
if ($group_name && !Bugzilla->feature('jsonrpc')) {
return "Comment tagging requires installation of the JSONRPC feature";
}
return check_group($group_name);
}
# OK, here are the parameter definitions themselves.
#
# Each definition is a hash with keys:
......@@ -465,6 +474,11 @@ Checks that the value is a valid number
Checks that the value is a valid regexp
=item C<check_comment_taggers_group>
Checks that the required modules for comment tagging are installed, and that a
valid group is provided.
=back
=head1 B<Methods in need of POD>
......
......@@ -56,7 +56,15 @@ sub get_param_list {
default => 'editbugs',
checker => \&check_group
},
{
name => 'comment_taggers_group',
type => 's',
choices => \&_get_all_group_names,
default => 'editbugs',
checker => \&check_comment_taggers_group
},
{
name => 'debug_group',
type => 's',
......@@ -84,4 +92,5 @@ sub _get_all_group_names {
unshift(@group_names, '');
return \@group_names;
}
1;
......@@ -69,6 +69,9 @@ use Memoize;
COMMENT_COLS
MAX_COMMENT_LENGTH
MIN_COMMENT_TAG_LENGTH
MAX_COMMENT_TAG_LENGTH
CMT_NORMAL
CMT_DUPE_OF
CMT_HAS_DUPE
......@@ -291,6 +294,10 @@ use constant COMMENT_COLS => 80;
# Used in _check_comment(). Gives the max length allowed for a comment.
use constant MAX_COMMENT_LENGTH => 65535;
# The minimum and maximum length of comment tags.
use constant MIN_COMMENT_TAG_LENGTH => 3;
use constant MAX_COMMENT_TAG_LENGTH => 24;
# The type of bug comments.
use constant CMT_NORMAL => 0;
use constant CMT_DUPE_OF => 1;
......
......@@ -406,6 +406,54 @@ use constant ABSTRACT_SCHEMA => {
],
},
longdescs_tags => {
FIELDS => [
id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
comment_id => { TYPE => 'INT4',
REFERENCES => { TABLE => 'longdescs',
COLUMN => 'comment_id',
DELETE => 'CASCADE' }},
tag => { TYPE => 'varchar(24)', NOTNULL => 1 },
],
INDEXES => [
longdescs_tags_idx => { FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE' },
],
},
longdescs_tags_weights => {
FIELDS => [
id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
tag => { TYPE => 'varchar(24)', NOTNULL => 1 },
weight => { TYPE => 'INT3', NOTNULL => 1 },
],
INDEXES => [
longdescs_tags_weights_tag_idx => { FIELDS => ['tag'], TYPE => 'UNIQUE' },
],
},
longdescs_tags_activity => {
FIELDS => [
id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
bug_id => { TYPE => 'INT3', NOTNULL => 1,
REFERENCES => { TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE' }},
comment_id => { TYPE => 'INT4',
REFERENCES => { TABLE => 'longdescs',
COLUMN => 'comment_id',
DELETE => 'CASCADE' }},
who => { TYPE => 'INT3', NOTNULL => 1,
REFERENCES => { TABLE => 'profiles',
COLUMN => 'userid' }},
bug_when => { TYPE => 'DATETIME', NOTNULL => 1 },
added => { TYPE => 'varchar(24)' },
removed => { TYPE => 'varchar(24)' },
],
INDEXES => [
longdescs_tags_activity_bug_id_idx => ['bug_id'],
],
},
dependencies => {
FIELDS => [
blocked => {TYPE => 'INT3', NOTNULL => 1,
......
......@@ -255,6 +255,7 @@ use constant DEFAULT_FIELDS => (
type => FIELD_TYPE_BUG_URLS},
{name => 'tag', desc => 'Tags', buglist => 1,
type => FIELD_TYPE_KEYWORDS},
{name => 'comment_tag', desc => 'Comment Tag'},
);
################
......
......@@ -1852,6 +1852,17 @@ sub is_timetracker {
return $self->{'is_timetracker'};
}
sub can_tag_comments {
my $self = shift;
if (!defined $self->{'can_tag_comments'}) {
my $group = Bugzilla->params->{'comment_taggers_group'};
$self->{'can_tag_comments'} =
($group && $self->in_group($group)) ? 1 : 0;
}
return $self->{'can_tag_comments'};
}
sub get_userlist {
my $self = shift;
......@@ -2648,6 +2659,12 @@ i.e. if the 'insidergroup' parameter is set and the user belongs to this group.
Returns true if the user is a global watcher,
i.e. if the 'globalwatchers' parameter contains the user.
=item C<can_tag_comments>
Returns true if the user can attach tags to comments.
i.e. if the 'comment_taggers_group' parameter is set and the user belongs to
this group.
=back
=head1 CLASS FUNCTIONS
......
......@@ -13,6 +13,7 @@ use strict;
use parent qw(Bugzilla::WebService);
use Bugzilla::Comment;
use Bugzilla::Comment::TagWeights;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Field;
......@@ -20,7 +21,7 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Util qw(filter filter_wants validate translate);
use Bugzilla::Bug;
use Bugzilla::BugMail;
use Bugzilla::Util qw(trick_taint trim diff_arrays);
use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural);
use Bugzilla::Version;
use Bugzilla::Milestone;
use Bugzilla::Status;
......@@ -320,7 +321,8 @@ sub _translate_comment {
my ($self, $comment, $filters) = @_;
my $attach_id = $comment->is_about_attachment ? $comment->extra_data
: undef;
return filter $filters, {
my $comment_hash = {
id => $self->type('int', $comment->id),
bug_id => $self->type('int', $comment->bug_id),
creator => $self->type('email', $comment->author->login),
......@@ -332,6 +334,16 @@ sub _translate_comment {
attachment_id => $self->type('int', $attach_id),
count => $self->type('int', $comment->count),
};
# Don't load comment tags unless enabled
if (Bugzilla->params->{'comment_taggers_group'}) {
$comment_hash->{tags} = [
map { $self->type('string', $_) }
@{ $comment->tags }
];
}
return filter $filters, $comment_hash;
}
sub get {
......@@ -999,6 +1011,70 @@ sub update_tags {
return { changes => \%changes };
}
sub update_comment_tags {
my ($self, $params) = @_;
my $user = Bugzilla->login(LOGIN_REQUIRED);
Bugzilla->params->{'comment_taggers_group'}
|| ThrowUserError("comment_tag_disabled");
$user->can_tag_comments
|| ThrowUserError("auth_failure",
{ group => Bugzilla->params->{'comment_taggers_group'},
action => "update",
object => "comment_tags" });
my $comment_id = $params->{comment_id}
// ThrowCodeError('param_required',
{ function => 'Bug.update_comment_tags',
param => 'comment_id' });
my $comment = Bugzilla::Comment->new($comment_id)
|| return [];
$comment->bug->check_is_visible();
if ($comment->is_private && !$user->is_insider) {
ThrowUserError('comment_is_private', { id => $comment_id });
}
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
foreach my $tag (@{ $params->{add} || [] }) {
$comment->add_tag($tag) if defined $tag;
}
foreach my $tag (@{ $params->{remove} || [] }) {
$comment->remove_tag($tag) if defined $tag;
}
$comment->update();
$dbh->bz_commit_transaction();
return $comment->tags;
}
sub search_comment_tags {
my ($self, $params) = @_;
Bugzilla->login(LOGIN_REQUIRED);
Bugzilla->params->{'comment_taggers_group'}
|| ThrowUserError("comment_tag_disabled");
Bugzilla->user->can_tag_comments
|| ThrowUserError("auth_failure", { group => Bugzilla->params->{'comment_taggers_group'},
action => "search",
object => "comment_tags"});
my $query = $params->{query};
$query
// ThrowCodeError('param_required', { param => 'query' });
my $limit = detaint_natural($params->{limit}) || 7;
my $tags = Bugzilla::Comment::TagWeights->match({
WHERE => {
'tag LIKE ?' => "\%$query\%",
},
LIMIT => $limit,
});
return [ map { $_->tag } @$tags ];
}
##############################
# Private Helper Subroutines #
##############################
......@@ -4032,6 +4108,137 @@ This method can throw the same errors as L</get>.
=back
=head2 search_comment_tags
B<UNSTABLE>
=over
=item B<Description>
Searches for tags which contain the provided substring.
=item B<REST>
To search for comment tags:
GET /bug/comment/tags/<query>
=item B<Params>
=over
=item C<query>
B<Required> C<string> Only tags containg this substring will be returned.
=item C<limit>
C<int> If provided will return no more than C<limit> tags. Defaults to C<10>.
=back
=item B<Returns>
An C<array of strings> of matching tags.
=item B<Errors>
This method can throw all of the errors that L</get> throws, plus:
=over
=item 125 (Comment Tagging Disabled)
Comment tagging support is not available or enabled.
=back
=item B<History>
=over
=item Added in Bugzilla B<5.0>.
=back
=back
=head2 update_comment_tags
B<UNSTABLE>
=over
=item B<Description>
Adds or removes tags from a comment.
=item B<REST>
To update the tags comments attached to a comment:
PUT /bug/comment/tags
The params to include in the PUT body as well as the returned data format,
are the same as below.
=item B<Params>
=over
=item C<comment_id>
B<Required> C<int> The ID of the comment to update.
=item C<add>
C<array of strings> The tags to attach to the comment.
=item C<remove>
C<array of strings> The tags to detach from the comment.
=back
=item B<Returns>
An C<array of strings> containing the comment's updated tags.
=item B<Errors>
This method can throw all of the errors that L</get> throws, plus:
=over
=item 125 (Comment Tagging Disabled)
Comment tagging support is not available or enabled.
=item 126 (Invalid Comment Tag)
The comment tag provided was not valid (eg. contains invalid characters).
=item 127 (Comment Tag Too Short)
The comment tag provided is shorter than the minimum length.
=item 128 (Comment Tag Too Long)
The comment tag provided is longer than the maximum length.
=back
=item B<History>
=over
=item Added in Bugzilla B<5.0>.
=back
=back
=head1 B<Methods in need of POD>
=over
......
......@@ -98,7 +98,12 @@ use constant WS_ERROR_CODE => {
comment_is_private => 110,
comment_id_invalid => 111,
comment_too_long => 114,
comment_invalid_isprivate => 117,
comment_invalid_isprivate => 117,
# Comment tagging
comment_tag_disabled => 125,
comment_tag_invalid => 126,
comment_tag_too_long => 127,
comment_tag_too_short => 128,
# See Also errors
bug_url_invalid => 112,
bug_url_too_long => 112,
......
......@@ -65,6 +65,22 @@ sub _rest_resources {
}
}
},
qr{^/bug/comment/tags/([^/]+)$}, {
GET => {
method => 'search_comment_tags',
params => sub {
return { query => $_[0] };
},
},
},
qr{^/bug/comment/([^/]+)/tags$}, {
PUT => {
method => 'update_comment_tags',
params => sub {
return { comment_id => $_[0] };
},
},
},
qr{^/bug/([^/]+)/history$}, {
GET => {
method => 'history',
......
......@@ -25,9 +25,9 @@ function toggle_comment_display(link, comment_id) {
var comment = document.getElementById('comment_text_' + comment_id);
var re = new RegExp(/\bcollapsed\b/);
if (comment.className.match(re))
expand_comment(link, comment);
expand_comment(link, comment, comment_id);
else
collapse_comment(link, comment);
collapse_comment(link, comment, comment_id);
}
function toggle_all_comments(action) {
......@@ -44,20 +44,22 @@ function toggle_all_comments(action) {
var id = comments[i].id.match(/\d*$/);
var link = document.getElementById('comment_link_' + id);
if (action == 'collapse')
collapse_comment(link, comment);
collapse_comment(link, comment, id);
else
expand_comment(link, comment);
expand_comment(link, comment, id);
}
}
function collapse_comment(link, comment) {
function collapse_comment(link, comment, comment_id) {
link.innerHTML = "[+]";
YAHOO.util.Dom.addClass(comment, 'collapsed');
YAHOO.util.Dom.addClass('comment_tag_' + comment_id, 'collapsed');
}
function expand_comment(link, comment) {
function expand_comment(link, comment, comment_id) {
link.innerHTML = "[&minus;]";
YAHOO.util.Dom.removeClass(comment, 'collapsed');
YAHOO.util.Dom.removeClass('comment_tag_' + comment_id, 'collapsed');
}
function wrapReplyText(text) {
......@@ -110,11 +112,12 @@ function wrapReplyText(text) {
/* This way, we are sure that browsers which do not support JS
* won't display this link */
function addCollapseLink(count, title) {
function addCollapseLink(count, collapsed, title) {
document.write(' <a href="#" class="bz_collapse_comment"' +
' id="comment_link_' + count +
'" onclick="toggle_comment_display(this, ' + count +
'); return false;" title="' + title + '">[&minus;]<\/a> ');
'); return false;" title="' + title + '">[' +
(collapsed ? '+' : '&minus;') + ']<\/a> ');
}
function goto_add_comments( anchor ){
......
......@@ -125,10 +125,7 @@ function bz_overlayBelow(item, parent) {
*/
function bz_isValueInArray(aArray, aValue)
{
var run = 0;
var len = aArray.length;
for ( ; run < len; run++) {
for (var run = 0, len = aArray.length ; run < len; run++) {
if (aArray[run] == aValue) {
return true;
}
......@@ -138,6 +135,26 @@ function bz_isValueInArray(aArray, aValue)
}
/**
* Checks if a specified value is in the specified array by performing a
* case-insensitive comparison.
*
* @param aArray Array to search for the value.
* @param aValue Value to search from the array.
* @return Boolean; true if value is found in the array and false if not.
*/
function bz_isValueInArrayIgnoreCase(aArray, aValue)
{
var re = new RegExp(aValue.replace(/([^A-Za-z0-9])/g, "\\$1"), 'i');
for (var run = 0, len = aArray.length ; run < len; run++) {
if (aArray[run].match(re)) {
return true;
}
}
return false;
}
/**
* Create wanted options in a select form control.
*
* @param aSelect Select form control to manipulate.
......
......@@ -38,7 +38,7 @@ my $bug = Bugzilla::Bug->check($id);
# visible immediately due to replication lag.
Bugzilla->switch_to_shadow_db;
($vars->{'operations'}, $vars->{'incomplete_data'}) = $bug->get_activity;
($vars->{'operations'}, $vars->{'incomplete_data'}) = $bug->get_activity(undef, undef, 1);
$vars->{'bug'} = $bug;
......
......@@ -121,3 +121,42 @@ table#flags {
.bz_bug .bz_alias_short_desc_container {
width: inherit;
}
.bz_comment_tags {
margin-top: 3px;
}
.bz_comment_tag {
border: 1px solid #c8c8ba;
padding: 1px 3px;
margin-right: 2px;
border-radius: 0.5em;
background-color: #eee;
color: #000;
}
#bz_ctag_div {
display: inline-block;
}
#bz_ctag_error {
border: 1px solid #ff6666;
padding: 0px 2px;
border-radius: 0.5em;
margin: 2px;
display: inline-block;
}
#comment_tags_collapse_expand_container {
padding-top: 1em;
}
#comment_tags_collapse_expand {
list-style-type: none;
padding-left: 1em;
}
#comment_tags_collapse_expand li {
margin-bottom: 0px;
}
......@@ -41,5 +41,9 @@
"entry form.<br> " _
"You can leave this empty: " _
"$terms.Bugzilla will then use the operating system that the browser " _
"reports to be running on as the default." }
"reports to be running on as the default.",
collapsed_comment_tags => "A comma separated list of tags which, when applied " _
"to comments, will cause them to be collapsed by default",
}
%]
......@@ -29,6 +29,9 @@
querysharegroup => "The name of the group of users who can share their " _
"saved searches with others.",
comment_taggers_group => "The name of the group of users who can tag comment." _
" Setting this to empty disables comment tagging.",
debug_group => "The name of the group of users who can view the actual " _
"SQL query generated when viewing $terms.bug lists and reports.",
......
......@@ -13,7 +13,7 @@
<script type="text/javascript">
<!--
/* Adds the reply text to the `comment' textarea */
/* Adds the reply text to the 'comment' textarea */
function replyToComment(id, real_id, name) {
var prefix = "(In reply to " + name + " from comment #" + id + ")\n";
var replytext = "";
......@@ -74,15 +74,18 @@
<td>
[% IF mode == "edit" %]
<ul class="bz_collapse_expand_comments">
<li><a href="#" onclick="toggle_all_comments('collapse');
<li><a href="#" onclick="toggle_all_comments('collapse');
return false;">Collapse All Comments</a></li>
<li><a href="#" onclick="toggle_all_comments('expand');
return false;">Expand All Comments</a></li>
[% IF Param('comment_taggers_group') %]
<li><div id="comment_tags_collapse_expand_container"></div></li>
[% END %]
[% IF user.settings.comment_box_position.value == "after_comments" && user.id %]
<li class="bz_add_comment"><a href="#"
onclick="return goto_add_comments('bug_status_bottom');">
Add Comment</a></li>
[% END %]
[% END %]
</ul>
[% END %]
</td>
......@@ -109,18 +112,21 @@
[% END %]
<div class="[% class_name FILTER html %]">
[% IF mode == "edit" %]
<span class="bz_comment_actions">
[% IF bug.check_can_change_field('longdesc', 0, 1) %]
[% IF user.can_tag_comments %]
[<a href="#"
onclick="YAHOO.bugzilla.commentTagging.toggle([% comment.id %], [% comment.count %]);return false">tag</a>]
[% END %]
[<a class="bz_reply_link" href="#add_comment"
[% IF user.settings.quote_replies.value != 'off' %]
onclick="replyToComment('[% comment.count %]', '[% comment.id %]', '[% comment.author.name || comment.author.nick FILTER html FILTER js %]'); return false;"
[% END %]
>reply</a>]
[% END %]
<script type="text/javascript"><!--
addCollapseLink([% comment.count %], 'Toggle comment display'); // -->
<script type="text/javascript">
addCollapseLink([% comment.count %], [% comment.collapsed FILTER js %], 'Toggle comment display');
</script>
</span>
[% END %]
......@@ -175,10 +181,29 @@
[% PROCESS formattimeunit time_unit=comment.work_time %]
[% END %]
[% IF user.id && Param('comment_taggers_group') %]
<div id="comment_tag_[% comment.count FILTER html %]"
class="bz_comment_tags[% " collapsed" IF comment.collapsed %]">
<span id="ct_[% comment.count %]"
class="[% 'bz_default_hidden' UNLESS comment.tags.size %]">
[% IF comment.tags.size %]
<script>
YAHOO.bugzilla.commentTagging.showTags([% comment.id FILTER none %],
[% comment.count FILTER none %], [
[% FOREACH tag = comment.tags %]
[%~%]'[% tag FILTER js %]'[% "," UNLESS loop.last %]
[% END %]
[%~%]]);
</script>
[% END %]
</span>
</div>
[% END %]
[%# Don't indent the <pre> block, since then the spaces are displayed in the
# generated HTML
#%]
<pre class="bz_comment_text"
<pre class="bz_comment_text[% " collapsed" IF comment.collapsed %]"
[% ' id="comment_text_' _ comment.count _ '"' IF mode == "edit" %]>
[%- comment_text FILTER quoteUrls(bug, comment) -%]
</pre>
......
......@@ -8,6 +8,31 @@
[% PROCESS bug/time.html.tmpl %]
[% IF Param('comment_taggers_group') %]
[% IF user.can_tag_comments %]
<div id="bz_ctag_div" class="bz_default_hidden">
<a href="javascript:void(0)" onclick="YAHOO.bugzilla.commentTagging.hideInput()">x</a>
<div>
<input id="bz_ctag_add" size="10" placeholder="add tag"
maxlength="[% constants.MAX_COMMENT_TAG_LENGTH FILTER html %]">
<span id="bz_ctag_autocomp"></span>
</div>
&nbsp;
</div>
<div id="bz_ctag_error" class="bz_default_hidden">
<a href="javascript:void(0)" onclick="YAHOO.bugzilla.commentTagging.hideError()">x</a>
<span id="bz_ctag_error_msg"></span>
</div>
[% END %]
[% IF user.id %]
<script type="text/javascript">
YAHOO.bugzilla.commentTagging.init([% user.can_tag_comments ? 'true' : 'false' %]);
YAHOO.bugzilla.commentTagging.min_len = [% constants.MIN_COMMENT_TAG_LENGTH FILTER js %];
YAHOO.bugzilla.commentTagging.max_len = [% constants.MAX_COMMENT_TAG_LENGTH FILTER js %];
</script>
[% END %]
[% END %]
<script type="text/javascript">
<!--
[% IF user.is_timetracker %]
......
......@@ -24,7 +24,10 @@
[% END %]
[% title = title _ filtered_desc %]
[% yui = ['autocomplete', 'calendar'] %]
[% yui.push('container') IF user.can_tag_comments %]
[% javascript_urls = [ "js/util.js", "js/field.js" ] %]
[% javascript_urls.push('js/comment-tagging.js')
IF user.id && Param('comment_taggers_group') %]
[% IF bug.defined %]
[% header = "$terms.Bug&nbsp;$bug.bug_id" %]
[% header_addl_info = "Last modified: $filtered_timestamp" %]
......
......@@ -170,6 +170,8 @@
classifications
[% ELSIF object == "components" %]
components
[% ELSIF object == "comment_tags" %]
comment tags
[% ELSIF object == "custom_fields" %]
custom fields
[% ELSIF object == "field_values" %]
......@@ -318,6 +320,25 @@
Comments cannot be longer than
[%+ constants.MAX_COMMENT_LENGTH FILTER html %] characters.
[% ELSIF error == "comment_tag_disabled" %]
[% title = "Comment Tagging Disabled" %]
The comment tagging is not enabled.
[% ELSIF error == "comment_tag_invalid" %]
[% title = "Invalid Comment Tag" %]
The comment tag "[% tag FILTER html %]" contains invalid characters or
words.
[% ELSIF error == "comment_tag_too_long" %]
[% title = "Comment Tag Too Long" %]
Comment tags cannot be longer than
[%+ constants.MAX_COMMENT_TAG_LENGTH FILTER html %] characters.
[% ELSIF error == "comment_tag_too_short" %]
[% title = "Comment Tag Too Short" %]
Comment tags must be at least
[%+ constants.MIN_COMMENT_TAG_LENGTH FILTER html %] characters.
[% ELSIF error == "auth_classification_not_enabled" %]
[% title = "Classification Not Enabled" %]
Sorry, classification is not enabled.
......@@ -1720,9 +1741,13 @@
[% ELSIF error == "tag_name_too_long" %]
[% title = "Tag Name Too Long" %]
The tag name must be less than [% constants.MAX_LEN_QUERY_NAME FILTER html %]
The tag must be less than [% constants.MAX_LEN_QUERY_NAME FILTER html %]
characters long.
[% ELSIF error == "tag_name_invalid" %]
[% title = "Invalid Tag Name" %]
A tag cannot contain a space or a comma.
[% ELSIF error == "token_does_not_exist" %]
[% title = "Token Does Not Exist" %]
The token you submitted does not exist, has expired, or has
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment