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