X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FTickets_Overlay.pm;h=0e6585c07094aae51b1707a7859060fe6a3baa3b;hp=969d887cf492af6c7c531ea6525f79b547f16981;hb=673b9a458d9138523026963df6fa3b4683e09bae;hpb=5fc8c5edf574ab024d4646914b6432d458e2ffbd diff --git a/rt/lib/RT/Tickets_Overlay.pm b/rt/lib/RT/Tickets_Overlay.pm index 969d887cf..0e6585c07 100644 --- a/rt/lib/RT/Tickets_Overlay.pm +++ b/rt/lib/RT/Tickets_Overlay.pm @@ -1,8 +1,14 @@ -# BEGIN LICENSE BLOCK +# BEGIN BPS TAGGED BLOCK {{{ # -# Copyright (c) 1996-2003 Jesse Vincent +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC +# # -# (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 @@ -14,13 +20,29 @@ # 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.) # +# 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 LICENSE BLOCK +# END BPS TAGGED BLOCK }}} # Major Changes: # - Decimated ProcessRestrictions and broke it into multiple @@ -51,42 +73,51 @@ =begin testing ok (require RT::Tickets); +ok( my $testtickets = RT::Tickets->new( $RT::SystemUser ) ); +ok( $testtickets->LimitStatus( VALUE => 'deleted' ) ); +# Should be zero until 'allow_deleted_search' +ok( $testtickets->Count == 0 ); =end testing =cut + +package RT::Tickets; + use strict; + +package RT::Tickets; + no warnings qw(redefine); use vars qw(@SORTFIELDS); - +use RT::CustomFields; # Configuration Tables: # FIELDS is a mapping of searchable Field name, to Type, and other # metadata. -my %FIELDS = - ( Status => ['ENUM'], - Queue => ['ENUM' => 'Queue',], - Type => ['ENUM',], - Creator => ['ENUM' => 'User',], - LastUpdatedBy => ['ENUM' => 'User',], - Owner => ['ENUM' => 'User',], - EffectiveId => ['INT',], - id => ['INT',], - InitialPriority => ['INT',], - FinalPriority => ['INT',], - Priority => ['INT',], - TimeLeft => ['INT',], - TimeWorked => ['INT',], - MemberOf => ['LINK' => To => 'MemberOf', ], - DependsOn => ['LINK' => To => 'DependsOn',], - RefersTo => ['LINK' => To => 'RefersTo',], - HasMember => ['LINK' => From => 'MemberOf',], - DependentOn => ['LINK' => From => 'DependsOn',], - ReferredToBy => ['LINK' => From => 'RefersTo',], -# HasDepender => ['LINK',], -# RelatedTo => ['LINK',], +my %FIELDS = ( + Status => ['ENUM'], + Queue => [ 'ENUM' => 'Queue', ], + Type => [ 'ENUM', ], + Creator => [ 'ENUM' => 'User', ], + LastUpdatedBy => [ 'ENUM' => 'User', ], + Owner => [ 'ENUM' => 'User', ], + EffectiveId => [ 'INT', ], + id => [ 'INT', ], + InitialPriority => [ 'INT', ], + FinalPriority => [ 'INT', ], + Priority => [ 'INT', ], + TimeLeft => [ 'INT', ], + TimeWorked => [ 'INT', ], + MemberOf => [ 'LINK' => To => 'MemberOf', ], + DependsOn => [ 'LINK' => To => 'DependsOn', ], + RefersTo => [ 'LINK' => To => 'RefersTo', ], + HasMember => [ 'LINK' => From => 'MemberOf', ], + DependentOn => [ 'LINK' => From => 'DependsOn', ], + DependedOnBy => [ 'LINK' => From => 'DependsOn', ], + ReferredToBy => [ 'LINK' => From => 'RefersTo', ], Told => ['DATE' => 'Told',], Starts => ['DATE' => 'Starts',], Started => ['DATE' => 'Started',], @@ -95,75 +126,82 @@ my %FIELDS = LastUpdated => ['DATE' => 'LastUpdated',], Created => ['DATE' => 'Created',], Subject => ['STRING',], - Type => ['STRING',], Content => ['TRANSFIELD',], ContentType => ['TRANSFIELD',], Filename => ['TRANSFIELD',], TransactionDate => ['TRANSDATE',], Requestor => ['WATCHERFIELD' => 'Requestor',], + Requestors => ['WATCHERFIELD' => 'Requestor',], Cc => ['WATCHERFIELD' => 'Cc',], - AdminCc => ['WATCHERFIELD' => 'AdminCC',], + AdminCc => ['WATCHERFIELD' => 'AdminCc',], Watcher => ['WATCHERFIELD'], LinkedTo => ['LINKFIELD',], CustomFieldValue =>['CUSTOMFIELD',], CF => ['CUSTOMFIELD',], - ); + Updated => [ 'TRANSDATE', ], + RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], + CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], + AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], + WatcherGroup => [ 'MEMBERSHIPFIELD', ], +); # Mapping of Field Type to Function -my %dispatch = - ( ENUM => \&_EnumLimit, - INT => \&_IntLimit, - LINK => \&_LinkLimit, - DATE => \&_DateLimit, - STRING => \&_StringLimit, - TRANSFIELD => \&_TransLimit, - TRANSDATE => \&_TransDateLimit, +my %dispatch = ( + ENUM => \&_EnumLimit, + INT => \&_IntLimit, + LINK => \&_LinkLimit, + DATE => \&_DateLimit, + STRING => \&_StringLimit, + TRANSFIELD => \&_TransLimit, + TRANSDATE => \&_TransDateLimit, WATCHERFIELD => \&_WatcherLimit, - LINKFIELD => \&_LinkFieldLimit, - CUSTOMFIELD => \&_CustomFieldLimit, - ); -my %can_bundle = - ( WATCHERFIELD => "yeps", - ); + MEMBERSHIPFIELD => \&_WatcherMembershipLimit, + 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 => { '=' => 'OR', - '>='=> 'AND', - '<='=> 'AND', - '>' => 'AND', - '<' => 'AND' - }, - STRING => { '=' => 'OR', - '!='=> 'AND', - 'LIKE'=> 'AND', - 'NOT LIKE' => 'AND' - }, - TRANSFIELD => 'AND', - TRANSDATE => 'AND', - LINK => 'OR', - LINKFIELD => 'AND', - TARGET => 'AND', - BASE => 'AND', - WATCHERFIELD => { '=' => 'OR', - '!='=> 'AND', - 'LIKE'=> 'OR', - 'NOT LIKE' => 'AND' - }, - - CUSTOMFIELD => 'OR', - ); - + INT => 'AND', + ENUM => { + '=' => 'OR', + '!=' => 'AND' + }, + DATE => { + '=' => 'OR', + '>=' => 'AND', + '<=' => 'AND', + '>' => 'AND', + '<' => 'AND' + }, + STRING => { + '=' => 'OR', + '!=' => 'AND', + 'LIKE' => 'AND', + 'NOT LIKE' => 'AND' + }, + TRANSFIELD => 'AND', + TRANSDATE => 'AND', + LINK => 'OR', + LINKFIELD => 'AND', + TARGET => 'AND', + BASE => 'AND', + WATCHERFIELD => { + '=' => 'OR', + '!=' => 'AND', + 'LIKE' => 'OR', + 'NOT LIKE' => 'AND' + }, + + CUSTOMFIELD => 'OR', +); # Helper functions for passing the above lexically scoped tables above # into Tickets_Overlay_SQL. -sub FIELDS { return \%FIELDS } -sub dispatch { return \%dispatch } +sub FIELDS { return \%FIELDS } +sub dispatch { return \%dispatch } sub can_bundle { return \%can_bundle } # Bring in the clowns. @@ -172,10 +210,10 @@ require RT::Tickets_Overlay_SQL; # {{{ sub SortFields @SORTFIELDS = qw(id Status - Queue Subject - Owner Created Due Starts Started - Told - Resolved LastUpdated Priority TimeWorked TimeLeft); + Queue Subject + Owner Created Due Starts Started + Told + Resolved LastUpdated Priority TimeWorked TimeLeft); =head2 SortFields @@ -184,14 +222,12 @@ Returns the list of fields that lists of tickets can easily be sorted by =cut sub SortFields { - my $self = shift; - return(@SORTFIELDS); + my $self = shift; + return (@SORTFIELDS); } - # }}} - # BEGIN SQL STUFF ********************************* =head1 Limit Helper Routines @@ -226,26 +262,27 @@ Meta Data: =cut sub _EnumLimit { - my ($sb,$field,$op,$value,@rest) = @_; + my ( $sb, $field, $op, $value, @rest ) = @_; - # SQL::Statement changes != to <>. (Can we remove this now?) - $op = "!=" if $op eq "<>"; + # SQL::Statement changes != to <>. (Can we remove this now?) + $op = "!=" if $op eq "<>"; - die "Invalid Operation: $op for $field" - unless $op eq "=" or $op eq "!="; + die "Invalid Operation: $op for $field" + unless $op eq "=" or $op eq "!="; - my $meta = $FIELDS{$field}; - if (defined $meta->[1]) { - my $class = "RT::" . $meta->[1]; - my $o = $class->new($sb->CurrentUser); - $o->Load( $value ); - $value = $o->Id; - } - $sb->_SQLLimit( FIELD => $field,, - VALUE => $value, - OPERATOR => $op, - @rest, - ); + my $meta = $FIELDS{$field}; + if ( defined $meta->[1] ) { + my $class = "RT::" . $meta->[1]; + my $o = $class->new( $sb->CurrentUser ); + $o->Load($value); + $value = $o->Id; + } + $sb->_SQLLimit( + FIELD => $field, + VALUE => $value, + OPERATOR => $op, + @rest, + ); } =head2 _IntLimit @@ -259,89 +296,138 @@ Meta Data: =cut sub _IntLimit { - my ($sb,$field,$op,$value,@rest) = @_; + my ( $sb, $field, $op, $value, @rest ) = @_; - die "Invalid Operator $op for $field" - unless $op =~ /^(=|!=|>|<|>=|<=)$/; + die "Invalid Operator $op for $field" + unless $op =~ /^(=|!=|>|<|>=|<=)$/; - $sb->_SQLLimit( - FIELD => $field, - VALUE => $value, - OPERATOR => $op, - @rest, - ); + $sb->_SQLLimit( + FIELD => $field, + VALUE => $value, + OPERATOR => $op, + @rest, + ); } - =head2 _LinkLimit 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 sub _LinkLimit { - my ($sb,$field,$op,$value,@rest) = @_; - - die "Op must be =" - unless $op eq "="; - - my $meta = $FIELDS{$field}; - die "Incorrect Meta Data for $field" - unless (defined $meta->[1] and defined $meta->[2]); + my ( $sb, $field, $op, $value, @rest ) = @_; - $sb->{_sql_linkalias} = $sb->NewAlias ('Links') - unless defined $sb->{_sql_linkalias}; + my $meta = $FIELDS{$field}; + die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS)/io; - $sb->_OpenParen(); + die "Incorrect Metadata for $field" + unless ( defined $meta->[1] and defined $meta->[2] ); - $sb->_SQLLimit( - ALIAS => $sb->{_sql_linkalias}, - FIELD => 'Type', - OPERATOR => '=', - VALUE => $meta->[2], - @rest, - ); + my $direction = $meta->[1]; - if ($meta->[1] eq "To") { - my $matchfield = ( $value =~ /^(\d+)$/ ? "LocalTarget" : "Target" ); - - $sb->_SQLLimit( - ALIAS => $sb->{_sql_linkalias}, - ENTRYAGGREGATOR => 'AND', - FIELD => $matchfield, - OPERATOR => '=', - VALUE => $value , - ); + my $matchfield; + my $linkfield; + my $is_local = 1; + my $is_null = 0; + if ( $direction eq 'To' ) { + $matchfield = "Target"; + $linkfield = "Base"; - #If we're searching on target, join the base to ticket.id - $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" ); + } + elsif ( $direction eq 'From' ) { + $linkfield = "Target"; + $matchfield = "Base"; - $sb->_SQLLimit( - ALIAS => $sb->{_sql_linkalias}, - ENTRYAGGREGATOR => 'AND', - FIELD => $matchfield, - OPERATOR => '=', - VALUE => $value , - ); + } + else { + die "Invalid link direction '$meta->[1]' for $field\n"; + } - #If we're searching on base, join the target to ticket.id - $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, - ALIAS2 => $sb->{_sql_linkalias}, FIELD2 => 'LocalTarget'); + if ( $op eq '=' || $op =~ /^is/oi ) { + if ( $value eq '' || $value =~ /^null$/io ) { + $is_null = 1; + } + elsif ( $value =~ /\D/o ) { + $is_local = 0; + } + else { + $is_local = 1; + } + } - } else { - die "Invalid link direction '$meta->[1]' for $field\n"; - } +#For doing a left join to find "unlinked tickets" we want to generate a query that looks like this +# SELECT main.* FROM Tickets main +# LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf') +# AND(main.id = Links_1.LocalTarget)) +# WHERE ((main.EffectiveId = main.id)) +# AND ((main.Status != 'deleted')) +# AND (Links_1.LocalBase IS NULL); + + if ($is_null) { + my $linkalias = $sb->Join( + TYPE => 'left', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'Links', + FIELD2 => 'Local' . $linkfield + ); + + $sb->SUPER::Limit( + LEFTJOIN => $linkalias, + FIELD => 'Type', + OPERATOR => '=', + VALUE => $meta->[2], + @rest, + ); + + $sb->_SQLLimit( + ALIAS => $linkalias, + ENTRYAGGREGATOR => 'AND', + FIELD => ( $is_local ? "Local$matchfield" : $matchfield ), + OPERATOR => 'IS', + VALUE => 'NULL', + QUOTEVALUE => '0', + ); - $sb->_CloseParen(); + } + else { + $sb->{_sql_linkalias} = $sb->NewAlias('Links') + unless defined $sb->{_sql_linkalias}; + + $sb->_OpenParen(); + + $sb->_SQLLimit( + ALIAS => $sb->{_sql_linkalias}, + FIELD => 'Type', + OPERATOR => '=', + VALUE => $meta->[2], + @rest, + ); + + $sb->_SQLLimit( + ALIAS => $sb->{_sql_linkalias}, + ENTRYAGGREGATOR => 'AND', + FIELD => ( $is_local ? "Local$matchfield" : $matchfield ), + OPERATOR => '=', + VALUE => $value, + ); + + #If we're searching on target, join the base to ticket.id + $sb->_SQLJoin( + ALIAS1 => 'main', + FIELD1 => $sb->{'primary_key'}, + ALIAS2 => $sb->{_sql_linkalias}, + FIELD2 => 'Local' . $linkfield + ); + + $sb->_CloseParen(); + } } =head2 _DateLimit @@ -349,69 +435,66 @@ sub _LinkLimit { Handle date fields. (Created, LastTold..) Meta Data: - 1: type of relationship. (Probably not necessary.) + 1: type of link. (Probably not necessary.) =cut sub _DateLimit { - my ($sb,$field,$op,$value,@rest) = @_; + my ( $sb, $field, $op, $value, @rest ) = @_; - die "Invalid Date Op: $op" - unless $op =~ /^(=|>|<|>=|<=)$/; + die "Invalid Date Op: $op" + unless $op =~ /^(=|>|<|>=|<=)$/; - my $meta = $FIELDS{$field}; - die "Incorrect Meta Data for $field" - unless (defined $meta->[1]); + my $meta = $FIELDS{$field}; + die "Incorrect Meta Data for $field" + unless ( defined $meta->[1] ); - require Time::ParseDate; - use POSIX 'strftime'; + use POSIX 'strftime'; + + my $date = RT::Date->new($sb->CurrentUser); + $date->Set(Format => 'unknown', Value => $value); + my $time = $date->Unix; - # 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), - FUZZY => 1 - ); + if ( $op eq "=" ) { - 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. + # 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 ))); + 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->_OpenParen; - $sb->_SQLLimit( - FIELD => $meta->[1], - OPERATOR => ">=", - VALUE => $daystart, - @rest, - ); + $sb->_SQLLimit( + FIELD => $meta->[1], + OPERATOR => ">=", + VALUE => $daystart, + @rest, + ); - $sb->_SQLLimit( - FIELD => $meta->[1], - OPERATOR => "<=", - VALUE => $dayend, - @rest, - ENTRYAGGREGATOR => 'AND', - ); + $sb->_SQLLimit( + FIELD => $meta->[1], + OPERATOR => "<=", + VALUE => $dayend, + @rest, + ENTRYAGGREGATOR => 'AND', + ); - $sb-> _CloseParen; + $sb->_CloseParen; - } else { - $value = strftime("%Y-%m-%d %H:%M", gmtime($time)); - $sb->_SQLLimit( - FIELD => $meta->[1], - OPERATOR => $op, - VALUE => $value, - @rest, - ); - } + } + else { + $value = strftime( "%Y-%m-%d %H:%M", gmtime($time) ); + $sb->_SQLLimit( + FIELD => $meta->[1], + OPERATOR => $op, + VALUE => $value, + @rest, + ); + } } =head2 _StringLimit @@ -424,19 +507,19 @@ Meta Data: =cut sub _StringLimit { - my ($sb,$field,$op,$value,@rest) = @_; + my ( $sb, $field, $op, $value, @rest ) = @_; - # FIXME: - # Valid Operators: - # =, !=, LIKE, NOT LIKE + # FIXME: + # Valid Operators: + # =, !=, LIKE, NOT LIKE - $sb->_SQLLimit( - FIELD => $field, - OPERATOR => $op, - VALUE => $value, - CASESENSITIVE => 0, - @rest, - ); + $sb->_SQLLimit( + FIELD => $field, + OPERATOR => $op, + VALUE => $value, + CASESENSITIVE => 0, + @rest, + ); } =head2 _TransDateLimit @@ -450,40 +533,91 @@ Meta Data: =cut +# This routine should really be factored into translimit. sub _TransDateLimit { - my ($sb,$field,$op,$value,@rest) = @_; + my ( $sb, $field, $op, $value, @rest ) = @_; + + # See the comments for TransLimit, they apply here too + + $sb->{_sql_transalias} = $sb->NewAlias('Transactions') + unless defined $sb->{_sql_transalias}; + $sb->{_sql_trattachalias} = $sb->NewAlias('Attachments') + unless defined $sb->{_sql_trattachalias}; + + my $date = RT::Date->new( $sb->CurrentUser ); + $date->Set( Format => 'unknown', Value => $value ); + my $time = $date->Unix; + + $sb->_OpenParen; + 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->_SQLLimit( + ALIAS => $sb->{_sql_transalias}, + FIELD => 'Created', + OPERATOR => ">=", + VALUE => $daystart, + CASESENSITIVE => 0, + @rest + ); + $sb->_SQLLimit( + ALIAS => $sb->{_sql_transalias}, + FIELD => 'Created', + OPERATOR => "<=", + VALUE => $dayend, + CASESENSITIVE => 0, + @rest, + ENTRYAGGREGATOR => 'AND', + ); - # See the comments for TransLimit, they apply here too + } - $sb->{_sql_transalias} = $sb->NewAlias ('Transactions') - unless defined $sb->{_sql_transalias}; - $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments') - unless defined $sb->{_sql_trattachalias}; + # not searching for a single day + else { - $sb->_OpenParen; + #Search for the right field + $sb->_SQLLimit( + ALIAS => $sb->{_sql_transalias}, + FIELD => 'Created', + OPERATOR => $op, + VALUE => $value, + CASESENSITIVE => 0, + @rest + ); + } - # Join Transactions To Attachments - $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId', - ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id'); + # Join Transactions To Attachments - # Join Transactions to Tickets - $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH! - ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket'); + $sb->_SQLJoin( + ALIAS1 => $sb->{_sql_trattachalias}, + FIELD1 => 'TransactionId', + ALIAS2 => $sb->{_sql_transalias}, + FIELD2 => 'id', + ); - my $d = new RT::Date( $sb->CurrentUser ); - $d->Set( Format => 'ISO', Value => $value); - $value = $d->ISO; + # Join Transactions to Tickets + $sb->_SQLJoin( + ALIAS1 => 'main', + FIELD1 => $sb->{'primary_key'}, # UGH! + ALIAS2 => $sb->{_sql_transalias}, + FIELD2 => 'ObjectId' + ); - #Search for the right field - $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias}, - FIELD => 'Created', - OPERATOR => $op, - VALUE => $value, - CASESENSITIVE => 0, - @rest - ); + $sb->SUPER::Limit( + ALIAS => $sb->{_sql_transalias}, + FIELD => 'ObjectType', + VALUE => 'RT::Ticket' + ); - $sb->_CloseParen; + $sb->_CloseParen; } =head2 _TransLimit @@ -496,65 +630,81 @@ Meta Data: =cut sub _TransLimit { - # Content, ContentType, Filename - - # If only this was this simple. We've got to do something - # complicated here: - #Basically, we want to make sure that the limits apply to - #the same attachment, rather than just another attachment - #for the same ticket, no matter how many clauses we lump - #on. We put them in TicketAliases so that they get nuked - #when we redo the join. + # Content, ContentType, Filename - # In the SQL, we might have - # (( Content = foo ) or ( Content = bar AND Content = baz )) - # The AND group should share the same Alias. + # If only this was this simple. We've got to do something + # complicated here: - # Actually, maybe it doesn't matter. We use the same alias and it - # works itself out? (er.. different.) + #Basically, we want to make sure that the limits apply to + #the same attachment, rather than just another attachment + #for the same ticket, no matter how many clauses we lump + #on. We put them in TicketAliases so that they get nuked + #when we redo the join. - # Steal more from _ProcessRestrictions + # In the SQL, we might have + # (( Content = foo ) or ( Content = bar AND Content = baz )) + # The AND group should share the same Alias. - # FIXME: Maybe look at the previous FooLimit call, and if it was a - # TransLimit and EntryAggregator == AND, reuse the Aliases? + # Actually, maybe it doesn't matter. We use the same alias and it + # works itself out? (er.. different.) - # Or better - store the aliases on a per subclause basis - since - # those are going to be the things we want to relate to each other, - # anyway. + # Steal more from _ProcessRestrictions - # maybe we should not allow certain kinds of aggregation of these - # clauses and do a psuedo regex instead? - the problem is getting - # them all into the same subclause when you have (A op B op C) - the - # way they get parsed in the tree they're in different subclauses. + # FIXME: Maybe look at the previous FooLimit call, and if it was a + # TransLimit and EntryAggregator == AND, reuse the Aliases? - my ($sb,$field,$op,$value,@rest) = @_; + # Or better - store the aliases on a per subclause basis - since + # those are going to be the things we want to relate to each other, + # anyway. - $sb->{_sql_transalias} = $sb->NewAlias ('Transactions') - unless defined $sb->{_sql_transalias}; - $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments') - unless defined $sb->{_sql_trattachalias}; + # maybe we should not allow certain kinds of aggregation of these + # clauses and do a psuedo regex instead? - the problem is getting + # them all into the same subclause when you have (A op B op C) - the + # way they get parsed in the tree they're in different subclauses. - $sb->_OpenParen; + my ( $self, $field, $op, $value, @rest ) = @_; - #Search for the right field - $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias}, - FIELD => $field, - OPERATOR => $op, - VALUE => $value, - CASESENSITIVE => 0, - @rest - ); + $self->{_sql_transalias} = $self->NewAlias('Transactions') + unless defined $self->{_sql_transalias}; + $self->{_sql_trattachalias} = $self->NewAlias('Attachments') + unless defined $self->{_sql_trattachalias}; - # Join Transactions To Attachments - $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId', - ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id'); + $self->_OpenParen; - # Join Transactions to Tickets - $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH! - ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket'); + #Search for the right field + $self->_SQLLimit( + ALIAS => $self->{_sql_trattachalias}, + FIELD => $field, + OPERATOR => $op, + VALUE => $value, + CASESENSITIVE => 0, + @rest + ); + + $self->_SQLJoin( + ALIAS1 => $self->{_sql_trattachalias}, + FIELD1 => 'TransactionId', + ALIAS2 => $self->{_sql_transalias}, + FIELD2 => 'id' + ); + + # Join Transactions to Tickets + $self->_SQLJoin( + ALIAS1 => 'main', + FIELD1 => $self->{'primary_key'}, # Why not use "id" here? + ALIAS2 => $self->{_sql_transalias}, + FIELD2 => 'ObjectId' + ); + + $self->SUPER::Limit( + ALIAS => $self->{_sql_transalias}, + FIELD => 'ObjectType', + VALUE => 'RT::Ticket', + ENTRYAGGREGATOR => 'AND' + ); - $sb->_CloseParen; + $self->_CloseParen; } @@ -565,139 +715,376 @@ 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 + +# Test to make sure that you can search for tickets by requestor address and +# by requestor name. + +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); + +my $t2 = RT::Ticket->new($RT::SystemUser); +($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]); +ok ($id, $msg); + + +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); + + +my $tix1 = RT::Tickets->new($RT::SystemUser); +$tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"'); + +is ($tix1->Count, 3); + +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); - $self->_OpenParen; +my $tix4 = RT::Tickets->new($RT::SystemUser); +$tix4->FromSQL('Requestor.Name LIKE "TestOne" '); - my $groups = $self->NewAlias('Groups'); - my $groupmembers = $self->NewAlias('CachedGroupMembers'); - my $users = $self->NewAlias('Users'); +is ($tix4->Count, 2); +# 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" - #Find user watchers -# my $subclause = undef; -# my $aggregator = 'OR'; -# if ($restriction->{'OPERATOR'} =~ /!|NOT/i ){ -# $subclause = 'AndEmailIsNot'; -# $aggregator = 'AND'; -# } +# 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 = (@_); - if (ref $field) { # gross hack - my @bundle = @$field; $self->_OpenParen; - for my $chunk (@bundle) { - ($field,$op,$value,@rest) = @$chunk; - $self->_SQLLimit(ALIAS => $users, - FIELD => $rest{SUBKEY} || 'EmailAddress', - VALUE => $value, - OPERATOR => $op, - CASESENSITIVE => 0, - @rest, - ); + + # Find out what sort of watcher we're looking for + my $fieldname; + if ( ref $field ) { + $fieldname = $field->[0]->[0]; + } + else { + $fieldname = $field; + } + my $meta = $FIELDS{$fieldname}; + my $type = ( defined $meta->[1] ? $meta->[1] : undef ); + +# We only want _one_ clause for all of requestors, cc, admincc +# It's less flexible than what we used to do, but now it sort of actually works. (no huge cartesian products that hose the db) + my $groups = $self->{ 'watcherlimit_' . ('global') . "_groups" } ||= + $self->NewAlias('Groups'); + my $groupmembers = + $self->{ 'watcherlimit_' . ('global') . "_groupmembers" } ||= + $self->NewAlias('CachedGroupMembers'); + my $users = $self->{ 'watcherlimit_' . ('global') . "_users" } ||= + $self->NewAlias('Users'); + +# Use regular joins instead of SQL joins since we don't want the joins inside ticketsql or we get a huge cartesian product + $self->SUPER::Limit( + ALIAS => $groups, + FIELD => 'Domain', + VALUE => 'RT::Ticket-Role', + ENTRYAGGREGATOR => 'AND' + ); + $self->Join( + ALIAS1 => $groups, + FIELD1 => 'Instance', + ALIAS2 => 'main', + FIELD2 => 'id' + ); + $self->Join( + ALIAS1 => $groups, + FIELD1 => 'id', + ALIAS2 => $groupmembers, + FIELD2 => 'GroupId' + ); + $self->Join( + ALIAS1 => $groupmembers, + FIELD1 => 'MemberId', + ALIAS2 => $users, + FIELD2 => 'id' + ); + + # If we're looking for multiple watchers of a given type, + # TicketSQL will be handing it to us as an array of clauses 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 + ); } + + $self->_SQLLimit( + ALIAS => $groups, + FIELD => 'Type', + VALUE => $type, + ENTRYAGGREGATOR => 'AND' + ) + if ($type); + $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'); +=head2 _WatcherMembershipLimit - $self->_SQLJoin(ALIAS1 => $groups, FIELD1 => 'Instance', - ALIAS2 => 'main', FIELD2 => 'id'); - # }}} +Handle watcher membership limits, i.e. whether the watcher belongs to a +specific group or not. - # If we care about which sort of watcher - my $meta = $FIELDS{$field}; - my $type = ( defined $meta->[1] ? $meta->[1] : undef ); +Meta Data: + 1: Field to query on + +SELECT DISTINCT main.* +FROM + Tickets main, + Groups Groups_1, + CachedGroupMembers CachedGroupMembers_2, + Users Users_3 +WHERE ( + (main.EffectiveId = main.id) +) AND ( + (main.Status != 'deleted') +) AND ( + (main.Type = 'ticket') +) AND ( + ( + (Users_3.EmailAddress = '22') + AND + (Groups_1.Domain = 'RT::Ticket-Role') + AND + (Groups_1.Type = 'RequestorGroup') + ) +) AND + Groups_1.Instance = main.id +AND + Groups_1.id = CachedGroupMembers_2.GroupId +AND + CachedGroupMembers_2.MemberId = Users_3.id +ORDER BY main.id ASC +LIMIT 25 + +=cut - if ( $type ) { - $self->_SQLLimit(ALIAS => $groups, - FIELD => 'Type', - VALUE => $type, - ENTRYAGGREGATOR => 'AND'); - } +sub _WatcherMembershipLimit { + my ( $self, $field, $op, $value, @rest ) = @_; + my %rest = @rest; - $self->_SQLJoin (ALIAS1 => $groups, FIELD1 => 'id', - ALIAS2 => $groupmembers, FIELD2 => 'GroupId'); + $self->_OpenParen; + + my $groups = $self->NewAlias('Groups'); + my $groupmembers = $self->NewAlias('CachedGroupMembers'); + my $users = $self->NewAlias('Users'); + my $memberships = $self->NewAlias('CachedGroupMembers'); + + if ( ref $field ) { # gross hack + my @bundle = @$field; + $self->_OpenParen; + for my $chunk (@bundle) { + ( $field, $op, $value, @rest ) = @$chunk; + $self->_SQLLimit( + ALIAS => $memberships, + FIELD => 'GroupId', + VALUE => $value, + OPERATOR => $op, + @rest, + ); + } + $self->_CloseParen; + } + else { + $self->_SQLLimit( + ALIAS => $memberships, + FIELD => 'GroupId', + VALUE => $value, + OPERATOR => $op, + @rest, + ); + } + + # {{{ 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' + ); + + # }}} + + # 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 => $groupmembers, FIELD1 => 'MemberId', - ALIAS2 => $users, FIELD2 => 'id'); + $self->Join( + ALIAS1 => $groups, + FIELD1 => 'id', + ALIAS2 => $groupmembers, + FIELD2 => 'GroupId' + ); + + $self->Join( + ALIAS1 => $groupmembers, + FIELD1 => 'MemberId', + ALIAS2 => $users, + FIELD2 => 'id' + ); + + $self->Join( + ALIAS1 => $memberships, + FIELD1 => 'MemberId', + ALIAS2 => $users, + FIELD2 => 'id' + ); - $self->_CloseParen; + $self->_CloseParen; } sub _LinkFieldLimit { - my $restriction; - my $self; - my $LinkAlias; - my %args; - if ($restriction->{'TYPE'}) { - $self->SUPER::Limit(ALIAS => $LinkAlias, - ENTRYAGGREGATOR => 'AND', - FIELD => 'Type', - OPERATOR => '=', - VALUE => $restriction->{'TYPE'} ); - } - - #If we're trying to limit it to things that are target of - if ($restriction->{'TARGET'}) { - # If the TARGET is an integer that means that we want to look at - # the LocalTarget field. otherwise, we want to look at the - # "Target" field - my ($matchfield); - if ($restriction->{'TARGET'} =~/^(\d+)$/) { - $matchfield = "LocalTarget"; - } else { - $matchfield = "Target"; + my $restriction; + my $self; + my $LinkAlias; + my %args; + if ( $restriction->{'TYPE'} ) { + $self->SUPER::Limit( + ALIAS => $LinkAlias, + ENTRYAGGREGATOR => 'AND', + FIELD => 'Type', + OPERATOR => '=', + VALUE => $restriction->{'TYPE'} + ); } - $self->SUPER::Limit(ALIAS => $LinkAlias, - ENTRYAGGREGATOR => 'AND', - FIELD => $matchfield, - OPERATOR => '=', - VALUE => $restriction->{'TARGET'} ); - #If we're searching on target, join the base to ticket.id - $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'}, - ALIAS2 => $LinkAlias, - FIELD2 => 'LocalBase'); - } - #If we're trying to limit it to things that are base of - elsif ($restriction->{'BASE'}) { - # If we're trying to match a numeric link, we want to look at - # LocalBase, otherwise we want to look at "Base" - my ($matchfield); - if ($restriction->{'BASE'} =~/^(\d+)$/) { - $matchfield = "LocalBase"; - } else { - $matchfield = "Base"; + + #If we're trying to limit it to things that are target of + if ( $restriction->{'TARGET'} ) { + + # If the TARGET is an integer that means that we want to look at + # the LocalTarget field. otherwise, we want to look at the + # "Target" field + my ($matchfield); + if ( $restriction->{'TARGET'} =~ /^(\d+)$/ ) { + $matchfield = "LocalTarget"; + } + else { + $matchfield = "Target"; + } + $self->SUPER::Limit( + ALIAS => $LinkAlias, + ENTRYAGGREGATOR => 'AND', + FIELD => $matchfield, + OPERATOR => '=', + VALUE => $restriction->{'TARGET'} + ); + + #If we're searching on target, join the base to ticket.id + $self->_SQLJoin( + ALIAS1 => 'main', + FIELD1 => $self->{'primary_key'}, + ALIAS2 => $LinkAlias, + FIELD2 => 'LocalBase' + ); } - $self->SUPER::Limit(ALIAS => $LinkAlias, - ENTRYAGGREGATOR => 'AND', - FIELD => $matchfield, - OPERATOR => '=', - VALUE => $restriction->{'BASE'} ); - #If we're searching on base, join the target to ticket.id - $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'}, - ALIAS2 => $LinkAlias, - FIELD2 => 'LocalTarget') - } -} + #If we're trying to limit it to things that are base of + elsif ( $restriction->{'BASE'} ) { + # If we're trying to match a numeric link, we want to look at + # LocalBase, otherwise we want to look at "Base" + my ($matchfield); + if ( $restriction->{'BASE'} =~ /^(\d+)$/ ) { + $matchfield = "LocalBase"; + } + else { + $matchfield = "Base"; + } + + $self->SUPER::Limit( + ALIAS => $LinkAlias, + ENTRYAGGREGATOR => 'AND', + FIELD => $matchfield, + OPERATOR => '=', + VALUE => $restriction->{'BASE'} + ); + + #If we're searching on base, join the target to ticket.id + $self->_SQLJoin( + ALIAS1 => 'main', + FIELD1 => $self->{'primary_key'}, + ALIAS2 => $LinkAlias, + FIELD2 => 'LocalTarget' + ); + } +} =head2 KeywordLimit @@ -709,102 +1096,137 @@ Meta Data: =cut sub _CustomFieldLimit { - my ($self,$_field,$op,$value,@rest) = @_; - - my %rest = @rest; - 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 - - 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); - $field = $2; - $CF->LimitToQueue( $q->Id ); - $queue = $q->Id; - } else { - $field = $1 if $field =~ /^{(.+)}$/; # trim { } - $CF->LimitToGlobal; - } - $CF->FindAllRows; - - my $cfid = 0; - - # this is pretty inefficient for huge numbers of CFs... - while ( my $CustomField = $CF->Next ) { - if (lc $CustomField->Name eq lc $field) { - $cfid = $CustomField->Id; - last; + my ( $self, $_field, $op, $value, @rest ) = @_; + + my %rest = @rest; + my $field = $rest{SUBKEY} || die "No field specified"; + + # For our sanity, we can only limit on one queue at a time + my $queue = 0; + + if ( $field =~ /^(.+?)\.{(.+)}$/ ) { + $queue = $1; + $field = $2; } - } - die "No custom field named $field found\n" - unless $cfid; + $field = $1 if $field =~ /^{(.+)}$/; # trim { } -# use RT::CustomFields; -# my $CF = RT::CustomField->new( $self->CurrentUser ); -# $CF->Load( $cfid ); +# 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. Note that +# we explicitly don't include the "IS NULL" case, since we would +# otherwise end up with a redundant clause. - my $null_columns_ok; + my $null_columns_ok; + if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) { + $null_columns_ok = 1; + } - 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' ); - } + my $cfid = 0; + if ($queue) { - $self->_OpenParen; + my $q = RT::Queue->new( $self->CurrentUser ); + $q->Load($queue) if ($queue); - $self->_SQLLimit( ALIAS => $TicketCFs, - FIELD => 'Content', - OPERATOR => $op, - VALUE => $value, - QUOTEVALUE => 1, - @rest ); + my $cf; + if ( $q->id ) { + $cf = $q->CustomField($field); + } + else { + $cf = RT::CustomField->new( $self->CurrentUser ); + $cf->LoadByNameAndQueue( Queue => '0', Name => $field ); + } - if ( $op =~ /^IS$/i - or ( $op eq '!=' ) ) { - $null_columns_ok = 1; - } + $cfid = $cf->id; - #If we're trying to find tickets where the keyword isn't somethng, - #also check ones where it _IS_ null + } - if ( $op eq '!=' ) { - $self->_SQLLimit( ALIAS => $TicketCFs, - FIELD => 'Content', - OPERATOR => 'IS', - VALUE => 'NULL', - QUOTEVALUE => 0, - ENTRYAGGREGATOR => 'OR', ); - } + my $TicketCFs; + my $cfkey = $cfid ? $cfid : "$queue.$field"; - $self->_SQLLimit( LEFTJOIN => $TicketCFs, - FIELD => 'CustomField', - VALUE => $cfid, - ENTRYAGGREGATOR => 'OR' ); + # Perform one Join per CustomField + if ( $self->{_sql_object_cf_alias}{$cfkey} ) { + $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey}; + } + else { + if ($cfid) { + $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join( + TYPE => 'left', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'ObjectCustomFieldValues', + FIELD2 => 'ObjectId', + ); + $self->SUPER::Limit( + LEFTJOIN => $TicketCFs, + FIELD => 'CustomField', + VALUE => $cfid, + ENTRYAGGREGATOR => 'AND' + ); + } else { + my $cfalias = $self->Join( + TYPE => 'left', + EXPRESSION => "'$field'", + TABLE2 => 'CustomFields', + FIELD2 => 'Name', + ); + + $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join( + TYPE => 'left', + ALIAS1 => $cfalias, + FIELD1 => 'id', + TABLE2 => 'ObjectCustomFieldValues', + FIELD2 => 'CustomField', + ); + $self->SUPER::Limit( + LEFTJOIN => $TicketCFs, + FIELD => 'ObjectId', + VALUE => 'main.id', + QUOTEVALUE => 0, + ENTRYAGGREGATOR => 'AND', + ); + } + $self->SUPER::Limit( + LEFTJOIN => $TicketCFs, + FIELD => 'ObjectType', + VALUE => ref( $self->NewItem ) + , # we want a single item, not a collection + ENTRYAGGREGATOR => 'AND' + ); + $self->SUPER::Limit( + LEFTJOIN => $TicketCFs, + FIELD => 'Disabled', + OPERATOR => '=', + VALUE => '0', + ENTRYAGGREGATOR => 'AND'); + } + $self->_OpenParen if ($null_columns_ok); + + $self->_SQLLimit( + ALIAS => $TicketCFs, + FIELD => 'Content', + OPERATOR => $op, + VALUE => $value, + QUOTEVALUE => 1, + @rest + ); + + if ($null_columns_ok) { + $self->_SQLLimit( + ALIAS => $TicketCFs, + FIELD => 'Content', + OPERATOR => 'IS', + VALUE => 'NULL', + QUOTEVALUE => 0, + ENTRYAGGREGATOR => 'OR', + ); + } + $self->_CloseParen if ($null_columns_ok); - $self->_CloseParen; } - # End Helper Functions # End of SQL Stuff ------------------------------------------------- @@ -819,33 +1241,35 @@ Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION Generally best called from LimitFoo methods =cut + sub Limit { my $self = shift; - my %args = ( FIELD => undef, - OPERATOR => '=', - VALUE => undef, - DESCRIPTION => undef, - @_ - ); - $args{'DESCRIPTION'} = $self->loc( - "[_1] [_2] [_3]", $args{'FIELD'}, $args{'OPERATOR'}, $args{'VALUE'} - ) if (!defined $args{'DESCRIPTION'}) ; + my %args = ( + FIELD => undef, + OPERATOR => '=', + VALUE => undef, + DESCRIPTION => undef, + @_ + ); + $args{'DESCRIPTION'} = $self->loc( "[_1] [_2] [_3]", + $args{'FIELD'}, $args{'OPERATOR'}, $args{'VALUE'} ) + if ( !defined $args{'DESCRIPTION'} ); my $index = $self->_NextIndex; - #make the TicketRestrictions hash the equivalent of whatever we just passed in; + #make the TicketRestrictions hash the equivalent of whatever we just passed in; - %{$self->{'TicketRestrictions'}{$index}} = %args; + %{ $self->{'TicketRestrictions'}{$index} } = %args; $self->{'RecalcTicketLimits'} = 1; - # If we're looking at the effective id, we don't want to append the other clause - # which limits us to tickets where id = effective id - if ($args{'FIELD'} eq 'EffectiveId') { +# If we're looking at the effective id, we don't want to append the other clause +# which limits us to tickets where id = effective id + if ( $args{'FIELD'} eq 'EffectiveId' ) { $self->{'looking_at_effective_id'} = 1; } - if ($args{'FIELD'} eq 'Type') { + if ( $args{'FIELD'} eq 'Type' ) { $self->{'looking_at_type'} = 1; } @@ -854,9 +1278,6 @@ sub Limit { # }}} - - - =head2 FreezeLimits Returns a frozen string suitable for handing back to ThawLimits. @@ -864,18 +1285,18 @@ Returns a frozen string suitable for handing back to ThawLimits. =cut sub _FreezeThawKeys { - 'TicketRestrictions', - 'restriction_index', - 'looking_at_effective_id', - 'looking_at_type' + 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id', + 'looking_at_type'; } # {{{ sub FreezeLimits sub FreezeLimits { - my $self = shift; - require FreezeThaw; - return (FreezeThaw::freeze(@{$self}{$self->_FreezeThawKeys})); + my $self = shift; + require Storable; + require MIME::Base64; + MIME::Base64::base64_encode( + Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) ); } # }}} @@ -886,25 +1307,26 @@ Take a frozen Limits string generated by FreezeLimits and make this tickets object have that set of limits. =cut + # {{{ sub ThawLimits sub ThawLimits { - my $self = shift; - my $in = shift; - - #if we don't have $in, get outta here. - return undef unless ($in); + my $self = shift; + my $in = shift; + + #if we don't have $in, get outta here. + return undef unless ($in); + + $self->{'RecalcTicketLimits'} = 1; + + require Storable; + require MIME::Base64; - $self->{'RecalcTicketLimits'} = 1; + #We don't need to die if the thaw fails. + @{$self}{ $self->_FreezeThawKeys } = + eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; }; - require FreezeThaw; - - #We don't need to die if the thaw fails. - - eval { - @{$self}{$self->_FreezeThawKeys} = FreezeThaw::thaw($in); - }; - $RT::Logger->error( $@ ) if $@; + $RT::Logger->error($@) if $@; } @@ -925,16 +1347,18 @@ VALUE is a queue id or Name. sub LimitQueue { my $self = shift; - my %args = (VALUE => undef, - OPERATOR => '=', - @_); + my %args = ( + VALUE => undef, + OPERATOR => '=', + @_ + ); #TODO VALUE should also take queue names and queue objects #TODO FIXME why are we canonicalizing to name, not id, robrt? - if ($args{VALUE} =~ /^\d+$/) { - my $queue = new RT::Queue($self->CurrentUser); - $queue->Load($args{'VALUE'}); - $args{VALUE} = $queue->Name; + if ( $args{VALUE} =~ /^\d+$/ ) { + my $queue = new RT::Queue( $self->CurrentUser ); + $queue->Load( $args{'VALUE'} ); + $args{VALUE} = $queue->Name; } # What if they pass in an Id? Check for isNum() and convert to @@ -942,15 +1366,16 @@ sub LimitQueue { #TODO check for a valid queue here - $self->Limit (FIELD => 'Queue', - VALUE => $args{VALUE}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE}, - ), - ); + $self->Limit( + FIELD => 'Queue', + VALUE => $args{VALUE}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => + join( ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE}, ), + ); } + # }}} # {{{ sub LimitStatus @@ -961,19 +1386,27 @@ Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of = or !=. VALUE is a status. +RT adds Status != 'deleted' until object has +allow_deleted_search internal property set. +$tickets->{'allow_deleted_search'} = 1; +$tickets->LimitStatus( VALUE => 'deleted' ); + =cut sub LimitStatus { my $self = shift; - my %args = ( OPERATOR => '=', - @_); - $self->Limit (FIELD => 'Status', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Status'), $args{'OPERATOR'}, $self->loc($args{'VALUE'}) - ), - ); + my %args = ( + OPERATOR => '=', + @_ + ); + $self->Limit( + FIELD => 'Status', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => join( ' ', + $self->loc('Status'), $args{'OPERATOR'}, + $self->loc( $args{'VALUE'} ) ), + ); } # }}} @@ -1015,16 +1448,18 @@ VALUE is a string to search for in the type of the ticket. sub LimitType { my $self = shift; - my %args = (OPERATOR => '=', - VALUE => undef, - @_); - $self->Limit (FIELD => 'Type', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, - ), - ); + my %args = ( + OPERATOR => '=', + VALUE => undef, + @_ + ); + $self->Limit( + FIELD => 'Type', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => + join( ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ), + ); } # }}} @@ -1046,13 +1481,14 @@ VALUE is a string to search for in the subject of the ticket. sub LimitSubject { my $self = shift; my %args = (@_); - $self->Limit (FIELD => 'Subject', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => 'Subject', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => join( + ' ', $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, + ), + ); } # }}} @@ -1074,16 +1510,18 @@ VALUE is a ticket Id to search for sub LimitId { my $self = shift; - my %args = (OPERATOR => '=', - @_); + my %args = ( + OPERATOR => '=', + @_ + ); - $self->Limit (FIELD => 'id', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => 'id', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => + join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ), + ); } # }}} @@ -1101,13 +1539,14 @@ VALUE is a value to match the ticket\'s priority against sub LimitPriority { my $self = shift; my %args = (@_); - $self->Limit (FIELD => 'Priority', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Priority'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => 'Priority', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => join( ' ', + $self->loc('Priority'), + $args{'OPERATOR'}, $args{'VALUE'}, ), + ); } # }}} @@ -1126,13 +1565,14 @@ VALUE is a value to match the ticket\'s initial priority against sub LimitInitialPriority { my $self = shift; my %args = (@_); - $self->Limit (FIELD => 'InitialPriority', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Initial Priority'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => 'InitialPriority', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => join( ' ', + $self->loc('Initial Priority'), $args{'OPERATOR'}, + $args{'VALUE'}, ), + ); } # }}} @@ -1150,13 +1590,14 @@ VALUE is a value to match the ticket\'s final priority against sub LimitFinalPriority { my $self = shift; my %args = (@_); - $self->Limit (FIELD => 'FinalPriority', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Final Priority'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => 'FinalPriority', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => join( ' ', + $self->loc('Final Priority'), + $args{'OPERATOR'}, $args{'VALUE'}, ), + ); } # }}} @@ -1174,13 +1615,14 @@ VALUE is a value to match the ticket's TimeWorked attribute sub LimitTimeWorked { my $self = shift; my %args = (@_); - $self->Limit (FIELD => 'TimeWorked', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Time worked'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => 'TimeWorked', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => join( ' ', + $self->loc('Time worked'), + $args{'OPERATOR'}, $args{'VALUE'}, ), + ); } # }}} @@ -1198,13 +1640,14 @@ VALUE is a value to match the ticket's TimeLeft attribute sub LimitTimeLeft { my $self = shift; my %args = (@_); - $self->Limit (FIELD => 'TimeLeft', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Time left'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => 'TimeLeft', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => join( ' ', + $self->loc('Time left'), + $args{'OPERATOR'}, $args{'VALUE'}, ), + ); } # }}} @@ -1222,16 +1665,18 @@ OPERATOR is one of =, LIKE, NOT LIKE or !=. VALUE is a string to search for in the body of the ticket =cut + sub LimitContent { my $self = shift; my %args = (@_); - $self->Limit (FIELD => 'Content', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Ticket content'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => 'Content', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => join( ' ', + $self->loc('Ticket content'), + $args{'OPERATOR'}, $args{'VALUE'}, ), + ); } # }}} @@ -1245,16 +1690,18 @@ OPERATOR is one of =, LIKE, NOT LIKE or !=. VALUE is a string to search for in the body of the ticket =cut + sub LimitFilename { my $self = shift; my %args = (@_); - $self->Limit (FIELD => 'Filename', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Attachment filename'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => 'Filename', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => join( ' ', + $self->loc('Attachment filename'), $args{'OPERATOR'}, + $args{'VALUE'}, ), + ); } # }}} @@ -1271,14 +1718,16 @@ VALUE is a content type to search ticket attachments for sub LimitContentType { my $self = shift; my %args = (@_); - $self->Limit (FIELD => 'ContentType', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Ticket content type'), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => 'ContentType', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => join( ' ', + $self->loc('Ticket content type'), $args{'OPERATOR'}, + $args{'VALUE'}, ), + ); } + # }}} # }}} @@ -1297,19 +1746,22 @@ VALUE is a user id. sub LimitOwner { my $self = shift; - my %args = ( OPERATOR => '=', - @_); + my %args = ( + OPERATOR => '=', + @_ + ); + + my $owner = new RT::User( $self->CurrentUser ); + $owner->Load( $args{'VALUE'} ); - my $owner = new RT::User($self->CurrentUser); - $owner->Load($args{'VALUE'}); # FIXME: check for a valid $owner - $self->Limit (FIELD => 'Owner', - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - DESCRIPTION => join( - ' ', $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), - ), - ); + $self->Limit( + FIELD => 'Owner', + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + DESCRIPTION => + join( ' ', $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ), + ); } @@ -1319,7 +1771,6 @@ sub LimitOwner { # {{{ sub LimitWatcher - =head2 LimitWatcher Takes a paramhash with the fields OPERATOR, TYPE and VALUE. @@ -1338,44 +1789,46 @@ $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \ sub LimitWatcher { my $self = shift; - my %args = ( OPERATOR => '=', - VALUE => undef, - TYPE => undef, - @_); - + my %args = ( + OPERATOR => '=', + VALUE => undef, + TYPE => undef, + @_ + ); #build us up a description - my ($watcher_type, $desc); - if ($args{'TYPE'}) { - $watcher_type = $args{'TYPE'}; + my ( $watcher_type, $desc ); + if ( $args{'TYPE'} ) { + $watcher_type = $args{'TYPE'}; } else { - $watcher_type = "Watcher"; + $watcher_type = "Watcher"; } - $self->Limit (FIELD => $watcher_type, - VALUE => $args{'VALUE'}, - OPERATOR => $args{'OPERATOR'}, - TYPE => $args{'TYPE'}, - DESCRIPTION => join( - ' ', $self->loc($watcher_type), $args{'OPERATOR'}, $args{'VALUE'}, - ), - ); + $self->Limit( + FIELD => $watcher_type, + VALUE => $args{'VALUE'}, + OPERATOR => $args{'OPERATOR'}, + TYPE => $args{'TYPE'}, + DESCRIPTION => join( ' ', + $self->loc($watcher_type), + $args{'OPERATOR'}, $args{'VALUE'}, ), + ); } - sub LimitRequestor { my $self = shift; my %args = (@_); - my ($package, $filename, $line) = caller; - $RT::Logger->error("Tickets->LimitRequestor is deprecated. please rewrite call at $package - $filename: $line"); - $self->LimitWatcher(TYPE => 'Requestor', @_); + my ( $package, $filename, $line ) = caller; + $RT::Logger->error( +"Tickets->LimitRequestor is deprecated. please rewrite call at $package - $filename: $line" + ); + $self->LimitWatcher( TYPE => 'Requestor', @_ ); } # }}} - # }}} # }}} @@ -1387,7 +1840,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 } @@ -1399,23 +1852,25 @@ TARGET is the id or URI of the TARGET of the link sub LimitLinkedTo { my $self = shift; my %args = ( - TICKET => undef, - TARGET => undef, - TYPE => undef, - @_); + TICKET => undef, + TARGET => undef, + TYPE => undef, + @_ + ); $self->Limit( - FIELD => 'LinkedTo', - BASE => undef, - TARGET => ($args{'TARGET'} || $args{'TICKET'}), - TYPE => $args{'TYPE'}, - DESCRIPTION => $self->loc( - "Tickets [_1] by [_2]", $self->loc($args{'TYPE'}), ($args{'TARGET'} || $args{'TICKET'}) - ), - ); + FIELD => 'LinkedTo', + BASE => undef, + TARGET => ( $args{'TARGET'} || $args{'TICKET'} ), + TYPE => $args{'TYPE'}, + DESCRIPTION => $self->loc( + "Tickets [_1] by [_2]", + $self->loc( $args{'TYPE'} ), + ( $args{'TARGET'} || $args{'TICKET'} ) + ), + ); } - # }}} # {{{ LimitLinkedFrom @@ -1423,7 +1878,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 @@ -1434,62 +1889,71 @@ BASE is the id or URI of the BASE of the link sub LimitLinkedFrom { my $self = shift; - my %args = ( BASE => undef, - TICKET => undef, - TYPE => undef, - @_); + my %args = ( + BASE => undef, + TICKET => undef, + TYPE => undef, + @_ + ); # translate RT2 From/To naming to RT3 TicketSQL naming my %fromToMap = qw(DependsOn DependentOn - MemberOf HasMember - RefersTo ReferredToBy); + MemberOf HasMember + RefersTo ReferredToBy); my $type = $args{'TYPE'}; - $type = $fromToMap{$type} if exists($fromToMap{$type}); + $type = $fromToMap{$type} if exists( $fromToMap{$type} ); - $self->Limit( FIELD => 'LinkedTo', - TARGET => undef, - BASE => ($args{'BASE'} || $args{'TICKET'}), - TYPE => $type, - DESCRIPTION => $self->loc( - "Tickets [_1] [_2]", $self->loc($args{'TYPE'}), ($args{'BASE'} || $args{'TICKET'}) - ), - ); + $self->Limit( + FIELD => 'LinkedTo', + TARGET => undef, + BASE => ( $args{'BASE'} || $args{'TICKET'} ), + TYPE => $type, + DESCRIPTION => $self->loc( + "Tickets [_1] [_2]", + $self->loc( $args{'TYPE'} ), + ( $args{'BASE'} || $args{'TICKET'} ) + ), + ); } - # }}} # {{{ LimitMemberOf sub LimitMemberOf { - my $self = shift; + my $self = shift; my $ticket_id = shift; - $self->LimitLinkedTo ( TARGET=> "$ticket_id", - TYPE => 'MemberOf', - ); + $self->LimitLinkedTo( + TARGET => "$ticket_id", + TYPE => 'MemberOf', + ); } + # }}} # {{{ LimitHasMember sub LimitHasMember { - my $self = shift; - my $ticket_id =shift; - $self->LimitLinkedFrom ( BASE => "$ticket_id", - TYPE => 'HasMember', - ); + my $self = shift; + my $ticket_id = shift; + $self->LimitLinkedFrom( + BASE => "$ticket_id", + TYPE => 'HasMember', + ); } + # }}} # {{{ LimitDependsOn sub LimitDependsOn { - my $self = shift; + my $self = shift; my $ticket_id = shift; - $self->LimitLinkedTo ( TARGET => "$ticket_id", - TYPE => 'DependsOn', - ); + $self->LimitLinkedTo( + TARGET => "$ticket_id", + TYPE => 'DependsOn', + ); } @@ -1498,25 +1962,26 @@ sub LimitDependsOn { # {{{ LimitDependedOnBy sub LimitDependedOnBy { - my $self = shift; + my $self = shift; my $ticket_id = shift; - $self->LimitLinkedFrom ( BASE => "$ticket_id", - TYPE => 'DependentOn', - ); + $self->LimitLinkedFrom( + BASE => "$ticket_id", + TYPE => 'DependentOn', + ); } # }}} - # {{{ LimitRefersTo sub LimitRefersTo { - my $self = shift; + my $self = shift; my $ticket_id = shift; - $self->LimitLinkedTo ( TARGET => "$ticket_id", - TYPE => 'RefersTo', - ); + $self->LimitLinkedTo( + TARGET => "$ticket_id", + TYPE => 'RefersTo', + ); } @@ -1525,11 +1990,12 @@ sub LimitRefersTo { # {{{ LimitReferredToBy sub LimitReferredToBy { - my $self = shift; + my $self = shift; my $ticket_id = shift; - $self->LimitLinkedFrom ( BASE=> "$ticket_id", - TYPE => 'ReferredTo', - ); + $self->LimitLinkedFrom( + BASE => "$ticket_id", + TYPE => 'ReferredToBy', + ); } @@ -1557,56 +2023,64 @@ the need to pass in a FIELD argument. sub LimitDate { my $self = shift; my %args = ( - FIELD => undef, - VALUE => undef, - OPERATOR => undef, + FIELD => undef, + VALUE => undef, + OPERATOR => undef, - @_); + @_ + ); #Set the description if we didn't get handed it above - unless ($args{'DESCRIPTION'} ) { - $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT" + unless ( $args{'DESCRIPTION'} ) { + $args{'DESCRIPTION'} = + $args{'FIELD'} . " " + . $args{'OPERATOR'} . " " + . $args{'VALUE'} . " GMT"; } - $self->Limit (%args); + $self->Limit(%args); } # }}} - - - sub LimitCreated { my $self = shift; - $self->LimitDate( FIELD => 'Created', @_); + $self->LimitDate( FIELD => 'Created', @_ ); } + sub LimitDue { my $self = shift; - $self->LimitDate( FIELD => 'Due', @_); + $self->LimitDate( FIELD => 'Due', @_ ); } + sub LimitStarts { my $self = shift; - $self->LimitDate( FIELD => 'Starts', @_); + $self->LimitDate( FIELD => 'Starts', @_ ); } + sub LimitStarted { my $self = shift; - $self->LimitDate( FIELD => 'Started', @_); + $self->LimitDate( FIELD => 'Started', @_ ); } + sub LimitResolved { my $self = shift; - $self->LimitDate( FIELD => 'Resolved', @_); + $self->LimitDate( FIELD => 'Resolved', @_ ); } + sub LimitTold { my $self = shift; - $self->LimitDate( FIELD => 'Told', @_); + $self->LimitDate( FIELD => 'Told', @_ ); } + sub LimitLastUpdated { my $self = shift; - $self->LimitDate( FIELD => 'LastUpdated', @_); + $self->LimitDate( FIELD => 'LastUpdated', @_ ); } + # # {{{ sub LimitTransactionDate @@ -1623,21 +2097,25 @@ VALUE is a date and time in ISO format in GMT sub LimitTransactionDate { my $self = shift; my %args = ( - FIELD => 'TransactionDate', - VALUE => undef, - OPERATOR => undef, + FIELD => 'TransactionDate', + VALUE => undef, + OPERATOR => undef, - @_); + @_ + ); # <20021217042756.GK28744@pallas.fsck.com> # "Kill It" - Jesse. #Set the description if we didn't get handed it above - unless ($args{'DESCRIPTION'} ) { - $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT" + unless ( $args{'DESCRIPTION'} ) { + $args{'DESCRIPTION'} = + $args{'FIELD'} . " " + . $args{'OPERATOR'} . " " + . $args{'VALUE'} . " GMT"; } - $self->Limit (%args); + $self->Limit(%args); } @@ -1654,8 +2132,7 @@ Takes a paramhash of key/value pairs with the following keys: =over 4 -=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 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 - The usual Limit operators @@ -1667,58 +2144,67 @@ parameter QUEUE may also be passed to distinguish the custom field. sub LimitCustomField { my $self = shift; - my %args = ( VALUE => undef, - CUSTOMFIELD => undef, - OPERATOR => '=', - DESCRIPTION => undef, - FIELD => 'CustomFieldValue', - QUOTEVALUE => 1, - @_ ); - - use RT::CustomFields; + my %args = ( + VALUE => undef, + CUSTOMFIELD => undef, + OPERATOR => '=', + DESCRIPTION => undef, + FIELD => 'CustomFieldValue', + QUOTEVALUE => 1, + @_ + ); + my $CF = RT::CustomField->new( $self->CurrentUser ); - if ( $args{CUSTOMFIELD} =~ /^\d+$/) { - $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; + $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 ) { - $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has no value.", $CF->Name); + $args{'DESCRIPTION'} ||= + $self->loc( "Custom field [_1] has no value.", $CF->Name ); } elsif ( $args{'OPERATOR'} =~ /^is not$/i ) { - $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has a value.", $CF->Name); + $args{'DESCRIPTION'} ||= + $self->loc( "Custom field [_1] has a value.", $CF->Name ); } # if we're not looking to compare with a null value else { - $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] [_2] [_3]", $CF->Name , $args{OPERATOR} , $args{VALUE}); + $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]", + $CF->Name, $args{OPERATOR}, $args{VALUE} ); } my $q = ""; - if ($CF->Queue) { - my $qo = new RT::Queue( $self->CurrentUser ); - $qo->load( $CF->Queue ); - $q = $qo->Name; + if ( $CF->Queue ) { + my $qo = new RT::Queue( $self->CurrentUser ); + $qo->load( $CF->Queue ); + $q = $qo->Name; } my @rest; @rest = ( ENTRYAGGREGATOR => 'AND' ) - if ($CF->Type eq 'SelectMultiple'); - - $self->Limit( VALUE => $args{VALUE}, - FIELD => "CF.".( $q - ? $q . ".{" . $CF->Name . "}" - : $CF->Name - ), - OPERATOR => $args{OPERATOR}, - CUSTOMFIELD => 1, - @rest, - ); + if ( $CF->Type eq 'SelectMultiple' ); + $self->Limit( + VALUE => $args{VALUE}, + FIELD => "CF." + . ( + $q + ? $q . ".{" . $CF->Name . "}" + : $CF->Name + ), + OPERATOR => $args{OPERATOR}, + CUSTOMFIELD => 1, + @rest, + ); $self->{'RecalcTicketLimits'} = 1; } @@ -1726,7 +2212,6 @@ sub LimitCustomField { # }}} # }}} - # {{{ sub _NextIndex =head2 _NextIndex @@ -1737,8 +2222,9 @@ Keep track of the counter for the array of restrictions sub _NextIndex { my $self = shift; - return ($self->{'restriction_index'}++); + return ( $self->{'restriction_index'}++ ); } + # }}} # }}} @@ -1746,14 +2232,14 @@ sub _NextIndex { # {{{ Core bits to make this a DBIx::SearchBuilder object # {{{ sub _Init -sub _Init { +sub _Init { my $self = shift; - $self->{'table'} = "Tickets"; - $self->{'RecalcTicketLimits'} = 1; + $self->{'table'} = "Tickets"; + $self->{'RecalcTicketLimits'} = 1; $self->{'looking_at_effective_id'} = 0; - $self->{'looking_at_type'} = 0; - $self->{'restriction_index'} =1; - $self->{'primary_key'} = "id"; + $self->{'looking_at_type'} = 0; + $self->{'restriction_index'} = 1; + $self->{'primary_key'} = "id"; delete $self->{'items_array'}; delete $self->{'item_map'}; delete $self->{'columns_to_display'}; @@ -1762,24 +2248,26 @@ sub _Init { $self->_InitSQL; } + # }}} # {{{ sub Count sub Count { - my $self = shift; - $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 ); - return($self->SUPER::Count()); + my $self = shift; + $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 ); + return ( $self->SUPER::Count() ); } + # }}} # {{{ sub CountAll sub CountAll { - my $self = shift; - $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 ); - return($self->SUPER::CountAll()); + my $self = shift; + $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 ); + return ( $self->SUPER::CountAll() ); } -# }}} +# }}} # {{{ sub ItemsArrayRef @@ -1798,28 +2286,27 @@ sub ItemsArrayRef { my $placeholder = $self->_ItemsCounter; $self->GotoFirstItem(); while ( my $item = $self->Next ) { - push ( @{ $self->{'items_array'} }, $item ); + push( @{ $self->{'items_array'} }, $item ); } $self->GotoItem($placeholder); + $self->{'items_array'} = $self->ItemsOrderBy( $self->{'items_array'} ); } return ( $self->{'items_array'} ); } + # }}} # {{{ sub Next sub Next { - my $self = shift; - - $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 ); + my $self = shift; - my $Ticket = $self->SUPER::Next(); - if ((defined($Ticket)) and (ref($Ticket))) { + $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 ); - #Make sure we _never_ show deleted tickets - #TODO we should be doing this in the where clause. - #but you can't do multiple clauses on the same field just yet :/ + my $Ticket = $self->SUPER::Next(); + if ( ( defined($Ticket) ) and ( ref($Ticket) ) ) { - if ($Ticket->__Value('Status') eq 'deleted') { + if ( $Ticket->__Value('Status') eq 'deleted' && + !$self->{'allow_deleted_search'} ) { return($self->Next()); } # Since Ticket could be granted with more rights instead @@ -1831,17 +2318,33 @@ sub Next { return($Ticket); } - #If the user doesn't have the right to show this ticket - else { - return($self->Next()); - } - } - #if there never was any ticket - else { - return(undef); - } + if ( $Ticket->__Value('Status') eq 'deleted' ) { + return ( $self->Next() ); + } + + # Since Ticket could be granted with more rights instead + # of being revoked, it's ok if queue rights allow + # ShowTicket. It seems need another query, but we have + # rights cache in Principal::HasRight. + elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket') + || $Ticket->CurrentUserHasRight('ShowTicket') ) + { + return ($Ticket); + } + + #If the user doesn't have the right to show this ticket + else { + return ( $self->Next() ); + } + } + + #if there never was any ticket + else { + return (undef); + } } + # }}} # }}} @@ -1870,16 +2373,17 @@ is a description of the purpose of that TicketRestriction =cut -sub DescribeRestrictions { +sub DescribeRestrictions { my $self = shift; - my ($row, %listing); + my ( $row, %listing ); - foreach $row (keys %{$self->{'TicketRestrictions'}}) { - $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'}; + foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) { + $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'}; } return (%listing); } + # }}} # {{{ sub RestrictionValues @@ -1892,14 +2396,13 @@ to. =cut sub RestrictionValues { - my $self = shift; + my $self = shift; my $field = shift; - map $self->{'TicketRestrictions'}{$_}{'VALUE'}, - grep { - $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field - && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "=" - } - keys %{$self->{'TicketRestrictions'}}; + map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep { + $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field + && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "=" + } + keys %{ $self->{'TicketRestrictions'} }; } # }}} @@ -1916,8 +2419,8 @@ sub ClearRestrictions { my $self = shift; delete $self->{'TicketRestrictions'}; $self->{'looking_at_effective_id'} = 0; - $self->{'looking_at_type'} = 0; - $self->{'RecalcTicketLimits'} =1; + $self->{'looking_at_type'} = 0; + $self->{'RecalcTicketLimits'} = 1; } # }}} @@ -1931,13 +2434,13 @@ Removes that restriction from the session's limits. =cut - sub DeleteRestriction { my $self = shift; - my $row = shift; + my $row = shift; delete $self->{'TicketRestrictions'}{$row}; $self->{'RecalcTicketLimits'} = 1; + #make the underlying easysearch object forget all its preconceptions } @@ -1948,89 +2451,93 @@ sub DeleteRestriction { # Convert a set of oldstyle SB Restrictions to Clauses for RQL sub _RestrictionsToClauses { - my $self = shift; - - my $row; - my %clause; - foreach $row (keys %{$self->{'TicketRestrictions'}}) { - my $restriction = $self->{'TicketRestrictions'}{$row}; - #use Data::Dumper; - #print Dumper($restriction),"\n"; - - # We need to reimplement the subclause aggregation that SearchBuilder does. - # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main', - # Then SB AND's the different Subclauses together. - - # So, we want to group things into Subclauses, convert them to - # SQL, and then join them with the appropriate DefaultEA. - # Then join each subclause group with AND. - - my $field = $restriction->{'FIELD'}; - my $realfield = $field; # CustomFields fake up a fieldname, so - # we need to figure that out - - # One special case - # Rewrite LinkedTo meta field to the real field - if ($field =~ /LinkedTo/) { - $realfield = $field = $restriction->{'TYPE'}; - } + my $self = shift; - # Two special case - # CustomFields have a different real field - if ($field =~ /^CF\./) { - $realfield = "CF" - } + my $row; + my %clause; + foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) { + my $restriction = $self->{'TicketRestrictions'}{$row}; - die "I don't know about $field yet" - unless (exists $FIELDS{$realfield} or $restriction->{CUSTOMFIELD}); - - my $type = $FIELDS{$realfield}->[0]; - my $op = $restriction->{'OPERATOR'}; - - my $value = ( grep { defined } - map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET))[0]; - - # this performs the moral equivalent of defined or/dor/C, - # without the short circuiting.You need to use a 'defined or' - # type thing instead of just checking for truth values, because - # VALUE could be 0.(i.e. "false") - - # You could also use this, but I find it less aesthetic: - # (although it does short circuit) - #( defined $restriction->{'VALUE'}? $restriction->{VALUE} : - # defined $restriction->{'TICKET'} ? - # $restriction->{TICKET} : - # defined $restriction->{'BASE'} ? - # $restriction->{BASE} : - # defined $restriction->{'TARGET'} ? - # $restriction->{TARGET} ) - - my $ea = $restriction->{ENTRYAGGREGATOR} || $DefaultEA{$type} || "AND"; - if ( ref $ea ) { - die "Invalid operator $op for $field ($type)" - unless exists $ea->{$op}; - $ea = $ea->{$op}; - } + #use Data::Dumper; + #print Dumper($restriction),"\n"; - # Each CustomField should be put into a different Clause so they - # are ANDed together. - if ($restriction->{CUSTOMFIELD}) { - $realfield = $field; - } + # We need to reimplement the subclause aggregation that SearchBuilder does. + # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main', + # Then SB AND's the different Subclauses together. + + # So, we want to group things into Subclauses, convert them to + # SQL, and then join them with the appropriate DefaultEA. + # Then join each subclause group with AND. + + my $field = $restriction->{'FIELD'}; + my $realfield = $field; # CustomFields fake up a fieldname, so + # we need to figure that out + + # One special case + # Rewrite LinkedTo meta field to the real field + if ( $field =~ /LinkedTo/ ) { + $realfield = $field = $restriction->{'TYPE'}; + } + + # Two special case + # Handle subkey fields with a different real field + if ( $field =~ /^(\w+)\./ ) { + $realfield = $1; + } + + die "I don't know about $field yet" + unless ( exists $FIELDS{$realfield} or $restriction->{CUSTOMFIELD} ); + + my $type = $FIELDS{$realfield}->[0]; + my $op = $restriction->{'OPERATOR'}; + + my $value = ( + grep { defined } + map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET) + )[0]; + + # this performs the moral equivalent of defined or/dor/C, + # without the short circuiting.You need to use a 'defined or' + # type thing instead of just checking for truth values, because + # VALUE could be 0.(i.e. "false") + + # You could also use this, but I find it less aesthetic: + # (although it does short circuit) + #( defined $restriction->{'VALUE'}? $restriction->{VALUE} : + # defined $restriction->{'TICKET'} ? + # $restriction->{TICKET} : + # defined $restriction->{'BASE'} ? + # $restriction->{BASE} : + # defined $restriction->{'TARGET'} ? + # $restriction->{TARGET} ) + + my $ea = $restriction->{ENTRYAGGREGATOR} || $DefaultEA{$type} || "AND"; + if ( ref $ea ) { + die "Invalid operator $op for $field ($type)" + unless exists $ea->{$op}; + $ea = $ea->{$op}; + } - exists $clause{$realfield} or $clause{$realfield} = []; - # Escape Quotes - $field =~ s!(['"])!\\$1!g; - $value =~ s!(['"])!\\$1!g; - my $data = [ $ea, $type, $field, $op, $value ]; + # 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; + $value =~ s!(['"])!\\$1!g; + my $data = [ $ea, $type, $field, $op, $value ]; - # here is where we store extra data, say if it's a keyword or - # something. (I.e. "TYPE SPECIFIC STUFF") + # here is where we store extra data, say if it's a keyword or + # something. (I.e. "TYPE SPECIFIC STUFF") - #print Dumper($data); - push @{$clause{$realfield}}, $data; - } - return \%clause; + #print Dumper($data); + push @{ $clause{$realfield} }, $data; + } + return \%clause; } # }}} @@ -2046,29 +2553,30 @@ sub _RestrictionsToClauses { sub _ProcessRestrictions { my $self = shift; - + #Blow away ticket aliases since we'll need to regenerate them for #a new search delete $self->{'TicketAliases'}; - delete $self->{'items_array'}; + 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"; - my $clauseRef = eval { $self->_RestrictionsToClauses; }; - if ($@) { - $RT::Logger->error( "RestrictionsToClauses: " . $@ ); - $self->FromSQL(""); - } else { - $sql = $self->ClausesToSQL($clauseRef); - $self->FromSQL($sql); - } - } + my $sql = $self->Query; # Violating the _SQL namespace + if ( !$sql || $self->{'RecalcTicketLimits'} ) { + + # "Restrictions to Clauses Branch\n"; + my $clauseRef = eval { $self->_RestrictionsToClauses; }; + if ($@) { + $RT::Logger->error( "RestrictionsToClauses: " . $@ ); + $self->FromSQL(""); + } + else { + $sql = $self->ClausesToSQL($clauseRef); + $self->FromSQL($sql); + } + } $self->{'RecalcTicketLimits'} = 0; @@ -2084,22 +2592,22 @@ sub _BuildItemMap { my $self = shift; my $items = $self->ItemsArrayRef; - my $prev = 0 ; + my $prev = 0; delete $self->{'item_map'}; - if ($items->[0]) { - $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; + if ( $items->[0] ) { + $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; } -} - +} =head2 ItemMap @@ -2107,22 +2615,21 @@ 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'}); - return ($self->{'item_map'}); + $self->_BuildItemMap() + unless ( $self->{'items_array'} and $self->{'item_map'} ); + return ( $self->{'item_map'} ); } - - - =cut + } @@ -2137,12 +2644,30 @@ You don't want to serialize a big tickets object, as the {items} hash will be in =cut - sub PrepForSerialization { my $self = shift; delete $self->{'items'}; $self->RedoSearch(); } + +=head1 FLAGS + +RT::Tickets supports several flags which alter search behavior: + + +allow_deleted_search (Otherwise never show deleted tickets in search results) +looking_at_type (otherwise limit to type=ticket) + +These flags are set by calling + +$tickets->{'flagname'} = 1; + +BUG: There should be an API for this + +=cut + 1; + +