1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
48 # - Decimated ProcessRestrictions and broke it into multiple
49 # functions joined by a LUT
50 # - Semi-Generic SQL stuff moved to another file
52 # Known Issues: FIXME!
54 # - ClearRestrictions and Reinitialization is messy and unclear. The
55 # only good way to do it is to create a new RT::Tickets object.
59 RT::Tickets - A collection of Ticket objects
65 my $tickets = new RT::Tickets($CurrentUser);
69 A collection of RT::Tickets.
75 ok (require RT::Tickets);
76 ok( my $testtickets = RT::Tickets->new( $RT::SystemUser ) );
77 ok( $testtickets->LimitStatus( VALUE => 'deleted' ) );
78 # Should be zero until 'allow_deleted_search'
79 ok( $testtickets->Count == 0 );
91 no warnings qw(redefine);
92 use vars qw(@SORTFIELDS);
95 # Configuration Tables:
97 # FIELDS is a mapping of searchable Field name, to Type, and other
102 Queue => [ 'ENUM' => 'Queue', ],
104 Creator => [ 'ENUM' => 'User', ],
105 LastUpdatedBy => [ 'ENUM' => 'User', ],
106 Owner => [ 'ENUM' => 'User', ],
107 EffectiveId => [ 'INT', ],
109 InitialPriority => [ 'INT', ],
110 FinalPriority => [ 'INT', ],
111 Priority => [ 'INT', ],
112 TimeLeft => [ 'INT', ],
113 TimeWorked => [ 'INT', ],
114 MemberOf => [ 'LINK' => To => 'MemberOf', ],
115 DependsOn => [ 'LINK' => To => 'DependsOn', ],
116 RefersTo => [ 'LINK' => To => 'RefersTo', ],
117 HasMember => [ 'LINK' => From => 'MemberOf', ],
118 DependentOn => [ 'LINK' => From => 'DependsOn', ],
119 DependedOnBy => [ 'LINK' => From => 'DependsOn', ],
120 ReferredToBy => [ 'LINK' => From => 'RefersTo', ],
121 Told => ['DATE' => 'Told',],
122 Starts => ['DATE' => 'Starts',],
123 Started => ['DATE' => 'Started',],
124 Due => ['DATE' => 'Due',],
125 Resolved => ['DATE' => 'Resolved',],
126 LastUpdated => ['DATE' => 'LastUpdated',],
127 Created => ['DATE' => 'Created',],
128 Subject => ['STRING',],
129 Content => ['TRANSFIELD',],
130 ContentType => ['TRANSFIELD',],
131 Filename => ['TRANSFIELD',],
132 TransactionDate => ['TRANSDATE',],
133 Requestor => ['WATCHERFIELD' => 'Requestor',],
134 Requestors => ['WATCHERFIELD' => 'Requestor',],
135 Cc => ['WATCHERFIELD' => 'Cc',],
136 AdminCc => ['WATCHERFIELD' => 'AdminCc',],
137 Watcher => ['WATCHERFIELD'],
138 LinkedTo => ['LINKFIELD',],
139 CustomFieldValue =>['CUSTOMFIELD',],
140 CF => ['CUSTOMFIELD',],
141 Updated => [ 'TRANSDATE', ],
142 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ],
143 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ],
144 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ],
145 WatcherGroup => [ 'MEMBERSHIPFIELD', ],
148 # Mapping of Field Type to Function
150 ENUM => \&_EnumLimit,
152 LINK => \&_LinkLimit,
153 DATE => \&_DateLimit,
154 STRING => \&_StringLimit,
155 TRANSFIELD => \&_TransLimit,
156 TRANSDATE => \&_TransDateLimit,
157 WATCHERFIELD => \&_WatcherLimit,
158 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
159 LINKFIELD => \&_LinkFieldLimit,
160 CUSTOMFIELD => \&_CustomFieldLimit,
162 my %can_bundle = ( WATCHERFIELD => "yeps", );
164 # Default EntryAggregator per type
165 # if you specify OP, you must specify all valid OPs
201 # Helper functions for passing the above lexically scoped tables above
202 # into Tickets_Overlay_SQL.
203 sub FIELDS { return \%FIELDS }
204 sub dispatch { return \%dispatch }
205 sub can_bundle { return \%can_bundle }
207 # Bring in the clowns.
208 require RT::Tickets_Overlay_SQL;
212 @SORTFIELDS = qw(id Status
214 Owner Created Due Starts Started
216 Resolved LastUpdated Priority TimeWorked TimeLeft);
220 Returns the list of fields that lists of tickets can easily be sorted by
226 return (@SORTFIELDS);
231 # BEGIN SQL STUFF *********************************
233 =head1 Limit Helper Routines
235 These routines are the targets of a dispatch table depending on the
236 type of field. They all share the same signature:
238 my ($self,$field,$op,$value,@rest) = @_;
240 The values in @rest should be suitable for passing directly to
241 DBIx::SearchBuilder::Limit.
243 Essentially they are an expanded/broken out (and much simplified)
244 version of what ProcessRestrictions used to do. They're also much
245 more clearly delineated by the TYPE of field being processed.
249 Handle Fields which are limited to certain values, and potentially
250 need to be looked up from another class.
252 This subroutine actually handles two different kinds of fields. For
253 some the user is responsible for limiting the values. (i.e. Status,
256 For others, the value specified by the user will be looked by via
260 name of class to lookup in (Optional)
265 my ( $sb, $field, $op, $value, @rest ) = @_;
267 # SQL::Statement changes != to <>. (Can we remove this now?)
268 $op = "!=" if $op eq "<>";
270 die "Invalid Operation: $op for $field"
271 unless $op eq "=" or $op eq "!=";
273 my $meta = $FIELDS{$field};
274 if ( defined $meta->[1] ) {
275 my $class = "RT::" . $meta->[1];
276 my $o = $class->new( $sb->CurrentUser );
290 Handle fields where the values are limited to integers. (For example,
291 Priority, TimeWorked.)
299 my ( $sb, $field, $op, $value, @rest ) = @_;
301 die "Invalid Operator $op for $field"
302 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
314 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
317 1: Direction (From,To)
318 2: Link Type (MemberOf, DependsOn,RefersTo)
323 my ( $sb, $field, $op, $value, @rest ) = @_;
325 my $meta = $FIELDS{$field};
326 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS)/io;
328 die "Incorrect Metadata for $field"
329 unless ( defined $meta->[1] and defined $meta->[2] );
331 my $direction = $meta->[1];
337 if ( $direction eq 'To' ) {
338 $matchfield = "Target";
342 elsif ( $direction eq 'From' ) {
343 $linkfield = "Target";
344 $matchfield = "Base";
348 die "Invalid link direction '$meta->[1]' for $field\n";
351 if ( $op eq '=' || $op =~ /^is/oi ) {
352 if ( $value eq '' || $value =~ /^null$/io ) {
355 elsif ( $value =~ /\D/o ) {
363 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
364 # SELECT main.* FROM Tickets main
365 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
366 # AND(main.id = Links_1.LocalTarget))
367 # WHERE ((main.EffectiveId = main.id))
368 # AND ((main.Status != 'deleted'))
369 # AND (Links_1.LocalBase IS NULL);
372 my $linkalias = $sb->Join(
377 FIELD2 => 'Local' . $linkfield
381 LEFTJOIN => $linkalias,
390 ENTRYAGGREGATOR => 'AND',
391 FIELD => ( $is_local ? "Local$matchfield" : $matchfield ),
400 $sb->{_sql_linkalias} = $sb->NewAlias('Links')
401 unless defined $sb->{_sql_linkalias};
406 ALIAS => $sb->{_sql_linkalias},
414 ALIAS => $sb->{_sql_linkalias},
415 ENTRYAGGREGATOR => 'AND',
416 FIELD => ( $is_local ? "Local$matchfield" : $matchfield ),
421 #If we're searching on target, join the base to ticket.id
424 FIELD1 => $sb->{'primary_key'},
425 ALIAS2 => $sb->{_sql_linkalias},
426 FIELD2 => 'Local' . $linkfield
435 Handle date fields. (Created, LastTold..)
438 1: type of link. (Probably not necessary.)
443 my ( $sb, $field, $op, $value, @rest ) = @_;
445 die "Invalid Date Op: $op"
446 unless $op =~ /^(=|>|<|>=|<=)$/;
448 my $meta = $FIELDS{$field};
449 die "Incorrect Meta Data for $field"
450 unless ( defined $meta->[1] );
452 use POSIX 'strftime';
454 my $date = RT::Date->new($sb->CurrentUser);
455 $date->Set(Format => 'unknown', Value => $value);
456 my $time = $date->Unix;
460 # if we're specifying =, that means we want everything on a
461 # particular single day. in the database, we need to check for >
462 # and < the edges of that day.
465 strftime( "%Y-%m-%d %H:%M", gmtime( $time - ( $time % 86400 ) ) );
466 my $dayend = strftime( "%Y-%m-%d %H:%M",
467 gmtime( $time + ( 86399 - $time % 86400 ) ) );
483 ENTRYAGGREGATOR => 'AND',
490 $value = strftime( "%Y-%m-%d %H:%M", gmtime($time) );
502 Handle simple fields which are just strings. (Subject,Type)
510 my ( $sb, $field, $op, $value, @rest ) = @_;
514 # =, !=, LIKE, NOT LIKE
525 =head2 _TransDateLimit
527 Handle fields limiting based on Transaction Date.
529 The inpupt value must be in a format parseable by Time::ParseDate
536 # This routine should really be factored into translimit.
537 sub _TransDateLimit {
538 my ( $sb, $field, $op, $value, @rest ) = @_;
540 # See the comments for TransLimit, they apply here too
542 $sb->{_sql_transalias} = $sb->NewAlias('Transactions')
543 unless defined $sb->{_sql_transalias};
544 $sb->{_sql_trattachalias} = $sb->NewAlias('Attachments')
545 unless defined $sb->{_sql_trattachalias};
547 my $date = RT::Date->new( $sb->CurrentUser );
548 $date->Set( Format => 'unknown', Value => $value );
549 my $time = $date->Unix;
554 # if we're specifying =, that means we want everything on a
555 # particular single day. in the database, we need to check for >
556 # and < the edges of that day.
558 my $daystart = strftime( "%Y-%m-%d %H:%M",
559 gmtime( $time - ( $time % 86400 ) ) );
560 my $dayend = strftime( "%Y-%m-%d %H:%M",
561 gmtime( $time + ( 86399 - $time % 86400 ) ) );
564 ALIAS => $sb->{_sql_transalias},
572 ALIAS => $sb->{_sql_transalias},
578 ENTRYAGGREGATOR => 'AND',
583 # not searching for a single day
586 #Search for the right field
588 ALIAS => $sb->{_sql_transalias},
597 # Join Transactions To Attachments
600 ALIAS1 => $sb->{_sql_trattachalias},
601 FIELD1 => 'TransactionId',
602 ALIAS2 => $sb->{_sql_transalias},
606 # Join Transactions to Tickets
609 FIELD1 => $sb->{'primary_key'}, # UGH!
610 ALIAS2 => $sb->{_sql_transalias},
615 ALIAS => $sb->{_sql_transalias},
616 FIELD => 'ObjectType',
617 VALUE => 'RT::Ticket'
625 Limit based on the Content of a transaction or the ContentType.
634 # Content, ContentType, Filename
636 # If only this was this simple. We've got to do something
639 #Basically, we want to make sure that the limits apply to
640 #the same attachment, rather than just another attachment
641 #for the same ticket, no matter how many clauses we lump
642 #on. We put them in TicketAliases so that they get nuked
643 #when we redo the join.
645 # In the SQL, we might have
646 # (( Content = foo ) or ( Content = bar AND Content = baz ))
647 # The AND group should share the same Alias.
649 # Actually, maybe it doesn't matter. We use the same alias and it
650 # works itself out? (er.. different.)
652 # Steal more from _ProcessRestrictions
654 # FIXME: Maybe look at the previous FooLimit call, and if it was a
655 # TransLimit and EntryAggregator == AND, reuse the Aliases?
657 # Or better - store the aliases on a per subclause basis - since
658 # those are going to be the things we want to relate to each other,
661 # maybe we should not allow certain kinds of aggregation of these
662 # clauses and do a psuedo regex instead? - the problem is getting
663 # them all into the same subclause when you have (A op B op C) - the
664 # way they get parsed in the tree they're in different subclauses.
666 my ( $self, $field, $op, $value, @rest ) = @_;
668 $self->{_sql_transalias} = $self->NewAlias('Transactions')
669 unless defined $self->{_sql_transalias};
670 $self->{_sql_trattachalias} = $self->NewAlias('Attachments')
671 unless defined $self->{_sql_trattachalias};
675 #Search for the right field
677 ALIAS => $self->{_sql_trattachalias},
686 ALIAS1 => $self->{_sql_trattachalias},
687 FIELD1 => 'TransactionId',
688 ALIAS2 => $self->{_sql_transalias},
692 # Join Transactions to Tickets
695 FIELD1 => $self->{'primary_key'}, # Why not use "id" here?
696 ALIAS2 => $self->{_sql_transalias},
701 ALIAS => $self->{_sql_transalias},
702 FIELD => 'ObjectType',
703 VALUE => 'RT::Ticket',
704 ENTRYAGGREGATOR => 'AND'
713 Handle watcher limits. (Requestor, CC, etc..)
721 # Test to make sure that you can search for tickets by requestor address and
725 my $u1 = RT::User->new($RT::SystemUser);
726 ($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
728 my $u2 = RT::User->new($RT::SystemUser);
729 ($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
732 my $t1 = RT::Ticket->new($RT::SystemUser);
734 ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
737 my $t2 = RT::Ticket->new($RT::SystemUser);
738 ($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
742 my $t3 = RT::Ticket->new($RT::SystemUser);
743 ($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
747 my $tix1 = RT::Tickets->new($RT::SystemUser);
748 $tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
750 is ($tix1->Count, 3);
752 my $tix2 = RT::Tickets->new($RT::SystemUser);
753 $tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
755 is ($tix2->Count, 3);
758 my $tix3 = RT::Tickets->new($RT::SystemUser);
759 $tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
761 is ($tix3->Count, 2);
763 my $tix4 = RT::Tickets->new($RT::SystemUser);
764 $tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
766 is ($tix4->Count, 2);
768 # Searching for tickets that have two requestors isn't supported
769 # There's no way to differentiate "one requestor name that matches foo and bar"
770 # and "two requestors, one matching foo and one matching bar"
772 # my $tix5 = RT::Tickets->new($RT::SystemUser);
773 # $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
775 # is ($tix5->Count, 1);
777 # my $tix6 = RT::Tickets->new($RT::SystemUser);
778 # $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
780 # is ($tix6->Count, 1);
796 # Find out what sort of watcher we're looking for
799 $fieldname = $field->[0]->[0];
804 my $meta = $FIELDS{$fieldname};
805 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
807 # We only want _one_ clause for all of requestors, cc, admincc
808 # It's less flexible than what we used to do, but now it sort of actually works. (no huge cartesian products that hose the db)
809 my $groups = $self->{ 'watcherlimit_' . ('global') . "_groups" } ||=
810 $self->NewAlias('Groups');
812 $self->{ 'watcherlimit_' . ('global') . "_groupmembers" } ||=
813 $self->NewAlias('CachedGroupMembers');
814 my $users = $self->{ 'watcherlimit_' . ('global') . "_users" } ||=
815 $self->NewAlias('Users');
817 # Use regular joins instead of SQL joins since we don't want the joins inside ticketsql or we get a huge cartesian product
821 VALUE => 'RT::Ticket-Role',
822 ENTRYAGGREGATOR => 'AND'
826 FIELD1 => 'Instance',
833 ALIAS2 => $groupmembers,
837 ALIAS1 => $groupmembers,
838 FIELD1 => 'MemberId',
843 # If we're looking for multiple watchers of a given type,
844 # TicketSQL will be handing it to us as an array of clauses in
846 if ( ref $field ) { # gross hack
848 for my $chunk (@$field) {
849 ( $field, $op, $value, %rest ) = @$chunk;
852 FIELD => $rest{SUBKEY} || 'EmailAddress',
864 FIELD => $rest{SUBKEY} || 'EmailAddress',
876 ENTRYAGGREGATOR => 'AND'
883 =head2 _WatcherMembershipLimit
885 Handle watcher membership limits, i.e. whether the watcher belongs to a
886 specific group or not.
891 SELECT DISTINCT main.*
895 CachedGroupMembers CachedGroupMembers_2,
898 (main.EffectiveId = main.id)
900 (main.Status != 'deleted')
902 (main.Type = 'ticket')
905 (Users_3.EmailAddress = '22')
907 (Groups_1.Domain = 'RT::Ticket-Role')
909 (Groups_1.Type = 'RequestorGroup')
912 Groups_1.Instance = main.id
914 Groups_1.id = CachedGroupMembers_2.GroupId
916 CachedGroupMembers_2.MemberId = Users_3.id
922 sub _WatcherMembershipLimit {
923 my ( $self, $field, $op, $value, @rest ) = @_;
928 my $groups = $self->NewAlias('Groups');
929 my $groupmembers = $self->NewAlias('CachedGroupMembers');
930 my $users = $self->NewAlias('Users');
931 my $memberships = $self->NewAlias('CachedGroupMembers');
933 if ( ref $field ) { # gross hack
934 my @bundle = @$field;
936 for my $chunk (@bundle) {
937 ( $field, $op, $value, @rest ) = @$chunk;
939 ALIAS => $memberships,
950 ALIAS => $memberships,
958 # {{{ Tie to groups for tickets we care about
962 VALUE => 'RT::Ticket-Role',
963 ENTRYAGGREGATOR => 'AND'
968 FIELD1 => 'Instance',
975 # If we care about which sort of watcher
976 my $meta = $FIELDS{$field};
977 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
984 ENTRYAGGREGATOR => 'AND'
991 ALIAS2 => $groupmembers,
996 ALIAS1 => $groupmembers,
997 FIELD1 => 'MemberId',
1003 ALIAS1 => $memberships,
1004 FIELD1 => 'MemberId',
1013 sub _LinkFieldLimit {
1018 if ( $restriction->{'TYPE'} ) {
1019 $self->SUPER::Limit(
1020 ALIAS => $LinkAlias,
1021 ENTRYAGGREGATOR => 'AND',
1024 VALUE => $restriction->{'TYPE'}
1028 #If we're trying to limit it to things that are target of
1029 if ( $restriction->{'TARGET'} ) {
1031 # If the TARGET is an integer that means that we want to look at
1032 # the LocalTarget field. otherwise, we want to look at the
1035 if ( $restriction->{'TARGET'} =~ /^(\d+)$/ ) {
1036 $matchfield = "LocalTarget";
1039 $matchfield = "Target";
1041 $self->SUPER::Limit(
1042 ALIAS => $LinkAlias,
1043 ENTRYAGGREGATOR => 'AND',
1044 FIELD => $matchfield,
1046 VALUE => $restriction->{'TARGET'}
1049 #If we're searching on target, join the base to ticket.id
1052 FIELD1 => $self->{'primary_key'},
1053 ALIAS2 => $LinkAlias,
1054 FIELD2 => 'LocalBase'
1058 #If we're trying to limit it to things that are base of
1059 elsif ( $restriction->{'BASE'} ) {
1061 # If we're trying to match a numeric link, we want to look at
1062 # LocalBase, otherwise we want to look at "Base"
1064 if ( $restriction->{'BASE'} =~ /^(\d+)$/ ) {
1065 $matchfield = "LocalBase";
1068 $matchfield = "Base";
1071 $self->SUPER::Limit(
1072 ALIAS => $LinkAlias,
1073 ENTRYAGGREGATOR => 'AND',
1074 FIELD => $matchfield,
1076 VALUE => $restriction->{'BASE'}
1079 #If we're searching on base, join the target to ticket.id
1082 FIELD1 => $self->{'primary_key'},
1083 ALIAS2 => $LinkAlias,
1084 FIELD2 => 'LocalTarget'
1091 Limit based on Keywords
1098 sub _CustomFieldLimit {
1099 my ( $self, $_field, $op, $value, @rest ) = @_;
1102 my $field = $rest{SUBKEY} || die "No field specified";
1104 # For our sanity, we can only limit on one queue at a time
1107 if ( $field =~ /^(.+?)\.{(.+)}$/ ) {
1111 $field = $1 if $field =~ /^{(.+)}$/; # trim { }
1114 # If we're trying to find custom fields that don't match something, we
1115 # want tickets where the custom field has no value at all. Note that
1116 # we explicitly don't include the "IS NULL" case, since we would
1117 # otherwise end up with a redundant clause.
1119 my $null_columns_ok;
1120 if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
1121 $null_columns_ok = 1;
1127 my $q = RT::Queue->new( $self->CurrentUser );
1128 $q->Load($queue) if ($queue);
1132 $cf = $q->CustomField($field);
1135 $cf = RT::CustomField->new( $self->CurrentUser );
1136 $cf->LoadByNameAndQueue( Queue => '0', Name => $field );
1144 my $cfkey = $cfid ? $cfid : "$queue.$field";
1146 # Perform one Join per CustomField
1147 if ( $self->{_sql_object_cf_alias}{$cfkey} ) {
1148 $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey};
1152 $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
1156 TABLE2 => 'ObjectCustomFieldValues',
1157 FIELD2 => 'ObjectId',
1159 $self->SUPER::Limit(
1160 LEFTJOIN => $TicketCFs,
1161 FIELD => 'CustomField',
1163 ENTRYAGGREGATOR => 'AND'
1166 my $cfalias = $self->Join(
1168 EXPRESSION => "'$field'",
1169 TABLE2 => 'CustomFields',
1173 $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
1177 TABLE2 => 'ObjectCustomFieldValues',
1178 FIELD2 => 'CustomField',
1180 $self->SUPER::Limit(
1181 LEFTJOIN => $TicketCFs,
1182 FIELD => 'ObjectId',
1185 ENTRYAGGREGATOR => 'AND',
1188 $self->SUPER::Limit(
1189 LEFTJOIN => $TicketCFs,
1190 FIELD => 'ObjectType',
1191 VALUE => ref( $self->NewItem )
1192 , # we want a single item, not a collection
1193 ENTRYAGGREGATOR => 'AND'
1195 $self->SUPER::Limit(
1196 LEFTJOIN => $TicketCFs,
1197 FIELD => 'Disabled',
1200 ENTRYAGGREGATOR => 'AND');
1203 $self->_OpenParen if ($null_columns_ok);
1206 ALIAS => $TicketCFs,
1214 if ($null_columns_ok) {
1216 ALIAS => $TicketCFs,
1221 ENTRYAGGREGATOR => 'OR',
1224 $self->_CloseParen if ($null_columns_ok);
1230 # End Helper Functions
1232 # End of SQL Stuff -------------------------------------------------
1234 # {{{ Limit the result set based on content
1240 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1241 Generally best called from LimitFoo methods
1251 DESCRIPTION => undef,
1254 $args{'DESCRIPTION'} = $self->loc( "[_1] [_2] [_3]",
1255 $args{'FIELD'}, $args{'OPERATOR'}, $args{'VALUE'} )
1256 if ( !defined $args{'DESCRIPTION'} );
1258 my $index = $self->_NextIndex;
1260 #make the TicketRestrictions hash the equivalent of whatever we just passed in;
1262 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1264 $self->{'RecalcTicketLimits'} = 1;
1266 # If we're looking at the effective id, we don't want to append the other clause
1267 # which limits us to tickets where id = effective id
1268 if ( $args{'FIELD'} eq 'EffectiveId' ) {
1269 $self->{'looking_at_effective_id'} = 1;
1272 if ( $args{'FIELD'} eq 'Type' ) {
1273 $self->{'looking_at_type'} = 1;
1283 Returns a frozen string suitable for handing back to ThawLimits.
1287 sub _FreezeThawKeys {
1288 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1292 # {{{ sub FreezeLimits
1297 require MIME::Base64;
1298 MIME::Base64::base64_encode(
1299 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1306 Take a frozen Limits string generated by FreezeLimits and make this tickets
1307 object have that set of limits.
1311 # {{{ sub ThawLimits
1317 #if we don't have $in, get outta here.
1318 return undef unless ($in);
1320 $self->{'RecalcTicketLimits'} = 1;
1323 require MIME::Base64;
1325 #We don't need to die if the thaw fails.
1326 @{$self}{ $self->_FreezeThawKeys } =
1327 eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1329 $RT::Logger->error($@) if $@;
1335 # {{{ Limit by enum or foreign key
1337 # {{{ sub LimitQueue
1341 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1342 OPERATOR is one of = or !=. (It defaults to =).
1343 VALUE is a queue id or Name.
1356 #TODO VALUE should also take queue names and queue objects
1357 #TODO FIXME why are we canonicalizing to name, not id, robrt?
1358 if ( $args{VALUE} =~ /^\d+$/ ) {
1359 my $queue = new RT::Queue( $self->CurrentUser );
1360 $queue->Load( $args{'VALUE'} );
1361 $args{VALUE} = $queue->Name;
1364 # What if they pass in an Id? Check for isNum() and convert to
1367 #TODO check for a valid queue here
1371 VALUE => $args{VALUE},
1372 OPERATOR => $args{'OPERATOR'},
1374 join( ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE}, ),
1381 # {{{ sub LimitStatus
1385 Takes a paramhash with the fields OPERATOR and VALUE.
1386 OPERATOR is one of = or !=.
1389 RT adds Status != 'deleted' until object has
1390 allow_deleted_search internal property set.
1391 $tickets->{'allow_deleted_search'} = 1;
1392 $tickets->LimitStatus( VALUE => 'deleted' );
1404 VALUE => $args{'VALUE'},
1405 OPERATOR => $args{'OPERATOR'},
1406 DESCRIPTION => join( ' ',
1407 $self->loc('Status'), $args{'OPERATOR'},
1408 $self->loc( $args{'VALUE'} ) ),
1414 # {{{ sub IgnoreType
1418 If called, this search will not automatically limit the set of results found
1419 to tickets of type "Ticket". Tickets of other types, such as "project" and
1420 "approval" will be found.
1427 # Instead of faking a Limit that later gets ignored, fake up the
1428 # fact that we're already looking at type, so that the check in
1429 # Tickets_Overlay_SQL/FromSQL goes down the right branch
1431 # $self->LimitType(VALUE => '__any');
1432 $self->{looking_at_type} = 1;
1441 Takes a paramhash with the fields OPERATOR and VALUE.
1442 OPERATOR is one of = or !=, it defaults to "=".
1443 VALUE is a string to search for in the type of the ticket.
1458 VALUE => $args{'VALUE'},
1459 OPERATOR => $args{'OPERATOR'},
1461 join( ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
1469 # {{{ Limit by string field
1471 # {{{ sub LimitSubject
1475 Takes a paramhash with the fields OPERATOR and VALUE.
1476 OPERATOR is one of = or !=.
1477 VALUE is a string to search for in the subject of the ticket.
1486 VALUE => $args{'VALUE'},
1487 OPERATOR => $args{'OPERATOR'},
1488 DESCRIPTION => join(
1489 ' ', $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'},
1498 # {{{ Limit based on ticket numerical attributes
1499 # Things that can be > < = !=
1505 Takes a paramhash with the fields OPERATOR and VALUE.
1506 OPERATOR is one of =, >, < or !=.
1507 VALUE is a ticket Id to search for
1520 VALUE => $args{'VALUE'},
1521 OPERATOR => $args{'OPERATOR'},
1523 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1529 # {{{ sub LimitPriority
1531 =head2 LimitPriority
1533 Takes a paramhash with the fields OPERATOR and VALUE.
1534 OPERATOR is one of =, >, < or !=.
1535 VALUE is a value to match the ticket\'s priority against
1543 FIELD => 'Priority',
1544 VALUE => $args{'VALUE'},
1545 OPERATOR => $args{'OPERATOR'},
1546 DESCRIPTION => join( ' ',
1547 $self->loc('Priority'),
1548 $args{'OPERATOR'}, $args{'VALUE'}, ),
1554 # {{{ sub LimitInitialPriority
1556 =head2 LimitInitialPriority
1558 Takes a paramhash with the fields OPERATOR and VALUE.
1559 OPERATOR is one of =, >, < or !=.
1560 VALUE is a value to match the ticket\'s initial priority against
1565 sub LimitInitialPriority {
1569 FIELD => 'InitialPriority',
1570 VALUE => $args{'VALUE'},
1571 OPERATOR => $args{'OPERATOR'},
1572 DESCRIPTION => join( ' ',
1573 $self->loc('Initial Priority'), $args{'OPERATOR'},
1580 # {{{ sub LimitFinalPriority
1582 =head2 LimitFinalPriority
1584 Takes a paramhash with the fields OPERATOR and VALUE.
1585 OPERATOR is one of =, >, < or !=.
1586 VALUE is a value to match the ticket\'s final priority against
1590 sub LimitFinalPriority {
1594 FIELD => 'FinalPriority',
1595 VALUE => $args{'VALUE'},
1596 OPERATOR => $args{'OPERATOR'},
1597 DESCRIPTION => join( ' ',
1598 $self->loc('Final Priority'),
1599 $args{'OPERATOR'}, $args{'VALUE'}, ),
1605 # {{{ sub LimitTimeWorked
1607 =head2 LimitTimeWorked
1609 Takes a paramhash with the fields OPERATOR and VALUE.
1610 OPERATOR is one of =, >, < or !=.
1611 VALUE is a value to match the ticket's TimeWorked attribute
1615 sub LimitTimeWorked {
1619 FIELD => 'TimeWorked',
1620 VALUE => $args{'VALUE'},
1621 OPERATOR => $args{'OPERATOR'},
1622 DESCRIPTION => join( ' ',
1623 $self->loc('Time worked'),
1624 $args{'OPERATOR'}, $args{'VALUE'}, ),
1630 # {{{ sub LimitTimeLeft
1632 =head2 LimitTimeLeft
1634 Takes a paramhash with the fields OPERATOR and VALUE.
1635 OPERATOR is one of =, >, < or !=.
1636 VALUE is a value to match the ticket's TimeLeft attribute
1644 FIELD => 'TimeLeft',
1645 VALUE => $args{'VALUE'},
1646 OPERATOR => $args{'OPERATOR'},
1647 DESCRIPTION => join( ' ',
1648 $self->loc('Time left'),
1649 $args{'OPERATOR'}, $args{'VALUE'}, ),
1657 # {{{ Limiting based on attachment attributes
1659 # {{{ sub LimitContent
1663 Takes a paramhash with the fields OPERATOR and VALUE.
1664 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1665 VALUE is a string to search for in the body of the ticket
1674 VALUE => $args{'VALUE'},
1675 OPERATOR => $args{'OPERATOR'},
1676 DESCRIPTION => join( ' ',
1677 $self->loc('Ticket content'),
1678 $args{'OPERATOR'}, $args{'VALUE'}, ),
1684 # {{{ sub LimitFilename
1686 =head2 LimitFilename
1688 Takes a paramhash with the fields OPERATOR and VALUE.
1689 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1690 VALUE is a string to search for in the body of the ticket
1698 FIELD => 'Filename',
1699 VALUE => $args{'VALUE'},
1700 OPERATOR => $args{'OPERATOR'},
1701 DESCRIPTION => join( ' ',
1702 $self->loc('Attachment filename'), $args{'OPERATOR'},
1708 # {{{ sub LimitContentType
1710 =head2 LimitContentType
1712 Takes a paramhash with the fields OPERATOR and VALUE.
1713 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1714 VALUE is a content type to search ticket attachments for
1718 sub LimitContentType {
1722 FIELD => 'ContentType',
1723 VALUE => $args{'VALUE'},
1724 OPERATOR => $args{'OPERATOR'},
1725 DESCRIPTION => join( ' ',
1726 $self->loc('Ticket content type'), $args{'OPERATOR'},
1735 # {{{ Limiting based on people
1737 # {{{ sub LimitOwner
1741 Takes a paramhash with the fields OPERATOR and VALUE.
1742 OPERATOR is one of = or !=.
1754 my $owner = new RT::User( $self->CurrentUser );
1755 $owner->Load( $args{'VALUE'} );
1757 # FIXME: check for a valid $owner
1760 VALUE => $args{'VALUE'},
1761 OPERATOR => $args{'OPERATOR'},
1763 join( ' ', $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
1770 # {{{ Limiting watchers
1772 # {{{ sub LimitWatcher
1776 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1777 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1778 VALUE is a value to match the ticket\'s watcher email addresses against
1779 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1783 my $t1 = RT::Ticket->new($RT::SystemUser);
1784 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
1799 #build us up a description
1800 my ( $watcher_type, $desc );
1801 if ( $args{'TYPE'} ) {
1802 $watcher_type = $args{'TYPE'};
1805 $watcher_type = "Watcher";
1809 FIELD => $watcher_type,
1810 VALUE => $args{'VALUE'},
1811 OPERATOR => $args{'OPERATOR'},
1812 TYPE => $args{'TYPE'},
1813 DESCRIPTION => join( ' ',
1814 $self->loc($watcher_type),
1815 $args{'OPERATOR'}, $args{'VALUE'}, ),
1819 sub LimitRequestor {
1822 my ( $package, $filename, $line ) = caller;
1824 "Tickets->LimitRequestor is deprecated. please rewrite call at $package - $filename: $line"
1826 $self->LimitWatcher( TYPE => 'Requestor', @_ );
1836 # {{{ Limiting based on links
1840 =head2 LimitLinkedTo
1842 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1843 TYPE limits the sort of link we want to search on
1845 TYPE = { RefersTo, MemberOf, DependsOn }
1847 TARGET is the id or URI of the TARGET of the link
1848 (TARGET used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as TARGET
1862 FIELD => 'LinkedTo',
1864 TARGET => ( $args{'TARGET'} || $args{'TICKET'} ),
1865 TYPE => $args{'TYPE'},
1866 DESCRIPTION => $self->loc(
1867 "Tickets [_1] by [_2]",
1868 $self->loc( $args{'TYPE'} ),
1869 ( $args{'TARGET'} || $args{'TICKET'} )
1876 # {{{ LimitLinkedFrom
1878 =head2 LimitLinkedFrom
1880 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1881 TYPE limits the sort of link we want to search on
1884 BASE is the id or URI of the BASE of the link
1885 (BASE used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as BASE
1890 sub LimitLinkedFrom {
1899 # translate RT2 From/To naming to RT3 TicketSQL naming
1900 my %fromToMap = qw(DependsOn DependentOn
1902 RefersTo ReferredToBy);
1904 my $type = $args{'TYPE'};
1905 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
1908 FIELD => 'LinkedTo',
1910 BASE => ( $args{'BASE'} || $args{'TICKET'} ),
1912 DESCRIPTION => $self->loc(
1913 "Tickets [_1] [_2]",
1914 $self->loc( $args{'TYPE'} ),
1915 ( $args{'BASE'} || $args{'TICKET'} )
1925 my $ticket_id = shift;
1926 $self->LimitLinkedTo(
1927 TARGET => "$ticket_id",
1935 # {{{ LimitHasMember
1936 sub LimitHasMember {
1938 my $ticket_id = shift;
1939 $self->LimitLinkedFrom(
1940 BASE => "$ticket_id",
1941 TYPE => 'HasMember',
1948 # {{{ LimitDependsOn
1950 sub LimitDependsOn {
1952 my $ticket_id = shift;
1953 $self->LimitLinkedTo(
1954 TARGET => "$ticket_id",
1955 TYPE => 'DependsOn',
1962 # {{{ LimitDependedOnBy
1964 sub LimitDependedOnBy {
1966 my $ticket_id = shift;
1967 $self->LimitLinkedFrom(
1968 BASE => "$ticket_id",
1969 TYPE => 'DependentOn',
1980 my $ticket_id = shift;
1981 $self->LimitLinkedTo(
1982 TARGET => "$ticket_id",
1990 # {{{ LimitReferredToBy
1992 sub LimitReferredToBy {
1994 my $ticket_id = shift;
1995 $self->LimitLinkedFrom(
1996 BASE => "$ticket_id",
1997 TYPE => 'ReferredToBy',
2006 # {{{ limit based on ticket date attribtes
2010 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2012 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2014 OPERATOR is one of > or <
2015 VALUE is a date and time in ISO format in GMT
2016 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2018 There are also helper functions of the form LimitFIELD that eliminate
2019 the need to pass in a FIELD argument.
2033 #Set the description if we didn't get handed it above
2034 unless ( $args{'DESCRIPTION'} ) {
2035 $args{'DESCRIPTION'} =
2036 $args{'FIELD'} . " "
2037 . $args{'OPERATOR'} . " "
2038 . $args{'VALUE'} . " GMT";
2041 $self->Limit(%args);
2049 $self->LimitDate( FIELD => 'Created', @_ );
2054 $self->LimitDate( FIELD => 'Due', @_ );
2060 $self->LimitDate( FIELD => 'Starts', @_ );
2066 $self->LimitDate( FIELD => 'Started', @_ );
2071 $self->LimitDate( FIELD => 'Resolved', @_ );
2076 $self->LimitDate( FIELD => 'Told', @_ );
2079 sub LimitLastUpdated {
2081 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2085 # {{{ sub LimitTransactionDate
2087 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2089 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2091 OPERATOR is one of > or <
2092 VALUE is a date and time in ISO format in GMT
2097 sub LimitTransactionDate {
2100 FIELD => 'TransactionDate',
2107 # <20021217042756.GK28744@pallas.fsck.com>
2108 # "Kill It" - Jesse.
2110 #Set the description if we didn't get handed it above
2111 unless ( $args{'DESCRIPTION'} ) {
2112 $args{'DESCRIPTION'} =
2113 $args{'FIELD'} . " "
2114 . $args{'OPERATOR'} . " "
2115 . $args{'VALUE'} . " GMT";
2118 $self->Limit(%args);
2126 # {{{ Limit based on custom fields
2127 # {{{ sub LimitCustomField
2129 =head2 LimitCustomField
2131 Takes a paramhash of key/value pairs with the following keys:
2135 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2137 =item OPERATOR - The usual Limit operators
2139 =item VALUE - The value to compare against
2145 sub LimitCustomField {
2149 CUSTOMFIELD => undef,
2151 DESCRIPTION => undef,
2152 FIELD => 'CustomFieldValue',
2157 my $CF = RT::CustomField->new( $self->CurrentUser );
2158 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2159 $CF->Load( $args{CUSTOMFIELD} );
2162 $CF->LoadByNameAndQueue(
2163 Name => $args{CUSTOMFIELD},
2164 Queue => $args{QUEUE}
2166 $args{CUSTOMFIELD} = $CF->Id;
2169 #If we are looking to compare with a null value.
2170 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2171 $args{'DESCRIPTION'} ||=
2172 $self->loc( "Custom field [_1] has no value.", $CF->Name );
2174 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2175 $args{'DESCRIPTION'} ||=
2176 $self->loc( "Custom field [_1] has a value.", $CF->Name );
2179 # if we're not looking to compare with a null value
2181 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2182 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2187 my $qo = new RT::Queue( $self->CurrentUser );
2188 $qo->load( $CF->Queue );
2193 @rest = ( ENTRYAGGREGATOR => 'AND' )
2194 if ( $CF->Type eq 'SelectMultiple' );
2197 VALUE => $args{VALUE},
2201 ? $q . ".{" . $CF->Name . "}"
2204 OPERATOR => $args{OPERATOR},
2209 $self->{'RecalcTicketLimits'} = 1;
2215 # {{{ sub _NextIndex
2219 Keep track of the counter for the array of restrictions
2225 return ( $self->{'restriction_index'}++ );
2232 # {{{ Core bits to make this a DBIx::SearchBuilder object
2237 $self->{'table'} = "Tickets";
2238 $self->{'RecalcTicketLimits'} = 1;
2239 $self->{'looking_at_effective_id'} = 0;
2240 $self->{'looking_at_type'} = 0;
2241 $self->{'restriction_index'} = 1;
2242 $self->{'primary_key'} = "id";
2243 delete $self->{'items_array'};
2244 delete $self->{'item_map'};
2245 delete $self->{'columns_to_display'};
2246 $self->SUPER::_Init(@_);
2257 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2258 return ( $self->SUPER::Count() );
2266 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2267 return ( $self->SUPER::CountAll() );
2272 # {{{ sub ItemsArrayRef
2274 =head2 ItemsArrayRef
2276 Returns a reference to the set of all items found in this search
2284 unless ( $self->{'items_array'} ) {
2286 my $placeholder = $self->_ItemsCounter;
2287 $self->GotoFirstItem();
2288 while ( my $item = $self->Next ) {
2289 push( @{ $self->{'items_array'} }, $item );
2291 $self->GotoItem($placeholder);
2292 $self->{'items_array'} = $self->ItemsOrderBy( $self->{'items_array'} );
2294 return ( $self->{'items_array'} );
2303 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2305 my $Ticket = $self->SUPER::Next();
2306 if ( ( defined($Ticket) ) and ( ref($Ticket) ) ) {
2308 if ( $Ticket->__Value('Status') eq 'deleted' &&
2309 !$self->{'allow_deleted_search'} ) {
2310 return($self->Next());
2312 # Since Ticket could be granted with more rights instead
2313 # of being revoked, it's ok if queue rights allow
2314 # ShowTicket. It seems need another query, but we have
2315 # rights cache in Principal::HasRight.
2316 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket') ||
2317 $Ticket->CurrentUserHasRight('ShowTicket')) {
2321 if ( $Ticket->__Value('Status') eq 'deleted' ) {
2322 return ( $self->Next() );
2325 # Since Ticket could be granted with more rights instead
2326 # of being revoked, it's ok if queue rights allow
2327 # ShowTicket. It seems need another query, but we have
2328 # rights cache in Principal::HasRight.
2329 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
2330 || $Ticket->CurrentUserHasRight('ShowTicket') )
2335 #If the user doesn't have the right to show this ticket
2337 return ( $self->Next() );
2341 #if there never was any ticket
2352 # {{{ Deal with storing and restoring restrictions
2354 # {{{ sub LoadRestrictions
2356 =head2 LoadRestrictions
2358 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
2359 TODO It is not yet implemented
2365 # {{{ sub DescribeRestrictions
2367 =head2 DescribeRestrictions
2370 Returns a hash keyed by restriction id.
2371 Each element of the hash is currently a one element hash that contains DESCRIPTION which
2372 is a description of the purpose of that TicketRestriction
2376 sub DescribeRestrictions {
2379 my ( $row, %listing );
2381 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2382 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
2389 # {{{ sub RestrictionValues
2391 =head2 RestrictionValues FIELD
2393 Takes a restriction field and returns a list of values this field is restricted
2398 sub RestrictionValues {
2401 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
2402 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
2403 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
2405 keys %{ $self->{'TicketRestrictions'} };
2410 # {{{ sub ClearRestrictions
2412 =head2 ClearRestrictions
2414 Removes all restrictions irretrievably
2418 sub ClearRestrictions {
2420 delete $self->{'TicketRestrictions'};
2421 $self->{'looking_at_effective_id'} = 0;
2422 $self->{'looking_at_type'} = 0;
2423 $self->{'RecalcTicketLimits'} = 1;
2428 # {{{ sub DeleteRestriction
2430 =head2 DeleteRestriction
2432 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
2433 Removes that restriction from the session's limits.
2437 sub DeleteRestriction {
2440 delete $self->{'TicketRestrictions'}{$row};
2442 $self->{'RecalcTicketLimits'} = 1;
2444 #make the underlying easysearch object forget all its preconceptions
2449 # {{{ sub _RestrictionsToClauses
2451 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
2453 sub _RestrictionsToClauses {
2458 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2459 my $restriction = $self->{'TicketRestrictions'}{$row};
2462 #print Dumper($restriction),"\n";
2464 # We need to reimplement the subclause aggregation that SearchBuilder does.
2465 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
2466 # Then SB AND's the different Subclauses together.
2468 # So, we want to group things into Subclauses, convert them to
2469 # SQL, and then join them with the appropriate DefaultEA.
2470 # Then join each subclause group with AND.
2472 my $field = $restriction->{'FIELD'};
2473 my $realfield = $field; # CustomFields fake up a fieldname, so
2474 # we need to figure that out
2477 # Rewrite LinkedTo meta field to the real field
2478 if ( $field =~ /LinkedTo/ ) {
2479 $realfield = $field = $restriction->{'TYPE'};
2483 # Handle subkey fields with a different real field
2484 if ( $field =~ /^(\w+)\./ ) {
2488 die "I don't know about $field yet"
2489 unless ( exists $FIELDS{$realfield} or $restriction->{CUSTOMFIELD} );
2491 my $type = $FIELDS{$realfield}->[0];
2492 my $op = $restriction->{'OPERATOR'};
2496 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
2499 # this performs the moral equivalent of defined or/dor/C<//>,
2500 # without the short circuiting.You need to use a 'defined or'
2501 # type thing instead of just checking for truth values, because
2502 # VALUE could be 0.(i.e. "false")
2504 # You could also use this, but I find it less aesthetic:
2505 # (although it does short circuit)
2506 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2507 # defined $restriction->{'TICKET'} ?
2508 # $restriction->{TICKET} :
2509 # defined $restriction->{'BASE'} ?
2510 # $restriction->{BASE} :
2511 # defined $restriction->{'TARGET'} ?
2512 # $restriction->{TARGET} )
2514 my $ea = $restriction->{ENTRYAGGREGATOR} || $DefaultEA{$type} || "AND";
2516 die "Invalid operator $op for $field ($type)"
2517 unless exists $ea->{$op};
2521 # Each CustomField should be put into a different Clause so they
2522 # are ANDed together.
2523 if ( $restriction->{CUSTOMFIELD} ) {
2524 $realfield = $field;
2527 exists $clause{$realfield} or $clause{$realfield} = [];
2530 $field =~ s!(['"])!\\$1!g;
2531 $value =~ s!(['"])!\\$1!g;
2532 my $data = [ $ea, $type, $field, $op, $value ];
2534 # here is where we store extra data, say if it's a keyword or
2535 # something. (I.e. "TYPE SPECIFIC STUFF")
2537 #print Dumper($data);
2538 push @{ $clause{$realfield} }, $data;
2545 # {{{ sub _ProcessRestrictions
2547 =head2 _ProcessRestrictions PARAMHASH
2549 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
2550 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
2554 sub _ProcessRestrictions {
2557 #Blow away ticket aliases since we'll need to regenerate them for
2559 delete $self->{'TicketAliases'};
2560 delete $self->{'items_array'};
2561 delete $self->{'item_map'};
2562 delete $self->{'raw_rows'};
2563 delete $self->{'rows'};
2564 delete $self->{'count_all'};
2566 my $sql = $self->Query; # Violating the _SQL namespace
2567 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
2569 # "Restrictions to Clauses Branch\n";
2570 my $clauseRef = eval { $self->_RestrictionsToClauses; };
2572 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2576 $sql = $self->ClausesToSQL($clauseRef);
2577 $self->FromSQL($sql);
2581 $self->{'RecalcTicketLimits'} = 0;
2585 =head2 _BuildItemMap
2587 # Build up a map of first/last/next/prev items, so that we can display search nav quickly
2594 my $items = $self->ItemsArrayRef;
2597 delete $self->{'item_map'};
2598 if ( $items->[0] ) {
2599 $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
2600 while ( my $item = shift @$items ) {
2601 my $id = $item->EffectiveId;
2602 $self->{'item_map'}->{$id}->{'defined'} = 1;
2603 $self->{'item_map'}->{$id}->{prev} = $prev;
2604 $self->{'item_map'}->{$id}->{next} = $items->[0]->EffectiveId
2608 $self->{'item_map'}->{'last'} = $prev;
2614 Returns an a map of all items found by this search. The map is of the form
2616 $ItemMap->{'first'} = first ticketid found
2617 $ItemMap->{'last'} = last ticketid found
2618 $ItemMap->{$id}->{prev} = the ticket id found before $id
2619 $ItemMap->{$id}->{next} = the ticket id found after $id
2625 $self->_BuildItemMap()
2626 unless ( $self->{'items_array'} and $self->{'item_map'} );
2627 return ( $self->{'item_map'} );
2641 =head2 PrepForSerialization
2643 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
2647 sub PrepForSerialization {
2649 delete $self->{'items'};
2650 $self->RedoSearch();
2656 RT::Tickets supports several flags which alter search behavior:
2659 allow_deleted_search (Otherwise never show deleted tickets in search results)
2660 looking_at_type (otherwise limit to type=ticket)
2662 These flags are set by calling
2664 $tickets->{'flagname'} = 1;
2666 BUG: There should be an API for this