1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
48 # - Decimated ProcessRestrictions and broke it into multiple
49 # functions joined by a LUT
50 # - Semi-Generic SQL stuff moved to another file
52 # Known Issues: FIXME!
54 # - ClearRestrictions and Reinitialization is messy and unclear. The
55 # only good way to do it is to create a new RT::Tickets object.
59 RT::Tickets - A collection of Ticket objects
65 my $tickets = new RT::Tickets($CurrentUser);
69 A collection of RT::Tickets.
75 ok (require RT::Tickets);
76 ok( my $testtickets = RT::Tickets->new( $RT::SystemUser ) );
77 ok( $testtickets->LimitStatus( VALUE => 'deleted' ) );
78 # Should be zero until 'allow_deleted_search'
79 ok( $testtickets->Count == 0 );
91 no warnings qw(redefine);
92 use vars qw(@SORTFIELDS);
95 # Configuration Tables:
97 # FIELDS is a mapping of searchable Field name, to Type, and other
102 Queue => [ 'ENUM' => 'Queue', ],
104 Creator => [ 'ENUM' => 'User', ],
105 LastUpdatedBy => [ 'ENUM' => 'User', ],
106 Owner => [ 'WATCHERFIELD' => 'Owner', ],
107 EffectiveId => [ 'INT', ],
109 InitialPriority => [ 'INT', ],
110 FinalPriority => [ 'INT', ],
111 Priority => [ 'INT', ],
112 TimeLeft => [ 'INT', ],
113 TimeWorked => [ 'INT', ],
114 MemberOf => [ 'LINK' => To => 'MemberOf', ],
115 DependsOn => [ 'LINK' => To => 'DependsOn', ],
116 RefersTo => [ 'LINK' => To => 'RefersTo', ],
117 HasMember => [ 'LINK' => From => 'MemberOf', ],
118 DependentOn => [ 'LINK' => From => 'DependsOn', ],
119 DependedOnBy => [ 'LINK' => From => 'DependsOn', ],
120 ReferredToBy => [ 'LINK' => From => 'RefersTo', ],
121 Told => [ 'DATE' => 'Told', ],
122 Starts => [ 'DATE' => 'Starts', ],
123 Started => [ 'DATE' => 'Started', ],
124 Due => [ 'DATE' => 'Due', ],
125 Resolved => [ 'DATE' => 'Resolved', ],
126 LastUpdated => [ 'DATE' => 'LastUpdated', ],
127 Created => [ 'DATE' => 'Created', ],
128 Subject => [ 'STRING', ],
129 Content => [ 'TRANSFIELD', ],
130 ContentType => [ 'TRANSFIELD', ],
131 Filename => [ 'TRANSFIELD', ],
132 TransactionDate => [ 'TRANSDATE', ],
133 Requestor => [ 'WATCHERFIELD' => 'Requestor', ],
134 Requestors => [ 'WATCHERFIELD' => 'Requestor', ],
135 Cc => [ 'WATCHERFIELD' => 'Cc', ],
136 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ],
137 Watcher => ['WATCHERFIELD'],
138 LinkedTo => [ 'LINKFIELD', ],
139 CustomFieldValue => [ 'CUSTOMFIELD', ],
140 CF => [ 'CUSTOMFIELD', ],
141 Updated => [ 'TRANSDATE', ],
142 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ],
143 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ],
144 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ],
145 WatcherGroup => [ 'MEMBERSHIPFIELD', ],
148 # Mapping of Field Type to Function
150 ENUM => \&_EnumLimit,
152 LINK => \&_LinkLimit,
153 DATE => \&_DateLimit,
154 STRING => \&_StringLimit,
155 TRANSFIELD => \&_TransLimit,
156 TRANSDATE => \&_TransDateLimit,
157 WATCHERFIELD => \&_WatcherLimit,
158 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
159 LINKFIELD => \&_LinkFieldLimit,
160 CUSTOMFIELD => \&_CustomFieldLimit,
162 my %can_bundle = ( WATCHERFIELD => "yes", );
164 # Default EntryAggregator per type
165 # if you specify OP, you must specify all valid OPs
201 # Helper functions for passing the above lexically scoped tables above
202 # into Tickets_Overlay_SQL.
203 sub FIELDS { return \%FIELDS }
204 sub dispatch { return \%dispatch }
205 sub can_bundle { return \%can_bundle }
207 # Bring in the clowns.
208 require RT::Tickets_Overlay_SQL;
212 @SORTFIELDS = qw(id Status
214 Owner Created Due Starts Started
216 Resolved LastUpdated Priority TimeWorked TimeLeft);
220 Returns the list of fields that lists of tickets can easily be sorted by
226 return (@SORTFIELDS);
231 # BEGIN SQL STUFF *********************************
233 =head1 Limit Helper Routines
235 These routines are the targets of a dispatch table depending on the
236 type of field. They all share the same signature:
238 my ($self,$field,$op,$value,@rest) = @_;
240 The values in @rest should be suitable for passing directly to
241 DBIx::SearchBuilder::Limit.
243 Essentially they are an expanded/broken out (and much simplified)
244 version of what ProcessRestrictions used to do. They're also much
245 more clearly delineated by the TYPE of field being processed.
249 Handle Fields which are limited to certain values, and potentially
250 need to be looked up from another class.
252 This subroutine actually handles two different kinds of fields. For
253 some the user is responsible for limiting the values. (i.e. Status,
256 For others, the value specified by the user will be looked by via
260 name of class to lookup in (Optional)
265 my ( $sb, $field, $op, $value, @rest ) = @_;
267 # SQL::Statement changes != to <>. (Can we remove this now?)
268 $op = "!=" if $op eq "<>";
270 die "Invalid Operation: $op for $field"
274 my $meta = $FIELDS{$field};
275 if ( defined $meta->[1] ) {
276 my $class = "RT::" . $meta->[1];
277 my $o = $class->new( $sb->CurrentUser );
291 Handle fields where the values are limited to integers. (For example,
292 Priority, TimeWorked.)
300 my ( $sb, $field, $op, $value, @rest ) = @_;
302 die "Invalid Operator $op for $field"
303 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
315 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
318 1: Direction (From,To)
319 2: Link Type (MemberOf, DependsOn,RefersTo)
324 my ( $sb, $field, $op, $value, @rest ) = @_;
326 my $meta = $FIELDS{$field};
327 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS)/io;
329 die "Incorrect Metadata for $field"
330 unless ( defined $meta->[1] and defined $meta->[2] );
332 my $direction = $meta->[1];
338 if ( $direction eq 'To' ) {
339 $matchfield = "Target";
343 elsif ( $direction eq 'From' ) {
344 $linkfield = "Target";
345 $matchfield = "Base";
349 die "Invalid link direction '$meta->[1]' for $field\n";
352 if ( $op eq '=' || $op =~ /^is/oi ) {
353 if ( $value eq '' || $value =~ /^null$/io ) {
356 elsif ( $value =~ /\D/o ) {
364 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
365 # SELECT main.* FROM Tickets main
366 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
367 # AND(main.id = Links_1.LocalTarget))
368 # WHERE ((main.EffectiveId = main.id))
369 # AND ((main.Status != 'deleted'))
370 # AND (Links_1.LocalBase IS NULL);
373 my $linkalias = $sb->Join(
378 FIELD2 => 'Local' . $linkfield
382 LEFTJOIN => $linkalias,
391 ENTRYAGGREGATOR => 'AND',
392 FIELD => ( $is_local ? "Local$matchfield" : $matchfield ),
401 $sb->{_sql_linkalias} = $sb->NewAlias('Links')
402 unless defined $sb->{_sql_linkalias};
407 ALIAS => $sb->{_sql_linkalias},
415 ALIAS => $sb->{_sql_linkalias},
416 ENTRYAGGREGATOR => 'AND',
417 FIELD => ( $is_local ? "Local$matchfield" : $matchfield ),
422 #If we're searching on target, join the base to ticket.id
425 FIELD1 => $sb->{'primary_key'},
426 ALIAS2 => $sb->{_sql_linkalias},
427 FIELD2 => 'Local' . $linkfield
436 Handle date fields. (Created, LastTold..)
439 1: type of link. (Probably not necessary.)
444 my ( $sb, $field, $op, $value, @rest ) = @_;
446 die "Invalid Date Op: $op"
447 unless $op =~ /^(=|>|<|>=|<=)$/;
449 my $meta = $FIELDS{$field};
450 die "Incorrect Meta Data for $field"
451 unless ( defined $meta->[1] );
453 use POSIX 'strftime';
455 my $date = RT::Date->new( $sb->CurrentUser );
456 $date->Set( Format => 'unknown', Value => $value );
457 my $time = $date->Unix;
461 # if we're specifying =, that means we want everything on a
462 # particular single day. in the database, we need to check for >
463 # and < the edges of that day.
465 my $daystart = strftime( "%Y-%m-%d %H:%M",
466 gmtime( $time - ( $time % 86400 ) ) );
467 my $dayend = strftime( "%Y-%m-%d %H:%M",
468 gmtime( $time + ( 86399 - $time % 86400 ) ) );
484 ENTRYAGGREGATOR => 'AND',
491 $value = strftime( "%Y-%m-%d %H:%M", gmtime($time) );
503 Handle simple fields which are just strings. (Subject,Type)
511 my ( $sb, $field, $op, $value, @rest ) = @_;
515 # =, !=, LIKE, NOT LIKE
526 =head2 _TransDateLimit
528 Handle fields limiting based on Transaction Date.
530 The inpupt value must be in a format parseable by Time::ParseDate
537 # This routine should really be factored into translimit.
538 sub _TransDateLimit {
539 my ( $sb, $field, $op, $value, @rest ) = @_;
541 # See the comments for TransLimit, they apply here too
543 $sb->{_sql_transalias} = $sb->NewAlias('Transactions')
544 unless defined $sb->{_sql_transalias};
546 my $date = RT::Date->new( $sb->CurrentUser );
547 $date->Set( Format => 'unknown', Value => $value );
548 my $time = $date->Unix;
553 # if we're specifying =, that means we want everything on a
554 # particular single day. in the database, we need to check for >
555 # and < the edges of that day.
557 my $daystart = strftime( "%Y-%m-%d %H:%M",
558 gmtime( $time - ( $time % 86400 ) ) );
559 my $dayend = strftime( "%Y-%m-%d %H:%M",
560 gmtime( $time + ( 86399 - $time % 86400 ) ) );
563 ALIAS => $sb->{_sql_transalias},
571 ALIAS => $sb->{_sql_transalias},
577 ENTRYAGGREGATOR => 'AND',
582 # not searching for a single day
585 #Search for the right field
587 ALIAS => $sb->{_sql_transalias},
596 # Join Transactions to Tickets
599 FIELD1 => $sb->{'primary_key'}, # UGH!
600 ALIAS2 => $sb->{_sql_transalias},
605 ALIAS => $sb->{_sql_transalias},
606 FIELD => 'ObjectType',
607 VALUE => 'RT::Ticket'
615 Limit based on the Content of a transaction or the ContentType.
624 # Content, ContentType, Filename
626 # If only this was this simple. We've got to do something
629 #Basically, we want to make sure that the limits apply to
630 #the same attachment, rather than just another attachment
631 #for the same ticket, no matter how many clauses we lump
632 #on. We put them in TicketAliases so that they get nuked
633 #when we redo the join.
635 # In the SQL, we might have
636 # (( Content = foo ) or ( Content = bar AND Content = baz ))
637 # The AND group should share the same Alias.
639 # Actually, maybe it doesn't matter. We use the same alias and it
640 # works itself out? (er.. different.)
642 # Steal more from _ProcessRestrictions
644 # FIXME: Maybe look at the previous FooLimit call, and if it was a
645 # TransLimit and EntryAggregator == AND, reuse the Aliases?
647 # Or better - store the aliases on a per subclause basis - since
648 # those are going to be the things we want to relate to each other,
651 # maybe we should not allow certain kinds of aggregation of these
652 # clauses and do a psuedo regex instead? - the problem is getting
653 # them all into the same subclause when you have (A op B op C) - the
654 # way they get parsed in the tree they're in different subclauses.
656 my ( $self, $field, $op, $value, @rest ) = @_;
658 $self->{_sql_transalias} = $self->NewAlias('Transactions')
659 unless defined $self->{_sql_transalias};
660 $self->{_sql_trattachalias} = $self->NewAlias('Attachments')
661 unless defined $self->{_sql_trattachalias};
665 #Search for the right field
667 ALIAS => $self->{_sql_trattachalias},
676 ALIAS1 => $self->{_sql_trattachalias},
677 FIELD1 => 'TransactionId',
678 ALIAS2 => $self->{_sql_transalias},
682 # Join Transactions to Tickets
685 FIELD1 => $self->{'primary_key'}, # Why not use "id" here?
686 ALIAS2 => $self->{_sql_transalias},
691 ALIAS => $self->{_sql_transalias},
692 FIELD => 'ObjectType',
693 VALUE => 'RT::Ticket',
694 ENTRYAGGREGATOR => 'AND'
703 Handle watcher limits. (Requestor, CC, etc..)
711 # Test to make sure that you can search for tickets by requestor address and
715 my $u1 = RT::User->new($RT::SystemUser);
716 ($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
718 my $u2 = RT::User->new($RT::SystemUser);
719 ($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
722 my $t1 = RT::Ticket->new($RT::SystemUser);
724 ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
727 my $t2 = RT::Ticket->new($RT::SystemUser);
728 ($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
732 my $t3 = RT::Ticket->new($RT::SystemUser);
733 ($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
737 my $tix1 = RT::Tickets->new($RT::SystemUser);
738 $tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
740 is ($tix1->Count, 3);
742 my $tix2 = RT::Tickets->new($RT::SystemUser);
743 $tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
745 is ($tix2->Count, 3);
748 my $tix3 = RT::Tickets->new($RT::SystemUser);
749 $tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
751 is ($tix3->Count, 2);
753 my $tix4 = RT::Tickets->new($RT::SystemUser);
754 $tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
756 is ($tix4->Count, 2);
758 # Searching for tickets that have two requestors isn't supported
759 # There's no way to differentiate "one requestor name that matches foo and bar"
760 # and "two requestors, one matching foo and one matching bar"
762 # my $tix5 = RT::Tickets->new($RT::SystemUser);
763 # $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
765 # is ($tix5->Count, 1);
767 # my $tix6 = RT::Tickets->new($RT::SystemUser);
768 # $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
770 # is ($tix6->Count, 1);
784 # Find out what sort of watcher we're looking for
787 $fieldname = $field->[0]->[0];
791 $field = [ [ $field, $op, $value, %rest ] ]; # gross hack
793 my $meta = $FIELDS{$fieldname};
794 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
796 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
797 # search by id and Name at the same time, this is workaround
798 # to preserve backward compatibility
799 if ( $fieldname eq 'Owner' ) {
801 for my $chunk ( splice @$field ) {
802 my ( $f, $op, $value, %rest ) = @$chunk;
803 if ( !$rest{SUBKEY} && $op =~ /^!?=$/ ) {
804 $self->_OpenParen unless $flag++;
805 my $o = RT::User->new( $self->CurrentUser );
816 push @$field, $chunk;
819 $self->_CloseParen if $flag;
820 return unless @$field;
823 my $users = $self->_WatcherJoin($type);
825 # If we're looking for multiple watchers of a given type,
826 # TicketSQL will be handing it to us as an array of clauses in
829 for my $chunk (@$field) {
830 ( $field, $op, $value, %rest ) = @$chunk;
831 $rest{SUBKEY} ||= 'EmailAddress';
833 my $re_negative_op = qr[!=|NOT LIKE];
834 $self->_OpenParen if $op =~ /$re_negative_op/;
838 FIELD => $rest{SUBKEY},
845 if ( $op =~ /$re_negative_op/ ) {
848 FIELD => $rest{SUBKEY},
851 ENTRYAGGREGATOR => 'OR',
861 Helper function which provides joins to a watchers table both for limits
870 # we cache joins chain per watcher type
871 # if we limit by requestor then we shouldn't join requestors again
872 # for sort or limit on other requestors
873 if ( $self->{'_watcher_join_users_alias'}{ $type || 'any' } ) {
874 return $self->{'_watcher_join_users_alias'}{ $type || 'any' };
877 # we always have watcher groups for ticket
878 # this join should be NORMAL
879 # XXX: if we change this from Join to NewAlias+Limit
880 # then Pg will complain because SB build wrong query.
881 # Query looks like "FROM (Tickets LEFT JOIN CGM ON(Groups.id = CGM.GroupId)), Groups"
882 # Pg doesn't like that fact that it doesn't know about Groups table yet when
883 # join CGM table into Tickets. Problem is in Join method which doesn't use
884 # ALIAS1 argument when build braces.
885 my $groups = $self->Join(
889 FIELD2 => 'Instance',
890 ENTRYAGGREGATOR => 'AND'
895 VALUE => 'RT::Ticket-Role',
896 ENTRYAGGREGATOR => 'AND'
902 ENTRYAGGREGATOR => 'AND'
906 my $groupmembers = $self->Join(
910 TABLE2 => 'CachedGroupMembers',
914 # XXX: work around, we must hide groups that
915 # are members of the role group we search in,
916 # otherwise them result in wrong NULLs in Users
917 # table and break ordering. Now, we know that
918 # RT doesn't allow to add groups as members of the
919 # ticket roles, so we just hide entries in CGM table
920 # with MemberId == GroupId from results
921 my $groupmembers = $self->SUPER::Limit(
922 LEFTJOIN => $groupmembers,
925 VALUE => "$groupmembers.MemberId",
928 my $users = $self->Join(
930 ALIAS1 => $groupmembers,
931 FIELD1 => 'MemberId',
935 return $self->{'_watcher_join_users_alias'}{ $type || 'any' } = $users;
938 =head2 _WatcherMembershipLimit
940 Handle watcher membership limits, i.e. whether the watcher belongs to a
941 specific group or not.
946 SELECT DISTINCT main.*
950 CachedGroupMembers CachedGroupMembers_2,
953 (main.EffectiveId = main.id)
955 (main.Status != 'deleted')
957 (main.Type = 'ticket')
960 (Users_3.EmailAddress = '22')
962 (Groups_1.Domain = 'RT::Ticket-Role')
964 (Groups_1.Type = 'RequestorGroup')
967 Groups_1.Instance = main.id
969 Groups_1.id = CachedGroupMembers_2.GroupId
971 CachedGroupMembers_2.MemberId = Users_3.id
977 sub _WatcherMembershipLimit {
978 my ( $self, $field, $op, $value, @rest ) = @_;
983 my $groups = $self->NewAlias('Groups');
984 my $groupmembers = $self->NewAlias('CachedGroupMembers');
985 my $users = $self->NewAlias('Users');
986 my $memberships = $self->NewAlias('CachedGroupMembers');
988 if ( ref $field ) { # gross hack
989 my @bundle = @$field;
991 for my $chunk (@bundle) {
992 ( $field, $op, $value, @rest ) = @$chunk;
994 ALIAS => $memberships,
1005 ALIAS => $memberships,
1013 # {{{ Tie to groups for tickets we care about
1017 VALUE => 'RT::Ticket-Role',
1018 ENTRYAGGREGATOR => 'AND'
1023 FIELD1 => 'Instance',
1030 # If we care about which sort of watcher
1031 my $meta = $FIELDS{$field};
1032 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1039 ENTRYAGGREGATOR => 'AND'
1046 ALIAS2 => $groupmembers,
1051 ALIAS1 => $groupmembers,
1052 FIELD1 => 'MemberId',
1058 ALIAS1 => $memberships,
1059 FIELD1 => 'MemberId',
1068 sub _LinkFieldLimit {
1073 if ( $restriction->{'TYPE'} ) {
1074 $self->SUPER::Limit(
1075 ALIAS => $LinkAlias,
1076 ENTRYAGGREGATOR => 'AND',
1079 VALUE => $restriction->{'TYPE'}
1083 #If we're trying to limit it to things that are target of
1084 if ( $restriction->{'TARGET'} ) {
1086 # If the TARGET is an integer that means that we want to look at
1087 # the LocalTarget field. otherwise, we want to look at the
1090 if ( $restriction->{'TARGET'} =~ /^(\d+)$/ ) {
1091 $matchfield = "LocalTarget";
1094 $matchfield = "Target";
1096 $self->SUPER::Limit(
1097 ALIAS => $LinkAlias,
1098 ENTRYAGGREGATOR => 'AND',
1099 FIELD => $matchfield,
1101 VALUE => $restriction->{'TARGET'}
1104 #If we're searching on target, join the base to ticket.id
1107 FIELD1 => $self->{'primary_key'},
1108 ALIAS2 => $LinkAlias,
1109 FIELD2 => 'LocalBase'
1113 #If we're trying to limit it to things that are base of
1114 elsif ( $restriction->{'BASE'} ) {
1116 # If we're trying to match a numeric link, we want to look at
1117 # LocalBase, otherwise we want to look at "Base"
1119 if ( $restriction->{'BASE'} =~ /^(\d+)$/ ) {
1120 $matchfield = "LocalBase";
1123 $matchfield = "Base";
1126 $self->SUPER::Limit(
1127 ALIAS => $LinkAlias,
1128 ENTRYAGGREGATOR => 'AND',
1129 FIELD => $matchfield,
1131 VALUE => $restriction->{'BASE'}
1134 #If we're searching on base, join the target to ticket.id
1137 FIELD1 => $self->{'primary_key'},
1138 ALIAS2 => $LinkAlias,
1139 FIELD2 => 'LocalTarget'
1146 Limit based on Keywords
1153 sub _CustomFieldLimit {
1154 my ( $self, $_field, $op, $value, @rest ) = @_;
1157 my $field = $rest{SUBKEY} || die "No field specified";
1159 # For our sanity, we can only limit on one queue at a time
1162 if ( $field =~ /^(.+?)\.{(.+)}$/ ) {
1166 $field = $1 if $field =~ /^{(.+)}$/; # trim { }
1168 # If we're trying to find custom fields that don't match something, we
1169 # want tickets where the custom field has no value at all. Note that
1170 # we explicitly don't include the "IS NULL" case, since we would
1171 # otherwise end up with a redundant clause.
1173 my $null_columns_ok;
1174 if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
1175 $null_columns_ok = 1;
1181 my $q = RT::Queue->new( $self->CurrentUser );
1182 $q->Load($queue) if ($queue);
1186 $cf = $q->CustomField($field);
1189 $cf = RT::CustomField->new( $self->CurrentUser );
1190 $cf->LoadByNameAndQueue( Queue => '0', Name => $field );
1198 my $cfkey = $cfid ? $cfid : "$queue.$field";
1200 # Perform one Join per CustomField
1201 if ( $self->{_sql_object_cf_alias}{$cfkey} ) {
1202 $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey};
1206 $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
1210 TABLE2 => 'ObjectCustomFieldValues',
1211 FIELD2 => 'ObjectId',
1213 $self->SUPER::Limit(
1214 LEFTJOIN => $TicketCFs,
1215 FIELD => 'CustomField',
1217 ENTRYAGGREGATOR => 'AND'
1221 my $cfalias = $self->Join(
1223 EXPRESSION => "'$field'",
1224 TABLE2 => 'CustomFields',
1228 $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
1232 TABLE2 => 'ObjectCustomFieldValues',
1233 FIELD2 => 'CustomField',
1235 $self->SUPER::Limit(
1236 LEFTJOIN => $TicketCFs,
1237 FIELD => 'ObjectId',
1240 ENTRYAGGREGATOR => 'AND',
1243 $self->SUPER::Limit(
1244 LEFTJOIN => $TicketCFs,
1245 FIELD => 'ObjectType',
1246 VALUE => ref( $self->NewItem )
1247 , # we want a single item, not a collection
1248 ENTRYAGGREGATOR => 'AND'
1250 $self->SUPER::Limit(
1251 LEFTJOIN => $TicketCFs,
1252 FIELD => 'Disabled',
1255 ENTRYAGGREGATOR => 'AND'
1259 $self->_OpenParen if ($null_columns_ok);
1262 ALIAS => $TicketCFs,
1270 if ($null_columns_ok) {
1272 ALIAS => $TicketCFs,
1277 ENTRYAGGREGATOR => 'OR',
1280 $self->_CloseParen if ($null_columns_ok);
1284 # End Helper Functions
1286 # End of SQL Stuff -------------------------------------------------
1288 # {{{ Limit the result set based on content
1294 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1295 Generally best called from LimitFoo methods
1305 DESCRIPTION => undef,
1308 $args{'DESCRIPTION'} = $self->loc(
1309 "[_1] [_2] [_3]", $args{'FIELD'},
1310 $args{'OPERATOR'}, $args{'VALUE'}
1312 if ( !defined $args{'DESCRIPTION'} );
1314 my $index = $self->_NextIndex;
1316 #make the TicketRestrictions hash the equivalent of whatever we just passed in;
1318 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1320 $self->{'RecalcTicketLimits'} = 1;
1322 # If we're looking at the effective id, we don't want to append the other clause
1323 # which limits us to tickets where id = effective id
1324 if ( $args{'FIELD'} eq 'EffectiveId'
1325 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1327 $self->{'looking_at_effective_id'} = 1;
1330 if ( $args{'FIELD'} eq 'Type'
1331 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1333 $self->{'looking_at_type'} = 1;
1343 Returns a frozen string suitable for handing back to ThawLimits.
1347 sub _FreezeThawKeys {
1348 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1352 # {{{ sub FreezeLimits
1357 require MIME::Base64;
1358 MIME::Base64::base64_encode(
1359 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1366 Take a frozen Limits string generated by FreezeLimits and make this tickets
1367 object have that set of limits.
1371 # {{{ sub ThawLimits
1377 #if we don't have $in, get outta here.
1378 return undef unless ($in);
1380 $self->{'RecalcTicketLimits'} = 1;
1383 require MIME::Base64;
1385 #We don't need to die if the thaw fails.
1386 @{$self}{ $self->_FreezeThawKeys }
1387 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1389 $RT::Logger->error($@) if $@;
1395 # {{{ Limit by enum or foreign key
1397 # {{{ sub LimitQueue
1401 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1402 OPERATOR is one of = or !=. (It defaults to =).
1403 VALUE is a queue id or Name.
1416 #TODO VALUE should also take queue names and queue objects
1417 #TODO FIXME why are we canonicalizing to name, not id, robrt?
1418 if ( $args{VALUE} =~ /^\d+$/ ) {
1419 my $queue = new RT::Queue( $self->CurrentUser );
1420 $queue->Load( $args{'VALUE'} );
1421 $args{VALUE} = $queue->Name;
1424 # What if they pass in an Id? Check for isNum() and convert to
1427 #TODO check for a valid queue here
1431 VALUE => $args{VALUE},
1432 OPERATOR => $args{'OPERATOR'},
1433 DESCRIPTION => join(
1434 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE},
1442 # {{{ sub LimitStatus
1446 Takes a paramhash with the fields OPERATOR and VALUE.
1447 OPERATOR is one of = or !=.
1450 RT adds Status != 'deleted' until object has
1451 allow_deleted_search internal property set.
1452 $tickets->{'allow_deleted_search'} = 1;
1453 $tickets->LimitStatus( VALUE => 'deleted' );
1465 VALUE => $args{'VALUE'},
1466 OPERATOR => $args{'OPERATOR'},
1467 DESCRIPTION => join( ' ',
1468 $self->loc('Status'), $args{'OPERATOR'},
1469 $self->loc( $args{'VALUE'} ) ),
1475 # {{{ sub IgnoreType
1479 If called, this search will not automatically limit the set of results found
1480 to tickets of type "Ticket". Tickets of other types, such as "project" and
1481 "approval" will be found.
1488 # Instead of faking a Limit that later gets ignored, fake up the
1489 # fact that we're already looking at type, so that the check in
1490 # Tickets_Overlay_SQL/FromSQL goes down the right branch
1492 # $self->LimitType(VALUE => '__any');
1493 $self->{looking_at_type} = 1;
1502 Takes a paramhash with the fields OPERATOR and VALUE.
1503 OPERATOR is one of = or !=, it defaults to "=".
1504 VALUE is a string to search for in the type of the ticket.
1519 VALUE => $args{'VALUE'},
1520 OPERATOR => $args{'OPERATOR'},
1521 DESCRIPTION => join( ' ',
1522 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
1530 # {{{ Limit by string field
1532 # {{{ sub LimitSubject
1536 Takes a paramhash with the fields OPERATOR and VALUE.
1537 OPERATOR is one of = or !=.
1538 VALUE is a string to search for in the subject of the ticket.
1547 VALUE => $args{'VALUE'},
1548 OPERATOR => $args{'OPERATOR'},
1549 DESCRIPTION => join( ' ',
1550 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1558 # {{{ Limit based on ticket numerical attributes
1559 # Things that can be > < = !=
1565 Takes a paramhash with the fields OPERATOR and VALUE.
1566 OPERATOR is one of =, >, < or !=.
1567 VALUE is a ticket Id to search for
1580 VALUE => $args{'VALUE'},
1581 OPERATOR => $args{'OPERATOR'},
1583 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1589 # {{{ sub LimitPriority
1591 =head2 LimitPriority
1593 Takes a paramhash with the fields OPERATOR and VALUE.
1594 OPERATOR is one of =, >, < or !=.
1595 VALUE is a value to match the ticket\'s priority against
1603 FIELD => 'Priority',
1604 VALUE => $args{'VALUE'},
1605 OPERATOR => $args{'OPERATOR'},
1606 DESCRIPTION => join( ' ',
1607 $self->loc('Priority'),
1608 $args{'OPERATOR'}, $args{'VALUE'}, ),
1614 # {{{ sub LimitInitialPriority
1616 =head2 LimitInitialPriority
1618 Takes a paramhash with the fields OPERATOR and VALUE.
1619 OPERATOR is one of =, >, < or !=.
1620 VALUE is a value to match the ticket\'s initial priority against
1625 sub LimitInitialPriority {
1629 FIELD => 'InitialPriority',
1630 VALUE => $args{'VALUE'},
1631 OPERATOR => $args{'OPERATOR'},
1632 DESCRIPTION => join( ' ',
1633 $self->loc('Initial Priority'), $args{'OPERATOR'},
1640 # {{{ sub LimitFinalPriority
1642 =head2 LimitFinalPriority
1644 Takes a paramhash with the fields OPERATOR and VALUE.
1645 OPERATOR is one of =, >, < or !=.
1646 VALUE is a value to match the ticket\'s final priority against
1650 sub LimitFinalPriority {
1654 FIELD => 'FinalPriority',
1655 VALUE => $args{'VALUE'},
1656 OPERATOR => $args{'OPERATOR'},
1657 DESCRIPTION => join( ' ',
1658 $self->loc('Final Priority'), $args{'OPERATOR'},
1665 # {{{ sub LimitTimeWorked
1667 =head2 LimitTimeWorked
1669 Takes a paramhash with the fields OPERATOR and VALUE.
1670 OPERATOR is one of =, >, < or !=.
1671 VALUE is a value to match the ticket's TimeWorked attribute
1675 sub LimitTimeWorked {
1679 FIELD => 'TimeWorked',
1680 VALUE => $args{'VALUE'},
1681 OPERATOR => $args{'OPERATOR'},
1682 DESCRIPTION => join( ' ',
1683 $self->loc('Time worked'),
1684 $args{'OPERATOR'}, $args{'VALUE'}, ),
1690 # {{{ sub LimitTimeLeft
1692 =head2 LimitTimeLeft
1694 Takes a paramhash with the fields OPERATOR and VALUE.
1695 OPERATOR is one of =, >, < or !=.
1696 VALUE is a value to match the ticket's TimeLeft attribute
1704 FIELD => 'TimeLeft',
1705 VALUE => $args{'VALUE'},
1706 OPERATOR => $args{'OPERATOR'},
1707 DESCRIPTION => join( ' ',
1708 $self->loc('Time left'),
1709 $args{'OPERATOR'}, $args{'VALUE'}, ),
1717 # {{{ Limiting based on attachment attributes
1719 # {{{ sub LimitContent
1723 Takes a paramhash with the fields OPERATOR and VALUE.
1724 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1725 VALUE is a string to search for in the body of the ticket
1734 VALUE => $args{'VALUE'},
1735 OPERATOR => $args{'OPERATOR'},
1736 DESCRIPTION => join( ' ',
1737 $self->loc('Ticket content'), $args{'OPERATOR'},
1744 # {{{ sub LimitFilename
1746 =head2 LimitFilename
1748 Takes a paramhash with the fields OPERATOR and VALUE.
1749 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1750 VALUE is a string to search for in the body of the ticket
1758 FIELD => 'Filename',
1759 VALUE => $args{'VALUE'},
1760 OPERATOR => $args{'OPERATOR'},
1761 DESCRIPTION => join( ' ',
1762 $self->loc('Attachment filename'), $args{'OPERATOR'},
1768 # {{{ sub LimitContentType
1770 =head2 LimitContentType
1772 Takes a paramhash with the fields OPERATOR and VALUE.
1773 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1774 VALUE is a content type to search ticket attachments for
1778 sub LimitContentType {
1782 FIELD => 'ContentType',
1783 VALUE => $args{'VALUE'},
1784 OPERATOR => $args{'OPERATOR'},
1785 DESCRIPTION => join( ' ',
1786 $self->loc('Ticket content type'), $args{'OPERATOR'},
1795 # {{{ Limiting based on people
1797 # {{{ sub LimitOwner
1801 Takes a paramhash with the fields OPERATOR and VALUE.
1802 OPERATOR is one of = or !=.
1814 my $owner = new RT::User( $self->CurrentUser );
1815 $owner->Load( $args{'VALUE'} );
1817 # FIXME: check for a valid $owner
1820 VALUE => $args{'VALUE'},
1821 OPERATOR => $args{'OPERATOR'},
1822 DESCRIPTION => join( ' ',
1823 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
1830 # {{{ Limiting watchers
1832 # {{{ sub LimitWatcher
1836 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1837 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1838 VALUE is a value to match the ticket\'s watcher email addresses against
1839 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1843 my $t1 = RT::Ticket->new($RT::SystemUser);
1844 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
1859 #build us up a description
1860 my ( $watcher_type, $desc );
1861 if ( $args{'TYPE'} ) {
1862 $watcher_type = $args{'TYPE'};
1865 $watcher_type = "Watcher";
1869 FIELD => $watcher_type,
1870 VALUE => $args{'VALUE'},
1871 OPERATOR => $args{'OPERATOR'},
1872 TYPE => $args{'TYPE'},
1873 DESCRIPTION => join( ' ',
1874 $self->loc($watcher_type),
1875 $args{'OPERATOR'}, $args{'VALUE'}, ),
1879 sub LimitRequestor {
1882 $RT::Logger->error( "Tickets->LimitRequestor is deprecated at ("
1883 . join( ":", caller )
1885 $self->LimitWatcher( TYPE => 'Requestor', @_ );
1895 # {{{ Limiting based on links
1899 =head2 LimitLinkedTo
1901 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1902 TYPE limits the sort of link we want to search on
1904 TYPE = { RefersTo, MemberOf, DependsOn }
1906 TARGET is the id or URI of the TARGET of the link
1907 (TARGET used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as TARGET
1921 FIELD => 'LinkedTo',
1923 TARGET => ( $args{'TARGET'} || $args{'TICKET'} ),
1924 TYPE => $args{'TYPE'},
1925 DESCRIPTION => $self->loc(
1926 "Tickets [_1] by [_2]",
1927 $self->loc( $args{'TYPE'} ),
1928 ( $args{'TARGET'} || $args{'TICKET'} )
1935 # {{{ LimitLinkedFrom
1937 =head2 LimitLinkedFrom
1939 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1940 TYPE limits the sort of link we want to search on
1943 BASE is the id or URI of the BASE of the link
1944 (BASE used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as BASE
1949 sub LimitLinkedFrom {
1958 # translate RT2 From/To naming to RT3 TicketSQL naming
1959 my %fromToMap = qw(DependsOn DependentOn
1961 RefersTo ReferredToBy);
1963 my $type = $args{'TYPE'};
1964 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
1967 FIELD => 'LinkedTo',
1969 BASE => ( $args{'BASE'} || $args{'TICKET'} ),
1971 DESCRIPTION => $self->loc(
1972 "Tickets [_1] [_2]",
1973 $self->loc( $args{'TYPE'} ),
1974 ( $args{'BASE'} || $args{'TICKET'} )
1984 my $ticket_id = shift;
1985 $self->LimitLinkedTo(
1986 TARGET => "$ticket_id",
1994 # {{{ LimitHasMember
1995 sub LimitHasMember {
1997 my $ticket_id = shift;
1998 $self->LimitLinkedFrom(
1999 BASE => "$ticket_id",
2000 TYPE => 'HasMember',
2007 # {{{ LimitDependsOn
2009 sub LimitDependsOn {
2011 my $ticket_id = shift;
2012 $self->LimitLinkedTo(
2013 TARGET => "$ticket_id",
2014 TYPE => 'DependsOn',
2021 # {{{ LimitDependedOnBy
2023 sub LimitDependedOnBy {
2025 my $ticket_id = shift;
2026 $self->LimitLinkedFrom(
2027 BASE => "$ticket_id",
2028 TYPE => 'DependentOn',
2039 my $ticket_id = shift;
2040 $self->LimitLinkedTo(
2041 TARGET => "$ticket_id",
2049 # {{{ LimitReferredToBy
2051 sub LimitReferredToBy {
2053 my $ticket_id = shift;
2054 $self->LimitLinkedFrom(
2055 BASE => "$ticket_id",
2056 TYPE => 'ReferredToBy',
2065 # {{{ limit based on ticket date attribtes
2069 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2071 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2073 OPERATOR is one of > or <
2074 VALUE is a date and time in ISO format in GMT
2075 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2077 There are also helper functions of the form LimitFIELD that eliminate
2078 the need to pass in a FIELD argument.
2092 #Set the description if we didn't get handed it above
2093 unless ( $args{'DESCRIPTION'} ) {
2094 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2095 . $args{'OPERATOR'} . " "
2096 . $args{'VALUE'} . " GMT";
2099 $self->Limit(%args);
2107 $self->LimitDate( FIELD => 'Created', @_ );
2112 $self->LimitDate( FIELD => 'Due', @_ );
2118 $self->LimitDate( FIELD => 'Starts', @_ );
2124 $self->LimitDate( FIELD => 'Started', @_ );
2129 $self->LimitDate( FIELD => 'Resolved', @_ );
2134 $self->LimitDate( FIELD => 'Told', @_ );
2137 sub LimitLastUpdated {
2139 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2143 # {{{ sub LimitTransactionDate
2145 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2147 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2149 OPERATOR is one of > or <
2150 VALUE is a date and time in ISO format in GMT
2155 sub LimitTransactionDate {
2158 FIELD => 'TransactionDate',
2165 # <20021217042756.GK28744@pallas.fsck.com>
2166 # "Kill It" - Jesse.
2168 #Set the description if we didn't get handed it above
2169 unless ( $args{'DESCRIPTION'} ) {
2170 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2171 . $args{'OPERATOR'} . " "
2172 . $args{'VALUE'} . " GMT";
2175 $self->Limit(%args);
2183 # {{{ Limit based on custom fields
2184 # {{{ sub LimitCustomField
2186 =head2 LimitCustomField
2188 Takes a paramhash of key/value pairs with the following keys:
2192 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2194 =item OPERATOR - The usual Limit operators
2196 =item VALUE - The value to compare against
2202 sub LimitCustomField {
2206 CUSTOMFIELD => undef,
2208 DESCRIPTION => undef,
2209 FIELD => 'CustomFieldValue',
2214 my $CF = RT::CustomField->new( $self->CurrentUser );
2215 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2216 $CF->Load( $args{CUSTOMFIELD} );
2219 $CF->LoadByNameAndQueue(
2220 Name => $args{CUSTOMFIELD},
2221 Queue => $args{QUEUE}
2223 $args{CUSTOMFIELD} = $CF->Id;
2226 #If we are looking to compare with a null value.
2227 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2228 $args{'DESCRIPTION'}
2229 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2231 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2232 $args{'DESCRIPTION'}
2233 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2236 # if we're not looking to compare with a null value
2238 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2239 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2244 my $qo = new RT::Queue( $self->CurrentUser );
2245 $qo->Load( $CF->Queue );
2250 @rest = ( ENTRYAGGREGATOR => 'AND' )
2251 if ( $CF->Type eq 'SelectMultiple' );
2254 VALUE => $args{VALUE},
2258 ? $q . ".{" . $CF->Name . "}"
2261 OPERATOR => $args{OPERATOR},
2266 $self->{'RecalcTicketLimits'} = 1;
2272 # {{{ sub _NextIndex
2276 Keep track of the counter for the array of restrictions
2282 return ( $self->{'restriction_index'}++ );
2289 # {{{ Core bits to make this a DBIx::SearchBuilder object
2294 $self->{'table'} = "Tickets";
2295 $self->{'RecalcTicketLimits'} = 1;
2296 $self->{'looking_at_effective_id'} = 0;
2297 $self->{'looking_at_type'} = 0;
2298 $self->{'restriction_index'} = 1;
2299 $self->{'primary_key'} = "id";
2300 delete $self->{'items_array'};
2301 delete $self->{'item_map'};
2302 delete $self->{'columns_to_display'};
2303 $self->SUPER::_Init(@_);
2314 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2315 return ( $self->SUPER::Count() );
2323 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2324 return ( $self->SUPER::CountAll() );
2329 # {{{ sub ItemsArrayRef
2331 =head2 ItemsArrayRef
2333 Returns a reference to the set of all items found in this search
2341 unless ( $self->{'items_array'} ) {
2343 my $placeholder = $self->_ItemsCounter;
2344 $self->GotoFirstItem();
2345 while ( my $item = $self->Next ) {
2346 push( @{ $self->{'items_array'} }, $item );
2348 $self->GotoItem($placeholder);
2349 $self->{'items_array'}
2350 = $self->ItemsOrderBy( $self->{'items_array'} );
2352 return ( $self->{'items_array'} );
2361 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2363 my $Ticket = $self->SUPER::Next();
2364 if ( ( defined($Ticket) ) and ( ref($Ticket) ) ) {
2366 if ( $Ticket->__Value('Status') eq 'deleted'
2367 && !$self->{'allow_deleted_search'} )
2369 return ( $self->Next() );
2372 # Since Ticket could be granted with more rights instead
2373 # of being revoked, it's ok if queue rights allow
2374 # ShowTicket. It seems need another query, but we have
2375 # rights cache in Principal::HasRight.
2376 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
2377 || $Ticket->CurrentUserHasRight('ShowTicket') )
2382 if ( $Ticket->__Value('Status') eq 'deleted' ) {
2383 return ( $self->Next() );
2386 # Since Ticket could be granted with more rights instead
2387 # of being revoked, it's ok if queue rights allow
2388 # ShowTicket. It seems need another query, but we have
2389 # rights cache in Principal::HasRight.
2390 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
2391 || $Ticket->CurrentUserHasRight('ShowTicket') )
2396 #If the user doesn't have the right to show this ticket
2398 return ( $self->Next() );
2402 #if there never was any ticket
2413 # {{{ Deal with storing and restoring restrictions
2415 # {{{ sub LoadRestrictions
2417 =head2 LoadRestrictions
2419 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
2420 TODO It is not yet implemented
2426 # {{{ sub DescribeRestrictions
2428 =head2 DescribeRestrictions
2431 Returns a hash keyed by restriction id.
2432 Each element of the hash is currently a one element hash that contains DESCRIPTION which
2433 is a description of the purpose of that TicketRestriction
2437 sub DescribeRestrictions {
2440 my ( $row, %listing );
2442 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2443 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
2450 # {{{ sub RestrictionValues
2452 =head2 RestrictionValues FIELD
2454 Takes a restriction field and returns a list of values this field is restricted
2459 sub RestrictionValues {
2462 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
2463 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
2464 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
2466 keys %{ $self->{'TicketRestrictions'} };
2471 # {{{ sub ClearRestrictions
2473 =head2 ClearRestrictions
2475 Removes all restrictions irretrievably
2479 sub ClearRestrictions {
2481 delete $self->{'TicketRestrictions'};
2482 $self->{'looking_at_effective_id'} = 0;
2483 $self->{'looking_at_type'} = 0;
2484 $self->{'RecalcTicketLimits'} = 1;
2489 # {{{ sub DeleteRestriction
2491 =head2 DeleteRestriction
2493 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
2494 Removes that restriction from the session's limits.
2498 sub DeleteRestriction {
2501 delete $self->{'TicketRestrictions'}{$row};
2503 $self->{'RecalcTicketLimits'} = 1;
2505 #make the underlying easysearch object forget all its preconceptions
2510 # {{{ sub _RestrictionsToClauses
2512 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
2514 sub _RestrictionsToClauses {
2519 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2520 my $restriction = $self->{'TicketRestrictions'}{$row};
2523 #print Dumper($restriction),"\n";
2525 # We need to reimplement the subclause aggregation that SearchBuilder does.
2526 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
2527 # Then SB AND's the different Subclauses together.
2529 # So, we want to group things into Subclauses, convert them to
2530 # SQL, and then join them with the appropriate DefaultEA.
2531 # Then join each subclause group with AND.
2533 my $field = $restriction->{'FIELD'};
2534 my $realfield = $field; # CustomFields fake up a fieldname, so
2535 # we need to figure that out
2538 # Rewrite LinkedTo meta field to the real field
2539 if ( $field =~ /LinkedTo/ ) {
2540 $realfield = $field = $restriction->{'TYPE'};
2544 # Handle subkey fields with a different real field
2545 if ( $field =~ /^(\w+)\./ ) {
2549 die "I don't know about $field yet"
2550 unless ( exists $FIELDS{$realfield}
2551 or $restriction->{CUSTOMFIELD} );
2553 my $type = $FIELDS{$realfield}->[0];
2554 my $op = $restriction->{'OPERATOR'};
2558 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
2561 # this performs the moral equivalent of defined or/dor/C<//>,
2562 # without the short circuiting.You need to use a 'defined or'
2563 # type thing instead of just checking for truth values, because
2564 # VALUE could be 0.(i.e. "false")
2566 # You could also use this, but I find it less aesthetic:
2567 # (although it does short circuit)
2568 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2569 # defined $restriction->{'TICKET'} ?
2570 # $restriction->{TICKET} :
2571 # defined $restriction->{'BASE'} ?
2572 # $restriction->{BASE} :
2573 # defined $restriction->{'TARGET'} ?
2574 # $restriction->{TARGET} )
2576 my $ea = $restriction->{ENTRYAGGREGATOR}
2577 || $DefaultEA{$type}
2580 die "Invalid operator $op for $field ($type)"
2581 unless exists $ea->{$op};
2585 # Each CustomField should be put into a different Clause so they
2586 # are ANDed together.
2587 if ( $restriction->{CUSTOMFIELD} ) {
2588 $realfield = $field;
2591 exists $clause{$realfield} or $clause{$realfield} = [];
2594 $field =~ s!(['"])!\\$1!g;
2595 $value =~ s!(['"])!\\$1!g;
2596 my $data = [ $ea, $type, $field, $op, $value ];
2598 # here is where we store extra data, say if it's a keyword or
2599 # something. (I.e. "TYPE SPECIFIC STUFF")
2601 #print Dumper($data);
2602 push @{ $clause{$realfield} }, $data;
2609 # {{{ sub _ProcessRestrictions
2611 =head2 _ProcessRestrictions PARAMHASH
2613 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
2614 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
2618 sub _ProcessRestrictions {
2621 #Blow away ticket aliases since we'll need to regenerate them for
2623 delete $self->{'TicketAliases'};
2624 delete $self->{'items_array'};
2625 delete $self->{'item_map'};
2626 delete $self->{'raw_rows'};
2627 delete $self->{'rows'};
2628 delete $self->{'count_all'};
2630 my $sql = $self->Query; # Violating the _SQL namespace
2631 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
2633 # "Restrictions to Clauses Branch\n";
2634 my $clauseRef = eval { $self->_RestrictionsToClauses; };
2636 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2640 $sql = $self->ClausesToSQL($clauseRef);
2641 $self->FromSQL($sql) if $sql;
2645 $self->{'RecalcTicketLimits'} = 0;
2649 =head2 _BuildItemMap
2651 # Build up a map of first/last/next/prev items, so that we can display search nav quickly
2658 my $items = $self->ItemsArrayRef;
2661 delete $self->{'item_map'};
2662 if ( $items->[0] ) {
2663 $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
2664 while ( my $item = shift @$items ) {
2665 my $id = $item->EffectiveId;
2666 $self->{'item_map'}->{$id}->{'defined'} = 1;
2667 $self->{'item_map'}->{$id}->{prev} = $prev;
2668 $self->{'item_map'}->{$id}->{next} = $items->[0]->EffectiveId
2672 $self->{'item_map'}->{'last'} = $prev;
2678 Returns an a map of all items found by this search. The map is of the form
2680 $ItemMap->{'first'} = first ticketid found
2681 $ItemMap->{'last'} = last ticketid found
2682 $ItemMap->{$id}->{prev} = the ticket id found before $id
2683 $ItemMap->{$id}->{next} = the ticket id found after $id
2689 $self->_BuildItemMap()
2690 unless ( $self->{'items_array'} and $self->{'item_map'} );
2691 return ( $self->{'item_map'} );
2706 =head2 PrepForSerialization
2708 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
2712 sub PrepForSerialization {
2714 delete $self->{'items'};
2715 $self->RedoSearch();
2720 RT::Tickets supports several flags which alter search behavior:
2723 allow_deleted_search (Otherwise never show deleted tickets in search results)
2724 looking_at_type (otherwise limit to type=ticket)
2726 These flags are set by calling
2728 $tickets->{'flagname'} = 1;
2730 BUG: There should be an API for this
2736 # We assume that we've got some tickets hanging around from before.
2737 ok( my $unlimittickets = RT::Tickets->new( $RT::SystemUser ) );
2738 ok( $unlimittickets->UnLimit );
2739 ok( $unlimittickets->Count > 0, "UnLimited tickets object should return tickets" );