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