import rt 3.6.10
[freeside.git] / rt / lib / RT / Tickets_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC 
6 #                                          <jesse@bestpractical.com>
7
8 # (Except where explicitly superseded by other copyright notices)
9
10
11 # LICENSE:
12
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28
29
30 # CONTRIBUTION SUBMISSION POLICY:
31
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46
47 # END BPS TAGGED BLOCK }}}
48 # Major Changes:
49
50 # - Decimated ProcessRestrictions and broke it into multiple
51 # functions joined by a LUT
52 # - Semi-Generic SQL stuff moved to another file
53
54 # Known Issues: FIXME!
55
56 # - ClearRestrictions and Reinitialization is messy and unclear.  The
57 # only good way to do it is to create a new RT::Tickets object.
58
59 =head1 NAME
60
61   RT::Tickets - A collection of Ticket objects
62
63
64 =head1 SYNOPSIS
65
66   use RT::Tickets;
67   my $tickets = new RT::Tickets($CurrentUser);
68
69 =head1 DESCRIPTION
70
71    A collection of RT::Tickets.
72
73 =head1 METHODS
74
75 =begin testing
76
77 ok (require RT::Tickets);
78 ok( my $testtickets = RT::Tickets->new( $RT::SystemUser ) );
79 ok( $testtickets->LimitStatus( VALUE => 'deleted' ) );
80 # Should be zero until 'allow_deleted_search'
81 ok( $testtickets->Count == 0 );
82
83 =end testing
84
85 =cut
86
87 package RT::Tickets;
88
89 use strict;
90 no warnings qw(redefine);
91
92 use RT::CustomFields;
93 use DBIx::SearchBuilder::Unique;
94
95 # Configuration Tables:
96
97 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
98 # metadata.
99
100 my %FIELD_METADATA = (
101     Status          => [ 'ENUM', ],
102     Queue           => [ 'ENUM' => 'Queue', ],
103     Type            => [ 'ENUM', ],
104     Creator         => [ 'ENUM' => 'User', ],
105     LastUpdatedBy   => [ 'ENUM' => 'User', ],
106     Owner           => [ 'WATCHERFIELD' => 'Owner', ],
107     EffectiveId     => [ 'INT', ],
108     id              => [ 'INT', ],
109     InitialPriority => [ 'INT', ],
110     FinalPriority   => [ 'INT', ],
111     Priority        => [ 'INT', ],
112     TimeLeft        => [ 'INT', ],
113     TimeWorked      => [ 'INT', ],
114     TimeEstimated   => [ 'INT', ],
115     MemberOf        => [ 'LINK' => To => 'MemberOf', ],
116     DependsOn       => [ 'LINK' => To => 'DependsOn', ],
117     RefersTo        => [ 'LINK' => To => 'RefersTo', ],
118     HasMember       => [ 'LINK' => From => 'MemberOf', ],
119     DependentOn     => [ 'LINK' => From => 'DependsOn', ],
120     DependedOnBy    => [ 'LINK' => From => 'DependsOn', ],
121     ReferredToBy    => [ 'LINK' => From => 'RefersTo', ],
122     Told             => [ 'DATE'            => 'Told', ],
123     Starts           => [ 'DATE'            => 'Starts', ],
124     Started          => [ 'DATE'            => 'Started', ],
125     Due              => [ 'DATE'            => 'Due', ],
126     Resolved         => [ 'DATE'            => 'Resolved', ],
127     LastUpdated      => [ 'DATE'            => 'LastUpdated', ],
128     Created          => [ 'DATE'            => 'Created', ],
129     Subject          => [ 'STRING', ],
130     Content          => [ 'TRANSFIELD', ],
131     ContentType      => [ 'TRANSFIELD', ],
132     Filename         => [ 'TRANSFIELD', ],
133     TransactionDate  => [ 'TRANSDATE', ],
134     Requestor        => [ 'WATCHERFIELD'    => 'Requestor', ],
135     Requestors       => [ 'WATCHERFIELD'    => 'Requestor', ],
136     Cc               => [ 'WATCHERFIELD'    => 'Cc', ],
137     AdminCc          => [ 'WATCHERFIELD'    => 'AdminCc', ],
138     Watcher          => [ 'WATCHERFIELD', ],
139     QueueCc          => [ 'WATCHERFIELD'    => 'Cc'      => 'Queue', ],
140     QueueAdminCc     => [ 'WATCHERFIELD'    => 'AdminCc' => 'Queue', ],
141     QueueWatcher     => [ 'WATCHERFIELD'    => undef     => 'Queue', ],
142     LinkedTo         => [ 'LINKFIELD', ],
143     CustomFieldValue => [ 'CUSTOMFIELD', ],
144     CustomField      => [ 'CUSTOMFIELD', ],
145     CF               => [ 'CUSTOMFIELD', ],
146     Updated          => [ 'TRANSDATE', ],
147     RequestorGroup   => [ 'MEMBERSHIPFIELD' => 'Requestor', ],
148     CCGroup          => [ 'MEMBERSHIPFIELD' => 'Cc', ],
149     AdminCCGroup     => [ 'MEMBERSHIPFIELD' => 'AdminCc', ],
150     WatcherGroup     => [ 'MEMBERSHIPFIELD', ],
151     HasAttribute     => [ 'HASATTRIBUTE', 1 ],
152     HasNoAttribute     => [ 'HASATTRIBUTE', 0 ],
153 );
154
155 # Mapping of Field Type to Function
156 my %dispatch = (
157     ENUM            => \&_EnumLimit,
158     INT             => \&_IntLimit,
159     LINK            => \&_LinkLimit,
160     DATE            => \&_DateLimit,
161     STRING          => \&_StringLimit,
162     TRANSFIELD      => \&_TransLimit,
163     TRANSDATE       => \&_TransDateLimit,
164     WATCHERFIELD    => \&_WatcherLimit,
165     MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
166     LINKFIELD       => \&_LinkFieldLimit,
167     CUSTOMFIELD     => \&_CustomFieldLimit,
168     HASATTRIBUTE    => \&_HasAttributeLimit,
169 );
170 my %can_bundle = (); # WATCHERFIELD => "yes", );
171
172 # Default EntryAggregator per type
173 # if you specify OP, you must specify all valid OPs
174 my %DefaultEA = (
175     INT  => 'AND',
176     ENUM => {
177         '='  => 'OR',
178         '!=' => 'AND'
179     },
180     DATE => {
181         '='  => 'OR',
182         '>=' => 'AND',
183         '<=' => 'AND',
184         '>'  => 'AND',
185         '<'  => 'AND'
186     },
187     STRING => {
188         '='        => 'OR',
189         '!='       => 'AND',
190         'LIKE'     => 'AND',
191         'NOT LIKE' => 'AND'
192     },
193     TRANSFIELD   => 'AND',
194     TRANSDATE    => 'AND',
195     LINK         => 'OR',
196     LINKFIELD    => 'AND',
197     TARGET       => 'AND',
198     BASE         => 'AND',
199     WATCHERFIELD => {
200         '='        => 'OR',
201         '!='       => 'AND',
202         'LIKE'     => 'OR',
203         'NOT LIKE' => 'AND'
204     },
205
206     HASATTRIBUTE => {
207         '='        => 'AND',
208         '!='       => 'AND',
209     },
210
211     CUSTOMFIELD => 'OR',
212 );
213
214 # Helper functions for passing the above lexically scoped tables above
215 # into Tickets_Overlay_SQL.
216 sub FIELDS     { return \%FIELD_METADATA }
217 sub dispatch   { return \%dispatch }
218 sub can_bundle { return \%can_bundle }
219
220 # Bring in the clowns.
221 require RT::Tickets_Overlay_SQL;
222
223 # {{{ sub SortFields
224
225 our @SORTFIELDS = qw(id Status
226     Queue Subject
227     Owner Created Due Starts Started
228     Told
229     Resolved LastUpdated Priority TimeWorked TimeLeft);
230
231 =head2 SortFields
232
233 Returns the list of fields that lists of tickets can easily be sorted by
234
235 =cut
236
237 sub SortFields {
238     my $self = shift;
239     return (@SORTFIELDS);
240 }
241
242 # }}}
243
244 # BEGIN SQL STUFF *********************************
245
246
247 sub CleanSlate {
248     my $self = shift;
249     $self->SUPER::CleanSlate( @_ );
250     delete $self->{$_} foreach qw(
251         _sql_cf_alias
252         _sql_group_members_aliases
253         _sql_object_cfv_alias
254         _sql_role_group_aliases
255         _sql_transalias
256         _sql_trattachalias
257         _sql_u_watchers_alias_for_sort
258         _sql_u_watchers_aliases
259     );
260 }
261
262 =head1 Limit Helper Routines
263
264 These routines are the targets of a dispatch table depending on the
265 type of field.  They all share the same signature:
266
267   my ($self,$field,$op,$value,@rest) = @_;
268
269 The values in @rest should be suitable for passing directly to
270 DBIx::SearchBuilder::Limit.
271
272 Essentially they are an expanded/broken out (and much simplified)
273 version of what ProcessRestrictions used to do.  They're also much
274 more clearly delineated by the TYPE of field being processed.
275
276 =head2 _EnumLimit
277
278 Handle Fields which are limited to certain values, and potentially
279 need to be looked up from another class.
280
281 This subroutine actually handles two different kinds of fields.  For
282 some the user is responsible for limiting the values.  (i.e. Status,
283 Type).
284
285 For others, the value specified by the user will be looked by via
286 specified class.
287
288 Meta Data:
289   name of class to lookup in (Optional)
290
291 =cut
292
293 sub _EnumLimit {
294     my ( $sb, $field, $op, $value, @rest ) = @_;
295
296     # SQL::Statement changes != to <>.  (Can we remove this now?)
297     $op = "!=" if $op eq "<>";
298
299     die "Invalid Operation: $op for $field"
300         unless $op eq "="
301         or $op     eq "!=";
302
303     my $meta = $FIELD_METADATA{$field};
304     if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
305         my $class = "RT::" . $meta->[1];
306         my $o     = $class->new( $sb->CurrentUser );
307         $o->Load($value);
308         $value = $o->Id;
309     }
310     $sb->_SQLLimit(
311         FIELD    => $field,
312         VALUE    => $value,
313         OPERATOR => $op,
314         @rest,
315     );
316 }
317
318 =head2 _IntLimit
319
320 Handle fields where the values are limited to integers.  (For example,
321 Priority, TimeWorked.)
322
323 Meta Data:
324   None
325
326 =cut
327
328 sub _IntLimit {
329     my ( $sb, $field, $op, $value, @rest ) = @_;
330
331     die "Invalid Operator $op for $field"
332         unless $op =~ /^(=|!=|>|<|>=|<=)$/;
333
334     $sb->_SQLLimit(
335         FIELD    => $field,
336         VALUE    => $value,
337         OPERATOR => $op,
338         @rest,
339     );
340 }
341
342 =head2 _LinkLimit
343
344 Handle fields which deal with links between tickets.  (MemberOf, DependsOn)
345
346 Meta Data:
347   1: Direction (From, To)
348   2: Link Type (MemberOf, DependsOn, RefersTo)
349
350 =cut
351
352 sub _LinkLimit {
353     my ( $sb, $field, $op, $value, @rest ) = @_;
354
355     my $meta = $FIELD_METADATA{$field};
356     die "Incorrect Metadata for $field"
357         unless defined $meta->[1] && defined $meta->[2];
358
359     die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
360
361     my $direction = $meta->[1];
362
363     my $matchfield;
364     my $linkfield;
365     if ( $direction eq 'To' ) {
366         $matchfield = "Target";
367         $linkfield  = "Base";
368
369     }
370     elsif ( $direction eq 'From' ) {
371         $linkfield  = "Target";
372         $matchfield = "Base";
373
374     }
375     else {
376         die "Invalid link direction '$meta->[1]' for $field\n";
377     }
378
379     my ($is_local, $is_null) = (1, 0);
380     if ( !$value || $value =~ /^null$/io ) {
381         $is_null = 1;
382         $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
383     }
384     elsif ( $value =~ /\D/o ) {
385         $is_local = 0;
386     }
387     $matchfield = "Local$matchfield" if $is_local;
388
389     my $is_negative = 0;
390     if ( $op eq '!=' ) {
391         $is_negative = 1;
392         $op = '=';
393     }
394
395 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
396 #    SELECT main.* FROM Tickets main
397 #        LEFT JOIN Links Links_1 ON (     (Links_1.Type = 'MemberOf')
398 #                                      AND(main.id = Links_1.LocalTarget))
399 #        WHERE Links_1.LocalBase IS NULL;
400
401     if ($is_null) {
402         my $linkalias = $sb->Join(
403             TYPE   => 'LEFT',
404             ALIAS1 => 'main',
405             FIELD1 => 'id',
406             TABLE2 => 'Links',
407             FIELD2 => 'Local' . $linkfield
408         );
409         $sb->SUPER::Limit(
410             LEFTJOIN => $linkalias,
411             FIELD    => 'Type',
412             OPERATOR => '=',
413             VALUE    => $meta->[2],
414         );
415         $sb->_SQLLimit(
416             @rest,
417             ALIAS      => $linkalias,
418             FIELD      => $matchfield,
419             OPERATOR   => $op,
420             VALUE      => 'NULL',
421             QUOTEVALUE => 0,
422         );
423     }
424     elsif ( $is_negative ) {
425         my $linkalias = $sb->Join(
426             TYPE   => 'LEFT',
427             ALIAS1 => 'main',
428             FIELD1 => 'id',
429             TABLE2 => 'Links',
430             FIELD2 => 'Local' . $linkfield
431         );
432         $sb->SUPER::Limit(
433             LEFTJOIN => $linkalias,
434             FIELD    => 'Type',
435             OPERATOR => '=',
436             VALUE    => $meta->[2],
437         );
438         $sb->SUPER::Limit(
439             LEFTJOIN => $linkalias,
440             FIELD    => $matchfield,
441             OPERATOR => $op,
442             VALUE    => $value,
443         );
444         $sb->_SQLLimit(
445             @rest,
446             ALIAS      => $linkalias,
447             FIELD      => $matchfield,
448             OPERATOR   => 'IS',
449             VALUE      => 'NULL',
450             QUOTEVALUE => 0,
451         );
452     }
453     else {
454         my $linkalias = $sb->NewAlias('Links');
455         $sb->_OpenParen();
456         $sb->_SQLLimit(
457             @rest,
458             ALIAS    => $linkalias,
459             FIELD    => 'Type',
460             OPERATOR => '=',
461             VALUE    => $meta->[2],
462         );
463         $sb->_SQLLimit(
464             ALIAS           => $linkalias,
465             FIELD           => 'Local' . $linkfield,
466             OPERATOR        => '=',
467             VALUE           => 'main.id',
468             QUOTEVALUE      => 0,
469             ENTRYAGGREGATOR => 'AND',
470         );
471         $sb->_SQLLimit(
472             ALIAS           => $linkalias,
473             FIELD           => $matchfield,
474             OPERATOR        => $op,
475             VALUE           => $value,
476             ENTRYAGGREGATOR => 'AND',
477         );
478         $sb->_CloseParen();
479     }
480 }
481
482 =head2 _DateLimit
483
484 Handle date fields.  (Created, LastTold..)
485
486 Meta Data:
487   1: type of link.  (Probably not necessary.)
488
489 =cut
490
491 sub _DateLimit {
492     my ( $sb, $field, $op, $value, @rest ) = @_;
493
494     die "Invalid Date Op: $op"
495         unless $op =~ /^(=|>|<|>=|<=)$/;
496
497     my $meta = $FIELD_METADATA{$field};
498     die "Incorrect Meta Data for $field"
499         unless ( defined $meta->[1] );
500
501     my $date = RT::Date->new( $sb->CurrentUser );
502     $date->Set( Format => 'unknown', Value => $value );
503
504     if ( $op eq "=" ) {
505
506         # if we're specifying =, that means we want everything on a
507         # particular single day.  in the database, we need to check for >
508         # and < the edges of that day.
509
510         $date->SetToMidnight( Timezone => 'server' );
511         my $daystart = $date->ISO;
512         $date->AddDay;
513         my $dayend = $date->ISO;
514
515         $sb->_OpenParen;
516
517         $sb->_SQLLimit(
518             FIELD    => $meta->[1],
519             OPERATOR => ">=",
520             VALUE    => $daystart,
521             @rest,
522         );
523
524         $sb->_SQLLimit(
525             FIELD    => $meta->[1],
526             OPERATOR => "<=",
527             VALUE    => $dayend,
528             @rest,
529             ENTRYAGGREGATOR => 'AND',
530         );
531
532         $sb->_CloseParen;
533
534     }
535     else {
536         $sb->_SQLLimit(
537             FIELD    => $meta->[1],
538             OPERATOR => $op,
539             VALUE    => $date->ISO,
540             @rest,
541         );
542     }
543 }
544
545 =head2 _StringLimit
546
547 Handle simple fields which are just strings.  (Subject,Type)
548
549 Meta Data:
550   None
551
552 =cut
553
554 sub _StringLimit {
555     my ( $sb, $field, $op, $value, @rest ) = @_;
556
557     # FIXME:
558     # Valid Operators:
559     #  =, !=, LIKE, NOT LIKE
560
561     $sb->_SQLLimit(
562         FIELD         => $field,
563         OPERATOR      => $op,
564         VALUE         => $value,
565         CASESENSITIVE => 0,
566         @rest,
567     );
568 }
569
570 =head2 _TransDateLimit
571
572 Handle fields limiting based on Transaction Date.
573
574 The inpupt value must be in a format parseable by Time::ParseDate
575
576 Meta Data:
577   None
578
579 =cut
580
581 # This routine should really be factored into translimit.
582 sub _TransDateLimit {
583     my ( $sb, $field, $op, $value, @rest ) = @_;
584
585     # See the comments for TransLimit, they apply here too
586
587     unless ( $sb->{_sql_transalias} ) {
588         $sb->{_sql_transalias} = $sb->Join(
589             ALIAS1 => 'main',
590             FIELD1 => 'id',
591             TABLE2 => 'Transactions',
592             FIELD2 => 'ObjectId',
593         );
594         $sb->SUPER::Limit(
595             ALIAS           => $sb->{_sql_transalias},
596             FIELD           => 'ObjectType',
597             VALUE           => 'RT::Ticket',
598             ENTRYAGGREGATOR => 'AND',
599         );
600     }
601
602     my $date = RT::Date->new( $sb->CurrentUser );
603     $date->Set( Format => 'unknown', Value => $value );
604
605     $sb->_OpenParen;
606     if ( $op eq "=" ) {
607
608         # if we're specifying =, that means we want everything on a
609         # particular single day.  in the database, we need to check for >
610         # and < the edges of that day.
611
612         $date->SetToMidnight( Timezone => 'server' );
613         my $daystart = $date->ISO;
614         $date->AddDay;
615         my $dayend = $date->ISO;
616
617         $sb->_SQLLimit(
618             ALIAS         => $sb->{_sql_transalias},
619             FIELD         => 'Created',
620             OPERATOR      => ">=",
621             VALUE         => $daystart,
622             CASESENSITIVE => 0,
623             @rest
624         );
625         $sb->_SQLLimit(
626             ALIAS         => $sb->{_sql_transalias},
627             FIELD         => 'Created',
628             OPERATOR      => "<=",
629             VALUE         => $dayend,
630             CASESENSITIVE => 0,
631             @rest,
632             ENTRYAGGREGATOR => 'AND',
633         );
634
635     }
636
637     # not searching for a single day
638     else {
639
640         #Search for the right field
641         $sb->_SQLLimit(
642             ALIAS         => $sb->{_sql_transalias},
643             FIELD         => 'Created',
644             OPERATOR      => $op,
645             VALUE         => $date->ISO,
646             CASESENSITIVE => 0,
647             @rest
648         );
649     }
650
651     $sb->_CloseParen;
652 }
653
654 =head2 _TransLimit
655
656 Limit based on the Content of a transaction or the ContentType.
657
658 Meta Data:
659   none
660
661 =cut
662
663 sub _TransLimit {
664
665     # Content, ContentType, Filename
666
667     # If only this was this simple.  We've got to do something
668     # complicated here:
669
670     #Basically, we want to make sure that the limits apply to
671     #the same attachment, rather than just another attachment
672     #for the same ticket, no matter how many clauses we lump
673     #on. We put them in TicketAliases so that they get nuked
674     #when we redo the join.
675
676     # In the SQL, we might have
677     #       (( Content = foo ) or ( Content = bar AND Content = baz ))
678     # The AND group should share the same Alias.
679
680     # Actually, maybe it doesn't matter.  We use the same alias and it
681     # works itself out? (er.. different.)
682
683     # Steal more from _ProcessRestrictions
684
685     # FIXME: Maybe look at the previous FooLimit call, and if it was a
686     # TransLimit and EntryAggregator == AND, reuse the Aliases?
687
688     # Or better - store the aliases on a per subclause basis - since
689     # those are going to be the things we want to relate to each other,
690     # anyway.
691
692     # maybe we should not allow certain kinds of aggregation of these
693     # clauses and do a psuedo regex instead? - the problem is getting
694     # them all into the same subclause when you have (A op B op C) - the
695     # way they get parsed in the tree they're in different subclauses.
696
697     my ( $self, $field, $op, $value, @rest ) = @_;
698
699     unless ( $self->{_sql_transalias} ) {
700         $self->{_sql_transalias} = $self->Join(
701             ALIAS1 => 'main',
702             FIELD1 => 'id',
703             TABLE2 => 'Transactions',
704             FIELD2 => 'ObjectId',
705         );
706         $self->SUPER::Limit(
707             ALIAS           => $self->{_sql_transalias},
708             FIELD           => 'ObjectType',
709             VALUE           => 'RT::Ticket',
710             ENTRYAGGREGATOR => 'AND',
711         );
712     }
713     unless ( defined $self->{_sql_trattachalias} ) {
714         $self->{_sql_trattachalias} = $self->_SQLJoin(
715             TYPE   => 'LEFT', # not all txns have an attachment
716             ALIAS1 => $self->{_sql_transalias},
717             FIELD1 => 'id',
718             TABLE2 => 'Attachments',
719             FIELD2 => 'TransactionId',
720         );
721     }
722
723     $self->_OpenParen;
724
725     #Search for the right field
726     if ($field eq 'Content' and $RT::DontSearchFileAttachments) {
727        $self->_SQLLimit(
728                         ALIAS         => $self->{_sql_trattachalias},
729                         FIELD         => 'Filename',
730                         OPERATOR      => 'IS',
731                         VALUE         => 'NULL',
732                         SUBCLAUSE     => 'contentquery',
733                         ENTRYAGGREGATOR => 'AND',
734                        );
735        $self->_SQLLimit(
736                         ALIAS         => $self->{_sql_trattachalias},
737                         FIELD         => $field,
738                         OPERATOR      => $op,
739                         VALUE         => $value,
740                         CASESENSITIVE => 0,
741                         @rest,
742                         ENTRYAGGREGATOR => 'AND',
743                         SUBCLAUSE     => 'contentquery',
744                        );
745     } else {
746         $self->_SQLLimit(
747                         ALIAS         => $self->{_sql_trattachalias},
748                         FIELD         => $field,
749                         OPERATOR      => $op,
750                         VALUE         => $value,
751                         CASESENSITIVE => 0,
752                         ENTRYAGGREGATOR => 'AND',
753                         @rest
754                 );
755     }
756
757     $self->_CloseParen;
758
759 }
760
761 =head2 _WatcherLimit
762
763 Handle watcher limits.  (Requestor, CC, etc..)
764
765 Meta Data:
766   1: Field to query on
767
768
769 =begin testing
770
771 # Test to make sure that you can search for tickets by requestor address and
772 # by requestor name.
773
774 my ($id,$msg);
775 my $u1 = RT::User->new($RT::SystemUser);
776 ($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
777 ok ($id,$msg);
778 my $u2 = RT::User->new($RT::SystemUser);
779 ($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
780 ok ($id,$msg);
781
782 my $t1 = RT::Ticket->new($RT::SystemUser);
783 my ($trans);
784 ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
785 ok ($id, $msg);
786
787 my $t2 = RT::Ticket->new($RT::SystemUser);
788 ($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
789 ok ($id, $msg);
790
791
792 my $t3 = RT::Ticket->new($RT::SystemUser);
793 ($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
794 ok ($id, $msg);
795
796
797 my $tix1 = RT::Tickets->new($RT::SystemUser);
798 $tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
799
800 is ($tix1->Count, 3);
801
802 my $tix2 = RT::Tickets->new($RT::SystemUser);
803 $tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
804
805 is ($tix2->Count, 3);
806
807
808 my $tix3 = RT::Tickets->new($RT::SystemUser);
809 $tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
810
811 is ($tix3->Count, 2);
812
813 my $tix4 = RT::Tickets->new($RT::SystemUser);
814 $tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
815
816 is ($tix4->Count, 2);
817
818 # Searching for tickets that have two requestors isn't supported
819 # There's no way to differentiate "one requestor name that matches foo and bar"
820 # and "two requestors, one matching foo and one matching bar"
821
822 # my $tix5 = RT::Tickets->new($RT::SystemUser);
823 # $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
824
825 # is ($tix5->Count, 1);
826
827 # my $tix6 = RT::Tickets->new($RT::SystemUser);
828 # $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
829
830 # is ($tix6->Count, 1);
831
832
833 =end testing
834
835 =cut
836
837 sub _WatcherLimit {
838     my $self  = shift;
839     my $field = shift;
840     my $op    = shift;
841     my $value = shift;
842     my %rest  = (@_);
843
844     my $meta = $FIELD_METADATA{ $field };
845     my $type = $meta->[1] || '';
846     my $class = $meta->[2] || 'Ticket';
847
848     # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
849     # search by id and Name at the same time, this is workaround
850     # to preserve backward compatibility
851     if ( $field eq 'Owner' && !$rest{SUBKEY} && $op =~ /^!?=$/ ) {
852         my $o = RT::User->new( $self->CurrentUser );
853         $o->Load( $value );
854         $self->_SQLLimit(
855             FIELD    => 'Owner',
856             OPERATOR => $op,
857             VALUE    => $o->Id,
858             %rest,
859         );
860         return;
861     }
862     $rest{SUBKEY} ||= 'EmailAddress';
863
864     my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
865
866     $self->_OpenParen;
867     if ( $op =~ /^IS(?: NOT)?$/ ) {
868         my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
869         # to avoid joining the table Users into the query, we just join GM
870         # and make sure we don't match records where group is member of itself
871         $self->SUPER::Limit(
872             LEFTJOIN   => $group_members,
873             FIELD      => 'GroupId',
874             OPERATOR   => '!=',
875             VALUE      => "$group_members.MemberId",
876             QUOTEVALUE => 0,
877         );
878         $self->_SQLLimit(
879             ALIAS         => $group_members,
880             FIELD         => 'GroupId',
881             OPERATOR      => $op,
882             VALUE         => $value,
883             %rest,
884         );
885     }
886     elsif ( $op =~ /^!=$|^NOT\s+/i ) {
887         # reverse op
888         $op =~ s/!|NOT\s+//i;
889
890         # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
891         # "X = 'Y'" matches more then one user so we try to fetch two records and
892         # do the right thing when there is only one exist and semi-working solution
893         # otherwise.
894         my $users_obj = RT::Users->new( $self->CurrentUser );
895         $users_obj->Limit(
896             FIELD         => $rest{SUBKEY},
897             OPERATOR      => $op,
898             VALUE         => $value,
899         );
900         $users_obj->OrderBy;
901         $users_obj->RowsPerPage(2);
902         my @users = @{ $users_obj->ItemsArrayRef };
903
904         my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
905         if ( @users <= 1 ) {
906             my $uid = 0;
907             $uid = $users[0]->id if @users;
908             $self->SUPER::Limit(
909                 LEFTJOIN      => $group_members,
910                 ALIAS         => $group_members,
911                 FIELD         => 'MemberId',
912                 VALUE         => $uid,
913             );
914             $self->_SQLLimit(
915                 %rest,
916                 ALIAS           => $group_members,
917                 FIELD           => 'id',
918                 OPERATOR        => 'IS',
919                 VALUE           => 'NULL',
920             );
921         } else {
922             $self->SUPER::Limit(
923                 LEFTJOIN   => $group_members,
924                 FIELD      => 'GroupId',
925                 OPERATOR   => '!=',
926                 VALUE      => "$group_members.MemberId",
927                 QUOTEVALUE => 0,
928             );
929             my $users = $self->Join(
930                 TYPE            => 'LEFT',
931                 ALIAS1          => $group_members,
932                 FIELD1          => 'MemberId',
933                 TABLE2          => 'Users',
934                 FIELD2          => 'id',
935             );
936             $self->SUPER::Limit(
937                 LEFTJOIN      => $users,
938                 ALIAS         => $users,
939                 FIELD         => $rest{SUBKEY},
940                 OPERATOR      => $op,
941                 VALUE         => $value,
942                 CASESENSITIVE => 0,
943             );
944             $self->_SQLLimit(
945                 %rest,
946                 ALIAS         => $users,
947                 FIELD         => 'id',
948                 OPERATOR      => 'IS',
949                 VALUE         => 'NULL',
950             );
951         }
952     } else {
953         my $group_members = $self->_GroupMembersJoin(
954             GroupsAlias => $groups,
955             New => 0,
956         );
957
958         my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
959         unless ( $users ) {
960             $users = $self->{'_sql_u_watchers_aliases'}{$group_members} = 
961                 $self->NewAlias('Users');
962             $self->SUPER::Limit(
963                 LEFTJOIN      => $group_members,
964                 ALIAS         => $group_members,
965                 FIELD         => 'MemberId',
966                 VALUE         => "$users.id",
967                 QUOTEVALUE    => 0,
968             );
969         }
970
971         # we join users table without adding some join condition between tables,
972         # the only conditions we have are conditions on the table iteslf,
973         # for example Users.EmailAddress = 'x'. We should add this condition to
974         # the top level of the query and bundle it with another similar conditions,
975         # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
976         # To achive this goal we use own SUBCLAUSE for conditions on the users table.
977         $self->SUPER::Limit(
978             %rest,
979             SUBCLAUSE       => '_sql_u_watchers_'. $users,
980             ALIAS           => $users,
981             FIELD           => $rest{'SUBKEY'},
982             VALUE           => $value,
983             OPERATOR        => $op,
984             CASESENSITIVE   => 0,
985         );
986         # A condition which ties Users and Groups (role groups) is a left join condition
987         # of CachedGroupMembers table. To get correct results of the query we check
988         # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
989         $self->_SQLLimit(
990             %rest,
991             ALIAS           => $group_members,
992             FIELD           => 'id',
993             OPERATOR        => 'IS NOT',
994             VALUE           => 'NULL',
995         );
996     }
997     $self->_CloseParen;
998 }
999
1000 sub _RoleGroupsJoin {
1001     my $self = shift;
1002     my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1003     return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1004         if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1005            && !$args{'New'};
1006
1007     # XXX: this has been fixed in DBIx::SB-1.48
1008     # XXX: if we change this from Join to NewAlias+Limit
1009     # then Pg and mysql 5.x will complain because SB build wrong query.
1010     # Query looks like "FROM (Tickets LEFT JOIN CGM ON(Groups.id = CGM.GroupId)), Groups"
1011     # Pg doesn't like that fact that it doesn't know about Groups table yet when
1012     # join CGM table into Tickets. Problem is in Join method which doesn't use
1013     # ALIAS1 argument when build braces.
1014
1015     # we always have watcher groups for ticket, so we use INNER join
1016     my $groups = $self->Join(
1017         ALIAS1          => 'main',
1018         FIELD1          => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1019         TABLE2          => 'Groups',
1020         FIELD2          => 'Instance',
1021         ENTRYAGGREGATOR => 'AND',
1022     );
1023     $self->SUPER::Limit(
1024         LEFTJOIN        => $groups,
1025         ALIAS           => $groups,
1026         FIELD           => 'Domain',
1027         VALUE           => 'RT::'. $args{'Class'} .'-Role',
1028     );
1029     $self->SUPER::Limit(
1030         LEFTJOIN        => $groups,
1031         ALIAS           => $groups,
1032         FIELD           => 'Type',
1033         VALUE           => $args{'Type'},
1034     ) if $args{'Type'};
1035
1036     $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1037         unless $args{'New'};
1038
1039     return $groups;
1040 }
1041
1042 sub _GroupMembersJoin {
1043     my $self = shift;
1044     my %args = (New => 1, GroupsAlias => undef, @_);
1045
1046     return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1047         if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1048             && !$args{'New'};
1049
1050     my $alias = $self->Join(
1051         TYPE            => 'LEFT',
1052         ALIAS1          => $args{'GroupsAlias'},
1053         FIELD1          => 'id',
1054         TABLE2          => 'CachedGroupMembers',
1055         FIELD2          => 'GroupId',
1056         ENTRYAGGREGATOR => 'AND',
1057     );
1058
1059     $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1060         unless $args{'New'};
1061
1062     return $alias;
1063 }
1064
1065 =head2 _WatcherJoin
1066
1067 Helper function which provides joins to a watchers table both for limits
1068 and for ordering.
1069
1070 =cut
1071
1072 sub _WatcherJoin {
1073     my $self = shift;
1074     my $type = shift || '';
1075
1076
1077     my $groups = $self->_RoleGroupsJoin( Type => $type );
1078     my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1079     # XXX: work around, we must hide groups that
1080     # are members of the role group we search in,
1081     # otherwise them result in wrong NULLs in Users
1082     # table and break ordering. Now, we know that
1083     # RT doesn't allow to add groups as members of the
1084     # ticket roles, so we just hide entries in CGM table
1085     # with MemberId == GroupId from results
1086     $self->SUPER::Limit(
1087         LEFTJOIN   => $group_members,
1088         FIELD      => 'GroupId',
1089         OPERATOR   => '!=',
1090         VALUE      => "$group_members.MemberId",
1091         QUOTEVALUE => 0,
1092     );
1093     my $users = $self->Join(
1094         TYPE            => 'LEFT',
1095         ALIAS1          => $group_members,
1096         FIELD1          => 'MemberId',
1097         TABLE2          => 'Users',
1098         FIELD2          => 'id',
1099     );
1100     return ($groups, $group_members, $users);
1101 }
1102
1103 =head2 _WatcherMembershipLimit
1104
1105 Handle watcher membership limits, i.e. whether the watcher belongs to a
1106 specific group or not.
1107
1108 Meta Data:
1109   1: Field to query on
1110
1111 SELECT DISTINCT main.*
1112 FROM
1113     Tickets main,
1114     Groups Groups_1,
1115     CachedGroupMembers CachedGroupMembers_2,
1116     Users Users_3
1117 WHERE (
1118     (main.EffectiveId = main.id)
1119 ) AND (
1120     (main.Status != 'deleted')
1121 ) AND (
1122     (main.Type = 'ticket')
1123 ) AND (
1124     (
1125         (Users_3.EmailAddress = '22')
1126             AND
1127         (Groups_1.Domain = 'RT::Ticket-Role')
1128             AND
1129         (Groups_1.Type = 'RequestorGroup')
1130     )
1131 ) AND
1132     Groups_1.Instance = main.id
1133 AND
1134     Groups_1.id = CachedGroupMembers_2.GroupId
1135 AND
1136     CachedGroupMembers_2.MemberId = Users_3.id
1137 ORDER BY main.id ASC
1138 LIMIT 25
1139
1140 =cut
1141
1142 sub _WatcherMembershipLimit {
1143     my ( $self, $field, $op, $value, @rest ) = @_;
1144     my %rest = @rest;
1145
1146     $self->_OpenParen;
1147
1148     my $groups       = $self->NewAlias('Groups');
1149     my $groupmembers = $self->NewAlias('CachedGroupMembers');
1150     my $users        = $self->NewAlias('Users');
1151     my $memberships  = $self->NewAlias('CachedGroupMembers');
1152
1153     if ( ref $field ) {    # gross hack
1154         my @bundle = @$field;
1155         $self->_OpenParen;
1156         for my $chunk (@bundle) {
1157             ( $field, $op, $value, @rest ) = @$chunk;
1158             $self->_SQLLimit(
1159                 ALIAS    => $memberships,
1160                 FIELD    => 'GroupId',
1161                 VALUE    => $value,
1162                 OPERATOR => $op,
1163                 @rest,
1164             );
1165         }
1166         $self->_CloseParen;
1167     }
1168     else {
1169         $self->_SQLLimit(
1170             ALIAS    => $memberships,
1171             FIELD    => 'GroupId',
1172             VALUE    => $value,
1173             OPERATOR => $op,
1174             @rest,
1175         );
1176     }
1177
1178     # {{{ Tie to groups for tickets we care about
1179     $self->_SQLLimit(
1180         ALIAS           => $groups,
1181         FIELD           => 'Domain',
1182         VALUE           => 'RT::Ticket-Role',
1183         ENTRYAGGREGATOR => 'AND'
1184     );
1185
1186     $self->Join(
1187         ALIAS1 => $groups,
1188         FIELD1 => 'Instance',
1189         ALIAS2 => 'main',
1190         FIELD2 => 'id'
1191     );
1192
1193     # }}}
1194
1195     # If we care about which sort of watcher
1196     my $meta = $FIELD_METADATA{$field};
1197     my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1198
1199     if ($type) {
1200         $self->_SQLLimit(
1201             ALIAS           => $groups,
1202             FIELD           => 'Type',
1203             VALUE           => $type,
1204             ENTRYAGGREGATOR => 'AND'
1205         );
1206     }
1207
1208     $self->Join(
1209         ALIAS1 => $groups,
1210         FIELD1 => 'id',
1211         ALIAS2 => $groupmembers,
1212         FIELD2 => 'GroupId'
1213     );
1214
1215     $self->Join(
1216         ALIAS1 => $groupmembers,
1217         FIELD1 => 'MemberId',
1218         ALIAS2 => $users,
1219         FIELD2 => 'id'
1220     );
1221
1222     $self->Join(
1223         ALIAS1 => $memberships,
1224         FIELD1 => 'MemberId',
1225         ALIAS2 => $users,
1226         FIELD2 => 'id'
1227     );
1228
1229     $self->_CloseParen;
1230
1231 }
1232
1233 sub _LinkFieldLimit {
1234     my $restriction;
1235     my $self;
1236     my $LinkAlias;
1237     my %args;
1238     if ( $restriction->{'TYPE'} ) {
1239         $self->SUPER::Limit(
1240             ALIAS           => $LinkAlias,
1241             ENTRYAGGREGATOR => 'AND',
1242             FIELD           => 'Type',
1243             OPERATOR        => '=',
1244             VALUE           => $restriction->{'TYPE'}
1245         );
1246     }
1247
1248     #If we're trying to limit it to things that are target of
1249     if ( $restriction->{'TARGET'} ) {
1250
1251         # If the TARGET is an integer that means that we want to look at
1252         # the LocalTarget field. otherwise, we want to look at the
1253         # "Target" field
1254         my ($matchfield);
1255         if ( $restriction->{'TARGET'} =~ /^(\d+)$/ ) {
1256             $matchfield = "LocalTarget";
1257         }
1258         else {
1259             $matchfield = "Target";
1260         }
1261         $self->SUPER::Limit(
1262             ALIAS           => $LinkAlias,
1263             ENTRYAGGREGATOR => 'AND',
1264             FIELD           => $matchfield,
1265             OPERATOR        => '=',
1266             VALUE           => $restriction->{'TARGET'}
1267         );
1268
1269         #If we're searching on target, join the base to ticket.id
1270         $self->_SQLJoin(
1271             ALIAS1 => 'main',
1272             FIELD1 => $self->{'primary_key'},
1273             ALIAS2 => $LinkAlias,
1274             FIELD2 => 'LocalBase'
1275         );
1276     }
1277
1278     #If we're trying to limit it to things that are base of
1279     elsif ( $restriction->{'BASE'} ) {
1280
1281         # If we're trying to match a numeric link, we want to look at
1282         # LocalBase, otherwise we want to look at "Base"
1283         my ($matchfield);
1284         if ( $restriction->{'BASE'} =~ /^(\d+)$/ ) {
1285             $matchfield = "LocalBase";
1286         }
1287         else {
1288             $matchfield = "Base";
1289         }
1290
1291         $self->SUPER::Limit(
1292             ALIAS           => $LinkAlias,
1293             ENTRYAGGREGATOR => 'AND',
1294             FIELD           => $matchfield,
1295             OPERATOR        => '=',
1296             VALUE           => $restriction->{'BASE'}
1297         );
1298
1299         #If we're searching on base, join the target to ticket.id
1300         $self->_SQLJoin(
1301             ALIAS1 => 'main',
1302             FIELD1 => $self->{'primary_key'},
1303             ALIAS2 => $LinkAlias,
1304             FIELD2 => 'LocalTarget'
1305         );
1306     }
1307 }
1308
1309
1310 =head2 _CustomFieldDecipher
1311
1312 Try and turn a CF descriptor into (cfid, cfname) object pair.
1313
1314 =cut
1315
1316 sub _CustomFieldDecipher {
1317     my ($self, $field) = @_;
1318  
1319     my $queue = undef;
1320     if ( $field =~ /^(.+?)\.{(.+)}$/ ) {
1321         ($queue, $field) = ($1, $2);
1322     }
1323     $field = $1 if $field =~ /^{(.+)}$/;    # trim { }
1324
1325     my $cfid;
1326     if ( $queue ) {
1327         my $q = RT::Queue->new( $self->CurrentUser );
1328         $q->Load( $queue );
1329
1330         my $cf;
1331         if ( $q->id ) {
1332             # $queue = $q->Name; # should we normalize the queue?
1333             $cf = $q->CustomField( $field );
1334         }
1335         else {
1336             $cf = RT::CustomField->new( $self->CurrentUser );
1337             $cf->LoadByNameAndQueue( Queue => 0, Name => $field );
1338         }
1339         return ($queue, $field, $cf->id, $cf)
1340             if $cf && $cf->id;
1341         return ($queue, $field);
1342     }
1343
1344     my $cfs = RT::CustomFields->new( $self->CurrentUser );
1345     $cfs->Limit( FIELD => 'Name', VALUE => $field );
1346     $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1347     my $count = $cfs->Count;
1348     return (undef, $field, undef) if $count > 1;
1349     return (undef, $field, 0) if $count == 0;
1350     my $cf = $cfs->First;
1351     return (undef, $field, $cf->id, $cf) if $cf && $cf->id;
1352     return (undef, $field, undef);
1353 }
1354  
1355 =head2 _CustomFieldJoin
1356
1357 Factor out the Join of custom fields so we can use it for sorting too
1358
1359 =cut
1360
1361 sub _CustomFieldJoin {
1362     my ($self, $cfkey, $cfid, $field) = @_;
1363     # Perform one Join per CustomField
1364     if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1365          $self->{_sql_cf_alias}{$cfkey} )
1366     {
1367         return ( $self->{_sql_object_cfv_alias}{$cfkey},
1368                  $self->{_sql_cf_alias}{$cfkey} );
1369     }
1370
1371     my ($TicketCFs, $CFs);
1372     if ( $cfid ) {
1373         $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1374             TYPE   => 'LEFT',
1375             ALIAS1 => 'main',
1376             FIELD1 => 'id',
1377             TABLE2 => 'ObjectCustomFieldValues',
1378             FIELD2 => 'ObjectId',
1379         );
1380         $self->SUPER::Limit(
1381             LEFTJOIN        => $TicketCFs,
1382             FIELD           => 'CustomField',
1383             VALUE           => $cfid,
1384             ENTRYAGGREGATOR => 'AND'
1385         );
1386     }
1387     else {
1388         my $ocfalias = $self->Join(
1389             TYPE       => 'LEFT',
1390             FIELD1     => 'Queue',
1391             TABLE2     => 'ObjectCustomFields',
1392             FIELD2     => 'ObjectId',
1393         );
1394
1395         $self->SUPER::Limit(
1396             LEFTJOIN        => $ocfalias,
1397             ENTRYAGGREGATOR => 'OR',
1398             FIELD           => 'ObjectId',
1399             VALUE           => '0',
1400         );
1401
1402         $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1403             TYPE       => 'LEFT',
1404             ALIAS1     => $ocfalias,
1405             FIELD1     => 'CustomField',
1406             TABLE2     => 'CustomFields',
1407             FIELD2     => 'id',
1408         );
1409         $self->SUPER::Limit(
1410             LEFTJOIN        => $CFs,
1411             ENTRYAGGREGATOR => 'AND',
1412             FIELD           => 'LookupType',
1413             VALUE           => 'RT::Queue-RT::Ticket',
1414         );
1415         $self->SUPER::Limit(
1416             LEFTJOIN        => $CFs,
1417             ENTRYAGGREGATOR => 'AND',
1418             FIELD           => 'Name',
1419             VALUE           => $field,
1420         );
1421
1422         $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1423             TYPE   => 'LEFT',
1424             ALIAS1 => $CFs,
1425             FIELD1 => 'id',
1426             TABLE2 => 'ObjectCustomFieldValues',
1427             FIELD2 => 'CustomField',
1428         );
1429         $self->SUPER::Limit(
1430             LEFTJOIN        => $TicketCFs,
1431             FIELD           => 'ObjectId',
1432             VALUE           => 'main.id',
1433             QUOTEVALUE      => 0,
1434             ENTRYAGGREGATOR => 'AND',
1435         );
1436     }
1437     $self->SUPER::Limit(
1438         LEFTJOIN        => $TicketCFs,
1439         FIELD           => 'ObjectType',
1440         VALUE           => 'RT::Ticket',
1441         ENTRYAGGREGATOR => 'AND'
1442     );
1443     $self->SUPER::Limit(
1444         LEFTJOIN        => $TicketCFs,
1445         FIELD           => 'Disabled',
1446         OPERATOR        => '=',
1447         VALUE           => '0',
1448         ENTRYAGGREGATOR => 'AND'
1449     );
1450
1451     return ($TicketCFs, $CFs);
1452 }
1453
1454 =head2 _CustomFieldLimit
1455
1456 Limit based on CustomFields
1457
1458 Meta Data:
1459   none
1460
1461 =cut
1462
1463 sub _CustomFieldLimit {
1464     my ( $self, $_field, $op, $value, @rest ) = @_;
1465
1466     my %rest  = @rest;
1467     my $field = $rest{SUBKEY} || die "No field specified";
1468
1469     # For our sanity, we can only limit on one queue at a time
1470
1471     my ($queue, $cfid);
1472     ($queue, $field, $cfid ) = $self->_CustomFieldDecipher( $field );
1473
1474 # If we're trying to find custom fields that don't match something, we
1475 # want tickets where the custom field has no value at all.  Note that
1476 # we explicitly don't include the "IS NULL" case, since we would
1477 # otherwise end up with a redundant clause.
1478
1479     my $null_columns_ok;
1480     if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
1481         $null_columns_ok = 1;
1482     }
1483
1484     my $cfkey = $cfid ? $cfid : "$queue.$field";
1485     my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1486
1487     $self->_OpenParen;
1488
1489     $self->_OpenParen;
1490     $self->_SQLLimit(
1491         ALIAS      => $TicketCFs,
1492         FIELD      => 'Content',
1493         OPERATOR   => $op,
1494         VALUE      => $value,
1495         @rest
1496     );
1497
1498     # XXX: if we join via CustomFields table then
1499     # because of order of left joins we get NULLs in
1500     # CF table and then get nulls for those records
1501     # in OCFVs table what result in wrong results
1502     # as decifer method now tries to load a CF then
1503     # we fall into this situation only when there
1504     # are more than one CF with the name in the DB.
1505     # the same thing applies to order by call.
1506     # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1507     # we want treat IS NULL as (not applies or has
1508     # no value)
1509     $self->_SQLLimit(
1510         ALIAS      => $CFs,
1511         FIELD      => 'Name',
1512         OPERATOR   => 'IS NOT',
1513         VALUE      => 'NULL',
1514         QUOTEVALUE => 0,
1515         ENTRYAGGREGATOR => 'AND',
1516     ) if $CFs;
1517     $self->_CloseParen;
1518
1519     if ($null_columns_ok) {
1520         $self->_SQLLimit(
1521             ALIAS           => $TicketCFs,
1522             FIELD           => 'Content',
1523             OPERATOR        => 'IS',
1524             VALUE           => 'NULL',
1525             QUOTEVALUE      => 0,
1526             ENTRYAGGREGATOR => 'OR',
1527         );
1528     }
1529
1530     $self->_CloseParen;
1531
1532 }
1533
1534 sub _HasAttributeLimit {
1535     my ( $self, $field, $op, $value, %rest ) = @_;
1536
1537     my $alias = $self->Join(
1538         TYPE   => 'LEFT',
1539         ALIAS1 => 'main',
1540         FIELD1 => 'id',
1541         TABLE2 => 'Attributes',
1542         FIELD2 => 'ObjectId',
1543     );
1544     $self->SUPER::Limit(
1545         LEFTJOIN        => $alias,
1546         FIELD           => 'ObjectType',
1547         VALUE           => 'RT::Ticket',
1548         ENTRYAGGREGATOR => 'AND'
1549     );
1550     $self->SUPER::Limit(
1551         LEFTJOIN        => $alias,
1552         FIELD           => 'Name',
1553         OPERATOR        => $op,
1554         VALUE           => $value,
1555         ENTRYAGGREGATOR => 'AND'
1556     );
1557     $self->_SQLLimit(
1558         %rest,
1559         ALIAS      => $alias,
1560         FIELD      => 'id',
1561         OPERATOR   => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1562         VALUE      => 'NULL',
1563         QUOTEVALUE => 0,
1564     );
1565 }
1566
1567
1568 # End Helper Functions
1569
1570 # End of SQL Stuff -------------------------------------------------
1571
1572 # {{{ Allow sorting on watchers
1573
1574 =head2 OrderByCols ARRAY
1575
1576 A modified version of the OrderBy method which automatically joins where
1577 C<ALIAS> is set to the name of a watcher type.
1578
1579 =cut
1580
1581 sub OrderByCols {
1582     my $self = shift;
1583     my @args = @_;
1584     my $clause;
1585     my @res   = ();
1586     my $order = 0;
1587
1588     foreach my $row (@args) {
1589         if ( $row->{ALIAS} ) {
1590             push @res, $row;
1591             next;
1592         }
1593         if ( $row->{FIELD} !~ /\./ ) {
1594             my $meta = $self->FIELDS->{ $row->{FIELD} };
1595             unless ( $meta ) {
1596                 push @res, $row;
1597                 next;
1598             }
1599
1600             if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1601                 my $alias = $self->Join(
1602                     TYPE   => 'LEFT',
1603                     ALIAS1 => 'main',
1604                     FIELD1 => $row->{'FIELD'},
1605                     TABLE2 => 'Queues',
1606                     FIELD2 => 'id',
1607                 );
1608                 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1609             } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1610                 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1611             ) {
1612                 my $alias = $self->Join(
1613                     TYPE   => 'LEFT',
1614                     ALIAS1 => 'main',
1615                     FIELD1 => $row->{'FIELD'},
1616                     TABLE2 => 'Users',
1617                     FIELD2 => 'id',
1618                 );
1619                 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1620             } else {
1621                 push @res, $row;
1622             }
1623             next;
1624         }
1625
1626         my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1627         my $meta = $self->FIELDS->{$field};
1628         if ( $meta->[0] eq 'WATCHERFIELD' ) {
1629             # cache alias as we want to use one alias per watcher type for sorting
1630             my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1631             unless ( $users ) {
1632                 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1633                     = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1634             }
1635             push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1636        } elsif ( $meta->[0] eq 'CUSTOMFIELD' ) {
1637            my ($queue, $field, $cfid, $cf_obj) = $self->_CustomFieldDecipher( $subkey );
1638            my $cfkey = $cfid ? $cfid : "$queue.$field";
1639            $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1640            my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1641            # this is described in _CustomFieldLimit
1642            $self->_SQLLimit(
1643                ALIAS      => $CFs,
1644                FIELD      => 'Name',
1645                OPERATOR   => 'IS NOT',
1646                VALUE      => 'NULL',
1647                QUOTEVALUE => 1,
1648                ENTRYAGGREGATOR => 'AND',
1649            ) if $CFs;
1650            unless ($cfid) {
1651                # For those cases where we are doing a join against the
1652                # CF name, and don't have a CFid, use Unique to make sure
1653                # we don't show duplicate tickets.  NOTE: I'm pretty sure
1654                # this will stay mixed in for the life of the
1655                # class/package, and not just for the life of the object.
1656                # Potential performance issue.
1657                require DBIx::SearchBuilder::Unique;
1658                DBIx::SearchBuilder::Unique->import;
1659            }
1660            my $CFvs = $self->Join(
1661                TYPE   => 'LEFT',
1662                ALIAS1 => $TicketCFs,
1663                FIELD1 => 'CustomField',
1664                TABLE2 => 'CustomFieldValues',
1665                FIELD2 => 'CustomField',
1666            );
1667            $self->SUPER::Limit(
1668                LEFTJOIN        => $CFvs,
1669                FIELD           => 'Name',
1670                QUOTEVALUE      => 0,
1671                VALUE           => $TicketCFs . ".Content",
1672                ENTRYAGGREGATOR => 'AND'
1673            );
1674
1675            push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1676            push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1677        } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1678            # PAW logic is "reversed"
1679            my $order = "ASC";
1680            if (exists $row->{ORDER} ) {
1681                my $o = $row->{ORDER};
1682                delete $row->{ORDER};
1683                $order = "DESC" if $o =~ /asc/i;
1684            }
1685
1686            # Unowned
1687            # Else
1688
1689            # Ticket.Owner  1 0 0
1690            my $ownerId = $self->CurrentUser->Id;
1691            push @res, { %$row, FIELD => "Owner=$ownerId", ORDER => $order } ;
1692
1693            # Unowned Tickets 0 1 0
1694            my $nobodyId = $RT::Nobody->Id;
1695            push @res, { %$row, FIELD => "Owner=$nobodyId", ORDER => $order } ;
1696
1697            push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1698        }
1699        else {
1700            push @res, $row;
1701        }
1702     }
1703     return $self->SUPER::OrderByCols(@res);
1704 }
1705
1706 # }}}
1707
1708 # {{{ Limit the result set based on content
1709
1710 # {{{ sub Limit
1711
1712 =head2 Limit
1713
1714 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1715 Generally best called from LimitFoo methods
1716
1717 =cut
1718
1719 sub Limit {
1720     my $self = shift;
1721     my %args = (
1722         FIELD       => undef,
1723         OPERATOR    => '=',
1724         VALUE       => undef,
1725         DESCRIPTION => undef,
1726         @_
1727     );
1728     $args{'DESCRIPTION'} = $self->loc(
1729         "[_1] [_2] [_3]",  $args{'FIELD'},
1730         $args{'OPERATOR'}, $args{'VALUE'}
1731         )
1732         if ( !defined $args{'DESCRIPTION'} );
1733
1734     my $index = $self->_NextIndex;
1735
1736 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1737
1738     %{ $self->{'TicketRestrictions'}{$index} } = %args;
1739
1740     $self->{'RecalcTicketLimits'} = 1;
1741
1742 # If we're looking at the effective id, we don't want to append the other clause
1743 # which limits us to tickets where id = effective id
1744     if ( $args{'FIELD'} eq 'EffectiveId'
1745         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1746     {
1747         $self->{'looking_at_effective_id'} = 1;
1748     }
1749
1750     if ( $args{'FIELD'} eq 'Type'
1751         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1752     {
1753         $self->{'looking_at_type'} = 1;
1754     }
1755
1756     return ($index);
1757 }
1758
1759 # }}}
1760
1761 =head2 FreezeLimits
1762
1763 Returns a frozen string suitable for handing back to ThawLimits.
1764
1765 =cut
1766
1767 sub _FreezeThawKeys {
1768     'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1769         'looking_at_type';
1770 }
1771
1772 # {{{ sub FreezeLimits
1773
1774 sub FreezeLimits {
1775     my $self = shift;
1776     require Storable;
1777     require MIME::Base64;
1778     MIME::Base64::base64_encode(
1779         Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1780 }
1781
1782 # }}}
1783
1784 =head2 ThawLimits
1785
1786 Take a frozen Limits string generated by FreezeLimits and make this tickets
1787 object have that set of limits.
1788
1789 =cut
1790
1791 # {{{ sub ThawLimits
1792
1793 sub ThawLimits {
1794     my $self = shift;
1795     my $in   = shift;
1796
1797     #if we don't have $in, get outta here.
1798     return undef unless ($in);
1799
1800     $self->{'RecalcTicketLimits'} = 1;
1801
1802     require Storable;
1803     require MIME::Base64;
1804
1805     #We don't need to die if the thaw fails.
1806     @{$self}{ $self->_FreezeThawKeys }
1807         = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1808
1809     $RT::Logger->error($@) if $@;
1810
1811 }
1812
1813 # }}}
1814
1815 # {{{ Limit by enum or foreign key
1816
1817 # {{{ sub LimitQueue
1818
1819 =head2 LimitQueue
1820
1821 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1822 OPERATOR is one of = or !=. (It defaults to =).
1823 VALUE is a queue id or Name.
1824
1825
1826 =cut
1827
1828 sub LimitQueue {
1829     my $self = shift;
1830     my %args = (
1831         VALUE    => undef,
1832         OPERATOR => '=',
1833         @_
1834     );
1835
1836     #TODO  VALUE should also take queue objects
1837     if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1838         my $queue = new RT::Queue( $self->CurrentUser );
1839         $queue->Load( $args{'VALUE'} );
1840         $args{'VALUE'} = $queue->Id;
1841     }
1842
1843     # What if they pass in an Id?  Check for isNum() and convert to
1844     # string.
1845
1846     #TODO check for a valid queue here
1847
1848     $self->Limit(
1849         FIELD       => 'Queue',
1850         VALUE       => $args{'VALUE'},
1851         OPERATOR    => $args{'OPERATOR'},
1852         DESCRIPTION => join(
1853             ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1854         ),
1855     );
1856
1857 }
1858
1859 # }}}
1860
1861 # {{{ sub LimitStatus
1862
1863 =head2 LimitStatus
1864
1865 Takes a paramhash with the fields OPERATOR and VALUE.
1866 OPERATOR is one of = or !=.
1867 VALUE is a status.
1868
1869 RT adds Status != 'deleted' until object has
1870 allow_deleted_search internal property set.
1871 $tickets->{'allow_deleted_search'} = 1;
1872 $tickets->LimitStatus( VALUE => 'deleted' );
1873
1874 =cut
1875
1876 sub LimitStatus {
1877     my $self = shift;
1878     my %args = (
1879         OPERATOR => '=',
1880         @_
1881     );
1882     $self->Limit(
1883         FIELD       => 'Status',
1884         VALUE       => $args{'VALUE'},
1885         OPERATOR    => $args{'OPERATOR'},
1886         DESCRIPTION => join( ' ',
1887             $self->loc('Status'), $args{'OPERATOR'},
1888             $self->loc( $args{'VALUE'} ) ),
1889     );
1890 }
1891
1892 # }}}
1893
1894 # {{{ sub IgnoreType
1895
1896 =head2 IgnoreType
1897
1898 If called, this search will not automatically limit the set of results found
1899 to tickets of type "Ticket". Tickets of other types, such as "project" and
1900 "approval" will be found.
1901
1902 =cut
1903
1904 sub IgnoreType {
1905     my $self = shift;
1906
1907     # Instead of faking a Limit that later gets ignored, fake up the
1908     # fact that we're already looking at type, so that the check in
1909     # Tickets_Overlay_SQL/FromSQL goes down the right branch
1910
1911     #  $self->LimitType(VALUE => '__any');
1912     $self->{looking_at_type} = 1;
1913 }
1914
1915 # }}}
1916
1917 # {{{ sub LimitType
1918
1919 =head2 LimitType
1920
1921 Takes a paramhash with the fields OPERATOR and VALUE.
1922 OPERATOR is one of = or !=, it defaults to "=".
1923 VALUE is a string to search for in the type of the ticket.
1924
1925
1926
1927 =cut
1928
1929 sub LimitType {
1930     my $self = shift;
1931     my %args = (
1932         OPERATOR => '=',
1933         VALUE    => undef,
1934         @_
1935     );
1936     $self->Limit(
1937         FIELD       => 'Type',
1938         VALUE       => $args{'VALUE'},
1939         OPERATOR    => $args{'OPERATOR'},
1940         DESCRIPTION => join( ' ',
1941             $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
1942     );
1943 }
1944
1945 # }}}
1946
1947 # }}}
1948
1949 # {{{ Limit by string field
1950
1951 # {{{ sub LimitSubject
1952
1953 =head2 LimitSubject
1954
1955 Takes a paramhash with the fields OPERATOR and VALUE.
1956 OPERATOR is one of = or !=.
1957 VALUE is a string to search for in the subject of the ticket.
1958
1959 =cut
1960
1961 sub LimitSubject {
1962     my $self = shift;
1963     my %args = (@_);
1964     $self->Limit(
1965         FIELD       => 'Subject',
1966         VALUE       => $args{'VALUE'},
1967         OPERATOR    => $args{'OPERATOR'},
1968         DESCRIPTION => join( ' ',
1969             $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1970     );
1971 }
1972
1973 # }}}
1974
1975 # }}}
1976
1977 # {{{ Limit based on ticket numerical attributes
1978 # Things that can be > < = !=
1979
1980 # {{{ sub LimitId
1981
1982 =head2 LimitId
1983
1984 Takes a paramhash with the fields OPERATOR and VALUE.
1985 OPERATOR is one of =, >, < or !=.
1986 VALUE is a ticket Id to search for
1987
1988 =cut
1989
1990 sub LimitId {
1991     my $self = shift;
1992     my %args = (
1993         OPERATOR => '=',
1994         @_
1995     );
1996
1997     $self->Limit(
1998         FIELD       => 'id',
1999         VALUE       => $args{'VALUE'},
2000         OPERATOR    => $args{'OPERATOR'},
2001         DESCRIPTION =>
2002             join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2003     );
2004 }
2005
2006 # }}}
2007
2008 # {{{ sub LimitPriority
2009
2010 =head2 LimitPriority
2011
2012 Takes a paramhash with the fields OPERATOR and VALUE.
2013 OPERATOR is one of =, >, < or !=.
2014 VALUE is a value to match the ticket\'s priority against
2015
2016 =cut
2017
2018 sub LimitPriority {
2019     my $self = shift;
2020     my %args = (@_);
2021     $self->Limit(
2022         FIELD       => 'Priority',
2023         VALUE       => $args{'VALUE'},
2024         OPERATOR    => $args{'OPERATOR'},
2025         DESCRIPTION => join( ' ',
2026             $self->loc('Priority'),
2027             $args{'OPERATOR'}, $args{'VALUE'}, ),
2028     );
2029 }
2030
2031 # }}}
2032
2033 # {{{ sub LimitInitialPriority
2034
2035 =head2 LimitInitialPriority
2036
2037 Takes a paramhash with the fields OPERATOR and VALUE.
2038 OPERATOR is one of =, >, < or !=.
2039 VALUE is a value to match the ticket\'s initial priority against
2040
2041
2042 =cut
2043
2044 sub LimitInitialPriority {
2045     my $self = shift;
2046     my %args = (@_);
2047     $self->Limit(
2048         FIELD       => 'InitialPriority',
2049         VALUE       => $args{'VALUE'},
2050         OPERATOR    => $args{'OPERATOR'},
2051         DESCRIPTION => join( ' ',
2052             $self->loc('Initial Priority'), $args{'OPERATOR'},
2053             $args{'VALUE'}, ),
2054     );
2055 }
2056
2057 # }}}
2058
2059 # {{{ sub LimitFinalPriority
2060
2061 =head2 LimitFinalPriority
2062
2063 Takes a paramhash with the fields OPERATOR and VALUE.
2064 OPERATOR is one of =, >, < or !=.
2065 VALUE is a value to match the ticket\'s final priority against
2066
2067 =cut
2068
2069 sub LimitFinalPriority {
2070     my $self = shift;
2071     my %args = (@_);
2072     $self->Limit(
2073         FIELD       => 'FinalPriority',
2074         VALUE       => $args{'VALUE'},
2075         OPERATOR    => $args{'OPERATOR'},
2076         DESCRIPTION => join( ' ',
2077             $self->loc('Final Priority'), $args{'OPERATOR'},
2078             $args{'VALUE'}, ),
2079     );
2080 }
2081
2082 # }}}
2083
2084 # {{{ sub LimitTimeWorked
2085
2086 =head2 LimitTimeWorked
2087
2088 Takes a paramhash with the fields OPERATOR and VALUE.
2089 OPERATOR is one of =, >, < or !=.
2090 VALUE is a value to match the ticket's TimeWorked attribute
2091
2092 =cut
2093
2094 sub LimitTimeWorked {
2095     my $self = shift;
2096     my %args = (@_);
2097     $self->Limit(
2098         FIELD       => 'TimeWorked',
2099         VALUE       => $args{'VALUE'},
2100         OPERATOR    => $args{'OPERATOR'},
2101         DESCRIPTION => join( ' ',
2102             $self->loc('Time Worked'),
2103             $args{'OPERATOR'}, $args{'VALUE'}, ),
2104     );
2105 }
2106
2107 # }}}
2108
2109 # {{{ sub LimitTimeLeft
2110
2111 =head2 LimitTimeLeft
2112
2113 Takes a paramhash with the fields OPERATOR and VALUE.
2114 OPERATOR is one of =, >, < or !=.
2115 VALUE is a value to match the ticket's TimeLeft attribute
2116
2117 =cut
2118
2119 sub LimitTimeLeft {
2120     my $self = shift;
2121     my %args = (@_);
2122     $self->Limit(
2123         FIELD       => 'TimeLeft',
2124         VALUE       => $args{'VALUE'},
2125         OPERATOR    => $args{'OPERATOR'},
2126         DESCRIPTION => join( ' ',
2127             $self->loc('Time Left'),
2128             $args{'OPERATOR'}, $args{'VALUE'}, ),
2129     );
2130 }
2131
2132 # }}}
2133
2134 # }}}
2135
2136 # {{{ Limiting based on attachment attributes
2137
2138 # {{{ sub LimitContent
2139
2140 =head2 LimitContent
2141
2142 Takes a paramhash with the fields OPERATOR and VALUE.
2143 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2144 VALUE is a string to search for in the body of the ticket
2145
2146 =cut
2147
2148 sub LimitContent {
2149     my $self = shift;
2150     my %args = (@_);
2151     $self->Limit(
2152         FIELD       => 'Content',
2153         VALUE       => $args{'VALUE'},
2154         OPERATOR    => $args{'OPERATOR'},
2155         DESCRIPTION => join( ' ',
2156             $self->loc('Ticket content'), $args{'OPERATOR'},
2157             $args{'VALUE'}, ),
2158     );
2159 }
2160
2161 # }}}
2162
2163 # {{{ sub LimitFilename
2164
2165 =head2 LimitFilename
2166
2167 Takes a paramhash with the fields OPERATOR and VALUE.
2168 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2169 VALUE is a string to search for in the body of the ticket
2170
2171 =cut
2172
2173 sub LimitFilename {
2174     my $self = shift;
2175     my %args = (@_);
2176     $self->Limit(
2177         FIELD       => 'Filename',
2178         VALUE       => $args{'VALUE'},
2179         OPERATOR    => $args{'OPERATOR'},
2180         DESCRIPTION => join( ' ',
2181             $self->loc('Attachment filename'), $args{'OPERATOR'},
2182             $args{'VALUE'}, ),
2183     );
2184 }
2185
2186 # }}}
2187 # {{{ sub LimitContentType
2188
2189 =head2 LimitContentType
2190
2191 Takes a paramhash with the fields OPERATOR and VALUE.
2192 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2193 VALUE is a content type to search ticket attachments for
2194
2195 =cut
2196
2197 sub LimitContentType {
2198     my $self = shift;
2199     my %args = (@_);
2200     $self->Limit(
2201         FIELD       => 'ContentType',
2202         VALUE       => $args{'VALUE'},
2203         OPERATOR    => $args{'OPERATOR'},
2204         DESCRIPTION => join( ' ',
2205             $self->loc('Ticket content type'), $args{'OPERATOR'},
2206             $args{'VALUE'}, ),
2207     );
2208 }
2209
2210 # }}}
2211
2212 # }}}
2213
2214 # {{{ Limiting based on people
2215
2216 # {{{ sub LimitOwner
2217
2218 =head2 LimitOwner
2219
2220 Takes a paramhash with the fields OPERATOR and VALUE.
2221 OPERATOR is one of = or !=.
2222 VALUE is a user id.
2223
2224 =cut
2225
2226 sub LimitOwner {
2227     my $self = shift;
2228     my %args = (
2229         OPERATOR => '=',
2230         @_
2231     );
2232
2233     my $owner = new RT::User( $self->CurrentUser );
2234     $owner->Load( $args{'VALUE'} );
2235
2236     # FIXME: check for a valid $owner
2237     $self->Limit(
2238         FIELD       => 'Owner',
2239         VALUE       => $args{'VALUE'},
2240         OPERATOR    => $args{'OPERATOR'},
2241         DESCRIPTION => join( ' ',
2242             $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2243     );
2244
2245 }
2246
2247 # }}}
2248
2249 # {{{ Limiting watchers
2250
2251 # {{{ sub LimitWatcher
2252
2253 =head2 LimitWatcher
2254
2255   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2256   OPERATOR is one of =, LIKE, NOT LIKE or !=.
2257   VALUE is a value to match the ticket\'s watcher email addresses against
2258   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2259
2260 =begin testing
2261
2262 my $t1 = RT::Ticket->new($RT::SystemUser);
2263 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
2264
2265 =end testing
2266
2267 =cut
2268
2269 sub LimitWatcher {
2270     my $self = shift;
2271     my %args = (
2272         OPERATOR => '=',
2273         VALUE    => undef,
2274         TYPE     => undef,
2275         @_
2276     );
2277
2278     #build us up a description
2279     my ( $watcher_type, $desc );
2280     if ( $args{'TYPE'} ) {
2281         $watcher_type = $args{'TYPE'};
2282     }
2283     else {
2284         $watcher_type = "Watcher";
2285     }
2286
2287     $self->Limit(
2288         FIELD       => $watcher_type,
2289         VALUE       => $args{'VALUE'},
2290         OPERATOR    => $args{'OPERATOR'},
2291         TYPE        => $args{'TYPE'},
2292         DESCRIPTION => join( ' ',
2293             $self->loc($watcher_type),
2294             $args{'OPERATOR'}, $args{'VALUE'}, ),
2295     );
2296 }
2297
2298 sub LimitRequestor {
2299     my $self = shift;
2300     my %args = (@_);
2301     $RT::Logger->error( "Tickets->LimitRequestor is deprecated  at ("
2302             . join( ":", caller )
2303             . ")" );
2304     $self->LimitWatcher( TYPE => 'Requestor', @_ );
2305
2306 }
2307
2308 # }}}
2309
2310 # }}}
2311
2312 # }}}
2313
2314 # {{{ Limiting based on links
2315
2316 # {{{ LimitLinkedTo
2317
2318 =head2 LimitLinkedTo
2319
2320 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2321 TYPE limits the sort of link we want to search on
2322
2323 TYPE = { RefersTo, MemberOf, DependsOn }
2324
2325 TARGET is the id or URI of the TARGET of the link
2326 (TARGET used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as TARGET
2327
2328 =cut
2329
2330 sub LimitLinkedTo {
2331     my $self = shift;
2332     my %args = (
2333         TICKET   => undef,
2334         TARGET   => undef,
2335         TYPE     => undef,
2336         OPERATOR => '=',
2337         @_
2338     );
2339
2340     $self->Limit(
2341         FIELD       => 'LinkedTo',
2342         BASE        => undef,
2343         TARGET      => ( $args{'TARGET'} || $args{'TICKET'} ),
2344         TYPE        => $args{'TYPE'},
2345         DESCRIPTION => $self->loc(
2346             "Tickets [_1] by [_2]",
2347             $self->loc( $args{'TYPE'} ),
2348             ( $args{'TARGET'} || $args{'TICKET'} )
2349         ),
2350         OPERATOR    => $args{'OPERATOR'},
2351     );
2352 }
2353
2354 # }}}
2355
2356 # {{{ LimitLinkedFrom
2357
2358 =head2 LimitLinkedFrom
2359
2360 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2361 TYPE limits the sort of link we want to search on
2362
2363
2364 BASE is the id or URI of the BASE of the link
2365 (BASE used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as BASE
2366
2367
2368 =cut
2369
2370 sub LimitLinkedFrom {
2371     my $self = shift;
2372     my %args = (
2373         BASE     => undef,
2374         TICKET   => undef,
2375         TYPE     => undef,
2376         OPERATOR => '=',
2377         @_
2378     );
2379
2380     # translate RT2 From/To naming to RT3 TicketSQL naming
2381     my %fromToMap = qw(DependsOn DependentOn
2382         MemberOf  HasMember
2383         RefersTo  ReferredToBy);
2384
2385     my $type = $args{'TYPE'};
2386     $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2387
2388     $self->Limit(
2389         FIELD       => 'LinkedTo',
2390         TARGET      => undef,
2391         BASE        => ( $args{'BASE'} || $args{'TICKET'} ),
2392         TYPE        => $type,
2393         DESCRIPTION => $self->loc(
2394             "Tickets [_1] [_2]",
2395             $self->loc( $args{'TYPE'} ),
2396             ( $args{'BASE'} || $args{'TICKET'} )
2397         ),
2398         OPERATOR    => $args{'OPERATOR'},
2399     );
2400 }
2401
2402 # }}}
2403
2404 # {{{ LimitMemberOf
2405 sub LimitMemberOf {
2406     my $self      = shift;
2407     my $ticket_id = shift;
2408     return $self->LimitLinkedTo(
2409         @_,
2410         TARGET => $ticket_id,
2411         TYPE   => 'MemberOf',
2412     );
2413 }
2414
2415 # }}}
2416
2417 # {{{ LimitHasMember
2418 sub LimitHasMember {
2419     my $self      = shift;
2420     my $ticket_id = shift;
2421     return $self->LimitLinkedFrom(
2422         @_,
2423         BASE => "$ticket_id",
2424         TYPE => 'HasMember',
2425     );
2426
2427 }
2428
2429 # }}}
2430
2431 # {{{ LimitDependsOn
2432
2433 sub LimitDependsOn {
2434     my $self      = shift;
2435     my $ticket_id = shift;
2436     return $self->LimitLinkedTo(
2437         @_,
2438         TARGET => $ticket_id,
2439         TYPE   => 'DependsOn',
2440     );
2441
2442 }
2443
2444 # }}}
2445
2446 # {{{ LimitDependedOnBy
2447
2448 sub LimitDependedOnBy {
2449     my $self      = shift;
2450     my $ticket_id = shift;
2451     return $self->LimitLinkedFrom(
2452         @_,
2453         BASE => $ticket_id,
2454         TYPE => 'DependentOn',
2455     );
2456
2457 }
2458
2459 # }}}
2460
2461 # {{{ LimitRefersTo
2462
2463 sub LimitRefersTo {
2464     my $self      = shift;
2465     my $ticket_id = shift;
2466     return $self->LimitLinkedTo(
2467         @_,
2468         TARGET => $ticket_id,
2469         TYPE   => 'RefersTo',
2470     );
2471
2472 }
2473
2474 # }}}
2475
2476 # {{{ LimitReferredToBy
2477
2478 sub LimitReferredToBy {
2479     my $self      = shift;
2480     my $ticket_id = shift;
2481     return $self->LimitLinkedFrom(
2482         @_,
2483         BASE => $ticket_id,
2484         TYPE => 'ReferredToBy',
2485     );
2486 }
2487
2488 # }}}
2489
2490 # }}}
2491
2492 # {{{ limit based on ticket date attribtes
2493
2494 # {{{ sub LimitDate
2495
2496 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2497
2498 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2499
2500 OPERATOR is one of > or <
2501 VALUE is a date and time in ISO format in GMT
2502 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2503
2504 There are also helper functions of the form LimitFIELD that eliminate
2505 the need to pass in a FIELD argument.
2506
2507 =cut
2508
2509 sub LimitDate {
2510     my $self = shift;
2511     my %args = (
2512         FIELD    => undef,
2513         VALUE    => undef,
2514         OPERATOR => undef,
2515
2516         @_
2517     );
2518
2519     #Set the description if we didn't get handed it above
2520     unless ( $args{'DESCRIPTION'} ) {
2521         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2522             . $args{'OPERATOR'} . " "
2523             . $args{'VALUE'} . " GMT";
2524     }
2525
2526     $self->Limit(%args);
2527
2528 }
2529
2530 # }}}
2531
2532 sub LimitCreated {
2533     my $self = shift;
2534     $self->LimitDate( FIELD => 'Created', @_ );
2535 }
2536
2537 sub LimitDue {
2538     my $self = shift;
2539     $self->LimitDate( FIELD => 'Due', @_ );
2540
2541 }
2542
2543 sub LimitStarts {
2544     my $self = shift;
2545     $self->LimitDate( FIELD => 'Starts', @_ );
2546
2547 }
2548
2549 sub LimitStarted {
2550     my $self = shift;
2551     $self->LimitDate( FIELD => 'Started', @_ );
2552 }
2553
2554 sub LimitResolved {
2555     my $self = shift;
2556     $self->LimitDate( FIELD => 'Resolved', @_ );
2557 }
2558
2559 sub LimitTold {
2560     my $self = shift;
2561     $self->LimitDate( FIELD => 'Told', @_ );
2562 }
2563
2564 sub LimitLastUpdated {
2565     my $self = shift;
2566     $self->LimitDate( FIELD => 'LastUpdated', @_ );
2567 }
2568
2569 #
2570 # {{{ sub LimitTransactionDate
2571
2572 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2573
2574 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2575
2576 OPERATOR is one of > or <
2577 VALUE is a date and time in ISO format in GMT
2578
2579
2580 =cut
2581
2582 sub LimitTransactionDate {
2583     my $self = shift;
2584     my %args = (
2585         FIELD    => 'TransactionDate',
2586         VALUE    => undef,
2587         OPERATOR => undef,
2588
2589         @_
2590     );
2591
2592     #  <20021217042756.GK28744@pallas.fsck.com>
2593     #    "Kill It" - Jesse.
2594
2595     #Set the description if we didn't get handed it above
2596     unless ( $args{'DESCRIPTION'} ) {
2597         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2598             . $args{'OPERATOR'} . " "
2599             . $args{'VALUE'} . " GMT";
2600     }
2601
2602     $self->Limit(%args);
2603
2604 }
2605
2606 # }}}
2607
2608 # }}}
2609
2610 # {{{ Limit based on custom fields
2611 # {{{ sub LimitCustomField
2612
2613 =head2 LimitCustomField
2614
2615 Takes a paramhash of key/value pairs with the following keys:
2616
2617 =over 4
2618
2619 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2620
2621 =item OPERATOR - The usual Limit operators
2622
2623 =item VALUE - The value to compare against
2624
2625 =back
2626
2627 =cut
2628
2629 sub LimitCustomField {
2630     my $self = shift;
2631     my %args = (
2632         VALUE       => undef,
2633         CUSTOMFIELD => undef,
2634         OPERATOR    => '=',
2635         DESCRIPTION => undef,
2636         FIELD       => 'CustomFieldValue',
2637         QUOTEVALUE  => 1,
2638         @_
2639     );
2640
2641     my $CF = RT::CustomField->new( $self->CurrentUser );
2642     if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2643         $CF->Load( $args{CUSTOMFIELD} );
2644     }
2645     else {
2646         $CF->LoadByNameAndQueue(
2647             Name  => $args{CUSTOMFIELD},
2648             Queue => $args{QUEUE}
2649         );
2650         $args{CUSTOMFIELD} = $CF->Id;
2651     }
2652
2653     #If we are looking to compare with a null value.
2654     if ( $args{'OPERATOR'} =~ /^is$/i ) {
2655         $args{'DESCRIPTION'}
2656             ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2657     }
2658     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2659         $args{'DESCRIPTION'}
2660             ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2661     }
2662
2663     # if we're not looking to compare with a null value
2664     else {
2665         $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2666             $CF->Name, $args{OPERATOR}, $args{VALUE} );
2667     }
2668
2669     my $q = "";
2670     if ( $CF->Queue ) {
2671         my $qo = new RT::Queue( $self->CurrentUser );
2672         $qo->Load( $CF->Queue );
2673         $q = $qo->Name;
2674     }
2675
2676     my @rest;
2677     @rest = ( ENTRYAGGREGATOR => 'AND' )
2678         if ( $CF->Type eq 'SelectMultiple' );
2679
2680     $self->Limit(
2681         VALUE => $args{VALUE},
2682         FIELD => "CF."
2683             . (
2684               $q
2685             ? $q . ".{" . $CF->Name . "}"
2686             : $CF->Name
2687             ),
2688         OPERATOR    => $args{OPERATOR},
2689         CUSTOMFIELD => 1,
2690         @rest,
2691     );
2692
2693     $self->{'RecalcTicketLimits'} = 1;
2694 }
2695
2696 # }}}
2697 # }}}
2698
2699 # {{{ sub _NextIndex
2700
2701 =head2 _NextIndex
2702
2703 Keep track of the counter for the array of restrictions
2704
2705 =cut
2706
2707 sub _NextIndex {
2708     my $self = shift;
2709     return ( $self->{'restriction_index'}++ );
2710 }
2711
2712 # }}}
2713
2714 # }}}
2715
2716 # {{{ Core bits to make this a DBIx::SearchBuilder object
2717
2718 # {{{ sub _Init
2719 sub _Init {
2720     my $self = shift;
2721     $self->{'table'}                   = "Tickets";
2722     $self->{'RecalcTicketLimits'}      = 1;
2723     $self->{'looking_at_effective_id'} = 0;
2724     $self->{'looking_at_type'}         = 0;
2725     $self->{'restriction_index'}       = 1;
2726     $self->{'primary_key'}             = "id";
2727     delete $self->{'items_array'};
2728     delete $self->{'item_map'};
2729     delete $self->{'columns_to_display'};
2730     $self->SUPER::_Init(@_);
2731
2732     $self->_InitSQL;
2733
2734 }
2735
2736 # }}}
2737
2738 # {{{ sub Count
2739 sub Count {
2740     my $self = shift;
2741     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2742     return ( $self->SUPER::Count() );
2743 }
2744
2745 # }}}
2746
2747 # {{{ sub CountAll
2748 sub CountAll {
2749     my $self = shift;
2750     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2751     return ( $self->SUPER::CountAll() );
2752 }
2753
2754 # }}}
2755
2756 # {{{ sub ItemsArrayRef
2757
2758 =head2 ItemsArrayRef
2759
2760 Returns a reference to the set of all items found in this search
2761
2762 =cut
2763
2764 sub ItemsArrayRef {
2765     my $self = shift;
2766     my @items;
2767
2768     unless ( $self->{'items_array'} ) {
2769
2770         my $placeholder = $self->_ItemsCounter;
2771         $self->GotoFirstItem();
2772         while ( my $item = $self->Next ) {
2773             push( @{ $self->{'items_array'} }, $item );
2774         }
2775         $self->GotoItem($placeholder);
2776         $self->{'items_array'}
2777             = $self->ItemsOrderBy( $self->{'items_array'} );
2778     }
2779     return ( $self->{'items_array'} );
2780 }
2781
2782 # }}}
2783
2784 # {{{ sub Next
2785 sub Next {
2786     my $self = shift;
2787
2788     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2789
2790     my $Ticket = $self->SUPER::Next();
2791     if ( ( defined($Ticket) ) and ( ref($Ticket) ) ) {
2792
2793         if ( $Ticket->__Value('Status') eq 'deleted'
2794             && !$self->{'allow_deleted_search'} )
2795         {
2796             return ( $self->Next() );
2797         }
2798
2799         # Since Ticket could be granted with more rights instead
2800         # of being revoked, it's ok if queue rights allow
2801         # ShowTicket.  It seems need another query, but we have
2802         # rights cache in Principal::HasRight.
2803         elsif ( $Ticket->CurrentUserHasRight('ShowTicket') )
2804         {
2805             return ($Ticket);
2806         }
2807
2808         #If the user doesn't have the right to show this ticket
2809         else {
2810             return ( $self->Next() );
2811         }
2812     }
2813
2814     #if there never was any ticket
2815     else {
2816         return (undef);
2817     }
2818
2819 }
2820
2821 # }}}
2822
2823 # }}}
2824
2825 # {{{ Deal with storing and restoring restrictions
2826
2827 # {{{ sub LoadRestrictions
2828
2829 =head2 LoadRestrictions
2830
2831 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
2832 TODO It is not yet implemented
2833
2834 =cut
2835
2836 # }}}
2837
2838 # {{{ sub DescribeRestrictions
2839
2840 =head2 DescribeRestrictions
2841
2842 takes nothing.
2843 Returns a hash keyed by restriction id.
2844 Each element of the hash is currently a one element hash that contains DESCRIPTION which
2845 is a description of the purpose of that TicketRestriction
2846
2847 =cut
2848
2849 sub DescribeRestrictions {
2850     my $self = shift;
2851
2852     my ( $row, %listing );
2853
2854     foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2855         $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
2856     }
2857     return (%listing);
2858 }
2859
2860 # }}}
2861
2862 # {{{ sub RestrictionValues
2863
2864 =head2 RestrictionValues FIELD
2865
2866 Takes a restriction field and returns a list of values this field is restricted
2867 to.
2868
2869 =cut
2870
2871 sub RestrictionValues {
2872     my $self  = shift;
2873     my $field = shift;
2874     map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
2875                $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
2876             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
2877         }
2878         keys %{ $self->{'TicketRestrictions'} };
2879 }
2880
2881 # }}}
2882
2883 # {{{ sub ClearRestrictions
2884
2885 =head2 ClearRestrictions
2886
2887 Removes all restrictions irretrievably
2888
2889 =cut
2890
2891 sub ClearRestrictions {
2892     my $self = shift;
2893     delete $self->{'TicketRestrictions'};
2894     $self->{'looking_at_effective_id'} = 0;
2895     $self->{'looking_at_type'}         = 0;
2896     $self->{'RecalcTicketLimits'}      = 1;
2897 }
2898
2899 # }}}
2900
2901 # {{{ sub DeleteRestriction
2902
2903 =head2 DeleteRestriction
2904
2905 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
2906 Removes that restriction from the session's limits.
2907
2908 =cut
2909
2910 sub DeleteRestriction {
2911     my $self = shift;
2912     my $row  = shift;
2913     delete $self->{'TicketRestrictions'}{$row};
2914
2915     $self->{'RecalcTicketLimits'} = 1;
2916
2917     #make the underlying easysearch object forget all its preconceptions
2918 }
2919
2920 # }}}
2921
2922 # {{{ sub _RestrictionsToClauses
2923
2924 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
2925
2926 sub _RestrictionsToClauses {
2927     my $self = shift;
2928
2929     my $row;
2930     my %clause;
2931     foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2932         my $restriction = $self->{'TicketRestrictions'}{$row};
2933
2934         #use Data::Dumper;
2935         #print Dumper($restriction),"\n";
2936
2937         # We need to reimplement the subclause aggregation that SearchBuilder does.
2938         # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
2939         # Then SB AND's the different Subclauses together.
2940
2941         # So, we want to group things into Subclauses, convert them to
2942         # SQL, and then join them with the appropriate DefaultEA.
2943         # Then join each subclause group with AND.
2944
2945         my $field = $restriction->{'FIELD'};
2946         my $realfield = $field;    # CustomFields fake up a fieldname, so
2947                                    # we need to figure that out
2948
2949         # One special case
2950         # Rewrite LinkedTo meta field to the real field
2951         if ( $field =~ /LinkedTo/ ) {
2952             $realfield = $field = $restriction->{'TYPE'};
2953         }
2954
2955         # Two special case
2956         # Handle subkey fields with a different real field
2957         if ( $field =~ /^(\w+)\./ ) {
2958             $realfield = $1;
2959         }
2960
2961         die "I don't know about $field yet"
2962             unless ( exists $FIELD_METADATA{$realfield}
2963                 or $restriction->{CUSTOMFIELD} );
2964
2965         my $type = $FIELD_METADATA{$realfield}->[0];
2966         my $op   = $restriction->{'OPERATOR'};
2967
2968         my $value = (
2969             grep    {defined}
2970                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
2971         )[0];
2972
2973         # this performs the moral equivalent of defined or/dor/C<//>,
2974         # without the short circuiting.You need to use a 'defined or'
2975         # type thing instead of just checking for truth values, because
2976         # VALUE could be 0.(i.e. "false")
2977
2978         # You could also use this, but I find it less aesthetic:
2979         # (although it does short circuit)
2980         #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2981         # defined $restriction->{'TICKET'} ?
2982         # $restriction->{TICKET} :
2983         # defined $restriction->{'BASE'} ?
2984         # $restriction->{BASE} :
2985         # defined $restriction->{'TARGET'} ?
2986         # $restriction->{TARGET} )
2987
2988         my $ea = $restriction->{ENTRYAGGREGATOR}
2989             || $DefaultEA{$type}
2990             || "AND";
2991         if ( ref $ea ) {
2992             die "Invalid operator $op for $field ($type)"
2993                 unless exists $ea->{$op};
2994             $ea = $ea->{$op};
2995         }
2996
2997         # Each CustomField should be put into a different Clause so they
2998         # are ANDed together.
2999         if ( $restriction->{CUSTOMFIELD} ) {
3000             $realfield = $field;
3001         }
3002
3003         exists $clause{$realfield} or $clause{$realfield} = [];
3004
3005         # Escape Quotes
3006         $field =~ s!(['"])!\\$1!g;
3007         $value =~ s!(['"])!\\$1!g;
3008         my $data = [ $ea, $type, $field, $op, $value ];
3009
3010         # here is where we store extra data, say if it's a keyword or
3011         # something.  (I.e. "TYPE SPECIFIC STUFF")
3012
3013         #print Dumper($data);
3014         push @{ $clause{$realfield} }, $data;
3015     }
3016     return \%clause;
3017 }
3018
3019 # }}}
3020
3021 # {{{ sub _ProcessRestrictions
3022
3023 =head2 _ProcessRestrictions PARAMHASH
3024
3025 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3026 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3027
3028 =cut
3029
3030 sub _ProcessRestrictions {
3031     my $self = shift;
3032
3033     #Blow away ticket aliases since we'll need to regenerate them for
3034     #a new search
3035     delete $self->{'TicketAliases'};
3036     delete $self->{'items_array'};
3037     delete $self->{'item_map'};
3038     delete $self->{'raw_rows'};
3039     delete $self->{'rows'};
3040     delete $self->{'count_all'};
3041
3042     my $sql = $self->Query;    # Violating the _SQL namespace
3043     if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3044
3045         #  "Restrictions to Clauses Branch\n";
3046         my $clauseRef = eval { $self->_RestrictionsToClauses; };
3047         if ($@) {
3048             $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3049             $self->FromSQL("");
3050         }
3051         else {
3052             $sql = $self->ClausesToSQL($clauseRef);
3053             $self->FromSQL($sql) if $sql;
3054         }
3055     }
3056
3057     $self->{'RecalcTicketLimits'} = 0;
3058
3059 }
3060
3061 =head2 _BuildItemMap
3062
3063     # Build up a map of first/last/next/prev items, so that we can display search nav quickly
3064
3065 =cut
3066
3067 sub _BuildItemMap {
3068     my $self = shift;
3069
3070     my $items = $self->ItemsArrayRef;
3071     my $prev  = 0;
3072
3073     delete $self->{'item_map'};
3074     if ( $items->[0] ) {
3075         $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
3076         while ( my $item = shift @$items ) {
3077             my $id = $item->EffectiveId;
3078             $self->{'item_map'}->{$id}->{'defined'} = 1;
3079             $self->{'item_map'}->{$id}->{prev}      = $prev;
3080             $self->{'item_map'}->{$id}->{next}      = $items->[0]->EffectiveId
3081                 if ( $items->[0] );
3082             $prev = $id;
3083         }
3084         $self->{'item_map'}->{'last'} = $prev;
3085     }
3086 }
3087
3088 =head2 ItemMap
3089
3090 Returns an a map of all items found by this search. The map is of the form
3091
3092 $ItemMap->{'first'} = first ticketid found
3093 $ItemMap->{'last'} = last ticketid found
3094 $ItemMap->{$id}->{prev} = the ticket id found before $id
3095 $ItemMap->{$id}->{next} = the ticket id found after $id
3096
3097 =cut
3098
3099 sub ItemMap {
3100     my $self = shift;
3101     $self->_BuildItemMap()
3102         unless ( $self->{'items_array'} and $self->{'item_map'} );
3103     return ( $self->{'item_map'} );
3104 }
3105
3106 =cut
3107
3108
3109
3110 }
3111
3112
3113
3114 # }}}
3115
3116 # }}}
3117
3118 =head2 PrepForSerialization
3119
3120 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
3121
3122 =cut
3123
3124 sub PrepForSerialization {
3125     my $self = shift;
3126     delete $self->{'items'};
3127     $self->RedoSearch();
3128 }
3129
3130 =head1 FLAGS
3131
3132 RT::Tickets supports several flags which alter search behavior:
3133
3134
3135 allow_deleted_search  (Otherwise never show deleted tickets in search results)
3136 looking_at_type (otherwise limit to type=ticket)
3137
3138 These flags are set by calling 
3139
3140 $tickets->{'flagname'} = 1;
3141
3142 BUG: There should be an API for this
3143
3144
3145 =begin testing
3146
3147 # We assume that we've got some tickets hanging around from before.
3148 ok( my $unlimittickets = RT::Tickets->new( $RT::SystemUser ) );
3149 ok( $unlimittickets->UnLimit );
3150 ok( $unlimittickets->Count > 0, "UnLimited tickets object should return tickets" );
3151
3152 =end testing
3153
3154 =cut
3155
3156 1;
3157
3158
3159