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