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 => [ 'WATCHERFIELD' => 'Owner', ],
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 => "yes", );
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"
274 my $meta = $FIELDS{$field};
275 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
276 my $class = "RT::" . $meta->[1];
277 my $o = $class->new( $sb->CurrentUser );
291 Handle fields where the values are limited to integers. (For example,
292 Priority, TimeWorked.)
300 my ( $sb, $field, $op, $value, @rest ) = @_;
302 die "Invalid Operator $op for $field"
303 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
315 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
318 1: Direction (From,To)
319 2: Link Type (MemberOf, DependsOn,RefersTo)
324 my ( $sb, $field, $op, $value, @rest ) = @_;
326 my $meta = $FIELDS{$field};
327 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS)/io;
329 die "Incorrect Metadata for $field"
330 unless ( defined $meta->[1] and defined $meta->[2] );
332 my $direction = $meta->[1];
338 if ( $direction eq 'To' ) {
339 $matchfield = "Target";
343 elsif ( $direction eq 'From' ) {
344 $linkfield = "Target";
345 $matchfield = "Base";
349 die "Invalid link direction '$meta->[1]' for $field\n";
352 if ( $op eq '=' || $op =~ /^is/oi ) {
353 if ( $value eq '' || $value =~ /^null$/io ) {
356 elsif ( $value =~ /\D/o ) {
364 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
365 # SELECT main.* FROM Tickets main
366 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
367 # AND(main.id = Links_1.LocalTarget))
368 # WHERE ((main.EffectiveId = main.id))
369 # AND ((main.Status != 'deleted'))
370 # AND (Links_1.LocalBase IS NULL);
373 my $linkalias = $sb->Join(
378 FIELD2 => 'Local' . $linkfield
382 LEFTJOIN => $linkalias,
391 ENTRYAGGREGATOR => 'AND',
392 FIELD => ( $is_local ? "Local$matchfield" : $matchfield ),
401 $sb->{_sql_linkalias} = $sb->NewAlias('Links')
402 unless defined $sb->{_sql_linkalias};
407 ALIAS => $sb->{_sql_linkalias},
415 ALIAS => $sb->{_sql_linkalias},
416 ENTRYAGGREGATOR => 'AND',
417 FIELD => ( $is_local ? "Local$matchfield" : $matchfield ),
422 #If we're searching on target, join the base to ticket.id
425 FIELD1 => $sb->{'primary_key'},
426 ALIAS2 => $sb->{_sql_linkalias},
427 FIELD2 => 'Local' . $linkfield
436 Handle date fields. (Created, LastTold..)
439 1: type of link. (Probably not necessary.)
444 my ( $sb, $field, $op, $value, @rest ) = @_;
446 die "Invalid Date Op: $op"
447 unless $op =~ /^(=|>|<|>=|<=)$/;
449 my $meta = $FIELDS{$field};
450 die "Incorrect Meta Data for $field"
451 unless ( defined $meta->[1] );
453 my $date = RT::Date->new( $sb->CurrentUser );
454 $date->Set( Format => 'unknown', Value => $value );
458 # if we're specifying =, that means we want everything on a
459 # particular single day. in the database, we need to check for >
460 # and < the edges of that day.
462 $date->SetToMidnight( Timezone => 'server' );
463 my $daystart = $date->ISO;
465 my $dayend = $date->ISO;
481 ENTRYAGGREGATOR => 'AND',
499 Handle simple fields which are just strings. (Subject,Type)
507 my ( $sb, $field, $op, $value, @rest ) = @_;
511 # =, !=, LIKE, NOT LIKE
522 =head2 _TransDateLimit
524 Handle fields limiting based on Transaction Date.
526 The inpupt value must be in a format parseable by Time::ParseDate
533 # This routine should really be factored into translimit.
534 sub _TransDateLimit {
535 my ( $sb, $field, $op, $value, @rest ) = @_;
537 # See the comments for TransLimit, they apply here too
539 $sb->{_sql_transalias} = $sb->NewAlias('Transactions')
540 unless defined $sb->{_sql_transalias};
542 my $date = RT::Date->new( $sb->CurrentUser );
543 $date->Set( Format => 'unknown', Value => $value );
548 # if we're specifying =, that means we want everything on a
549 # particular single day. in the database, we need to check for >
550 # and < the edges of that day.
552 $date->SetToMidnight( Timezone => 'server' );
553 my $daystart = $date->ISO;
555 my $dayend = $date->ISO;
558 ALIAS => $sb->{_sql_transalias},
566 ALIAS => $sb->{_sql_transalias},
572 ENTRYAGGREGATOR => 'AND',
577 # not searching for a single day
580 #Search for the right field
582 ALIAS => $sb->{_sql_transalias},
591 # Join Transactions to Tickets
594 FIELD1 => $sb->{'primary_key'}, # UGH!
595 ALIAS2 => $sb->{_sql_transalias},
600 ALIAS => $sb->{_sql_transalias},
601 FIELD => 'ObjectType',
602 VALUE => 'RT::Ticket'
610 Limit based on the Content of a transaction or the ContentType.
619 # Content, ContentType, Filename
621 # If only this was this simple. We've got to do something
624 #Basically, we want to make sure that the limits apply to
625 #the same attachment, rather than just another attachment
626 #for the same ticket, no matter how many clauses we lump
627 #on. We put them in TicketAliases so that they get nuked
628 #when we redo the join.
630 # In the SQL, we might have
631 # (( Content = foo ) or ( Content = bar AND Content = baz ))
632 # The AND group should share the same Alias.
634 # Actually, maybe it doesn't matter. We use the same alias and it
635 # works itself out? (er.. different.)
637 # Steal more from _ProcessRestrictions
639 # FIXME: Maybe look at the previous FooLimit call, and if it was a
640 # TransLimit and EntryAggregator == AND, reuse the Aliases?
642 # Or better - store the aliases on a per subclause basis - since
643 # those are going to be the things we want to relate to each other,
646 # maybe we should not allow certain kinds of aggregation of these
647 # clauses and do a psuedo regex instead? - the problem is getting
648 # them all into the same subclause when you have (A op B op C) - the
649 # way they get parsed in the tree they're in different subclauses.
651 my ( $self, $field, $op, $value, @rest ) = @_;
653 $self->{_sql_transalias} = $self->NewAlias('Transactions')
654 unless defined $self->{_sql_transalias};
655 $self->{_sql_trattachalias} = $self->NewAlias('Attachments')
656 unless defined $self->{_sql_trattachalias};
660 #Search for the right field
662 ALIAS => $self->{_sql_trattachalias},
671 ALIAS1 => $self->{_sql_trattachalias},
672 FIELD1 => 'TransactionId',
673 ALIAS2 => $self->{_sql_transalias},
677 # Join Transactions to Tickets
680 FIELD1 => $self->{'primary_key'}, # Why not use "id" here?
681 ALIAS2 => $self->{_sql_transalias},
686 ALIAS => $self->{_sql_transalias},
687 FIELD => 'ObjectType',
688 VALUE => 'RT::Ticket',
689 ENTRYAGGREGATOR => 'AND'
698 Handle watcher limits. (Requestor, CC, etc..)
706 # Test to make sure that you can search for tickets by requestor address and
710 my $u1 = RT::User->new($RT::SystemUser);
711 ($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
713 my $u2 = RT::User->new($RT::SystemUser);
714 ($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
717 my $t1 = RT::Ticket->new($RT::SystemUser);
719 ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
722 my $t2 = RT::Ticket->new($RT::SystemUser);
723 ($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
727 my $t3 = RT::Ticket->new($RT::SystemUser);
728 ($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
732 my $tix1 = RT::Tickets->new($RT::SystemUser);
733 $tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
735 is ($tix1->Count, 3);
737 my $tix2 = RT::Tickets->new($RT::SystemUser);
738 $tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
740 is ($tix2->Count, 3);
743 my $tix3 = RT::Tickets->new($RT::SystemUser);
744 $tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
746 is ($tix3->Count, 2);
748 my $tix4 = RT::Tickets->new($RT::SystemUser);
749 $tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
751 is ($tix4->Count, 2);
753 # Searching for tickets that have two requestors isn't supported
754 # There's no way to differentiate "one requestor name that matches foo and bar"
755 # and "two requestors, one matching foo and one matching bar"
757 # my $tix5 = RT::Tickets->new($RT::SystemUser);
758 # $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
760 # is ($tix5->Count, 1);
762 # my $tix6 = RT::Tickets->new($RT::SystemUser);
763 # $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
765 # is ($tix6->Count, 1);
779 # Find out what sort of watcher we're looking for
782 $fieldname = $field->[0]->[0];
786 $field = [ [ $field, $op, $value, %rest ] ]; # gross hack
788 my $meta = $FIELDS{$fieldname};
789 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
791 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
792 # search by id and Name at the same time, this is workaround
793 # to preserve backward compatibility
794 if ( $fieldname eq 'Owner' ) {
796 for my $chunk ( splice @$field ) {
797 my ( $f, $op, $value, %rest ) = @$chunk;
798 if ( !$rest{SUBKEY} && $op =~ /^!?=$/ ) {
799 $self->_OpenParen unless $flag++;
800 my $o = RT::User->new( $self->CurrentUser );
811 push @$field, $chunk;
814 $self->_CloseParen if $flag;
815 return unless @$field;
818 my $users = $self->_WatcherJoin($type);
820 # If we're looking for multiple watchers of a given type,
821 # TicketSQL will be handing it to us as an array of clauses in
824 for my $chunk (@$field) {
825 ( $field, $op, $value, %rest ) = @$chunk;
826 $rest{SUBKEY} ||= 'EmailAddress';
828 my $re_negative_op = qr[!=|NOT LIKE];
829 $self->_OpenParen if $op =~ /$re_negative_op/;
833 FIELD => $rest{SUBKEY},
840 if ( $op =~ /$re_negative_op/ ) {
843 FIELD => $rest{SUBKEY},
846 ENTRYAGGREGATOR => 'OR',
856 Helper function which provides joins to a watchers table both for limits
865 # we cache joins chain per watcher type
866 # if we limit by requestor then we shouldn't join requestors again
867 # for sort or limit on other requestors
868 if ( $self->{'_watcher_join_users_alias'}{ $type || 'any' } ) {
869 return $self->{'_watcher_join_users_alias'}{ $type || 'any' };
872 # we always have watcher groups for ticket
873 # this join should be NORMAL
874 # XXX: if we change this from Join to NewAlias+Limit
875 # then Pg will complain because SB build wrong query.
876 # Query looks like "FROM (Tickets LEFT JOIN CGM ON(Groups.id = CGM.GroupId)), Groups"
877 # Pg doesn't like that fact that it doesn't know about Groups table yet when
878 # join CGM table into Tickets. Problem is in Join method which doesn't use
879 # ALIAS1 argument when build braces.
880 my $groups = $self->Join(
884 FIELD2 => 'Instance',
885 ENTRYAGGREGATOR => 'AND'
890 VALUE => 'RT::Ticket-Role',
891 ENTRYAGGREGATOR => 'AND'
897 ENTRYAGGREGATOR => 'AND'
901 my $groupmembers = $self->Join(
905 TABLE2 => 'CachedGroupMembers',
909 # XXX: work around, we must hide groups that
910 # are members of the role group we search in,
911 # otherwise them result in wrong NULLs in Users
912 # table and break ordering. Now, we know that
913 # RT doesn't allow to add groups as members of the
914 # ticket roles, so we just hide entries in CGM table
915 # with MemberId == GroupId from results
917 LEFTJOIN => $groupmembers,
920 VALUE => "$groupmembers.MemberId",
923 my $users = $self->Join(
925 ALIAS1 => $groupmembers,
926 FIELD1 => 'MemberId',
930 return $self->{'_watcher_join_users_alias'}{ $type || 'any' } = $users;
933 =head2 _WatcherMembershipLimit
935 Handle watcher membership limits, i.e. whether the watcher belongs to a
936 specific group or not.
941 SELECT DISTINCT main.*
945 CachedGroupMembers CachedGroupMembers_2,
948 (main.EffectiveId = main.id)
950 (main.Status != 'deleted')
952 (main.Type = 'ticket')
955 (Users_3.EmailAddress = '22')
957 (Groups_1.Domain = 'RT::Ticket-Role')
959 (Groups_1.Type = 'RequestorGroup')
962 Groups_1.Instance = main.id
964 Groups_1.id = CachedGroupMembers_2.GroupId
966 CachedGroupMembers_2.MemberId = Users_3.id
972 sub _WatcherMembershipLimit {
973 my ( $self, $field, $op, $value, @rest ) = @_;
978 my $groups = $self->NewAlias('Groups');
979 my $groupmembers = $self->NewAlias('CachedGroupMembers');
980 my $users = $self->NewAlias('Users');
981 my $memberships = $self->NewAlias('CachedGroupMembers');
983 if ( ref $field ) { # gross hack
984 my @bundle = @$field;
986 for my $chunk (@bundle) {
987 ( $field, $op, $value, @rest ) = @$chunk;
989 ALIAS => $memberships,
1000 ALIAS => $memberships,
1008 # {{{ Tie to groups for tickets we care about
1012 VALUE => 'RT::Ticket-Role',
1013 ENTRYAGGREGATOR => 'AND'
1018 FIELD1 => 'Instance',
1025 # If we care about which sort of watcher
1026 my $meta = $FIELDS{$field};
1027 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1034 ENTRYAGGREGATOR => 'AND'
1041 ALIAS2 => $groupmembers,
1046 ALIAS1 => $groupmembers,
1047 FIELD1 => 'MemberId',
1053 ALIAS1 => $memberships,
1054 FIELD1 => 'MemberId',
1063 sub _LinkFieldLimit {
1068 if ( $restriction->{'TYPE'} ) {
1069 $self->SUPER::Limit(
1070 ALIAS => $LinkAlias,
1071 ENTRYAGGREGATOR => 'AND',
1074 VALUE => $restriction->{'TYPE'}
1078 #If we're trying to limit it to things that are target of
1079 if ( $restriction->{'TARGET'} ) {
1081 # If the TARGET is an integer that means that we want to look at
1082 # the LocalTarget field. otherwise, we want to look at the
1085 if ( $restriction->{'TARGET'} =~ /^(\d+)$/ ) {
1086 $matchfield = "LocalTarget";
1089 $matchfield = "Target";
1091 $self->SUPER::Limit(
1092 ALIAS => $LinkAlias,
1093 ENTRYAGGREGATOR => 'AND',
1094 FIELD => $matchfield,
1096 VALUE => $restriction->{'TARGET'}
1099 #If we're searching on target, join the base to ticket.id
1102 FIELD1 => $self->{'primary_key'},
1103 ALIAS2 => $LinkAlias,
1104 FIELD2 => 'LocalBase'
1108 #If we're trying to limit it to things that are base of
1109 elsif ( $restriction->{'BASE'} ) {
1111 # If we're trying to match a numeric link, we want to look at
1112 # LocalBase, otherwise we want to look at "Base"
1114 if ( $restriction->{'BASE'} =~ /^(\d+)$/ ) {
1115 $matchfield = "LocalBase";
1118 $matchfield = "Base";
1121 $self->SUPER::Limit(
1122 ALIAS => $LinkAlias,
1123 ENTRYAGGREGATOR => 'AND',
1124 FIELD => $matchfield,
1126 VALUE => $restriction->{'BASE'}
1129 #If we're searching on base, join the target to ticket.id
1132 FIELD1 => $self->{'primary_key'},
1133 ALIAS2 => $LinkAlias,
1134 FIELD2 => 'LocalTarget'
1141 Limit based on Keywords
1148 sub _CustomFieldLimit {
1149 my ( $self, $_field, $op, $value, @rest ) = @_;
1152 my $field = $rest{SUBKEY} || die "No field specified";
1154 # For our sanity, we can only limit on one queue at a time
1157 if ( $field =~ /^(.+?)\.{(.+)}$/ ) {
1161 $field = $1 if $field =~ /^{(.+)}$/; # trim { }
1163 # If we're trying to find custom fields that don't match something, we
1164 # want tickets where the custom field has no value at all. Note that
1165 # we explicitly don't include the "IS NULL" case, since we would
1166 # otherwise end up with a redundant clause.
1168 my $null_columns_ok;
1169 if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
1170 $null_columns_ok = 1;
1176 my $q = RT::Queue->new( $self->CurrentUser );
1177 $q->Load($queue) if ($queue);
1181 $cf = $q->CustomField($field);
1184 $cf = RT::CustomField->new( $self->CurrentUser );
1185 $cf->LoadByNameAndQueue( Queue => '0', Name => $field );
1193 my $cfkey = $cfid ? $cfid : "$queue.$field";
1195 # Perform one Join per CustomField
1196 if ( $self->{_sql_object_cf_alias}{$cfkey} ) {
1197 $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey};
1201 $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
1205 TABLE2 => 'ObjectCustomFieldValues',
1206 FIELD2 => 'ObjectId',
1208 $self->SUPER::Limit(
1209 LEFTJOIN => $TicketCFs,
1210 FIELD => 'CustomField',
1212 ENTRYAGGREGATOR => 'AND'
1216 my $cfalias = $self->Join(
1218 EXPRESSION => "'$field'",
1219 TABLE2 => 'CustomFields',
1223 $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
1227 TABLE2 => 'ObjectCustomFieldValues',
1228 FIELD2 => 'CustomField',
1230 $self->SUPER::Limit(
1231 LEFTJOIN => $TicketCFs,
1232 FIELD => 'ObjectId',
1235 ENTRYAGGREGATOR => 'AND',
1238 $self->SUPER::Limit(
1239 LEFTJOIN => $TicketCFs,
1240 FIELD => 'ObjectType',
1241 VALUE => ref( $self->NewItem )
1242 , # we want a single item, not a collection
1243 ENTRYAGGREGATOR => 'AND'
1245 $self->SUPER::Limit(
1246 LEFTJOIN => $TicketCFs,
1247 FIELD => 'Disabled',
1250 ENTRYAGGREGATOR => 'AND'
1254 $self->_OpenParen if ($null_columns_ok);
1257 ALIAS => $TicketCFs,
1265 if ($null_columns_ok) {
1267 ALIAS => $TicketCFs,
1272 ENTRYAGGREGATOR => 'OR',
1275 $self->_CloseParen if ($null_columns_ok);
1279 # End Helper Functions
1281 # End of SQL Stuff -------------------------------------------------
1283 # {{{ Limit the result set based on content
1289 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1290 Generally best called from LimitFoo methods
1300 DESCRIPTION => undef,
1303 $args{'DESCRIPTION'} = $self->loc(
1304 "[_1] [_2] [_3]", $args{'FIELD'},
1305 $args{'OPERATOR'}, $args{'VALUE'}
1307 if ( !defined $args{'DESCRIPTION'} );
1309 my $index = $self->_NextIndex;
1311 #make the TicketRestrictions hash the equivalent of whatever we just passed in;
1313 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1315 $self->{'RecalcTicketLimits'} = 1;
1317 # If we're looking at the effective id, we don't want to append the other clause
1318 # which limits us to tickets where id = effective id
1319 if ( $args{'FIELD'} eq 'EffectiveId'
1320 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1322 $self->{'looking_at_effective_id'} = 1;
1325 if ( $args{'FIELD'} eq 'Type'
1326 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1328 $self->{'looking_at_type'} = 1;
1338 Returns a frozen string suitable for handing back to ThawLimits.
1342 sub _FreezeThawKeys {
1343 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1347 # {{{ sub FreezeLimits
1352 require MIME::Base64;
1353 MIME::Base64::base64_encode(
1354 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1361 Take a frozen Limits string generated by FreezeLimits and make this tickets
1362 object have that set of limits.
1366 # {{{ sub ThawLimits
1372 #if we don't have $in, get outta here.
1373 return undef unless ($in);
1375 $self->{'RecalcTicketLimits'} = 1;
1378 require MIME::Base64;
1380 #We don't need to die if the thaw fails.
1381 @{$self}{ $self->_FreezeThawKeys }
1382 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1384 $RT::Logger->error($@) if $@;
1390 # {{{ Limit by enum or foreign key
1392 # {{{ sub LimitQueue
1396 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1397 OPERATOR is one of = or !=. (It defaults to =).
1398 VALUE is a queue id or Name.
1411 #TODO VALUE should also take queue objects
1412 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1413 my $queue = new RT::Queue( $self->CurrentUser );
1414 $queue->Load( $args{'VALUE'} );
1415 $args{'VALUE'} = $queue->Id;
1418 # What if they pass in an Id? Check for isNum() and convert to
1421 #TODO check for a valid queue here
1425 VALUE => $args{'VALUE'},
1426 OPERATOR => $args{'OPERATOR'},
1427 DESCRIPTION => join(
1428 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1436 # {{{ sub LimitStatus
1440 Takes a paramhash with the fields OPERATOR and VALUE.
1441 OPERATOR is one of = or !=.
1444 RT adds Status != 'deleted' until object has
1445 allow_deleted_search internal property set.
1446 $tickets->{'allow_deleted_search'} = 1;
1447 $tickets->LimitStatus( VALUE => 'deleted' );
1459 VALUE => $args{'VALUE'},
1460 OPERATOR => $args{'OPERATOR'},
1461 DESCRIPTION => join( ' ',
1462 $self->loc('Status'), $args{'OPERATOR'},
1463 $self->loc( $args{'VALUE'} ) ),
1469 # {{{ sub IgnoreType
1473 If called, this search will not automatically limit the set of results found
1474 to tickets of type "Ticket". Tickets of other types, such as "project" and
1475 "approval" will be found.
1482 # Instead of faking a Limit that later gets ignored, fake up the
1483 # fact that we're already looking at type, so that the check in
1484 # Tickets_Overlay_SQL/FromSQL goes down the right branch
1486 # $self->LimitType(VALUE => '__any');
1487 $self->{looking_at_type} = 1;
1496 Takes a paramhash with the fields OPERATOR and VALUE.
1497 OPERATOR is one of = or !=, it defaults to "=".
1498 VALUE is a string to search for in the type of the ticket.
1513 VALUE => $args{'VALUE'},
1514 OPERATOR => $args{'OPERATOR'},
1515 DESCRIPTION => join( ' ',
1516 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
1524 # {{{ Limit by string field
1526 # {{{ sub LimitSubject
1530 Takes a paramhash with the fields OPERATOR and VALUE.
1531 OPERATOR is one of = or !=.
1532 VALUE is a string to search for in the subject of the ticket.
1541 VALUE => $args{'VALUE'},
1542 OPERATOR => $args{'OPERATOR'},
1543 DESCRIPTION => join( ' ',
1544 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1552 # {{{ Limit based on ticket numerical attributes
1553 # Things that can be > < = !=
1559 Takes a paramhash with the fields OPERATOR and VALUE.
1560 OPERATOR is one of =, >, < or !=.
1561 VALUE is a ticket Id to search for
1574 VALUE => $args{'VALUE'},
1575 OPERATOR => $args{'OPERATOR'},
1577 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1583 # {{{ sub LimitPriority
1585 =head2 LimitPriority
1587 Takes a paramhash with the fields OPERATOR and VALUE.
1588 OPERATOR is one of =, >, < or !=.
1589 VALUE is a value to match the ticket\'s priority against
1597 FIELD => 'Priority',
1598 VALUE => $args{'VALUE'},
1599 OPERATOR => $args{'OPERATOR'},
1600 DESCRIPTION => join( ' ',
1601 $self->loc('Priority'),
1602 $args{'OPERATOR'}, $args{'VALUE'}, ),
1608 # {{{ sub LimitInitialPriority
1610 =head2 LimitInitialPriority
1612 Takes a paramhash with the fields OPERATOR and VALUE.
1613 OPERATOR is one of =, >, < or !=.
1614 VALUE is a value to match the ticket\'s initial priority against
1619 sub LimitInitialPriority {
1623 FIELD => 'InitialPriority',
1624 VALUE => $args{'VALUE'},
1625 OPERATOR => $args{'OPERATOR'},
1626 DESCRIPTION => join( ' ',
1627 $self->loc('Initial Priority'), $args{'OPERATOR'},
1634 # {{{ sub LimitFinalPriority
1636 =head2 LimitFinalPriority
1638 Takes a paramhash with the fields OPERATOR and VALUE.
1639 OPERATOR is one of =, >, < or !=.
1640 VALUE is a value to match the ticket\'s final priority against
1644 sub LimitFinalPriority {
1648 FIELD => 'FinalPriority',
1649 VALUE => $args{'VALUE'},
1650 OPERATOR => $args{'OPERATOR'},
1651 DESCRIPTION => join( ' ',
1652 $self->loc('Final Priority'), $args{'OPERATOR'},
1659 # {{{ sub LimitTimeWorked
1661 =head2 LimitTimeWorked
1663 Takes a paramhash with the fields OPERATOR and VALUE.
1664 OPERATOR is one of =, >, < or !=.
1665 VALUE is a value to match the ticket's TimeWorked attribute
1669 sub LimitTimeWorked {
1673 FIELD => 'TimeWorked',
1674 VALUE => $args{'VALUE'},
1675 OPERATOR => $args{'OPERATOR'},
1676 DESCRIPTION => join( ' ',
1677 $self->loc('Time worked'),
1678 $args{'OPERATOR'}, $args{'VALUE'}, ),
1684 # {{{ sub LimitTimeLeft
1686 =head2 LimitTimeLeft
1688 Takes a paramhash with the fields OPERATOR and VALUE.
1689 OPERATOR is one of =, >, < or !=.
1690 VALUE is a value to match the ticket's TimeLeft attribute
1698 FIELD => 'TimeLeft',
1699 VALUE => $args{'VALUE'},
1700 OPERATOR => $args{'OPERATOR'},
1701 DESCRIPTION => join( ' ',
1702 $self->loc('Time left'),
1703 $args{'OPERATOR'}, $args{'VALUE'}, ),
1711 # {{{ Limiting based on attachment attributes
1713 # {{{ sub LimitContent
1717 Takes a paramhash with the fields OPERATOR and VALUE.
1718 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1719 VALUE is a string to search for in the body of the ticket
1728 VALUE => $args{'VALUE'},
1729 OPERATOR => $args{'OPERATOR'},
1730 DESCRIPTION => join( ' ',
1731 $self->loc('Ticket content'), $args{'OPERATOR'},
1738 # {{{ sub LimitFilename
1740 =head2 LimitFilename
1742 Takes a paramhash with the fields OPERATOR and VALUE.
1743 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1744 VALUE is a string to search for in the body of the ticket
1752 FIELD => 'Filename',
1753 VALUE => $args{'VALUE'},
1754 OPERATOR => $args{'OPERATOR'},
1755 DESCRIPTION => join( ' ',
1756 $self->loc('Attachment filename'), $args{'OPERATOR'},
1762 # {{{ sub LimitContentType
1764 =head2 LimitContentType
1766 Takes a paramhash with the fields OPERATOR and VALUE.
1767 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1768 VALUE is a content type to search ticket attachments for
1772 sub LimitContentType {
1776 FIELD => 'ContentType',
1777 VALUE => $args{'VALUE'},
1778 OPERATOR => $args{'OPERATOR'},
1779 DESCRIPTION => join( ' ',
1780 $self->loc('Ticket content type'), $args{'OPERATOR'},
1789 # {{{ Limiting based on people
1791 # {{{ sub LimitOwner
1795 Takes a paramhash with the fields OPERATOR and VALUE.
1796 OPERATOR is one of = or !=.
1808 my $owner = new RT::User( $self->CurrentUser );
1809 $owner->Load( $args{'VALUE'} );
1811 # FIXME: check for a valid $owner
1814 VALUE => $args{'VALUE'},
1815 OPERATOR => $args{'OPERATOR'},
1816 DESCRIPTION => join( ' ',
1817 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
1824 # {{{ Limiting watchers
1826 # {{{ sub LimitWatcher
1830 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1831 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1832 VALUE is a value to match the ticket\'s watcher email addresses against
1833 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1837 my $t1 = RT::Ticket->new($RT::SystemUser);
1838 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
1853 #build us up a description
1854 my ( $watcher_type, $desc );
1855 if ( $args{'TYPE'} ) {
1856 $watcher_type = $args{'TYPE'};
1859 $watcher_type = "Watcher";
1863 FIELD => $watcher_type,
1864 VALUE => $args{'VALUE'},
1865 OPERATOR => $args{'OPERATOR'},
1866 TYPE => $args{'TYPE'},
1867 DESCRIPTION => join( ' ',
1868 $self->loc($watcher_type),
1869 $args{'OPERATOR'}, $args{'VALUE'}, ),
1873 sub LimitRequestor {
1876 $RT::Logger->error( "Tickets->LimitRequestor is deprecated at ("
1877 . join( ":", caller )
1879 $self->LimitWatcher( TYPE => 'Requestor', @_ );
1889 # {{{ Limiting based on links
1893 =head2 LimitLinkedTo
1895 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1896 TYPE limits the sort of link we want to search on
1898 TYPE = { RefersTo, MemberOf, DependsOn }
1900 TARGET is the id or URI of the TARGET of the link
1901 (TARGET used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as TARGET
1915 FIELD => 'LinkedTo',
1917 TARGET => ( $args{'TARGET'} || $args{'TICKET'} ),
1918 TYPE => $args{'TYPE'},
1919 DESCRIPTION => $self->loc(
1920 "Tickets [_1] by [_2]",
1921 $self->loc( $args{'TYPE'} ),
1922 ( $args{'TARGET'} || $args{'TICKET'} )
1929 # {{{ LimitLinkedFrom
1931 =head2 LimitLinkedFrom
1933 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1934 TYPE limits the sort of link we want to search on
1937 BASE is the id or URI of the BASE of the link
1938 (BASE used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as BASE
1943 sub LimitLinkedFrom {
1952 # translate RT2 From/To naming to RT3 TicketSQL naming
1953 my %fromToMap = qw(DependsOn DependentOn
1955 RefersTo ReferredToBy);
1957 my $type = $args{'TYPE'};
1958 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
1961 FIELD => 'LinkedTo',
1963 BASE => ( $args{'BASE'} || $args{'TICKET'} ),
1965 DESCRIPTION => $self->loc(
1966 "Tickets [_1] [_2]",
1967 $self->loc( $args{'TYPE'} ),
1968 ( $args{'BASE'} || $args{'TICKET'} )
1978 my $ticket_id = shift;
1979 $self->LimitLinkedTo(
1980 TARGET => "$ticket_id",
1988 # {{{ LimitHasMember
1989 sub LimitHasMember {
1991 my $ticket_id = shift;
1992 $self->LimitLinkedFrom(
1993 BASE => "$ticket_id",
1994 TYPE => 'HasMember',
2001 # {{{ LimitDependsOn
2003 sub LimitDependsOn {
2005 my $ticket_id = shift;
2006 $self->LimitLinkedTo(
2007 TARGET => "$ticket_id",
2008 TYPE => 'DependsOn',
2015 # {{{ LimitDependedOnBy
2017 sub LimitDependedOnBy {
2019 my $ticket_id = shift;
2020 $self->LimitLinkedFrom(
2021 BASE => "$ticket_id",
2022 TYPE => 'DependentOn',
2033 my $ticket_id = shift;
2034 $self->LimitLinkedTo(
2035 TARGET => "$ticket_id",
2043 # {{{ LimitReferredToBy
2045 sub LimitReferredToBy {
2047 my $ticket_id = shift;
2048 $self->LimitLinkedFrom(
2049 BASE => "$ticket_id",
2050 TYPE => 'ReferredToBy',
2059 # {{{ limit based on ticket date attribtes
2063 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2065 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2067 OPERATOR is one of > or <
2068 VALUE is a date and time in ISO format in GMT
2069 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2071 There are also helper functions of the form LimitFIELD that eliminate
2072 the need to pass in a FIELD argument.
2086 #Set the description if we didn't get handed it above
2087 unless ( $args{'DESCRIPTION'} ) {
2088 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2089 . $args{'OPERATOR'} . " "
2090 . $args{'VALUE'} . " GMT";
2093 $self->Limit(%args);
2101 $self->LimitDate( FIELD => 'Created', @_ );
2106 $self->LimitDate( FIELD => 'Due', @_ );
2112 $self->LimitDate( FIELD => 'Starts', @_ );
2118 $self->LimitDate( FIELD => 'Started', @_ );
2123 $self->LimitDate( FIELD => 'Resolved', @_ );
2128 $self->LimitDate( FIELD => 'Told', @_ );
2131 sub LimitLastUpdated {
2133 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2137 # {{{ sub LimitTransactionDate
2139 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2141 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2143 OPERATOR is one of > or <
2144 VALUE is a date and time in ISO format in GMT
2149 sub LimitTransactionDate {
2152 FIELD => 'TransactionDate',
2159 # <20021217042756.GK28744@pallas.fsck.com>
2160 # "Kill It" - Jesse.
2162 #Set the description if we didn't get handed it above
2163 unless ( $args{'DESCRIPTION'} ) {
2164 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2165 . $args{'OPERATOR'} . " "
2166 . $args{'VALUE'} . " GMT";
2169 $self->Limit(%args);
2177 # {{{ Limit based on custom fields
2178 # {{{ sub LimitCustomField
2180 =head2 LimitCustomField
2182 Takes a paramhash of key/value pairs with the following keys:
2186 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2188 =item OPERATOR - The usual Limit operators
2190 =item VALUE - The value to compare against
2196 sub LimitCustomField {
2200 CUSTOMFIELD => undef,
2202 DESCRIPTION => undef,
2203 FIELD => 'CustomFieldValue',
2208 my $CF = RT::CustomField->new( $self->CurrentUser );
2209 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2210 $CF->Load( $args{CUSTOMFIELD} );
2213 $CF->LoadByNameAndQueue(
2214 Name => $args{CUSTOMFIELD},
2215 Queue => $args{QUEUE}
2217 $args{CUSTOMFIELD} = $CF->Id;
2220 #If we are looking to compare with a null value.
2221 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2222 $args{'DESCRIPTION'}
2223 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2225 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2226 $args{'DESCRIPTION'}
2227 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2230 # if we're not looking to compare with a null value
2232 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2233 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2238 my $qo = new RT::Queue( $self->CurrentUser );
2239 $qo->Load( $CF->Queue );
2244 @rest = ( ENTRYAGGREGATOR => 'AND' )
2245 if ( $CF->Type eq 'SelectMultiple' );
2248 VALUE => $args{VALUE},
2252 ? $q . ".{" . $CF->Name . "}"
2255 OPERATOR => $args{OPERATOR},
2260 $self->{'RecalcTicketLimits'} = 1;
2266 # {{{ sub _NextIndex
2270 Keep track of the counter for the array of restrictions
2276 return ( $self->{'restriction_index'}++ );
2283 # {{{ Core bits to make this a DBIx::SearchBuilder object
2288 $self->{'table'} = "Tickets";
2289 $self->{'RecalcTicketLimits'} = 1;
2290 $self->{'looking_at_effective_id'} = 0;
2291 $self->{'looking_at_type'} = 0;
2292 $self->{'restriction_index'} = 1;
2293 $self->{'primary_key'} = "id";
2294 delete $self->{'items_array'};
2295 delete $self->{'item_map'};
2296 delete $self->{'columns_to_display'};
2297 $self->SUPER::_Init(@_);
2308 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2309 return ( $self->SUPER::Count() );
2317 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2318 return ( $self->SUPER::CountAll() );
2323 # {{{ sub ItemsArrayRef
2325 =head2 ItemsArrayRef
2327 Returns a reference to the set of all items found in this search
2335 unless ( $self->{'items_array'} ) {
2337 my $placeholder = $self->_ItemsCounter;
2338 $self->GotoFirstItem();
2339 while ( my $item = $self->Next ) {
2340 push( @{ $self->{'items_array'} }, $item );
2342 $self->GotoItem($placeholder);
2343 $self->{'items_array'}
2344 = $self->ItemsOrderBy( $self->{'items_array'} );
2346 return ( $self->{'items_array'} );
2355 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2357 my $Ticket = $self->SUPER::Next();
2358 if ( ( defined($Ticket) ) and ( ref($Ticket) ) ) {
2360 if ( $Ticket->__Value('Status') eq 'deleted'
2361 && !$self->{'allow_deleted_search'} )
2363 return ( $self->Next() );
2366 # Since Ticket could be granted with more rights instead
2367 # of being revoked, it's ok if queue rights allow
2368 # ShowTicket. It seems need another query, but we have
2369 # rights cache in Principal::HasRight.
2370 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
2371 || $Ticket->CurrentUserHasRight('ShowTicket') )
2376 if ( $Ticket->__Value('Status') eq 'deleted' ) {
2377 return ( $self->Next() );
2380 # Since Ticket could be granted with more rights instead
2381 # of being revoked, it's ok if queue rights allow
2382 # ShowTicket. It seems need another query, but we have
2383 # rights cache in Principal::HasRight.
2384 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
2385 || $Ticket->CurrentUserHasRight('ShowTicket') )
2390 #If the user doesn't have the right to show this ticket
2392 return ( $self->Next() );
2396 #if there never was any ticket
2407 # {{{ Deal with storing and restoring restrictions
2409 # {{{ sub LoadRestrictions
2411 =head2 LoadRestrictions
2413 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
2414 TODO It is not yet implemented
2420 # {{{ sub DescribeRestrictions
2422 =head2 DescribeRestrictions
2425 Returns a hash keyed by restriction id.
2426 Each element of the hash is currently a one element hash that contains DESCRIPTION which
2427 is a description of the purpose of that TicketRestriction
2431 sub DescribeRestrictions {
2434 my ( $row, %listing );
2436 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2437 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
2444 # {{{ sub RestrictionValues
2446 =head2 RestrictionValues FIELD
2448 Takes a restriction field and returns a list of values this field is restricted
2453 sub RestrictionValues {
2456 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
2457 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
2458 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
2460 keys %{ $self->{'TicketRestrictions'} };
2465 # {{{ sub ClearRestrictions
2467 =head2 ClearRestrictions
2469 Removes all restrictions irretrievably
2473 sub ClearRestrictions {
2475 delete $self->{'TicketRestrictions'};
2476 $self->{'looking_at_effective_id'} = 0;
2477 $self->{'looking_at_type'} = 0;
2478 $self->{'RecalcTicketLimits'} = 1;
2483 # {{{ sub DeleteRestriction
2485 =head2 DeleteRestriction
2487 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
2488 Removes that restriction from the session's limits.
2492 sub DeleteRestriction {
2495 delete $self->{'TicketRestrictions'}{$row};
2497 $self->{'RecalcTicketLimits'} = 1;
2499 #make the underlying easysearch object forget all its preconceptions
2504 # {{{ sub _RestrictionsToClauses
2506 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
2508 sub _RestrictionsToClauses {
2513 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2514 my $restriction = $self->{'TicketRestrictions'}{$row};
2517 #print Dumper($restriction),"\n";
2519 # We need to reimplement the subclause aggregation that SearchBuilder does.
2520 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
2521 # Then SB AND's the different Subclauses together.
2523 # So, we want to group things into Subclauses, convert them to
2524 # SQL, and then join them with the appropriate DefaultEA.
2525 # Then join each subclause group with AND.
2527 my $field = $restriction->{'FIELD'};
2528 my $realfield = $field; # CustomFields fake up a fieldname, so
2529 # we need to figure that out
2532 # Rewrite LinkedTo meta field to the real field
2533 if ( $field =~ /LinkedTo/ ) {
2534 $realfield = $field = $restriction->{'TYPE'};
2538 # Handle subkey fields with a different real field
2539 if ( $field =~ /^(\w+)\./ ) {
2543 die "I don't know about $field yet"
2544 unless ( exists $FIELDS{$realfield}
2545 or $restriction->{CUSTOMFIELD} );
2547 my $type = $FIELDS{$realfield}->[0];
2548 my $op = $restriction->{'OPERATOR'};
2552 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
2555 # this performs the moral equivalent of defined or/dor/C<//>,
2556 # without the short circuiting.You need to use a 'defined or'
2557 # type thing instead of just checking for truth values, because
2558 # VALUE could be 0.(i.e. "false")
2560 # You could also use this, but I find it less aesthetic:
2561 # (although it does short circuit)
2562 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2563 # defined $restriction->{'TICKET'} ?
2564 # $restriction->{TICKET} :
2565 # defined $restriction->{'BASE'} ?
2566 # $restriction->{BASE} :
2567 # defined $restriction->{'TARGET'} ?
2568 # $restriction->{TARGET} )
2570 my $ea = $restriction->{ENTRYAGGREGATOR}
2571 || $DefaultEA{$type}
2574 die "Invalid operator $op for $field ($type)"
2575 unless exists $ea->{$op};
2579 # Each CustomField should be put into a different Clause so they
2580 # are ANDed together.
2581 if ( $restriction->{CUSTOMFIELD} ) {
2582 $realfield = $field;
2585 exists $clause{$realfield} or $clause{$realfield} = [];
2588 $field =~ s!(['"])!\\$1!g;
2589 $value =~ s!(['"])!\\$1!g;
2590 my $data = [ $ea, $type, $field, $op, $value ];
2592 # here is where we store extra data, say if it's a keyword or
2593 # something. (I.e. "TYPE SPECIFIC STUFF")
2595 #print Dumper($data);
2596 push @{ $clause{$realfield} }, $data;
2603 # {{{ sub _ProcessRestrictions
2605 =head2 _ProcessRestrictions PARAMHASH
2607 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
2608 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
2612 sub _ProcessRestrictions {
2615 #Blow away ticket aliases since we'll need to regenerate them for
2617 delete $self->{'TicketAliases'};
2618 delete $self->{'items_array'};
2619 delete $self->{'item_map'};
2620 delete $self->{'raw_rows'};
2621 delete $self->{'rows'};
2622 delete $self->{'count_all'};
2624 my $sql = $self->Query; # Violating the _SQL namespace
2625 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
2627 # "Restrictions to Clauses Branch\n";
2628 my $clauseRef = eval { $self->_RestrictionsToClauses; };
2630 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2634 $sql = $self->ClausesToSQL($clauseRef);
2635 $self->FromSQL($sql) if $sql;
2639 $self->{'RecalcTicketLimits'} = 0;
2643 =head2 _BuildItemMap
2645 # Build up a map of first/last/next/prev items, so that we can display search nav quickly
2652 my $items = $self->ItemsArrayRef;
2655 delete $self->{'item_map'};
2656 if ( $items->[0] ) {
2657 $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
2658 while ( my $item = shift @$items ) {
2659 my $id = $item->EffectiveId;
2660 $self->{'item_map'}->{$id}->{'defined'} = 1;
2661 $self->{'item_map'}->{$id}->{prev} = $prev;
2662 $self->{'item_map'}->{$id}->{next} = $items->[0]->EffectiveId
2666 $self->{'item_map'}->{'last'} = $prev;
2672 Returns an a map of all items found by this search. The map is of the form
2674 $ItemMap->{'first'} = first ticketid found
2675 $ItemMap->{'last'} = last ticketid found
2676 $ItemMap->{$id}->{prev} = the ticket id found before $id
2677 $ItemMap->{$id}->{next} = the ticket id found after $id
2683 $self->_BuildItemMap()
2684 unless ( $self->{'items_array'} and $self->{'item_map'} );
2685 return ( $self->{'item_map'} );
2700 =head2 PrepForSerialization
2702 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
2706 sub PrepForSerialization {
2708 delete $self->{'items'};
2709 $self->RedoSearch();
2714 RT::Tickets supports several flags which alter search behavior:
2717 allow_deleted_search (Otherwise never show deleted tickets in search results)
2718 looking_at_type (otherwise limit to type=ticket)
2720 These flags are set by calling
2722 $tickets->{'flagname'} = 1;
2724 BUG: There should be an API for this
2730 # We assume that we've got some tickets hanging around from before.
2731 ok( my $unlimittickets = RT::Tickets->new( $RT::SystemUser ) );
2732 ok( $unlimittickets->UnLimit );
2733 ok( $unlimittickets->Count > 0, "UnLimited tickets object should return tickets" );