This commit was generated by cvs2svn to compensate for changes in r3921,
[freeside.git] / rt / lib / RT / Tickets_Overlay.pm
index d8a1ac8..582e786 100644 (file)
@@ -1,8 +1,14 @@
-# BEGIN LICENSE BLOCK
+# {{{ BEGIN BPS TAGGED BLOCK
 # 
-# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
+# COPYRIGHT:
+#  
+# This software is Copyright (c) 1996-2004 Best Practical Solutions, LLC 
+#                                          <jesse@bestpractical.com>
 # 
-# (Except where explictly superceded by other copyright notices)
+# (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
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
 # 
-# Unless otherwise specified, all modifications, corrections or
-# extensions to this work which alter its source code become the
-# property of Best Practical Solutions, LLC when submitted for
-# inclusion in the work.
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+# 
+# 
+# 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.)
 # 
-# END LICENSE BLOCK
+# 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
 # Major Changes:
 
 # - Decimated ProcessRestrictions and broke it into multiple
@@ -58,6 +80,7 @@ ok (require RT::Tickets);
 use strict;
 no warnings qw(redefine);
 use vars qw(@SORTFIELDS);
+use RT::CustomFields;
 
 
 # Configuration Tables:
@@ -84,7 +107,8 @@ my %FIELDS =
     RefersTo        => ['LINK' => To => 'RefersTo',],
     HasMember      => ['LINK' => From => 'MemberOf',],
     DependentOn     => ['LINK' => From => 'DependsOn',],
-    ReferredTo      => ['LINK' => From => 'RefersTo',],
+    DependedOnBy     => ['LINK' => From => 'DependsOn',],
+    ReferredToBy    => ['LINK' => From => 'RefersTo',],
 #   HasDepender            => ['LINK',],
 #   RelatedTo      => ['LINK',],
     Told           => ['DATE' => 'Told',],
@@ -95,14 +119,14 @@ my %FIELDS =
     LastUpdated            => ['DATE' => 'LastUpdated',],
     Created        => ['DATE' => 'Created',],
     Subject        => ['STRING',],
-    Type           => ['STRING',],
     Content        => ['TRANSFIELD',],
     ContentType            => ['TRANSFIELD',],
     Filename        => ['TRANSFIELD',],
     TransactionDate => ['TRANSDATE',],
     Requestor       => ['WATCHERFIELD' => 'Requestor',],
-    CC              => ['WATCHERFIELD' => 'Cc',],
-    AdminCC         => ['WATCHERFIELD' => 'AdminCC',],
+    Requestors       => ['WATCHERFIELD' => 'Requestor',],
+    Cc              => ['WATCHERFIELD' => 'Cc',],
+    AdminCc         => ['WATCHERFIELD' => 'AdminCC',],
     Watcher        => ['WATCHERFIELD'],
     LinkedTo       => ['LINKFIELD',],
     CustomFieldValue =>['CUSTOMFIELD',],
@@ -122,14 +146,23 @@ my %dispatch =
     LINKFIELD      => \&_LinkFieldLimit,
     CUSTOMFIELD    => \&_CustomFieldLimit,
   );
+my %can_bundle =
+  ( WATCHERFIELD => "yeps",
+  );
 
 # Default EntryAggregator per type
+# if you specify OP, you must specify all valid OPs
 my %DefaultEA = (
                  INT           => 'AND',
                  ENUM          => { '=' => 'OR',
                                     '!='=> 'AND'
                                   },
-                 DATE          => 'AND',
+                 DATE          => { '=' => 'OR',
+                                    '>='=> 'AND',
+                                    '<='=> 'AND',
+                                    '>' => 'AND',
+                                    '<' => 'AND'
+                                  },
                  STRING                => { '=' => 'OR',
                                     '!='=> 'AND',
                                     'LIKE'=> 'AND',
@@ -137,6 +170,7 @@ my %DefaultEA = (
                                   },
                  TRANSFIELD    => 'AND',
                  TRANSDATE     => 'AND',
+                 LINK           => 'OR',
                  LINKFIELD     => 'AND',
                  TARGET                => 'AND',
                  BASE          => 'AND',
@@ -154,6 +188,7 @@ my %DefaultEA = (
 # into Tickets_Overlay_SQL.
 sub FIELDS   { return \%FIELDS   }
 sub dispatch { return \%dispatch }
+sub can_bundle { return \%can_bundle }
 
 # Bring in the clowns.
 require RT::Tickets_Overlay_SQL;
@@ -268,7 +303,7 @@ Handle fields which deal with links between tickets.  (MemberOf, DependsOn)
 
 Meta Data:
   1: Direction (From,To)
-  2: Relationship Type (MemberOf, DependsOn,RefersTo)
+  2: Link Type (MemberOf, DependsOn,RefersTo)
 
 =cut
 
@@ -282,12 +317,13 @@ sub _LinkLimit {
   die "Incorrect Meta Data for $field"
     unless (defined $meta->[1] and defined $meta->[2]);
 
-  my $LinkAlias = $sb->NewAlias ('Links');
+  $sb->{_sql_linkalias} = $sb->NewAlias ('Links')
+    unless defined $sb->{_sql_linkalias};
 
   $sb->_OpenParen();
 
   $sb->_SQLLimit(
-            ALIAS => $LinkAlias,
+            ALIAS => $sb->{_sql_linkalias},
             FIELD =>   'Type',
             OPERATOR => '=',
             VALUE => $meta->[2],
@@ -298,7 +334,7 @@ sub _LinkLimit {
     my $matchfield = ( $value  =~ /^(\d+)$/ ? "LocalTarget" : "Target" );
 
     $sb->_SQLLimit(
-              ALIAS => $LinkAlias,
+              ALIAS => $sb->{_sql_linkalias},
               ENTRYAGGREGATOR => 'AND',
               FIELD =>   $matchfield,
               OPERATOR => '=',
@@ -306,14 +342,14 @@ sub _LinkLimit {
              );
 
     #If we're searching on target, join the base to ticket.id
-    $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
-              ALIAS2 => $LinkAlias,     FIELD2 => 'LocalBase');
+    $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
+              ALIAS2 => $sb->{_sql_linkalias},  FIELD2 => 'LocalBase');
 
   } elsif ( $meta->[1] eq "From" ) {
     my $matchfield = ( $value  =~ /^(\d+)$/ ? "LocalBase" : "Base" );
 
     $sb->_SQLLimit(
-              ALIAS => $LinkAlias,
+              ALIAS => $sb->{_sql_linkalias},
               ENTRYAGGREGATOR => 'AND',
               FIELD =>   $matchfield,
               OPERATOR => '=',
@@ -321,8 +357,8 @@ sub _LinkLimit {
              );
 
     #If we're searching on base, join the target to ticket.id
-    $sb->Join( ALIAS1 => 'main',     FIELD1 => $sb->{'primary_key'},
-              ALIAS2 => $LinkAlias, FIELD2 => 'LocalTarget');
+    $sb->_SQLJoin( ALIAS1 => 'main',     FIELD1 => $sb->{'primary_key'},
+              ALIAS2 => $sb->{_sql_linkalias}, FIELD2 => 'LocalTarget');
 
   } else {
     die "Invalid link direction '$meta->[1]' for $field\n";
@@ -337,7 +373,7 @@ sub _LinkLimit {
 Handle date fields.  (Created, LastTold..)
 
 Meta Data:
-  1: type of relationship.  (Probably not necessary.)
+  1: type of link.  (Probably not necessary.)
 
 =cut
 
@@ -345,7 +381,7 @@ sub _DateLimit {
   my ($sb,$field,$op,$value,@rest) = @_;
 
   die "Invalid Date Op: $op"
-     unless $op =~ /^(=|!=|>|<|>=|<=)$/;
+     unless $op =~ /^(=|>|<|>=|<=)$/;
 
   my $meta = $FIELDS{$field};
   die "Incorrect Meta Data for $field"
@@ -354,18 +390,52 @@ sub _DateLimit {
   require Time::ParseDate;
   use POSIX 'strftime';
 
+  # FIXME: Replace me with RT::Date( Type => 'unknown' ...)
   my $time = Time::ParseDate::parsedate( $value,
                        UK => $RT::DateDayBeforeMonth,
                        PREFER_PAST => $RT::AmbiguousDayInPast,
-                       PREFER_FUTURE => !($RT::AmbiguousDayInPast));
-  $value = strftime("%Y-%m-%d %H:%M",localtime($time));
+                       PREFER_FUTURE => !($RT::AmbiguousDayInPast),
+                        FUZZY => 1
+                                      );
 
-  $sb->_SQLLimit(
-            FIELD => $meta->[1],
-            OPERATOR => $op,
-            VALUE => $value,
-            @rest,
-           );
+  if ($op eq "=") {
+    # if we're specifying =, that means we want everything on a
+    # particular single day.  in the database, we need to check for >
+    # and < the edges of that day.
+
+    my $daystart = strftime("%Y-%m-%d %H:%M",
+                           gmtime($time - ( $time % 86400 )));
+    my $dayend   = strftime("%Y-%m-%d %H:%M",
+                           gmtime($time + ( 86399 - $time % 86400 )));
+
+    $sb-> _OpenParen;
+
+    $sb->_SQLLimit(
+                  FIELD => $meta->[1],
+                  OPERATOR => ">=",
+                  VALUE => $daystart,
+                  @rest,
+                 );
+
+    $sb->_SQLLimit(
+                  FIELD => $meta->[1],
+                  OPERATOR => "<=",
+                  VALUE => $dayend,
+                  @rest,
+                  ENTRYAGGREGATOR => 'AND',
+                 );
+
+    $sb-> _CloseParen;
+
+  } else {
+    $value = strftime("%Y-%m-%d %H:%M", gmtime($time));
+    $sb->_SQLLimit(
+                  FIELD => $meta->[1],
+                  OPERATOR => $op,
+                  VALUE => $value,
+                  @rest,
+                 );
+  }
 }
 
 =head2 _StringLimit
@@ -417,16 +487,16 @@ sub _TransDateLimit {
   $sb->_OpenParen;
 
   # Join Transactions To Attachments
-  $sb->Join( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
+  $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
             ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
 
   # Join Transactions to Tickets
-  $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
+  $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
             ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
 
   my $d = new RT::Date( $sb->CurrentUser );
-  $d->Set($value);
-  $value = $d->ISO;
+  $d->Set( Format => 'ISO', Value => $value);
+   $value = $d->ISO;
 
   #Search for the right field
   $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
@@ -491,14 +561,6 @@ sub _TransLimit {
 
   $sb->_OpenParen;
 
-  # Join Transactions To Attachments
-  $sb->Join( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
-            ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
-
-  # Join Transactions to Tickets
-  $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
-            ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
-
   #Search for the right field
   $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
                 FIELD =>    $field,
@@ -508,6 +570,14 @@ sub _TransLimit {
                 @rest
                );
 
+  # Join Transactions To Attachments
+  $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
+            ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
+
+  # Join Transactions to Tickets
+  $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
+            ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
+
   $sb->_CloseParen;
 
 }
@@ -519,74 +589,162 @@ Handle watcher limits.  (Requestor, CC, etc..)
 Meta Data:
   1: Field to query on
 
-=cut
 
-sub _WatcherLimit {
-  my ($self,$field,$op,$value,@rest) = @_;
-  my %rest = @rest;
+=begin testing
 
-  $self->_OpenParen;
+# Test to make sure that you can search for tickets by requestor address and
+# by requestor name.
 
-  my $groups       = $self->NewAlias('Groups');
-  my $group_princs  = $self->NewAlias('Principals');
-  my $groupmembers  = $self->NewAlias('CachedGroupMembers');
-  my $member_princs = $self->NewAlias('Principals');
-  my $users        = $self->NewAlias('Users');
+my ($id,$msg);
+my $u1 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
+ok ($id,$msg);
+my $u2 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
+ok ($id,$msg);
 
+my $t1 = RT::Ticket->new($RT::SystemUser);
+my ($trans);
+($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
+ok ($id, $msg);
 
-  #Find user watchers
-#  my $subclause = undef;
-#  my $aggregator = 'OR';
-#  if ($restriction->{'OPERATOR'} =~ /!|NOT/i ){
-#    $subclause = 'AndEmailIsNot';
-#    $aggregator = 'AND';
-#  }
+my $t2 = RT::Ticket->new($RT::SystemUser);
+($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
+ok ($id, $msg);
 
 
-  $self->_SQLLimit(ALIAS => $users,
-                  FIELD => $rest{SUBKEY} || 'EmailAddress',
-                  VALUE           => $value,
-                  OPERATOR        => $op,
-                  CASESENSITIVE   => 0,
-                  @rest,
-                 );
+my $t3 = RT::Ticket->new($RT::SystemUser);
+($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
+ok ($id, $msg);
 
-  # {{{ Tie to groups for tickets we care about
-  $self->_SQLLimit(ALIAS => $groups,
-                  FIELD => 'Domain',
-                  VALUE => 'RT::Ticket-Role',
-                  ENTRYAGGREGATOR => 'AND');
 
-  $self->Join(ALIAS1 => $groups, FIELD1 => 'Instance',
-             ALIAS2 => 'main',   FIELD2 => 'id');
-  # }}}
+my $tix1 = RT::Tickets->new($RT::SystemUser);
+$tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
 
-  # If we care about which sort of watcher
-  my $meta = $FIELDS{$field};
-  my $type = ( defined $meta->[1] ? $meta->[1] : undef );
+is ($tix1->Count, 3);
 
-  if ( $type ) {
-    $self->_SQLLimit(ALIAS => $groups,
-                    FIELD => 'Type',
-                    VALUE => $type,
-                    ENTRYAGGREGATOR => 'AND');
-  }
+my $tix2 = RT::Tickets->new($RT::SystemUser);
+$tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
+
+is ($tix2->Count, 3);
+
+
+my $tix3 = RT::Tickets->new($RT::SystemUser);
+$tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
+
+is ($tix3->Count, 2);
+
+my $tix4 = RT::Tickets->new($RT::SystemUser);
+$tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
 
-  $self->Join (ALIAS1 => $groups,  FIELD1 => 'id',
-              ALIAS2 => $group_princs, FIELD2 => 'ObjectId');
-  $self->_SQLLimit(ALIAS => $group_princs,
-                  FIELD => 'PrincipalType',
-                  VALUE => 'Group',
-                  ENTRYAGGREGATOR => 'AND');
-  $self->Join( ALIAS1 => $group_princs, FIELD1 => 'id',
-              ALIAS2 => $groupmembers, FIELD2 => 'GroupId');
+is ($tix4->Count, 2);
 
-  $self->Join( ALIAS1 => $groupmembers, FIELD1 => 'MemberId',
-              ALIAS2 => $member_princs, FIELD2 => 'id');
-  $self->Join (ALIAS1 => $member_princs, FIELD1 => 'ObjectId',
-              ALIAS2 => $users, FIELD2 => 'id');
+# Searching for tickets that have two requestors isn't supported
+# There's no way to differentiate "one requestor name that matches foo and bar"
+# and "two requestors, one matching foo and one matching bar"
 
- $self->_CloseParen;
+# my $tix5 = RT::Tickets->new($RT::SystemUser);
+# $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
+# 
+# is ($tix5->Count, 1);
+# 
+# my $tix6 = RT::Tickets->new($RT::SystemUser);
+# $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
+# 
+# is ($tix6->Count, 1);
+
+
+=end testing
+
+=cut
+
+sub _WatcherLimit {
+    my $self  = shift;
+    my $field = shift;
+    my $op    = shift;
+    my $value = shift;
+    my %rest  = (@_);
+
+    $self->_OpenParen;
+
+    my $groups       = $self->NewAlias('Groups');
+    my $groupmembers = $self->NewAlias('CachedGroupMembers');
+    my $users        = $self->NewAlias('Users');
+
+    # If we're looking for multiple watchers of a given type,
+    # TicketSQL will be handing it to us as an array of cluases in
+    # $field
+    if ( ref $field ) {    # gross hack
+        $self->_OpenParen;
+        for my $chunk (@$field) {
+            ( $field, $op, $value, %rest ) = @$chunk;
+            $self->_SQLLimit(
+                ALIAS         => $users,
+                FIELD         => $rest{SUBKEY} || 'EmailAddress',
+                VALUE         => $value,
+                OPERATOR      => $op,
+                CASESENSITIVE => 0,
+                %rest
+            );
+        }
+        $self->_CloseParen;
+    }
+    else {
+        $self->_SQLLimit(
+            ALIAS         => $users,
+            FIELD         => $rest{SUBKEY} || 'EmailAddress',
+            VALUE         => $value,
+            OPERATOR      => $op,
+            CASESENSITIVE => 0,
+            %rest,
+        );
+    }
+
+    # {{{ Tie to groups for tickets we care about
+    $self->_SQLLimit(
+        ALIAS           => $groups,
+        FIELD           => 'Domain',
+        VALUE           => 'RT::Ticket-Role',
+        ENTRYAGGREGATOR => 'AND'
+    );
+
+    $self->_SQLJoin(
+        ALIAS1 => $groups,
+        FIELD1 => 'Instance',
+        ALIAS2 => 'main',
+        FIELD2 => 'id'
+    );
+
+    # }}}
+
+    # If we care about which sort of watcher
+    my $meta = $FIELDS{$field};
+    my $type = ( defined $meta->[1] ? $meta->[1] : undef );
+
+    if ($type) {
+        $self->_SQLLimit(
+            ALIAS           => $groups,
+            FIELD           => 'Type',
+            VALUE           => $type,
+            ENTRYAGGREGATOR => 'AND'
+        );
+    }
+
+    $self->_SQLJoin(
+        ALIAS1 => $groups,
+        FIELD1 => 'id',
+        ALIAS2 => $groupmembers,
+        FIELD2 => 'GroupId'
+    );
+
+    $self->_SQLJoin(
+        ALIAS1 => $groupmembers,
+        FIELD1 => 'MemberId',
+        ALIAS2 => $users,
+        FIELD2 => 'id'
+    );
+
+    $self->_CloseParen;
 
 }
 
@@ -620,7 +778,7 @@ sub _LinkFieldLimit {
                        OPERATOR => '=',
                        VALUE =>    $restriction->{'TARGET'} );
     #If we're searching on target, join the base to ticket.id
-    $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
+    $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
                 ALIAS2 => $LinkAlias,
                 FIELD2 => 'LocalBase');
   }
@@ -641,7 +799,7 @@ sub _LinkFieldLimit {
                        OPERATOR => '=',
                        VALUE =>    $restriction->{'BASE'} );
     #If we're searching on base, join the target to ticket.id
-    $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
+    $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
                 ALIAS2 => $LinkAlias,
                 FIELD2 => 'LocalTarget')
   }
@@ -664,47 +822,51 @@ sub _CustomFieldLimit {
   my $field = $rest{SUBKEY} || die "No field specified";
 
   # For our sanity, we can only limit on one queue at a time
-  my $queue = undef;
-  # Ugh.    This will not do well for things with underscores in them
+  my $queue = 0;
 
-  use RT::CustomFields;
-  my $CF = RT::CustomFields->new( $self->CurrentUser );
-  #$CF->Load( $cfid} );
 
-  my $q;
   if ($field =~ /^(.+?)\.{(.+)}$/) {
-    my $q = RT::Queue->new($self->CurrentUser);
-    $q->Load($1);
+    $queue =  $1;
     $field = $2;
-    $CF->LimitToQueue( $q->Id );
-    $queue = $q->Id;
-  } else {
-    $CF->LimitToGlobal;
-  }
-  $CF->FindAllRows;
+   }
+    $field = $1 if $field =~ /^{(.+)}$/; # trim { }
 
-  my $cfid = 0;
+    my $q = RT::Queue->new($self->CurrentUser);
+    $q->Load($queue) if ($queue);
 
-  while ( my $CustomField = $CF->Next ) {
-    if ($CustomField->Name eq $field) {
-      $cfid = $CustomField->Id;
-      last;
+    my $cf;
+    if ($q->id) {
+        $cf = $q->CustomField($field);
     }
-  }
-  die "No custom field named $field found\n"
-    unless $cfid;
+    else { 
+        $cf = RT::CustomField->new($self->CurrentUser);
+        $cf->LoadByNameAndQueue(Queue => '0', Name => $field);
+    }
+
+
+
+
+
+  my $cfid = $cf->id;
+
+  die "No custom field named $field found\n" unless $cfid;
 
-#   use RT::CustomFields;
-#   my $CF = RT::CustomField->new( $self->CurrentUser );
-#   $CF->Load( $cfid );
 
 
   my $null_columns_ok;
-  my $TicketCFs = $self->Join( TYPE   => 'left',
-                              ALIAS1 => 'main',
-                              FIELD1 => 'id',
-                              TABLE2 => 'TicketCustomFieldValues',
-                              FIELD2 => 'Ticket' );
+
+  my $TicketCFs;
+  # Perform one Join per CustomField
+  if ($self->{_sql_keywordalias}{$cfid}) {
+    $TicketCFs = $self->{_sql_keywordalias}{$cfid};
+  } else {
+    $TicketCFs = $self->{_sql_keywordalias}{$cfid} =
+      $self->_SQLJoin( TYPE   => 'left',
+                  ALIAS1 => 'main',
+                  FIELD1 => 'id',
+                  TABLE2 => 'TicketCustomFieldValues',
+                  FIELD2 => 'Ticket' );
+  }
 
   $self->_OpenParen;
 
@@ -715,15 +877,16 @@ sub _CustomFieldLimit {
                    QUOTEVALUE => 1,
                    @rest );
 
-  if (   $op =~ /^IS$/i
-        or ( $op eq '!=' ) ) {
+
+   # If we're trying to find custom fields that don't match something, we want tickets
+   # where the custom field has no value at all
+
+  if (   ($op =~ /^IS$/i) || ($op =~ /^NOT LIKE$/i) || ( $op eq '!=' ) ) {
     $null_columns_ok = 1;
   }
+    
 
-  #If we're trying to find tickets where the keyword isn't somethng,
-  #also check ones where it _IS_ null
-
-  if ( $op eq '!=' ) {
+  if ( $null_columns_ok && $op !~ /IS/i && uc $value ne "NULL") {
     $self->_SQLLimit( ALIAS           => $TicketCFs,
                      FIELD           => 'Content',
                      OPERATOR        => 'IS',
@@ -801,14 +964,20 @@ sub Limit {
 Returns a frozen string suitable for handing back to ThawLimits.
 
 =cut
+
+sub _FreezeThawKeys {
+    'TicketRestrictions',
+    'restriction_index',
+    'looking_at_effective_id',
+    'looking_at_type'
+}
+
 # {{{ sub FreezeLimits
 
 sub FreezeLimits {
        my $self = shift;
        require FreezeThaw;
-       return (FreezeThaw::freeze($self->{'TicketRestrictions'},
-                                  $self->{'restriction_index'}
-                                 ));
+       return (FreezeThaw::freeze(@{$self}{$self->_FreezeThawKeys}));
 }
 
 # }}}
@@ -835,10 +1004,9 @@ sub ThawLimits {
        #We don't need to die if the thaw fails.
        
        eval {
-               ($self->{'TicketRestrictions'},
-               $self->{'restriction_index'}
-               ) = FreezeThaw::thaw($in);
-       }
+               @{$self}{$self->_FreezeThawKeys} = FreezeThaw::thaw($in);
+       };
+       $RT::Logger->error( $@ ) if $@;
 
 }
 
@@ -1321,7 +1489,7 @@ sub LimitRequestor {
 =head2 LimitLinkedTo
 
 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
-TYPE limits the sort of relationship we want to search on
+TYPE limits the sort of link we want to search on
 
 TYPE = { RefersTo, MemberOf, DependsOn }
 
@@ -1357,7 +1525,7 @@ sub LimitLinkedTo {
 =head2 LimitLinkedFrom
 
 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
-TYPE limits the sort of relationship we want to search on
+TYPE limits the sort of link we want to search on
 
 
 BASE is the id or URI of the BASE of the link
@@ -1373,11 +1541,18 @@ sub LimitLinkedFrom {
                 TYPE => undef,
                 @_);
 
+    # translate RT2 From/To naming to RT3 TicketSQL naming
+    my %fromToMap = qw(DependsOn DependentOn
+                      MemberOf  HasMember
+                      RefersTo  ReferredToBy);
+
+    my $type = $args{'TYPE'};
+    $type = $fromToMap{$type} if exists($fromToMap{$type});
 
     $self->Limit( FIELD => 'LinkedTo',
                  TARGET => undef,
                  BASE => ($args{'BASE'} || $args{'TICKET'}),
-                 TYPE => $args{'TYPE'},
+                 TYPE => $type,
                  DESCRIPTION => $self->loc(
                   "Tickets [_1] [_2]", $self->loc($args{'TYPE'}), ($args{'BASE'} || $args{'TICKET'})
                  ),
@@ -1581,11 +1756,12 @@ Takes a paramhash of key/value pairs with the following keys:
 
 =over 4
 
-=item KEYWORDSELECT - KeywordSelect id
+=item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional
+parameter QUEUE may also be passed to distinguish the custom field.
 
-=item OPERATOR - (for KEYWORD only - KEYWORDSELECT operator is always `=')
+=item OPERATOR - The usual Limit operators
 
-=item KEYWORD - Keyword id
+=item VALUE - The value to compare against
 
 =back
 
@@ -1601,9 +1777,14 @@ sub LimitCustomField {
                  QUOTEVALUE    => 1,
                  @_ );
 
-    use RT::CustomFields;
     my $CF = RT::CustomField->new( $self->CurrentUser );
-    $CF->Load( $args{CUSTOMFIELD} );
+    if ( $args{CUSTOMFIELD} =~ /^\d+$/) {
+       $CF->Load( $args{CUSTOMFIELD} );
+    }
+    else {
+       $CF->LoadByNameAndQueue( Name => $args{CUSTOMFIELD}, Queue => $args{QUEUE} );
+       $args{CUSTOMFIELD} = $CF->Id;
+    }
 
     #If we are looking to compare with a null value.
     if ( $args{'OPERATOR'} =~ /^is$/i ) {
@@ -1618,10 +1799,6 @@ sub LimitCustomField {
         $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] [_2] [_3]",  $CF->Name , $args{OPERATOR} , $args{VALUE});
     }
 
-#    my $index = $self->_NextIndex;
-#    %{ $self->{'TicketRestrictions'}{$index} } = %args;
-
-
     my $q = "";
     if ($CF->Queue) {
       my $qo = new RT::Queue( $self->CurrentUser );
@@ -1629,6 +1806,10 @@ sub LimitCustomField {
       $q = $qo->Name;
     }
 
+    my @rest;
+    @rest = ( ENTRYAGGREGATOR => 'AND' )
+      if ($CF->Type eq 'SelectMultiple');
+
     $self->Limit( VALUE => $args{VALUE},
                  FIELD => "CF.".( $q
                             ? $q . ".{" . $CF->Name . "}"
@@ -1636,11 +1817,11 @@ sub LimitCustomField {
                           ),
                  OPERATOR => $args{OPERATOR},
                  CUSTOMFIELD => 1,
+                 @rest,
                );
 
 
     $self->{'RecalcTicketLimits'} = 1;
-  #  return ($index);
 }
 
 # }}}
@@ -1676,6 +1857,7 @@ sub _Init  {
     $self->{'primary_key'} = "id";
     delete $self->{'items_array'};
     delete $self->{'item_map'};
+    delete $self->{'columns_to_display'};
     $self->SUPER::_Init(@_);
 
     $self->_InitSQL;
@@ -1720,6 +1902,7 @@ sub ItemsArrayRef {
             push ( @{ $self->{'items_array'} }, $item );
         }
         $self->GotoItem($placeholder);
+        $self->{'items_array'} = $self->ItemsOrderBy($self->{'items_array'});
     }
     return ( $self->{'items_array'} );
 }
@@ -1924,12 +2107,19 @@ sub _RestrictionsToClauses {
     # defined $restriction->{'TARGET'} ?
     # $restriction->{TARGET} )
 
-    my $ea = $DefaultEA{$type};
+    my $ea = $restriction->{ENTRYAGGREGATOR} || $DefaultEA{$type} || "AND";
     if ( ref $ea ) {
       die "Invalid operator $op for $field ($type)"
        unless exists $ea->{$op};
       $ea = $ea->{$op};
     }
+
+    # Each CustomField should be put into a different Clause so they
+    # are ANDed together.
+    if ($restriction->{CUSTOMFIELD}) {
+      $realfield = $field;
+    }
+
     exists $clause{$realfield} or $clause{$realfield} = [];
     # Escape Quotes
     $field =~ s!(['"])!\\$1!g;
@@ -1963,6 +2153,11 @@ sub _ProcessRestrictions {
     #a new search
     delete $self->{'TicketAliases'};
     delete $self->{'items_array'};                                                                                                                   
+    delete $self->{'item_map'};
+    delete $self->{'raw_rows'};
+    delete $self->{'rows'};
+    delete $self->{'count_all'};
     my $sql = $self->{_sql_query}; # Violating the _SQL namespace
     if (!$sql||$self->{'RecalcTicketLimits'}) {
       #  "Restrictions to Clauses Branch\n";
@@ -1995,15 +2190,15 @@ sub _BuildItemMap {
 
     delete $self->{'item_map'};
     if ($items->[0]) {
-    $self->{'item_map'}->{'first'} = $items->[0]->Id;
-    while (my $item = shift @$items ) {
-        my $id = $item->Id;
-        $self->{'item_map'}->{$id}->{'defined'} = 1;
-        $self->{'item_map'}->{$id}->{prev}  = $prev;
-        $self->{'item_map'}->{$id}->{next}  = $items->[0]->Id if ($items->[0]);
-        $prev = $id;
-    }
-    $self->{'item_map'}->{'last'} = $prev;
+        $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
+        while (my $item = shift @$items ) {
+            my $id = $item->EffectiveId;
+            $self->{'item_map'}->{$id}->{'defined'} = 1;
+            $self->{'item_map'}->{$id}->{prev}  = $prev;
+            $self->{'item_map'}->{$id}->{next}  = $items->[0]->EffectiveId if ($items->[0]);
+            $prev = $id;
+        }
+        $self->{'item_map'}->{'last'} = $prev;
     }
 } 
 
@@ -2014,14 +2209,14 @@ Returns an a map of all items found by this search. The map is of the form
 
 $ItemMap->{'first'} = first ticketid found
 $ItemMap->{'last'} = last ticketid found
-$ItemMap->{$id}->{prev} = the tikcet id found before $id
-$ItemMap->{$id}->{next} = the tikcet id found after $id
+$ItemMap->{$id}->{prev} = the ticket id found before $id
+$ItemMap->{$id}->{next} = the ticket id found after $id
 
 =cut
 
 sub ItemMap {
     my $self = shift;
-    $self->_BuildItemMap() unless ($self->{'item_map'});
+    $self->_BuildItemMap() unless ($self->{'items_array'} and $self->{'item_map'});
     return ($self->{'item_map'});
 }