[Rt-commit] rt branch, master, updated. rt-4.1.8-211-g4ee6d1a

Alex Vandiver alexmv at bestpractical.com
Thu May 2 15:56:22 EDT 2013


The branch, master has been updated
       via  4ee6d1a70d67d213bc51f0813f9e244d65532038 (commit)
       via  da1a67bfea314349a5d0a5bf7deb2bb446cfad4f (commit)
       via  7e227664346b4b27b4622a5ea7a293b684cb6296 (commit)
       via  a66d41f692e83b026a15d464cdf5e308c0129b49 (commit)
       via  794d3cc615035714795a1bf92bd955f0b278a7c7 (commit)
       via  38eab60e5115186a82fbec57ffdefb54d8395681 (commit)
       via  871f4f61f0926942871b13fcb6ccc80257a2c733 (commit)
       via  c72a5f5261b40a4a3c46dc41c04cf09b4a8ca224 (commit)
       via  eff3e1fc9b9dce47bc5136a74931449d6de81a2d (commit)
       via  c0b2b29e4693ed6d04fa21d07ba49187de0404c8 (commit)
       via  00e34fe5b5842157558306ad6bcad492367f684a (commit)
       via  906cd2b7b0c1aea2eccc709172ed523d98b7ecbe (commit)
       via  ebae01c6cdd5f2482cbf4ca282d96ab793a24007 (commit)
       via  9ebdbe566b8a298f056fa42e936ffbbc77e59a07 (commit)
       via  8915771b39bae9e6479123a81ec5f830c4d73b2e (commit)
       via  54f1a73b4fc057071d66ddd339d940c3d36cc826 (commit)
       via  2ef7f4839001155dbf2806053da0fb61eca776a2 (commit)
       via  7834bfa81b0f16867f440318f38f92a53dbf920e (commit)
       via  0cfd6223b87c9e6b1997482f2a0b78b2d9304f90 (commit)
       via  adc47505d64b8afcf4127b7a03dacba9b42f7b8b (commit)
       via  42e4fddb5f8b787eab1e52624781238ff7b852d9 (commit)
       via  479999c26f7dedd98c23f50288d7807373a83b93 (commit)
       via  72449cac0140de4ac370ccfb0565b4e7a74a8965 (commit)
       via  808beaeaf0a531fb5f080164b25697538b9b1dad (commit)
       via  15a18f9fc0c9960ef580905378c38e83bd962db8 (commit)
       via  dc221d91198594f515ab22937f3bb6b421d2faf0 (commit)
       via  d731ee1ee9696c7f42dfe5ce581eb776379fe477 (commit)
       via  a2263b7b789455b92eab1791a06c58e8bc43df08 (commit)
       via  c9af1f4084675d9b2fe6cc76deb6278dfb2f18d3 (commit)
       via  ac8392aae70f674417e89a0090abbfec25ab26ab (commit)
       via  28991d859ee23734e5c61406d2baad37c4f6f327 (commit)
       via  19b60e7723e8a8145649479cba6b68a9a33bfd5e (commit)
       via  d4fee32ea48d8b2999329d2505107fa6dd58c8a0 (commit)
       via  c8e5badb67af85d94565028faef82b1d4946c1ed (commit)
       via  8d40c6ff11ff17ecf047c65c1f45133ed7ebb5c3 (commit)
       via  ae0945c0b383d8cfb693f03b01e22575849f460d (commit)
       via  d3795e7c37531fdd56b46d4322522989f4a3c737 (commit)
       via  a512f4106fbf32cd1fd6f4e915156b68aa4ec75c (commit)
       via  9e8748ccb46aecda82be408734bc369482f30dcc (commit)
       via  cef9cc19efc5b89725d6fa6689637536e31877b5 (commit)
       via  9c2e33551f1b83e216e2c4500022bd0ac1c61f95 (commit)
       via  a28cf0cfc0f739b61b8dbc99339e5af1cc2f9903 (commit)
       via  946dc6e1708e6bcaf61a1c0bfe0a75e59a209764 (commit)
       via  3823e4c87f268901ecc1b6c5be53a114c6fddb3b (commit)
       via  d6a86655aa8b5f4d5d39702fdaaa24241c53871c (commit)
       via  5cee2fe87cd00c09fd92bc8f803334933b161225 (commit)
       via  35b87bb8d3f96a4c3090b8da1cae23f19bc49f81 (commit)
       via  042ac27969a8d38038aee9db16cb96c2b79eb5d2 (commit)
       via  a356872c01c7a8a684eac81208482ae1717381d3 (commit)
       via  efaf506bfa077ee07ac0cfa4b4e3bc8cad278bbf (commit)
       via  529800c2c90f0c6cc76b8a73416c1874d1d925cb (commit)
       via  27bd738eafa5367e6013836210e8a4a7d4244d71 (commit)
       via  5453b8d458aaab66dc8ac4fd16509137a7349de4 (commit)
       via  c2120ccbecf734475fc3a142554a6636fdcd6e5f (commit)
       via  d28f76f9be059c21907419d7364c395f326c056d (commit)
       via  dddc05435f84eed91a2a452d594432cb9412e42c (commit)
       via  5d153f131eb1943de0b2b4904aadcb9d1c306808 (commit)
       via  61662a68e9bca84ef74c7a728368d8d35928e910 (commit)
       via  e483b9d112afca6faa007dd100aa4908926c2f5a (commit)
      from  cf92245cec4fa440a1e196b8a145e391cc3e9ec3 (commit)

Summary of changes:
 .gitattributes                                     |   2 +-
 Makefile.in                                        |  18 +-
 docs/UPGRADING-3.4                                 |   7 +
 docs/UPGRADING-4.0                                 |  12 +
 etc/RT_Config.pm.in                                |  12 +
 etc/upgrade/3.9.8/schema.Oracle                    |   0
 etc/upgrade/3.9.8/schema.Pg                        |   0
 etc/upgrade/3.9.8/schema.SQLite                    |   0
 etc/upgrade/3.9.8/schema.mysql                     |   0
 etc/upgrade/4.0.1/acl.Pg                           |   0
 etc/upgrade/4.0.12/schema.Oracle                   |   1 +
 etc/upgrade/4.0.12/schema.Pg                       |   1 +
 etc/upgrade/4.0.12/schema.mysql                    |   1 +
 etc/upgrade/4.1.1/acl.Pg                           |   0
 lib/RT.pm                                          |  46 ++-
 lib/RT/Action/CreateTickets.pm                     |   4 +-
 lib/RT/Class.pm                                    |   2 +-
 lib/RT/Dashboard.pm                                |   2 +-
 lib/RT/Dashboard/Mailer.pm                         |   8 +-
 lib/RT/Handle.pm                                   |   6 +
 lib/RT/Interface/Email.pm                          |   8 +-
 lib/RT/Interface/Web.pm                            |  38 +-
 lib/RT/Interface/Web/Session.pm                    |   4 +-
 lib/RT/SavedSearch.pm                              |   2 +-
 lib/RT/SearchBuilder/Role/Roles.pm                 |  18 +-
 lib/RT/SharedSetting.pm                            |   4 +-
 lib/RT/Test.pm                                     |   3 +
 lib/RT/Ticket.pm                                   |  12 +
 lib/RT/Tickets.pm                                  |  97 ++++-
 lib/RT/User.pm                                     |   2 +-
 sbin/rt-setup-database.in                          |  10 +-
 .../ScrubHTML => Helpers/Autocomplete/autohandler} |  10 +-
 .../{Elements/ScrubHTML => Helpers/autohandler}    |   9 +-
 share/html/NoAuth/css/aileron/nav.css              |   2 +-
 share/html/Prefs/Quicksearch.html                  |  10 +-
 share/html/REST/1.0/Forms/attachment/default       |   4 +-
 share/html/SelfService/Prefs.html                  |  42 +++
 share/po/ar.po                                     | 393 ++++++++++++---------
 share/po/bg.po                                     | 393 ++++++++++++---------
 share/po/ca.po                                     | 393 ++++++++++++---------
 share/po/cs.po                                     | 393 ++++++++++++---------
 share/po/da.po                                     | 393 ++++++++++++---------
 share/po/de.po                                     | 393 ++++++++++++---------
 share/po/el.po                                     | 393 ++++++++++++---------
 share/po/en_GB.po                                  | 290 +++++++--------
 share/po/es.po                                     | 393 ++++++++++++---------
 share/po/et.po                                     | 393 ++++++++++++---------
 share/po/fi.po                                     | 393 ++++++++++++---------
 share/po/fr.po                                     | 393 ++++++++++++---------
 share/po/he.po                                     | 393 ++++++++++++---------
 share/po/hr.po                                     | 393 ++++++++++++---------
 share/po/hu.po                                     | 393 ++++++++++++---------
 share/po/id.po                                     | 393 ++++++++++++---------
 share/po/is.po                                     | 393 ++++++++++++---------
 share/po/it.po                                     | 393 ++++++++++++---------
 share/po/ja.po                                     | 393 ++++++++++++---------
 share/po/lt.po                                     | 393 ++++++++++++---------
 share/po/lv.po                                     | 393 ++++++++++++---------
 share/po/mk.po                                     | 393 ++++++++++++---------
 share/po/nb.po                                     | 393 ++++++++++++---------
 share/po/nl.po                                     | 393 ++++++++++++---------
 share/po/nn.po                                     | 393 ++++++++++++---------
 share/po/oc.po                                     | 393 ++++++++++++---------
 share/po/pl.po                                     | 393 ++++++++++++---------
 share/po/pt.po                                     | 393 ++++++++++++---------
 share/po/pt_BR.po                                  | 393 ++++++++++++---------
 share/po/pt_PT.po                                  | 393 ++++++++++++---------
 share/po/rt.pot                                    | 393 ++++++++++++---------
 share/po/ru.po                                     | 393 ++++++++++++---------
 share/po/sk.po                                     | 393 ++++++++++++---------
 share/po/sl.po                                     | 393 ++++++++++++---------
 share/po/sv.po                                     | 393 ++++++++++++---------
 share/po/tr.po                                     | 393 ++++++++++++---------
 share/po/zh_CN.po                                  | 391 +++++++++++---------
 share/po/zh_TW.po                                  | 391 +++++++++++---------
 t/99-policy.t                                      |   5 +-
 t/api/ticket.t                                     |  28 ++
 t/articles/class.t                                 |  16 +-
 t/ticket/search_by_watcher.t                       |  73 ++--
 t/web/helpers-http-cache-headers.t                 |  97 +++++
 80 files changed, 8765 insertions(+), 6678 deletions(-)
 mode change 100755 => 100644 etc/upgrade/3.9.8/schema.Oracle
 mode change 100755 => 100644 etc/upgrade/3.9.8/schema.Pg
 mode change 100755 => 100644 etc/upgrade/3.9.8/schema.SQLite
 mode change 100755 => 100644 etc/upgrade/3.9.8/schema.mysql
 mode change 100755 => 100644 etc/upgrade/4.0.1/acl.Pg
 create mode 100644 etc/upgrade/4.0.12/schema.Oracle
 create mode 100644 etc/upgrade/4.0.12/schema.Pg
 create mode 100644 etc/upgrade/4.0.12/schema.mysql
 mode change 100755 => 100644 etc/upgrade/4.1.1/acl.Pg
 copy share/html/{Elements/ScrubHTML => Helpers/Autocomplete/autohandler} (92%)
 copy share/html/{Elements/ScrubHTML => Helpers/autohandler} (92%)
 create mode 100644 t/web/helpers-http-cache-headers.t

- Log -----------------------------------------------------------------
commit 4ee6d1a70d67d213bc51f0813f9e244d65532038
Merge: cf92245 da1a67b
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Thu May 2 15:24:33 2013 -0400

    Merge branch '4.0-trunk'
    
    This merges work on requestor bundling (merged to 4.0-trunk in 7e22766),
    which is made more complicated by three factors:
     * The old (unused) bundling code was removed in 4.2, in 093efcc
     * Tickets_SQL.pm was merged into Tickets.pm in c6be454
     * Watcher searching was refactored into a SearchBuilder role in 4b15643
    
    This merge moves the newly added bundling code into the appropriate
    places in lib/RT/Tickets.pm and lib/RT/SearchBuilder/Role/Roles.pm
    
    Conflicts:
    	lib/RT/Tickets.pm
    	lib/RT/Tickets_SQL.pm
    	share/html/NoAuth/RichText/ckeditor/contents.css
    	share/html/Prefs/Quicksearch.html
    	share/html/Ticket/Elements/ShowRequestor
    	t/99-policy.t
    	t/api/ticket.t

diff --cc etc/upgrade/4.1.1/acl.Pg
index 9e8fc0a,0000000..9e8fc0a
mode 100755,000000..100644
--- a/etc/upgrade/4.1.1/acl.Pg
+++ b/etc/upgrade/4.1.1/acl.Pg
diff --cc lib/RT/SearchBuilder/Role/Roles.pm
index 8031b82,0000000..7167b13
mode 100644,000000..100644
--- a/lib/RT/SearchBuilder/Role/Roles.pm
+++ b/lib/RT/SearchBuilder/Role/Roles.pm
@@@ -1,372 -1,0 +1,378 @@@
 +# BEGIN BPS TAGGED BLOCK {{{
 +#
 +# COPYRIGHT:
 +#
 +# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
 +#                                          <sales at bestpractical.com>
 +#
 +# (Except where explicitly superseded by other copyright notices)
 +#
 +#
 +# LICENSE:
 +#
 +# This work is made available to you under the terms of Version 2 of
 +# the GNU General Public License. A copy of that license should have
 +# been provided with this software, but in any event can be snarfed
 +# from www.gnu.org.
 +#
 +# This work is distributed in the hope that it will be useful, but
 +# WITHOUT ANY WARRANTY; without even the implied warranty of
 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 +# General Public License for more details.
 +#
 +# You should have received a copy of the GNU General Public License
 +# along with this program; if not, write to the Free Software
 +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 +# 02110-1301 or visit their web page on the internet at
 +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
 +#
 +#
 +# CONTRIBUTION SUBMISSION POLICY:
 +#
 +# (The following paragraph is not intended to limit the rights granted
 +# to you to modify and distribute this software under the terms of
 +# the GNU General Public License and is only of importance to you if
 +# you choose to contribute your changes and enhancements to the
 +# community by submitting them to Best Practical Solutions, LLC.)
 +#
 +# By intentionally submitting any modifications, corrections or
 +# derivatives to this work, or any other work intended for use with
 +# Request Tracker, to Best Practical Solutions, LLC, you confirm that
 +# you are the copyright holder for those contributions and you grant
 +# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
 +# royalty-free, perpetual, license to use, copy, create derivative
 +# works based on those contributions, and sublicense and distribute
 +# those contributions and any derivatives thereof.
 +#
 +# END BPS TAGGED BLOCK }}}
 +
 +use strict;
 +use warnings;
 +
 +package RT::SearchBuilder::Role::Roles;
 +use Role::Basic;
 +use Scalar::Util qw(blessed);
 +
 +=head1 NAME
 +
 +RT::Record::Role::Roles - Common methods for records which "watchers" or "roles"
 +
 +=head1 REQUIRES
 +
 +=head2 L<RT::SearchBuilder::Role>
 +
 +=cut
 +
 +with 'RT::SearchBuilder::Role';
 +
 +require RT::System;
 +require RT::Principal;
 +require RT::Group;
 +require RT::User;
 +
 +require RT::EmailParser;
 +
 +=head1 PROVIDES
 +
 +=head2 _RoleGroupClass
 +
 +Returns the class name on which role searches should be based.  This relates to
 +the internal L<RT::Group/Domain> and distinguishes between roles on the objects
 +being searched and their counterpart roles on containing classes.  For example,
 +limiting on L<RT::Queue> roles while searching for L<RT::Ticket>s.
 +
 +The default implementation is:
 +
 +    blessed($self->NewItem)
 +
 +which is the class that this collection object searches and instatiates objects
 +for.  If you're doing something hinky, you may need to override this method.
 +
 +=cut
 +
 +sub _RoleGroupClass {
 +    my $self = shift;
 +    return blessed($self->NewItem);
 +}
 +
 +sub _RoleGroupsJoin {
 +    my $self = shift;
 +    my %args = (New => 0, Class => '', Type => '', @_);
 +
 +    $args{'Class'} ||= $self->_RoleGroupClass;
 +
 +    return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
 +        if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
 +           && !$args{'New'};
 +
 +    # If we're looking at a role group on a class that "contains" this record
 +    # (i.e. roles on queues for tickets), then we assume that the current
 +    # record has a column named after the containing class (i.e.
 +    # Tickets.Queue).
 +    my $instance = $self->_RoleGroupClass eq $args{Class} ? "id" : $args{Class};
 +       $instance =~ s/^RT:://;
 +
 +    # Watcher groups are always created for each record, so we use INNER join.
 +    my $groups = $self->Join(
 +        ALIAS1          => 'main',
 +        FIELD1          => $instance,
 +        TABLE2          => 'Groups',
 +        FIELD2          => 'Instance',
 +        ENTRYAGGREGATOR => 'AND',
 +    );
 +    $self->Limit(
 +        LEFTJOIN        => $groups,
 +        ALIAS           => $groups,
 +        FIELD           => 'Domain',
 +        VALUE           => $args{'Class'} .'-Role',
 +    );
 +    $self->Limit(
 +        LEFTJOIN        => $groups,
 +        ALIAS           => $groups,
 +        FIELD           => 'Type',
 +        VALUE           => $args{'Type'},
 +    ) if $args{'Type'};
 +
 +    $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
 +        unless $args{'New'};
 +
 +    return $groups;
 +}
 +
 +sub _GroupMembersJoin {
 +    my $self = shift;
 +    my %args = (New => 1, GroupsAlias => undef, Left => 1, @_);
 +
 +    return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
 +        if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
 +            && !$args{'New'};
 +
 +    my $alias = $self->Join(
 +        $args{'Left'} ? (TYPE            => 'LEFT') : (),
 +        ALIAS1          => $args{'GroupsAlias'},
 +        FIELD1          => 'id',
 +        TABLE2          => 'CachedGroupMembers',
 +        FIELD2          => 'GroupId',
 +        ENTRYAGGREGATOR => 'AND',
 +    );
 +    $self->Limit(
 +        LEFTJOIN => $alias,
 +        ALIAS => $alias,
 +        FIELD => 'Disabled',
 +        VALUE => 0,
 +    );
 +
 +    $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
 +        unless $args{'New'};
 +
 +    return $alias;
 +}
 +
 +=head2 _WatcherJoin
 +
 +Helper function which provides joins to a watchers table both for limits
 +and for ordering.
 +
 +=cut
 +
 +sub _WatcherJoin {
 +    my $self = shift;
 +
 +    my $groups = $self->_RoleGroupsJoin(@_);
 +    my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
 +    # XXX: work around, we must hide groups that
 +    # are members of the role group we search in,
 +    # otherwise them result in wrong NULLs in Users
 +    # table and break ordering. Now, we know that
 +    # RT doesn't allow to add groups as members of the
 +    # ticket roles, so we just hide entries in CGM table
 +    # with MemberId == GroupId from results
 +    $self->Limit(
 +        LEFTJOIN   => $group_members,
 +        FIELD      => 'GroupId',
 +        OPERATOR   => '!=',
 +        VALUE      => "$group_members.MemberId",
 +        QUOTEVALUE => 0,
 +    );
 +    my $users = $self->Join(
 +        TYPE            => 'LEFT',
 +        ALIAS1          => $group_members,
 +        FIELD1          => 'MemberId',
 +        TABLE2          => 'Users',
 +        FIELD2          => 'id',
 +    );
 +    return ($groups, $group_members, $users);
 +}
 +
 +
 +sub RoleLimit {
 +    my $self = shift;
 +    my %args = (
 +        TYPE => '',
 +        CLASS => '',
 +        FIELD => undef,
 +        OPERATOR => '=',
 +        VALUE => undef,
 +        @_
 +    );
 +
 +    my $class = $args{CLASS} || $self->_RoleGroupClass;
 +
 +    $args{FIELD} ||= 'id' if $args{VALUE} =~ /^\d+$/;
 +
 +    my $type = delete $args{TYPE};
 +    if ($type and not $class->HasRole($type)) {
 +        RT->Logger->warn("RoleLimit called with invalid role $type for $class");
 +        return;
 +    }
 +
 +    my $column = $type ? $class->Role($type)->{Column} : undef;
 +
 +    # if it's equality op and search by Email or Name then we can preload user
 +    # we do it to help some DBs better estimate number of rows and get better plans
 +    if ( $args{OPERATOR} =~ /^!?=$/
 +             && (!$args{FIELD} || $args{FIELD} eq 'Name' || $args{FIELD} eq 'EmailAddress') ) {
 +        my $o = RT::User->new( $self->CurrentUser );
 +        my $method =
 +            !$args{FIELD}
 +            ? ($column ? 'Load' : 'LoadByEmail')
 +            : $args{FIELD} eq 'EmailAddress' ? 'LoadByEmail': 'Load';
 +        $o->$method( $args{VALUE} );
 +        $args{FIELD} = 'id';
 +        $args{VALUE} = $o->id || 0;
 +    }
 +
 +    if ( $column and $args{FIELD} and $args{FIELD} eq 'id' ) {
 +        $self->Limit(
 +            %args,
 +            FIELD => $column,
 +        );
 +        return;
 +    }
 +
 +    $args{FIELD} ||= 'EmailAddress';
 +
-     my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class, New => !$type );
++    my ($groups, $group_members, $users);
++    if ( $args{'BUNDLE'} ) {
++        ($groups, $group_members, $users) = @{ $args{'BUNDLE'} };
++    } else {
++        $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class, New => !$type );
++    }
 +
 +    $self->_OpenParen( $args{SUBCLAUSE} ) if $args{SUBCLAUSE};
 +    if ( $args{OPERATOR} =~ /^IS(?: NOT)?$/i ) {
 +        # is [not] empty case
 +
-         my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
++        $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
 +        # to avoid joining the table Users into the query, we just join GM
 +        # and make sure we don't match records where group is member of itself
 +        $self->Limit(
 +            LEFTJOIN   => $group_members,
 +            FIELD      => 'GroupId',
 +            OPERATOR   => '!=',
 +            VALUE      => "$group_members.MemberId",
 +            QUOTEVALUE => 0,
 +        );
 +        $self->Limit(
 +            %args,
 +            ALIAS         => $group_members,
 +            FIELD         => 'GroupId',
 +            OPERATOR      => $args{OPERATOR},
 +            VALUE         => $args{VALUE},
 +        );
 +    }
 +    elsif ( $args{OPERATOR} =~ /^!=$|^NOT\s+/i ) {
 +        # negative condition case
 +
 +        # reverse op
 +        $args{OPERATOR} =~ s/!|NOT\s+//i;
 +
 +        # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
 +        # "X = 'Y'" matches more then one user so we try to fetch two records and
 +        # do the right thing when there is only one exist and semi-working solution
 +        # otherwise.
 +        my $users_obj = RT::Users->new( $self->CurrentUser );
 +        $users_obj->Limit(
 +            FIELD         => $args{FIELD},
 +            OPERATOR      => $args{OPERATOR},
 +            VALUE         => $args{VALUE},
 +        );
 +        $users_obj->OrderBy;
 +        $users_obj->RowsPerPage(2);
 +        my @users = @{ $users_obj->ItemsArrayRef };
 +
-         my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
++        $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
 +        if ( @users <= 1 ) {
 +            my $uid = 0;
 +            $uid = $users[0]->id if @users;
 +            $self->Limit(
 +                LEFTJOIN      => $group_members,
 +                ALIAS         => $group_members,
 +                FIELD         => 'MemberId',
 +                VALUE         => $uid,
 +            );
 +            $self->Limit(
 +                %args,
 +                ALIAS           => $group_members,
 +                FIELD           => 'id',
 +                OPERATOR        => 'IS',
 +                VALUE           => 'NULL',
 +            );
 +        } else {
 +            $self->Limit(
 +                LEFTJOIN   => $group_members,
 +                FIELD      => 'GroupId',
 +                OPERATOR   => '!=',
 +                VALUE      => "$group_members.MemberId",
 +                QUOTEVALUE => 0,
 +            );
-             my $users = $self->Join(
++            $users ||= $self->Join(
 +                TYPE            => 'LEFT',
 +                ALIAS1          => $group_members,
 +                FIELD1          => 'MemberId',
 +                TABLE2          => 'Users',
 +                FIELD2          => 'id',
 +            );
 +            $self->Limit(
 +                LEFTJOIN      => $users,
 +                ALIAS         => $users,
 +                FIELD         => $args{FIELD},
 +                OPERATOR      => $args{OPERATOR},
 +                VALUE         => $args{VALUE},
 +                CASESENSITIVE => 0,
 +            );
 +            $self->Limit(
 +                %args,
 +                ALIAS         => $users,
 +                FIELD         => 'id',
 +                OPERATOR      => 'IS',
 +                VALUE         => 'NULL',
 +            );
 +        }
 +    } else {
 +        # positive condition case
 +
-         my $group_members = $self->_GroupMembersJoin(
++        $group_members ||= $self->_GroupMembersJoin(
 +            GroupsAlias => $groups, New => 1, Left => 0
 +        );
-         my $users = $self->Join(
++        $users ||= $self->Join(
 +            TYPE            => 'LEFT',
 +            ALIAS1          => $group_members,
 +            FIELD1          => 'MemberId',
 +            TABLE2          => 'Users',
 +            FIELD2          => 'id',
 +        );
 +        $self->Limit(
 +            %args,
 +            ALIAS           => $users,
 +            FIELD           => $args{FIELD},
 +            OPERATOR        => $args{OPERATOR},
 +            VALUE           => $args{VALUE},
 +            CASESENSITIVE   => 0,
 +        );
 +    }
 +    $self->_CloseParen( $args{SUBCLAUSE} ) if $args{SUBCLAUSE};
++    return ($groups, $group_members, $users);
 +}
 +
 +1;
diff --cc lib/RT/SharedSetting.pm
index b3b2cc6,3467167..1e704f8
--- a/lib/RT/SharedSetting.pm
+++ b/lib/RT/SharedSetting.pm
@@@ -208,11 -207,10 +208,11 @@@ sub Save 
      my ($att_id, $att_msg) = $self->SaveAttribute($object, \%args);
  
      if ($att_id) {
 -        $self->{'Attribute'} = $object->Attributes->WithId($att_id);
 +        $self->{'Attribute'} = RT::Attribute->new($self->CurrentUser);
 +        $self->{'Attribute'}->Load( $att_id );
          $self->{'Id'}        = $att_id;
          $self->{'Privacy'}   = $privacy;
-         return ( 1, $self->loc( "Saved [_1] [_2]", $self->ObjectName, $name ) );
+         return ( 1, $self->loc( "Saved [_1] [_2]", $self->loc( $self->ObjectName ), $name ) );
      }
      else {
          $RT::Logger->error($self->ObjectName . " save failure: $att_msg");
diff --cc lib/RT/Ticket.pm
index 653d82e,1945545..46de60e
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@@ -360,10 -382,86 +360,13 @@@ sub Create 
      $args{'TimeWorked'}    = 0 unless defined $args{'TimeWorked'};
      $args{'TimeLeft'}      = 0 unless defined $args{'TimeLeft'};
  
 -    # }}}
 -
 -    # Deal with setting the owner
 -
 -    my $Owner;
 -    if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
 -        if ( $args{'Owner'}->id ) {
 -            $Owner = $args{'Owner'};
 -        } else {
 -            $RT::Logger->error('Passed an empty RT::User for owner');
 -            push @non_fatal_errors,
 -                $self->loc("Owner could not be set.") . " ".
 -            $self->loc("Invalid value for [_1]",loc('owner'));
 -            $Owner = undef;
 -        }
 -    }
 -
 -    #If we've been handed something else, try to load the user.
 -    elsif ( $args{'Owner'} ) {
 -        $Owner = RT::User->new( $self->CurrentUser );
 -        $Owner->Load( $args{'Owner'} );
 -        if (!$Owner->id) {
 -            $Owner->LoadByEmail( $args{'Owner'} )
 -        }
 -        unless ( $Owner->Id ) {
 -            push @non_fatal_errors,
 -                $self->loc("Owner could not be set.") . " "
 -              . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
 -            $Owner = undef;
 -        }
 -    }
 -
 -    #If we have a proposed owner and they don't have the right
 -    #to own a ticket, scream about it and make them not the owner
 -   
 -    my $DeferOwner;  
 -    if ( $Owner && $Owner->Id != RT->Nobody->Id 
 -        && !$Owner->HasRight( Object => $QueueObj, Right  => 'OwnTicket' ) )
 -    {
 -        $DeferOwner = $Owner;
 -        $Owner = undef;
 -        $RT::Logger->debug('going to deffer setting owner');
 -
 -    }
 -
 -    #If we haven't been handed a valid owner, make it nobody.
 -    unless ( defined($Owner) && $Owner->Id ) {
 -        $Owner = RT::User->new( $self->CurrentUser );
 -        $Owner->Load( RT->Nobody->Id );
 -    }
 -
 -    # }}}
 -
 -# We attempt to load or create each of the people who might have a role for this ticket
 -# _outside_ the transaction, so we don't get into ticket creation races
 -    foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
 -        $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
 -        foreach my $watcher ( splice @{ $args{$type} } ) {
 -            next unless $watcher;
 -            if ( $watcher =~ /^\d+$/ ) {
 -                push @{ $args{$type} }, $watcher;
 -            } else {
 -                my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
 -                foreach my $address( @addresses ) {
 -                    my $user = RT::User->new( RT->SystemUser );
 -                    my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
 -                    unless ( $uid ) {
 -                        push @non_fatal_errors,
 -                            $self->loc("Couldn't load or create user: [_1]", $msg);
 -                    } else {
 -                        push @{ $args{$type} }, $user->id;
 -                    }
 -                }
 -            }
 -        }
 -    }
 +    # Figure out users for roles
 +    my $roles = {};
 +    push @non_fatal_errors, $self->_ResolveRoles( $roles, %args );
  
+     $args{'Type'} = lc $args{'Type'}
+         if $args{'Type'} =~ /^(ticket|approval|reminder)$/i;
+ 
      $RT::Handle->BeginTransaction();
  
      my %params = (
diff --cc lib/RT/Tickets.pm
index 6437d78,116a4e2..12e4afa
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@@ -367,8 -378,10 +367,10 @@@ sub _EnumLimit 
          my $o     = $class->new( $sb->CurrentUser );
          $o->Load($value);
          $value = $o->Id || 0;
+     } elsif ( $field eq "Type" ) {
+         $value = lc $value if $value =~ /^(ticket|approval|reminder)$/i;
      }
 -    $sb->_SQLLimit(
 +    $sb->Limit(
          FIELD    => $field,
          VALUE    => $value,
          OPERATOR => $op,
@@@ -3313,124 -3569,7 +3315,213 @@@ BUG: There should be an API for thi
  
  =cut
  
 +=head2 FromSQL
 +
 +Convert a RT-SQL string into a set of SearchBuilder restrictions.
 +
 +Returns (1, 'Status message') on success and (0, 'Error Message') on
 +failure.
 +
 +=cut
 +
 +sub _parser {
 +    my ($self,$string) = @_;
 +    my $ea = '';
 +
++    # Bundling of joins is implemented by dynamically tracking a parallel query
++    # tree in %sub_tree as the TicketSQL is parsed.
++    #
++    # Only positive, OR'd watcher conditions are bundled currently.  Each key
++    # in %sub_tree is a watcher type (Requestor, Cc, AdminCc) or the generic
++    # "Watcher" for any watcher type.  Owner is not bundled because it is
++    # denormalized into a Tickets column and doesn't need a join.  AND'd
++    # conditions are not bundled since a record may have multiple watchers
++    # which independently match the conditions, thus necessitating two joins.
++    #
++    # The values of %sub_tree are arrayrefs made up of:
++    #
++    #   * Open parentheses "(" pushed on by the OpenParen callback
++    #   * Arrayrefs of bundled join aliases pushed on by the Condition callback
++    #   * Entry aggregators (AND/OR) pushed on by the EntryAggregator callback
++    #
++    # The CloseParen callback takes care of backing off the query trees until
++    # outside of the just-closed parenthetical, thus restoring the tree state
++    # an equivalent of before the parenthetical was entered.
++    #
++    # The Condition callback handles starting a new subtree or extending an
++    # existing one, determining if bundling the current condition with any
++    # subtree is possible, and pruning any dangling entry aggregators from
++    # trees.
++    #
++
++    my %sub_tree;
++    my $depth = 0;
++
 +    my %callback;
 +    $callback{'OpenParen'} = sub {
-       $self->_OpenParen
++      $self->_OpenParen;
++      $depth++;
++      push @$_, '(' foreach values %sub_tree;
 +    };
 +    $callback{'CloseParen'} = sub {
 +      $self->_CloseParen;
++      $depth--;
++      foreach my $list ( values %sub_tree ) {
++          if ( $list->[-1] eq '(' ) {
++              pop @$list;
++              pop @$list if $list->[-1] =~ /^(?:AND|OR)$/i;
++          }
++          else {
++              pop @$list while $list->[-2] ne '(';
++              $list->[-1] = pop @$list;
++          }
++      }
++    };
++    $callback{'EntryAggregator'} = sub {
++      $ea = $_[0] || '';
++      push @$_, $ea foreach grep @$_ && $_->[-1] ne '(', values %sub_tree;
 +    };
-     $callback{'EntryAggregator'} = sub { $ea = $_[0] || '' };
 +    $callback{'Condition'} = sub {
 +        my ($key, $op, $value) = @_;
 +
++        my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
++        my $null_op = ( 'is not' eq lc($op) || 'is' eq lc($op) );
 +        # key has dot then it's compound variant and we have subkey
 +        my $subkey = '';
 +        ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
 +
 +        # normalize key and get class (type)
 +        my $class;
 +        if (exists $LOWER_CASE_FIELDS{lc $key}) {
 +            $key = $LOWER_CASE_FIELDS{lc $key};
 +            $class = $FIELD_METADATA{$key}->[0];
 +        }
 +        die "Unknown field '$key' in '$string'" unless $class;
 +
 +        # replace __CurrentUser__ with id
 +        $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
 +
 +
 +        unless( $dispatch{ $class } ) {
 +            die "No dispatch method for class '$class'"
 +        }
 +        my $sub = $dispatch{ $class };
 +
-         $sub->( $self, $key, $op, $value,
++        my @res; my $bundle_with;
++        if ( $class eq 'WATCHERFIELD' && $key ne 'Owner' && !$negative_op && (!$null_op || $subkey) ) {
++            if ( !$sub_tree{$key} ) {
++              $sub_tree{$key} = [ ('(')x$depth, \@res ];
++            } else {
++              $bundle_with = $self->_check_bundling_possibility( $string, @{ $sub_tree{$key} } );
++              if ( $sub_tree{$key}[-1] eq '(' ) {
++                    push @{ $sub_tree{$key} }, \@res;
++              }
++            }
++        }
++
++        # Remove our aggregator from subtrees where our condition didn't get added
++        pop @$_ foreach grep @$_ && $_->[-1] =~ /^(?:AND|OR)$/i, values %sub_tree;
++
++        # A reference to @res may be pushed onto $sub_tree{$key} from
++        # above, and we fill it here.
++        @res = $sub->( $self, $key, $op, $value,
++                SUBCLAUSE       => '',  # don't need anymore
 +                ENTRYAGGREGATOR => $ea,
 +                SUBKEY          => $subkey,
++                BUNDLE          => $bundle_with,
 +              );
 +        $ea = '';
 +    };
 +    RT::SQL::Parse($string, \%callback);
 +}
 +
 +sub FromSQL {
 +    my ($self,$query) = @_;
 +
 +    {
 +        # preserve first_row and show_rows across the CleanSlate
 +        local ($self->{'first_row'}, $self->{'show_rows'}, $self->{_sql_looking_at});
 +        $self->CleanSlate;
 +        $self->_InitSQL();
 +    }
 +
 +    return (1, $self->loc("No Query")) unless $query;
 +
 +    $self->{_sql_query} = $query;
 +    eval {
 +        local $self->{parsing_ticketsql} = 1;
 +        $self->_parser( $query );
 +    };
 +    if ( $@ ) {
 +        $RT::Logger->error( $@ );
 +        return (0, $@);
 +    }
 +
 +    # We only want to look at EffectiveId's (mostly) for these searches.
 +    unless ( $self->{_sql_looking_at}{effectiveid} ) {
 +        # instead of EffectiveId = id we do IsMerged IS NULL
 +        $self->Limit(
 +            FIELD           => 'IsMerged',
 +            OPERATOR        => 'IS',
 +            VALUE           => 'NULL',
 +            ENTRYAGGREGATOR => 'AND',
 +            QUOTEVALUE      => 0,
 +        );
 +    }
 +    unless ( $self->{_sql_looking_at}{type} ) {
 +        $self->Limit( FIELD => 'Type', VALUE => 'ticket' );
 +    }
 +
 +    # We don't want deleted tickets unless 'allow_deleted_search' is set
 +    unless( $self->{'allow_deleted_search'} ) {
 +        $self->Limit(
 +            FIELD    => 'Status',
 +            OPERATOR => '!=',
 +            VALUE => 'deleted',
 +        );
 +    }
 +
 +    # set SB's dirty flag
 +    $self->{'must_redo_search'} = 1;
 +    $self->{'RecalcTicketLimits'} = 0;
 +
 +    return (1, $self->loc("Valid Query"));
 +}
 +
 +=head2 Query
 +
 +Returns the last string passed to L</FromSQL>.
 +
 +=cut
 +
 +sub Query {
 +    my $self = shift;
 +    return $self->{_sql_query};
 +}
 +
++sub _check_bundling_possibility {
++    my $self = shift;
++    my $string = shift;
++    my @list = reverse @_;
++    while (my $e = shift @list) {
++        next if $e eq '(';
++        if ( lc($e) eq 'and' ) {
++            return undef;
++        }
++        elsif ( lc($e) eq 'or' ) {
++            return shift @list;
++        }
++        else {
++            # should not happen
++            $RT::Logger->error(
++                "Joins optimization failed when parsing '$string'. It's bug in RT, contact Best Practical"
++            );
++            die "Internal error. Contact your system administrator.";
++        }
++    }
++    return undef;
++}
+ 
  
  =head2 NewItem
  
diff --cc share/html/Prefs/Quicksearch.html
index 5330ac6,cb4292a..81757b9
--- a/share/html/Prefs/Quicksearch.html
+++ b/share/html/Prefs/Quicksearch.html
@@@ -57,12 -57,19 +57,20 @@@
  % unless ($unwanted->{$queue->Name}) {
  checked="checked"
  % }
 -/><%$queue->Name%><% $queue->Description ? ': '.$queue->Description : '' %></li>
 +/>
 +<label for="Want-<%$queue->Name%>"><%$queue->Name%><% $queue->Description ? ': '.$queue->Description : '' %></label>
 +</li>
  % }
  </ul>
- <& /Elements/Submit, CheckAll => 1, ClearAll => 1, Caption => loc("Save Changes"), Label => loc('Save'), Name => 'Save'&>
 -
+ <& /Elements/Submit,
+     Caption             => loc("Save Changes"),
+     Label               => loc('Save'),
+     Name                => 'Save',
+     Reset               => 1,
+     CheckAll            => 1,
+     ClearAll            => 1,
+     CheckboxNameRegex   => '/^Want-/',
+     &>
  
  </form>
  
diff --cc share/html/SelfService/Prefs.html
index 3f1cca0,6478ef2..ebe42ac
--- a/share/html/SelfService/Prefs.html
+++ b/share/html/SelfService/Prefs.html
@@@ -57,9 -74,10 +74,10 @@@
  &>
  </&>
  
+ </td></tr></table>
  <br />
  <& /Elements/Submit, Label => loc('Save Changes') &>
 -	  </form>
 +</form>
  
  
  <%INIT>
diff --cc t/99-policy.t
index 717af8c,1980e34..a8e512c
--- a/t/99-policy.t
+++ b/t/99-policy.t
@@@ -119,7 -97,5 +119,10 @@@ check( $_, exec => -1 
  check( $_, exec => -1 )
      for grep {m{^t/data/}} @files;
  
+ check( $_, exec => -1, bps_tag => -1 )
+     for grep {m{^etc/upgrade/[^/]+/}} @files;
++
 +check( $_, warnings => 1, strict => 1, compile => 1, no_tabs => 1 )
 +    for grep {m{^etc/upgrade/.*/content$}} @files;
 +
 +done_testing;

-----------------------------------------------------------------------


More information about the Rt-commit mailing list