1 # {{{ BEGIN BPS TAGGED BLOCK
5 # This software is Copyright (c) 1996-2004 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);
81 no warnings qw(redefine);
82 use vars qw(@SORTFIELDS);
86 # Configuration Tables:
88 # FIELDS is a mapping of searchable Field name, to Type, and other
93 Queue => ['ENUM' => 'Queue',],
95 Creator => ['ENUM' => 'User',],
96 LastUpdatedBy => ['ENUM' => 'User',],
97 Owner => ['ENUM' => 'User',],
98 EffectiveId => ['INT',],
100 InitialPriority => ['INT',],
101 FinalPriority => ['INT',],
102 Priority => ['INT',],
103 TimeLeft => ['INT',],
104 TimeWorked => ['INT',],
105 MemberOf => ['LINK' => To => 'MemberOf', ],
106 DependsOn => ['LINK' => To => 'DependsOn',],
107 RefersTo => ['LINK' => To => 'RefersTo',],
108 HasMember => ['LINK' => From => 'MemberOf',],
109 DependentOn => ['LINK' => From => 'DependsOn',],
110 DependedOnBy => ['LINK' => From => 'DependsOn',],
111 ReferredToBy => ['LINK' => From => 'RefersTo',],
112 # HasDepender => ['LINK',],
113 # RelatedTo => ['LINK',],
114 Told => ['DATE' => 'Told',],
115 Starts => ['DATE' => 'Starts',],
116 Started => ['DATE' => 'Started',],
117 Due => ['DATE' => 'Due',],
118 Resolved => ['DATE' => 'Resolved',],
119 LastUpdated => ['DATE' => 'LastUpdated',],
120 Created => ['DATE' => 'Created',],
121 Subject => ['STRING',],
122 Content => ['TRANSFIELD',],
123 ContentType => ['TRANSFIELD',],
124 Filename => ['TRANSFIELD',],
125 TransactionDate => ['TRANSDATE',],
126 Requestor => ['WATCHERFIELD' => 'Requestor',],
127 Requestors => ['WATCHERFIELD' => 'Requestor',],
128 Cc => ['WATCHERFIELD' => 'Cc',],
129 AdminCc => ['WATCHERFIELD' => 'AdminCC',],
130 Watcher => ['WATCHERFIELD'],
131 LinkedTo => ['LINKFIELD',],
132 CustomFieldValue =>['CUSTOMFIELD',],
133 CF => ['CUSTOMFIELD',],
136 # Mapping of Field Type to Function
138 ( ENUM => \&_EnumLimit,
140 LINK => \&_LinkLimit,
141 DATE => \&_DateLimit,
142 STRING => \&_StringLimit,
143 TRANSFIELD => \&_TransLimit,
144 TRANSDATE => \&_TransDateLimit,
145 WATCHERFIELD => \&_WatcherLimit,
146 LINKFIELD => \&_LinkFieldLimit,
147 CUSTOMFIELD => \&_CustomFieldLimit,
150 ( WATCHERFIELD => "yeps",
153 # Default EntryAggregator per type
154 # if you specify OP, you must specify all valid OPs
157 ENUM => { '=' => 'OR',
160 DATE => { '=' => 'OR',
166 STRING => { '=' => 'OR',
177 WATCHERFIELD => { '=' => 'OR',
187 # Helper functions for passing the above lexically scoped tables above
188 # into Tickets_Overlay_SQL.
189 sub FIELDS { return \%FIELDS }
190 sub dispatch { return \%dispatch }
191 sub can_bundle { return \%can_bundle }
193 # Bring in the clowns.
194 require RT::Tickets_Overlay_SQL;
198 @SORTFIELDS = qw(id Status
200 Owner Created Due Starts Started
202 Resolved LastUpdated Priority TimeWorked TimeLeft);
206 Returns the list of fields that lists of tickets can easily be sorted by
219 # BEGIN SQL STUFF *********************************
221 =head1 Limit Helper Routines
223 These routines are the targets of a dispatch table depending on the
224 type of field. They all share the same signature:
226 my ($self,$field,$op,$value,@rest) = @_;
228 The values in @rest should be suitable for passing directly to
229 DBIx::SearchBuilder::Limit.
231 Essentially they are an expanded/broken out (and much simplified)
232 version of what ProcessRestrictions used to do. They're also much
233 more clearly delineated by the TYPE of field being processed.
237 Handle Fields which are limited to certain values, and potentially
238 need to be looked up from another class.
240 This subroutine actually handles two different kinds of fields. For
241 some the user is responsible for limiting the values. (i.e. Status,
244 For others, the value specified by the user will be looked by via
248 name of class to lookup in (Optional)
253 my ($sb,$field,$op,$value,@rest) = @_;
255 # SQL::Statement changes != to <>. (Can we remove this now?)
256 $op = "!=" if $op eq "<>";
258 die "Invalid Operation: $op for $field"
259 unless $op eq "=" or $op eq "!=";
261 my $meta = $FIELDS{$field};
262 if (defined $meta->[1]) {
263 my $class = "RT::" . $meta->[1];
264 my $o = $class->new($sb->CurrentUser);
268 $sb->_SQLLimit( FIELD => $field,,
277 Handle fields where the values are limited to integers. (For example,
278 Priority, TimeWorked.)
286 my ($sb,$field,$op,$value,@rest) = @_;
288 die "Invalid Operator $op for $field"
289 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
302 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
305 1: Direction (From,To)
306 2: Link Type (MemberOf, DependsOn,RefersTo)
311 my ($sb,$field,$op,$value,@rest) = @_;
316 my $meta = $FIELDS{$field};
317 die "Incorrect Meta Data for $field"
318 unless (defined $meta->[1] and defined $meta->[2]);
320 $sb->{_sql_linkalias} = $sb->NewAlias ('Links')
321 unless defined $sb->{_sql_linkalias};
326 ALIAS => $sb->{_sql_linkalias},
333 if ($meta->[1] eq "To") {
334 my $matchfield = ( $value =~ /^(\d+)$/ ? "LocalTarget" : "Target" );
337 ALIAS => $sb->{_sql_linkalias},
338 ENTRYAGGREGATOR => 'AND',
339 FIELD => $matchfield,
344 #If we're searching on target, join the base to ticket.id
345 $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
346 ALIAS2 => $sb->{_sql_linkalias}, FIELD2 => 'LocalBase');
348 } elsif ( $meta->[1] eq "From" ) {
349 my $matchfield = ( $value =~ /^(\d+)$/ ? "LocalBase" : "Base" );
352 ALIAS => $sb->{_sql_linkalias},
353 ENTRYAGGREGATOR => 'AND',
354 FIELD => $matchfield,
359 #If we're searching on base, join the target to ticket.id
360 $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
361 ALIAS2 => $sb->{_sql_linkalias}, FIELD2 => 'LocalTarget');
364 die "Invalid link direction '$meta->[1]' for $field\n";
373 Handle date fields. (Created, LastTold..)
376 1: type of link. (Probably not necessary.)
381 my ($sb,$field,$op,$value,@rest) = @_;
383 die "Invalid Date Op: $op"
384 unless $op =~ /^(=|>|<|>=|<=)$/;
386 my $meta = $FIELDS{$field};
387 die "Incorrect Meta Data for $field"
388 unless (defined $meta->[1]);
390 require Time::ParseDate;
391 use POSIX 'strftime';
393 # FIXME: Replace me with RT::Date( Type => 'unknown' ...)
394 my $time = Time::ParseDate::parsedate( $value,
395 UK => $RT::DateDayBeforeMonth,
396 PREFER_PAST => $RT::AmbiguousDayInPast,
397 PREFER_FUTURE => !($RT::AmbiguousDayInPast),
402 # if we're specifying =, that means we want everything on a
403 # particular single day. in the database, we need to check for >
404 # and < the edges of that day.
406 my $daystart = strftime("%Y-%m-%d %H:%M",
407 gmtime($time - ( $time % 86400 )));
408 my $dayend = strftime("%Y-%m-%d %H:%M",
409 gmtime($time + ( 86399 - $time % 86400 )));
425 ENTRYAGGREGATOR => 'AND',
431 $value = strftime("%Y-%m-%d %H:%M", gmtime($time));
443 Handle simple fields which are just strings. (Subject,Type)
451 my ($sb,$field,$op,$value,@rest) = @_;
455 # =, !=, LIKE, NOT LIKE
466 =head2 _TransDateLimit
468 Handle fields limiting based on Transaction Date.
470 The inpupt value must be in a format parseable by Time::ParseDate
477 sub _TransDateLimit {
478 my ($sb,$field,$op,$value,@rest) = @_;
480 # See the comments for TransLimit, they apply here too
482 $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
483 unless defined $sb->{_sql_transalias};
484 $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
485 unless defined $sb->{_sql_trattachalias};
489 # Join Transactions To Attachments
490 $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
491 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
493 # Join Transactions to Tickets
494 $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
495 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
497 my $d = new RT::Date( $sb->CurrentUser );
498 $d->Set( Format => 'ISO', Value => $value);
501 #Search for the right field
502 $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
515 Limit based on the Content of a transaction or the ContentType.
523 # Content, ContentType, Filename
525 # If only this was this simple. We've got to do something
528 #Basically, we want to make sure that the limits apply to
529 #the same attachment, rather than just another attachment
530 #for the same ticket, no matter how many clauses we lump
531 #on. We put them in TicketAliases so that they get nuked
532 #when we redo the join.
534 # In the SQL, we might have
535 # (( Content = foo ) or ( Content = bar AND Content = baz ))
536 # The AND group should share the same Alias.
538 # Actually, maybe it doesn't matter. We use the same alias and it
539 # works itself out? (er.. different.)
541 # Steal more from _ProcessRestrictions
543 # FIXME: Maybe look at the previous FooLimit call, and if it was a
544 # TransLimit and EntryAggregator == AND, reuse the Aliases?
546 # Or better - store the aliases on a per subclause basis - since
547 # those are going to be the things we want to relate to each other,
550 # maybe we should not allow certain kinds of aggregation of these
551 # clauses and do a psuedo regex instead? - the problem is getting
552 # them all into the same subclause when you have (A op B op C) - the
553 # way they get parsed in the tree they're in different subclauses.
555 my ($sb,$field,$op,$value,@rest) = @_;
557 $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
558 unless defined $sb->{_sql_transalias};
559 $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
560 unless defined $sb->{_sql_trattachalias};
564 #Search for the right field
565 $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
573 # Join Transactions To Attachments
574 $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
575 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
577 # Join Transactions to Tickets
578 $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
579 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
587 Handle watcher limits. (Requestor, CC, etc..)
595 # Test to make sure that you can search for tickets by requestor address and
599 my $u1 = RT::User->new($RT::SystemUser);
600 ($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
602 my $u2 = RT::User->new($RT::SystemUser);
603 ($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
606 my $t1 = RT::Ticket->new($RT::SystemUser);
608 ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
611 my $t2 = RT::Ticket->new($RT::SystemUser);
612 ($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
616 my $t3 = RT::Ticket->new($RT::SystemUser);
617 ($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
621 my $tix1 = RT::Tickets->new($RT::SystemUser);
622 $tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
624 is ($tix1->Count, 3);
626 my $tix2 = RT::Tickets->new($RT::SystemUser);
627 $tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
629 is ($tix2->Count, 3);
632 my $tix3 = RT::Tickets->new($RT::SystemUser);
633 $tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
635 is ($tix3->Count, 2);
637 my $tix4 = RT::Tickets->new($RT::SystemUser);
638 $tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
640 is ($tix4->Count, 2);
642 # Searching for tickets that have two requestors isn't supported
643 # There's no way to differentiate "one requestor name that matches foo and bar"
644 # and "two requestors, one matching foo and one matching bar"
646 # my $tix5 = RT::Tickets->new($RT::SystemUser);
647 # $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
649 # is ($tix5->Count, 1);
651 # my $tix6 = RT::Tickets->new($RT::SystemUser);
652 # $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
654 # is ($tix6->Count, 1);
670 my $groups = $self->NewAlias('Groups');
671 my $groupmembers = $self->NewAlias('CachedGroupMembers');
672 my $users = $self->NewAlias('Users');
674 # If we're looking for multiple watchers of a given type,
675 # TicketSQL will be handing it to us as an array of cluases in
677 if ( ref $field ) { # gross hack
679 for my $chunk (@$field) {
680 ( $field, $op, $value, %rest ) = @$chunk;
683 FIELD => $rest{SUBKEY} || 'EmailAddress',
695 FIELD => $rest{SUBKEY} || 'EmailAddress',
703 # {{{ Tie to groups for tickets we care about
707 VALUE => 'RT::Ticket-Role',
708 ENTRYAGGREGATOR => 'AND'
713 FIELD1 => 'Instance',
720 # If we care about which sort of watcher
721 my $meta = $FIELDS{$field};
722 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
729 ENTRYAGGREGATOR => 'AND'
736 ALIAS2 => $groupmembers,
741 ALIAS1 => $groupmembers,
742 FIELD1 => 'MemberId',
751 sub _LinkFieldLimit {
756 if ($restriction->{'TYPE'}) {
757 $self->SUPER::Limit(ALIAS => $LinkAlias,
758 ENTRYAGGREGATOR => 'AND',
761 VALUE => $restriction->{'TYPE'} );
764 #If we're trying to limit it to things that are target of
765 if ($restriction->{'TARGET'}) {
766 # If the TARGET is an integer that means that we want to look at
767 # the LocalTarget field. otherwise, we want to look at the
770 if ($restriction->{'TARGET'} =~/^(\d+)$/) {
771 $matchfield = "LocalTarget";
773 $matchfield = "Target";
775 $self->SUPER::Limit(ALIAS => $LinkAlias,
776 ENTRYAGGREGATOR => 'AND',
777 FIELD => $matchfield,
779 VALUE => $restriction->{'TARGET'} );
780 #If we're searching on target, join the base to ticket.id
781 $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
782 ALIAS2 => $LinkAlias,
783 FIELD2 => 'LocalBase');
785 #If we're trying to limit it to things that are base of
786 elsif ($restriction->{'BASE'}) {
787 # If we're trying to match a numeric link, we want to look at
788 # LocalBase, otherwise we want to look at "Base"
790 if ($restriction->{'BASE'} =~/^(\d+)$/) {
791 $matchfield = "LocalBase";
793 $matchfield = "Base";
796 $self->SUPER::Limit(ALIAS => $LinkAlias,
797 ENTRYAGGREGATOR => 'AND',
798 FIELD => $matchfield,
800 VALUE => $restriction->{'BASE'} );
801 #If we're searching on base, join the target to ticket.id
802 $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
803 ALIAS2 => $LinkAlias,
804 FIELD2 => 'LocalTarget')
811 Limit based on Keywords
818 sub _CustomFieldLimit {
819 my ($self,$_field,$op,$value,@rest) = @_;
822 my $field = $rest{SUBKEY} || die "No field specified";
824 # For our sanity, we can only limit on one queue at a time
828 if ($field =~ /^(.+?)\.{(.+)}$/) {
832 $field = $1 if $field =~ /^{(.+)}$/; # trim { }
834 my $q = RT::Queue->new($self->CurrentUser);
835 $q->Load($queue) if ($queue);
839 $cf = $q->CustomField($field);
842 $cf = RT::CustomField->new($self->CurrentUser);
843 $cf->LoadByNameAndQueue(Queue => '0', Name => $field);
852 die "No custom field named $field found\n" unless $cfid;
859 # Perform one Join per CustomField
860 if ($self->{_sql_keywordalias}{$cfid}) {
861 $TicketCFs = $self->{_sql_keywordalias}{$cfid};
863 $TicketCFs = $self->{_sql_keywordalias}{$cfid} =
864 $self->_SQLJoin( TYPE => 'left',
867 TABLE2 => 'TicketCustomFieldValues',
868 FIELD2 => 'Ticket' );
873 $self->_SQLLimit( ALIAS => $TicketCFs,
881 # If we're trying to find custom fields that don't match something, we want tickets
882 # where the custom field has no value at all
884 if ( ($op =~ /^IS$/i) || ($op =~ /^NOT LIKE$/i) || ( $op eq '!=' ) ) {
885 $null_columns_ok = 1;
889 if ( $null_columns_ok && $op !~ /IS/i && uc $value ne "NULL") {
890 $self->_SQLLimit( ALIAS => $TicketCFs,
895 ENTRYAGGREGATOR => 'OR', );
898 $self->_SQLLimit( LEFTJOIN => $TicketCFs,
899 FIELD => 'CustomField',
901 ENTRYAGGREGATOR => 'OR' );
910 # End Helper Functions
912 # End of SQL Stuff -------------------------------------------------
914 # {{{ Limit the result set based on content
920 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
921 Generally best called from LimitFoo methods
926 my %args = ( FIELD => undef,
929 DESCRIPTION => undef,
932 $args{'DESCRIPTION'} = $self->loc(
933 "[_1] [_2] [_3]", $args{'FIELD'}, $args{'OPERATOR'}, $args{'VALUE'}
934 ) if (!defined $args{'DESCRIPTION'}) ;
936 my $index = $self->_NextIndex;
938 #make the TicketRestrictions hash the equivalent of whatever we just passed in;
940 %{$self->{'TicketRestrictions'}{$index}} = %args;
942 $self->{'RecalcTicketLimits'} = 1;
944 # If we're looking at the effective id, we don't want to append the other clause
945 # which limits us to tickets where id = effective id
946 if ($args{'FIELD'} eq 'EffectiveId') {
947 $self->{'looking_at_effective_id'} = 1;
950 if ($args{'FIELD'} eq 'Type') {
951 $self->{'looking_at_type'} = 1;
964 Returns a frozen string suitable for handing back to ThawLimits.
968 sub _FreezeThawKeys {
969 'TicketRestrictions',
971 'looking_at_effective_id',
975 # {{{ sub FreezeLimits
980 return (FreezeThaw::freeze(@{$self}{$self->_FreezeThawKeys}));
987 Take a frozen Limits string generated by FreezeLimits and make this tickets
988 object have that set of limits.
997 #if we don't have $in, get outta here.
998 return undef unless ($in);
1000 $self->{'RecalcTicketLimits'} = 1;
1004 #We don't need to die if the thaw fails.
1007 @{$self}{$self->_FreezeThawKeys} = FreezeThaw::thaw($in);
1009 $RT::Logger->error( $@ ) if $@;
1015 # {{{ Limit by enum or foreign key
1017 # {{{ sub LimitQueue
1021 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1022 OPERATOR is one of = or !=. (It defaults to =).
1023 VALUE is a queue id or Name.
1030 my %args = (VALUE => undef,
1034 #TODO VALUE should also take queue names and queue objects
1035 #TODO FIXME why are we canonicalizing to name, not id, robrt?
1036 if ($args{VALUE} =~ /^\d+$/) {
1037 my $queue = new RT::Queue($self->CurrentUser);
1038 $queue->Load($args{'VALUE'});
1039 $args{VALUE} = $queue->Name;
1042 # What if they pass in an Id? Check for isNum() and convert to
1045 #TODO check for a valid queue here
1047 $self->Limit (FIELD => 'Queue',
1048 VALUE => $args{VALUE},
1049 OPERATOR => $args{'OPERATOR'},
1050 DESCRIPTION => join(
1051 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE},
1058 # {{{ sub LimitStatus
1062 Takes a paramhash with the fields OPERATOR and VALUE.
1063 OPERATOR is one of = or !=.
1070 my %args = ( OPERATOR => '=',
1072 $self->Limit (FIELD => 'Status',
1073 VALUE => $args{'VALUE'},
1074 OPERATOR => $args{'OPERATOR'},
1075 DESCRIPTION => join(
1076 ' ', $self->loc('Status'), $args{'OPERATOR'}, $self->loc($args{'VALUE'})
1083 # {{{ sub IgnoreType
1087 If called, this search will not automatically limit the set of results found
1088 to tickets of type "Ticket". Tickets of other types, such as "project" and
1089 "approval" will be found.
1096 # Instead of faking a Limit that later gets ignored, fake up the
1097 # fact that we're already looking at type, so that the check in
1098 # Tickets_Overlay_SQL/FromSQL goes down the right branch
1100 # $self->LimitType(VALUE => '__any');
1101 $self->{looking_at_type} = 1;
1110 Takes a paramhash with the fields OPERATOR and VALUE.
1111 OPERATOR is one of = or !=, it defaults to "=".
1112 VALUE is a string to search for in the type of the ticket.
1120 my %args = (OPERATOR => '=',
1123 $self->Limit (FIELD => 'Type',
1124 VALUE => $args{'VALUE'},
1125 OPERATOR => $args{'OPERATOR'},
1126 DESCRIPTION => join(
1127 ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'},
1136 # {{{ Limit by string field
1138 # {{{ sub LimitSubject
1142 Takes a paramhash with the fields OPERATOR and VALUE.
1143 OPERATOR is one of = or !=.
1144 VALUE is a string to search for in the subject of the ticket.
1151 $self->Limit (FIELD => 'Subject',
1152 VALUE => $args{'VALUE'},
1153 OPERATOR => $args{'OPERATOR'},
1154 DESCRIPTION => join(
1155 ' ', $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'},
1164 # {{{ Limit based on ticket numerical attributes
1165 # Things that can be > < = !=
1171 Takes a paramhash with the fields OPERATOR and VALUE.
1172 OPERATOR is one of =, >, < or !=.
1173 VALUE is a ticket Id to search for
1179 my %args = (OPERATOR => '=',
1182 $self->Limit (FIELD => 'id',
1183 VALUE => $args{'VALUE'},
1184 OPERATOR => $args{'OPERATOR'},
1185 DESCRIPTION => join(
1186 ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'},
1193 # {{{ sub LimitPriority
1195 =head2 LimitPriority
1197 Takes a paramhash with the fields OPERATOR and VALUE.
1198 OPERATOR is one of =, >, < or !=.
1199 VALUE is a value to match the ticket\'s priority against
1206 $self->Limit (FIELD => 'Priority',
1207 VALUE => $args{'VALUE'},
1208 OPERATOR => $args{'OPERATOR'},
1209 DESCRIPTION => join(
1210 ' ', $self->loc('Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1217 # {{{ sub LimitInitialPriority
1219 =head2 LimitInitialPriority
1221 Takes a paramhash with the fields OPERATOR and VALUE.
1222 OPERATOR is one of =, >, < or !=.
1223 VALUE is a value to match the ticket\'s initial priority against
1228 sub LimitInitialPriority {
1231 $self->Limit (FIELD => 'InitialPriority',
1232 VALUE => $args{'VALUE'},
1233 OPERATOR => $args{'OPERATOR'},
1234 DESCRIPTION => join(
1235 ' ', $self->loc('Initial Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1242 # {{{ sub LimitFinalPriority
1244 =head2 LimitFinalPriority
1246 Takes a paramhash with the fields OPERATOR and VALUE.
1247 OPERATOR is one of =, >, < or !=.
1248 VALUE is a value to match the ticket\'s final priority against
1252 sub LimitFinalPriority {
1255 $self->Limit (FIELD => 'FinalPriority',
1256 VALUE => $args{'VALUE'},
1257 OPERATOR => $args{'OPERATOR'},
1258 DESCRIPTION => join(
1259 ' ', $self->loc('Final Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1266 # {{{ sub LimitTimeWorked
1268 =head2 LimitTimeWorked
1270 Takes a paramhash with the fields OPERATOR and VALUE.
1271 OPERATOR is one of =, >, < or !=.
1272 VALUE is a value to match the ticket's TimeWorked attribute
1276 sub LimitTimeWorked {
1279 $self->Limit (FIELD => 'TimeWorked',
1280 VALUE => $args{'VALUE'},
1281 OPERATOR => $args{'OPERATOR'},
1282 DESCRIPTION => join(
1283 ' ', $self->loc('Time worked'), $args{'OPERATOR'}, $args{'VALUE'},
1290 # {{{ sub LimitTimeLeft
1292 =head2 LimitTimeLeft
1294 Takes a paramhash with the fields OPERATOR and VALUE.
1295 OPERATOR is one of =, >, < or !=.
1296 VALUE is a value to match the ticket's TimeLeft attribute
1303 $self->Limit (FIELD => 'TimeLeft',
1304 VALUE => $args{'VALUE'},
1305 OPERATOR => $args{'OPERATOR'},
1306 DESCRIPTION => join(
1307 ' ', $self->loc('Time left'), $args{'OPERATOR'}, $args{'VALUE'},
1316 # {{{ Limiting based on attachment attributes
1318 # {{{ sub LimitContent
1322 Takes a paramhash with the fields OPERATOR and VALUE.
1323 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1324 VALUE is a string to search for in the body of the ticket
1330 $self->Limit (FIELD => 'Content',
1331 VALUE => $args{'VALUE'},
1332 OPERATOR => $args{'OPERATOR'},
1333 DESCRIPTION => join(
1334 ' ', $self->loc('Ticket content'), $args{'OPERATOR'}, $args{'VALUE'},
1341 # {{{ sub LimitFilename
1343 =head2 LimitFilename
1345 Takes a paramhash with the fields OPERATOR and VALUE.
1346 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1347 VALUE is a string to search for in the body of the ticket
1353 $self->Limit (FIELD => 'Filename',
1354 VALUE => $args{'VALUE'},
1355 OPERATOR => $args{'OPERATOR'},
1356 DESCRIPTION => join(
1357 ' ', $self->loc('Attachment filename'), $args{'OPERATOR'}, $args{'VALUE'},
1363 # {{{ sub LimitContentType
1365 =head2 LimitContentType
1367 Takes a paramhash with the fields OPERATOR and VALUE.
1368 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1369 VALUE is a content type to search ticket attachments for
1373 sub LimitContentType {
1376 $self->Limit (FIELD => 'ContentType',
1377 VALUE => $args{'VALUE'},
1378 OPERATOR => $args{'OPERATOR'},
1379 DESCRIPTION => join(
1380 ' ', $self->loc('Ticket content type'), $args{'OPERATOR'}, $args{'VALUE'},
1388 # {{{ Limiting based on people
1390 # {{{ sub LimitOwner
1394 Takes a paramhash with the fields OPERATOR and VALUE.
1395 OPERATOR is one of = or !=.
1402 my %args = ( OPERATOR => '=',
1405 my $owner = new RT::User($self->CurrentUser);
1406 $owner->Load($args{'VALUE'});
1407 # FIXME: check for a valid $owner
1408 $self->Limit (FIELD => 'Owner',
1409 VALUE => $args{'VALUE'},
1410 OPERATOR => $args{'OPERATOR'},
1411 DESCRIPTION => join(
1412 ' ', $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(),
1420 # {{{ Limiting watchers
1422 # {{{ sub LimitWatcher
1427 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1428 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1429 VALUE is a value to match the ticket\'s watcher email addresses against
1430 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1434 my $t1 = RT::Ticket->new($RT::SystemUser);
1435 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
1443 my %args = ( OPERATOR => '=',
1449 #build us up a description
1450 my ($watcher_type, $desc);
1451 if ($args{'TYPE'}) {
1452 $watcher_type = $args{'TYPE'};
1455 $watcher_type = "Watcher";
1458 $self->Limit (FIELD => $watcher_type,
1459 VALUE => $args{'VALUE'},
1460 OPERATOR => $args{'OPERATOR'},
1461 TYPE => $args{'TYPE'},
1462 DESCRIPTION => join(
1463 ' ', $self->loc($watcher_type), $args{'OPERATOR'}, $args{'VALUE'},
1469 sub LimitRequestor {
1472 my ($package, $filename, $line) = caller;
1473 $RT::Logger->error("Tickets->LimitRequestor is deprecated. please rewrite call at $package - $filename: $line");
1474 $self->LimitWatcher(TYPE => 'Requestor', @_);
1485 # {{{ Limiting based on links
1489 =head2 LimitLinkedTo
1491 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1492 TYPE limits the sort of link we want to search on
1494 TYPE = { RefersTo, MemberOf, DependsOn }
1496 TARGET is the id or URI of the TARGET of the link
1497 (TARGET used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as TARGET
1510 FIELD => 'LinkedTo',
1512 TARGET => ($args{'TARGET'} || $args{'TICKET'}),
1513 TYPE => $args{'TYPE'},
1514 DESCRIPTION => $self->loc(
1515 "Tickets [_1] by [_2]", $self->loc($args{'TYPE'}), ($args{'TARGET'} || $args{'TICKET'})
1523 # {{{ LimitLinkedFrom
1525 =head2 LimitLinkedFrom
1527 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1528 TYPE limits the sort of link we want to search on
1531 BASE is the id or URI of the BASE of the link
1532 (BASE used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as BASE
1537 sub LimitLinkedFrom {
1539 my %args = ( BASE => undef,
1544 # translate RT2 From/To naming to RT3 TicketSQL naming
1545 my %fromToMap = qw(DependsOn DependentOn
1547 RefersTo ReferredToBy);
1549 my $type = $args{'TYPE'};
1550 $type = $fromToMap{$type} if exists($fromToMap{$type});
1552 $self->Limit( FIELD => 'LinkedTo',
1554 BASE => ($args{'BASE'} || $args{'TICKET'}),
1556 DESCRIPTION => $self->loc(
1557 "Tickets [_1] [_2]", $self->loc($args{'TYPE'}), ($args{'BASE'} || $args{'TICKET'})
1568 my $ticket_id = shift;
1569 $self->LimitLinkedTo ( TARGET=> "$ticket_id",
1576 # {{{ LimitHasMember
1577 sub LimitHasMember {
1579 my $ticket_id =shift;
1580 $self->LimitLinkedFrom ( BASE => "$ticket_id",
1581 TYPE => 'HasMember',
1587 # {{{ LimitDependsOn
1589 sub LimitDependsOn {
1591 my $ticket_id = shift;
1592 $self->LimitLinkedTo ( TARGET => "$ticket_id",
1593 TYPE => 'DependsOn',
1600 # {{{ LimitDependedOnBy
1602 sub LimitDependedOnBy {
1604 my $ticket_id = shift;
1605 $self->LimitLinkedFrom ( BASE => "$ticket_id",
1606 TYPE => 'DependentOn',
1618 my $ticket_id = shift;
1619 $self->LimitLinkedTo ( TARGET => "$ticket_id",
1627 # {{{ LimitReferredToBy
1629 sub LimitReferredToBy {
1631 my $ticket_id = shift;
1632 $self->LimitLinkedFrom ( BASE=> "$ticket_id",
1633 TYPE => 'ReferredTo',
1642 # {{{ limit based on ticket date attribtes
1646 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
1648 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
1650 OPERATOR is one of > or <
1651 VALUE is a date and time in ISO format in GMT
1652 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
1654 There are also helper functions of the form LimitFIELD that eliminate
1655 the need to pass in a FIELD argument.
1668 #Set the description if we didn't get handed it above
1669 unless ($args{'DESCRIPTION'} ) {
1670 $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
1673 $self->Limit (%args);
1684 $self->LimitDate( FIELD => 'Created', @_);
1688 $self->LimitDate( FIELD => 'Due', @_);
1693 $self->LimitDate( FIELD => 'Starts', @_);
1698 $self->LimitDate( FIELD => 'Started', @_);
1702 $self->LimitDate( FIELD => 'Resolved', @_);
1706 $self->LimitDate( FIELD => 'Told', @_);
1708 sub LimitLastUpdated {
1710 $self->LimitDate( FIELD => 'LastUpdated', @_);
1713 # {{{ sub LimitTransactionDate
1715 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
1717 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
1719 OPERATOR is one of > or <
1720 VALUE is a date and time in ISO format in GMT
1725 sub LimitTransactionDate {
1728 FIELD => 'TransactionDate',
1734 # <20021217042756.GK28744@pallas.fsck.com>
1735 # "Kill It" - Jesse.
1737 #Set the description if we didn't get handed it above
1738 unless ($args{'DESCRIPTION'} ) {
1739 $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
1742 $self->Limit (%args);
1750 # {{{ Limit based on custom fields
1751 # {{{ sub LimitCustomField
1753 =head2 LimitCustomField
1755 Takes a paramhash of key/value pairs with the following keys:
1759 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional
1760 parameter QUEUE may also be passed to distinguish the custom field.
1762 =item OPERATOR - The usual Limit operators
1764 =item VALUE - The value to compare against
1770 sub LimitCustomField {
1772 my %args = ( VALUE => undef,
1773 CUSTOMFIELD => undef,
1775 DESCRIPTION => undef,
1776 FIELD => 'CustomFieldValue',
1780 my $CF = RT::CustomField->new( $self->CurrentUser );
1781 if ( $args{CUSTOMFIELD} =~ /^\d+$/) {
1782 $CF->Load( $args{CUSTOMFIELD} );
1785 $CF->LoadByNameAndQueue( Name => $args{CUSTOMFIELD}, Queue => $args{QUEUE} );
1786 $args{CUSTOMFIELD} = $CF->Id;
1789 #If we are looking to compare with a null value.
1790 if ( $args{'OPERATOR'} =~ /^is$/i ) {
1791 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has no value.", $CF->Name);
1793 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
1794 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has a value.", $CF->Name);
1797 # if we're not looking to compare with a null value
1799 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] [_2] [_3]", $CF->Name , $args{OPERATOR} , $args{VALUE});
1804 my $qo = new RT::Queue( $self->CurrentUser );
1805 $qo->load( $CF->Queue );
1810 @rest = ( ENTRYAGGREGATOR => 'AND' )
1811 if ($CF->Type eq 'SelectMultiple');
1813 $self->Limit( VALUE => $args{VALUE},
1815 ? $q . ".{" . $CF->Name . "}"
1818 OPERATOR => $args{OPERATOR},
1824 $self->{'RecalcTicketLimits'} = 1;
1831 # {{{ sub _NextIndex
1835 Keep track of the counter for the array of restrictions
1841 return ($self->{'restriction_index'}++);
1847 # {{{ Core bits to make this a DBIx::SearchBuilder object
1852 $self->{'table'} = "Tickets";
1853 $self->{'RecalcTicketLimits'} = 1;
1854 $self->{'looking_at_effective_id'} = 0;
1855 $self->{'looking_at_type'} = 0;
1856 $self->{'restriction_index'} =1;
1857 $self->{'primary_key'} = "id";
1858 delete $self->{'items_array'};
1859 delete $self->{'item_map'};
1860 delete $self->{'columns_to_display'};
1861 $self->SUPER::_Init(@_);
1871 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1872 return($self->SUPER::Count());
1879 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1880 return($self->SUPER::CountAll());
1885 # {{{ sub ItemsArrayRef
1887 =head2 ItemsArrayRef
1889 Returns a reference to the set of all items found in this search
1897 unless ( $self->{'items_array'} ) {
1899 my $placeholder = $self->_ItemsCounter;
1900 $self->GotoFirstItem();
1901 while ( my $item = $self->Next ) {
1902 push ( @{ $self->{'items_array'} }, $item );
1904 $self->GotoItem($placeholder);
1905 $self->{'items_array'} = $self->ItemsOrderBy($self->{'items_array'});
1907 return ( $self->{'items_array'} );
1915 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1917 my $Ticket = $self->SUPER::Next();
1918 if ((defined($Ticket)) and (ref($Ticket))) {
1920 #Make sure we _never_ show deleted tickets
1921 #TODO we should be doing this in the where clause.
1922 #but you can't do multiple clauses on the same field just yet :/
1924 if ($Ticket->__Value('Status') eq 'deleted') {
1925 return($self->Next());
1927 # Since Ticket could be granted with more rights instead
1928 # of being revoked, it's ok if queue rights allow
1929 # ShowTicket. It seems need another query, but we have
1930 # rights cache in Principal::HasRight.
1931 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket') ||
1932 $Ticket->CurrentUserHasRight('ShowTicket')) {
1936 #If the user doesn't have the right to show this ticket
1938 return($self->Next());
1941 #if there never was any ticket
1951 # {{{ Deal with storing and restoring restrictions
1953 # {{{ sub LoadRestrictions
1955 =head2 LoadRestrictions
1957 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
1958 TODO It is not yet implemented
1964 # {{{ sub DescribeRestrictions
1966 =head2 DescribeRestrictions
1969 Returns a hash keyed by restriction id.
1970 Each element of the hash is currently a one element hash that contains DESCRIPTION which
1971 is a description of the purpose of that TicketRestriction
1975 sub DescribeRestrictions {
1978 my ($row, %listing);
1980 foreach $row (keys %{$self->{'TicketRestrictions'}}) {
1981 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
1987 # {{{ sub RestrictionValues
1989 =head2 RestrictionValues FIELD
1991 Takes a restriction field and returns a list of values this field is restricted
1996 sub RestrictionValues {
1999 map $self->{'TicketRestrictions'}{$_}{'VALUE'},
2001 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
2002 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
2004 keys %{$self->{'TicketRestrictions'}};
2009 # {{{ sub ClearRestrictions
2011 =head2 ClearRestrictions
2013 Removes all restrictions irretrievably
2017 sub ClearRestrictions {
2019 delete $self->{'TicketRestrictions'};
2020 $self->{'looking_at_effective_id'} = 0;
2021 $self->{'looking_at_type'} = 0;
2022 $self->{'RecalcTicketLimits'} =1;
2027 # {{{ sub DeleteRestriction
2029 =head2 DeleteRestriction
2031 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
2032 Removes that restriction from the session's limits.
2037 sub DeleteRestriction {
2040 delete $self->{'TicketRestrictions'}{$row};
2042 $self->{'RecalcTicketLimits'} = 1;
2043 #make the underlying easysearch object forget all its preconceptions
2048 # {{{ sub _RestrictionsToClauses
2050 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
2052 sub _RestrictionsToClauses {
2057 foreach $row (keys %{$self->{'TicketRestrictions'}}) {
2058 my $restriction = $self->{'TicketRestrictions'}{$row};
2060 #print Dumper($restriction),"\n";
2062 # We need to reimplement the subclause aggregation that SearchBuilder does.
2063 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
2064 # Then SB AND's the different Subclauses together.
2066 # So, we want to group things into Subclauses, convert them to
2067 # SQL, and then join them with the appropriate DefaultEA.
2068 # Then join each subclause group with AND.
2070 my $field = $restriction->{'FIELD'};
2071 my $realfield = $field; # CustomFields fake up a fieldname, so
2072 # we need to figure that out
2075 # Rewrite LinkedTo meta field to the real field
2076 if ($field =~ /LinkedTo/) {
2077 $realfield = $field = $restriction->{'TYPE'};
2081 # CustomFields have a different real field
2082 if ($field =~ /^CF\./) {
2086 die "I don't know about $field yet"
2087 unless (exists $FIELDS{$realfield} or $restriction->{CUSTOMFIELD});
2089 my $type = $FIELDS{$realfield}->[0];
2090 my $op = $restriction->{'OPERATOR'};
2092 my $value = ( grep { defined }
2093 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET))[0];
2095 # this performs the moral equivalent of defined or/dor/C<//>,
2096 # without the short circuiting.You need to use a 'defined or'
2097 # type thing instead of just checking for truth values, because
2098 # VALUE could be 0.(i.e. "false")
2100 # You could also use this, but I find it less aesthetic:
2101 # (although it does short circuit)
2102 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2103 # defined $restriction->{'TICKET'} ?
2104 # $restriction->{TICKET} :
2105 # defined $restriction->{'BASE'} ?
2106 # $restriction->{BASE} :
2107 # defined $restriction->{'TARGET'} ?
2108 # $restriction->{TARGET} )
2110 my $ea = $restriction->{ENTRYAGGREGATOR} || $DefaultEA{$type} || "AND";
2112 die "Invalid operator $op for $field ($type)"
2113 unless exists $ea->{$op};
2117 # Each CustomField should be put into a different Clause so they
2118 # are ANDed together.
2119 if ($restriction->{CUSTOMFIELD}) {
2120 $realfield = $field;
2123 exists $clause{$realfield} or $clause{$realfield} = [];
2125 $field =~ s!(['"])!\\$1!g;
2126 $value =~ s!(['"])!\\$1!g;
2127 my $data = [ $ea, $type, $field, $op, $value ];
2129 # here is where we store extra data, say if it's a keyword or
2130 # something. (I.e. "TYPE SPECIFIC STUFF")
2132 #print Dumper($data);
2133 push @{$clause{$realfield}}, $data;
2140 # {{{ sub _ProcessRestrictions
2142 =head2 _ProcessRestrictions PARAMHASH
2144 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
2145 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
2149 sub _ProcessRestrictions {
2152 #Blow away ticket aliases since we'll need to regenerate them for
2154 delete $self->{'TicketAliases'};
2155 delete $self->{'items_array'};
2156 delete $self->{'item_map'};
2157 delete $self->{'raw_rows'};
2158 delete $self->{'rows'};
2159 delete $self->{'count_all'};
2161 my $sql = $self->{_sql_query}; # Violating the _SQL namespace
2162 if (!$sql||$self->{'RecalcTicketLimits'}) {
2163 # "Restrictions to Clauses Branch\n";
2164 my $clauseRef = eval { $self->_RestrictionsToClauses; };
2166 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2169 $sql = $self->ClausesToSQL($clauseRef);
2170 $self->FromSQL($sql);
2175 $self->{'RecalcTicketLimits'} = 0;
2179 =head2 _BuildItemMap
2181 # Build up a map of first/last/next/prev items, so that we can display search nav quickly
2188 my $items = $self->ItemsArrayRef;
2191 delete $self->{'item_map'};
2193 $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
2194 while (my $item = shift @$items ) {
2195 my $id = $item->EffectiveId;
2196 $self->{'item_map'}->{$id}->{'defined'} = 1;
2197 $self->{'item_map'}->{$id}->{prev} = $prev;
2198 $self->{'item_map'}->{$id}->{next} = $items->[0]->EffectiveId if ($items->[0]);
2201 $self->{'item_map'}->{'last'} = $prev;
2208 Returns an a map of all items found by this search. The map is of the form
2210 $ItemMap->{'first'} = first ticketid found
2211 $ItemMap->{'last'} = last ticketid found
2212 $ItemMap->{$id}->{prev} = the ticket id found before $id
2213 $ItemMap->{$id}->{next} = the ticket id found after $id
2219 $self->_BuildItemMap() unless ($self->{'items_array'} and $self->{'item_map'});
2220 return ($self->{'item_map'});
2236 =head2 PrepForSerialization
2238 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
2243 sub PrepForSerialization {
2245 delete $self->{'items'};
2246 $self->RedoSearch();