This commit was generated by cvs2svn to compensate for changes in r3921,
[freeside.git] / rt / lib / RT / Tickets_Overlay.pm
1 # {{{ BEGIN BPS TAGGED BLOCK
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2004 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
77 =end testing
78
79 =cut
80 use strict;
81 no warnings qw(redefine);
82 use vars qw(@SORTFIELDS);
83 use RT::CustomFields;
84
85
86 # Configuration Tables:
87
88 # FIELDS is a mapping of searchable Field name, to Type, and other
89 # metadata.
90
91 my %FIELDS =
92   ( Status          => ['ENUM'],
93     Queue           => ['ENUM' => 'Queue',],
94     Type            => ['ENUM',],
95     Creator         => ['ENUM' => 'User',],
96     LastUpdatedBy   => ['ENUM' => 'User',],
97     Owner           => ['ENUM' => 'User',],
98     EffectiveId     => ['INT',],
99     id              => ['INT',],
100     InitialPriority => ['INT',],
101     FinalPriority   => ['INT',],
102     Priority        => ['INT',],
103     TimeLeft        => ['INT',],
104     TimeWorked      => ['INT',],
105     MemberOf        => ['LINK' => To => 'MemberOf', ],
106     DependsOn       => ['LINK' => To => 'DependsOn',],
107     RefersTo        => ['LINK' => To => 'RefersTo',],
108     HasMember       => ['LINK' => From => 'MemberOf',],
109     DependentOn     => ['LINK' => From => 'DependsOn',],
110     DependedOnBy     => ['LINK' => From => 'DependsOn',],
111     ReferredToBy    => ['LINK' => From => 'RefersTo',],
112 #   HasDepender     => ['LINK',],
113 #   RelatedTo       => ['LINK',],
114     Told            => ['DATE' => 'Told',],
115     Starts          => ['DATE' => 'Starts',],
116     Started         => ['DATE' => 'Started',],
117     Due             => ['DATE' => 'Due',],
118     Resolved        => ['DATE' => 'Resolved',],
119     LastUpdated     => ['DATE' => 'LastUpdated',],
120     Created         => ['DATE' => 'Created',],
121     Subject         => ['STRING',],
122     Content         => ['TRANSFIELD',],
123     ContentType     => ['TRANSFIELD',],
124     Filename        => ['TRANSFIELD',],
125     TransactionDate => ['TRANSDATE',],
126     Requestor       => ['WATCHERFIELD' => 'Requestor',],
127     Requestors       => ['WATCHERFIELD' => 'Requestor',],
128     Cc              => ['WATCHERFIELD' => 'Cc',],
129     AdminCc         => ['WATCHERFIELD' => 'AdminCC',],
130     Watcher         => ['WATCHERFIELD'],
131     LinkedTo        => ['LINKFIELD',],
132     CustomFieldValue =>['CUSTOMFIELD',],
133     CF              => ['CUSTOMFIELD',],
134   );
135
136 # Mapping of Field Type to Function
137 my %dispatch =
138   ( ENUM            => \&_EnumLimit,
139     INT             => \&_IntLimit,
140     LINK            => \&_LinkLimit,
141     DATE            => \&_DateLimit,
142     STRING          => \&_StringLimit,
143     TRANSFIELD      => \&_TransLimit,
144     TRANSDATE       => \&_TransDateLimit,
145     WATCHERFIELD    => \&_WatcherLimit,
146     LINKFIELD       => \&_LinkFieldLimit,
147     CUSTOMFIELD    => \&_CustomFieldLimit,
148   );
149 my %can_bundle =
150   ( WATCHERFIELD => "yeps",
151   );
152
153 # Default EntryAggregator per type
154 # if you specify OP, you must specify all valid OPs
155 my %DefaultEA = (
156                  INT            => 'AND',
157                  ENUM           => { '=' => 'OR',
158                                      '!='=> 'AND'
159                                    },
160                  DATE           => { '=' => 'OR',
161                                      '>='=> 'AND',
162                                      '<='=> 'AND',
163                                      '>' => 'AND',
164                                      '<' => 'AND'
165                                    },
166                  STRING         => { '=' => 'OR',
167                                      '!='=> 'AND',
168                                      'LIKE'=> 'AND',
169                                      'NOT LIKE' => 'AND'
170                                    },
171                  TRANSFIELD     => 'AND',
172                  TRANSDATE      => 'AND',
173                  LINK           => 'OR',
174                  LINKFIELD      => 'AND',
175                  TARGET         => 'AND',
176                  BASE           => 'AND',
177                  WATCHERFIELD   => { '=' => 'OR',
178                                      '!='=> 'AND',
179                                      'LIKE'=> 'OR',
180                                      'NOT LIKE' => 'AND'
181                                    },
182
183                  CUSTOMFIELD    => 'OR',
184                 );
185
186
187 # Helper functions for passing the above lexically scoped tables above
188 # into Tickets_Overlay_SQL.
189 sub FIELDS   { return \%FIELDS   }
190 sub dispatch { return \%dispatch }
191 sub can_bundle { return \%can_bundle }
192
193 # Bring in the clowns.
194 require RT::Tickets_Overlay_SQL;
195
196 # {{{ sub SortFields
197
198 @SORTFIELDS = qw(id Status
199                  Queue Subject
200          Owner Created Due Starts Started
201          Told
202                  Resolved LastUpdated Priority TimeWorked TimeLeft);
203
204 =head2 SortFields
205
206 Returns the list of fields that lists of tickets can easily be sorted by
207
208 =cut
209
210 sub SortFields {
211         my $self = shift;
212         return(@SORTFIELDS);
213 }
214
215
216 # }}}
217
218
219 # BEGIN SQL STUFF *********************************
220
221 =head1 Limit Helper Routines
222
223 These routines are the targets of a dispatch table depending on the
224 type of field.  They all share the same signature:
225
226   my ($self,$field,$op,$value,@rest) = @_;
227
228 The values in @rest should be suitable for passing directly to
229 DBIx::SearchBuilder::Limit.
230
231 Essentially they are an expanded/broken out (and much simplified)
232 version of what ProcessRestrictions used to do.  They're also much
233 more clearly delineated by the TYPE of field being processed.
234
235 =head2 _EnumLimit
236
237 Handle Fields which are limited to certain values, and potentially
238 need to be looked up from another class.
239
240 This subroutine actually handles two different kinds of fields.  For
241 some the user is responsible for limiting the values.  (i.e. Status,
242 Type).
243
244 For others, the value specified by the user will be looked by via
245 specified class.
246
247 Meta Data:
248   name of class to lookup in (Optional)
249
250 =cut
251
252 sub _EnumLimit {
253   my ($sb,$field,$op,$value,@rest) = @_;
254
255   # SQL::Statement changes != to <>.  (Can we remove this now?)
256   $op = "!=" if $op eq "<>";
257
258   die "Invalid Operation: $op for $field"
259     unless $op eq "=" or $op eq "!=";
260
261   my $meta = $FIELDS{$field};
262   if (defined $meta->[1]) {
263     my $class = "RT::" . $meta->[1];
264     my $o = $class->new($sb->CurrentUser);
265     $o->Load( $value );
266     $value = $o->Id;
267   }
268   $sb->_SQLLimit( FIELD => $field,,
269               VALUE => $value,
270               OPERATOR => $op,
271               @rest,
272             );
273 }
274
275 =head2 _IntLimit
276
277 Handle fields where the values are limited to integers.  (For example,
278 Priority, TimeWorked.)
279
280 Meta Data:
281   None
282
283 =cut
284
285 sub _IntLimit {
286   my ($sb,$field,$op,$value,@rest) = @_;
287
288   die "Invalid Operator $op for $field"
289     unless $op =~ /^(=|!=|>|<|>=|<=)$/;
290
291   $sb->_SQLLimit(
292              FIELD => $field,
293              VALUE => $value,
294              OPERATOR => $op,
295              @rest,
296             );
297 }
298
299
300 =head2 _LinkLimit
301
302 Handle fields which deal with links between tickets.  (MemberOf, DependsOn)
303
304 Meta Data:
305   1: Direction (From,To)
306   2: Link Type (MemberOf, DependsOn,RefersTo)
307
308 =cut
309
310 sub _LinkLimit {
311   my ($sb,$field,$op,$value,@rest) = @_;
312
313   die "Op must be ="
314     unless $op eq "=";
315
316   my $meta = $FIELDS{$field};
317   die "Incorrect Meta Data for $field"
318     unless (defined $meta->[1] and defined $meta->[2]);
319
320   $sb->{_sql_linkalias} = $sb->NewAlias ('Links')
321     unless defined $sb->{_sql_linkalias};
322
323   $sb->_OpenParen();
324
325   $sb->_SQLLimit(
326              ALIAS => $sb->{_sql_linkalias},
327              FIELD =>   'Type',
328              OPERATOR => '=',
329              VALUE => $meta->[2],
330              @rest,
331             );
332
333   if ($meta->[1] eq "To") {
334     my $matchfield = ( $value  =~ /^(\d+)$/ ? "LocalTarget" : "Target" );
335
336     $sb->_SQLLimit(
337                ALIAS => $sb->{_sql_linkalias},
338                ENTRYAGGREGATOR => 'AND',
339                FIELD =>   $matchfield,
340                OPERATOR => '=',
341                VALUE => $value ,
342               );
343
344     #If we're searching on target, join the base to ticket.id
345     $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
346                ALIAS2 => $sb->{_sql_linkalias},  FIELD2 => 'LocalBase');
347
348   } elsif ( $meta->[1] eq "From" ) {
349     my $matchfield = ( $value  =~ /^(\d+)$/ ? "LocalBase" : "Base" );
350
351     $sb->_SQLLimit(
352                ALIAS => $sb->{_sql_linkalias},
353                ENTRYAGGREGATOR => 'AND',
354                FIELD =>   $matchfield,
355                OPERATOR => '=',
356                VALUE => $value ,
357               );
358
359     #If we're searching on base, join the target to ticket.id
360     $sb->_SQLJoin( ALIAS1 => 'main',     FIELD1 => $sb->{'primary_key'},
361                ALIAS2 => $sb->{_sql_linkalias}, FIELD2 => 'LocalTarget');
362
363   } else {
364     die "Invalid link direction '$meta->[1]' for $field\n";
365   }
366
367   $sb->_CloseParen();
368
369 }
370
371 =head2 _DateLimit
372
373 Handle date fields.  (Created, LastTold..)
374
375 Meta Data:
376   1: type of link.  (Probably not necessary.)
377
378 =cut
379
380 sub _DateLimit {
381   my ($sb,$field,$op,$value,@rest) = @_;
382
383   die "Invalid Date Op: $op"
384      unless $op =~ /^(=|>|<|>=|<=)$/;
385
386   my $meta = $FIELDS{$field};
387   die "Incorrect Meta Data for $field"
388     unless (defined $meta->[1]);
389
390   require Time::ParseDate;
391   use POSIX 'strftime';
392
393   # FIXME: Replace me with RT::Date( Type => 'unknown' ...)
394   my $time = Time::ParseDate::parsedate( $value,
395                         UK => $RT::DateDayBeforeMonth,
396                         PREFER_PAST => $RT::AmbiguousDayInPast,
397                         PREFER_FUTURE => !($RT::AmbiguousDayInPast),
398                         FUZZY => 1
399                                        );
400
401   if ($op eq "=") {
402     # if we're specifying =, that means we want everything on a
403     # particular single day.  in the database, we need to check for >
404     # and < the edges of that day.
405
406     my $daystart = strftime("%Y-%m-%d %H:%M",
407                             gmtime($time - ( $time % 86400 )));
408     my $dayend   = strftime("%Y-%m-%d %H:%M",
409                             gmtime($time + ( 86399 - $time % 86400 )));
410
411     $sb-> _OpenParen;
412
413     $sb->_SQLLimit(
414                    FIELD => $meta->[1],
415                    OPERATOR => ">=",
416                    VALUE => $daystart,
417                    @rest,
418                   );
419
420     $sb->_SQLLimit(
421                    FIELD => $meta->[1],
422                    OPERATOR => "<=",
423                    VALUE => $dayend,
424                    @rest,
425                    ENTRYAGGREGATOR => 'AND',
426                   );
427
428     $sb-> _CloseParen;
429
430   } else {
431     $value = strftime("%Y-%m-%d %H:%M", gmtime($time));
432     $sb->_SQLLimit(
433                    FIELD => $meta->[1],
434                    OPERATOR => $op,
435                    VALUE => $value,
436                    @rest,
437                   );
438   }
439 }
440
441 =head2 _StringLimit
442
443 Handle simple fields which are just strings.  (Subject,Type)
444
445 Meta Data:
446   None
447
448 =cut
449
450 sub _StringLimit {
451   my ($sb,$field,$op,$value,@rest) = @_;
452
453   # FIXME:
454   # Valid Operators:
455   #  =, !=, LIKE, NOT LIKE
456
457   $sb->_SQLLimit(
458              FIELD => $field,
459              OPERATOR => $op,
460              VALUE => $value,
461              CASESENSITIVE => 0,
462              @rest,
463             );
464 }
465
466 =head2 _TransDateLimit
467
468 Handle fields limiting based on Transaction Date.
469
470 The inpupt value must be in a format parseable by Time::ParseDate
471
472 Meta Data:
473   None
474
475 =cut
476
477 sub _TransDateLimit {
478   my ($sb,$field,$op,$value,@rest) = @_;
479
480   # See the comments for TransLimit, they apply here too
481
482   $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
483     unless defined $sb->{_sql_transalias};
484   $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
485     unless defined $sb->{_sql_trattachalias};
486
487   $sb->_OpenParen;
488
489   # Join Transactions To Attachments
490   $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
491              ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
492
493   # Join Transactions to Tickets
494   $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
495              ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
496
497   my $d = new RT::Date( $sb->CurrentUser );
498   $d->Set( Format => 'ISO', Value => $value);
499    $value = $d->ISO;
500
501   #Search for the right field
502   $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
503                  FIELD =>    'Created',
504                  OPERATOR => $op,
505                  VALUE =>    $value,
506                  CASESENSITIVE => 0,
507                  @rest
508                 );
509
510   $sb->_CloseParen;
511 }
512
513 =head2 _TransLimit
514
515 Limit based on the Content of a transaction or the ContentType.
516
517 Meta Data:
518   none
519
520 =cut
521
522 sub _TransLimit {
523   # Content, ContentType, Filename
524
525   # If only this was this simple.  We've got to do something
526   # complicated here:
527
528             #Basically, we want to make sure that the limits apply to
529             #the same attachment, rather than just another attachment
530             #for the same ticket, no matter how many clauses we lump
531             #on. We put them in TicketAliases so that they get nuked
532             #when we redo the join.
533
534   # In the SQL, we might have
535   #       (( Content = foo ) or ( Content = bar AND Content = baz ))
536   # The AND group should share the same Alias.
537
538   # Actually, maybe it doesn't matter.  We use the same alias and it
539   # works itself out? (er.. different.)
540
541   # Steal more from _ProcessRestrictions
542
543   # FIXME: Maybe look at the previous FooLimit call, and if it was a
544   # TransLimit and EntryAggregator == AND, reuse the Aliases?
545
546   # Or better - store the aliases on a per subclause basis - since
547   # those are going to be the things we want to relate to each other,
548   # anyway.
549
550   # maybe we should not allow certain kinds of aggregation of these
551   # clauses and do a psuedo regex instead? - the problem is getting
552   # them all into the same subclause when you have (A op B op C) - the
553   # way they get parsed in the tree they're in different subclauses.
554
555   my ($sb,$field,$op,$value,@rest) = @_;
556
557   $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
558     unless defined $sb->{_sql_transalias};
559   $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
560     unless defined $sb->{_sql_trattachalias};
561
562   $sb->_OpenParen;
563
564   #Search for the right field
565   $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
566                  FIELD =>    $field,
567                  OPERATOR => $op,
568                  VALUE =>    $value,
569                  CASESENSITIVE => 0,
570                  @rest
571                 );
572
573   # Join Transactions To Attachments
574   $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
575              ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
576
577   # Join Transactions to Tickets
578   $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
579              ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
580
581   $sb->_CloseParen;
582
583 }
584
585 =head2 _WatcherLimit
586
587 Handle watcher limits.  (Requestor, CC, etc..)
588
589 Meta Data:
590   1: Field to query on
591
592
593 =begin testing
594
595 # Test to make sure that you can search for tickets by requestor address and
596 # by requestor name.
597
598 my ($id,$msg);
599 my $u1 = RT::User->new($RT::SystemUser);
600 ($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
601 ok ($id,$msg);
602 my $u2 = RT::User->new($RT::SystemUser);
603 ($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
604 ok ($id,$msg);
605
606 my $t1 = RT::Ticket->new($RT::SystemUser);
607 my ($trans);
608 ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
609 ok ($id, $msg);
610
611 my $t2 = RT::Ticket->new($RT::SystemUser);
612 ($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
613 ok ($id, $msg);
614
615
616 my $t3 = RT::Ticket->new($RT::SystemUser);
617 ($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
618 ok ($id, $msg);
619
620
621 my $tix1 = RT::Tickets->new($RT::SystemUser);
622 $tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
623
624 is ($tix1->Count, 3);
625
626 my $tix2 = RT::Tickets->new($RT::SystemUser);
627 $tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
628
629 is ($tix2->Count, 3);
630
631
632 my $tix3 = RT::Tickets->new($RT::SystemUser);
633 $tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
634
635 is ($tix3->Count, 2);
636
637 my $tix4 = RT::Tickets->new($RT::SystemUser);
638 $tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
639
640 is ($tix4->Count, 2);
641
642 # Searching for tickets that have two requestors isn't supported
643 # There's no way to differentiate "one requestor name that matches foo and bar"
644 # and "two requestors, one matching foo and one matching bar"
645
646 # my $tix5 = RT::Tickets->new($RT::SystemUser);
647 # $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
648
649 # is ($tix5->Count, 1);
650
651 # my $tix6 = RT::Tickets->new($RT::SystemUser);
652 # $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
653
654 # is ($tix6->Count, 1);
655
656
657 =end testing
658
659 =cut
660
661 sub _WatcherLimit {
662     my $self  = shift;
663     my $field = shift;
664     my $op    = shift;
665     my $value = shift;
666     my %rest  = (@_);
667
668     $self->_OpenParen;
669
670     my $groups       = $self->NewAlias('Groups');
671     my $groupmembers = $self->NewAlias('CachedGroupMembers');
672     my $users        = $self->NewAlias('Users');
673
674     # If we're looking for multiple watchers of a given type,
675     # TicketSQL will be handing it to us as an array of cluases in
676     # $field
677     if ( ref $field ) {    # gross hack
678         $self->_OpenParen;
679         for my $chunk (@$field) {
680             ( $field, $op, $value, %rest ) = @$chunk;
681             $self->_SQLLimit(
682                 ALIAS         => $users,
683                 FIELD         => $rest{SUBKEY} || 'EmailAddress',
684                 VALUE         => $value,
685                 OPERATOR      => $op,
686                 CASESENSITIVE => 0,
687                 %rest
688             );
689         }
690         $self->_CloseParen;
691     }
692     else {
693         $self->_SQLLimit(
694             ALIAS         => $users,
695             FIELD         => $rest{SUBKEY} || 'EmailAddress',
696             VALUE         => $value,
697             OPERATOR      => $op,
698             CASESENSITIVE => 0,
699             %rest,
700         );
701     }
702
703     # {{{ Tie to groups for tickets we care about
704     $self->_SQLLimit(
705         ALIAS           => $groups,
706         FIELD           => 'Domain',
707         VALUE           => 'RT::Ticket-Role',
708         ENTRYAGGREGATOR => 'AND'
709     );
710
711     $self->_SQLJoin(
712         ALIAS1 => $groups,
713         FIELD1 => 'Instance',
714         ALIAS2 => 'main',
715         FIELD2 => 'id'
716     );
717
718     # }}}
719
720     # If we care about which sort of watcher
721     my $meta = $FIELDS{$field};
722     my $type = ( defined $meta->[1] ? $meta->[1] : undef );
723
724     if ($type) {
725         $self->_SQLLimit(
726             ALIAS           => $groups,
727             FIELD           => 'Type',
728             VALUE           => $type,
729             ENTRYAGGREGATOR => 'AND'
730         );
731     }
732
733     $self->_SQLJoin(
734         ALIAS1 => $groups,
735         FIELD1 => 'id',
736         ALIAS2 => $groupmembers,
737         FIELD2 => 'GroupId'
738     );
739
740     $self->_SQLJoin(
741         ALIAS1 => $groupmembers,
742         FIELD1 => 'MemberId',
743         ALIAS2 => $users,
744         FIELD2 => 'id'
745     );
746
747     $self->_CloseParen;
748
749 }
750
751 sub _LinkFieldLimit {
752   my $restriction;
753   my $self;
754   my $LinkAlias;
755   my %args;
756   if ($restriction->{'TYPE'}) {
757     $self->SUPER::Limit(ALIAS => $LinkAlias,
758                         ENTRYAGGREGATOR => 'AND',
759                         FIELD =>   'Type',
760                         OPERATOR => '=',
761                         VALUE =>    $restriction->{'TYPE'} );
762   }
763
764    #If we're trying to limit it to things that are target of
765   if ($restriction->{'TARGET'}) {
766     # If the TARGET is an integer that means that we want to look at
767     # the LocalTarget field. otherwise, we want to look at the
768     # "Target" field
769     my ($matchfield);
770     if ($restriction->{'TARGET'} =~/^(\d+)$/) {
771       $matchfield = "LocalTarget";
772     } else {
773       $matchfield = "Target";
774     }
775     $self->SUPER::Limit(ALIAS => $LinkAlias,
776                         ENTRYAGGREGATOR => 'AND',
777                         FIELD =>   $matchfield,
778                         OPERATOR => '=',
779                         VALUE =>    $restriction->{'TARGET'} );
780     #If we're searching on target, join the base to ticket.id
781     $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
782                  ALIAS2 => $LinkAlias,
783                  FIELD2 => 'LocalBase');
784   }
785   #If we're trying to limit it to things that are base of
786   elsif ($restriction->{'BASE'}) {
787     # If we're trying to match a numeric link, we want to look at
788     # LocalBase, otherwise we want to look at "Base"
789     my ($matchfield);
790     if ($restriction->{'BASE'} =~/^(\d+)$/) {
791       $matchfield = "LocalBase";
792     } else {
793       $matchfield = "Base";
794     }
795
796     $self->SUPER::Limit(ALIAS => $LinkAlias,
797                         ENTRYAGGREGATOR => 'AND',
798                         FIELD => $matchfield,
799                         OPERATOR => '=',
800                         VALUE =>    $restriction->{'BASE'} );
801     #If we're searching on base, join the target to ticket.id
802     $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
803                  ALIAS2 => $LinkAlias,
804                  FIELD2 => 'LocalTarget')
805   }
806 }
807
808
809 =head2 KeywordLimit
810
811 Limit based on Keywords
812
813 Meta Data:
814   none
815
816 =cut
817
818 sub _CustomFieldLimit {
819   my ($self,$_field,$op,$value,@rest) = @_;
820
821   my %rest = @rest;
822   my $field = $rest{SUBKEY} || die "No field specified";
823
824   # For our sanity, we can only limit on one queue at a time
825   my $queue = 0;
826
827
828   if ($field =~ /^(.+?)\.{(.+)}$/) {
829     $queue =  $1;
830     $field = $2;
831    }
832     $field = $1 if $field =~ /^{(.+)}$/; # trim { }
833
834     my $q = RT::Queue->new($self->CurrentUser);
835     $q->Load($queue) if ($queue);
836
837     my $cf;
838     if ($q->id) {
839         $cf = $q->CustomField($field);
840     }
841     else { 
842         $cf = RT::CustomField->new($self->CurrentUser);
843         $cf->LoadByNameAndQueue(Queue => '0', Name => $field);
844     }
845
846
847
848
849
850   my $cfid = $cf->id;
851
852   die "No custom field named $field found\n" unless $cfid;
853
854
855
856   my $null_columns_ok;
857
858   my $TicketCFs;
859   # Perform one Join per CustomField
860   if ($self->{_sql_keywordalias}{$cfid}) {
861     $TicketCFs = $self->{_sql_keywordalias}{$cfid};
862   } else {
863     $TicketCFs = $self->{_sql_keywordalias}{$cfid} =
864       $self->_SQLJoin( TYPE   => 'left',
865                    ALIAS1 => 'main',
866                    FIELD1 => 'id',
867                    TABLE2 => 'TicketCustomFieldValues',
868                    FIELD2 => 'Ticket' );
869   }
870
871   $self->_OpenParen;
872
873   $self->_SQLLimit( ALIAS      => $TicketCFs,
874                     FIELD      => 'Content',
875                     OPERATOR   => $op,
876                     VALUE      => $value,
877                     QUOTEVALUE => 1,
878                     @rest );
879
880
881    # If we're trying to find custom fields that don't match something, we want tickets
882    # where the custom field has no value at all
883
884   if (   ($op =~ /^IS$/i) || ($op =~ /^NOT LIKE$/i) || ( $op eq '!=' ) ) {
885     $null_columns_ok = 1;
886   }
887     
888
889   if ( $null_columns_ok && $op !~ /IS/i && uc $value ne "NULL") {
890     $self->_SQLLimit( ALIAS           => $TicketCFs,
891                       FIELD           => 'Content',
892                       OPERATOR        => 'IS',
893                       VALUE           => 'NULL',
894                       QUOTEVALUE      => 0,
895                       ENTRYAGGREGATOR => 'OR', );
896   }
897
898   $self->_SQLLimit( LEFTJOIN => $TicketCFs,
899                     FIELD    => 'CustomField',
900                     VALUE    => $cfid,
901                     ENTRYAGGREGATOR => 'OR' );
902
903
904
905   $self->_CloseParen;
906
907 }
908
909
910 # End Helper Functions
911
912 # End of SQL Stuff -------------------------------------------------
913
914 # {{{ Limit the result set based on content
915
916 # {{{ sub Limit
917
918 =head2 Limit
919
920 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
921 Generally best called from LimitFoo methods
922
923 =cut
924 sub Limit {
925     my $self = shift;
926     my %args = ( FIELD => undef,
927                  OPERATOR => '=',
928                  VALUE => undef,
929                  DESCRIPTION => undef,
930                  @_
931                );
932     $args{'DESCRIPTION'} = $self->loc(
933         "[_1] [_2] [_3]", $args{'FIELD'}, $args{'OPERATOR'}, $args{'VALUE'}
934     ) if (!defined $args{'DESCRIPTION'}) ;
935
936     my $index = $self->_NextIndex;
937
938     #make the TicketRestrictions hash the equivalent of whatever we just passed in;
939
940     %{$self->{'TicketRestrictions'}{$index}} = %args;
941
942     $self->{'RecalcTicketLimits'} = 1;
943
944     # If we're looking at the effective id, we don't want to append the other clause
945     # which limits us to tickets where id = effective id
946     if ($args{'FIELD'} eq 'EffectiveId') {
947         $self->{'looking_at_effective_id'} = 1;
948     }
949
950     if ($args{'FIELD'} eq 'Type') {
951         $self->{'looking_at_type'} = 1;
952     }
953
954     return ($index);
955 }
956
957 # }}}
958
959
960
961
962 =head2 FreezeLimits
963
964 Returns a frozen string suitable for handing back to ThawLimits.
965
966 =cut
967
968 sub _FreezeThawKeys {
969     'TicketRestrictions',
970     'restriction_index',
971     'looking_at_effective_id',
972     'looking_at_type'
973 }
974
975 # {{{ sub FreezeLimits
976
977 sub FreezeLimits {
978         my $self = shift;
979         require FreezeThaw;
980         return (FreezeThaw::freeze(@{$self}{$self->_FreezeThawKeys}));
981 }
982
983 # }}}
984
985 =head2 ThawLimits
986
987 Take a frozen Limits string generated by FreezeLimits and make this tickets
988 object have that set of limits.
989
990 =cut
991 # {{{ sub ThawLimits
992
993 sub ThawLimits {
994         my $self = shift;
995         my $in = shift;
996         
997         #if we don't have $in, get outta here.
998         return undef unless ($in);
999
1000         $self->{'RecalcTicketLimits'} = 1;
1001
1002         require FreezeThaw;
1003         
1004         #We don't need to die if the thaw fails.
1005         
1006         eval {
1007                 @{$self}{$self->_FreezeThawKeys} = FreezeThaw::thaw($in);
1008         };
1009         $RT::Logger->error( $@ ) if $@;
1010
1011 }
1012
1013 # }}}
1014
1015 # {{{ Limit by enum or foreign key
1016
1017 # {{{ sub LimitQueue
1018
1019 =head2 LimitQueue
1020
1021 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1022 OPERATOR is one of = or !=. (It defaults to =).
1023 VALUE is a queue id or Name.
1024
1025
1026 =cut
1027
1028 sub LimitQueue {
1029     my $self = shift;
1030     my %args = (VALUE => undef,
1031                 OPERATOR => '=',
1032                 @_);
1033
1034     #TODO  VALUE should also take queue names and queue objects
1035     #TODO FIXME why are we canonicalizing to name, not id, robrt?
1036     if ($args{VALUE} =~ /^\d+$/) {
1037       my $queue = new RT::Queue($self->CurrentUser);
1038       $queue->Load($args{'VALUE'});
1039       $args{VALUE} = $queue->Name;
1040     }
1041
1042     # What if they pass in an Id?  Check for isNum() and convert to
1043     # string.
1044
1045     #TODO check for a valid queue here
1046
1047     $self->Limit (FIELD => 'Queue',
1048                   VALUE => $args{VALUE},
1049                   OPERATOR => $args{'OPERATOR'},
1050                   DESCRIPTION => join(
1051                    ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE},
1052                   ),
1053                  );
1054
1055 }
1056 # }}}
1057
1058 # {{{ sub LimitStatus
1059
1060 =head2 LimitStatus
1061
1062 Takes a paramhash with the fields OPERATOR and VALUE.
1063 OPERATOR is one of = or !=.
1064 VALUE is a status.
1065
1066 =cut
1067
1068 sub LimitStatus {
1069     my $self = shift;
1070     my %args = ( OPERATOR => '=',
1071                   @_);
1072     $self->Limit (FIELD => 'Status',
1073                   VALUE => $args{'VALUE'},
1074                   OPERATOR => $args{'OPERATOR'},
1075                   DESCRIPTION => join(
1076                    ' ', $self->loc('Status'), $args{'OPERATOR'}, $self->loc($args{'VALUE'})
1077                   ),
1078                  );
1079 }
1080
1081 # }}}
1082
1083 # {{{ sub IgnoreType
1084
1085 =head2 IgnoreType
1086
1087 If called, this search will not automatically limit the set of results found
1088 to tickets of type "Ticket". Tickets of other types, such as "project" and
1089 "approval" will be found.
1090
1091 =cut
1092
1093 sub IgnoreType {
1094     my $self = shift;
1095
1096     # Instead of faking a Limit that later gets ignored, fake up the
1097     # fact that we're already looking at type, so that the check in
1098     # Tickets_Overlay_SQL/FromSQL goes down the right branch
1099
1100     #  $self->LimitType(VALUE => '__any');
1101     $self->{looking_at_type} = 1;
1102 }
1103
1104 # }}}
1105
1106 # {{{ sub LimitType
1107
1108 =head2 LimitType
1109
1110 Takes a paramhash with the fields OPERATOR and VALUE.
1111 OPERATOR is one of = or !=, it defaults to "=".
1112 VALUE is a string to search for in the type of the ticket.
1113
1114
1115
1116 =cut
1117
1118 sub LimitType {
1119     my $self = shift;
1120     my %args = (OPERATOR => '=',
1121                 VALUE => undef,
1122                 @_);
1123     $self->Limit (FIELD => 'Type',
1124                   VALUE => $args{'VALUE'},
1125                   OPERATOR => $args{'OPERATOR'},
1126                   DESCRIPTION => join(
1127                    ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'},
1128                   ),
1129                  );
1130 }
1131
1132 # }}}
1133
1134 # }}}
1135
1136 # {{{ Limit by string field
1137
1138 # {{{ sub LimitSubject
1139
1140 =head2 LimitSubject
1141
1142 Takes a paramhash with the fields OPERATOR and VALUE.
1143 OPERATOR is one of = or !=.
1144 VALUE is a string to search for in the subject of the ticket.
1145
1146 =cut
1147
1148 sub LimitSubject {
1149     my $self = shift;
1150     my %args = (@_);
1151     $self->Limit (FIELD => 'Subject',
1152                   VALUE => $args{'VALUE'},
1153                   OPERATOR => $args{'OPERATOR'},
1154                   DESCRIPTION => join(
1155                    ' ', $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'},
1156                   ),
1157                  );
1158 }
1159
1160 # }}}
1161
1162 # }}}
1163
1164 # {{{ Limit based on ticket numerical attributes
1165 # Things that can be > < = !=
1166
1167 # {{{ sub LimitId
1168
1169 =head2 LimitId
1170
1171 Takes a paramhash with the fields OPERATOR and VALUE.
1172 OPERATOR is one of =, >, < or !=.
1173 VALUE is a ticket Id to search for
1174
1175 =cut
1176
1177 sub LimitId {
1178     my $self = shift;
1179     my %args = (OPERATOR => '=',
1180                 @_);
1181
1182     $self->Limit (FIELD => 'id',
1183                   VALUE => $args{'VALUE'},
1184                   OPERATOR => $args{'OPERATOR'},
1185                   DESCRIPTION => join(
1186                    ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'},
1187                   ),
1188                  );
1189 }
1190
1191 # }}}
1192
1193 # {{{ sub LimitPriority
1194
1195 =head2 LimitPriority
1196
1197 Takes a paramhash with the fields OPERATOR and VALUE.
1198 OPERATOR is one of =, >, < or !=.
1199 VALUE is a value to match the ticket\'s priority against
1200
1201 =cut
1202
1203 sub LimitPriority {
1204     my $self = shift;
1205     my %args = (@_);
1206     $self->Limit (FIELD => 'Priority',
1207                   VALUE => $args{'VALUE'},
1208                   OPERATOR => $args{'OPERATOR'},
1209                   DESCRIPTION => join(
1210                    ' ', $self->loc('Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1211                   ),
1212                  );
1213 }
1214
1215 # }}}
1216
1217 # {{{ sub LimitInitialPriority
1218
1219 =head2 LimitInitialPriority
1220
1221 Takes a paramhash with the fields OPERATOR and VALUE.
1222 OPERATOR is one of =, >, < or !=.
1223 VALUE is a value to match the ticket\'s initial priority against
1224
1225
1226 =cut
1227
1228 sub LimitInitialPriority {
1229     my $self = shift;
1230     my %args = (@_);
1231     $self->Limit (FIELD => 'InitialPriority',
1232                   VALUE => $args{'VALUE'},
1233                   OPERATOR => $args{'OPERATOR'},
1234                   DESCRIPTION => join(
1235                    ' ', $self->loc('Initial Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1236                   ),
1237                  );
1238 }
1239
1240 # }}}
1241
1242 # {{{ sub LimitFinalPriority
1243
1244 =head2 LimitFinalPriority
1245
1246 Takes a paramhash with the fields OPERATOR and VALUE.
1247 OPERATOR is one of =, >, < or !=.
1248 VALUE is a value to match the ticket\'s final priority against
1249
1250 =cut
1251
1252 sub LimitFinalPriority {
1253     my $self = shift;
1254     my %args = (@_);
1255     $self->Limit (FIELD => 'FinalPriority',
1256                   VALUE => $args{'VALUE'},
1257                   OPERATOR => $args{'OPERATOR'},
1258                   DESCRIPTION => join(
1259                    ' ', $self->loc('Final Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1260                   ),
1261                  );
1262 }
1263
1264 # }}}
1265
1266 # {{{ sub LimitTimeWorked
1267
1268 =head2 LimitTimeWorked
1269
1270 Takes a paramhash with the fields OPERATOR and VALUE.
1271 OPERATOR is one of =, >, < or !=.
1272 VALUE is a value to match the ticket's TimeWorked attribute
1273
1274 =cut
1275
1276 sub LimitTimeWorked {
1277     my $self = shift;
1278     my %args = (@_);
1279     $self->Limit (FIELD => 'TimeWorked',
1280                   VALUE => $args{'VALUE'},
1281                   OPERATOR => $args{'OPERATOR'},
1282                   DESCRIPTION => join(
1283                    ' ', $self->loc('Time worked'), $args{'OPERATOR'}, $args{'VALUE'},
1284                   ),
1285                  );
1286 }
1287
1288 # }}}
1289
1290 # {{{ sub LimitTimeLeft
1291
1292 =head2 LimitTimeLeft
1293
1294 Takes a paramhash with the fields OPERATOR and VALUE.
1295 OPERATOR is one of =, >, < or !=.
1296 VALUE is a value to match the ticket's TimeLeft attribute
1297
1298 =cut
1299
1300 sub LimitTimeLeft {
1301     my $self = shift;
1302     my %args = (@_);
1303     $self->Limit (FIELD => 'TimeLeft',
1304                   VALUE => $args{'VALUE'},
1305                   OPERATOR => $args{'OPERATOR'},
1306                   DESCRIPTION => join(
1307                    ' ', $self->loc('Time left'), $args{'OPERATOR'}, $args{'VALUE'},
1308                   ),
1309                  );
1310 }
1311
1312 # }}}
1313
1314 # }}}
1315
1316 # {{{ Limiting based on attachment attributes
1317
1318 # {{{ sub LimitContent
1319
1320 =head2 LimitContent
1321
1322 Takes a paramhash with the fields OPERATOR and VALUE.
1323 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1324 VALUE is a string to search for in the body of the ticket
1325
1326 =cut
1327 sub LimitContent {
1328     my $self = shift;
1329     my %args = (@_);
1330     $self->Limit (FIELD => 'Content',
1331                   VALUE => $args{'VALUE'},
1332                   OPERATOR => $args{'OPERATOR'},
1333                   DESCRIPTION => join(
1334                    ' ', $self->loc('Ticket content'), $args{'OPERATOR'}, $args{'VALUE'},
1335                   ),
1336                  );
1337 }
1338
1339 # }}}
1340
1341 # {{{ sub LimitFilename
1342
1343 =head2 LimitFilename
1344
1345 Takes a paramhash with the fields OPERATOR and VALUE.
1346 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1347 VALUE is a string to search for in the body of the ticket
1348
1349 =cut
1350 sub LimitFilename {
1351     my $self = shift;
1352     my %args = (@_);
1353     $self->Limit (FIELD => 'Filename',
1354                   VALUE => $args{'VALUE'},
1355                   OPERATOR => $args{'OPERATOR'},
1356                   DESCRIPTION => join(
1357                    ' ', $self->loc('Attachment filename'), $args{'OPERATOR'}, $args{'VALUE'},
1358                   ),
1359                  );
1360 }
1361
1362 # }}}
1363 # {{{ sub LimitContentType
1364
1365 =head2 LimitContentType
1366
1367 Takes a paramhash with the fields OPERATOR and VALUE.
1368 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1369 VALUE is a content type to search ticket attachments for
1370
1371 =cut
1372
1373 sub LimitContentType {
1374     my $self = shift;
1375     my %args = (@_);
1376     $self->Limit (FIELD => 'ContentType',
1377                   VALUE => $args{'VALUE'},
1378                   OPERATOR => $args{'OPERATOR'},
1379                   DESCRIPTION => join(
1380                    ' ', $self->loc('Ticket content type'), $args{'OPERATOR'}, $args{'VALUE'},
1381                   ),
1382                  );
1383 }
1384 # }}}
1385
1386 # }}}
1387
1388 # {{{ Limiting based on people
1389
1390 # {{{ sub LimitOwner
1391
1392 =head2 LimitOwner
1393
1394 Takes a paramhash with the fields OPERATOR and VALUE.
1395 OPERATOR is one of = or !=.
1396 VALUE is a user id.
1397
1398 =cut
1399
1400 sub LimitOwner {
1401     my $self = shift;
1402     my %args = ( OPERATOR => '=',
1403                  @_);
1404
1405     my $owner = new RT::User($self->CurrentUser);
1406     $owner->Load($args{'VALUE'});
1407     # FIXME: check for a valid $owner
1408     $self->Limit (FIELD => 'Owner',
1409                   VALUE => $args{'VALUE'},
1410                   OPERATOR => $args{'OPERATOR'},
1411                   DESCRIPTION => join(
1412                    ' ', $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(),
1413                   ),
1414                  );
1415
1416 }
1417
1418 # }}}
1419
1420 # {{{ Limiting watchers
1421
1422 # {{{ sub LimitWatcher
1423
1424
1425 =head2 LimitWatcher
1426
1427   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1428   OPERATOR is one of =, LIKE, NOT LIKE or !=.
1429   VALUE is a value to match the ticket\'s watcher email addresses against
1430   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1431
1432 =begin testing
1433
1434 my $t1 = RT::Ticket->new($RT::SystemUser);
1435 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
1436
1437 =end testing
1438
1439 =cut
1440
1441 sub LimitWatcher {
1442     my $self = shift;
1443     my %args = ( OPERATOR => '=',
1444                  VALUE => undef,
1445                  TYPE => undef,
1446                 @_);
1447
1448
1449     #build us up a description
1450     my ($watcher_type, $desc);
1451     if ($args{'TYPE'}) {
1452         $watcher_type = $args{'TYPE'};
1453     }
1454     else {
1455         $watcher_type = "Watcher";
1456     }
1457
1458     $self->Limit (FIELD => $watcher_type,
1459                   VALUE => $args{'VALUE'},
1460                   OPERATOR => $args{'OPERATOR'},
1461                   TYPE => $args{'TYPE'},
1462                   DESCRIPTION => join(
1463                    ' ', $self->loc($watcher_type), $args{'OPERATOR'}, $args{'VALUE'},
1464                   ),
1465                  );
1466 }
1467
1468
1469 sub LimitRequestor {
1470     my $self = shift;
1471     my %args = (@_);
1472   my ($package, $filename, $line) = caller;
1473     $RT::Logger->error("Tickets->LimitRequestor is deprecated. please rewrite call at  $package - $filename: $line");
1474     $self->LimitWatcher(TYPE => 'Requestor', @_);
1475
1476 }
1477
1478 # }}}
1479
1480
1481 # }}}
1482
1483 # }}}
1484
1485 # {{{ Limiting based on links
1486
1487 # {{{ LimitLinkedTo
1488
1489 =head2 LimitLinkedTo
1490
1491 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1492 TYPE limits the sort of link we want to search on
1493
1494 TYPE = { RefersTo, MemberOf, DependsOn }
1495
1496 TARGET is the id or URI of the TARGET of the link
1497 (TARGET used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as TARGET
1498
1499 =cut
1500
1501 sub LimitLinkedTo {
1502     my $self = shift;
1503     my %args = (
1504                 TICKET => undef,
1505                 TARGET => undef,
1506                 TYPE => undef,
1507                  @_);
1508
1509     $self->Limit(
1510                  FIELD => 'LinkedTo',
1511                  BASE => undef,
1512                  TARGET => ($args{'TARGET'} || $args{'TICKET'}),
1513                  TYPE => $args{'TYPE'},
1514                  DESCRIPTION => $self->loc(
1515                    "Tickets [_1] by [_2]", $self->loc($args{'TYPE'}), ($args{'TARGET'} || $args{'TICKET'})
1516                   ),
1517                 );
1518 }
1519
1520
1521 # }}}
1522
1523 # {{{ LimitLinkedFrom
1524
1525 =head2 LimitLinkedFrom
1526
1527 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1528 TYPE limits the sort of link we want to search on
1529
1530
1531 BASE is the id or URI of the BASE of the link
1532 (BASE used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as BASE
1533
1534
1535 =cut
1536
1537 sub LimitLinkedFrom {
1538     my $self = shift;
1539     my %args = ( BASE => undef,
1540                  TICKET => undef,
1541                  TYPE => undef,
1542                  @_);
1543
1544     # translate RT2 From/To naming to RT3 TicketSQL naming
1545     my %fromToMap = qw(DependsOn DependentOn
1546                        MemberOf  HasMember
1547                        RefersTo  ReferredToBy);
1548
1549     my $type = $args{'TYPE'};
1550     $type = $fromToMap{$type} if exists($fromToMap{$type});
1551
1552     $self->Limit( FIELD => 'LinkedTo',
1553                   TARGET => undef,
1554                   BASE => ($args{'BASE'} || $args{'TICKET'}),
1555                   TYPE => $type,
1556                   DESCRIPTION => $self->loc(
1557                    "Tickets [_1] [_2]", $self->loc($args{'TYPE'}), ($args{'BASE'} || $args{'TICKET'})
1558                   ),
1559                 );
1560 }
1561
1562
1563 # }}}
1564
1565 # {{{ LimitMemberOf
1566 sub LimitMemberOf {
1567     my $self = shift;
1568     my $ticket_id = shift;
1569     $self->LimitLinkedTo ( TARGET=> "$ticket_id",
1570                            TYPE => 'MemberOf',
1571                           );
1572
1573 }
1574 # }}}
1575
1576 # {{{ LimitHasMember
1577 sub LimitHasMember {
1578     my $self = shift;
1579     my $ticket_id =shift;
1580     $self->LimitLinkedFrom ( BASE => "$ticket_id",
1581                              TYPE => 'HasMember',
1582                              );
1583
1584 }
1585 # }}}
1586
1587 # {{{ LimitDependsOn
1588
1589 sub LimitDependsOn {
1590     my $self = shift;
1591     my $ticket_id = shift;
1592     $self->LimitLinkedTo ( TARGET => "$ticket_id",
1593                            TYPE => 'DependsOn',
1594                            );
1595
1596 }
1597
1598 # }}}
1599
1600 # {{{ LimitDependedOnBy
1601
1602 sub LimitDependedOnBy {
1603     my $self = shift;
1604     my $ticket_id = shift;
1605     $self->LimitLinkedFrom (  BASE => "$ticket_id",
1606                                TYPE => 'DependentOn',
1607                              );
1608
1609 }
1610
1611 # }}}
1612
1613
1614 # {{{ LimitRefersTo
1615
1616 sub LimitRefersTo {
1617     my $self = shift;
1618     my $ticket_id = shift;
1619     $self->LimitLinkedTo ( TARGET => "$ticket_id",
1620                            TYPE => 'RefersTo',
1621                            );
1622
1623 }
1624
1625 # }}}
1626
1627 # {{{ LimitReferredToBy
1628
1629 sub LimitReferredToBy {
1630     my $self = shift;
1631     my $ticket_id = shift;
1632     $self->LimitLinkedFrom (  BASE=> "$ticket_id",
1633                                TYPE => 'ReferredTo',
1634                              );
1635
1636 }
1637
1638 # }}}
1639
1640 # }}}
1641
1642 # {{{ limit based on ticket date attribtes
1643
1644 # {{{ sub LimitDate
1645
1646 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
1647
1648 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
1649
1650 OPERATOR is one of > or <
1651 VALUE is a date and time in ISO format in GMT
1652 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
1653
1654 There are also helper functions of the form LimitFIELD that eliminate
1655 the need to pass in a FIELD argument.
1656
1657 =cut
1658
1659 sub LimitDate {
1660     my $self = shift;
1661     my %args = (
1662                   FIELD => undef,
1663                   VALUE => undef,
1664                   OPERATOR => undef,
1665
1666                   @_);
1667
1668     #Set the description if we didn't get handed it above
1669     unless ($args{'DESCRIPTION'} ) {
1670         $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
1671     }
1672
1673     $self->Limit (%args);
1674
1675 }
1676
1677 # }}}
1678
1679
1680
1681
1682 sub LimitCreated {
1683     my $self = shift;
1684     $self->LimitDate( FIELD => 'Created', @_);
1685 }
1686 sub LimitDue {
1687     my $self = shift;
1688     $self->LimitDate( FIELD => 'Due', @_);
1689
1690 }
1691 sub LimitStarts {
1692     my $self = shift;
1693     $self->LimitDate( FIELD => 'Starts', @_);
1694
1695 }
1696 sub LimitStarted {
1697     my $self = shift;
1698     $self->LimitDate( FIELD => 'Started', @_);
1699 }
1700 sub LimitResolved {
1701     my $self = shift;
1702     $self->LimitDate( FIELD => 'Resolved', @_);
1703 }
1704 sub LimitTold {
1705     my $self = shift;
1706     $self->LimitDate( FIELD => 'Told', @_);
1707 }
1708 sub LimitLastUpdated {
1709     my $self = shift;
1710     $self->LimitDate( FIELD => 'LastUpdated', @_);
1711 }
1712 #
1713 # {{{ sub LimitTransactionDate
1714
1715 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
1716
1717 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
1718
1719 OPERATOR is one of > or <
1720 VALUE is a date and time in ISO format in GMT
1721
1722
1723 =cut
1724
1725 sub LimitTransactionDate {
1726     my $self = shift;
1727     my %args = (
1728                   FIELD => 'TransactionDate',
1729                   VALUE => undef,
1730                   OPERATOR => undef,
1731
1732                   @_);
1733
1734     #  <20021217042756.GK28744@pallas.fsck.com>
1735     #    "Kill It" - Jesse.
1736
1737     #Set the description if we didn't get handed it above
1738     unless ($args{'DESCRIPTION'} ) {
1739         $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
1740     }
1741
1742     $self->Limit (%args);
1743
1744 }
1745
1746 # }}}
1747
1748 # }}}
1749
1750 # {{{ Limit based on custom fields
1751 # {{{ sub LimitCustomField
1752
1753 =head2 LimitCustomField
1754
1755 Takes a paramhash of key/value pairs with the following keys:
1756
1757 =over 4
1758
1759 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional
1760 parameter QUEUE may also be passed to distinguish the custom field.
1761
1762 =item OPERATOR - The usual Limit operators
1763
1764 =item VALUE - The value to compare against
1765
1766 =back
1767
1768 =cut
1769
1770 sub LimitCustomField {
1771     my $self = shift;
1772     my %args = ( VALUE        => undef,
1773                  CUSTOMFIELD   => undef,
1774                  OPERATOR      => '=',
1775                  DESCRIPTION   => undef,
1776                  FIELD         => 'CustomFieldValue',
1777                  QUOTEVALUE    => 1,
1778                  @_ );
1779
1780     my $CF = RT::CustomField->new( $self->CurrentUser );
1781     if ( $args{CUSTOMFIELD} =~ /^\d+$/) {
1782         $CF->Load( $args{CUSTOMFIELD} );
1783     }
1784     else {
1785         $CF->LoadByNameAndQueue( Name => $args{CUSTOMFIELD}, Queue => $args{QUEUE} );
1786         $args{CUSTOMFIELD} = $CF->Id;
1787     }
1788
1789     #If we are looking to compare with a null value.
1790     if ( $args{'OPERATOR'} =~ /^is$/i ) {
1791       $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has no value.", $CF->Name);
1792     }
1793     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
1794       $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has a value.", $CF->Name);
1795     }
1796
1797     # if we're not looking to compare with a null value
1798     else {
1799         $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] [_2] [_3]",  $CF->Name , $args{OPERATOR} , $args{VALUE});
1800     }
1801
1802     my $q = "";
1803     if ($CF->Queue) {
1804       my $qo = new RT::Queue( $self->CurrentUser );
1805       $qo->load( $CF->Queue );
1806       $q = $qo->Name;
1807     }
1808
1809     my @rest;
1810     @rest = ( ENTRYAGGREGATOR => 'AND' )
1811       if ($CF->Type eq 'SelectMultiple');
1812
1813     $self->Limit( VALUE => $args{VALUE},
1814                   FIELD => "CF.".( $q
1815                              ? $q . ".{" . $CF->Name . "}"
1816                              : $CF->Name
1817                            ),
1818                   OPERATOR => $args{OPERATOR},
1819                   CUSTOMFIELD => 1,
1820                   @rest,
1821                 );
1822
1823
1824     $self->{'RecalcTicketLimits'} = 1;
1825 }
1826
1827 # }}}
1828 # }}}
1829
1830
1831 # {{{ sub _NextIndex
1832
1833 =head2 _NextIndex
1834
1835 Keep track of the counter for the array of restrictions
1836
1837 =cut
1838
1839 sub _NextIndex {
1840     my $self = shift;
1841     return ($self->{'restriction_index'}++);
1842 }
1843 # }}}
1844
1845 # }}}
1846
1847 # {{{ Core bits to make this a DBIx::SearchBuilder object
1848
1849 # {{{ sub _Init
1850 sub _Init  {
1851     my $self = shift;
1852     $self->{'table'} = "Tickets";
1853     $self->{'RecalcTicketLimits'} = 1;
1854     $self->{'looking_at_effective_id'} = 0;
1855     $self->{'looking_at_type'} = 0;
1856     $self->{'restriction_index'} =1;
1857     $self->{'primary_key'} = "id";
1858     delete $self->{'items_array'};
1859     delete $self->{'item_map'};
1860     delete $self->{'columns_to_display'};
1861     $self->SUPER::_Init(@_);
1862
1863     $self->_InitSQL;
1864
1865 }
1866 # }}}
1867
1868 # {{{ sub Count
1869 sub Count {
1870   my $self = shift;
1871   $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1872   return($self->SUPER::Count());
1873 }
1874 # }}}
1875
1876 # {{{ sub CountAll
1877 sub CountAll {
1878   my $self = shift;
1879   $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1880   return($self->SUPER::CountAll());
1881 }
1882 # }}}
1883
1884
1885 # {{{ sub ItemsArrayRef
1886
1887 =head2 ItemsArrayRef
1888
1889 Returns a reference to the set of all items found in this search
1890
1891 =cut
1892
1893 sub ItemsArrayRef {
1894     my $self = shift;
1895     my @items;
1896
1897     unless ( $self->{'items_array'} ) {
1898
1899         my $placeholder = $self->_ItemsCounter;
1900         $self->GotoFirstItem();
1901         while ( my $item = $self->Next ) {
1902             push ( @{ $self->{'items_array'} }, $item );
1903         }
1904         $self->GotoItem($placeholder);
1905         $self->{'items_array'} = $self->ItemsOrderBy($self->{'items_array'});
1906     }
1907     return ( $self->{'items_array'} );
1908 }
1909 # }}}
1910
1911 # {{{ sub Next
1912 sub Next {
1913         my $self = shift;
1914         
1915         $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1916
1917         my $Ticket = $self->SUPER::Next();
1918         if ((defined($Ticket)) and (ref($Ticket))) {
1919
1920             #Make sure we _never_ show deleted tickets
1921             #TODO we should be doing this in the where clause.
1922             #but you can't do multiple clauses on the same field just yet :/
1923
1924             if ($Ticket->__Value('Status') eq 'deleted') {
1925                 return($self->Next());
1926             }
1927             # Since Ticket could be granted with more rights instead
1928             # of being revoked, it's ok if queue rights allow
1929             # ShowTicket.  It seems need another query, but we have
1930             # rights cache in Principal::HasRight.
1931             elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket') ||
1932                    $Ticket->CurrentUserHasRight('ShowTicket')) {
1933                 return($Ticket);
1934             }
1935
1936             #If the user doesn't have the right to show this ticket
1937             else {      
1938                 return($self->Next());
1939             }
1940         }
1941         #if there never was any ticket
1942         else {
1943                 return(undef);
1944         }       
1945
1946 }
1947 # }}}
1948
1949 # }}}
1950
1951 # {{{ Deal with storing and restoring restrictions
1952
1953 # {{{ sub LoadRestrictions
1954
1955 =head2 LoadRestrictions
1956
1957 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
1958 TODO It is not yet implemented
1959
1960 =cut
1961
1962 # }}}
1963
1964 # {{{ sub DescribeRestrictions
1965
1966 =head2 DescribeRestrictions
1967
1968 takes nothing.
1969 Returns a hash keyed by restriction id.
1970 Each element of the hash is currently a one element hash that contains DESCRIPTION which
1971 is a description of the purpose of that TicketRestriction
1972
1973 =cut
1974
1975 sub DescribeRestrictions  {
1976     my $self = shift;
1977
1978     my ($row, %listing);
1979
1980     foreach $row (keys %{$self->{'TicketRestrictions'}}) {
1981         $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
1982     }
1983     return (%listing);
1984 }
1985 # }}}
1986
1987 # {{{ sub RestrictionValues
1988
1989 =head2 RestrictionValues FIELD
1990
1991 Takes a restriction field and returns a list of values this field is restricted
1992 to.
1993
1994 =cut
1995
1996 sub RestrictionValues {
1997     my $self = shift;
1998     my $field = shift;
1999     map $self->{'TicketRestrictions'}{$_}{'VALUE'},
2000       grep {
2001              $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
2002              && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
2003            }
2004         keys %{$self->{'TicketRestrictions'}};
2005 }
2006
2007 # }}}
2008
2009 # {{{ sub ClearRestrictions
2010
2011 =head2 ClearRestrictions
2012
2013 Removes all restrictions irretrievably
2014
2015 =cut
2016
2017 sub ClearRestrictions {
2018     my $self = shift;
2019     delete $self->{'TicketRestrictions'};
2020     $self->{'looking_at_effective_id'} = 0;
2021     $self->{'looking_at_type'} = 0;
2022     $self->{'RecalcTicketLimits'} =1;
2023 }
2024
2025 # }}}
2026
2027 # {{{ sub DeleteRestriction
2028
2029 =head2 DeleteRestriction
2030
2031 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
2032 Removes that restriction from the session's limits.
2033
2034 =cut
2035
2036
2037 sub DeleteRestriction {
2038     my $self = shift;
2039     my $row = shift;
2040     delete $self->{'TicketRestrictions'}{$row};
2041
2042     $self->{'RecalcTicketLimits'} = 1;
2043     #make the underlying easysearch object forget all its preconceptions
2044 }
2045
2046 # }}}
2047
2048 # {{{ sub _RestrictionsToClauses
2049
2050 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
2051
2052 sub _RestrictionsToClauses {
2053   my $self = shift;
2054
2055   my $row;
2056   my %clause;
2057   foreach $row (keys %{$self->{'TicketRestrictions'}}) {
2058     my $restriction = $self->{'TicketRestrictions'}{$row};
2059     #use Data::Dumper;
2060     #print Dumper($restriction),"\n";
2061
2062       # We need to reimplement the subclause aggregation that SearchBuilder does.
2063       # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
2064       # Then SB AND's the different Subclauses together.
2065
2066       # So, we want to group things into Subclauses, convert them to
2067       # SQL, and then join them with the appropriate DefaultEA.
2068       # Then join each subclause group with AND.
2069
2070     my $field = $restriction->{'FIELD'};
2071     my $realfield = $field;     # CustomFields fake up a fieldname, so
2072                                 # we need to figure that out
2073
2074     # One special case
2075     # Rewrite LinkedTo meta field to the real field
2076     if ($field =~ /LinkedTo/) {
2077       $realfield = $field = $restriction->{'TYPE'};
2078     }
2079
2080     # Two special case
2081     # CustomFields have a different real field
2082     if ($field =~ /^CF\./) {
2083       $realfield = "CF"
2084     }
2085
2086     die "I don't know about $field yet"
2087       unless (exists $FIELDS{$realfield} or $restriction->{CUSTOMFIELD});
2088
2089     my $type = $FIELDS{$realfield}->[0];
2090     my $op   = $restriction->{'OPERATOR'};
2091
2092     my $value = ( grep { defined }
2093                   map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET))[0];
2094
2095     # this performs the moral equivalent of defined or/dor/C<//>,
2096     # without the short circuiting.You need to use a 'defined or'
2097     # type thing instead of just checking for truth values, because
2098     # VALUE could be 0.(i.e. "false")
2099
2100     # You could also use this, but I find it less aesthetic:
2101     # (although it does short circuit)
2102     #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2103     # defined $restriction->{'TICKET'} ?
2104     # $restriction->{TICKET} :
2105     # defined $restriction->{'BASE'} ?
2106     # $restriction->{BASE} :
2107     # defined $restriction->{'TARGET'} ?
2108     # $restriction->{TARGET} )
2109
2110     my $ea = $restriction->{ENTRYAGGREGATOR} || $DefaultEA{$type} || "AND";
2111     if ( ref $ea ) {
2112       die "Invalid operator $op for $field ($type)"
2113         unless exists $ea->{$op};
2114       $ea = $ea->{$op};
2115     }
2116
2117     # Each CustomField should be put into a different Clause so they
2118     # are ANDed together.
2119     if ($restriction->{CUSTOMFIELD}) {
2120       $realfield = $field;
2121     }
2122
2123     exists $clause{$realfield} or $clause{$realfield} = [];
2124     # Escape Quotes
2125     $field =~ s!(['"])!\\$1!g;
2126     $value =~ s!(['"])!\\$1!g;
2127     my $data = [ $ea, $type, $field, $op, $value ];
2128
2129     # here is where we store extra data, say if it's a keyword or
2130     # something.  (I.e. "TYPE SPECIFIC STUFF")
2131
2132     #print Dumper($data);
2133     push @{$clause{$realfield}}, $data;
2134   }
2135   return \%clause;
2136 }
2137
2138 # }}}
2139
2140 # {{{ sub _ProcessRestrictions
2141
2142 =head2 _ProcessRestrictions PARAMHASH
2143
2144 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
2145 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
2146
2147 =cut
2148
2149 sub _ProcessRestrictions {
2150     my $self = shift;
2151     
2152     #Blow away ticket aliases since we'll need to regenerate them for
2153     #a new search
2154     delete $self->{'TicketAliases'};
2155     delete $self->{'items_array'};                                                                                                                   
2156     delete $self->{'item_map'};
2157     delete $self->{'raw_rows'};
2158     delete $self->{'rows'};
2159     delete $self->{'count_all'};
2160  
2161     my $sql = $self->{_sql_query}; # Violating the _SQL namespace
2162     if (!$sql||$self->{'RecalcTicketLimits'}) {
2163       #  "Restrictions to Clauses Branch\n";
2164       my $clauseRef = eval { $self->_RestrictionsToClauses; };
2165       if ($@) {
2166         $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2167         $self->FromSQL("");
2168       } else {
2169         $sql = $self->ClausesToSQL($clauseRef);
2170         $self->FromSQL($sql);
2171       }
2172     }
2173
2174
2175     $self->{'RecalcTicketLimits'} = 0;
2176
2177 }
2178
2179 =head2 _BuildItemMap
2180
2181     # Build up a map of first/last/next/prev items, so that we can display search nav quickly
2182
2183 =cut
2184
2185 sub _BuildItemMap {
2186     my $self = shift;
2187
2188     my $items = $self->ItemsArrayRef;
2189     my $prev = 0 ;
2190
2191     delete $self->{'item_map'};
2192     if ($items->[0]) {
2193         $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
2194         while (my $item = shift @$items ) {
2195             my $id = $item->EffectiveId;
2196             $self->{'item_map'}->{$id}->{'defined'} = 1;
2197             $self->{'item_map'}->{$id}->{prev}  = $prev;
2198             $self->{'item_map'}->{$id}->{next}  = $items->[0]->EffectiveId if ($items->[0]);
2199             $prev = $id;
2200         }
2201         $self->{'item_map'}->{'last'} = $prev;
2202     }
2203
2204
2205
2206 =head2 ItemMap
2207
2208 Returns an a map of all items found by this search. The map is of the form
2209
2210 $ItemMap->{'first'} = first ticketid found
2211 $ItemMap->{'last'} = last ticketid found
2212 $ItemMap->{$id}->{prev} = the ticket id found before $id
2213 $ItemMap->{$id}->{next} = the ticket id found after $id
2214
2215 =cut
2216
2217 sub ItemMap {
2218     my $self = shift;
2219     $self->_BuildItemMap() unless ($self->{'items_array'} and $self->{'item_map'});
2220     return ($self->{'item_map'});
2221 }
2222
2223
2224
2225
2226 =cut
2227
2228 }
2229
2230
2231
2232 # }}}
2233
2234 # }}}
2235
2236 =head2 PrepForSerialization
2237
2238 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
2239
2240 =cut
2241
2242
2243 sub PrepForSerialization {
2244     my $self = shift;
2245     delete $self->{'items'};
2246     $self->RedoSearch();
2247 }
2248
2249 1;
2250