Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
bugzilla
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
etersoft
bugzilla
Commits
ba0765bf
Commit
ba0765bf
authored
Nov 25, 2013
by
Byron Jones
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Bug 793963: add the ability to tag comments with arbitrary tags
r=dkl, a=glob
parent
0aade93c
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
792 additions
and
38 deletions
+792
-38
Bug.pm
Bugzilla/Bug.pm
+28
-5
Comment.pm
Bugzilla/Comment.pm
+189
-6
TagWeights.pm
Bugzilla/Comment/TagWeights.pm
+74
-0
BugFields.pm
Bugzilla/Config/BugFields.pm
+7
-1
Common.pm
Bugzilla/Config/Common.pm
+14
-0
GroupSecurity.pm
Bugzilla/Config/GroupSecurity.pm
+10
-1
Constants.pm
Bugzilla/Constants.pm
+7
-0
Schema.pm
Bugzilla/DB/Schema.pm
+48
-0
Field.pm
Bugzilla/Field.pm
+1
-0
User.pm
Bugzilla/User.pm
+17
-0
Bug.pm
Bugzilla/WebService/Bug.pm
+209
-2
Constants.pm
Bugzilla/WebService/Constants.pm
+6
-1
Bug.pm
Bugzilla/WebService/Server/REST/Resources/Bug.pm
+16
-0
comment-tagging.js
js/comment-tagging.js
+0
-0
comments.js
js/comments.js
+11
-8
util.js
js/util.js
+21
-4
show_activity.cgi
show_activity.cgi
+1
-1
show_bug.css
skins/standard/show_bug.css
+39
-0
bugfields.html.tmpl
template/en/default/admin/params/bugfields.html.tmpl
+5
-1
groupsecurity.html.tmpl
template/en/default/admin/params/groupsecurity.html.tmpl
+3
-0
comments.html.tmpl
template/en/default/bug/comments.html.tmpl
+32
-7
edit.html.tmpl
template/en/default/bug/edit.html.tmpl
+25
-0
show-header.html.tmpl
template/en/default/bug/show-header.html.tmpl
+3
-0
user-error.html.tmpl
template/en/default/global/user-error.html.tmpl
+26
-1
No files found.
Bugzilla/Bug.pm
View file @
ba0765bf
...
...
@@ -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 bug
s_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
);
...
...
Bugzilla/Comment.pm
View file @
ba0765bf
...
...
@@ -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
...
...
Bugzilla/Comment/TagWeights.pm
0 → 100644
View file @
ba0765bf
# 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
Bugzilla/Config/BugFields.pm
View file @
ba0765bf
...
...
@@ -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
;
}
...
...
Bugzilla/Config/Common.pm
View file @
ba0765bf
...
...
@@ -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>
...
...
Bugzilla/Config/GroupSecurity.pm
View file @
ba0765bf
...
...
@@ -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
;
Bugzilla/Constants.pm
View file @
ba0765bf
...
...
@@ -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
;
...
...
Bugzilla/DB/Schema.pm
View file @
ba0765bf
...
...
@@ -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
,
...
...
Bugzilla/Field.pm
View file @
ba0765bf
...
...
@@ -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'
},
);
################
...
...
Bugzilla/User.pm
View file @
ba0765bf
...
...
@@ -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
...
...
Bugzilla/WebService/Bug.pm
View file @
ba0765bf
...
...
@@ -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
...
...
Bugzilla/WebService/Constants.pm
View file @
ba0765bf
...
...
@@ -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
,
...
...
Bugzilla/WebService/Server/REST/Resources/Bug.pm
View file @
ba0765bf
...
...
@@ -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'
,
...
...
js/comment-tagging.js
0 → 100644
View file @
ba0765bf
This diff is collapsed.
Click to expand it.
js/comments.js
View file @
ba0765bf
...
...
@@ -25,9 +25,9 @@ function toggle_comment_display(link, comment_id) {
var
comment
=
document
.
getElementById
(
'comment_text_'
+
comment_id
);
var
re
=
new
RegExp
(
/
\b
collapsed
\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
=
"[−]"
;
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
+
'">[−]<
\
/a> '
);
'); return false;" title="'
+
title
+
'">['
+
(
collapsed
?
'+'
:
'−'
)
+
']<
\
/a> '
);
}
function
goto_add_comments
(
anchor
){
...
...
js/util.js
View file @
ba0765bf
...
...
@@ -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.
...
...
show_activity.cgi
View file @
ba0765bf
...
...
@@ -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
;
...
...
skins/standard/show_bug.css
View file @
ba0765bf
...
...
@@ -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
;
}
template/en/default/admin/params/bugfields.html.tmpl
View file @
ba0765bf
...
...
@@ -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",
}
%]
template/en/default/admin/params/groupsecurity.html.tmpl
View file @
ba0765bf
...
...
@@ -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.",
...
...
template/en/default/bug/comments.html.tmpl
View file @
ba0765bf
...
...
@@ -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>
...
...
template/en/default/bug/edit.html.tmpl
View file @
ba0765bf
...
...
@@ -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>
</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 %]
...
...
template/en/default/bug/show-header.html.tmpl
View file @
ba0765bf
...
...
@@ -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 $bug.bug_id" %]
[% header_addl_info = "Last modified: $filtered_timestamp" %]
...
...
template/en/default/global/user-error.html.tmpl
View file @
ba0765bf
...
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment