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