diff options
-rw-r--r-- | Bugzilla/Config/GroupSecurity.pm | 8 | ||||
-rw-r--r-- | Bugzilla/DB/Schema.pm | 29 | ||||
-rw-r--r-- | Bugzilla/User.pm | 87 | ||||
-rwxr-xr-x | buglist.cgi | 93 | ||||
-rwxr-xr-x | checksetup.pl | 18 | ||||
-rw-r--r-- | docs/xml/using.xml | 16 | ||||
-rwxr-xr-x | editgroups.cgi | 25 | ||||
-rwxr-xr-x | editusers.cgi | 29 | ||||
-rwxr-xr-x | sanitycheck.cgi | 7 | ||||
-rw-r--r-- | skins/standard/global.css | 5 | ||||
-rw-r--r-- | template/en/default/account/prefs/saved-searches.html.tmpl | 88 | ||||
-rw-r--r-- | template/en/default/admin/groups/delete.html.tmpl | 20 | ||||
-rw-r--r-- | template/en/default/admin/params/groupsecurity.html.tmpl | 3 | ||||
-rw-r--r-- | template/en/default/admin/users/confirm-delete.html.tmpl | 33 | ||||
-rw-r--r-- | template/en/default/filterexceptions.pl | 9 | ||||
-rw-r--r-- | template/en/default/global/useful-links.html.tmpl | 38 | ||||
-rw-r--r-- | template/en/default/global/user-error.html.tmpl | 10 | ||||
-rwxr-xr-x | userprefs.cgi | 124 |
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> + — + </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 %] + — + [% 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&remaction=run&namedcmd= + [% q.name FILTER url_quote %]&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"> + <None> </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 Searches: + Saved 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 [% terms.Bugs %]</a> + [% IF user.showmybugslink %] + [% filtered_username = user.login FILTER url_quote %] + <a href="[% Param('mybugstemplate').replace('%userid%', filtered_username) %]">My [% 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&remaction=run&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&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&remaction=run&namedcmd= + [% q.name FILTER url_quote %]&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)); |