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