]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 69000: Permit a stored query to be marked "shared" and accessible by other users.
authorwurblzap%gmail.com <>
Fri, 14 Jul 2006 03:07:57 +0000 (03:07 +0000)
committerwurblzap%gmail.com <>
Fri, 14 Jul 2006 03:07:57 +0000 (03:07 +0000)
Patch by Marc Schumann <wurblzap@gmail.com>,
r=vladd, a=myk

18 files changed:
Bugzilla/Config/GroupSecurity.pm
Bugzilla/DB/Schema.pm
Bugzilla/User.pm
buglist.cgi
checksetup.pl
docs/xml/using.xml
editgroups.cgi
editusers.cgi
sanitycheck.cgi
skins/standard/global.css
template/en/default/account/prefs/saved-searches.html.tmpl
template/en/default/admin/groups/delete.html.tmpl
template/en/default/admin/params/groupsecurity.html.tmpl
template/en/default/admin/users/confirm-delete.html.tmpl
template/en/default/filterexceptions.pl
template/en/default/global/useful-links.html.tmpl
template/en/default/global/user-error.html.tmpl
userprefs.cgi

index 0235a8cb16613a22baa62de32f480096dddcd90c..1dee703d01fddbecd6038bea3dad3c171f65198f 100644 (file)
@@ -78,6 +78,14 @@ sub get_param_list {
    checker => \&check_group
   },
 
+  {
+   name => 'querysharegroup',
+   type => 's',
+   choices => \&_get_all_group_names,
+   default => 'editbugs',
+   checker => \&check_group
+  },
+  
   {
    name => 'usevisibilitygroups',
    type => 'b',
index 62a3da4454f8616a111575a04e4ecacf982eba4c..7c848bd5bd6d02ef01491c539992b73bd5c4e235 100644 (file)
@@ -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},
index 9977ca86c691fc4ec03005241c7bf8ca1de2cd78..e962ae7ae559d839d98d1887a71ebfc47a8a7d29 100644 (file)
@@ -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>
index 1400968fc7ad9d35059eb5f25c4fc3bfb42d9b54..c16e70a69fcc42c537ccc6f4091c3a030b27d629 100755 (executable)
@@ -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());
index a934b6519ea3b2b94354218913819b77ea5a8156..ba7e2e59af987dbcd4f9b287ca3d5e5fb48f60ee 100755 (executable)
@@ -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.
index 84221bcfff689054472a79c6def386b61570da43..650f056cb6b3561720edc3c4251b91da98acb292 100644 (file)
     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>
index 39e78e9e6faf00d57792effc1144444f62bcb2c0..57708cd3e756dd094ef5f42b5566236d4e1ee6dc 100755 (executable)
@@ -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 
index facb46600f4519ede4d2c170395ccf73439cc3d7..9a4bfd5c0d1cbf7e4eb8bfa6c3a71cf84a9bf703 100755 (executable)
@@ -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,
index cfe5e1fe3fa78da80bfde420c1280188f50e6d27..aeeee5ea93e6dfa5939d981c03b78f7a9cc28165 100755 (executable)
@@ -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"],
index aa303fc7e74034800aa1b3a9be00db7af42adb01..a2cf3ea931a99ff2c4db201a9d2487487eed2184 100644 (file)
@@ -128,11 +128,6 @@ body
         display: inline;
     }
 
-    #footer span
-    {
-        display: block;
-    }
-
     #footer .btn, #footer .txt
     {
         font-size: 0.8em;
index 7f0052bba34afcf479fec87c73a13123a4f84f05..16ec678760a917745a26550cf6d28b08bbff3846 100644 (file)
   # 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>
         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>
                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>
         </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 %]
index cca19981bc4ec8b16a7b3cae71e7c2d0b93c5fa5..d0c50f69a78142e5cf0767eb3e74776c24b95beb 100644 (file)
@@ -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.
   #%]
 
     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>
index 9016a7038c61be5794b09449b37cf1ba410d42d7..63077799889ec05e78bea7d4e6f5f0a950872ee2 100644 (file)
@@ -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?",
   
index 6b7bdcf10dc798d1d7889ce8b5f72963d9cef5ce..e5f3a392b028d200b88ba112123e210c9e3cde12 100644 (file)
@@ -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
         [% 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 %]
index 14043a9bfd6cd834ec6d88c63b9ffbb825862e5b..8008036d77221c81a1205d7981a4fbfb56949edf 100644 (file)
   'comp.bug_count'
 ],
 
+'admin/groups/delete.html.tmpl' => [
+  'shared_queries'
+],
+
 'admin/users/confirm-delete.html.tmpl' => [
   'andstring',
   'responsibilityterms.$responsibility',
   'flags.requestee',
   'flags.setter',
   'longdescs',
-  'namedqueries',
   'votes',
   'series',
   'watch.watched',
   'current_tab.name',
 ],
 
+'account/prefs/saved-searches.html.tmpl' => [
+  'group.id',
+],
+
 'account/prefs/settings.html.tmpl' => [
   'name',
   'default_name'
index 9d97b095bf19e0532e62f7c53b3ef46884f22a83..44a990279daa80ac866ef84b2498d7da32008547 100644 (file)
@@ -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% 
   <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>
 
index c615598b9f4b07ee1ff978dd43daf4e96bcac752..b8dbeb24fa8566e069f98a661261ce59f7a3d319 100644 (file)
                 
   [% 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 %]
index 15ca800b1a82b59a1b05e61be7550b69d3cd8eec..688a437fda96d5fa3aa37482652521a843989bf3 100755 (executable)
@@ -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));