summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Bugzilla/Config/GroupSecurity.pm8
-rw-r--r--Bugzilla/DB/Schema.pm29
-rw-r--r--Bugzilla/User.pm87
-rwxr-xr-xbuglist.cgi93
-rwxr-xr-xchecksetup.pl18
-rw-r--r--docs/xml/using.xml16
-rwxr-xr-xeditgroups.cgi25
-rwxr-xr-xeditusers.cgi29
-rwxr-xr-xsanitycheck.cgi7
-rw-r--r--skins/standard/global.css5
-rw-r--r--template/en/default/account/prefs/saved-searches.html.tmpl88
-rw-r--r--template/en/default/admin/groups/delete.html.tmpl20
-rw-r--r--template/en/default/admin/params/groupsecurity.html.tmpl3
-rw-r--r--template/en/default/admin/users/confirm-delete.html.tmpl33
-rw-r--r--template/en/default/filterexceptions.pl9
-rw-r--r--template/en/default/global/useful-links.html.tmpl38
-rw-r--r--template/en/default/global/user-error.html.tmpl10
-rwxr-xr-xuserprefs.cgi124
18 files changed, 549 insertions, 93 deletions
diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm
index 0235a8cb1..1dee703d0 100644
--- a/Bugzilla/Config/GroupSecurity.pm
+++ b/Bugzilla/Config/GroupSecurity.pm
@@ -79,6 +79,14 @@ sub get_param_list {
},
{
+ name => 'querysharegroup',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_group
+ },
+
+ {
name => 'usevisibilitygroups',
type => 'b',
default => 0
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index 62a3da445..7c848bd5b 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -666,9 +666,10 @@ use constant ABSTRACT_SCHEMA => {
namedqueries => {
FIELDS => [
+ id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
userid => {TYPE => 'INT3', NOTNULL => 1},
name => {TYPE => 'varchar(64)', NOTNULL => 1},
- linkinfooter => {TYPE => 'BOOLEAN', NOTNULL => 1},
query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
query_type => {TYPE => 'BOOLEAN', NOTNULL => 1},
],
@@ -678,6 +679,18 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ namedqueries_link_in_footer => {
+ FIELDS => [
+ namedquery_id => {TYPE => 'INT3', NOTNULL => 1},
+ user_id => {TYPE => 'INT3', NOTNULL => 1},
+ ],
+ INDEXES => [
+ namedqueries_link_in_footer_id_idx => {FIELDS => [qw(namedquery_id user_id)],
+ TYPE => 'UNIQUE'},
+ namedqueries_link_in_footer_userid_idx => ['user_id'],
+ ],
+ },
+
# Authentication
# --------------
@@ -806,6 +819,20 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ # This table determines which groups a user must be a member of
+ # in order to see a named query somebody else shares.
+ namedquery_group_map => {
+ FIELDS => [
+ namedquery_id => {TYPE => 'INT3', NOTNULL => 1},
+ group_id => {TYPE => 'INT3', NOTNULL => 1},
+ ],
+ INDEXES => [
+ namedquery_group_map_namedquery_id_idx =>
+ {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'},
+ namedquery_group_map_group_id_idx => ['group_id'],
+ ],
+ },
+
category_group_map => {
FIELDS => [
category_id => {TYPE => 'INT2', NOTNULL => 1},
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 9977ca86c..e962ae7ae 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -204,25 +204,46 @@ sub queries {
return [] unless $self->id;
my $dbh = Bugzilla->dbh;
- my $used_in_whine_ref = $dbh->selectcol_arrayref(q{
+ my $used_in_whine_ref = $dbh->selectall_hashref('
SELECT DISTINCT query_name
FROM whine_events we
INNER JOIN whine_queries wq
ON we.id = wq.eventid
- WHERE we.owner_userid = ?}, undef, $self->{id});
-
- my $queries_ref = $dbh->selectall_arrayref(q{
- SELECT name, query, linkinfooter, query_type
- FROM namedqueries
- WHERE userid = ?
- ORDER BY UPPER(name)},{'Slice'=>{}}, $self->{id});
-
- foreach my $name (@$used_in_whine_ref) {
- foreach my $queries_hash (@$queries_ref) {
- if ($queries_hash->{name} eq $name) {
- $queries_hash->{usedinwhine} = 1;
- last;
- }
+ WHERE we.owner_userid = ?',
+ 'query_name', undef, $self->id);
+
+ # If the user is in any group, there may be shared queries to be included.
+ my $or_nqgm_group_id_in_usergroups = '';
+ if ($self->groups_as_string) {
+ $or_nqgm_group_id_in_usergroups =
+ 'OR MAX(nqgm.group_id) IN (' . $self->groups_as_string . ') ';
+ }
+
+ my $queries_ref = $dbh->selectall_arrayref('
+ SELECT nq.id, MAX(userid) AS userid, name, query, query_type,
+ MAX(nqgm.group_id) AS shared_with_group,
+ COUNT(nql.namedquery_id) AS link_in_footer
+ FROM namedqueries AS nq
+ LEFT JOIN namedquery_group_map nqgm
+ ON nqgm.namedquery_id = nq.id
+ LEFT JOIN namedqueries_link_in_footer AS nql
+ ON nql.namedquery_id = nq.id
+ AND nql.user_id = ? ' .
+ $dbh->sql_group_by('nq.id', 'name, query, query_type') .
+ ' HAVING MAX(nq.userid) = ? ' .
+ $or_nqgm_group_id_in_usergroups .
+ ' ORDER BY UPPER(name)',
+ {'Slice'=>{}}, $self->id, $self->id);
+
+ foreach my $queries_hash (@$queries_ref) {
+ # For each query, determine whether it's being used in a whine.
+ if (exists($$used_in_whine_ref{$queries_hash->{'name'}})) {
+ $queries_hash->{'usedinwhine'} = 1;
+ }
+
+ # For shared queries, provide the sharer's user object.
+ if ($queries_hash->{'userid'} != $self->id) {
+ $queries_hash->{'user'} = new Bugzilla::User($queries_hash->{'userid'});
}
}
$self->{queries} = $queries_ref;
@@ -660,6 +681,24 @@ sub visible_groups_as_string {
return join(', ', @{$self->visible_groups_inherited()});
}
+# This function defines the groups a user may share a query with.
+# More restrictive sites may want to build this reference to a list of group IDs
+# from bless_groups instead of mirroring visible_groups_inherited, perhaps.
+sub queryshare_groups {
+ my $self = shift;
+ if ($self->in_group(Bugzilla->params->{'querysharegroup'})) {
+ return $self->visible_groups_inherited();
+ }
+ else {
+ return [];
+ }
+}
+
+sub queryshare_groups_as_string {
+ my $self = shift;
+ return join(', ', @{$self->queryshare_groups()});
+}
+
sub derive_regexp_groups {
my ($self) = @_;
@@ -734,8 +773,8 @@ sub can_bless {
}
# Otherwise, we're checking a specific group
- my $group_name = shift;
- return (grep {$$_{'name'} eq $group_name} (@{$self->bless_groups})) ? 1 : 0;
+ my $group_id = shift;
+ return (grep {$$_{'id'} eq $group_id} (@{$self->bless_groups})) ? 1 : 0;
}
sub flatten_group_membership {
@@ -1576,12 +1615,20 @@ Should only be called by C<Bugzilla::Auth::login>, for the most part.
=item C<queries>
Returns an array of the user's named queries, sorted in a case-insensitive
-order by name. Each entry is a hash with three keys:
+order by name. Each entry is a hash with five keys:
=over
=item *
+id - The ID of the query
+
+=item *
+
+userid - The query owner's user ID
+
+=item *
+
name - The name of the query
=item *
@@ -1590,7 +1637,7 @@ query - The text for the query
=item *
-linkinfooter - Whether or not the query should be displayed in the footer.
+link_in_footer - Whether or not the query should be displayed in the footer.
=back
@@ -1783,7 +1830,7 @@ When called with no arguments:
Returns C<1> if the user can bless at least one group, returns C<0> otherwise.
When called with one argument:
-Returns C<1> if the user can bless the group with that name, returns
+Returns C<1> if the user can bless the group with that id, returns
C<0> otherwise.
=item C<wants_bug_mail>
diff --git a/buglist.cgi b/buglist.cgi
index 1400968fc..c16e70a69 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -203,18 +203,42 @@ sub DiffDate {
}
sub LookupNamedQuery {
- my ($name) = @_;
+ my ($name, $sharer_id) = @_;
my $user = Bugzilla->login(LOGIN_REQUIRED);
my $dbh = Bugzilla->dbh;
- # $name is safe -- we only use it below in a SELECT placeholder and then
- # in error messages (which are always HTML-filtered).
+ my $owner_id;
+
+ # $name and $sharer_id are safe -- we only use them below in SELECT
+ # placeholders and then in error messages (which are always HTML-filtered).
$name || ThrowUserError("query_name_missing");
trick_taint($name);
- my $result = $dbh->selectrow_array("SELECT query FROM namedqueries"
- . " WHERE userid = ? AND name = ?"
- , undef, ($user->id, $name));
+ if ($sharer_id) {
+ trick_taint($sharer_id);
+ $owner_id = $sharer_id;
+ }
+ else {
+ $owner_id = $user->id;
+ }
+
+ my ($id, $result) = $dbh->selectrow_array('SELECT id, query
+ FROM namedqueries
+ WHERE userid = ? AND name = ?',
+ undef, ($owner_id, $name));
+ defined($result)
+ || ThrowUserError("missing_query", {'queryname' => $name,
+ 'sharer_id' => $sharer_id});
+
+ if ($sharer_id) {
+ my $group = $dbh->selectrow_array('SELECT group_id
+ FROM namedquery_group_map
+ WHERE namedquery_id = ?',
+ undef, $id);
+ if (!grep {$_ == $group} values(%{$user->groups()})) {
+ ThrowUserError("missing_query", {'queryname' => $name,
+ 'sharer_id' => $sharer_id});
+ }
+ }
- defined($result) || ThrowUserError("missing_query", {'queryname' => $name});
$result
|| ThrowUserError("buglist_parameters_required", {'queryname' => $name});
@@ -265,7 +289,8 @@ sub InsertNamedQuery {
# it when we display it to the user.
trick_taint($query);
- $dbh->bz_lock_tables('namedqueries WRITE');
+ $dbh->bz_lock_tables('namedqueries WRITE',
+ 'namedqueries_link_in_footer WRITE');
my $result = $dbh->selectrow_array("SELECT userid FROM namedqueries"
. " WHERE userid = ? AND name = ?"
@@ -273,15 +298,22 @@ sub InsertNamedQuery {
if ($result) {
$query_existed_before = 1;
$dbh->do("UPDATE namedqueries"
- . " SET query = ?, linkinfooter = ?, query_type = ?"
+ . " SET query = ?, query_type = ?"
. " WHERE userid = ? AND name = ?"
- , undef, ($query, $link_in_footer, $query_type, $userid, $query_name));
+ , undef, ($query, $query_type, $userid, $query_name));
} else {
$query_existed_before = 0;
$dbh->do("INSERT INTO namedqueries"
- . " (userid, name, query, linkinfooter, query_type)"
- . " VALUES (?, ?, ?, ?, ?)"
- , undef, ($userid, $query_name, $query, $link_in_footer, $query_type));
+ . " (userid, name, query, query_type)"
+ . " VALUES (?, ?, ?, ?)"
+ , undef, ($userid, $query_name, $query, $query_type));
+ if ($link_in_footer) {
+ $dbh->do('INSERT INTO namedqueries_link_in_footer
+ (namedquery_id, user_id)
+ VALUES (?, ?)',
+ undef,
+ ($dbh->bz_last_key('namedqueries', 'id'), $userid));
+ }
}
$dbh->bz_unlock_tables();
@@ -373,9 +405,15 @@ if ($cgi->param('cmdtype') eq "dorem" && $cgi->param('remaction') =~ /^run/) {
# Take appropriate action based on user's request.
if ($cgi->param('cmdtype') eq "dorem") {
if ($cgi->param('remaction') eq "run") {
- $buffer = LookupNamedQuery(scalar $cgi->param("namedcmd"));
- $vars->{'searchname'} = $cgi->param('namedcmd');
- $vars->{'searchtype'} = "saved";
+ $buffer = LookupNamedQuery(scalar $cgi->param("namedcmd"),
+ scalar $cgi->param('sharer_id'));
+ # If this is the user's own query, remember information about it
+ # so that it can be modified easily.
+ if (!$cgi->param('sharer_id') ||
+ $cgi->param('sharer_id') == Bugzilla->user->id) {
+ $vars->{'searchname'} = $cgi->param('namedcmd');
+ $vars->{'searchtype'} = "saved";
+ }
$params = new Bugzilla::CGI($buffer);
$order = $params->param('order') || $order;
@@ -415,9 +453,24 @@ if ($cgi->param('cmdtype') eq "dorem") {
}
# If we are here, then we can safely remove the saved search
- $dbh->do("DELETE FROM namedqueries"
- . " WHERE userid = ? AND name = ?"
- , undef, ($user->id, $qname));
+ my ($query_id) = $dbh->selectrow_array('SELECT id FROM namedqueries
+ WHERE userid = ?
+ AND name = ?',
+ undef, ($user->id, $qname));
+ if (!$query_id) {
+ # The user has no query of this name. Play along.
+ }
+ else {
+ $dbh->do('DELETE FROM namedqueries
+ WHERE id = ?',
+ undef, $query_id);
+ $dbh->do('DELETE FROM namedqueries_link_in_footer
+ WHERE namedquery_id = ?',
+ undef, $query_id);
+ $dbh->do('DELETE FROM namedquery_group_map
+ WHERE namedquery_id = ?',
+ undef, $query_id);
+ }
# Now reset the cached queries
$user->flush_queries_cache();
@@ -425,7 +478,7 @@ if ($cgi->param('cmdtype') eq "dorem") {
print $cgi->header();
# Generate and return the UI (HTML page) from the appropriate template.
$vars->{'message'} = "buglist_query_gone";
- $vars->{'namedcmd'} = $cgi->param('namedcmd');
+ $vars->{'namedcmd'} = $qname;
$vars->{'url'} = "query.cgi";
$template->process("global/message.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
diff --git a/checksetup.pl b/checksetup.pl
index a934b6519..ba7e2e59a 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -2319,8 +2319,6 @@ if (!$dbh->bz_index_info('profiles', 'profiles_login_name_idx') ||
$dbh->bz_add_column('profiles', 'mybugslink', {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'});
-$dbh->bz_add_column('namedqueries', 'linkinfooter',
- {TYPE => 'BOOLEAN', NOTNULL => 1}, 0);
my $comp_init_owner = $dbh->bz_column_info('components', 'initialowner');
if ($comp_init_owner && $comp_init_owner->{TYPE} eq 'TINYTEXT') {
@@ -4369,6 +4367,22 @@ if (-e "$datadir/versioncache") {
unlink "$datadir/versioncache";
}
+# 2006-07-01 wurblzap@gmail.com -- Bug 69000
+$dbh->bz_add_column('namedqueries', 'id',
+ {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+if ($dbh->bz_column_info("namedqueries", "linkinfooter")) {
+ # Move link-in-footer information into a table of its own.
+ my $sth_read = $dbh->prepare('SELECT id, userid
+ FROM namedqueries
+ WHERE linkinfooter = 1');
+ my $sth_write = $dbh->prepare('INSERT INTO namedqueries_link_in_footer
+ (namedquery_id, user_id) VALUES (?, ?)');
+ $sth_read->execute();
+ while (my ($id, $userid) = $sth_read->fetchrow_array()) {
+ $sth_write->execute($id, $userid);
+ }
+ $dbh->bz_drop_column("namedqueries", "linkinfooter");
+}
# If you had to change the --TABLE-- definition in any way, then add your
# differential change code *** A B O V E *** this comment.
diff --git a/docs/xml/using.xml b/docs/xml/using.xml
index 84221bcff..650f056cb 100644
--- a/docs/xml/using.xml
+++ b/docs/xml/using.xml
@@ -345,8 +345,20 @@
returns bugs where the content of the field matches any one of the selected
values. If none is selected, then the field can take any value.</para>
- <para>Once you've run a search, you can save it as a Saved Search, which
- appears in the page footer.</para>
+ <para>
+ Once you've run a search, you can save it as a Saved Search, which
+ appears in the page footer.
+ On the Saved Searches tab of your User Preferences page (the Prefs link
+ in Bugzilla's footer), members of the group defined in the
+ querysharegroup parameter can share such Saved Searches with user groups
+ so that other users may use them.
+ At the same place, you can see Saved Searches other users are sharing, and
+ have them show up in your personal Bugzilla footer along with your own
+ Saved Searches.
+ If somebody is sharing a Search with a group she or he is allowed to
+ <xref linkend="groups">assign users to</a>, it will show up in the
+ group's direct members' footers by default.
+ </para>
<section id="boolean">
<title>Boolean Charts</title>
diff --git a/editgroups.cgi b/editgroups.cgi
index 39e78e9e6..57708cd3e 100755
--- a/editgroups.cgi
+++ b/editgroups.cgi
@@ -358,6 +358,12 @@ if ($action eq 'del') {
WHERE group_id IN ($grouplist) AND isbless = 0 " .
$dbh->sql_limit(1)) || 0;
+ my ($shared_queries) =
+ $dbh->selectrow_array('SELECT COUNT(*)
+ FROM namedquery_group_map
+ WHERE group_id = ?',
+ undef, $gid);
+
my $bug_ids = $dbh->selectcol_arrayref('SELECT bug_id FROM bug_group_map
WHERE group_id = ?', undef, $gid);
@@ -372,14 +378,15 @@ if ($action eq 'del') {
$dbh->sql_limit(1),
undef, ($gid, $gid)) || 0;
- $vars->{'gid'} = $gid;
- $vars->{'name'} = $name;
- $vars->{'description'} = $desc;
- $vars->{'hasusers'} = $hasusers;
- $vars->{'hasbugs'} = $hasbugs;
- $vars->{'hasproduct'} = $hasproduct;
- $vars->{'hasflags'} = $hasflags;
- $vars->{'buglist'} = $buglist;
+ $vars->{'gid'} = $gid;
+ $vars->{'name'} = $name;
+ $vars->{'description'} = $desc;
+ $vars->{'hasusers'} = $hasusers;
+ $vars->{'hasbugs'} = $hasbugs;
+ $vars->{'hasproduct'} = $hasproduct;
+ $vars->{'hasflags'} = $hasflags;
+ $vars->{'shared_queries'} = $shared_queries;
+ $vars->{'buglist'} = $buglist;
print $cgi->header();
$template->process("admin/groups/delete.html.tmpl", $vars)
@@ -462,6 +469,8 @@ if ($action eq 'delete') {
$dbh->do('UPDATE flagtypes SET request_group_id = ?
WHERE request_group_id = ?',
undef, (undef, $gid));
+ $dbh->do('DELETE FROM namedquery_group_map WHERE group_id = ?',
+ undef, $gid);
$dbh->do('DELETE FROM user_group_map WHERE group_id = ?',
undef, $gid);
$dbh->do('DELETE FROM group_group_map
diff --git a/editusers.cgi b/editusers.cgi
index facb46600..9a4bfd5c0 100755
--- a/editusers.cgi
+++ b/editusers.cgi
@@ -460,9 +460,18 @@ if ($action eq 'search') {
$vars->{'longdescs'} = $dbh->selectrow_array(
'SELECT COUNT(*) FROM longdescs WHERE who = ?',
undef, $otherUserID);
- $vars->{'namedqueries'} = $dbh->selectrow_array(
- 'SELECT COUNT(*) FROM namedqueries WHERE userid = ?',
- undef, $otherUserID);
+ $vars->{'namedqueries'} = $dbh->selectcol_arrayref(
+ 'SELECT id FROM namedqueries WHERE userid = ?',
+ undef,
+ $otherUserID);
+ if (@{$vars->{'namedqueries'}}) {
+ $vars->{'namedquery_group_map'} = $dbh->selectrow_array(
+ 'SELECT COUNT(*) FROM namedquery_group_map WHERE namedquery_id IN' .
+ ' (' . join(', ', @{$vars->{'namedqueries'}}) . ')');
+ }
+ else {
+ $vars->{'namedquery_group_map'} = 0;
+ }
$vars->{'profile_setting'} = $dbh->selectrow_array(
'SELECT COUNT(*) FROM profile_setting WHERE user_id = ?',
undef, $otherUserID);
@@ -525,6 +534,8 @@ if ($action eq 'search') {
'flagtypes READ',
'cc WRITE',
'namedqueries WRITE',
+ 'namedqueries_link_in_footer WRITE',
+ 'namedquery_group_map WRITE',
'tokens WRITE',
'votes WRITE',
'watch WRITE',
@@ -545,6 +556,12 @@ if ($action eq 'search') {
Bugzilla->logout_user($otherUser);
+ # Get the named query list so we can delete namedquery_group_map entries.
+ my $namedqueries_as_string = join(', ', $dbh->selectcol_arrayref(
+ 'SELECT id FROM namedqueries WHERE userid = ?',
+ undef,
+ $otherUserID));
+
# Get the timestamp for LogActivityEntry.
my $timestamp = $dbh->selectrow_array('SELECT NOW()');
@@ -589,6 +606,12 @@ if ($action eq 'search') {
$otherUserID);
$dbh->do('DELETE FROM logincookies WHERE userid = ?', undef, $otherUserID);
$dbh->do('DELETE FROM namedqueries WHERE userid = ?', undef, $otherUserID);
+ $dbh->do('DELETE FROM namedqueries_link_in_footer WHERE user_id = ?', undef,
+ $otherUserID);
+ if ($namedqueries_as_string) {
+ $dbh->do('DELETE FROM namedquery_group_map WHERE namedquery_id IN ' .
+ "($namedqueries_as_string)");
+ }
$dbh->do('DELETE FROM profile_setting WHERE user_id = ?', undef,
$otherUserID);
$dbh->do('DELETE FROM profiles_activity WHERE userid = ? OR who = ?', undef,
diff --git a/sanitycheck.cgi b/sanitycheck.cgi
index cfe5e1fe3..aeeee5ea9 100755
--- a/sanitycheck.cgi
+++ b/sanitycheck.cgi
@@ -418,8 +418,14 @@ CrossCheck("groups", "id",
["group_group_map", "grantor_id"],
["group_group_map", "member_id"],
["group_control_map", "group_id"],
+ ["namedquery_group_map", "group_id"],
["user_group_map", "group_id"]);
+CrossCheck("namedqueries", "id",
+ ["namedqueries_link_in_footer", "namedquery_id"],
+ ["namedquery_group_map", "namedquery_id"],
+ );
+
CrossCheck("profiles", "userid",
['profiles_activity', 'userid'],
['profiles_activity', 'who'],
@@ -438,6 +444,7 @@ CrossCheck("profiles", "userid",
["longdescs", "who", "bug_id"],
["logincookies", "userid"],
["namedqueries", "userid"],
+ ["namedqueries_link_in_footer", "user_id"],
['series', 'creator', 'series_id', ['0']],
["watch", "watcher"],
["watch", "watched"],
diff --git a/skins/standard/global.css b/skins/standard/global.css
index aa303fc7e..a2cf3ea93 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -128,11 +128,6 @@ body
display: inline;
}
- #footer span
- {
- display: block;
- }
-
#footer .btn, #footer .txt
{
font-size: 0.8em;
diff --git a/template/en/default/account/prefs/saved-searches.html.tmpl b/template/en/default/account/prefs/saved-searches.html.tmpl
index 7f0052bba..16ec67876 100644
--- a/template/en/default/account/prefs/saved-searches.html.tmpl
+++ b/template/en/default/account/prefs/saved-searches.html.tmpl
@@ -19,6 +19,13 @@
# Contributor(s): Gervase Markham <gerv@gerv.net>
#%]
+[%# INTERFACE:
+ # queries: list of the named queries visible to the user, both own and shared
+ # by others. Cleaned-up result of Bugzilla::User::queries.
+ # queryshare_groups: list of groups the user may share queries with
+ # (id, name).
+ #%]
+
<p>Your saved searches are as follows:</p>
<blockquote>
@@ -40,6 +47,17 @@
Show in
Footer
</th>
+ [% querysharegroup_regexp = '^' _ Param('querysharegroup') _ '$' %]
+ [% may_share = user.groups.keys.grep($querysharegroup_regexp).size %]
+ [% IF may_share %]
+ <th>
+ Share With
+ a Group
+ [% UNLESS queryshare_groups.size %]
+ (there are no groups you may share queries with)
+ [% END %]
+ </th>
+ [% END %]
</tr>
<tr>
<td>My Bugs</td>
@@ -59,8 +77,12 @@
value="1"
[% " checked" IF user.showmybugslink %]>
</td>
+ <td>
+ &mdash;
+ </td>
</tr>
[% FOREACH q = queries %]
+ [% NEXT UNLESS q.userid == user.id %]
<tr>
<td>[% q.name FILTER html %]</td>
<td>
@@ -79,9 +101,71 @@
</td>
<td align="center">
<input type="checkbox"
- name="linkinfooter_[% q.name FILTER html %]"
+ name="link_in_footer_[% q.id FILTER html %]"
+ value="1"
+ [% " checked" IF q.link_in_footer %]>
+ </td>
+ <td>
+ [% IF queryshare_groups.size %]
+ <select name="share_[% q.id FILTER html %]">
+ <option value="">Don't share</option>
+ [% FOREACH group = queryshare_groups %]
+ <option value="[% group.id %]"
+ [% ' selected="selected"' IF q.shared_with_group == group.id %]>[% group.name FILTER html %]</option>
+ [% END %]
+ </select>
+ [% ELSE %]
+ &mdash;
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+</blockquote>
+
+<p>You may use these searches saved and shared by others:</p>
+
+<blockquote>
+ <table border="1" cellpadding="3">
+ <tr>
+ <th>
+ Search
+ </th>
+ <th>
+ Shared By
+ </th>
+ <th>
+ Run
+ </th>
+ <th>
+ Show in
+ Footer
+ </th>
+ </tr>
+ [% found_shared_query = 0 %]
+ [% FOREACH q = queries %]
+ [% NEXT IF q.userid == user.id %]
+ [% found_shared_query = 1 %]
+ <tr>
+ <td>[% q.name FILTER html %]</td>
+ <td>[% q.user.identity FILTER html %]</td>
+ <td>
+ <a href="buglist.cgi?cmdtype=dorem&amp;remaction=run&amp;namedcmd=
+ [% q.name FILTER url_quote %]&amp;sharer_id=
+ [% q.userid FILTER url_quote %]">Run</a>
+ </td>
+ <td align="center">
+ <input type="checkbox"
+ name="link_in_footer_[% q.id FILTER html %]"
value="1"
- [% " checked" IF q.linkinfooter %]>
+ [% " checked" IF q.link_in_footer %]>
+ </td>
+ </tr>
+ [% END %]
+ [% IF !found_shared_query %]
+ <tr>
+ <td colspan="4" style="text-align: center">
+ &lt;None&gt;
</td>
</tr>
[% END %]
diff --git a/template/en/default/admin/groups/delete.html.tmpl b/template/en/default/admin/groups/delete.html.tmpl
index cca19981b..d0c50f69a 100644
--- a/template/en/default/admin/groups/delete.html.tmpl
+++ b/template/en/default/admin/groups/delete.html.tmpl
@@ -30,6 +30,7 @@
# hasbugs: boolean int. True if the group includes bugs in it.
# hasproduct: boolean int. True if the group is binded to a product.
# hasflags: boolean int. True if the group is used by a flag type.
+ # shared_queries: int. Number of saved searches being shared with this group.
# buglist: string. The list of bugs included in this group.
#%]
@@ -92,6 +93,25 @@
flag types from this group for me.</p>
[% END %]
+ [% IF shared_queries %]
+ <p>
+ <b>There
+ [% IF shared_queries > 1 %]
+ are [% shared_queries %] saved searches
+ [% ELSE %]
+ is a saved search
+ [% END %]
+ being shared with this group.</b>
+ If you delete this group,
+ [% IF shared_queries > 1 %]
+ these saved searches
+ [% ELSE %]
+ this saved search
+ [% END %]
+ will fall back to being private again.
+ </p>
+ [% END %]
+
<h2>Confirmation</h2>
<p>Do you really want to delete this group?</p>
diff --git a/template/en/default/admin/params/groupsecurity.html.tmpl b/template/en/default/admin/params/groupsecurity.html.tmpl
index 9016a7038..630777998 100644
--- a/template/en/default/admin/params/groupsecurity.html.tmpl
+++ b/template/en/default/admin/params/groupsecurity.html.tmpl
@@ -46,6 +46,9 @@
timetrackinggroup => "The name of the group of users who can see/change time tracking " _
"information.",
+ querysharegroup => "The name of the group of users who can share their " _
+ "saved searches with others.",
+
usevisibilitygroups => "Do you wish to restrict visibility of users to members of " _
"specific groups?",
diff --git a/template/en/default/admin/users/confirm-delete.html.tmpl b/template/en/default/admin/users/confirm-delete.html.tmpl
index 6b7bdcf10..e5f3a392b 100644
--- a/template/en/default/admin/users/confirm-delete.html.tmpl
+++ b/template/en/default/admin/users/confirm-delete.html.tmpl
@@ -31,7 +31,8 @@
# flags.requestee: number of flags the viewed user is being asked for
# flags.setter: number of flags the viewed user has set
# longdescs: number of bug comments the viewed user has written
- # namedqueries: number of named queries the user has created
+ # namedqueries: array of IDs of named queries the user has created
+ # namedquery_group_map: number of named queries the user has shared
# profiles_activity: number of named queries the user has created
# series: number of series the viewed user has created
# votes: number of bugs the viewed user has voted on
@@ -301,17 +302,35 @@
[% IF namedqueries %]
<li>
[% otheruser.login FILTER html %] has
- [% IF namedqueries == 1 %]
- a named query
+ [% IF namedqueries.size == 1 %]
+ a [% 'shared' IF namedquery_group_map %] named search
[% ELSE %]
- [%+ namedqueries %] named queries
+ [%+ namedqueries.size %] named searches
[% END %].
- [% IF namedqueries == 1 %]
- This named query
+ [% IF namedqueries.size == 1 %]
+ This named search
[% ELSE %]
- These named queries
+ These named searches
[% END %]
will be deleted along with the user account.
+ [% IF namedquery_group_map %]
+ [% IF namedqueries.size > 1 %]
+ Of these,
+ [% IF namedquery_group_map > 1 %]
+ [%+ namedquery_group_map FILTER html %] are
+ [% ELSE %]
+ one is
+ [% END %]
+ shared.
+ [% END %]
+ Other users will not be able to use
+ [% IF namedquery_group_map > 1 %]
+ these shared named searches
+ [% ELSE %]
+ this shared named search
+ [% END %]
+ any more.
+ [% END %]
</li>
[% END %]
[% IF profile_setting %]
diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl
index 14043a9bf..8008036d7 100644
--- a/template/en/default/filterexceptions.pl
+++ b/template/en/default/filterexceptions.pl
@@ -545,6 +545,10 @@
'comp.bug_count'
],
+'admin/groups/delete.html.tmpl' => [
+ 'shared_queries'
+],
+
'admin/users/confirm-delete.html.tmpl' => [
'andstring',
'responsibilityterms.$responsibility',
@@ -554,7 +558,6 @@
'flags.requestee',
'flags.setter',
'longdescs',
- 'namedqueries',
'votes',
'series',
'watch.watched',
@@ -600,6 +603,10 @@
'current_tab.name',
],
+'account/prefs/saved-searches.html.tmpl' => [
+ 'group.id',
+],
+
'account/prefs/settings.html.tmpl' => [
'name',
'default_name'
diff --git a/template/en/default/global/useful-links.html.tmpl b/template/en/default/global/useful-links.html.tmpl
index 9d97b095b..44a990279 100644
--- a/template/en/default/global/useful-links.html.tmpl
+++ b/template/en/default/global/useful-links.html.tmpl
@@ -18,6 +18,7 @@
#
# Contributor(s): Gervase Markham <gerv@gerv.net>
# Svetlana Harisova <light@rathedg.com>
+ # Marc Schumann <wurblzap@gmail.com>
#%]
[%# Migration note: this whole file corresponds to the old %commandmenu%
@@ -67,23 +68,40 @@
<div id="links-saved">
<div class="label">
[% IF user.showmybugslink OR user.queries.size %]
- Saved&nbsp;Searches:
+ Saved&nbsp;Searches:
[% END %]
</div>
<div class="links">
- [% IF user.showmybugslink %]
- [% filtered_username = user.login FILTER url_quote %]
- <a href="[% Param('mybugstemplate').replace('%userid%', filtered_username) %]">My&nbsp;[% terms.Bugs %]</a>
+ [% IF user.showmybugslink %]
+ [% filtered_username = user.login FILTER url_quote %]
+ <a href="[% Param('mybugstemplate').replace('%userid%', filtered_username) %]">My&nbsp;[% terms.Bugs %]</a>
+ [% print_pipe = 1 %]
+ [% END %]
+
+ [% FOREACH q = user.queries %]
+ [% NEXT IF q.userid != user.id %]
+ [% IF q.link_in_footer %]
+ [% " | " IF print_pipe %]
+ <a href="buglist.cgi?cmdtype=dorem&amp;remaction=run&amp;namedcmd=
+ [% q.name FILTER url_quote %]">[% q.name FILTER html FILTER no_break %]</a>
[% print_pipe = 1 %]
[% END %]
+ [% END %]
- [% FOREACH q = user.queries %]
- [% IF q.linkinfooter %]
- [% " | " IF print_pipe %]
- <a href="buglist.cgi?cmdtype=runnamed&amp;namedcmd=[% q.name FILTER url_quote %]">[% q.name FILTER html FILTER no_break %]</a>
- [% print_pipe = 1 %]
- [% END %]
+ [% " <br> " IF print_pipe %]
+ [% print_pipe = 0 %]
+ [% FOREACH q = user.queries %]
+ [% NEXT IF q.userid == user.id %]
+ [% IF q.link_in_footer %]
+ [% " | " IF print_pipe %]
+ <a href="buglist.cgi?cmdtype=dorem&amp;remaction=run&amp;namedcmd=
+ [% q.name FILTER url_quote %]&amp;sharer_id=
+ [% q.userid FILTER url_quote %]"
+ class="shared"
+ title="Shared by [% q.user.identity FILTER html %]">[% q.name FILTER html FILTER no_break %]</a>
+ [% print_pipe = 1 %]
[% END %]
+ [% END %]
</div>
</div>
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index c615598b9..b8dbeb24f 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -920,8 +920,14 @@
[% ELSIF error == "missing_query" %]
[% title = "Missing Search" %]
- The search named <em>[% queryname FILTER html %]</em> does not
- exist.
+ [% docslinks = {'query.html' => "Searching for $terms.bugs",
+ 'list.html' => "$terms.Bug lists"} %]
+ The search named <em>[% queryname FILTER html %]</em>
+ [% IF sharer_id %]
+ has not been made visible to you.
+ [% ELSE %]
+ does not exist.
+ [% END %]
[% ELSIF error == "move_bugs_disabled" %]
[% title = BLOCK %][% terms.Bug %] Moving Disabled[% END %]
diff --git a/userprefs.cgi b/userprefs.cgi
index 15ca800b1..688a437fd 100755
--- a/userprefs.cgi
+++ b/userprefs.cgi
@@ -351,11 +351,12 @@ sub DoPermissions {
my ($nam, $desc) = @$group;
push(@has_bits, {"desc" => $desc, "name" => $nam});
}
- $groups = $dbh->selectall_arrayref(
- "SELECT DISTINCT name, description FROM groups ORDER BY name");
+ $groups = $dbh->selectall_arrayref('SELECT DISTINCT id, name, description
+ FROM groups
+ ORDER BY name');
foreach my $group (@$groups) {
- my ($nam, $desc) = @$group;
- if ($user->can_bless($nam)) {
+ my ($group_id, $nam, $desc) = @$group;
+ if ($user->can_bless($group_id)) {
push(@set_bits, {"desc" => $desc, "name" => $nam});
}
}
@@ -370,6 +371,7 @@ sub DoPermissions {
sub DoSavedSearches {
# 2004-12-13 - colin.ogilvie@gmail.com, bug 274397
# Need to work around the possibly missing query_format=advanced
+ my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
my @queries = @{$user->queries};
@@ -391,6 +393,12 @@ sub DoSavedSearches {
push @newqueries, $q;
}
$vars->{'queries'} = \@newqueries;
+ if ($user->queryshare_groups_as_string) {
+ $vars->{'queryshare_groups'} = $dbh->selectall_arrayref(
+ 'SELECT id, name FROM groups WHERE id IN ' .
+ '(' . $user->queryshare_groups_as_string .')',
+ {'Slice' => {}});
+ }
}
sub SaveSavedSearches {
@@ -398,18 +406,114 @@ sub SaveSavedSearches {
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
+ # We'll need this in a loop, so do the call once.
+ my $user_id = $user->id;
+
my @queries = @{$user->queries};
- my $sth = $dbh->prepare("UPDATE namedqueries SET linkinfooter = ?
- WHERE userid = ?
- AND name = ?");
+ my $sth_check_nl = $dbh->prepare('SELECT COUNT(*)
+ FROM namedqueries_link_in_footer
+ WHERE namedquery_id = ?
+ AND user_id = ?');
+ my $sth_insert_nl = $dbh->prepare('INSERT INTO namedqueries_link_in_footer
+ (namedquery_id, user_id)
+ VALUES (?, ?)');
+ my $sth_delete_nl = $dbh->prepare('DELETE FROM namedqueries_link_in_footer
+ WHERE namedquery_id = ?
+ AND user_id = ?');
+ my $sth_check_ngm = $dbh->prepare('SELECT COUNT(*)
+ FROM namedquery_group_map
+ WHERE namedquery_id = ?');
+ my $sth_insert_ngm = $dbh->prepare('INSERT INTO namedquery_group_map
+ (namedquery_id, group_id)
+ VALUES (?, ?)');
+ my $sth_update_ngm = $dbh->prepare('UPDATE namedquery_group_map
+ SET group_id = ?
+ WHERE namedquery_id = ?');
+ my $sth_delete_ngm = $dbh->prepare('DELETE FROM namedquery_group_map
+ WHERE namedquery_id = ?');
+ my $sth_direct_group_members =
+ $dbh->prepare('SELECT user_id
+ FROM user_group_map
+ WHERE group_id = ?
+ AND isbless = ' . GROUP_MEMBERSHIP . '
+ AND grant_type = ' . GRANT_DIRECT);
foreach my $q (@queries) {
- my $linkinfooter =
- defined($cgi->param("linkinfooter_$q->{'name'}")) ? 1 : 0;
- $sth->execute($linkinfooter, $user->id, $q->{'name'});
+ # Update namedqueries_link_in_footer.
+ $sth_check_nl->execute($q->{'id'}, $user_id);
+ my ($link_in_footer_entries) = $sth_check_nl->fetchrow_array();
+ if (defined($cgi->param("link_in_footer_$q->{'id'}"))) {
+ if ($link_in_footer_entries == 0) {
+ $sth_insert_nl->execute($q->{'id'}, $user_id);
+ }
+ }
+ else {
+ if ($link_in_footer_entries > 0) {
+ $sth_delete_nl->execute($q->{'id'}, $user_id);
+ }
+ }
+
+ # For user's own queries, update namedquery_group_map.
+ if ($q->{'userid'} == $user_id) {
+ my ($group_id, $group_map_entries);
+ if ($user->in_group(Bugzilla->params->{'querysharegroup'})) {
+ $sth_check_ngm->execute($q->{'id'});
+ ($group_map_entries) = $sth_check_ngm->fetchrow_array();
+ $group_id = $cgi->param("share_$q->{'id'}") || '';
+ }
+ if ($group_id) {
+ if (grep(/^\Q$group_id\E$/, @{$user->queryshare_groups()})) {
+ # $group_id is now definitely a valid ID of a group the
+ # user can see, so we can trick_taint.
+ trick_taint($group_id);
+ if ($group_map_entries == 0) {
+ $sth_insert_ngm->execute($q->{'id'}, $group_id);
+ }
+ else {
+ $sth_update_ngm->execute($group_id, $q->{'id'});
+ }
+
+ # If we're sharing our query with a group we can bless,
+ # we're subscribing direct group members to our search
+ # automatically. Otherwise, the group members need to
+ # opt in. This behaviour is deemed most likely to fit
+ # users' needs.
+ if ($user->can_bless($group_id)) {
+ $sth_direct_group_members->execute($group_id);
+ my $user_id;
+ while ($user_id =
+ $sth_direct_group_members->fetchrow_array()) {
+ next if $user_id == $user->id;
+
+ $sth_check_nl->execute($q->{'id'}, $user_id);
+ my ($already_shown_in_footer) =
+ $sth_check_nl->fetchrow_array();
+ if (! $already_shown_in_footer) {
+ $sth_insert_nl->execute($q->{'id'}, $user_id);
+ }
+ }
+ }
+ }
+ else {
+ # In the unlikely case somebody removed visibility to the
+ # group in the meantime, don't modify sharing.
+ }
+ }
+ else {
+ if ($group_map_entries > 0) {
+ $sth_delete_ngm->execute($q->{'id'});
+ }
+
+ # Don't remove namedqueries_link_in_footer entries for users
+ # subscribing to the shared query. The idea is that they will
+ # probably want to be subscribers again should the sharing
+ # user choose to share the query again.
+ }
+ }
}
$user->flush_queries_cache;
+ # Update profiles.mybugslink.
my $showmybugslink = defined($cgi->param("showmybugslink")) ? 1 : 0;
$dbh->do("UPDATE profiles SET mybugslink = ? WHERE userid = ?",
undef, ($showmybugslink, $user->id));