1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 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., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
51 # - Decimated ProcessRestrictions and broke it into multiple
52 # functions joined by a LUT
53 # - Semi-Generic SQL stuff moved to another file
55 # Known Issues: FIXME!
57 # - ClearRestrictions and Reinitialization is messy and unclear. The
58 # only good way to do it is to create a new RT::Tickets object.
62 RT::Tickets - A collection of Ticket objects
68 my $tickets = new RT::Tickets($CurrentUser);
72 A collection of RT::Tickets.
82 no warnings qw(redefine);
85 use DBIx::SearchBuilder::Unique;
87 # Configuration Tables:
89 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
92 our %FIELD_METADATA = (
93 Status => [ 'ENUM', ], #loc_left_pair
94 Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
95 Type => [ 'ENUM', ], #loc_left_pair
96 Creator => [ 'ENUM' => 'User', ], #loc_left_pair
97 LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
98 Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
99 EffectiveId => [ 'INT', ], #loc_left_pair
100 id => [ 'ID', ], #loc_left_pair
101 InitialPriority => [ 'INT', ], #loc_left_pair
102 FinalPriority => [ 'INT', ], #loc_left_pair
103 Priority => [ 'INT', ], #loc_left_pair
104 TimeLeft => [ 'INT', ], #loc_left_pair
105 TimeWorked => [ 'INT', ], #loc_left_pair
106 TimeEstimated => [ 'INT', ], #loc_left_pair
108 Linked => [ 'LINK' ], #loc_left_pair
109 LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
110 LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
111 MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
112 DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
113 RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
114 HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
115 DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
116 DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
117 ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
118 Told => [ 'DATE' => 'Told', ], #loc_left_pair
119 Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
120 Started => [ 'DATE' => 'Started', ], #loc_left_pair
121 Due => [ 'DATE' => 'Due', ], #loc_left_pair
122 Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
123 LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
124 Created => [ 'DATE' => 'Created', ], #loc_left_pair
125 Subject => [ 'STRING', ], #loc_left_pair
126 Content => [ 'TRANSFIELD', ], #loc_left_pair
127 ContentType => [ 'TRANSFIELD', ], #loc_left_pair
128 Filename => [ 'TRANSFIELD', ], #loc_left_pair
129 TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
130 Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
131 Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
132 Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
133 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
134 Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
135 QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
136 QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
137 QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
138 CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
139 CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
140 CF => [ 'CUSTOMFIELD', ], #loc_left_pair
141 Updated => [ 'TRANSDATE', ], #loc_left_pair
142 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
143 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
144 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
145 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
146 HasAttribute => [ 'HASATTRIBUTE', 1 ],
147 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
150 # Mapping of Field Type to Function
152 ENUM => \&_EnumLimit,
155 LINK => \&_LinkLimit,
156 DATE => \&_DateLimit,
157 STRING => \&_StringLimit,
158 TRANSFIELD => \&_TransLimit,
159 TRANSDATE => \&_TransDateLimit,
160 WATCHERFIELD => \&_WatcherLimit,
161 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
162 CUSTOMFIELD => \&_CustomFieldLimit,
163 HASATTRIBUTE => \&_HasAttributeLimit,
165 our %can_bundle = ();# WATCHERFIELD => "yes", );
167 # Default EntryAggregator per type
168 # if you specify OP, you must specify all valid OPs
209 # Helper functions for passing the above lexically scoped tables above
210 # into Tickets_Overlay_SQL.
211 sub FIELDS { return \%FIELD_METADATA }
212 sub dispatch { return \%dispatch }
213 sub can_bundle { return \%can_bundle }
215 # Bring in the clowns.
216 require RT::Tickets_Overlay_SQL;
220 our @SORTFIELDS = qw(id Status
222 Owner Created Due Starts Started
224 Resolved LastUpdated Priority TimeWorked TimeLeft);
228 Returns the list of fields that lists of tickets can easily be sorted by
234 return (@SORTFIELDS);
239 # BEGIN SQL STUFF *********************************
244 $self->SUPER::CleanSlate( @_ );
245 delete $self->{$_} foreach qw(
247 _sql_group_members_aliases
248 _sql_object_cfv_alias
249 _sql_role_group_aliases
252 _sql_u_watchers_alias_for_sort
253 _sql_u_watchers_aliases
254 _sql_current_user_can_see_applied
258 =head1 Limit Helper Routines
260 These routines are the targets of a dispatch table depending on the
261 type of field. They all share the same signature:
263 my ($self,$field,$op,$value,@rest) = @_;
265 The values in @rest should be suitable for passing directly to
266 DBIx::SearchBuilder::Limit.
268 Essentially they are an expanded/broken out (and much simplified)
269 version of what ProcessRestrictions used to do. They're also much
270 more clearly delineated by the TYPE of field being processed.
279 my ( $sb, $field, $op, $value, @rest ) = @_;
281 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
283 die "Invalid operator $op for __Bookmarked__ search on $field"
284 unless $op =~ /^(=|!=)$/;
287 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
288 $tmp = $tmp->Content if $tmp;
293 return $sb->_SQLLimit(
300 # as bookmarked tickets can be merged we have to use a join
301 # but it should be pretty lightweight
302 my $tickets_alias = $sb->Join(
307 FIELD2 => 'EffectiveId',
311 my $ea = $op eq '='? 'OR': 'AND';
312 foreach my $id ( sort @bookmarks ) {
314 ALIAS => $tickets_alias,
318 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
326 Handle Fields which are limited to certain values, and potentially
327 need to be looked up from another class.
329 This subroutine actually handles two different kinds of fields. For
330 some the user is responsible for limiting the values. (i.e. Status,
333 For others, the value specified by the user will be looked by via
337 name of class to lookup in (Optional)
342 my ( $sb, $field, $op, $value, @rest ) = @_;
344 # SQL::Statement changes != to <>. (Can we remove this now?)
345 $op = "!=" if $op eq "<>";
347 die "Invalid Operation: $op for $field"
351 my $meta = $FIELD_METADATA{$field};
352 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
353 my $class = "RT::" . $meta->[1];
354 my $o = $class->new( $sb->CurrentUser );
368 Handle fields where the values are limited to integers. (For example,
369 Priority, TimeWorked.)
377 my ( $sb, $field, $op, $value, @rest ) = @_;
379 die "Invalid Operator $op for $field"
380 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
392 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
395 1: Direction (From, To)
396 2: Link Type (MemberOf, DependsOn, RefersTo)
401 my ( $sb, $field, $op, $value, @rest ) = @_;
403 my $meta = $FIELD_METADATA{$field};
404 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
407 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
411 $is_null = 1 if !$value || $value =~ /^null$/io;
413 my $direction = $meta->[1] || '';
414 my ($matchfield, $linkfield) = ('', '');
415 if ( $direction eq 'To' ) {
416 ($matchfield, $linkfield) = ("Target", "Base");
418 elsif ( $direction eq 'From' ) {
419 ($matchfield, $linkfield) = ("Base", "Target");
421 elsif ( $direction ) {
422 die "Invalid link direction '$direction' for $field\n";
425 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
427 'LinkedFrom', $op, $value, @rest,
428 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
436 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
438 elsif ( $value =~ /\D/ ) {
441 $matchfield = "Local$matchfield" if $is_local;
443 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
444 # SELECT main.* FROM Tickets main
445 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
446 # AND(main.id = Links_1.LocalTarget))
447 # WHERE Links_1.LocalBase IS NULL;
450 my $linkalias = $sb->Join(
455 FIELD2 => 'Local' . $linkfield
458 LEFTJOIN => $linkalias,
466 FIELD => $matchfield,
473 my $linkalias = $sb->Join(
478 FIELD2 => 'Local' . $linkfield
481 LEFTJOIN => $linkalias,
487 LEFTJOIN => $linkalias,
488 FIELD => $matchfield,
495 FIELD => $matchfield,
496 OPERATOR => $is_negative? 'IS': 'IS NOT',
505 Handle date fields. (Created, LastTold..)
508 1: type of link. (Probably not necessary.)
513 my ( $sb, $field, $op, $value, @rest ) = @_;
515 die "Invalid Date Op: $op"
516 unless $op =~ /^(=|>|<|>=|<=)$/;
518 my $meta = $FIELD_METADATA{$field};
519 die "Incorrect Meta Data for $field"
520 unless ( defined $meta->[1] );
522 my $date = RT::Date->new( $sb->CurrentUser );
523 $date->Set( Format => 'unknown', Value => $value );
527 # if we're specifying =, that means we want everything on a
528 # particular single day. in the database, we need to check for >
529 # and < the edges of that day.
531 $date->SetToMidnight( Timezone => 'server' );
532 my $daystart = $date->ISO;
534 my $dayend = $date->ISO;
550 ENTRYAGGREGATOR => 'AND',
568 Handle simple fields which are just strings. (Subject,Type)
576 my ( $sb, $field, $op, $value, @rest ) = @_;
580 # =, !=, LIKE, NOT LIKE
581 if ( (!defined $value || !length $value)
582 && lc($op) ne 'is' && lc($op) ne 'is not'
583 && RT->Config->Get('DatabaseType') eq 'Oracle'
585 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
586 $op = $negative? 'IS NOT': 'IS';
599 =head2 _TransDateLimit
601 Handle fields limiting based on Transaction Date.
603 The inpupt value must be in a format parseable by Time::ParseDate
610 # This routine should really be factored into translimit.
611 sub _TransDateLimit {
612 my ( $sb, $field, $op, $value, @rest ) = @_;
614 # See the comments for TransLimit, they apply here too
616 unless ( $sb->{_sql_transalias} ) {
617 $sb->{_sql_transalias} = $sb->Join(
620 TABLE2 => 'Transactions',
621 FIELD2 => 'ObjectId',
624 ALIAS => $sb->{_sql_transalias},
625 FIELD => 'ObjectType',
626 VALUE => 'RT::Ticket',
627 ENTRYAGGREGATOR => 'AND',
631 my $date = RT::Date->new( $sb->CurrentUser );
632 $date->Set( Format => 'unknown', Value => $value );
637 # if we're specifying =, that means we want everything on a
638 # particular single day. in the database, we need to check for >
639 # and < the edges of that day.
641 $date->SetToMidnight( Timezone => 'server' );
642 my $daystart = $date->ISO;
644 my $dayend = $date->ISO;
647 ALIAS => $sb->{_sql_transalias},
655 ALIAS => $sb->{_sql_transalias},
661 ENTRYAGGREGATOR => 'AND',
666 # not searching for a single day
669 #Search for the right field
671 ALIAS => $sb->{_sql_transalias},
685 Limit based on the Content of a transaction or the ContentType.
694 # Content, ContentType, Filename
696 # If only this was this simple. We've got to do something
699 #Basically, we want to make sure that the limits apply to
700 #the same attachment, rather than just another attachment
701 #for the same ticket, no matter how many clauses we lump
702 #on. We put them in TicketAliases so that they get nuked
703 #when we redo the join.
705 # In the SQL, we might have
706 # (( Content = foo ) or ( Content = bar AND Content = baz ))
707 # The AND group should share the same Alias.
709 # Actually, maybe it doesn't matter. We use the same alias and it
710 # works itself out? (er.. different.)
712 # Steal more from _ProcessRestrictions
714 # FIXME: Maybe look at the previous FooLimit call, and if it was a
715 # TransLimit and EntryAggregator == AND, reuse the Aliases?
717 # Or better - store the aliases on a per subclause basis - since
718 # those are going to be the things we want to relate to each other,
721 # maybe we should not allow certain kinds of aggregation of these
722 # clauses and do a psuedo regex instead? - the problem is getting
723 # them all into the same subclause when you have (A op B op C) - the
724 # way they get parsed in the tree they're in different subclauses.
726 my ( $self, $field, $op, $value, %rest ) = @_;
728 unless ( $self->{_sql_transalias} ) {
729 $self->{_sql_transalias} = $self->Join(
732 TABLE2 => 'Transactions',
733 FIELD2 => 'ObjectId',
736 ALIAS => $self->{_sql_transalias},
737 FIELD => 'ObjectType',
738 VALUE => 'RT::Ticket',
739 ENTRYAGGREGATOR => 'AND',
742 unless ( defined $self->{_sql_trattachalias} ) {
743 $self->{_sql_trattachalias} = $self->_SQLJoin(
744 TYPE => 'LEFT', # not all txns have an attachment
745 ALIAS1 => $self->{_sql_transalias},
747 TABLE2 => 'Attachments',
748 FIELD2 => 'TransactionId',
752 #Search for the right field
753 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
757 ALIAS => $self->{_sql_trattachalias},
764 ENTRYAGGREGATOR => 'AND',
765 ALIAS => $self->{_sql_trattachalias},
774 ALIAS => $self->{_sql_trattachalias},
787 Handle watcher limits. (Requestor, CC, etc..)
803 my $meta = $FIELD_METADATA{ $field };
804 my $type = $meta->[1] || '';
805 my $class = $meta->[2] || 'Ticket';
807 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
808 # search by id and Name at the same time, this is workaround
809 # to preserve backward compatibility
810 if ( $field eq 'Owner' ) {
811 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
812 my $o = RT::User->new( $self->CurrentUser );
813 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
814 $o->$method( $value );
823 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
833 $rest{SUBKEY} ||= 'EmailAddress';
835 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
838 if ( $op =~ /^IS(?: NOT)?$/ ) {
839 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
840 # to avoid joining the table Users into the query, we just join GM
841 # and make sure we don't match records where group is member of itself
843 LEFTJOIN => $group_members,
846 VALUE => "$group_members.MemberId",
850 ALIAS => $group_members,
857 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
859 $op =~ s/!|NOT\s+//i;
861 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
862 # "X = 'Y'" matches more then one user so we try to fetch two records and
863 # do the right thing when there is only one exist and semi-working solution
865 my $users_obj = RT::Users->new( $self->CurrentUser );
867 FIELD => $rest{SUBKEY},
872 $users_obj->RowsPerPage(2);
873 my @users = @{ $users_obj->ItemsArrayRef };
875 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
878 $uid = $users[0]->id if @users;
880 LEFTJOIN => $group_members,
881 ALIAS => $group_members,
887 ALIAS => $group_members,
894 LEFTJOIN => $group_members,
897 VALUE => "$group_members.MemberId",
900 my $users = $self->Join(
902 ALIAS1 => $group_members,
903 FIELD1 => 'MemberId',
910 FIELD => $rest{SUBKEY},
924 my $group_members = $self->_GroupMembersJoin(
925 GroupsAlias => $groups,
929 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
931 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
932 $self->NewAlias('Users');
934 LEFTJOIN => $group_members,
935 ALIAS => $group_members,
937 VALUE => "$users.id",
942 # we join users table without adding some join condition between tables,
943 # the only conditions we have are conditions on the table iteslf,
944 # for example Users.EmailAddress = 'x'. We should add this condition to
945 # the top level of the query and bundle it with another similar conditions,
946 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
947 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
950 SUBCLAUSE => '_sql_u_watchers_'. $users,
952 FIELD => $rest{'SUBKEY'},
957 # A condition which ties Users and Groups (role groups) is a left join condition
958 # of CachedGroupMembers table. To get correct results of the query we check
959 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
962 ALIAS => $group_members,
964 OPERATOR => 'IS NOT',
971 sub _RoleGroupsJoin {
973 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
974 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
975 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
978 # we always have watcher groups for ticket, so we use INNER join
979 my $groups = $self->Join(
981 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
983 FIELD2 => 'Instance',
984 ENTRYAGGREGATOR => 'AND',
990 VALUE => 'RT::'. $args{'Class'} .'-Role',
996 VALUE => $args{'Type'},
999 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1000 unless $args{'New'};
1005 sub _GroupMembersJoin {
1007 my %args = (New => 1, GroupsAlias => undef, @_);
1009 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1010 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1013 my $alias = $self->Join(
1015 ALIAS1 => $args{'GroupsAlias'},
1017 TABLE2 => 'CachedGroupMembers',
1018 FIELD2 => 'GroupId',
1019 ENTRYAGGREGATOR => 'AND',
1022 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1023 unless $args{'New'};
1030 Helper function which provides joins to a watchers table both for limits
1037 my $type = shift || '';
1040 my $groups = $self->_RoleGroupsJoin( Type => $type );
1041 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1042 # XXX: work around, we must hide groups that
1043 # are members of the role group we search in,
1044 # otherwise them result in wrong NULLs in Users
1045 # table and break ordering. Now, we know that
1046 # RT doesn't allow to add groups as members of the
1047 # ticket roles, so we just hide entries in CGM table
1048 # with MemberId == GroupId from results
1049 $self->SUPER::Limit(
1050 LEFTJOIN => $group_members,
1053 VALUE => "$group_members.MemberId",
1056 my $users = $self->Join(
1058 ALIAS1 => $group_members,
1059 FIELD1 => 'MemberId',
1063 return ($groups, $group_members, $users);
1066 =head2 _WatcherMembershipLimit
1068 Handle watcher membership limits, i.e. whether the watcher belongs to a
1069 specific group or not.
1072 1: Field to query on
1074 SELECT DISTINCT main.*
1078 CachedGroupMembers CachedGroupMembers_2,
1081 (main.EffectiveId = main.id)
1083 (main.Status != 'deleted')
1085 (main.Type = 'ticket')
1088 (Users_3.EmailAddress = '22')
1090 (Groups_1.Domain = 'RT::Ticket-Role')
1092 (Groups_1.Type = 'RequestorGroup')
1095 Groups_1.Instance = main.id
1097 Groups_1.id = CachedGroupMembers_2.GroupId
1099 CachedGroupMembers_2.MemberId = Users_3.id
1100 ORDER BY main.id ASC
1105 sub _WatcherMembershipLimit {
1106 my ( $self, $field, $op, $value, @rest ) = @_;
1111 my $groups = $self->NewAlias('Groups');
1112 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1113 my $users = $self->NewAlias('Users');
1114 my $memberships = $self->NewAlias('CachedGroupMembers');
1116 if ( ref $field ) { # gross hack
1117 my @bundle = @$field;
1119 for my $chunk (@bundle) {
1120 ( $field, $op, $value, @rest ) = @$chunk;
1122 ALIAS => $memberships,
1133 ALIAS => $memberships,
1141 # {{{ Tie to groups for tickets we care about
1145 VALUE => 'RT::Ticket-Role',
1146 ENTRYAGGREGATOR => 'AND'
1151 FIELD1 => 'Instance',
1158 # If we care about which sort of watcher
1159 my $meta = $FIELD_METADATA{$field};
1160 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1167 ENTRYAGGREGATOR => 'AND'
1174 ALIAS2 => $groupmembers,
1179 ALIAS1 => $groupmembers,
1180 FIELD1 => 'MemberId',
1186 ALIAS1 => $memberships,
1187 FIELD1 => 'MemberId',
1196 =head2 _CustomFieldDecipher
1198 Try and turn a CF descriptor into (cfid, cfname) object pair.
1202 sub _CustomFieldDecipher {
1203 my ($self, $string) = @_;
1205 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
1206 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1210 my $q = RT::Queue->new( $self->CurrentUser );
1214 # $queue = $q->Name; # should we normalize the queue?
1215 $cf = $q->CustomField( $field );
1218 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1222 elsif ( $field =~ /\D/ ) {
1224 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1225 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1226 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1228 # if there is more then one field the current user can
1229 # see with the same name then we shouldn't return cf object
1230 # as we don't know which one to use
1233 $cf = undef if $cfs->Next;
1237 $cf = RT::CustomField->new( $self->CurrentUser );
1238 $cf->Load( $field );
1241 return ($queue, $field, $cf, $column);
1244 =head2 _CustomFieldJoin
1246 Factor out the Join of custom fields so we can use it for sorting too
1250 sub _CustomFieldJoin {
1251 my ($self, $cfkey, $cfid, $field) = @_;
1252 # Perform one Join per CustomField
1253 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1254 $self->{_sql_cf_alias}{$cfkey} )
1256 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1257 $self->{_sql_cf_alias}{$cfkey} );
1260 my ($TicketCFs, $CFs);
1262 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1266 TABLE2 => 'ObjectCustomFieldValues',
1267 FIELD2 => 'ObjectId',
1269 $self->SUPER::Limit(
1270 LEFTJOIN => $TicketCFs,
1271 FIELD => 'CustomField',
1273 ENTRYAGGREGATOR => 'AND'
1277 my $ocfalias = $self->Join(
1280 TABLE2 => 'ObjectCustomFields',
1281 FIELD2 => 'ObjectId',
1284 $self->SUPER::Limit(
1285 LEFTJOIN => $ocfalias,
1286 ENTRYAGGREGATOR => 'OR',
1287 FIELD => 'ObjectId',
1291 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1293 ALIAS1 => $ocfalias,
1294 FIELD1 => 'CustomField',
1295 TABLE2 => 'CustomFields',
1298 $self->SUPER::Limit(
1300 ENTRYAGGREGATOR => 'AND',
1301 FIELD => 'LookupType',
1302 VALUE => 'RT::Queue-RT::Ticket',
1304 $self->SUPER::Limit(
1306 ENTRYAGGREGATOR => 'AND',
1311 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1315 TABLE2 => 'ObjectCustomFieldValues',
1316 FIELD2 => 'CustomField',
1318 $self->SUPER::Limit(
1319 LEFTJOIN => $TicketCFs,
1320 FIELD => 'ObjectId',
1323 ENTRYAGGREGATOR => 'AND',
1326 $self->SUPER::Limit(
1327 LEFTJOIN => $TicketCFs,
1328 FIELD => 'ObjectType',
1329 VALUE => 'RT::Ticket',
1330 ENTRYAGGREGATOR => 'AND'
1332 $self->SUPER::Limit(
1333 LEFTJOIN => $TicketCFs,
1334 FIELD => 'Disabled',
1337 ENTRYAGGREGATOR => 'AND'
1340 return ($TicketCFs, $CFs);
1343 =head2 _CustomFieldLimit
1345 Limit based on CustomFields
1352 sub _CustomFieldLimit {
1353 my ( $self, $_field, $op, $value, %rest ) = @_;
1355 my $field = $rest{'SUBKEY'} || die "No field specified";
1357 # For our sanity, we can only limit on one queue at a time
1359 my ($queue, $cfid, $cf, $column);
1360 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1361 $cfid = $cf ? $cf->id : 0 ;
1363 # If we're trying to find custom fields that don't match something, we
1364 # want tickets where the custom field has no value at all. Note that
1365 # we explicitly don't include the "IS NULL" case, since we would
1366 # otherwise end up with a redundant clause.
1368 my ($negative_op, $null_op, $inv_op, $range_op)
1369 = $self->ClassifySQLOperation( $op );
1373 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1374 return 'MATCHES' if $op eq '=';
1375 return 'NOT MATCHES' if $op eq '!=';
1379 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1381 my $cfkey = $cfid ? $cfid : "$queue.$field";
1383 if ( $null_op && !$column ) {
1384 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1385 # we can reuse our default joins for this operation
1386 # with column specified we have different situation
1387 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1390 ALIAS => $TicketCFs,
1399 OPERATOR => 'IS NOT',
1402 ENTRYAGGREGATOR => 'AND',
1406 elsif ( !$negative_op || $single_value ) {
1407 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1408 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1415 # if column is defined then deal only with it
1416 # otherwise search in Content and in LargeContent
1419 ALIAS => $TicketCFs,
1421 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1426 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1427 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1429 ALIAS => $TicketCFs,
1438 ALIAS => $TicketCFs,
1442 ENTRYAGGREGATOR => 'OR'
1445 ALIAS => $TicketCFs,
1449 ENTRYAGGREGATOR => 'OR'
1453 ALIAS => $TicketCFs,
1454 FIELD => 'LargeContent',
1455 OPERATOR => $fix_op->($op),
1457 ENTRYAGGREGATOR => 'AND',
1463 ALIAS => $TicketCFs,
1473 ALIAS => $TicketCFs,
1477 ENTRYAGGREGATOR => 'OR'
1480 ALIAS => $TicketCFs,
1484 ENTRYAGGREGATOR => 'OR'
1488 ALIAS => $TicketCFs,
1489 FIELD => 'LargeContent',
1490 OPERATOR => $fix_op->($op),
1492 ENTRYAGGREGATOR => 'AND',
1498 # XXX: if we join via CustomFields table then
1499 # because of order of left joins we get NULLs in
1500 # CF table and then get nulls for those records
1501 # in OCFVs table what result in wrong results
1502 # as decifer method now tries to load a CF then
1503 # we fall into this situation only when there
1504 # are more than one CF with the name in the DB.
1505 # the same thing applies to order by call.
1506 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1507 # we want treat IS NULL as (not applies or has
1512 OPERATOR => 'IS NOT',
1515 ENTRYAGGREGATOR => 'AND',
1521 ALIAS => $TicketCFs,
1522 FIELD => $column || 'Content',
1526 ENTRYAGGREGATOR => 'OR',
1533 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1534 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1537 $op =~ s/!|NOT\s+//i;
1539 # if column is defined then deal only with it
1540 # otherwise search in Content and in LargeContent
1542 $self->SUPER::Limit(
1543 LEFTJOIN => $TicketCFs,
1544 ALIAS => $TicketCFs,
1546 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1551 $self->SUPER::Limit(
1552 LEFTJOIN => $TicketCFs,
1553 ALIAS => $TicketCFs,
1561 ALIAS => $TicketCFs,
1570 sub _HasAttributeLimit {
1571 my ( $self, $field, $op, $value, %rest ) = @_;
1573 my $alias = $self->Join(
1577 TABLE2 => 'Attributes',
1578 FIELD2 => 'ObjectId',
1580 $self->SUPER::Limit(
1582 FIELD => 'ObjectType',
1583 VALUE => 'RT::Ticket',
1584 ENTRYAGGREGATOR => 'AND'
1586 $self->SUPER::Limit(
1591 ENTRYAGGREGATOR => 'AND'
1597 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1604 # End Helper Functions
1606 # End of SQL Stuff -------------------------------------------------
1608 # {{{ Allow sorting on watchers
1610 =head2 OrderByCols ARRAY
1612 A modified version of the OrderBy method which automatically joins where
1613 C<ALIAS> is set to the name of a watcher type.
1624 foreach my $row (@args) {
1625 if ( $row->{ALIAS} ) {
1629 if ( $row->{FIELD} !~ /\./ ) {
1630 my $meta = $self->FIELDS->{ $row->{FIELD} };
1636 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1637 my $alias = $self->Join(
1640 FIELD1 => $row->{'FIELD'},
1644 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1645 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1646 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1648 my $alias = $self->Join(
1651 FIELD1 => $row->{'FIELD'},
1655 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1662 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1663 my $meta = $self->FIELDS->{$field};
1664 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1665 # cache alias as we want to use one alias per watcher type for sorting
1666 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1668 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1669 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1671 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1672 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1673 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1674 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1675 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1676 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1677 # this is described in _CustomFieldLimit
1681 OPERATOR => 'IS NOT',
1684 ENTRYAGGREGATOR => 'AND',
1687 # For those cases where we are doing a join against the
1688 # CF name, and don't have a CFid, use Unique to make sure
1689 # we don't show duplicate tickets. NOTE: I'm pretty sure
1690 # this will stay mixed in for the life of the
1691 # class/package, and not just for the life of the object.
1692 # Potential performance issue.
1693 require DBIx::SearchBuilder::Unique;
1694 DBIx::SearchBuilder::Unique->import;
1696 my $CFvs = $self->Join(
1698 ALIAS1 => $TicketCFs,
1699 FIELD1 => 'CustomField',
1700 TABLE2 => 'CustomFieldValues',
1701 FIELD2 => 'CustomField',
1703 $self->SUPER::Limit(
1707 VALUE => $TicketCFs . ".Content",
1708 ENTRYAGGREGATOR => 'AND'
1711 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1712 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1713 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1714 # PAW logic is "reversed"
1716 if (exists $row->{ORDER} ) {
1717 my $o = $row->{ORDER};
1718 delete $row->{ORDER};
1719 $order = "DESC" if $o =~ /asc/i;
1722 # Ticket.Owner 1 0 X
1723 # Unowned Tickets 0 1 X
1726 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1727 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1728 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1729 push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
1731 push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
1735 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1741 return $self->SUPER::OrderByCols(@res);
1746 # {{{ Limit the result set based on content
1752 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1753 Generally best called from LimitFoo methods
1763 DESCRIPTION => undef,
1766 $args{'DESCRIPTION'} = $self->loc(
1767 "[_1] [_2] [_3]", $args{'FIELD'},
1768 $args{'OPERATOR'}, $args{'VALUE'}
1770 if ( !defined $args{'DESCRIPTION'} );
1772 my $index = $self->_NextIndex;
1774 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1776 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1778 $self->{'RecalcTicketLimits'} = 1;
1780 # If we're looking at the effective id, we don't want to append the other clause
1781 # which limits us to tickets where id = effective id
1782 if ( $args{'FIELD'} eq 'EffectiveId'
1783 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1785 $self->{'looking_at_effective_id'} = 1;
1788 if ( $args{'FIELD'} eq 'Type'
1789 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1791 $self->{'looking_at_type'} = 1;
1801 Returns a frozen string suitable for handing back to ThawLimits.
1805 sub _FreezeThawKeys {
1806 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1810 # {{{ sub FreezeLimits
1815 require MIME::Base64;
1816 MIME::Base64::base64_encode(
1817 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1824 Take a frozen Limits string generated by FreezeLimits and make this tickets
1825 object have that set of limits.
1829 # {{{ sub ThawLimits
1835 #if we don't have $in, get outta here.
1836 return undef unless ($in);
1838 $self->{'RecalcTicketLimits'} = 1;
1841 require MIME::Base64;
1843 #We don't need to die if the thaw fails.
1844 @{$self}{ $self->_FreezeThawKeys }
1845 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1847 $RT::Logger->error($@) if $@;
1853 # {{{ Limit by enum or foreign key
1855 # {{{ sub LimitQueue
1859 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1860 OPERATOR is one of = or !=. (It defaults to =).
1861 VALUE is a queue id or Name.
1874 #TODO VALUE should also take queue objects
1875 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1876 my $queue = new RT::Queue( $self->CurrentUser );
1877 $queue->Load( $args{'VALUE'} );
1878 $args{'VALUE'} = $queue->Id;
1881 # What if they pass in an Id? Check for isNum() and convert to
1884 #TODO check for a valid queue here
1888 VALUE => $args{'VALUE'},
1889 OPERATOR => $args{'OPERATOR'},
1890 DESCRIPTION => join(
1891 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1899 # {{{ sub LimitStatus
1903 Takes a paramhash with the fields OPERATOR and VALUE.
1904 OPERATOR is one of = or !=.
1907 RT adds Status != 'deleted' until object has
1908 allow_deleted_search internal property set.
1909 $tickets->{'allow_deleted_search'} = 1;
1910 $tickets->LimitStatus( VALUE => 'deleted' );
1922 VALUE => $args{'VALUE'},
1923 OPERATOR => $args{'OPERATOR'},
1924 DESCRIPTION => join( ' ',
1925 $self->loc('Status'), $args{'OPERATOR'},
1926 $self->loc( $args{'VALUE'} ) ),
1932 # {{{ sub IgnoreType
1936 If called, this search will not automatically limit the set of results found
1937 to tickets of type "Ticket". Tickets of other types, such as "project" and
1938 "approval" will be found.
1945 # Instead of faking a Limit that later gets ignored, fake up the
1946 # fact that we're already looking at type, so that the check in
1947 # Tickets_Overlay_SQL/FromSQL goes down the right branch
1949 # $self->LimitType(VALUE => '__any');
1950 $self->{looking_at_type} = 1;
1959 Takes a paramhash with the fields OPERATOR and VALUE.
1960 OPERATOR is one of = or !=, it defaults to "=".
1961 VALUE is a string to search for in the type of the ticket.
1976 VALUE => $args{'VALUE'},
1977 OPERATOR => $args{'OPERATOR'},
1978 DESCRIPTION => join( ' ',
1979 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
1987 # {{{ Limit by string field
1989 # {{{ sub LimitSubject
1993 Takes a paramhash with the fields OPERATOR and VALUE.
1994 OPERATOR is one of = or !=.
1995 VALUE is a string to search for in the subject of the ticket.
2004 VALUE => $args{'VALUE'},
2005 OPERATOR => $args{'OPERATOR'},
2006 DESCRIPTION => join( ' ',
2007 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2015 # {{{ Limit based on ticket numerical attributes
2016 # Things that can be > < = !=
2022 Takes a paramhash with the fields OPERATOR and VALUE.
2023 OPERATOR is one of =, >, < or !=.
2024 VALUE is a ticket Id to search for
2037 VALUE => $args{'VALUE'},
2038 OPERATOR => $args{'OPERATOR'},
2040 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2046 # {{{ sub LimitPriority
2048 =head2 LimitPriority
2050 Takes a paramhash with the fields OPERATOR and VALUE.
2051 OPERATOR is one of =, >, < or !=.
2052 VALUE is a value to match the ticket\'s priority against
2060 FIELD => 'Priority',
2061 VALUE => $args{'VALUE'},
2062 OPERATOR => $args{'OPERATOR'},
2063 DESCRIPTION => join( ' ',
2064 $self->loc('Priority'),
2065 $args{'OPERATOR'}, $args{'VALUE'}, ),
2071 # {{{ sub LimitInitialPriority
2073 =head2 LimitInitialPriority
2075 Takes a paramhash with the fields OPERATOR and VALUE.
2076 OPERATOR is one of =, >, < or !=.
2077 VALUE is a value to match the ticket\'s initial priority against
2082 sub LimitInitialPriority {
2086 FIELD => 'InitialPriority',
2087 VALUE => $args{'VALUE'},
2088 OPERATOR => $args{'OPERATOR'},
2089 DESCRIPTION => join( ' ',
2090 $self->loc('Initial Priority'), $args{'OPERATOR'},
2097 # {{{ sub LimitFinalPriority
2099 =head2 LimitFinalPriority
2101 Takes a paramhash with the fields OPERATOR and VALUE.
2102 OPERATOR is one of =, >, < or !=.
2103 VALUE is a value to match the ticket\'s final priority against
2107 sub LimitFinalPriority {
2111 FIELD => 'FinalPriority',
2112 VALUE => $args{'VALUE'},
2113 OPERATOR => $args{'OPERATOR'},
2114 DESCRIPTION => join( ' ',
2115 $self->loc('Final Priority'), $args{'OPERATOR'},
2122 # {{{ sub LimitTimeWorked
2124 =head2 LimitTimeWorked
2126 Takes a paramhash with the fields OPERATOR and VALUE.
2127 OPERATOR is one of =, >, < or !=.
2128 VALUE is a value to match the ticket's TimeWorked attribute
2132 sub LimitTimeWorked {
2136 FIELD => 'TimeWorked',
2137 VALUE => $args{'VALUE'},
2138 OPERATOR => $args{'OPERATOR'},
2139 DESCRIPTION => join( ' ',
2140 $self->loc('Time Worked'),
2141 $args{'OPERATOR'}, $args{'VALUE'}, ),
2147 # {{{ sub LimitTimeLeft
2149 =head2 LimitTimeLeft
2151 Takes a paramhash with the fields OPERATOR and VALUE.
2152 OPERATOR is one of =, >, < or !=.
2153 VALUE is a value to match the ticket's TimeLeft attribute
2161 FIELD => 'TimeLeft',
2162 VALUE => $args{'VALUE'},
2163 OPERATOR => $args{'OPERATOR'},
2164 DESCRIPTION => join( ' ',
2165 $self->loc('Time Left'),
2166 $args{'OPERATOR'}, $args{'VALUE'}, ),
2174 # {{{ Limiting based on attachment attributes
2176 # {{{ sub LimitContent
2180 Takes a paramhash with the fields OPERATOR and VALUE.
2181 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2182 VALUE is a string to search for in the body of the ticket
2191 VALUE => $args{'VALUE'},
2192 OPERATOR => $args{'OPERATOR'},
2193 DESCRIPTION => join( ' ',
2194 $self->loc('Ticket content'), $args{'OPERATOR'},
2201 # {{{ sub LimitFilename
2203 =head2 LimitFilename
2205 Takes a paramhash with the fields OPERATOR and VALUE.
2206 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2207 VALUE is a string to search for in the body of the ticket
2215 FIELD => 'Filename',
2216 VALUE => $args{'VALUE'},
2217 OPERATOR => $args{'OPERATOR'},
2218 DESCRIPTION => join( ' ',
2219 $self->loc('Attachment filename'), $args{'OPERATOR'},
2225 # {{{ sub LimitContentType
2227 =head2 LimitContentType
2229 Takes a paramhash with the fields OPERATOR and VALUE.
2230 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2231 VALUE is a content type to search ticket attachments for
2235 sub LimitContentType {
2239 FIELD => 'ContentType',
2240 VALUE => $args{'VALUE'},
2241 OPERATOR => $args{'OPERATOR'},
2242 DESCRIPTION => join( ' ',
2243 $self->loc('Ticket content type'), $args{'OPERATOR'},
2252 # {{{ Limiting based on people
2254 # {{{ sub LimitOwner
2258 Takes a paramhash with the fields OPERATOR and VALUE.
2259 OPERATOR is one of = or !=.
2271 my $owner = new RT::User( $self->CurrentUser );
2272 $owner->Load( $args{'VALUE'} );
2274 # FIXME: check for a valid $owner
2277 VALUE => $args{'VALUE'},
2278 OPERATOR => $args{'OPERATOR'},
2279 DESCRIPTION => join( ' ',
2280 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2287 # {{{ Limiting watchers
2289 # {{{ sub LimitWatcher
2293 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2294 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2295 VALUE is a value to match the ticket\'s watcher email addresses against
2296 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2310 #build us up a description
2311 my ( $watcher_type, $desc );
2312 if ( $args{'TYPE'} ) {
2313 $watcher_type = $args{'TYPE'};
2316 $watcher_type = "Watcher";
2320 FIELD => $watcher_type,
2321 VALUE => $args{'VALUE'},
2322 OPERATOR => $args{'OPERATOR'},
2323 TYPE => $args{'TYPE'},
2324 DESCRIPTION => join( ' ',
2325 $self->loc($watcher_type),
2326 $args{'OPERATOR'}, $args{'VALUE'}, ),
2336 # {{{ Limiting based on links
2340 =head2 LimitLinkedTo
2342 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2343 TYPE limits the sort of link we want to search on
2345 TYPE = { RefersTo, MemberOf, DependsOn }
2347 TARGET is the id or URI of the TARGET of the link
2361 FIELD => 'LinkedTo',
2363 TARGET => $args{'TARGET'},
2364 TYPE => $args{'TYPE'},
2365 DESCRIPTION => $self->loc(
2366 "Tickets [_1] by [_2]",
2367 $self->loc( $args{'TYPE'} ),
2370 OPERATOR => $args{'OPERATOR'},
2376 # {{{ LimitLinkedFrom
2378 =head2 LimitLinkedFrom
2380 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2381 TYPE limits the sort of link we want to search on
2384 BASE is the id or URI of the BASE of the link
2388 sub LimitLinkedFrom {
2397 # translate RT2 From/To naming to RT3 TicketSQL naming
2398 my %fromToMap = qw(DependsOn DependentOn
2400 RefersTo ReferredToBy);
2402 my $type = $args{'TYPE'};
2403 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2406 FIELD => 'LinkedTo',
2408 BASE => $args{'BASE'},
2410 DESCRIPTION => $self->loc(
2411 "Tickets [_1] [_2]",
2412 $self->loc( $args{'TYPE'} ),
2415 OPERATOR => $args{'OPERATOR'},
2424 my $ticket_id = shift;
2425 return $self->LimitLinkedTo(
2427 TARGET => $ticket_id,
2434 # {{{ LimitHasMember
2435 sub LimitHasMember {
2437 my $ticket_id = shift;
2438 return $self->LimitLinkedFrom(
2440 BASE => "$ticket_id",
2441 TYPE => 'HasMember',
2448 # {{{ LimitDependsOn
2450 sub LimitDependsOn {
2452 my $ticket_id = shift;
2453 return $self->LimitLinkedTo(
2455 TARGET => $ticket_id,
2456 TYPE => 'DependsOn',
2463 # {{{ LimitDependedOnBy
2465 sub LimitDependedOnBy {
2467 my $ticket_id = shift;
2468 return $self->LimitLinkedFrom(
2471 TYPE => 'DependentOn',
2482 my $ticket_id = shift;
2483 return $self->LimitLinkedTo(
2485 TARGET => $ticket_id,
2493 # {{{ LimitReferredToBy
2495 sub LimitReferredToBy {
2497 my $ticket_id = shift;
2498 return $self->LimitLinkedFrom(
2501 TYPE => 'ReferredToBy',
2509 # {{{ limit based on ticket date attribtes
2513 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2515 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2517 OPERATOR is one of > or <
2518 VALUE is a date and time in ISO format in GMT
2519 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2521 There are also helper functions of the form LimitFIELD that eliminate
2522 the need to pass in a FIELD argument.
2536 #Set the description if we didn't get handed it above
2537 unless ( $args{'DESCRIPTION'} ) {
2538 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2539 . $args{'OPERATOR'} . " "
2540 . $args{'VALUE'} . " GMT";
2543 $self->Limit(%args);
2551 $self->LimitDate( FIELD => 'Created', @_ );
2556 $self->LimitDate( FIELD => 'Due', @_ );
2562 $self->LimitDate( FIELD => 'Starts', @_ );
2568 $self->LimitDate( FIELD => 'Started', @_ );
2573 $self->LimitDate( FIELD => 'Resolved', @_ );
2578 $self->LimitDate( FIELD => 'Told', @_ );
2581 sub LimitLastUpdated {
2583 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2587 # {{{ sub LimitTransactionDate
2589 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2591 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2593 OPERATOR is one of > or <
2594 VALUE is a date and time in ISO format in GMT
2599 sub LimitTransactionDate {
2602 FIELD => 'TransactionDate',
2609 # <20021217042756.GK28744@pallas.fsck.com>
2610 # "Kill It" - Jesse.
2612 #Set the description if we didn't get handed it above
2613 unless ( $args{'DESCRIPTION'} ) {
2614 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2615 . $args{'OPERATOR'} . " "
2616 . $args{'VALUE'} . " GMT";
2619 $self->Limit(%args);
2627 # {{{ Limit based on custom fields
2628 # {{{ sub LimitCustomField
2630 =head2 LimitCustomField
2632 Takes a paramhash of key/value pairs with the following keys:
2636 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2638 =item OPERATOR - The usual Limit operators
2640 =item VALUE - The value to compare against
2646 sub LimitCustomField {
2650 CUSTOMFIELD => undef,
2652 DESCRIPTION => undef,
2653 FIELD => 'CustomFieldValue',
2658 my $CF = RT::CustomField->new( $self->CurrentUser );
2659 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2660 $CF->Load( $args{CUSTOMFIELD} );
2663 $CF->LoadByNameAndQueue(
2664 Name => $args{CUSTOMFIELD},
2665 Queue => $args{QUEUE}
2667 $args{CUSTOMFIELD} = $CF->Id;
2670 #If we are looking to compare with a null value.
2671 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2672 $args{'DESCRIPTION'}
2673 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2675 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2676 $args{'DESCRIPTION'}
2677 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2680 # if we're not looking to compare with a null value
2682 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2683 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2686 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2687 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2688 $QueueObj->Load( $args{'QUEUE'} );
2689 $args{'QUEUE'} = $QueueObj->Id;
2691 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2694 @rest = ( ENTRYAGGREGATOR => 'AND' )
2695 if ( $CF->Type eq 'SelectMultiple' );
2698 VALUE => $args{VALUE},
2700 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2701 .".{" . $CF->Name . "}",
2702 OPERATOR => $args{OPERATOR},
2707 $self->{'RecalcTicketLimits'} = 1;
2713 # {{{ sub _NextIndex
2717 Keep track of the counter for the array of restrictions
2723 return ( $self->{'restriction_index'}++ );
2730 # {{{ Core bits to make this a DBIx::SearchBuilder object
2735 $self->{'table'} = "Tickets";
2736 $self->{'RecalcTicketLimits'} = 1;
2737 $self->{'looking_at_effective_id'} = 0;
2738 $self->{'looking_at_type'} = 0;
2739 $self->{'restriction_index'} = 1;
2740 $self->{'primary_key'} = "id";
2741 delete $self->{'items_array'};
2742 delete $self->{'item_map'};
2743 delete $self->{'columns_to_display'};
2744 $self->SUPER::_Init(@_);
2755 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2756 return ( $self->SUPER::Count() );
2764 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2765 return ( $self->SUPER::CountAll() );
2770 # {{{ sub ItemsArrayRef
2772 =head2 ItemsArrayRef
2774 Returns a reference to the set of all items found in this search
2781 return $self->{'items_array'} if $self->{'items_array'};
2783 my $placeholder = $self->_ItemsCounter;
2784 $self->GotoFirstItem();
2785 while ( my $item = $self->Next ) {
2786 push( @{ $self->{'items_array'} }, $item );
2788 $self->GotoItem($placeholder);
2789 $self->{'items_array'}
2790 = $self->ItemsOrderBy( $self->{'items_array'} );
2792 return $self->{'items_array'};
2795 sub ItemsArrayRefWindow {
2799 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2801 $self->RowsPerPage( $window );
2803 $self->GotoFirstItem;
2806 while ( my $item = $self->Next ) {
2810 $self->RowsPerPage( $old[1] );
2811 $self->FirstRow( $old[2] );
2812 $self->GotoItem( $old[0] );
2823 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2825 my $Ticket = $self->SUPER::Next;
2826 return $Ticket unless $Ticket;
2828 if ( $Ticket->__Value('Status') eq 'deleted'
2829 && !$self->{'allow_deleted_search'} )
2833 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2834 # if we found a ticket with this option enabled then
2835 # all tickets we found are ACLed, cache this fact
2836 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2837 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2840 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2845 # If the user doesn't have the right to show this ticket
2852 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2853 return $self->SUPER::_DoSearch( @_ );
2858 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2859 return $self->SUPER::_DoCount( @_ );
2865 my $cache_key = 'RolesHasRight;:;ShowTicket';
2867 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
2871 my $ACL = RT::ACL->new( $RT::SystemUser );
2872 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2873 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
2874 my $principal_alias = $ACL->Join(
2876 FIELD1 => 'PrincipalId',
2877 TABLE2 => 'Principals',
2880 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2883 while ( my $ACE = $ACL->Next ) {
2884 my $role = $ACE->PrincipalType;
2885 my $type = $ACE->ObjectType;
2886 if ( $type eq 'RT::System' ) {
2889 elsif ( $type eq 'RT::Queue' ) {
2890 next if $res{ $role } && !ref $res{ $role };
2891 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
2894 $RT::Logger->error('ShowTicket right is granted on unsupported object');
2897 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
2901 sub _DirectlyCanSeeIn {
2903 my $id = $self->CurrentUser->id;
2905 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
2906 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
2910 my $ACL = RT::ACL->new( $RT::SystemUser );
2911 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2912 my $principal_alias = $ACL->Join(
2914 FIELD1 => 'PrincipalId',
2915 TABLE2 => 'Principals',
2918 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2919 my $cgm_alias = $ACL->Join(
2921 FIELD1 => 'PrincipalId',
2922 TABLE2 => 'CachedGroupMembers',
2923 FIELD2 => 'GroupId',
2925 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
2926 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
2929 while ( my $ACE = $ACL->Next ) {
2930 my $type = $ACE->ObjectType;
2931 if ( $type eq 'RT::System' ) {
2932 # If user is direct member of a group that has the right
2933 # on the system then he can see any ticket
2934 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
2937 elsif ( $type eq 'RT::Queue' ) {
2938 push @res, $ACE->ObjectId;
2941 $RT::Logger->error('ShowTicket right is granted on unsupported object');
2944 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
2948 sub CurrentUserCanSee {
2950 return if $self->{'_sql_current_user_can_see_applied'};
2952 return $self->{'_sql_current_user_can_see_applied'} = 1
2953 if $self->CurrentUser->UserObj->HasRight(
2954 Right => 'SuperUser', Object => $RT::System
2957 my $id = $self->CurrentUser->id;
2959 # directly can see in all queues then we have nothing to do
2960 my @direct_queues = $self->_DirectlyCanSeeIn;
2961 return $self->{'_sql_current_user_can_see_applied'} = 1
2962 if @direct_queues && $direct_queues[0] == -1;
2964 my %roles = $self->_RolesCanSee;
2966 my %skip = map { $_ => 1 } @direct_queues;
2967 foreach my $role ( keys %roles ) {
2968 next unless ref $roles{ $role };
2970 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
2972 $roles{ $role } = \@queues;
2974 delete $roles{ $role };
2979 # there is no global watchers, only queues and tickes, if at
2980 # some point we will add global roles then it's gonna blow
2981 # the idea here is that if the right is set globaly for a role
2982 # and user plays this role for a queue directly not a ticket
2983 # then we have to check in advance
2984 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
2986 my $groups = RT::Groups->new( $RT::SystemUser );
2987 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
2989 $groups->Limit( FIELD => 'Type', VALUE => $_ );
2991 my $principal_alias = $groups->Join(
2994 TABLE2 => 'Principals',
2997 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2998 my $cgm_alias = $groups->Join(
3001 TABLE2 => 'CachedGroupMembers',
3002 FIELD2 => 'GroupId',
3004 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3005 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3006 while ( my $group = $groups->Next ) {
3007 push @direct_queues, $group->Instance;
3011 unless ( @direct_queues || keys %roles ) {
3012 $self->SUPER::Limit(
3017 ENTRYAGGREGATOR => 'AND',
3019 return $self->{'_sql_current_user_can_see_applied'} = 1;
3023 my $join_roles = keys %roles;
3024 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3025 my ($role_group_alias, $cgm_alias);
3026 if ( $join_roles ) {
3027 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3028 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3029 $self->SUPER::Limit(
3030 LEFTJOIN => $cgm_alias,
3031 FIELD => 'MemberId',
3036 my $limit_queues = sub {
3040 return unless @queues;
3041 if ( @queues == 1 ) {
3042 $self->SUPER::Limit(
3047 ENTRYAGGREGATOR => $ea,
3050 $self->SUPER::_OpenParen('ACL');
3051 foreach my $q ( @queues ) {
3052 $self->SUPER::Limit(
3057 ENTRYAGGREGATOR => $ea,
3061 $self->SUPER::_CloseParen('ACL');
3066 $self->SUPER::_OpenParen('ACL');
3068 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3069 while ( my ($role, $queues) = each %roles ) {
3070 $self->SUPER::_OpenParen('ACL');
3071 if ( $role eq 'Owner' ) {
3072 $self->SUPER::Limit(
3076 ENTRYAGGREGATOR => $ea,
3080 $self->SUPER::Limit(
3082 ALIAS => $cgm_alias,
3083 FIELD => 'MemberId',
3084 OPERATOR => 'IS NOT',
3087 ENTRYAGGREGATOR => $ea,
3089 $self->SUPER::Limit(
3091 ALIAS => $role_group_alias,
3094 ENTRYAGGREGATOR => 'AND',
3097 $limit_queues->( 'AND', @$queues ) if ref $queues;
3098 $ea = 'OR' if $ea eq 'AND';
3099 $self->SUPER::_CloseParen('ACL');
3101 $self->SUPER::_CloseParen('ACL');
3103 return $self->{'_sql_current_user_can_see_applied'} = 1;
3110 # {{{ Deal with storing and restoring restrictions
3112 # {{{ sub LoadRestrictions
3114 =head2 LoadRestrictions
3116 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3117 TODO It is not yet implemented
3123 # {{{ sub DescribeRestrictions
3125 =head2 DescribeRestrictions
3128 Returns a hash keyed by restriction id.
3129 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3130 is a description of the purpose of that TicketRestriction
3134 sub DescribeRestrictions {
3137 my ( $row, %listing );
3139 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3140 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3147 # {{{ sub RestrictionValues
3149 =head2 RestrictionValues FIELD
3151 Takes a restriction field and returns a list of values this field is restricted
3156 sub RestrictionValues {
3159 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3160 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3161 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3163 keys %{ $self->{'TicketRestrictions'} };
3168 # {{{ sub ClearRestrictions
3170 =head2 ClearRestrictions
3172 Removes all restrictions irretrievably
3176 sub ClearRestrictions {
3178 delete $self->{'TicketRestrictions'};
3179 $self->{'looking_at_effective_id'} = 0;
3180 $self->{'looking_at_type'} = 0;
3181 $self->{'RecalcTicketLimits'} = 1;
3186 # {{{ sub DeleteRestriction
3188 =head2 DeleteRestriction
3190 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3191 Removes that restriction from the session's limits.
3195 sub DeleteRestriction {
3198 delete $self->{'TicketRestrictions'}{$row};
3200 $self->{'RecalcTicketLimits'} = 1;
3202 #make the underlying easysearch object forget all its preconceptions
3207 # {{{ sub _RestrictionsToClauses
3209 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3211 sub _RestrictionsToClauses {
3216 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3217 my $restriction = $self->{'TicketRestrictions'}{$row};
3219 # We need to reimplement the subclause aggregation that SearchBuilder does.
3220 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3221 # Then SB AND's the different Subclauses together.
3223 # So, we want to group things into Subclauses, convert them to
3224 # SQL, and then join them with the appropriate DefaultEA.
3225 # Then join each subclause group with AND.
3227 my $field = $restriction->{'FIELD'};
3228 my $realfield = $field; # CustomFields fake up a fieldname, so
3229 # we need to figure that out
3232 # Rewrite LinkedTo meta field to the real field
3233 if ( $field =~ /LinkedTo/ ) {
3234 $realfield = $field = $restriction->{'TYPE'};
3238 # Handle subkey fields with a different real field
3239 if ( $field =~ /^(\w+)\./ ) {
3243 die "I don't know about $field yet"
3244 unless ( exists $FIELD_METADATA{$realfield}
3245 or $restriction->{CUSTOMFIELD} );
3247 my $type = $FIELD_METADATA{$realfield}->[0];
3248 my $op = $restriction->{'OPERATOR'};
3252 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3255 # this performs the moral equivalent of defined or/dor/C<//>,
3256 # without the short circuiting.You need to use a 'defined or'
3257 # type thing instead of just checking for truth values, because
3258 # VALUE could be 0.(i.e. "false")
3260 # You could also use this, but I find it less aesthetic:
3261 # (although it does short circuit)
3262 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3263 # defined $restriction->{'TICKET'} ?
3264 # $restriction->{TICKET} :
3265 # defined $restriction->{'BASE'} ?
3266 # $restriction->{BASE} :
3267 # defined $restriction->{'TARGET'} ?
3268 # $restriction->{TARGET} )
3270 my $ea = $restriction->{ENTRYAGGREGATOR}
3271 || $DefaultEA{$type}
3274 die "Invalid operator $op for $field ($type)"
3275 unless exists $ea->{$op};
3279 # Each CustomField should be put into a different Clause so they
3280 # are ANDed together.
3281 if ( $restriction->{CUSTOMFIELD} ) {
3282 $realfield = $field;
3285 exists $clause{$realfield} or $clause{$realfield} = [];
3288 $field =~ s!(['"])!\\$1!g;
3289 $value =~ s!(['"])!\\$1!g;
3290 my $data = [ $ea, $type, $field, $op, $value ];
3292 # here is where we store extra data, say if it's a keyword or
3293 # something. (I.e. "TYPE SPECIFIC STUFF")
3295 push @{ $clause{$realfield} }, $data;
3302 # {{{ sub _ProcessRestrictions
3304 =head2 _ProcessRestrictions PARAMHASH
3306 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3307 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3311 sub _ProcessRestrictions {
3314 #Blow away ticket aliases since we'll need to regenerate them for
3316 delete $self->{'TicketAliases'};
3317 delete $self->{'items_array'};
3318 delete $self->{'item_map'};
3319 delete $self->{'raw_rows'};
3320 delete $self->{'rows'};
3321 delete $self->{'count_all'};
3323 my $sql = $self->Query; # Violating the _SQL namespace
3324 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3326 # "Restrictions to Clauses Branch\n";
3327 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3329 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3333 $sql = $self->ClausesToSQL($clauseRef);
3334 $self->FromSQL($sql) if $sql;
3338 $self->{'RecalcTicketLimits'} = 0;
3342 =head2 _BuildItemMap
3344 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3345 display search nav quickly.
3352 my $window = RT->Config->Get('TicketsItemMapSize');
3354 $self->{'item_map'} = {};
3356 my $items = $self->ItemsArrayRefWindow( $window );
3357 return unless $items && @$items;
3360 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3361 for ( my $i = 0; $i < @$items; $i++ ) {
3362 my $item = $items->[$i];
3363 my $id = $item->EffectiveId;
3364 $self->{'item_map'}{$id}{'defined'} = 1;
3365 $self->{'item_map'}{$id}{'prev'} = $prev;
3366 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3370 $self->{'item_map'}{'last'} = $prev
3371 if !$window || @$items < $window;
3376 Returns an a map of all items found by this search. The map is a hash
3380 first => <first ticket id found>,
3381 last => <last ticket id found or undef>,
3384 prev => <the ticket id found before>,
3385 next => <the ticket id found after>,
3397 $self->_BuildItemMap unless $self->{'item_map'};
3398 return $self->{'item_map'};
3406 =head2 PrepForSerialization
3408 You don't want to serialize a big tickets object, as
3409 the {items} hash will be instantly invalid _and_ eat
3414 sub PrepForSerialization {
3416 delete $self->{'items'};
3417 delete $self->{'items_array'};
3418 $self->RedoSearch();
3423 RT::Tickets supports several flags which alter search behavior:
3426 allow_deleted_search (Otherwise never show deleted tickets in search results)
3427 looking_at_type (otherwise limit to type=ticket)
3429 These flags are set by calling
3431 $tickets->{'flagname'} = 1;
3433 BUG: There should be an API for this