import rt 3.4.6
[freeside.git] / rt / lib / RT / Tickets_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
6 #                                          <jesse@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
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
16 # from www.gnu.org.
17 #
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.
22 #
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.
26 #
27 #
28 # CONTRIBUTION SUBMISSION POLICY:
29 #
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.)
35 #
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.
44 #
45 # END BPS TAGGED BLOCK }}}
46 # Major Changes:
47
48 # - Decimated ProcessRestrictions and broke it into multiple
49 # functions joined by a LUT
50 # - Semi-Generic SQL stuff moved to another file
51
52 # Known Issues: FIXME!
53
54 # - ClearRestrictions and Reinitialization is messy and unclear.  The
55 # only good way to do it is to create a new RT::Tickets object.
56
57 =head1 NAME
58
59   RT::Tickets - A collection of Ticket objects
60
61
62 =head1 SYNOPSIS
63
64   use RT::Tickets;
65   my $tickets = new RT::Tickets($CurrentUser);
66
67 =head1 DESCRIPTION
68
69    A collection of RT::Tickets.
70
71 =head1 METHODS
72
73 =begin testing
74
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 );
80
81 =end testing
82
83 =cut
84
85 package RT::Tickets;
86
87 use strict;
88
89 package RT::Tickets;
90
91 no warnings qw(redefine);
92 use vars qw(@SORTFIELDS);
93 use RT::CustomFields;
94
95 # Configuration Tables:
96
97 # FIELDS is a mapping of searchable Field name, to Type, and other
98 # metadata.
99
100 my %FIELDS = (
101     Status          => ['ENUM'],
102     Queue           => [ 'ENUM' => 'Queue', ],
103     Type            => [ 'ENUM', ],
104     Creator         => [ 'ENUM' => 'User', ],
105     LastUpdatedBy   => [ 'ENUM' => 'User', ],
106     Owner           => [ 'WATCHERFIELD' => 'Owner', ],
107     EffectiveId     => [ 'INT', ],
108     id              => [ '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', ],
146 );
147
148 # Mapping of Field Type to Function
149 my %dispatch = (
150     ENUM            => \&_EnumLimit,
151     INT             => \&_IntLimit,
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,
161 );
162 my %can_bundle = ( WATCHERFIELD => "yes", );
163
164 # Default EntryAggregator per type
165 # if you specify OP, you must specify all valid OPs
166 my %DefaultEA = (
167     INT  => 'AND',
168     ENUM => {
169         '='  => 'OR',
170         '!=' => 'AND'
171     },
172     DATE => {
173         '='  => 'OR',
174         '>=' => 'AND',
175         '<=' => 'AND',
176         '>'  => 'AND',
177         '<'  => 'AND'
178     },
179     STRING => {
180         '='        => 'OR',
181         '!='       => 'AND',
182         'LIKE'     => 'AND',
183         'NOT LIKE' => 'AND'
184     },
185     TRANSFIELD   => 'AND',
186     TRANSDATE    => 'AND',
187     LINK         => 'OR',
188     LINKFIELD    => 'AND',
189     TARGET       => 'AND',
190     BASE         => 'AND',
191     WATCHERFIELD => {
192         '='        => 'OR',
193         '!='       => 'AND',
194         'LIKE'     => 'OR',
195         'NOT LIKE' => 'AND'
196     },
197
198     CUSTOMFIELD => 'OR',
199 );
200
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 }
206
207 # Bring in the clowns.
208 require RT::Tickets_Overlay_SQL;
209
210 # {{{ sub SortFields
211
212 @SORTFIELDS = qw(id Status
213     Queue Subject
214     Owner Created Due Starts Started
215     Told
216     Resolved LastUpdated Priority TimeWorked TimeLeft);
217
218 =head2 SortFields
219
220 Returns the list of fields that lists of tickets can easily be sorted by
221
222 =cut
223
224 sub SortFields {
225     my $self = shift;
226     return (@SORTFIELDS);
227 }
228
229 # }}}
230
231 # BEGIN SQL STUFF *********************************
232
233 =head1 Limit Helper Routines
234
235 These routines are the targets of a dispatch table depending on the
236 type of field.  They all share the same signature:
237
238   my ($self,$field,$op,$value,@rest) = @_;
239
240 The values in @rest should be suitable for passing directly to
241 DBIx::SearchBuilder::Limit.
242
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.
246
247 =head2 _EnumLimit
248
249 Handle Fields which are limited to certain values, and potentially
250 need to be looked up from another class.
251
252 This subroutine actually handles two different kinds of fields.  For
253 some the user is responsible for limiting the values.  (i.e. Status,
254 Type).
255
256 For others, the value specified by the user will be looked by via
257 specified class.
258
259 Meta Data:
260   name of class to lookup in (Optional)
261
262 =cut
263
264 sub _EnumLimit {
265     my ( $sb, $field, $op, $value, @rest ) = @_;
266
267     # SQL::Statement changes != to <>.  (Can we remove this now?)
268     $op = "!=" if $op eq "<>";
269
270     die "Invalid Operation: $op for $field"
271         unless $op eq "="
272         or $op     eq "!=";
273
274     my $meta = $FIELDS{$field};
275     if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
276         my $class = "RT::" . $meta->[1];
277         my $o     = $class->new( $sb->CurrentUser );
278         $o->Load($value);
279         $value = $o->Id;
280     }
281     $sb->_SQLLimit(
282         FIELD    => $field,
283         VALUE    => $value,
284         OPERATOR => $op,
285         @rest,
286     );
287 }
288
289 =head2 _IntLimit
290
291 Handle fields where the values are limited to integers.  (For example,
292 Priority, TimeWorked.)
293
294 Meta Data:
295   None
296
297 =cut
298
299 sub _IntLimit {
300     my ( $sb, $field, $op, $value, @rest ) = @_;
301
302     die "Invalid Operator $op for $field"
303         unless $op =~ /^(=|!=|>|<|>=|<=)$/;
304
305     $sb->_SQLLimit(
306         FIELD    => $field,
307         VALUE    => $value,
308         OPERATOR => $op,
309         @rest,
310     );
311 }
312
313 =head2 _LinkLimit
314
315 Handle fields which deal with links between tickets.  (MemberOf, DependsOn)
316
317 Meta Data:
318   1: Direction (From,To)
319   2: Link Type (MemberOf, DependsOn,RefersTo)
320
321 =cut
322
323 sub _LinkLimit {
324     my ( $sb, $field, $op, $value, @rest ) = @_;
325
326     my $meta = $FIELDS{$field};
327     die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS)/io;
328
329     die "Incorrect Metadata for $field"
330         unless ( defined $meta->[1] and defined $meta->[2] );
331
332     my $direction = $meta->[1];
333
334     my $matchfield;
335     my $linkfield;
336     my $is_local = 1;
337     my $is_null  = 0;
338     if ( $direction eq 'To' ) {
339         $matchfield = "Target";
340         $linkfield  = "Base";
341
342     }
343     elsif ( $direction eq 'From' ) {
344         $linkfield  = "Target";
345         $matchfield = "Base";
346
347     }
348     else {
349         die "Invalid link direction '$meta->[1]' for $field\n";
350     }
351
352     if ( $op eq '=' || $op =~ /^is/oi ) {
353         if ( $value eq '' || $value =~ /^null$/io ) {
354             $is_null = 1;
355         }
356         elsif ( $value =~ /\D/o ) {
357             $is_local = 0;
358         }
359         else {
360             $is_local = 1;
361         }
362     }
363
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);
371
372     if ($is_null) {
373         my $linkalias = $sb->Join(
374             TYPE   => 'left',
375             ALIAS1 => 'main',
376             FIELD1 => 'id',
377             TABLE2 => 'Links',
378             FIELD2 => 'Local' . $linkfield
379         );
380
381         $sb->SUPER::Limit(
382             LEFTJOIN => $linkalias,
383             FIELD    => 'Type',
384             OPERATOR => '=',
385             VALUE    => $meta->[2],
386             @rest,
387         );
388
389         $sb->_SQLLimit(
390             ALIAS           => $linkalias,
391             ENTRYAGGREGATOR => 'AND',
392             FIELD      => ( $is_local ? "Local$matchfield" : $matchfield ),
393             OPERATOR   => 'IS',
394             VALUE      => 'NULL',
395             QUOTEVALUE => '0',
396         );
397
398     }
399     else {
400
401         $sb->{_sql_linkalias} = $sb->NewAlias('Links')
402             unless defined $sb->{_sql_linkalias};
403
404         $sb->_OpenParen();
405
406         $sb->_SQLLimit(
407             ALIAS    => $sb->{_sql_linkalias},
408             FIELD    => 'Type',
409             OPERATOR => '=',
410             VALUE    => $meta->[2],
411             @rest,
412         );
413
414         $sb->_SQLLimit(
415             ALIAS           => $sb->{_sql_linkalias},
416             ENTRYAGGREGATOR => 'AND',
417             FIELD    => ( $is_local ? "Local$matchfield" : $matchfield ),
418             OPERATOR => '=',
419             VALUE    => $value,
420         );
421
422         #If we're searching on target, join the base to ticket.id
423         $sb->_SQLJoin(
424             ALIAS1 => 'main',
425             FIELD1 => $sb->{'primary_key'},
426             ALIAS2 => $sb->{_sql_linkalias},
427             FIELD2 => 'Local' . $linkfield
428         );
429
430         $sb->_CloseParen();
431     }
432 }
433
434 =head2 _DateLimit
435
436 Handle date fields.  (Created, LastTold..)
437
438 Meta Data:
439   1: type of link.  (Probably not necessary.)
440
441 =cut
442
443 sub _DateLimit {
444     my ( $sb, $field, $op, $value, @rest ) = @_;
445
446     die "Invalid Date Op: $op"
447         unless $op =~ /^(=|>|<|>=|<=)$/;
448
449     my $meta = $FIELDS{$field};
450     die "Incorrect Meta Data for $field"
451         unless ( defined $meta->[1] );
452
453     my $date = RT::Date->new( $sb->CurrentUser );
454     $date->Set( Format => 'unknown', Value => $value );
455
456     if ( $op eq "=" ) {
457
458         # if we're specifying =, that means we want everything on a
459         # particular single day.  in the database, we need to check for >
460         # and < the edges of that day.
461
462         $date->SetToMidnight( Timezone => 'server' );
463         my $daystart = $date->ISO;
464         $date->AddDay;
465         my $dayend = $date->ISO;
466
467         $sb->_OpenParen;
468
469         $sb->_SQLLimit(
470             FIELD    => $meta->[1],
471             OPERATOR => ">=",
472             VALUE    => $daystart,
473             @rest,
474         );
475
476         $sb->_SQLLimit(
477             FIELD    => $meta->[1],
478             OPERATOR => "<=",
479             VALUE    => $dayend,
480             @rest,
481             ENTRYAGGREGATOR => 'AND',
482         );
483
484         $sb->_CloseParen;
485
486     }
487     else {
488         $sb->_SQLLimit(
489             FIELD    => $meta->[1],
490             OPERATOR => $op,
491             VALUE    => $date->ISO,
492             @rest,
493         );
494     }
495 }
496
497 =head2 _StringLimit
498
499 Handle simple fields which are just strings.  (Subject,Type)
500
501 Meta Data:
502   None
503
504 =cut
505
506 sub _StringLimit {
507     my ( $sb, $field, $op, $value, @rest ) = @_;
508
509     # FIXME:
510     # Valid Operators:
511     #  =, !=, LIKE, NOT LIKE
512
513     $sb->_SQLLimit(
514         FIELD         => $field,
515         OPERATOR      => $op,
516         VALUE         => $value,
517         CASESENSITIVE => 0,
518         @rest,
519     );
520 }
521
522 =head2 _TransDateLimit
523
524 Handle fields limiting based on Transaction Date.
525
526 The inpupt value must be in a format parseable by Time::ParseDate
527
528 Meta Data:
529   None
530
531 =cut
532
533 # This routine should really be factored into translimit.
534 sub _TransDateLimit {
535     my ( $sb, $field, $op, $value, @rest ) = @_;
536
537     # See the comments for TransLimit, they apply here too
538
539     $sb->{_sql_transalias} = $sb->NewAlias('Transactions')
540         unless defined $sb->{_sql_transalias};
541
542     my $date = RT::Date->new( $sb->CurrentUser );
543     $date->Set( Format => 'unknown', Value => $value );
544
545     $sb->_OpenParen;
546     if ( $op eq "=" ) {
547
548         # if we're specifying =, that means we want everything on a
549         # particular single day.  in the database, we need to check for >
550         # and < the edges of that day.
551
552         $date->SetToMidnight( Timezone => 'server' );
553         my $daystart = $date->ISO;
554         $date->AddDay;
555         my $dayend = $date->ISO;
556
557         $sb->_SQLLimit(
558             ALIAS         => $sb->{_sql_transalias},
559             FIELD         => 'Created',
560             OPERATOR      => ">=",
561             VALUE         => $daystart,
562             CASESENSITIVE => 0,
563             @rest
564         );
565         $sb->_SQLLimit(
566             ALIAS         => $sb->{_sql_transalias},
567             FIELD         => 'Created',
568             OPERATOR      => "<=",
569             VALUE         => $dayend,
570             CASESENSITIVE => 0,
571             @rest,
572             ENTRYAGGREGATOR => 'AND',
573         );
574
575     }
576
577     # not searching for a single day
578     else {
579
580         #Search for the right field
581         $sb->_SQLLimit(
582             ALIAS         => $sb->{_sql_transalias},
583             FIELD         => 'Created',
584             OPERATOR      => $op,
585             VALUE         => $date->ISO,
586             CASESENSITIVE => 0,
587             @rest
588         );
589     }
590
591     # Join Transactions to Tickets
592     $sb->_SQLJoin(
593         ALIAS1 => 'main',
594         FIELD1 => $sb->{'primary_key'},     # UGH!
595         ALIAS2 => $sb->{_sql_transalias},
596         FIELD2 => 'ObjectId'
597     );
598
599     $sb->SUPER::Limit(
600         ALIAS => $sb->{_sql_transalias},
601         FIELD => 'ObjectType',
602         VALUE => 'RT::Ticket'
603     );
604
605     $sb->_CloseParen;
606 }
607
608 =head2 _TransLimit
609
610 Limit based on the Content of a transaction or the ContentType.
611
612 Meta Data:
613   none
614
615 =cut
616
617 sub _TransLimit {
618
619     # Content, ContentType, Filename
620
621     # If only this was this simple.  We've got to do something
622     # complicated here:
623
624     #Basically, we want to make sure that the limits apply to
625     #the same attachment, rather than just another attachment
626     #for the same ticket, no matter how many clauses we lump
627     #on. We put them in TicketAliases so that they get nuked
628     #when we redo the join.
629
630     # In the SQL, we might have
631     #       (( Content = foo ) or ( Content = bar AND Content = baz ))
632     # The AND group should share the same Alias.
633
634     # Actually, maybe it doesn't matter.  We use the same alias and it
635     # works itself out? (er.. different.)
636
637     # Steal more from _ProcessRestrictions
638
639     # FIXME: Maybe look at the previous FooLimit call, and if it was a
640     # TransLimit and EntryAggregator == AND, reuse the Aliases?
641
642     # Or better - store the aliases on a per subclause basis - since
643     # those are going to be the things we want to relate to each other,
644     # anyway.
645
646     # maybe we should not allow certain kinds of aggregation of these
647     # clauses and do a psuedo regex instead? - the problem is getting
648     # them all into the same subclause when you have (A op B op C) - the
649     # way they get parsed in the tree they're in different subclauses.
650
651     my ( $self, $field, $op, $value, @rest ) = @_;
652
653     $self->{_sql_transalias} = $self->NewAlias('Transactions')
654         unless defined $self->{_sql_transalias};
655     $self->{_sql_trattachalias} = $self->NewAlias('Attachments')
656         unless defined $self->{_sql_trattachalias};
657
658     $self->_OpenParen;
659
660     #Search for the right field
661     $self->_SQLLimit(
662         ALIAS         => $self->{_sql_trattachalias},
663         FIELD         => $field,
664         OPERATOR      => $op,
665         VALUE         => $value,
666         CASESENSITIVE => 0,
667         @rest
668     );
669
670     $self->_SQLJoin(
671         ALIAS1 => $self->{_sql_trattachalias},
672         FIELD1 => 'TransactionId',
673         ALIAS2 => $self->{_sql_transalias},
674         FIELD2 => 'id'
675     );
676
677     # Join Transactions to Tickets
678     $self->_SQLJoin(
679         ALIAS1 => 'main',
680         FIELD1 => $self->{'primary_key'},     # Why not use "id" here?
681         ALIAS2 => $self->{_sql_transalias},
682         FIELD2 => 'ObjectId'
683     );
684
685     $self->SUPER::Limit(
686         ALIAS           => $self->{_sql_transalias},
687         FIELD           => 'ObjectType',
688         VALUE           => 'RT::Ticket',
689         ENTRYAGGREGATOR => 'AND'
690     );
691
692     $self->_CloseParen;
693
694 }
695
696 =head2 _WatcherLimit
697
698 Handle watcher limits.  (Requestor, CC, etc..)
699
700 Meta Data:
701   1: Field to query on
702
703
704 =begin testing
705
706 # Test to make sure that you can search for tickets by requestor address and
707 # by requestor name.
708
709 my ($id,$msg);
710 my $u1 = RT::User->new($RT::SystemUser);
711 ($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
712 ok ($id,$msg);
713 my $u2 = RT::User->new($RT::SystemUser);
714 ($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
715 ok ($id,$msg);
716
717 my $t1 = RT::Ticket->new($RT::SystemUser);
718 my ($trans);
719 ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
720 ok ($id, $msg);
721
722 my $t2 = RT::Ticket->new($RT::SystemUser);
723 ($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
724 ok ($id, $msg);
725
726
727 my $t3 = RT::Ticket->new($RT::SystemUser);
728 ($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
729 ok ($id, $msg);
730
731
732 my $tix1 = RT::Tickets->new($RT::SystemUser);
733 $tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
734
735 is ($tix1->Count, 3);
736
737 my $tix2 = RT::Tickets->new($RT::SystemUser);
738 $tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
739
740 is ($tix2->Count, 3);
741
742
743 my $tix3 = RT::Tickets->new($RT::SystemUser);
744 $tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
745
746 is ($tix3->Count, 2);
747
748 my $tix4 = RT::Tickets->new($RT::SystemUser);
749 $tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
750
751 is ($tix4->Count, 2);
752
753 # Searching for tickets that have two requestors isn't supported
754 # There's no way to differentiate "one requestor name that matches foo and bar"
755 # and "two requestors, one matching foo and one matching bar"
756
757 # my $tix5 = RT::Tickets->new($RT::SystemUser);
758 # $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
759
760 # is ($tix5->Count, 1);
761
762 # my $tix6 = RT::Tickets->new($RT::SystemUser);
763 # $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
764
765 # is ($tix6->Count, 1);
766
767
768 =end testing
769
770 =cut
771
772 sub _WatcherLimit {
773     my $self  = shift;
774     my $field = shift;
775     my $op    = shift;
776     my $value = shift;
777     my %rest  = (@_);
778
779     # Find out what sort of watcher we're looking for
780     my $fieldname;
781     if ( ref $field ) {
782         $fieldname = $field->[0]->[0];
783     }
784     else {
785         $fieldname = $field;
786         $field = [ [ $field, $op, $value, %rest ] ];    # gross hack
787     }
788     my $meta = $FIELDS{$fieldname};
789     my $type = ( defined $meta->[1] ? $meta->[1] : undef );
790
791     # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
792     # search by id and Name at the same time, this is workaround
793     # to preserve backward compatibility
794     if ( $fieldname eq 'Owner' ) {
795         my $flag = 0;
796         for my $chunk ( splice @$field ) {
797             my ( $f, $op, $value, %rest ) = @$chunk;
798             if ( !$rest{SUBKEY} && $op =~ /^!?=$/ ) {
799                 $self->_OpenParen unless $flag++;
800                 my $o = RT::User->new( $self->CurrentUser );
801                 $o->Load($value);
802                 $value = $o->Id;
803                 $self->_SQLLimit(
804                     FIELD    => 'Owner',
805                     OPERATOR => $op,
806                     VALUE    => $value,
807                     %rest,
808                 );
809             }
810             else {
811                 push @$field, $chunk;
812             }
813         }
814         $self->_CloseParen if $flag;
815         return unless @$field;
816     }
817
818     my $users = $self->_WatcherJoin($type);
819
820     # If we're looking for multiple watchers of a given type,
821     # TicketSQL will be handing it to us as an array of clauses in
822     # $field
823     $self->_OpenParen;
824     for my $chunk (@$field) {
825         ( $field, $op, $value, %rest ) = @$chunk;
826         $rest{SUBKEY} ||= 'EmailAddress';
827
828         my $re_negative_op = qr[!=|NOT LIKE];
829         $self->_OpenParen if $op =~ /$re_negative_op/;
830
831         $self->_SQLLimit(
832             ALIAS         => $users,
833             FIELD         => $rest{SUBKEY},
834             VALUE         => $value,
835             OPERATOR      => $op,
836             CASESENSITIVE => 0,
837             %rest
838         );
839
840         if ( $op =~ /$re_negative_op/ ) {
841             $self->_SQLLimit(
842                 ALIAS           => $users,
843                 FIELD           => $rest{SUBKEY},
844                 OPERATOR        => 'IS',
845                 VALUE           => 'NULL',
846                 ENTRYAGGREGATOR => 'OR',
847             );
848             $self->_CloseParen;
849         }
850     }
851     $self->_CloseParen;
852 }
853
854 =head2 _WatcherJoin
855
856 Helper function which provides joins to a watchers table both for limits
857 and for ordering.
858
859 =cut
860
861 sub _WatcherJoin {
862     my $self = shift;
863     my $type = shift;
864
865     # we cache joins chain per watcher type
866     # if we limit by requestor then we shouldn't join requestors again
867     # for sort or limit on other requestors
868     if ( $self->{'_watcher_join_users_alias'}{ $type || 'any' } ) {
869         return $self->{'_watcher_join_users_alias'}{ $type || 'any' };
870     }
871
872 # we always have watcher groups for ticket
873 # this join should be NORMAL
874 # XXX: if we change this from Join to NewAlias+Limit
875 # then Pg will complain because SB build wrong query.
876 # Query looks like "FROM (Tickets LEFT JOIN CGM ON(Groups.id = CGM.GroupId)), Groups"
877 # Pg doesn't like that fact that it doesn't know about Groups table yet when
878 # join CGM table into Tickets. Problem is in Join method which doesn't use
879 # ALIAS1 argument when build braces.
880     my $groups = $self->Join(
881         ALIAS1          => 'main',
882         FIELD1          => 'id',
883         TABLE2          => 'Groups',
884         FIELD2          => 'Instance',
885         ENTRYAGGREGATOR => 'AND'
886     );
887     $self->SUPER::Limit(
888         ALIAS           => $groups,
889         FIELD           => 'Domain',
890         VALUE           => 'RT::Ticket-Role',
891         ENTRYAGGREGATOR => 'AND'
892     );
893     $self->SUPER::Limit(
894         ALIAS           => $groups,
895         FIELD           => 'Type',
896         VALUE           => $type,
897         ENTRYAGGREGATOR => 'AND'
898         )
899         if ($type);
900
901     my $groupmembers = $self->Join(
902         TYPE   => 'LEFT',
903         ALIAS1 => $groups,
904         FIELD1 => 'id',
905         TABLE2 => 'CachedGroupMembers',
906         FIELD2 => 'GroupId'
907     );
908
909     # XXX: work around, we must hide groups that
910     # are members of the role group we search in,
911     # otherwise them result in wrong NULLs in Users
912     # table and break ordering. Now, we know that
913     # RT doesn't allow to add groups as members of the
914     # ticket roles, so we just hide entries in CGM table
915     # with MemberId == GroupId from results
916     $self->SUPER::Limit(
917         LEFTJOIN   => $groupmembers,
918         FIELD      => 'GroupId',
919         OPERATOR   => '!=',
920         VALUE      => "$groupmembers.MemberId",
921         QUOTEVALUE => 0,
922     );
923     my $users = $self->Join(
924         TYPE   => 'LEFT',
925         ALIAS1 => $groupmembers,
926         FIELD1 => 'MemberId',
927         TABLE2 => 'Users',
928         FIELD2 => 'id'
929     );
930     return $self->{'_watcher_join_users_alias'}{ $type || 'any' } = $users;
931 }
932
933 =head2 _WatcherMembershipLimit
934
935 Handle watcher membership limits, i.e. whether the watcher belongs to a
936 specific group or not.
937
938 Meta Data:
939   1: Field to query on
940
941 SELECT DISTINCT main.*
942 FROM
943     Tickets main,
944     Groups Groups_1,
945     CachedGroupMembers CachedGroupMembers_2,
946     Users Users_3
947 WHERE (
948     (main.EffectiveId = main.id)
949 ) AND (
950     (main.Status != 'deleted')
951 ) AND (
952     (main.Type = 'ticket')
953 ) AND (
954     (
955         (Users_3.EmailAddress = '22')
956             AND
957         (Groups_1.Domain = 'RT::Ticket-Role')
958             AND
959         (Groups_1.Type = 'RequestorGroup')
960     )
961 ) AND
962     Groups_1.Instance = main.id
963 AND
964     Groups_1.id = CachedGroupMembers_2.GroupId
965 AND
966     CachedGroupMembers_2.MemberId = Users_3.id
967 ORDER BY main.id ASC
968 LIMIT 25
969
970 =cut
971
972 sub _WatcherMembershipLimit {
973     my ( $self, $field, $op, $value, @rest ) = @_;
974     my %rest = @rest;
975
976     $self->_OpenParen;
977
978     my $groups       = $self->NewAlias('Groups');
979     my $groupmembers = $self->NewAlias('CachedGroupMembers');
980     my $users        = $self->NewAlias('Users');
981     my $memberships  = $self->NewAlias('CachedGroupMembers');
982
983     if ( ref $field ) {    # gross hack
984         my @bundle = @$field;
985         $self->_OpenParen;
986         for my $chunk (@bundle) {
987             ( $field, $op, $value, @rest ) = @$chunk;
988             $self->_SQLLimit(
989                 ALIAS    => $memberships,
990                 FIELD    => 'GroupId',
991                 VALUE    => $value,
992                 OPERATOR => $op,
993                 @rest,
994             );
995         }
996         $self->_CloseParen;
997     }
998     else {
999         $self->_SQLLimit(
1000             ALIAS    => $memberships,
1001             FIELD    => 'GroupId',
1002             VALUE    => $value,
1003             OPERATOR => $op,
1004             @rest,
1005         );
1006     }
1007
1008     # {{{ Tie to groups for tickets we care about
1009     $self->_SQLLimit(
1010         ALIAS           => $groups,
1011         FIELD           => 'Domain',
1012         VALUE           => 'RT::Ticket-Role',
1013         ENTRYAGGREGATOR => 'AND'
1014     );
1015
1016     $self->Join(
1017         ALIAS1 => $groups,
1018         FIELD1 => 'Instance',
1019         ALIAS2 => 'main',
1020         FIELD2 => 'id'
1021     );
1022
1023     # }}}
1024
1025     # If we care about which sort of watcher
1026     my $meta = $FIELDS{$field};
1027     my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1028
1029     if ($type) {
1030         $self->_SQLLimit(
1031             ALIAS           => $groups,
1032             FIELD           => 'Type',
1033             VALUE           => $type,
1034             ENTRYAGGREGATOR => 'AND'
1035         );
1036     }
1037
1038     $self->Join(
1039         ALIAS1 => $groups,
1040         FIELD1 => 'id',
1041         ALIAS2 => $groupmembers,
1042         FIELD2 => 'GroupId'
1043     );
1044
1045     $self->Join(
1046         ALIAS1 => $groupmembers,
1047         FIELD1 => 'MemberId',
1048         ALIAS2 => $users,
1049         FIELD2 => 'id'
1050     );
1051
1052     $self->Join(
1053         ALIAS1 => $memberships,
1054         FIELD1 => 'MemberId',
1055         ALIAS2 => $users,
1056         FIELD2 => 'id'
1057     );
1058
1059     $self->_CloseParen;
1060
1061 }
1062
1063 sub _LinkFieldLimit {
1064     my $restriction;
1065     my $self;
1066     my $LinkAlias;
1067     my %args;
1068     if ( $restriction->{'TYPE'} ) {
1069         $self->SUPER::Limit(
1070             ALIAS           => $LinkAlias,
1071             ENTRYAGGREGATOR => 'AND',
1072             FIELD           => 'Type',
1073             OPERATOR        => '=',
1074             VALUE           => $restriction->{'TYPE'}
1075         );
1076     }
1077
1078     #If we're trying to limit it to things that are target of
1079     if ( $restriction->{'TARGET'} ) {
1080
1081         # If the TARGET is an integer that means that we want to look at
1082         # the LocalTarget field. otherwise, we want to look at the
1083         # "Target" field
1084         my ($matchfield);
1085         if ( $restriction->{'TARGET'} =~ /^(\d+)$/ ) {
1086             $matchfield = "LocalTarget";
1087         }
1088         else {
1089             $matchfield = "Target";
1090         }
1091         $self->SUPER::Limit(
1092             ALIAS           => $LinkAlias,
1093             ENTRYAGGREGATOR => 'AND',
1094             FIELD           => $matchfield,
1095             OPERATOR        => '=',
1096             VALUE           => $restriction->{'TARGET'}
1097         );
1098
1099         #If we're searching on target, join the base to ticket.id
1100         $self->_SQLJoin(
1101             ALIAS1 => 'main',
1102             FIELD1 => $self->{'primary_key'},
1103             ALIAS2 => $LinkAlias,
1104             FIELD2 => 'LocalBase'
1105         );
1106     }
1107
1108     #If we're trying to limit it to things that are base of
1109     elsif ( $restriction->{'BASE'} ) {
1110
1111         # If we're trying to match a numeric link, we want to look at
1112         # LocalBase, otherwise we want to look at "Base"
1113         my ($matchfield);
1114         if ( $restriction->{'BASE'} =~ /^(\d+)$/ ) {
1115             $matchfield = "LocalBase";
1116         }
1117         else {
1118             $matchfield = "Base";
1119         }
1120
1121         $self->SUPER::Limit(
1122             ALIAS           => $LinkAlias,
1123             ENTRYAGGREGATOR => 'AND',
1124             FIELD           => $matchfield,
1125             OPERATOR        => '=',
1126             VALUE           => $restriction->{'BASE'}
1127         );
1128
1129         #If we're searching on base, join the target to ticket.id
1130         $self->_SQLJoin(
1131             ALIAS1 => 'main',
1132             FIELD1 => $self->{'primary_key'},
1133             ALIAS2 => $LinkAlias,
1134             FIELD2 => 'LocalTarget'
1135         );
1136     }
1137 }
1138
1139 =head2 KeywordLimit
1140
1141 Limit based on Keywords
1142
1143 Meta Data:
1144   none
1145
1146 =cut
1147
1148 sub _CustomFieldLimit {
1149     my ( $self, $_field, $op, $value, @rest ) = @_;
1150
1151     my %rest  = @rest;
1152     my $field = $rest{SUBKEY} || die "No field specified";
1153
1154     # For our sanity, we can only limit on one queue at a time
1155     my $queue = 0;
1156
1157     if ( $field =~ /^(.+?)\.{(.+)}$/ ) {
1158         $queue = $1;
1159         $field = $2;
1160     }
1161     $field = $1 if $field =~ /^{(.+)}$/;    # trim { }
1162
1163     # If we're trying to find custom fields that don't match something, we
1164     # want tickets where the custom field has no value at all.  Note that
1165     # we explicitly don't include the "IS NULL" case, since we would
1166     # otherwise end up with a redundant clause.
1167
1168     my $null_columns_ok;
1169     if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
1170         $null_columns_ok = 1;
1171     }
1172
1173     my $cfid = 0;
1174     if ($queue) {
1175
1176         my $q = RT::Queue->new( $self->CurrentUser );
1177         $q->Load($queue) if ($queue);
1178
1179         my $cf;
1180         if ( $q->id ) {
1181             $cf = $q->CustomField($field);
1182         }
1183         else {
1184             $cf = RT::CustomField->new( $self->CurrentUser );
1185             $cf->LoadByNameAndQueue( Queue => '0', Name => $field );
1186         }
1187
1188         $cfid = $cf->id;
1189
1190     }
1191
1192     my $TicketCFs;
1193     my $cfkey = $cfid ? $cfid : "$queue.$field";
1194
1195     # Perform one Join per CustomField
1196     if ( $self->{_sql_object_cf_alias}{$cfkey} ) {
1197         $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey};
1198     }
1199     else {
1200         if ($cfid) {
1201             $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
1202                 TYPE   => 'left',
1203                 ALIAS1 => 'main',
1204                 FIELD1 => 'id',
1205                 TABLE2 => 'ObjectCustomFieldValues',
1206                 FIELD2 => 'ObjectId',
1207             );
1208             $self->SUPER::Limit(
1209                 LEFTJOIN        => $TicketCFs,
1210                 FIELD           => 'CustomField',
1211                 VALUE           => $cfid,
1212                 ENTRYAGGREGATOR => 'AND'
1213             );
1214         }
1215         else {
1216             my $cfalias = $self->Join(
1217                 TYPE       => 'left',
1218                 EXPRESSION => "'$field'",
1219                 TABLE2     => 'CustomFields',
1220                 FIELD2     => 'Name',
1221             );
1222
1223             $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
1224                 TYPE   => 'left',
1225                 ALIAS1 => $cfalias,
1226                 FIELD1 => 'id',
1227                 TABLE2 => 'ObjectCustomFieldValues',
1228                 FIELD2 => 'CustomField',
1229             );
1230             $self->SUPER::Limit(
1231                 LEFTJOIN        => $TicketCFs,
1232                 FIELD           => 'ObjectId',
1233                 VALUE           => 'main.id',
1234                 QUOTEVALUE      => 0,
1235                 ENTRYAGGREGATOR => 'AND',
1236             );
1237         }
1238         $self->SUPER::Limit(
1239             LEFTJOIN => $TicketCFs,
1240             FIELD    => 'ObjectType',
1241             VALUE    => ref( $self->NewItem )
1242             ,    # we want a single item, not a collection
1243             ENTRYAGGREGATOR => 'AND'
1244         );
1245         $self->SUPER::Limit(
1246             LEFTJOIN        => $TicketCFs,
1247             FIELD           => 'Disabled',
1248             OPERATOR        => '=',
1249             VALUE           => '0',
1250             ENTRYAGGREGATOR => 'AND'
1251         );
1252     }
1253
1254     $self->_OpenParen if ($null_columns_ok);
1255
1256     $self->_SQLLimit(
1257         ALIAS      => $TicketCFs,
1258         FIELD      => 'Content',
1259         OPERATOR   => $op,
1260         VALUE      => $value,
1261         QUOTEVALUE => 1,
1262         @rest
1263     );
1264
1265     if ($null_columns_ok) {
1266         $self->_SQLLimit(
1267             ALIAS           => $TicketCFs,
1268             FIELD           => 'Content',
1269             OPERATOR        => 'IS',
1270             VALUE           => 'NULL',
1271             QUOTEVALUE      => 0,
1272             ENTRYAGGREGATOR => 'OR',
1273         );
1274     }
1275     $self->_CloseParen if ($null_columns_ok);
1276
1277 }
1278
1279 # End Helper Functions
1280
1281 # End of SQL Stuff -------------------------------------------------
1282
1283 # {{{ Limit the result set based on content
1284
1285 # {{{ sub Limit
1286
1287 =head2 Limit
1288
1289 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1290 Generally best called from LimitFoo methods
1291
1292 =cut
1293
1294 sub Limit {
1295     my $self = shift;
1296     my %args = (
1297         FIELD       => undef,
1298         OPERATOR    => '=',
1299         VALUE       => undef,
1300         DESCRIPTION => undef,
1301         @_
1302     );
1303     $args{'DESCRIPTION'} = $self->loc(
1304         "[_1] [_2] [_3]",  $args{'FIELD'},
1305         $args{'OPERATOR'}, $args{'VALUE'}
1306         )
1307         if ( !defined $args{'DESCRIPTION'} );
1308
1309     my $index = $self->_NextIndex;
1310
1311 #make the TicketRestrictions hash the equivalent of whatever we just passed in;
1312
1313     %{ $self->{'TicketRestrictions'}{$index} } = %args;
1314
1315     $self->{'RecalcTicketLimits'} = 1;
1316
1317 # If we're looking at the effective id, we don't want to append the other clause
1318 # which limits us to tickets where id = effective id
1319     if ( $args{'FIELD'} eq 'EffectiveId'
1320         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1321     {
1322         $self->{'looking_at_effective_id'} = 1;
1323     }
1324
1325     if ( $args{'FIELD'} eq 'Type'
1326         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1327     {
1328         $self->{'looking_at_type'} = 1;
1329     }
1330
1331     return ($index);
1332 }
1333
1334 # }}}
1335
1336 =head2 FreezeLimits
1337
1338 Returns a frozen string suitable for handing back to ThawLimits.
1339
1340 =cut
1341
1342 sub _FreezeThawKeys {
1343     'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1344         'looking_at_type';
1345 }
1346
1347 # {{{ sub FreezeLimits
1348
1349 sub FreezeLimits {
1350     my $self = shift;
1351     require Storable;
1352     require MIME::Base64;
1353     MIME::Base64::base64_encode(
1354         Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1355 }
1356
1357 # }}}
1358
1359 =head2 ThawLimits
1360
1361 Take a frozen Limits string generated by FreezeLimits and make this tickets
1362 object have that set of limits.
1363
1364 =cut
1365
1366 # {{{ sub ThawLimits
1367
1368 sub ThawLimits {
1369     my $self = shift;
1370     my $in   = shift;
1371
1372     #if we don't have $in, get outta here.
1373     return undef unless ($in);
1374
1375     $self->{'RecalcTicketLimits'} = 1;
1376
1377     require Storable;
1378     require MIME::Base64;
1379
1380     #We don't need to die if the thaw fails.
1381     @{$self}{ $self->_FreezeThawKeys }
1382         = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1383
1384     $RT::Logger->error($@) if $@;
1385
1386 }
1387
1388 # }}}
1389
1390 # {{{ Limit by enum or foreign key
1391
1392 # {{{ sub LimitQueue
1393
1394 =head2 LimitQueue
1395
1396 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1397 OPERATOR is one of = or !=. (It defaults to =).
1398 VALUE is a queue id or Name.
1399
1400
1401 =cut
1402
1403 sub LimitQueue {
1404     my $self = shift;
1405     my %args = (
1406         VALUE    => undef,
1407         OPERATOR => '=',
1408         @_
1409     );
1410
1411     #TODO  VALUE should also take queue objects
1412     if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1413         my $queue = new RT::Queue( $self->CurrentUser );
1414         $queue->Load( $args{'VALUE'} );
1415         $args{'VALUE'} = $queue->Id;
1416     }
1417
1418     # What if they pass in an Id?  Check for isNum() and convert to
1419     # string.
1420
1421     #TODO check for a valid queue here
1422
1423     $self->Limit(
1424         FIELD       => 'Queue',
1425         VALUE       => $args{'VALUE'},
1426         OPERATOR    => $args{'OPERATOR'},
1427         DESCRIPTION => join(
1428             ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1429         ),
1430     );
1431
1432 }
1433
1434 # }}}
1435
1436 # {{{ sub LimitStatus
1437
1438 =head2 LimitStatus
1439
1440 Takes a paramhash with the fields OPERATOR and VALUE.
1441 OPERATOR is one of = or !=.
1442 VALUE is a status.
1443
1444 RT adds Status != 'deleted' until object has
1445 allow_deleted_search internal property set.
1446 $tickets->{'allow_deleted_search'} = 1;
1447 $tickets->LimitStatus( VALUE => 'deleted' );
1448
1449 =cut
1450
1451 sub LimitStatus {
1452     my $self = shift;
1453     my %args = (
1454         OPERATOR => '=',
1455         @_
1456     );
1457     $self->Limit(
1458         FIELD       => 'Status',
1459         VALUE       => $args{'VALUE'},
1460         OPERATOR    => $args{'OPERATOR'},
1461         DESCRIPTION => join( ' ',
1462             $self->loc('Status'), $args{'OPERATOR'},
1463             $self->loc( $args{'VALUE'} ) ),
1464     );
1465 }
1466
1467 # }}}
1468
1469 # {{{ sub IgnoreType
1470
1471 =head2 IgnoreType
1472
1473 If called, this search will not automatically limit the set of results found
1474 to tickets of type "Ticket". Tickets of other types, such as "project" and
1475 "approval" will be found.
1476
1477 =cut
1478
1479 sub IgnoreType {
1480     my $self = shift;
1481
1482     # Instead of faking a Limit that later gets ignored, fake up the
1483     # fact that we're already looking at type, so that the check in
1484     # Tickets_Overlay_SQL/FromSQL goes down the right branch
1485
1486     #  $self->LimitType(VALUE => '__any');
1487     $self->{looking_at_type} = 1;
1488 }
1489
1490 # }}}
1491
1492 # {{{ sub LimitType
1493
1494 =head2 LimitType
1495
1496 Takes a paramhash with the fields OPERATOR and VALUE.
1497 OPERATOR is one of = or !=, it defaults to "=".
1498 VALUE is a string to search for in the type of the ticket.
1499
1500
1501
1502 =cut
1503
1504 sub LimitType {
1505     my $self = shift;
1506     my %args = (
1507         OPERATOR => '=',
1508         VALUE    => undef,
1509         @_
1510     );
1511     $self->Limit(
1512         FIELD       => 'Type',
1513         VALUE       => $args{'VALUE'},
1514         OPERATOR    => $args{'OPERATOR'},
1515         DESCRIPTION => join( ' ',
1516             $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
1517     );
1518 }
1519
1520 # }}}
1521
1522 # }}}
1523
1524 # {{{ Limit by string field
1525
1526 # {{{ sub LimitSubject
1527
1528 =head2 LimitSubject
1529
1530 Takes a paramhash with the fields OPERATOR and VALUE.
1531 OPERATOR is one of = or !=.
1532 VALUE is a string to search for in the subject of the ticket.
1533
1534 =cut
1535
1536 sub LimitSubject {
1537     my $self = shift;
1538     my %args = (@_);
1539     $self->Limit(
1540         FIELD       => 'Subject',
1541         VALUE       => $args{'VALUE'},
1542         OPERATOR    => $args{'OPERATOR'},
1543         DESCRIPTION => join( ' ',
1544             $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1545     );
1546 }
1547
1548 # }}}
1549
1550 # }}}
1551
1552 # {{{ Limit based on ticket numerical attributes
1553 # Things that can be > < = !=
1554
1555 # {{{ sub LimitId
1556
1557 =head2 LimitId
1558
1559 Takes a paramhash with the fields OPERATOR and VALUE.
1560 OPERATOR is one of =, >, < or !=.
1561 VALUE is a ticket Id to search for
1562
1563 =cut
1564
1565 sub LimitId {
1566     my $self = shift;
1567     my %args = (
1568         OPERATOR => '=',
1569         @_
1570     );
1571
1572     $self->Limit(
1573         FIELD       => 'id',
1574         VALUE       => $args{'VALUE'},
1575         OPERATOR    => $args{'OPERATOR'},
1576         DESCRIPTION =>
1577             join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1578     );
1579 }
1580
1581 # }}}
1582
1583 # {{{ sub LimitPriority
1584
1585 =head2 LimitPriority
1586
1587 Takes a paramhash with the fields OPERATOR and VALUE.
1588 OPERATOR is one of =, >, < or !=.
1589 VALUE is a value to match the ticket\'s priority against
1590
1591 =cut
1592
1593 sub LimitPriority {
1594     my $self = shift;
1595     my %args = (@_);
1596     $self->Limit(
1597         FIELD       => 'Priority',
1598         VALUE       => $args{'VALUE'},
1599         OPERATOR    => $args{'OPERATOR'},
1600         DESCRIPTION => join( ' ',
1601             $self->loc('Priority'),
1602             $args{'OPERATOR'}, $args{'VALUE'}, ),
1603     );
1604 }
1605
1606 # }}}
1607
1608 # {{{ sub LimitInitialPriority
1609
1610 =head2 LimitInitialPriority
1611
1612 Takes a paramhash with the fields OPERATOR and VALUE.
1613 OPERATOR is one of =, >, < or !=.
1614 VALUE is a value to match the ticket\'s initial priority against
1615
1616
1617 =cut
1618
1619 sub LimitInitialPriority {
1620     my $self = shift;
1621     my %args = (@_);
1622     $self->Limit(
1623         FIELD       => 'InitialPriority',
1624         VALUE       => $args{'VALUE'},
1625         OPERATOR    => $args{'OPERATOR'},
1626         DESCRIPTION => join( ' ',
1627             $self->loc('Initial Priority'), $args{'OPERATOR'},
1628             $args{'VALUE'}, ),
1629     );
1630 }
1631
1632 # }}}
1633
1634 # {{{ sub LimitFinalPriority
1635
1636 =head2 LimitFinalPriority
1637
1638 Takes a paramhash with the fields OPERATOR and VALUE.
1639 OPERATOR is one of =, >, < or !=.
1640 VALUE is a value to match the ticket\'s final priority against
1641
1642 =cut
1643
1644 sub LimitFinalPriority {
1645     my $self = shift;
1646     my %args = (@_);
1647     $self->Limit(
1648         FIELD       => 'FinalPriority',
1649         VALUE       => $args{'VALUE'},
1650         OPERATOR    => $args{'OPERATOR'},
1651         DESCRIPTION => join( ' ',
1652             $self->loc('Final Priority'), $args{'OPERATOR'},
1653             $args{'VALUE'}, ),
1654     );
1655 }
1656
1657 # }}}
1658
1659 # {{{ sub LimitTimeWorked
1660
1661 =head2 LimitTimeWorked
1662
1663 Takes a paramhash with the fields OPERATOR and VALUE.
1664 OPERATOR is one of =, >, < or !=.
1665 VALUE is a value to match the ticket's TimeWorked attribute
1666
1667 =cut
1668
1669 sub LimitTimeWorked {
1670     my $self = shift;
1671     my %args = (@_);
1672     $self->Limit(
1673         FIELD       => 'TimeWorked',
1674         VALUE       => $args{'VALUE'},
1675         OPERATOR    => $args{'OPERATOR'},
1676         DESCRIPTION => join( ' ',
1677             $self->loc('Time worked'),
1678             $args{'OPERATOR'}, $args{'VALUE'}, ),
1679     );
1680 }
1681
1682 # }}}
1683
1684 # {{{ sub LimitTimeLeft
1685
1686 =head2 LimitTimeLeft
1687
1688 Takes a paramhash with the fields OPERATOR and VALUE.
1689 OPERATOR is one of =, >, < or !=.
1690 VALUE is a value to match the ticket's TimeLeft attribute
1691
1692 =cut
1693
1694 sub LimitTimeLeft {
1695     my $self = shift;
1696     my %args = (@_);
1697     $self->Limit(
1698         FIELD       => 'TimeLeft',
1699         VALUE       => $args{'VALUE'},
1700         OPERATOR    => $args{'OPERATOR'},
1701         DESCRIPTION => join( ' ',
1702             $self->loc('Time left'),
1703             $args{'OPERATOR'}, $args{'VALUE'}, ),
1704     );
1705 }
1706
1707 # }}}
1708
1709 # }}}
1710
1711 # {{{ Limiting based on attachment attributes
1712
1713 # {{{ sub LimitContent
1714
1715 =head2 LimitContent
1716
1717 Takes a paramhash with the fields OPERATOR and VALUE.
1718 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1719 VALUE is a string to search for in the body of the ticket
1720
1721 =cut
1722
1723 sub LimitContent {
1724     my $self = shift;
1725     my %args = (@_);
1726     $self->Limit(
1727         FIELD       => 'Content',
1728         VALUE       => $args{'VALUE'},
1729         OPERATOR    => $args{'OPERATOR'},
1730         DESCRIPTION => join( ' ',
1731             $self->loc('Ticket content'), $args{'OPERATOR'},
1732             $args{'VALUE'}, ),
1733     );
1734 }
1735
1736 # }}}
1737
1738 # {{{ sub LimitFilename
1739
1740 =head2 LimitFilename
1741
1742 Takes a paramhash with the fields OPERATOR and VALUE.
1743 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1744 VALUE is a string to search for in the body of the ticket
1745
1746 =cut
1747
1748 sub LimitFilename {
1749     my $self = shift;
1750     my %args = (@_);
1751     $self->Limit(
1752         FIELD       => 'Filename',
1753         VALUE       => $args{'VALUE'},
1754         OPERATOR    => $args{'OPERATOR'},
1755         DESCRIPTION => join( ' ',
1756             $self->loc('Attachment filename'), $args{'OPERATOR'},
1757             $args{'VALUE'}, ),
1758     );
1759 }
1760
1761 # }}}
1762 # {{{ sub LimitContentType
1763
1764 =head2 LimitContentType
1765
1766 Takes a paramhash with the fields OPERATOR and VALUE.
1767 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1768 VALUE is a content type to search ticket attachments for
1769
1770 =cut
1771
1772 sub LimitContentType {
1773     my $self = shift;
1774     my %args = (@_);
1775     $self->Limit(
1776         FIELD       => 'ContentType',
1777         VALUE       => $args{'VALUE'},
1778         OPERATOR    => $args{'OPERATOR'},
1779         DESCRIPTION => join( ' ',
1780             $self->loc('Ticket content type'), $args{'OPERATOR'},
1781             $args{'VALUE'}, ),
1782     );
1783 }
1784
1785 # }}}
1786
1787 # }}}
1788
1789 # {{{ Limiting based on people
1790
1791 # {{{ sub LimitOwner
1792
1793 =head2 LimitOwner
1794
1795 Takes a paramhash with the fields OPERATOR and VALUE.
1796 OPERATOR is one of = or !=.
1797 VALUE is a user id.
1798
1799 =cut
1800
1801 sub LimitOwner {
1802     my $self = shift;
1803     my %args = (
1804         OPERATOR => '=',
1805         @_
1806     );
1807
1808     my $owner = new RT::User( $self->CurrentUser );
1809     $owner->Load( $args{'VALUE'} );
1810
1811     # FIXME: check for a valid $owner
1812     $self->Limit(
1813         FIELD       => 'Owner',
1814         VALUE       => $args{'VALUE'},
1815         OPERATOR    => $args{'OPERATOR'},
1816         DESCRIPTION => join( ' ',
1817             $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
1818     );
1819
1820 }
1821
1822 # }}}
1823
1824 # {{{ Limiting watchers
1825
1826 # {{{ sub LimitWatcher
1827
1828 =head2 LimitWatcher
1829
1830   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1831   OPERATOR is one of =, LIKE, NOT LIKE or !=.
1832   VALUE is a value to match the ticket\'s watcher email addresses against
1833   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1834
1835 =begin testing
1836
1837 my $t1 = RT::Ticket->new($RT::SystemUser);
1838 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
1839
1840 =end testing
1841
1842 =cut
1843
1844 sub LimitWatcher {
1845     my $self = shift;
1846     my %args = (
1847         OPERATOR => '=',
1848         VALUE    => undef,
1849         TYPE     => undef,
1850         @_
1851     );
1852
1853     #build us up a description
1854     my ( $watcher_type, $desc );
1855     if ( $args{'TYPE'} ) {
1856         $watcher_type = $args{'TYPE'};
1857     }
1858     else {
1859         $watcher_type = "Watcher";
1860     }
1861
1862     $self->Limit(
1863         FIELD       => $watcher_type,
1864         VALUE       => $args{'VALUE'},
1865         OPERATOR    => $args{'OPERATOR'},
1866         TYPE        => $args{'TYPE'},
1867         DESCRIPTION => join( ' ',
1868             $self->loc($watcher_type),
1869             $args{'OPERATOR'}, $args{'VALUE'}, ),
1870     );
1871 }
1872
1873 sub LimitRequestor {
1874     my $self = shift;
1875     my %args = (@_);
1876     $RT::Logger->error( "Tickets->LimitRequestor is deprecated  at ("
1877             . join( ":", caller )
1878             . ")" );
1879     $self->LimitWatcher( TYPE => 'Requestor', @_ );
1880
1881 }
1882
1883 # }}}
1884
1885 # }}}
1886
1887 # }}}
1888
1889 # {{{ Limiting based on links
1890
1891 # {{{ LimitLinkedTo
1892
1893 =head2 LimitLinkedTo
1894
1895 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1896 TYPE limits the sort of link we want to search on
1897
1898 TYPE = { RefersTo, MemberOf, DependsOn }
1899
1900 TARGET is the id or URI of the TARGET of the link
1901 (TARGET used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as TARGET
1902
1903 =cut
1904
1905 sub LimitLinkedTo {
1906     my $self = shift;
1907     my %args = (
1908         TICKET => undef,
1909         TARGET => undef,
1910         TYPE   => undef,
1911         @_
1912     );
1913
1914     $self->Limit(
1915         FIELD       => 'LinkedTo',
1916         BASE        => undef,
1917         TARGET      => ( $args{'TARGET'} || $args{'TICKET'} ),
1918         TYPE        => $args{'TYPE'},
1919         DESCRIPTION => $self->loc(
1920             "Tickets [_1] by [_2]",
1921             $self->loc( $args{'TYPE'} ),
1922             ( $args{'TARGET'} || $args{'TICKET'} )
1923         ),
1924     );
1925 }
1926
1927 # }}}
1928
1929 # {{{ LimitLinkedFrom
1930
1931 =head2 LimitLinkedFrom
1932
1933 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1934 TYPE limits the sort of link we want to search on
1935
1936
1937 BASE is the id or URI of the BASE of the link
1938 (BASE used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as BASE
1939
1940
1941 =cut
1942
1943 sub LimitLinkedFrom {
1944     my $self = shift;
1945     my %args = (
1946         BASE   => undef,
1947         TICKET => undef,
1948         TYPE   => undef,
1949         @_
1950     );
1951
1952     # translate RT2 From/To naming to RT3 TicketSQL naming
1953     my %fromToMap = qw(DependsOn DependentOn
1954         MemberOf  HasMember
1955         RefersTo  ReferredToBy);
1956
1957     my $type = $args{'TYPE'};
1958     $type = $fromToMap{$type} if exists( $fromToMap{$type} );
1959
1960     $self->Limit(
1961         FIELD       => 'LinkedTo',
1962         TARGET      => undef,
1963         BASE        => ( $args{'BASE'} || $args{'TICKET'} ),
1964         TYPE        => $type,
1965         DESCRIPTION => $self->loc(
1966             "Tickets [_1] [_2]",
1967             $self->loc( $args{'TYPE'} ),
1968             ( $args{'BASE'} || $args{'TICKET'} )
1969         ),
1970     );
1971 }
1972
1973 # }}}
1974
1975 # {{{ LimitMemberOf
1976 sub LimitMemberOf {
1977     my $self      = shift;
1978     my $ticket_id = shift;
1979     $self->LimitLinkedTo(
1980         TARGET => "$ticket_id",
1981         TYPE   => 'MemberOf',
1982     );
1983
1984 }
1985
1986 # }}}
1987
1988 # {{{ LimitHasMember
1989 sub LimitHasMember {
1990     my $self      = shift;
1991     my $ticket_id = shift;
1992     $self->LimitLinkedFrom(
1993         BASE => "$ticket_id",
1994         TYPE => 'HasMember',
1995     );
1996
1997 }
1998
1999 # }}}
2000
2001 # {{{ LimitDependsOn
2002
2003 sub LimitDependsOn {
2004     my $self      = shift;
2005     my $ticket_id = shift;
2006     $self->LimitLinkedTo(
2007         TARGET => "$ticket_id",
2008         TYPE   => 'DependsOn',
2009     );
2010
2011 }
2012
2013 # }}}
2014
2015 # {{{ LimitDependedOnBy
2016
2017 sub LimitDependedOnBy {
2018     my $self      = shift;
2019     my $ticket_id = shift;
2020     $self->LimitLinkedFrom(
2021         BASE => "$ticket_id",
2022         TYPE => 'DependentOn',
2023     );
2024
2025 }
2026
2027 # }}}
2028
2029 # {{{ LimitRefersTo
2030
2031 sub LimitRefersTo {
2032     my $self      = shift;
2033     my $ticket_id = shift;
2034     $self->LimitLinkedTo(
2035         TARGET => "$ticket_id",
2036         TYPE   => 'RefersTo',
2037     );
2038
2039 }
2040
2041 # }}}
2042
2043 # {{{ LimitReferredToBy
2044
2045 sub LimitReferredToBy {
2046     my $self      = shift;
2047     my $ticket_id = shift;
2048     $self->LimitLinkedFrom(
2049         BASE => "$ticket_id",
2050         TYPE => 'ReferredToBy',
2051     );
2052
2053 }
2054
2055 # }}}
2056
2057 # }}}
2058
2059 # {{{ limit based on ticket date attribtes
2060
2061 # {{{ sub LimitDate
2062
2063 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2064
2065 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2066
2067 OPERATOR is one of > or <
2068 VALUE is a date and time in ISO format in GMT
2069 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2070
2071 There are also helper functions of the form LimitFIELD that eliminate
2072 the need to pass in a FIELD argument.
2073
2074 =cut
2075
2076 sub LimitDate {
2077     my $self = shift;
2078     my %args = (
2079         FIELD    => undef,
2080         VALUE    => undef,
2081         OPERATOR => undef,
2082
2083         @_
2084     );
2085
2086     #Set the description if we didn't get handed it above
2087     unless ( $args{'DESCRIPTION'} ) {
2088         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2089             . $args{'OPERATOR'} . " "
2090             . $args{'VALUE'} . " GMT";
2091     }
2092
2093     $self->Limit(%args);
2094
2095 }
2096
2097 # }}}
2098
2099 sub LimitCreated {
2100     my $self = shift;
2101     $self->LimitDate( FIELD => 'Created', @_ );
2102 }
2103
2104 sub LimitDue {
2105     my $self = shift;
2106     $self->LimitDate( FIELD => 'Due', @_ );
2107
2108 }
2109
2110 sub LimitStarts {
2111     my $self = shift;
2112     $self->LimitDate( FIELD => 'Starts', @_ );
2113
2114 }
2115
2116 sub LimitStarted {
2117     my $self = shift;
2118     $self->LimitDate( FIELD => 'Started', @_ );
2119 }
2120
2121 sub LimitResolved {
2122     my $self = shift;
2123     $self->LimitDate( FIELD => 'Resolved', @_ );
2124 }
2125
2126 sub LimitTold {
2127     my $self = shift;
2128     $self->LimitDate( FIELD => 'Told', @_ );
2129 }
2130
2131 sub LimitLastUpdated {
2132     my $self = shift;
2133     $self->LimitDate( FIELD => 'LastUpdated', @_ );
2134 }
2135
2136 #
2137 # {{{ sub LimitTransactionDate
2138
2139 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2140
2141 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2142
2143 OPERATOR is one of > or <
2144 VALUE is a date and time in ISO format in GMT
2145
2146
2147 =cut
2148
2149 sub LimitTransactionDate {
2150     my $self = shift;
2151     my %args = (
2152         FIELD    => 'TransactionDate',
2153         VALUE    => undef,
2154         OPERATOR => undef,
2155
2156         @_
2157     );
2158
2159     #  <20021217042756.GK28744@pallas.fsck.com>
2160     #    "Kill It" - Jesse.
2161
2162     #Set the description if we didn't get handed it above
2163     unless ( $args{'DESCRIPTION'} ) {
2164         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2165             . $args{'OPERATOR'} . " "
2166             . $args{'VALUE'} . " GMT";
2167     }
2168
2169     $self->Limit(%args);
2170
2171 }
2172
2173 # }}}
2174
2175 # }}}
2176
2177 # {{{ Limit based on custom fields
2178 # {{{ sub LimitCustomField
2179
2180 =head2 LimitCustomField
2181
2182 Takes a paramhash of key/value pairs with the following keys:
2183
2184 =over 4
2185
2186 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2187
2188 =item OPERATOR - The usual Limit operators
2189
2190 =item VALUE - The value to compare against
2191
2192 =back
2193
2194 =cut
2195
2196 sub LimitCustomField {
2197     my $self = shift;
2198     my %args = (
2199         VALUE       => undef,
2200         CUSTOMFIELD => undef,
2201         OPERATOR    => '=',
2202         DESCRIPTION => undef,
2203         FIELD       => 'CustomFieldValue',
2204         QUOTEVALUE  => 1,
2205         @_
2206     );
2207
2208     my $CF = RT::CustomField->new( $self->CurrentUser );
2209     if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2210         $CF->Load( $args{CUSTOMFIELD} );
2211     }
2212     else {
2213         $CF->LoadByNameAndQueue(
2214             Name  => $args{CUSTOMFIELD},
2215             Queue => $args{QUEUE}
2216         );
2217         $args{CUSTOMFIELD} = $CF->Id;
2218     }
2219
2220     #If we are looking to compare with a null value.
2221     if ( $args{'OPERATOR'} =~ /^is$/i ) {
2222         $args{'DESCRIPTION'}
2223             ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2224     }
2225     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2226         $args{'DESCRIPTION'}
2227             ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2228     }
2229
2230     # if we're not looking to compare with a null value
2231     else {
2232         $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2233             $CF->Name, $args{OPERATOR}, $args{VALUE} );
2234     }
2235
2236     my $q = "";
2237     if ( $CF->Queue ) {
2238         my $qo = new RT::Queue( $self->CurrentUser );
2239         $qo->Load( $CF->Queue );
2240         $q = $qo->Name;
2241     }
2242
2243     my @rest;
2244     @rest = ( ENTRYAGGREGATOR => 'AND' )
2245         if ( $CF->Type eq 'SelectMultiple' );
2246
2247     $self->Limit(
2248         VALUE => $args{VALUE},
2249         FIELD => "CF."
2250             . (
2251               $q
2252             ? $q . ".{" . $CF->Name . "}"
2253             : $CF->Name
2254             ),
2255         OPERATOR    => $args{OPERATOR},
2256         CUSTOMFIELD => 1,
2257         @rest,
2258     );
2259
2260     $self->{'RecalcTicketLimits'} = 1;
2261 }
2262
2263 # }}}
2264 # }}}
2265
2266 # {{{ sub _NextIndex
2267
2268 =head2 _NextIndex
2269
2270 Keep track of the counter for the array of restrictions
2271
2272 =cut
2273
2274 sub _NextIndex {
2275     my $self = shift;
2276     return ( $self->{'restriction_index'}++ );
2277 }
2278
2279 # }}}
2280
2281 # }}}
2282
2283 # {{{ Core bits to make this a DBIx::SearchBuilder object
2284
2285 # {{{ sub _Init
2286 sub _Init {
2287     my $self = shift;
2288     $self->{'table'}                   = "Tickets";
2289     $self->{'RecalcTicketLimits'}      = 1;
2290     $self->{'looking_at_effective_id'} = 0;
2291     $self->{'looking_at_type'}         = 0;
2292     $self->{'restriction_index'}       = 1;
2293     $self->{'primary_key'}             = "id";
2294     delete $self->{'items_array'};
2295     delete $self->{'item_map'};
2296     delete $self->{'columns_to_display'};
2297     $self->SUPER::_Init(@_);
2298
2299     $self->_InitSQL;
2300
2301 }
2302
2303 # }}}
2304
2305 # {{{ sub Count
2306 sub Count {
2307     my $self = shift;
2308     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2309     return ( $self->SUPER::Count() );
2310 }
2311
2312 # }}}
2313
2314 # {{{ sub CountAll
2315 sub CountAll {
2316     my $self = shift;
2317     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2318     return ( $self->SUPER::CountAll() );
2319 }
2320
2321 # }}}
2322
2323 # {{{ sub ItemsArrayRef
2324
2325 =head2 ItemsArrayRef
2326
2327 Returns a reference to the set of all items found in this search
2328
2329 =cut
2330
2331 sub ItemsArrayRef {
2332     my $self = shift;
2333     my @items;
2334
2335     unless ( $self->{'items_array'} ) {
2336
2337         my $placeholder = $self->_ItemsCounter;
2338         $self->GotoFirstItem();
2339         while ( my $item = $self->Next ) {
2340             push( @{ $self->{'items_array'} }, $item );
2341         }
2342         $self->GotoItem($placeholder);
2343         $self->{'items_array'}
2344             = $self->ItemsOrderBy( $self->{'items_array'} );
2345     }
2346     return ( $self->{'items_array'} );
2347 }
2348
2349 # }}}
2350
2351 # {{{ sub Next
2352 sub Next {
2353     my $self = shift;
2354
2355     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2356
2357     my $Ticket = $self->SUPER::Next();
2358     if ( ( defined($Ticket) ) and ( ref($Ticket) ) ) {
2359
2360         if ( $Ticket->__Value('Status') eq 'deleted'
2361             && !$self->{'allow_deleted_search'} )
2362         {
2363             return ( $self->Next() );
2364         }
2365
2366         # Since Ticket could be granted with more rights instead
2367         # of being revoked, it's ok if queue rights allow
2368         # ShowTicket.  It seems need another query, but we have
2369         # rights cache in Principal::HasRight.
2370         elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
2371             || $Ticket->CurrentUserHasRight('ShowTicket') )
2372         {
2373             return ($Ticket);
2374         }
2375
2376         if ( $Ticket->__Value('Status') eq 'deleted' ) {
2377             return ( $self->Next() );
2378         }
2379
2380         # Since Ticket could be granted with more rights instead
2381         # of being revoked, it's ok if queue rights allow
2382         # ShowTicket.  It seems need another query, but we have
2383         # rights cache in Principal::HasRight.
2384         elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
2385             || $Ticket->CurrentUserHasRight('ShowTicket') )
2386         {
2387             return ($Ticket);
2388         }
2389
2390         #If the user doesn't have the right to show this ticket
2391         else {
2392             return ( $self->Next() );
2393         }
2394     }
2395
2396     #if there never was any ticket
2397     else {
2398         return (undef);
2399     }
2400
2401 }
2402
2403 # }}}
2404
2405 # }}}
2406
2407 # {{{ Deal with storing and restoring restrictions
2408
2409 # {{{ sub LoadRestrictions
2410
2411 =head2 LoadRestrictions
2412
2413 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
2414 TODO It is not yet implemented
2415
2416 =cut
2417
2418 # }}}
2419
2420 # {{{ sub DescribeRestrictions
2421
2422 =head2 DescribeRestrictions
2423
2424 takes nothing.
2425 Returns a hash keyed by restriction id.
2426 Each element of the hash is currently a one element hash that contains DESCRIPTION which
2427 is a description of the purpose of that TicketRestriction
2428
2429 =cut
2430
2431 sub DescribeRestrictions {
2432     my $self = shift;
2433
2434     my ( $row, %listing );
2435
2436     foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2437         $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
2438     }
2439     return (%listing);
2440 }
2441
2442 # }}}
2443
2444 # {{{ sub RestrictionValues
2445
2446 =head2 RestrictionValues FIELD
2447
2448 Takes a restriction field and returns a list of values this field is restricted
2449 to.
2450
2451 =cut
2452
2453 sub RestrictionValues {
2454     my $self  = shift;
2455     my $field = shift;
2456     map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
2457                $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
2458             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
2459         }
2460         keys %{ $self->{'TicketRestrictions'} };
2461 }
2462
2463 # }}}
2464
2465 # {{{ sub ClearRestrictions
2466
2467 =head2 ClearRestrictions
2468
2469 Removes all restrictions irretrievably
2470
2471 =cut
2472
2473 sub ClearRestrictions {
2474     my $self = shift;
2475     delete $self->{'TicketRestrictions'};
2476     $self->{'looking_at_effective_id'} = 0;
2477     $self->{'looking_at_type'}         = 0;
2478     $self->{'RecalcTicketLimits'}      = 1;
2479 }
2480
2481 # }}}
2482
2483 # {{{ sub DeleteRestriction
2484
2485 =head2 DeleteRestriction
2486
2487 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
2488 Removes that restriction from the session's limits.
2489
2490 =cut
2491
2492 sub DeleteRestriction {
2493     my $self = shift;
2494     my $row  = shift;
2495     delete $self->{'TicketRestrictions'}{$row};
2496
2497     $self->{'RecalcTicketLimits'} = 1;
2498
2499     #make the underlying easysearch object forget all its preconceptions
2500 }
2501
2502 # }}}
2503
2504 # {{{ sub _RestrictionsToClauses
2505
2506 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
2507
2508 sub _RestrictionsToClauses {
2509     my $self = shift;
2510
2511     my $row;
2512     my %clause;
2513     foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2514         my $restriction = $self->{'TicketRestrictions'}{$row};
2515
2516         #use Data::Dumper;
2517         #print Dumper($restriction),"\n";
2518
2519    # We need to reimplement the subclause aggregation that SearchBuilder does.
2520    # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
2521    # Then SB AND's the different Subclauses together.
2522
2523         # So, we want to group things into Subclauses, convert them to
2524         # SQL, and then join them with the appropriate DefaultEA.
2525         # Then join each subclause group with AND.
2526
2527         my $field = $restriction->{'FIELD'};
2528         my $realfield = $field;    # CustomFields fake up a fieldname, so
2529                                    # we need to figure that out
2530
2531         # One special case
2532         # Rewrite LinkedTo meta field to the real field
2533         if ( $field =~ /LinkedTo/ ) {
2534             $realfield = $field = $restriction->{'TYPE'};
2535         }
2536
2537         # Two special case
2538         # Handle subkey fields with a different real field
2539         if ( $field =~ /^(\w+)\./ ) {
2540             $realfield = $1;
2541         }
2542
2543         die "I don't know about $field yet"
2544             unless ( exists $FIELDS{$realfield}
2545             or $restriction->{CUSTOMFIELD} );
2546
2547         my $type = $FIELDS{$realfield}->[0];
2548         my $op   = $restriction->{'OPERATOR'};
2549
2550         my $value = (
2551             grep    {defined}
2552                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
2553         )[0];
2554
2555         # this performs the moral equivalent of defined or/dor/C<//>,
2556         # without the short circuiting.You need to use a 'defined or'
2557         # type thing instead of just checking for truth values, because
2558         # VALUE could be 0.(i.e. "false")
2559
2560         # You could also use this, but I find it less aesthetic:
2561         # (although it does short circuit)
2562         #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2563         # defined $restriction->{'TICKET'} ?
2564         # $restriction->{TICKET} :
2565         # defined $restriction->{'BASE'} ?
2566         # $restriction->{BASE} :
2567         # defined $restriction->{'TARGET'} ?
2568         # $restriction->{TARGET} )
2569
2570         my $ea = $restriction->{ENTRYAGGREGATOR}
2571             || $DefaultEA{$type}
2572             || "AND";
2573         if ( ref $ea ) {
2574             die "Invalid operator $op for $field ($type)"
2575                 unless exists $ea->{$op};
2576             $ea = $ea->{$op};
2577         }
2578
2579         # Each CustomField should be put into a different Clause so they
2580         # are ANDed together.
2581         if ( $restriction->{CUSTOMFIELD} ) {
2582             $realfield = $field;
2583         }
2584
2585         exists $clause{$realfield} or $clause{$realfield} = [];
2586
2587         # Escape Quotes
2588         $field =~ s!(['"])!\\$1!g;
2589         $value =~ s!(['"])!\\$1!g;
2590         my $data = [ $ea, $type, $field, $op, $value ];
2591
2592         # here is where we store extra data, say if it's a keyword or
2593         # something.  (I.e. "TYPE SPECIFIC STUFF")
2594
2595         #print Dumper($data);
2596         push @{ $clause{$realfield} }, $data;
2597     }
2598     return \%clause;
2599 }
2600
2601 # }}}
2602
2603 # {{{ sub _ProcessRestrictions
2604
2605 =head2 _ProcessRestrictions PARAMHASH
2606
2607 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
2608 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
2609
2610 =cut
2611
2612 sub _ProcessRestrictions {
2613     my $self = shift;
2614
2615     #Blow away ticket aliases since we'll need to regenerate them for
2616     #a new search
2617     delete $self->{'TicketAliases'};
2618     delete $self->{'items_array'};
2619     delete $self->{'item_map'};
2620     delete $self->{'raw_rows'};
2621     delete $self->{'rows'};
2622     delete $self->{'count_all'};
2623
2624     my $sql = $self->Query;    # Violating the _SQL namespace
2625     if ( !$sql || $self->{'RecalcTicketLimits'} ) {
2626
2627         #  "Restrictions to Clauses Branch\n";
2628         my $clauseRef = eval { $self->_RestrictionsToClauses; };
2629         if ($@) {
2630             $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2631             $self->FromSQL("");
2632         }
2633         else {
2634             $sql = $self->ClausesToSQL($clauseRef);
2635             $self->FromSQL($sql) if $sql;
2636         }
2637     }
2638
2639     $self->{'RecalcTicketLimits'} = 0;
2640
2641 }
2642
2643 =head2 _BuildItemMap
2644
2645     # Build up a map of first/last/next/prev items, so that we can display search nav quickly
2646
2647 =cut
2648
2649 sub _BuildItemMap {
2650     my $self = shift;
2651
2652     my $items = $self->ItemsArrayRef;
2653     my $prev  = 0;
2654
2655     delete $self->{'item_map'};
2656     if ( $items->[0] ) {
2657         $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
2658         while ( my $item = shift @$items ) {
2659             my $id = $item->EffectiveId;
2660             $self->{'item_map'}->{$id}->{'defined'} = 1;
2661             $self->{'item_map'}->{$id}->{prev}      = $prev;
2662             $self->{'item_map'}->{$id}->{next}      = $items->[0]->EffectiveId
2663                 if ( $items->[0] );
2664             $prev = $id;
2665         }
2666         $self->{'item_map'}->{'last'} = $prev;
2667     }
2668 }
2669
2670 =head2 ItemMap
2671
2672 Returns an a map of all items found by this search. The map is of the form
2673
2674 $ItemMap->{'first'} = first ticketid found
2675 $ItemMap->{'last'} = last ticketid found
2676 $ItemMap->{$id}->{prev} = the ticket id found before $id
2677 $ItemMap->{$id}->{next} = the ticket id found after $id
2678
2679 =cut
2680
2681 sub ItemMap {
2682     my $self = shift;
2683     $self->_BuildItemMap()
2684         unless ( $self->{'items_array'} and $self->{'item_map'} );
2685     return ( $self->{'item_map'} );
2686 }
2687
2688 =cut
2689
2690
2691
2692 }
2693
2694
2695
2696 # }}}
2697
2698 # }}}
2699
2700 =head2 PrepForSerialization
2701
2702 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
2703
2704 =cut
2705
2706 sub PrepForSerialization {
2707     my $self = shift;
2708     delete $self->{'items'};
2709     $self->RedoSearch();
2710 }
2711
2712 =head1 FLAGS
2713
2714 RT::Tickets supports several flags which alter search behavior:
2715
2716
2717 allow_deleted_search  (Otherwise never show deleted tickets in search results)
2718 looking_at_type (otherwise limit to type=ticket)
2719
2720 These flags are set by calling 
2721
2722 $tickets->{'flagname'} = 1;
2723
2724 BUG: There should be an API for this
2725
2726 =cut
2727
2728 =begin testing
2729
2730 # We assume that we've got some tickets hanging around from before.
2731 ok( my $unlimittickets = RT::Tickets->new( $RT::SystemUser ) );
2732 ok( $unlimittickets->UnLimit );
2733 ok( $unlimittickets->Count > 0, "UnLimited tickets object should return tickets" );
2734
2735 =end testing
2736
2737 1;
2738
2739
2740