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
148 # Mapping of Field Type to Function
150 ENUM => \&_EnumLimit,
153 LINK => \&_LinkLimit,
154 DATE => \&_DateLimit,
155 STRING => \&_StringLimit,
156 TRANSFIELD => \&_TransLimit,
157 TRANSDATE => \&_TransDateLimit,
158 WATCHERFIELD => \&_WatcherLimit,
159 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
160 CUSTOMFIELD => \&_CustomFieldLimit,
162 our %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 \%FIELD_METADATA }
204 sub dispatch { return \%dispatch }
205 sub can_bundle { return \%can_bundle }
207 # Bring in the clowns.
208 require RT::Tickets_Overlay_SQL;
212 our @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 *********************************
236 $self->SUPER::CleanSlate( @_ );
237 delete $self->{$_} foreach qw(
239 _sql_group_members_aliases
240 _sql_object_cfv_alias
241 _sql_role_group_aliases
244 _sql_u_watchers_alias_for_sort
245 _sql_u_watchers_aliases
246 _sql_current_user_can_see_applied
250 =head1 Limit Helper Routines
252 These routines are the targets of a dispatch table depending on the
253 type of field. They all share the same signature:
255 my ($self,$field,$op,$value,@rest) = @_;
257 The values in @rest should be suitable for passing directly to
258 DBIx::SearchBuilder::Limit.
260 Essentially they are an expanded/broken out (and much simplified)
261 version of what ProcessRestrictions used to do. They're also much
262 more clearly delineated by the TYPE of field being processed.
271 my ( $sb, $field, $op, $value, @rest ) = @_;
273 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
275 die "Invalid operator $op for __Bookmarked__ search on $field"
276 unless $op =~ /^(=|!=)$/;
279 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
280 $tmp = $tmp->Content if $tmp;
285 return $sb->_SQLLimit(
292 # as bookmarked tickets can be merged we have to use a join
293 # but it should be pretty lightweight
294 my $tickets_alias = $sb->Join(
299 FIELD2 => 'EffectiveId',
303 my $ea = $op eq '='? 'OR': 'AND';
304 foreach my $id ( sort @bookmarks ) {
306 ALIAS => $tickets_alias,
310 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
318 Handle Fields which are limited to certain values, and potentially
319 need to be looked up from another class.
321 This subroutine actually handles two different kinds of fields. For
322 some the user is responsible for limiting the values. (i.e. Status,
325 For others, the value specified by the user will be looked by via
329 name of class to lookup in (Optional)
334 my ( $sb, $field, $op, $value, @rest ) = @_;
336 # SQL::Statement changes != to <>. (Can we remove this now?)
337 $op = "!=" if $op eq "<>";
339 die "Invalid Operation: $op for $field"
343 my $meta = $FIELD_METADATA{$field};
344 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
345 my $class = "RT::" . $meta->[1];
346 my $o = $class->new( $sb->CurrentUser );
360 Handle fields where the values are limited to integers. (For example,
361 Priority, TimeWorked.)
369 my ( $sb, $field, $op, $value, @rest ) = @_;
371 die "Invalid Operator $op for $field"
372 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
384 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
387 1: Direction (From, To)
388 2: Link Type (MemberOf, DependsOn, RefersTo)
393 my ( $sb, $field, $op, $value, @rest ) = @_;
395 my $meta = $FIELD_METADATA{$field};
396 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
399 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
403 $is_null = 1 if !$value || $value =~ /^null$/io;
405 my $direction = $meta->[1] || '';
406 my ($matchfield, $linkfield) = ('', '');
407 if ( $direction eq 'To' ) {
408 ($matchfield, $linkfield) = ("Target", "Base");
410 elsif ( $direction eq 'From' ) {
411 ($matchfield, $linkfield) = ("Base", "Target");
413 elsif ( $direction ) {
414 die "Invalid link direction '$direction' for $field\n";
417 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
419 'LinkedFrom', $op, $value, @rest,
420 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
428 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
430 elsif ( $value =~ /\D/ ) {
433 $matchfield = "Local$matchfield" if $is_local;
435 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
436 # SELECT main.* FROM Tickets main
437 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
438 # AND(main.id = Links_1.LocalTarget))
439 # WHERE Links_1.LocalBase IS NULL;
442 my $linkalias = $sb->Join(
447 FIELD2 => 'Local' . $linkfield
450 LEFTJOIN => $linkalias,
458 FIELD => $matchfield,
465 my $linkalias = $sb->Join(
470 FIELD2 => 'Local' . $linkfield
473 LEFTJOIN => $linkalias,
479 LEFTJOIN => $linkalias,
480 FIELD => $matchfield,
487 FIELD => $matchfield,
488 OPERATOR => $is_negative? 'IS': 'IS NOT',
497 Handle date fields. (Created, LastTold..)
500 1: type of link. (Probably not necessary.)
505 my ( $sb, $field, $op, $value, @rest ) = @_;
507 die "Invalid Date Op: $op"
508 unless $op =~ /^(=|>|<|>=|<=)$/;
510 my $meta = $FIELD_METADATA{$field};
511 die "Incorrect Meta Data for $field"
512 unless ( defined $meta->[1] );
514 my $date = RT::Date->new( $sb->CurrentUser );
515 $date->Set( Format => 'unknown', Value => $value );
519 # if we're specifying =, that means we want everything on a
520 # particular single day. in the database, we need to check for >
521 # and < the edges of that day.
523 $date->SetToMidnight( Timezone => 'server' );
524 my $daystart = $date->ISO;
526 my $dayend = $date->ISO;
542 ENTRYAGGREGATOR => 'AND',
560 Handle simple fields which are just strings. (Subject,Type)
568 my ( $sb, $field, $op, $value, @rest ) = @_;
572 # =, !=, LIKE, NOT LIKE
573 if ( (!defined $value || !length $value)
574 && lc($op) ne 'is' && lc($op) ne 'is not'
575 && RT->Config->Get('DatabaseType') eq 'Oracle'
577 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
578 $op = $negative? 'IS NOT': 'IS';
591 =head2 _TransDateLimit
593 Handle fields limiting based on Transaction Date.
595 The inpupt value must be in a format parseable by Time::ParseDate
602 # This routine should really be factored into translimit.
603 sub _TransDateLimit {
604 my ( $sb, $field, $op, $value, @rest ) = @_;
606 # See the comments for TransLimit, they apply here too
608 unless ( $sb->{_sql_transalias} ) {
609 $sb->{_sql_transalias} = $sb->Join(
612 TABLE2 => 'Transactions',
613 FIELD2 => 'ObjectId',
616 ALIAS => $sb->{_sql_transalias},
617 FIELD => 'ObjectType',
618 VALUE => 'RT::Ticket',
619 ENTRYAGGREGATOR => 'AND',
623 my $date = RT::Date->new( $sb->CurrentUser );
624 $date->Set( Format => 'unknown', Value => $value );
629 # if we're specifying =, that means we want everything on a
630 # particular single day. in the database, we need to check for >
631 # and < the edges of that day.
633 $date->SetToMidnight( Timezone => 'server' );
634 my $daystart = $date->ISO;
636 my $dayend = $date->ISO;
639 ALIAS => $sb->{_sql_transalias},
647 ALIAS => $sb->{_sql_transalias},
653 ENTRYAGGREGATOR => 'AND',
658 # not searching for a single day
661 #Search for the right field
663 ALIAS => $sb->{_sql_transalias},
677 Limit based on the Content of a transaction or the ContentType.
686 # Content, ContentType, Filename
688 # If only this was this simple. We've got to do something
691 #Basically, we want to make sure that the limits apply to
692 #the same attachment, rather than just another attachment
693 #for the same ticket, no matter how many clauses we lump
694 #on. We put them in TicketAliases so that they get nuked
695 #when we redo the join.
697 # In the SQL, we might have
698 # (( Content = foo ) or ( Content = bar AND Content = baz ))
699 # The AND group should share the same Alias.
701 # Actually, maybe it doesn't matter. We use the same alias and it
702 # works itself out? (er.. different.)
704 # Steal more from _ProcessRestrictions
706 # FIXME: Maybe look at the previous FooLimit call, and if it was a
707 # TransLimit and EntryAggregator == AND, reuse the Aliases?
709 # Or better - store the aliases on a per subclause basis - since
710 # those are going to be the things we want to relate to each other,
713 # maybe we should not allow certain kinds of aggregation of these
714 # clauses and do a psuedo regex instead? - the problem is getting
715 # them all into the same subclause when you have (A op B op C) - the
716 # way they get parsed in the tree they're in different subclauses.
718 my ( $self, $field, $op, $value, @rest ) = @_;
720 unless ( $self->{_sql_transalias} ) {
721 $self->{_sql_transalias} = $self->Join(
724 TABLE2 => 'Transactions',
725 FIELD2 => 'ObjectId',
728 ALIAS => $self->{_sql_transalias},
729 FIELD => 'ObjectType',
730 VALUE => 'RT::Ticket',
731 ENTRYAGGREGATOR => 'AND',
734 unless ( defined $self->{_sql_trattachalias} ) {
735 $self->{_sql_trattachalias} = $self->_SQLJoin(
736 TYPE => 'LEFT', # not all txns have an attachment
737 ALIAS1 => $self->{_sql_transalias},
739 TABLE2 => 'Attachments',
740 FIELD2 => 'TransactionId',
746 #Search for the right field
747 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
749 ALIAS => $self->{_sql_trattachalias},
753 SUBCLAUSE => 'contentquery',
754 ENTRYAGGREGATOR => 'AND',
757 ALIAS => $self->{_sql_trattachalias},
763 ENTRYAGGREGATOR => 'AND',
764 SUBCLAUSE => 'contentquery',
768 ALIAS => $self->{_sql_trattachalias},
773 ENTRYAGGREGATOR => 'AND',
784 Handle watcher limits. (Requestor, CC, etc..)
800 my $meta = $FIELD_METADATA{ $field };
801 my $type = $meta->[1] || '';
802 my $class = $meta->[2] || 'Ticket';
804 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
805 # search by id and Name at the same time, this is workaround
806 # to preserve backward compatibility
807 if ( $field eq 'Owner' ) {
808 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
809 my $o = RT::User->new( $self->CurrentUser );
810 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
811 $o->$method( $value );
820 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
830 $rest{SUBKEY} ||= 'EmailAddress';
832 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
835 if ( $op =~ /^IS(?: NOT)?$/ ) {
836 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
837 # to avoid joining the table Users into the query, we just join GM
838 # and make sure we don't match records where group is member of itself
840 LEFTJOIN => $group_members,
843 VALUE => "$group_members.MemberId",
847 ALIAS => $group_members,
854 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
856 $op =~ s/!|NOT\s+//i;
858 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
859 # "X = 'Y'" matches more then one user so we try to fetch two records and
860 # do the right thing when there is only one exist and semi-working solution
862 my $users_obj = RT::Users->new( $self->CurrentUser );
864 FIELD => $rest{SUBKEY},
869 $users_obj->RowsPerPage(2);
870 my @users = @{ $users_obj->ItemsArrayRef };
872 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
875 $uid = $users[0]->id if @users;
877 LEFTJOIN => $group_members,
878 ALIAS => $group_members,
884 ALIAS => $group_members,
891 LEFTJOIN => $group_members,
894 VALUE => "$group_members.MemberId",
897 my $users = $self->Join(
899 ALIAS1 => $group_members,
900 FIELD1 => 'MemberId',
907 FIELD => $rest{SUBKEY},
921 my $group_members = $self->_GroupMembersJoin(
922 GroupsAlias => $groups,
926 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
928 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
929 $self->NewAlias('Users');
931 LEFTJOIN => $group_members,
932 ALIAS => $group_members,
934 VALUE => "$users.id",
939 # we join users table without adding some join condition between tables,
940 # the only conditions we have are conditions on the table iteslf,
941 # for example Users.EmailAddress = 'x'. We should add this condition to
942 # the top level of the query and bundle it with another similar conditions,
943 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
944 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
947 SUBCLAUSE => '_sql_u_watchers_'. $users,
949 FIELD => $rest{'SUBKEY'},
954 # A condition which ties Users and Groups (role groups) is a left join condition
955 # of CachedGroupMembers table. To get correct results of the query we check
956 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
959 ALIAS => $group_members,
961 OPERATOR => 'IS NOT',
968 sub _RoleGroupsJoin {
970 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
971 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
972 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
975 # we always have watcher groups for ticket, so we use INNER join
976 my $groups = $self->Join(
978 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
980 FIELD2 => 'Instance',
981 ENTRYAGGREGATOR => 'AND',
987 VALUE => 'RT::'. $args{'Class'} .'-Role',
993 VALUE => $args{'Type'},
996 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1002 sub _GroupMembersJoin {
1004 my %args = (New => 1, GroupsAlias => undef, @_);
1006 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1007 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1010 my $alias = $self->Join(
1012 ALIAS1 => $args{'GroupsAlias'},
1014 TABLE2 => 'CachedGroupMembers',
1015 FIELD2 => 'GroupId',
1016 ENTRYAGGREGATOR => 'AND',
1019 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1020 unless $args{'New'};
1027 Helper function which provides joins to a watchers table both for limits
1034 my $type = shift || '';
1037 my $groups = $self->_RoleGroupsJoin( Type => $type );
1038 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1039 # XXX: work around, we must hide groups that
1040 # are members of the role group we search in,
1041 # otherwise them result in wrong NULLs in Users
1042 # table and break ordering. Now, we know that
1043 # RT doesn't allow to add groups as members of the
1044 # ticket roles, so we just hide entries in CGM table
1045 # with MemberId == GroupId from results
1046 $self->SUPER::Limit(
1047 LEFTJOIN => $group_members,
1050 VALUE => "$group_members.MemberId",
1053 my $users = $self->Join(
1055 ALIAS1 => $group_members,
1056 FIELD1 => 'MemberId',
1060 return ($groups, $group_members, $users);
1063 =head2 _WatcherMembershipLimit
1065 Handle watcher membership limits, i.e. whether the watcher belongs to a
1066 specific group or not.
1069 1: Field to query on
1071 SELECT DISTINCT main.*
1075 CachedGroupMembers CachedGroupMembers_2,
1078 (main.EffectiveId = main.id)
1080 (main.Status != 'deleted')
1082 (main.Type = 'ticket')
1085 (Users_3.EmailAddress = '22')
1087 (Groups_1.Domain = 'RT::Ticket-Role')
1089 (Groups_1.Type = 'RequestorGroup')
1092 Groups_1.Instance = main.id
1094 Groups_1.id = CachedGroupMembers_2.GroupId
1096 CachedGroupMembers_2.MemberId = Users_3.id
1097 ORDER BY main.id ASC
1102 sub _WatcherMembershipLimit {
1103 my ( $self, $field, $op, $value, @rest ) = @_;
1108 my $groups = $self->NewAlias('Groups');
1109 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1110 my $users = $self->NewAlias('Users');
1111 my $memberships = $self->NewAlias('CachedGroupMembers');
1113 if ( ref $field ) { # gross hack
1114 my @bundle = @$field;
1116 for my $chunk (@bundle) {
1117 ( $field, $op, $value, @rest ) = @$chunk;
1119 ALIAS => $memberships,
1130 ALIAS => $memberships,
1138 # {{{ Tie to groups for tickets we care about
1142 VALUE => 'RT::Ticket-Role',
1143 ENTRYAGGREGATOR => 'AND'
1148 FIELD1 => 'Instance',
1155 # If we care about which sort of watcher
1156 my $meta = $FIELD_METADATA{$field};
1157 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1164 ENTRYAGGREGATOR => 'AND'
1171 ALIAS2 => $groupmembers,
1176 ALIAS1 => $groupmembers,
1177 FIELD1 => 'MemberId',
1183 ALIAS1 => $memberships,
1184 FIELD1 => 'MemberId',
1193 =head2 _CustomFieldDecipher
1195 Try and turn a CF descriptor into (cfid, cfname) object pair.
1199 sub _CustomFieldDecipher {
1200 my ($self, $string) = @_;
1202 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
1203 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1207 my $q = RT::Queue->new( $self->CurrentUser );
1211 # $queue = $q->Name; # should we normalize the queue?
1212 $cf = $q->CustomField( $field );
1215 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1219 elsif ( $field =~ /\D/ ) {
1221 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1222 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1223 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1225 # if there is more then one field the current user can
1226 # see with the same name then we shouldn't return cf object
1227 # as we don't know which one to use
1230 $cf = undef if $cfs->Next;
1234 $cf = RT::CustomField->new( $self->CurrentUser );
1235 $cf->Load( $field );
1238 return ($queue, $field, $cf, $column);
1241 =head2 _CustomFieldJoin
1243 Factor out the Join of custom fields so we can use it for sorting too
1247 sub _CustomFieldJoin {
1248 my ($self, $cfkey, $cfid, $field) = @_;
1249 # Perform one Join per CustomField
1250 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1251 $self->{_sql_cf_alias}{$cfkey} )
1253 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1254 $self->{_sql_cf_alias}{$cfkey} );
1257 my ($TicketCFs, $CFs);
1259 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1263 TABLE2 => 'ObjectCustomFieldValues',
1264 FIELD2 => 'ObjectId',
1266 $self->SUPER::Limit(
1267 LEFTJOIN => $TicketCFs,
1268 FIELD => 'CustomField',
1270 ENTRYAGGREGATOR => 'AND'
1274 my $ocfalias = $self->Join(
1277 TABLE2 => 'ObjectCustomFields',
1278 FIELD2 => 'ObjectId',
1281 $self->SUPER::Limit(
1282 LEFTJOIN => $ocfalias,
1283 ENTRYAGGREGATOR => 'OR',
1284 FIELD => 'ObjectId',
1288 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1290 ALIAS1 => $ocfalias,
1291 FIELD1 => 'CustomField',
1292 TABLE2 => 'CustomFields',
1295 $self->SUPER::Limit(
1297 ENTRYAGGREGATOR => 'AND',
1298 FIELD => 'LookupType',
1299 VALUE => 'RT::Queue-RT::Ticket',
1301 $self->SUPER::Limit(
1303 ENTRYAGGREGATOR => 'AND',
1308 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1312 TABLE2 => 'ObjectCustomFieldValues',
1313 FIELD2 => 'CustomField',
1315 $self->SUPER::Limit(
1316 LEFTJOIN => $TicketCFs,
1317 FIELD => 'ObjectId',
1320 ENTRYAGGREGATOR => 'AND',
1323 $self->SUPER::Limit(
1324 LEFTJOIN => $TicketCFs,
1325 FIELD => 'ObjectType',
1326 VALUE => 'RT::Ticket',
1327 ENTRYAGGREGATOR => 'AND'
1329 $self->SUPER::Limit(
1330 LEFTJOIN => $TicketCFs,
1331 FIELD => 'Disabled',
1334 ENTRYAGGREGATOR => 'AND'
1337 return ($TicketCFs, $CFs);
1340 =head2 _CustomFieldLimit
1342 Limit based on CustomFields
1349 sub _CustomFieldLimit {
1350 my ( $self, $_field, $op, $value, %rest ) = @_;
1352 my $field = $rest{'SUBKEY'} || die "No field specified";
1354 # For our sanity, we can only limit on one queue at a time
1356 my ($queue, $cfid, $cf, $column);
1357 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1358 $cfid = $cf ? $cf->id : 0 ;
1360 # If we're trying to find custom fields that don't match something, we
1361 # want tickets where the custom field has no value at all. Note that
1362 # we explicitly don't include the "IS NULL" case, since we would
1363 # otherwise end up with a redundant clause.
1365 my ($negative_op, $null_op, $inv_op, $range_op) = $self->ClassifySQLOperation( $op );
1369 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1370 return 'MATCHES' if $op eq '=';
1371 return 'NOT MATCHES' if $op eq '!=';
1375 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1377 my $cfkey = $cfid ? $cfid : "$queue.$field";
1379 if ( $null_op && !$column ) {
1380 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1381 # we can reuse our default joins for this operation
1382 # with column specified we have different situation
1383 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1386 ALIAS => $TicketCFs,
1395 OPERATOR => 'IS NOT',
1398 ENTRYAGGREGATOR => 'AND',
1402 elsif ( !$negative_op || $single_value ) {
1403 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1404 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1411 # if column is defined then deal only with it
1412 # otherwise search in Content and in LargeContent
1415 ALIAS => $TicketCFs,
1417 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1424 ALIAS => $TicketCFs,
1434 ALIAS => $TicketCFs,
1438 ENTRYAGGREGATOR => 'OR'
1441 ALIAS => $TicketCFs,
1445 ENTRYAGGREGATOR => 'OR'
1449 ALIAS => $TicketCFs,
1450 FIELD => 'LargeContent',
1451 OPERATOR => $fix_op->($op),
1453 ENTRYAGGREGATOR => 'AND',
1459 # XXX: if we join via CustomFields table then
1460 # because of order of left joins we get NULLs in
1461 # CF table and then get nulls for those records
1462 # in OCFVs table what result in wrong results
1463 # as decifer method now tries to load a CF then
1464 # we fall into this situation only when there
1465 # are more than one CF with the name in the DB.
1466 # the same thing applies to order by call.
1467 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1468 # we want treat IS NULL as (not applies or has
1473 OPERATOR => 'IS NOT',
1476 ENTRYAGGREGATOR => 'AND',
1482 ALIAS => $TicketCFs,
1483 FIELD => $column || 'Content',
1487 ENTRYAGGREGATOR => 'OR',
1494 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1495 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1498 $op =~ s/!|NOT\s+//i;
1500 # if column is defined then deal only with it
1501 # otherwise search in Content and in LargeContent
1503 $self->SUPER::Limit(
1504 LEFTJOIN => $TicketCFs,
1505 ALIAS => $TicketCFs,
1507 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1512 $self->SUPER::Limit(
1513 LEFTJOIN => $TicketCFs,
1514 ALIAS => $TicketCFs,
1522 ALIAS => $TicketCFs,
1531 # End Helper Functions
1533 # End of SQL Stuff -------------------------------------------------
1535 # {{{ Allow sorting on watchers
1537 =head2 OrderByCols ARRAY
1539 A modified version of the OrderBy method which automatically joins where
1540 C<ALIAS> is set to the name of a watcher type.
1551 foreach my $row (@args) {
1552 if ( $row->{ALIAS} ) {
1556 if ( $row->{FIELD} !~ /\./ ) {
1557 my $meta = $self->FIELDS->{ $row->{FIELD} };
1563 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1564 my $alias = $self->Join(
1567 FIELD1 => $row->{'FIELD'},
1571 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1572 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1573 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1575 my $alias = $self->Join(
1578 FIELD1 => $row->{'FIELD'},
1582 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1589 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1590 my $meta = $self->FIELDS->{$field};
1591 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1592 # cache alias as we want to use one alias per watcher type for sorting
1593 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1595 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1596 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1598 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1599 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1600 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1601 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1602 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1603 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1604 # this is described in _CustomFieldLimit
1608 OPERATOR => 'IS NOT',
1611 ENTRYAGGREGATOR => 'AND',
1614 # For those cases where we are doing a join against the
1615 # CF name, and don't have a CFid, use Unique to make sure
1616 # we don't show duplicate tickets. NOTE: I'm pretty sure
1617 # this will stay mixed in for the life of the
1618 # class/package, and not just for the life of the object.
1619 # Potential performance issue.
1620 require DBIx::SearchBuilder::Unique;
1621 DBIx::SearchBuilder::Unique->import;
1623 my $CFvs = $self->Join(
1625 ALIAS1 => $TicketCFs,
1626 FIELD1 => 'CustomField',
1627 TABLE2 => 'CustomFieldValues',
1628 FIELD2 => 'CustomField',
1630 $self->SUPER::Limit(
1634 VALUE => $TicketCFs . ".Content",
1635 ENTRYAGGREGATOR => 'AND'
1638 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1639 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1640 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1641 # PAW logic is "reversed"
1643 if (exists $row->{ORDER} ) {
1644 my $o = $row->{ORDER};
1645 delete $row->{ORDER};
1646 $order = "DESC" if $o =~ /asc/i;
1649 # Ticket.Owner 1 0 X
1650 # Unowned Tickets 0 1 X
1653 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1654 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1655 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1656 push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
1658 push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
1662 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1668 return $self->SUPER::OrderByCols(@res);
1673 # {{{ Limit the result set based on content
1679 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1680 Generally best called from LimitFoo methods
1690 DESCRIPTION => undef,
1693 $args{'DESCRIPTION'} = $self->loc(
1694 "[_1] [_2] [_3]", $args{'FIELD'},
1695 $args{'OPERATOR'}, $args{'VALUE'}
1697 if ( !defined $args{'DESCRIPTION'} );
1699 my $index = $self->_NextIndex;
1701 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1703 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1705 $self->{'RecalcTicketLimits'} = 1;
1707 # If we're looking at the effective id, we don't want to append the other clause
1708 # which limits us to tickets where id = effective id
1709 if ( $args{'FIELD'} eq 'EffectiveId'
1710 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1712 $self->{'looking_at_effective_id'} = 1;
1715 if ( $args{'FIELD'} eq 'Type'
1716 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1718 $self->{'looking_at_type'} = 1;
1728 Returns a frozen string suitable for handing back to ThawLimits.
1732 sub _FreezeThawKeys {
1733 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1737 # {{{ sub FreezeLimits
1742 require MIME::Base64;
1743 MIME::Base64::base64_encode(
1744 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1751 Take a frozen Limits string generated by FreezeLimits and make this tickets
1752 object have that set of limits.
1756 # {{{ sub ThawLimits
1762 #if we don't have $in, get outta here.
1763 return undef unless ($in);
1765 $self->{'RecalcTicketLimits'} = 1;
1768 require MIME::Base64;
1770 #We don't need to die if the thaw fails.
1771 @{$self}{ $self->_FreezeThawKeys }
1772 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1774 $RT::Logger->error($@) if $@;
1780 # {{{ Limit by enum or foreign key
1782 # {{{ sub LimitQueue
1786 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1787 OPERATOR is one of = or !=. (It defaults to =).
1788 VALUE is a queue id or Name.
1801 #TODO VALUE should also take queue objects
1802 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1803 my $queue = new RT::Queue( $self->CurrentUser );
1804 $queue->Load( $args{'VALUE'} );
1805 $args{'VALUE'} = $queue->Id;
1808 # What if they pass in an Id? Check for isNum() and convert to
1811 #TODO check for a valid queue here
1815 VALUE => $args{'VALUE'},
1816 OPERATOR => $args{'OPERATOR'},
1817 DESCRIPTION => join(
1818 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1826 # {{{ sub LimitStatus
1830 Takes a paramhash with the fields OPERATOR and VALUE.
1831 OPERATOR is one of = or !=.
1834 RT adds Status != 'deleted' until object has
1835 allow_deleted_search internal property set.
1836 $tickets->{'allow_deleted_search'} = 1;
1837 $tickets->LimitStatus( VALUE => 'deleted' );
1849 VALUE => $args{'VALUE'},
1850 OPERATOR => $args{'OPERATOR'},
1851 DESCRIPTION => join( ' ',
1852 $self->loc('Status'), $args{'OPERATOR'},
1853 $self->loc( $args{'VALUE'} ) ),
1859 # {{{ sub IgnoreType
1863 If called, this search will not automatically limit the set of results found
1864 to tickets of type "Ticket". Tickets of other types, such as "project" and
1865 "approval" will be found.
1872 # Instead of faking a Limit that later gets ignored, fake up the
1873 # fact that we're already looking at type, so that the check in
1874 # Tickets_Overlay_SQL/FromSQL goes down the right branch
1876 # $self->LimitType(VALUE => '__any');
1877 $self->{looking_at_type} = 1;
1886 Takes a paramhash with the fields OPERATOR and VALUE.
1887 OPERATOR is one of = or !=, it defaults to "=".
1888 VALUE is a string to search for in the type of the ticket.
1903 VALUE => $args{'VALUE'},
1904 OPERATOR => $args{'OPERATOR'},
1905 DESCRIPTION => join( ' ',
1906 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
1914 # {{{ Limit by string field
1916 # {{{ sub LimitSubject
1920 Takes a paramhash with the fields OPERATOR and VALUE.
1921 OPERATOR is one of = or !=.
1922 VALUE is a string to search for in the subject of the ticket.
1931 VALUE => $args{'VALUE'},
1932 OPERATOR => $args{'OPERATOR'},
1933 DESCRIPTION => join( ' ',
1934 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1942 # {{{ Limit based on ticket numerical attributes
1943 # Things that can be > < = !=
1949 Takes a paramhash with the fields OPERATOR and VALUE.
1950 OPERATOR is one of =, >, < or !=.
1951 VALUE is a ticket Id to search for
1964 VALUE => $args{'VALUE'},
1965 OPERATOR => $args{'OPERATOR'},
1967 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1973 # {{{ sub LimitPriority
1975 =head2 LimitPriority
1977 Takes a paramhash with the fields OPERATOR and VALUE.
1978 OPERATOR is one of =, >, < or !=.
1979 VALUE is a value to match the ticket\'s priority against
1987 FIELD => 'Priority',
1988 VALUE => $args{'VALUE'},
1989 OPERATOR => $args{'OPERATOR'},
1990 DESCRIPTION => join( ' ',
1991 $self->loc('Priority'),
1992 $args{'OPERATOR'}, $args{'VALUE'}, ),
1998 # {{{ sub LimitInitialPriority
2000 =head2 LimitInitialPriority
2002 Takes a paramhash with the fields OPERATOR and VALUE.
2003 OPERATOR is one of =, >, < or !=.
2004 VALUE is a value to match the ticket\'s initial priority against
2009 sub LimitInitialPriority {
2013 FIELD => 'InitialPriority',
2014 VALUE => $args{'VALUE'},
2015 OPERATOR => $args{'OPERATOR'},
2016 DESCRIPTION => join( ' ',
2017 $self->loc('Initial Priority'), $args{'OPERATOR'},
2024 # {{{ sub LimitFinalPriority
2026 =head2 LimitFinalPriority
2028 Takes a paramhash with the fields OPERATOR and VALUE.
2029 OPERATOR is one of =, >, < or !=.
2030 VALUE is a value to match the ticket\'s final priority against
2034 sub LimitFinalPriority {
2038 FIELD => 'FinalPriority',
2039 VALUE => $args{'VALUE'},
2040 OPERATOR => $args{'OPERATOR'},
2041 DESCRIPTION => join( ' ',
2042 $self->loc('Final Priority'), $args{'OPERATOR'},
2049 # {{{ sub LimitTimeWorked
2051 =head2 LimitTimeWorked
2053 Takes a paramhash with the fields OPERATOR and VALUE.
2054 OPERATOR is one of =, >, < or !=.
2055 VALUE is a value to match the ticket's TimeWorked attribute
2059 sub LimitTimeWorked {
2063 FIELD => 'TimeWorked',
2064 VALUE => $args{'VALUE'},
2065 OPERATOR => $args{'OPERATOR'},
2066 DESCRIPTION => join( ' ',
2067 $self->loc('Time Worked'),
2068 $args{'OPERATOR'}, $args{'VALUE'}, ),
2074 # {{{ sub LimitTimeLeft
2076 =head2 LimitTimeLeft
2078 Takes a paramhash with the fields OPERATOR and VALUE.
2079 OPERATOR is one of =, >, < or !=.
2080 VALUE is a value to match the ticket's TimeLeft attribute
2088 FIELD => 'TimeLeft',
2089 VALUE => $args{'VALUE'},
2090 OPERATOR => $args{'OPERATOR'},
2091 DESCRIPTION => join( ' ',
2092 $self->loc('Time Left'),
2093 $args{'OPERATOR'}, $args{'VALUE'}, ),
2101 # {{{ Limiting based on attachment attributes
2103 # {{{ sub LimitContent
2107 Takes a paramhash with the fields OPERATOR and VALUE.
2108 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2109 VALUE is a string to search for in the body of the ticket
2118 VALUE => $args{'VALUE'},
2119 OPERATOR => $args{'OPERATOR'},
2120 DESCRIPTION => join( ' ',
2121 $self->loc('Ticket content'), $args{'OPERATOR'},
2128 # {{{ sub LimitFilename
2130 =head2 LimitFilename
2132 Takes a paramhash with the fields OPERATOR and VALUE.
2133 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2134 VALUE is a string to search for in the body of the ticket
2142 FIELD => 'Filename',
2143 VALUE => $args{'VALUE'},
2144 OPERATOR => $args{'OPERATOR'},
2145 DESCRIPTION => join( ' ',
2146 $self->loc('Attachment filename'), $args{'OPERATOR'},
2152 # {{{ sub LimitContentType
2154 =head2 LimitContentType
2156 Takes a paramhash with the fields OPERATOR and VALUE.
2157 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2158 VALUE is a content type to search ticket attachments for
2162 sub LimitContentType {
2166 FIELD => 'ContentType',
2167 VALUE => $args{'VALUE'},
2168 OPERATOR => $args{'OPERATOR'},
2169 DESCRIPTION => join( ' ',
2170 $self->loc('Ticket content type'), $args{'OPERATOR'},
2179 # {{{ Limiting based on people
2181 # {{{ sub LimitOwner
2185 Takes a paramhash with the fields OPERATOR and VALUE.
2186 OPERATOR is one of = or !=.
2198 my $owner = new RT::User( $self->CurrentUser );
2199 $owner->Load( $args{'VALUE'} );
2201 # FIXME: check for a valid $owner
2204 VALUE => $args{'VALUE'},
2205 OPERATOR => $args{'OPERATOR'},
2206 DESCRIPTION => join( ' ',
2207 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2214 # {{{ Limiting watchers
2216 # {{{ sub LimitWatcher
2220 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2221 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2222 VALUE is a value to match the ticket\'s watcher email addresses against
2223 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2237 #build us up a description
2238 my ( $watcher_type, $desc );
2239 if ( $args{'TYPE'} ) {
2240 $watcher_type = $args{'TYPE'};
2243 $watcher_type = "Watcher";
2247 FIELD => $watcher_type,
2248 VALUE => $args{'VALUE'},
2249 OPERATOR => $args{'OPERATOR'},
2250 TYPE => $args{'TYPE'},
2251 DESCRIPTION => join( ' ',
2252 $self->loc($watcher_type),
2253 $args{'OPERATOR'}, $args{'VALUE'}, ),
2263 # {{{ Limiting based on links
2267 =head2 LimitLinkedTo
2269 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2270 TYPE limits the sort of link we want to search on
2272 TYPE = { RefersTo, MemberOf, DependsOn }
2274 TARGET is the id or URI of the TARGET of the link
2288 FIELD => 'LinkedTo',
2290 TARGET => $args{'TARGET'},
2291 TYPE => $args{'TYPE'},
2292 DESCRIPTION => $self->loc(
2293 "Tickets [_1] by [_2]",
2294 $self->loc( $args{'TYPE'} ),
2297 OPERATOR => $args{'OPERATOR'},
2303 # {{{ LimitLinkedFrom
2305 =head2 LimitLinkedFrom
2307 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2308 TYPE limits the sort of link we want to search on
2311 BASE is the id or URI of the BASE of the link
2315 sub LimitLinkedFrom {
2324 # translate RT2 From/To naming to RT3 TicketSQL naming
2325 my %fromToMap = qw(DependsOn DependentOn
2327 RefersTo ReferredToBy);
2329 my $type = $args{'TYPE'};
2330 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2333 FIELD => 'LinkedTo',
2335 BASE => $args{'BASE'},
2337 DESCRIPTION => $self->loc(
2338 "Tickets [_1] [_2]",
2339 $self->loc( $args{'TYPE'} ),
2342 OPERATOR => $args{'OPERATOR'},
2351 my $ticket_id = shift;
2352 return $self->LimitLinkedTo(
2354 TARGET => $ticket_id,
2361 # {{{ LimitHasMember
2362 sub LimitHasMember {
2364 my $ticket_id = shift;
2365 return $self->LimitLinkedFrom(
2367 BASE => "$ticket_id",
2368 TYPE => 'HasMember',
2375 # {{{ LimitDependsOn
2377 sub LimitDependsOn {
2379 my $ticket_id = shift;
2380 return $self->LimitLinkedTo(
2382 TARGET => $ticket_id,
2383 TYPE => 'DependsOn',
2390 # {{{ LimitDependedOnBy
2392 sub LimitDependedOnBy {
2394 my $ticket_id = shift;
2395 return $self->LimitLinkedFrom(
2398 TYPE => 'DependentOn',
2409 my $ticket_id = shift;
2410 return $self->LimitLinkedTo(
2412 TARGET => $ticket_id,
2420 # {{{ LimitReferredToBy
2422 sub LimitReferredToBy {
2424 my $ticket_id = shift;
2425 return $self->LimitLinkedFrom(
2428 TYPE => 'ReferredToBy',
2436 # {{{ limit based on ticket date attribtes
2440 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2442 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2444 OPERATOR is one of > or <
2445 VALUE is a date and time in ISO format in GMT
2446 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2448 There are also helper functions of the form LimitFIELD that eliminate
2449 the need to pass in a FIELD argument.
2463 #Set the description if we didn't get handed it above
2464 unless ( $args{'DESCRIPTION'} ) {
2465 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2466 . $args{'OPERATOR'} . " "
2467 . $args{'VALUE'} . " GMT";
2470 $self->Limit(%args);
2478 $self->LimitDate( FIELD => 'Created', @_ );
2483 $self->LimitDate( FIELD => 'Due', @_ );
2489 $self->LimitDate( FIELD => 'Starts', @_ );
2495 $self->LimitDate( FIELD => 'Started', @_ );
2500 $self->LimitDate( FIELD => 'Resolved', @_ );
2505 $self->LimitDate( FIELD => 'Told', @_ );
2508 sub LimitLastUpdated {
2510 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2514 # {{{ sub LimitTransactionDate
2516 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2518 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2520 OPERATOR is one of > or <
2521 VALUE is a date and time in ISO format in GMT
2526 sub LimitTransactionDate {
2529 FIELD => 'TransactionDate',
2536 # <20021217042756.GK28744@pallas.fsck.com>
2537 # "Kill It" - Jesse.
2539 #Set the description if we didn't get handed it above
2540 unless ( $args{'DESCRIPTION'} ) {
2541 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2542 . $args{'OPERATOR'} . " "
2543 . $args{'VALUE'} . " GMT";
2546 $self->Limit(%args);
2554 # {{{ Limit based on custom fields
2555 # {{{ sub LimitCustomField
2557 =head2 LimitCustomField
2559 Takes a paramhash of key/value pairs with the following keys:
2563 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2565 =item OPERATOR - The usual Limit operators
2567 =item VALUE - The value to compare against
2573 sub LimitCustomField {
2577 CUSTOMFIELD => undef,
2579 DESCRIPTION => undef,
2580 FIELD => 'CustomFieldValue',
2585 my $CF = RT::CustomField->new( $self->CurrentUser );
2586 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2587 $CF->Load( $args{CUSTOMFIELD} );
2590 $CF->LoadByNameAndQueue(
2591 Name => $args{CUSTOMFIELD},
2592 Queue => $args{QUEUE}
2594 $args{CUSTOMFIELD} = $CF->Id;
2597 #If we are looking to compare with a null value.
2598 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2599 $args{'DESCRIPTION'}
2600 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2602 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2603 $args{'DESCRIPTION'}
2604 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2607 # if we're not looking to compare with a null value
2609 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2610 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2613 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2614 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2615 $QueueObj->Load( $args{'QUEUE'} );
2616 $args{'QUEUE'} = $QueueObj->Id;
2618 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2621 @rest = ( ENTRYAGGREGATOR => 'AND' )
2622 if ( $CF->Type eq 'SelectMultiple' );
2625 VALUE => $args{VALUE},
2627 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2628 .".{" . $CF->Name . "}",
2629 OPERATOR => $args{OPERATOR},
2634 $self->{'RecalcTicketLimits'} = 1;
2640 # {{{ sub _NextIndex
2644 Keep track of the counter for the array of restrictions
2650 return ( $self->{'restriction_index'}++ );
2657 # {{{ Core bits to make this a DBIx::SearchBuilder object
2662 $self->{'table'} = "Tickets";
2663 $self->{'RecalcTicketLimits'} = 1;
2664 $self->{'looking_at_effective_id'} = 0;
2665 $self->{'looking_at_type'} = 0;
2666 $self->{'restriction_index'} = 1;
2667 $self->{'primary_key'} = "id";
2668 delete $self->{'items_array'};
2669 delete $self->{'item_map'};
2670 delete $self->{'columns_to_display'};
2671 $self->SUPER::_Init(@_);
2682 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2683 return ( $self->SUPER::Count() );
2691 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2692 return ( $self->SUPER::CountAll() );
2697 # {{{ sub ItemsArrayRef
2699 =head2 ItemsArrayRef
2701 Returns a reference to the set of all items found in this search
2708 unless ( $self->{'items_array'} ) {
2710 my $placeholder = $self->_ItemsCounter;
2711 $self->GotoFirstItem();
2712 while ( my $item = $self->Next ) {
2713 push( @{ $self->{'items_array'} }, $item );
2715 $self->GotoItem($placeholder);
2716 $self->{'items_array'}
2717 = $self->ItemsOrderBy( $self->{'items_array'} );
2719 return ( $self->{'items_array'} );
2728 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2730 my $Ticket = $self->SUPER::Next;
2731 return $Ticket unless $Ticket;
2733 if ( $Ticket->__Value('Status') eq 'deleted'
2734 && !$self->{'allow_deleted_search'} )
2738 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2739 # if we found a ticket with this option enabled then
2740 # all tickets we found are ACLed, cache this fact
2741 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2742 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2745 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2750 # If the user doesn't have the right to show this ticket
2757 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2758 return $self->SUPER::_DoSearch( @_ );
2763 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2764 return $self->SUPER::_DoCount( @_ );
2770 my $cache_key = 'RolesHasRight;:;ShowTicket';
2772 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
2776 my $ACL = RT::ACL->new( $RT::SystemUser );
2777 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2778 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
2779 my $principal_alias = $ACL->Join(
2781 FIELD1 => 'PrincipalId',
2782 TABLE2 => 'Principals',
2785 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2788 while ( my $ACE = $ACL->Next ) {
2789 my $role = $ACE->PrincipalType;
2790 my $type = $ACE->ObjectType;
2791 if ( $type eq 'RT::System' ) {
2794 elsif ( $type eq 'RT::Queue' ) {
2795 next if $res{ $role } && !ref $res{ $role };
2796 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
2799 $RT::Logger->error('ShowTicket right is granted on unsupported object');
2802 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
2806 sub _DirectlyCanSeeIn {
2808 my $id = $self->CurrentUser->id;
2810 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
2811 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
2815 my $ACL = RT::ACL->new( $RT::SystemUser );
2816 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2817 my $principal_alias = $ACL->Join(
2819 FIELD1 => 'PrincipalId',
2820 TABLE2 => 'Principals',
2823 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2824 my $cgm_alias = $ACL->Join(
2826 FIELD1 => 'PrincipalId',
2827 TABLE2 => 'CachedGroupMembers',
2828 FIELD2 => 'GroupId',
2830 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
2831 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
2834 while ( my $ACE = $ACL->Next ) {
2835 my $type = $ACE->ObjectType;
2836 if ( $type eq 'RT::System' ) {
2837 # If user is direct member of a group that has the right
2838 # on the system then he can see any ticket
2839 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
2842 elsif ( $type eq 'RT::Queue' ) {
2843 push @res, $ACE->ObjectId;
2846 $RT::Logger->error('ShowTicket right is granted on unsupported object');
2849 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
2853 sub CurrentUserCanSee {
2855 return if $self->{'_sql_current_user_can_see_applied'};
2857 return $self->{'_sql_current_user_can_see_applied'} = 1
2858 if $self->CurrentUser->UserObj->HasRight(
2859 Right => 'SuperUser', Object => $RT::System
2862 my $id = $self->CurrentUser->id;
2864 # directly can see in all queues then we have nothing to do
2865 my @direct_queues = $self->_DirectlyCanSeeIn;
2866 return $self->{'_sql_current_user_can_see_applied'} = 1
2867 if @direct_queues && $direct_queues[0] == -1;
2869 my %roles = $self->_RolesCanSee;
2871 my %skip = map { $_ => 1 } @direct_queues;
2872 foreach my $role ( keys %roles ) {
2873 next unless ref $roles{ $role };
2875 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
2877 $roles{ $role } = \@queues;
2879 delete $roles{ $role };
2884 # there is no global watchers, only queues and tickes, if at
2885 # some point we will add global roles then it's gonna blow
2886 # the idea here is that if the right is set globaly for a role
2887 # and user plays this role for a queue directly not a ticket
2888 # then we have to check in advance
2889 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
2891 my $groups = RT::Groups->new( $RT::SystemUser );
2892 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
2894 $groups->Limit( FIELD => 'Type', VALUE => $_ );
2896 my $principal_alias = $groups->Join(
2899 TABLE2 => 'Principals',
2902 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2903 my $cgm_alias = $groups->Join(
2906 TABLE2 => 'CachedGroupMembers',
2907 FIELD2 => 'GroupId',
2909 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
2910 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
2911 while ( my $group = $groups->Next ) {
2912 push @direct_queues, $group->Instance;
2917 my $join_roles = keys %roles;
2918 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
2919 my ($role_group_alias, $cgm_alias);
2920 if ( $join_roles ) {
2921 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
2922 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
2923 $self->SUPER::Limit(
2924 LEFTJOIN => $cgm_alias,
2925 FIELD => 'MemberId',
2930 my $limit_queues = sub {
2934 return unless @queues;
2935 if ( @queues == 1 ) {
2940 ENTRYAGGREGATOR => $ea,
2944 foreach my $q ( @queues ) {
2949 ENTRYAGGREGATOR => $ea,
2960 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
2961 while ( my ($role, $queues) = each %roles ) {
2963 if ( $role eq 'Owner' ) {
2967 ENTRYAGGREGATOR => $ea,
2972 ALIAS => $cgm_alias,
2973 FIELD => 'MemberId',
2974 OPERATOR => 'IS NOT',
2977 ENTRYAGGREGATOR => $ea,
2980 ALIAS => $role_group_alias,
2983 ENTRYAGGREGATOR => 'AND',
2986 $limit_queues->( 'AND', @$queues ) if ref $queues;
2987 $ea = 'OR' if $ea eq 'AND';
2992 return $self->{'_sql_current_user_can_see_applied'} = 1;
2999 # {{{ Deal with storing and restoring restrictions
3001 # {{{ sub LoadRestrictions
3003 =head2 LoadRestrictions
3005 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3006 TODO It is not yet implemented
3012 # {{{ sub DescribeRestrictions
3014 =head2 DescribeRestrictions
3017 Returns a hash keyed by restriction id.
3018 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3019 is a description of the purpose of that TicketRestriction
3023 sub DescribeRestrictions {
3026 my ( $row, %listing );
3028 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3029 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3036 # {{{ sub RestrictionValues
3038 =head2 RestrictionValues FIELD
3040 Takes a restriction field and returns a list of values this field is restricted
3045 sub RestrictionValues {
3048 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3049 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3050 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3052 keys %{ $self->{'TicketRestrictions'} };
3057 # {{{ sub ClearRestrictions
3059 =head2 ClearRestrictions
3061 Removes all restrictions irretrievably
3065 sub ClearRestrictions {
3067 delete $self->{'TicketRestrictions'};
3068 $self->{'looking_at_effective_id'} = 0;
3069 $self->{'looking_at_type'} = 0;
3070 $self->{'RecalcTicketLimits'} = 1;
3075 # {{{ sub DeleteRestriction
3077 =head2 DeleteRestriction
3079 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3080 Removes that restriction from the session's limits.
3084 sub DeleteRestriction {
3087 delete $self->{'TicketRestrictions'}{$row};
3089 $self->{'RecalcTicketLimits'} = 1;
3091 #make the underlying easysearch object forget all its preconceptions
3096 # {{{ sub _RestrictionsToClauses
3098 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3100 sub _RestrictionsToClauses {
3105 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3106 my $restriction = $self->{'TicketRestrictions'}{$row};
3108 # We need to reimplement the subclause aggregation that SearchBuilder does.
3109 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3110 # Then SB AND's the different Subclauses together.
3112 # So, we want to group things into Subclauses, convert them to
3113 # SQL, and then join them with the appropriate DefaultEA.
3114 # Then join each subclause group with AND.
3116 my $field = $restriction->{'FIELD'};
3117 my $realfield = $field; # CustomFields fake up a fieldname, so
3118 # we need to figure that out
3121 # Rewrite LinkedTo meta field to the real field
3122 if ( $field =~ /LinkedTo/ ) {
3123 $realfield = $field = $restriction->{'TYPE'};
3127 # Handle subkey fields with a different real field
3128 if ( $field =~ /^(\w+)\./ ) {
3132 die "I don't know about $field yet"
3133 unless ( exists $FIELD_METADATA{$realfield}
3134 or $restriction->{CUSTOMFIELD} );
3136 my $type = $FIELD_METADATA{$realfield}->[0];
3137 my $op = $restriction->{'OPERATOR'};
3141 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3144 # this performs the moral equivalent of defined or/dor/C<//>,
3145 # without the short circuiting.You need to use a 'defined or'
3146 # type thing instead of just checking for truth values, because
3147 # VALUE could be 0.(i.e. "false")
3149 # You could also use this, but I find it less aesthetic:
3150 # (although it does short circuit)
3151 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3152 # defined $restriction->{'TICKET'} ?
3153 # $restriction->{TICKET} :
3154 # defined $restriction->{'BASE'} ?
3155 # $restriction->{BASE} :
3156 # defined $restriction->{'TARGET'} ?
3157 # $restriction->{TARGET} )
3159 my $ea = $restriction->{ENTRYAGGREGATOR}
3160 || $DefaultEA{$type}
3163 die "Invalid operator $op for $field ($type)"
3164 unless exists $ea->{$op};
3168 # Each CustomField should be put into a different Clause so they
3169 # are ANDed together.
3170 if ( $restriction->{CUSTOMFIELD} ) {
3171 $realfield = $field;
3174 exists $clause{$realfield} or $clause{$realfield} = [];
3177 $field =~ s!(['"])!\\$1!g;
3178 $value =~ s!(['"])!\\$1!g;
3179 my $data = [ $ea, $type, $field, $op, $value ];
3181 # here is where we store extra data, say if it's a keyword or
3182 # something. (I.e. "TYPE SPECIFIC STUFF")
3184 push @{ $clause{$realfield} }, $data;
3191 # {{{ sub _ProcessRestrictions
3193 =head2 _ProcessRestrictions PARAMHASH
3195 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3196 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3200 sub _ProcessRestrictions {
3203 #Blow away ticket aliases since we'll need to regenerate them for
3205 delete $self->{'TicketAliases'};
3206 delete $self->{'items_array'};
3207 delete $self->{'item_map'};
3208 delete $self->{'raw_rows'};
3209 delete $self->{'rows'};
3210 delete $self->{'count_all'};
3212 my $sql = $self->Query; # Violating the _SQL namespace
3213 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3215 # "Restrictions to Clauses Branch\n";
3216 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3218 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3222 $sql = $self->ClausesToSQL($clauseRef);
3223 $self->FromSQL($sql) if $sql;
3227 $self->{'RecalcTicketLimits'} = 0;
3231 =head2 _BuildItemMap
3233 # Build up a map of first/last/next/prev items, so that we can display search nav quickly
3240 my $items = $self->ItemsArrayRef;
3243 delete $self->{'item_map'};
3244 if ( $items->[0] ) {
3245 $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
3246 while ( my $item = shift @$items ) {
3247 my $id = $item->EffectiveId;
3248 $self->{'item_map'}->{$id}->{'defined'} = 1;
3249 $self->{'item_map'}->{$id}->{prev} = $prev;
3250 $self->{'item_map'}->{$id}->{next} = $items->[0]->EffectiveId
3254 $self->{'item_map'}->{'last'} = $prev;
3260 Returns an a map of all items found by this search. The map is of the form
3262 $ItemMap->{'first'} = first ticketid found
3263 $ItemMap->{'last'} = last ticketid found
3264 $ItemMap->{$id}->{prev} = the ticket id found before $id
3265 $ItemMap->{$id}->{next} = the ticket id found after $id
3271 $self->_BuildItemMap()
3272 unless ( $self->{'items_array'} and $self->{'item_map'} );
3273 return ( $self->{'item_map'} );
3281 =head2 PrepForSerialization
3283 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
3287 sub PrepForSerialization {
3289 delete $self->{'items'};
3290 $self->RedoSearch();
3295 RT::Tickets supports several flags which alter search behavior:
3298 allow_deleted_search (Otherwise never show deleted tickets in search results)
3299 looking_at_type (otherwise limit to type=ticket)
3301 These flags are set by calling
3303 $tickets->{'flagname'} = 1;
3305 BUG: There should be an API for this