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