1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 # <sales@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 our %SEARCHABLE_SUBFIELDS = (
152 EmailAddress Name RealName Nickname Organization Address1 Address2
153 WorkPhone HomePhone MobilePhone PagerPhone id
157 # Mapping of Field Type to Function
159 ENUM => \&_EnumLimit,
162 LINK => \&_LinkLimit,
163 DATE => \&_DateLimit,
164 STRING => \&_StringLimit,
165 TRANSFIELD => \&_TransLimit,
166 TRANSDATE => \&_TransDateLimit,
167 WATCHERFIELD => \&_WatcherLimit,
168 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
169 CUSTOMFIELD => \&_CustomFieldLimit,
170 HASATTRIBUTE => \&_HasAttributeLimit,
172 our %can_bundle = ();# WATCHERFIELD => "yes", );
174 # Default EntryAggregator per type
175 # if you specify OP, you must specify all valid OPs
216 # Helper functions for passing the above lexically scoped tables above
217 # into Tickets_Overlay_SQL.
218 sub FIELDS { return \%FIELD_METADATA }
219 sub dispatch { return \%dispatch }
220 sub can_bundle { return \%can_bundle }
222 # Bring in the clowns.
223 require RT::Tickets_Overlay_SQL;
227 our @SORTFIELDS = qw(id Status
229 Owner Created Due Starts Started
231 Resolved LastUpdated Priority TimeWorked TimeLeft);
235 Returns the list of fields that lists of tickets can easily be sorted by
241 return (@SORTFIELDS);
246 # BEGIN SQL STUFF *********************************
251 $self->SUPER::CleanSlate( @_ );
252 delete $self->{$_} foreach qw(
254 _sql_group_members_aliases
255 _sql_object_cfv_alias
256 _sql_role_group_aliases
259 _sql_u_watchers_alias_for_sort
260 _sql_u_watchers_aliases
261 _sql_current_user_can_see_applied
265 =head1 Limit Helper Routines
267 These routines are the targets of a dispatch table depending on the
268 type of field. They all share the same signature:
270 my ($self,$field,$op,$value,@rest) = @_;
272 The values in @rest should be suitable for passing directly to
273 DBIx::SearchBuilder::Limit.
275 Essentially they are an expanded/broken out (and much simplified)
276 version of what ProcessRestrictions used to do. They're also much
277 more clearly delineated by the TYPE of field being processed.
286 my ( $sb, $field, $op, $value, @rest ) = @_;
288 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
290 die "Invalid operator $op for __Bookmarked__ search on $field"
291 unless $op =~ /^(=|!=)$/;
294 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
295 $tmp = $tmp->Content if $tmp;
300 return $sb->_SQLLimit(
307 # as bookmarked tickets can be merged we have to use a join
308 # but it should be pretty lightweight
309 my $tickets_alias = $sb->Join(
314 FIELD2 => 'EffectiveId',
318 my $ea = $op eq '='? 'OR': 'AND';
319 foreach my $id ( sort @bookmarks ) {
321 ALIAS => $tickets_alias,
325 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
333 Handle Fields which are limited to certain values, and potentially
334 need to be looked up from another class.
336 This subroutine actually handles two different kinds of fields. For
337 some the user is responsible for limiting the values. (i.e. Status,
340 For others, the value specified by the user will be looked by via
344 name of class to lookup in (Optional)
349 my ( $sb, $field, $op, $value, @rest ) = @_;
351 # SQL::Statement changes != to <>. (Can we remove this now?)
352 $op = "!=" if $op eq "<>";
354 die "Invalid Operation: $op for $field"
358 my $meta = $FIELD_METADATA{$field};
359 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
360 my $class = "RT::" . $meta->[1];
361 my $o = $class->new( $sb->CurrentUser );
375 Handle fields where the values are limited to integers. (For example,
376 Priority, TimeWorked.)
384 my ( $sb, $field, $op, $value, @rest ) = @_;
386 die "Invalid Operator $op for $field"
387 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
399 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
402 1: Direction (From, To)
403 2: Link Type (MemberOf, DependsOn, RefersTo)
408 my ( $sb, $field, $op, $value, @rest ) = @_;
410 my $meta = $FIELD_METADATA{$field};
411 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
414 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
418 $is_null = 1 if !$value || $value =~ /^null$/io;
420 my $direction = $meta->[1] || '';
421 my ($matchfield, $linkfield) = ('', '');
422 if ( $direction eq 'To' ) {
423 ($matchfield, $linkfield) = ("Target", "Base");
425 elsif ( $direction eq 'From' ) {
426 ($matchfield, $linkfield) = ("Base", "Target");
428 elsif ( $direction ) {
429 die "Invalid link direction '$direction' for $field\n";
432 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
434 'LinkedFrom', $op, $value, @rest,
435 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
443 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
445 elsif ( $value =~ /\D/ ) {
448 $matchfield = "Local$matchfield" if $is_local;
450 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
451 # SELECT main.* FROM Tickets main
452 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
453 # AND(main.id = Links_1.LocalTarget))
454 # WHERE Links_1.LocalBase IS NULL;
457 my $linkalias = $sb->Join(
462 FIELD2 => 'Local' . $linkfield
465 LEFTJOIN => $linkalias,
473 FIELD => $matchfield,
480 my $linkalias = $sb->Join(
485 FIELD2 => 'Local' . $linkfield
488 LEFTJOIN => $linkalias,
494 LEFTJOIN => $linkalias,
495 FIELD => $matchfield,
502 FIELD => $matchfield,
503 OPERATOR => $is_negative? 'IS': 'IS NOT',
512 Handle date fields. (Created, LastTold..)
515 1: type of link. (Probably not necessary.)
520 my ( $sb, $field, $op, $value, @rest ) = @_;
522 die "Invalid Date Op: $op"
523 unless $op =~ /^(=|>|<|>=|<=)$/;
525 my $meta = $FIELD_METADATA{$field};
526 die "Incorrect Meta Data for $field"
527 unless ( defined $meta->[1] );
529 my $date = RT::Date->new( $sb->CurrentUser );
530 $date->Set( Format => 'unknown', Value => $value );
534 # if we're specifying =, that means we want everything on a
535 # particular single day. in the database, we need to check for >
536 # and < the edges of that day.
538 $date->SetToMidnight( Timezone => 'server' );
539 my $daystart = $date->ISO;
541 my $dayend = $date->ISO;
557 ENTRYAGGREGATOR => 'AND',
575 Handle simple fields which are just strings. (Subject,Type)
583 my ( $sb, $field, $op, $value, @rest ) = @_;
587 # =, !=, LIKE, NOT LIKE
588 if ( (!defined $value || !length $value)
589 && lc($op) ne 'is' && lc($op) ne 'is not'
590 && RT->Config->Get('DatabaseType') eq 'Oracle'
592 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
593 $op = $negative? 'IS NOT': 'IS';
606 =head2 _TransDateLimit
608 Handle fields limiting based on Transaction Date.
610 The inpupt value must be in a format parseable by Time::ParseDate
617 # This routine should really be factored into translimit.
618 sub _TransDateLimit {
619 my ( $sb, $field, $op, $value, @rest ) = @_;
621 # See the comments for TransLimit, they apply here too
623 unless ( $sb->{_sql_transalias} ) {
624 $sb->{_sql_transalias} = $sb->Join(
627 TABLE2 => 'Transactions',
628 FIELD2 => 'ObjectId',
631 ALIAS => $sb->{_sql_transalias},
632 FIELD => 'ObjectType',
633 VALUE => 'RT::Ticket',
634 ENTRYAGGREGATOR => 'AND',
638 my $date = RT::Date->new( $sb->CurrentUser );
639 $date->Set( Format => 'unknown', Value => $value );
644 # if we're specifying =, that means we want everything on a
645 # particular single day. in the database, we need to check for >
646 # and < the edges of that day.
648 $date->SetToMidnight( Timezone => 'server' );
649 my $daystart = $date->ISO;
651 my $dayend = $date->ISO;
654 ALIAS => $sb->{_sql_transalias},
662 ALIAS => $sb->{_sql_transalias},
668 ENTRYAGGREGATOR => 'AND',
673 # not searching for a single day
676 #Search for the right field
678 ALIAS => $sb->{_sql_transalias},
692 Limit based on the Content of a transaction or the ContentType.
701 # Content, ContentType, Filename
703 # If only this was this simple. We've got to do something
706 #Basically, we want to make sure that the limits apply to
707 #the same attachment, rather than just another attachment
708 #for the same ticket, no matter how many clauses we lump
709 #on. We put them in TicketAliases so that they get nuked
710 #when we redo the join.
712 # In the SQL, we might have
713 # (( Content = foo ) or ( Content = bar AND Content = baz ))
714 # The AND group should share the same Alias.
716 # Actually, maybe it doesn't matter. We use the same alias and it
717 # works itself out? (er.. different.)
719 # Steal more from _ProcessRestrictions
721 # FIXME: Maybe look at the previous FooLimit call, and if it was a
722 # TransLimit and EntryAggregator == AND, reuse the Aliases?
724 # Or better - store the aliases on a per subclause basis - since
725 # those are going to be the things we want to relate to each other,
728 # maybe we should not allow certain kinds of aggregation of these
729 # clauses and do a psuedo regex instead? - the problem is getting
730 # them all into the same subclause when you have (A op B op C) - the
731 # way they get parsed in the tree they're in different subclauses.
733 my ( $self, $field, $op, $value, %rest ) = @_;
735 unless ( $self->{_sql_transalias} ) {
736 $self->{_sql_transalias} = $self->Join(
739 TABLE2 => 'Transactions',
740 FIELD2 => 'ObjectId',
743 ALIAS => $self->{_sql_transalias},
744 FIELD => 'ObjectType',
745 VALUE => 'RT::Ticket',
746 ENTRYAGGREGATOR => 'AND',
749 unless ( defined $self->{_sql_trattachalias} ) {
750 $self->{_sql_trattachalias} = $self->_SQLJoin(
751 TYPE => 'LEFT', # not all txns have an attachment
752 ALIAS1 => $self->{_sql_transalias},
754 TABLE2 => 'Attachments',
755 FIELD2 => 'TransactionId',
759 #Search for the right field
760 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
764 ALIAS => $self->{_sql_trattachalias},
771 ENTRYAGGREGATOR => 'AND',
772 ALIAS => $self->{_sql_trattachalias},
781 ALIAS => $self->{_sql_trattachalias},
794 Handle watcher limits. (Requestor, CC, etc..)
810 my $meta = $FIELD_METADATA{ $field };
811 my $type = $meta->[1] || '';
812 my $class = $meta->[2] || 'Ticket';
814 # Bail if the subfield is not allowed
816 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
818 die "Invalid watcher subfield: '$rest{SUBKEY}'";
821 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
822 # search by id and Name at the same time, this is workaround
823 # to preserve backward compatibility
824 if ( $field eq 'Owner' ) {
825 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
826 my $o = RT::User->new( $self->CurrentUser );
827 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
828 $o->$method( $value );
837 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
847 $rest{SUBKEY} ||= 'EmailAddress';
849 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
852 if ( $op =~ /^IS(?: NOT)?$/ ) {
853 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
854 # to avoid joining the table Users into the query, we just join GM
855 # and make sure we don't match records where group is member of itself
857 LEFTJOIN => $group_members,
860 VALUE => "$group_members.MemberId",
864 ALIAS => $group_members,
871 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
873 $op =~ s/!|NOT\s+//i;
875 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
876 # "X = 'Y'" matches more then one user so we try to fetch two records and
877 # do the right thing when there is only one exist and semi-working solution
879 my $users_obj = RT::Users->new( $self->CurrentUser );
881 FIELD => $rest{SUBKEY},
886 $users_obj->RowsPerPage(2);
887 my @users = @{ $users_obj->ItemsArrayRef };
889 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
892 $uid = $users[0]->id if @users;
894 LEFTJOIN => $group_members,
895 ALIAS => $group_members,
901 ALIAS => $group_members,
908 LEFTJOIN => $group_members,
911 VALUE => "$group_members.MemberId",
914 my $users = $self->Join(
916 ALIAS1 => $group_members,
917 FIELD1 => 'MemberId',
924 FIELD => $rest{SUBKEY},
938 my $group_members = $self->_GroupMembersJoin(
939 GroupsAlias => $groups,
943 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
945 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
946 $self->NewAlias('Users');
948 LEFTJOIN => $group_members,
949 ALIAS => $group_members,
951 VALUE => "$users.id",
956 # we join users table without adding some join condition between tables,
957 # the only conditions we have are conditions on the table iteslf,
958 # for example Users.EmailAddress = 'x'. We should add this condition to
959 # the top level of the query and bundle it with another similar conditions,
960 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
961 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
964 SUBCLAUSE => '_sql_u_watchers_'. $users,
966 FIELD => $rest{'SUBKEY'},
971 # A condition which ties Users and Groups (role groups) is a left join condition
972 # of CachedGroupMembers table. To get correct results of the query we check
973 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
976 ALIAS => $group_members,
978 OPERATOR => 'IS NOT',
985 sub _RoleGroupsJoin {
987 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
988 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
989 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
992 # we always have watcher groups for ticket, so we use INNER join
993 my $groups = $self->Join(
995 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
997 FIELD2 => 'Instance',
998 ENTRYAGGREGATOR => 'AND',
1000 $self->SUPER::Limit(
1001 LEFTJOIN => $groups,
1004 VALUE => 'RT::'. $args{'Class'} .'-Role',
1006 $self->SUPER::Limit(
1007 LEFTJOIN => $groups,
1010 VALUE => $args{'Type'},
1013 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1014 unless $args{'New'};
1019 sub _GroupMembersJoin {
1021 my %args = (New => 1, GroupsAlias => undef, @_);
1023 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1024 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1027 my $alias = $self->Join(
1029 ALIAS1 => $args{'GroupsAlias'},
1031 TABLE2 => 'CachedGroupMembers',
1032 FIELD2 => 'GroupId',
1033 ENTRYAGGREGATOR => 'AND',
1036 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1037 unless $args{'New'};
1044 Helper function which provides joins to a watchers table both for limits
1051 my $type = shift || '';
1054 my $groups = $self->_RoleGroupsJoin( Type => $type );
1055 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1056 # XXX: work around, we must hide groups that
1057 # are members of the role group we search in,
1058 # otherwise them result in wrong NULLs in Users
1059 # table and break ordering. Now, we know that
1060 # RT doesn't allow to add groups as members of the
1061 # ticket roles, so we just hide entries in CGM table
1062 # with MemberId == GroupId from results
1063 $self->SUPER::Limit(
1064 LEFTJOIN => $group_members,
1067 VALUE => "$group_members.MemberId",
1070 my $users = $self->Join(
1072 ALIAS1 => $group_members,
1073 FIELD1 => 'MemberId',
1077 return ($groups, $group_members, $users);
1080 =head2 _WatcherMembershipLimit
1082 Handle watcher membership limits, i.e. whether the watcher belongs to a
1083 specific group or not.
1086 1: Field to query on
1088 SELECT DISTINCT main.*
1092 CachedGroupMembers CachedGroupMembers_2,
1095 (main.EffectiveId = main.id)
1097 (main.Status != 'deleted')
1099 (main.Type = 'ticket')
1102 (Users_3.EmailAddress = '22')
1104 (Groups_1.Domain = 'RT::Ticket-Role')
1106 (Groups_1.Type = 'RequestorGroup')
1109 Groups_1.Instance = main.id
1111 Groups_1.id = CachedGroupMembers_2.GroupId
1113 CachedGroupMembers_2.MemberId = Users_3.id
1114 ORDER BY main.id ASC
1119 sub _WatcherMembershipLimit {
1120 my ( $self, $field, $op, $value, @rest ) = @_;
1125 my $groups = $self->NewAlias('Groups');
1126 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1127 my $users = $self->NewAlias('Users');
1128 my $memberships = $self->NewAlias('CachedGroupMembers');
1130 if ( ref $field ) { # gross hack
1131 my @bundle = @$field;
1133 for my $chunk (@bundle) {
1134 ( $field, $op, $value, @rest ) = @$chunk;
1136 ALIAS => $memberships,
1147 ALIAS => $memberships,
1155 # {{{ Tie to groups for tickets we care about
1159 VALUE => 'RT::Ticket-Role',
1160 ENTRYAGGREGATOR => 'AND'
1165 FIELD1 => 'Instance',
1172 # If we care about which sort of watcher
1173 my $meta = $FIELD_METADATA{$field};
1174 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1181 ENTRYAGGREGATOR => 'AND'
1188 ALIAS2 => $groupmembers,
1193 ALIAS1 => $groupmembers,
1194 FIELD1 => 'MemberId',
1200 ALIAS1 => $memberships,
1201 FIELD1 => 'MemberId',
1210 =head2 _CustomFieldDecipher
1212 Try and turn a CF descriptor into (cfid, cfname) object pair.
1216 sub _CustomFieldDecipher {
1217 my ($self, $string) = @_;
1219 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1220 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1224 my $q = RT::Queue->new( $self->CurrentUser );
1228 # $queue = $q->Name; # should we normalize the queue?
1229 $cf = $q->CustomField( $field );
1232 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1236 elsif ( $field =~ /\D/ ) {
1238 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1239 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1240 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1242 # if there is more then one field the current user can
1243 # see with the same name then we shouldn't return cf object
1244 # as we don't know which one to use
1247 $cf = undef if $cfs->Next;
1251 $cf = RT::CustomField->new( $self->CurrentUser );
1252 $cf->Load( $field );
1255 return ($queue, $field, $cf, $column);
1258 =head2 _CustomFieldJoin
1260 Factor out the Join of custom fields so we can use it for sorting too
1264 sub _CustomFieldJoin {
1265 my ($self, $cfkey, $cfid, $field) = @_;
1266 # Perform one Join per CustomField
1267 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1268 $self->{_sql_cf_alias}{$cfkey} )
1270 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1271 $self->{_sql_cf_alias}{$cfkey} );
1274 my ($TicketCFs, $CFs);
1276 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1280 TABLE2 => 'ObjectCustomFieldValues',
1281 FIELD2 => 'ObjectId',
1283 $self->SUPER::Limit(
1284 LEFTJOIN => $TicketCFs,
1285 FIELD => 'CustomField',
1287 ENTRYAGGREGATOR => 'AND'
1291 my $ocfalias = $self->Join(
1294 TABLE2 => 'ObjectCustomFields',
1295 FIELD2 => 'ObjectId',
1298 $self->SUPER::Limit(
1299 LEFTJOIN => $ocfalias,
1300 ENTRYAGGREGATOR => 'OR',
1301 FIELD => 'ObjectId',
1305 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1307 ALIAS1 => $ocfalias,
1308 FIELD1 => 'CustomField',
1309 TABLE2 => 'CustomFields',
1312 $self->SUPER::Limit(
1314 ENTRYAGGREGATOR => 'AND',
1315 FIELD => 'LookupType',
1316 VALUE => 'RT::Queue-RT::Ticket',
1318 $self->SUPER::Limit(
1320 ENTRYAGGREGATOR => 'AND',
1325 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1329 TABLE2 => 'ObjectCustomFieldValues',
1330 FIELD2 => 'CustomField',
1332 $self->SUPER::Limit(
1333 LEFTJOIN => $TicketCFs,
1334 FIELD => 'ObjectId',
1337 ENTRYAGGREGATOR => 'AND',
1340 $self->SUPER::Limit(
1341 LEFTJOIN => $TicketCFs,
1342 FIELD => 'ObjectType',
1343 VALUE => 'RT::Ticket',
1344 ENTRYAGGREGATOR => 'AND'
1346 $self->SUPER::Limit(
1347 LEFTJOIN => $TicketCFs,
1348 FIELD => 'Disabled',
1351 ENTRYAGGREGATOR => 'AND'
1354 return ($TicketCFs, $CFs);
1357 =head2 _CustomFieldLimit
1359 Limit based on CustomFields
1366 sub _CustomFieldLimit {
1367 my ( $self, $_field, $op, $value, %rest ) = @_;
1369 my $field = $rest{'SUBKEY'} || die "No field specified";
1371 # For our sanity, we can only limit on one queue at a time
1373 my ($queue, $cfid, $cf, $column);
1374 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1375 $cfid = $cf ? $cf->id : 0 ;
1377 # If we're trying to find custom fields that don't match something, we
1378 # want tickets where the custom field has no value at all. Note that
1379 # we explicitly don't include the "IS NULL" case, since we would
1380 # otherwise end up with a redundant clause.
1382 my ($negative_op, $null_op, $inv_op, $range_op)
1383 = $self->ClassifySQLOperation( $op );
1387 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1388 return 'MATCHES' if $op eq '=';
1389 return 'NOT MATCHES' if $op eq '!=';
1393 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1395 my $cfkey = $cfid ? $cfid : "$queue.$field";
1397 if ( $null_op && !$column ) {
1398 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1399 # we can reuse our default joins for this operation
1400 # with column specified we have different situation
1401 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1404 ALIAS => $TicketCFs,
1413 OPERATOR => 'IS NOT',
1416 ENTRYAGGREGATOR => 'AND',
1420 elsif ( !$negative_op || $single_value ) {
1421 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1422 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1429 # if column is defined then deal only with it
1430 # otherwise search in Content and in LargeContent
1433 ALIAS => $TicketCFs,
1435 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1440 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1441 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1443 ALIAS => $TicketCFs,
1452 ALIAS => $TicketCFs,
1456 ENTRYAGGREGATOR => 'OR'
1459 ALIAS => $TicketCFs,
1463 ENTRYAGGREGATOR => 'OR'
1467 ALIAS => $TicketCFs,
1468 FIELD => 'LargeContent',
1469 OPERATOR => $fix_op->($op),
1471 ENTRYAGGREGATOR => 'AND',
1477 ALIAS => $TicketCFs,
1487 ALIAS => $TicketCFs,
1491 ENTRYAGGREGATOR => 'OR'
1494 ALIAS => $TicketCFs,
1498 ENTRYAGGREGATOR => 'OR'
1502 ALIAS => $TicketCFs,
1503 FIELD => 'LargeContent',
1504 OPERATOR => $fix_op->($op),
1506 ENTRYAGGREGATOR => 'AND',
1512 # XXX: if we join via CustomFields table then
1513 # because of order of left joins we get NULLs in
1514 # CF table and then get nulls for those records
1515 # in OCFVs table what result in wrong results
1516 # as decifer method now tries to load a CF then
1517 # we fall into this situation only when there
1518 # are more than one CF with the name in the DB.
1519 # the same thing applies to order by call.
1520 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1521 # we want treat IS NULL as (not applies or has
1526 OPERATOR => 'IS NOT',
1529 ENTRYAGGREGATOR => 'AND',
1535 ALIAS => $TicketCFs,
1536 FIELD => $column || 'Content',
1540 ENTRYAGGREGATOR => 'OR',
1547 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1548 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1551 $op =~ s/!|NOT\s+//i;
1553 # if column is defined then deal only with it
1554 # otherwise search in Content and in LargeContent
1556 $self->SUPER::Limit(
1557 LEFTJOIN => $TicketCFs,
1558 ALIAS => $TicketCFs,
1560 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1565 $self->SUPER::Limit(
1566 LEFTJOIN => $TicketCFs,
1567 ALIAS => $TicketCFs,
1575 ALIAS => $TicketCFs,
1584 sub _HasAttributeLimit {
1585 my ( $self, $field, $op, $value, %rest ) = @_;
1587 my $alias = $self->Join(
1591 TABLE2 => 'Attributes',
1592 FIELD2 => 'ObjectId',
1594 $self->SUPER::Limit(
1596 FIELD => 'ObjectType',
1597 VALUE => 'RT::Ticket',
1598 ENTRYAGGREGATOR => 'AND'
1600 $self->SUPER::Limit(
1605 ENTRYAGGREGATOR => 'AND'
1611 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1618 # End Helper Functions
1620 # End of SQL Stuff -------------------------------------------------
1622 # {{{ Allow sorting on watchers
1624 =head2 OrderByCols ARRAY
1626 A modified version of the OrderBy method which automatically joins where
1627 C<ALIAS> is set to the name of a watcher type.
1638 foreach my $row (@args) {
1639 if ( $row->{ALIAS} ) {
1643 if ( $row->{FIELD} !~ /\./ ) {
1644 my $meta = $self->FIELDS->{ $row->{FIELD} };
1650 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1651 my $alias = $self->Join(
1654 FIELD1 => $row->{'FIELD'},
1658 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1659 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1660 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1662 my $alias = $self->Join(
1665 FIELD1 => $row->{'FIELD'},
1669 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1676 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1677 my $meta = $self->FIELDS->{$field};
1678 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1679 # cache alias as we want to use one alias per watcher type for sorting
1680 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1682 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1683 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1685 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1686 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1687 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1688 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1689 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1690 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1691 # this is described in _CustomFieldLimit
1695 OPERATOR => 'IS NOT',
1698 ENTRYAGGREGATOR => 'AND',
1701 # For those cases where we are doing a join against the
1702 # CF name, and don't have a CFid, use Unique to make sure
1703 # we don't show duplicate tickets. NOTE: I'm pretty sure
1704 # this will stay mixed in for the life of the
1705 # class/package, and not just for the life of the object.
1706 # Potential performance issue.
1707 require DBIx::SearchBuilder::Unique;
1708 DBIx::SearchBuilder::Unique->import;
1710 my $CFvs = $self->Join(
1712 ALIAS1 => $TicketCFs,
1713 FIELD1 => 'CustomField',
1714 TABLE2 => 'CustomFieldValues',
1715 FIELD2 => 'CustomField',
1717 $self->SUPER::Limit(
1721 VALUE => $TicketCFs . ".Content",
1722 ENTRYAGGREGATOR => 'AND'
1725 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1726 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1727 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1728 # PAW logic is "reversed"
1730 if (exists $row->{ORDER} ) {
1731 my $o = $row->{ORDER};
1732 delete $row->{ORDER};
1733 $order = "DESC" if $o =~ /asc/i;
1736 # Ticket.Owner 1 0 X
1737 # Unowned Tickets 0 1 X
1740 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1741 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1742 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1747 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1754 FUNCTION => "Owner=$uid",
1760 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1766 return $self->SUPER::OrderByCols(@res);
1771 # {{{ Limit the result set based on content
1777 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1778 Generally best called from LimitFoo methods
1788 DESCRIPTION => undef,
1791 $args{'DESCRIPTION'} = $self->loc(
1792 "[_1] [_2] [_3]", $args{'FIELD'},
1793 $args{'OPERATOR'}, $args{'VALUE'}
1795 if ( !defined $args{'DESCRIPTION'} );
1797 my $index = $self->_NextIndex;
1799 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1801 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1803 $self->{'RecalcTicketLimits'} = 1;
1805 # If we're looking at the effective id, we don't want to append the other clause
1806 # which limits us to tickets where id = effective id
1807 if ( $args{'FIELD'} eq 'EffectiveId'
1808 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1810 $self->{'looking_at_effective_id'} = 1;
1813 if ( $args{'FIELD'} eq 'Type'
1814 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1816 $self->{'looking_at_type'} = 1;
1826 Returns a frozen string suitable for handing back to ThawLimits.
1830 sub _FreezeThawKeys {
1831 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1835 # {{{ sub FreezeLimits
1840 require MIME::Base64;
1841 MIME::Base64::base64_encode(
1842 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1849 Take a frozen Limits string generated by FreezeLimits and make this tickets
1850 object have that set of limits.
1854 # {{{ sub ThawLimits
1860 #if we don't have $in, get outta here.
1861 return undef unless ($in);
1863 $self->{'RecalcTicketLimits'} = 1;
1866 require MIME::Base64;
1868 #We don't need to die if the thaw fails.
1869 @{$self}{ $self->_FreezeThawKeys }
1870 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1872 $RT::Logger->error($@) if $@;
1878 # {{{ Limit by enum or foreign key
1880 # {{{ sub LimitQueue
1884 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1885 OPERATOR is one of = or !=. (It defaults to =).
1886 VALUE is a queue id or Name.
1899 #TODO VALUE should also take queue objects
1900 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1901 my $queue = new RT::Queue( $self->CurrentUser );
1902 $queue->Load( $args{'VALUE'} );
1903 $args{'VALUE'} = $queue->Id;
1906 # What if they pass in an Id? Check for isNum() and convert to
1909 #TODO check for a valid queue here
1913 VALUE => $args{'VALUE'},
1914 OPERATOR => $args{'OPERATOR'},
1915 DESCRIPTION => join(
1916 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1924 # {{{ sub LimitStatus
1928 Takes a paramhash with the fields OPERATOR and VALUE.
1929 OPERATOR is one of = or !=.
1932 RT adds Status != 'deleted' until object has
1933 allow_deleted_search internal property set.
1934 $tickets->{'allow_deleted_search'} = 1;
1935 $tickets->LimitStatus( VALUE => 'deleted' );
1947 VALUE => $args{'VALUE'},
1948 OPERATOR => $args{'OPERATOR'},
1949 DESCRIPTION => join( ' ',
1950 $self->loc('Status'), $args{'OPERATOR'},
1951 $self->loc( $args{'VALUE'} ) ),
1957 # {{{ sub IgnoreType
1961 If called, this search will not automatically limit the set of results found
1962 to tickets of type "Ticket". Tickets of other types, such as "project" and
1963 "approval" will be found.
1970 # Instead of faking a Limit that later gets ignored, fake up the
1971 # fact that we're already looking at type, so that the check in
1972 # Tickets_Overlay_SQL/FromSQL goes down the right branch
1974 # $self->LimitType(VALUE => '__any');
1975 $self->{looking_at_type} = 1;
1984 Takes a paramhash with the fields OPERATOR and VALUE.
1985 OPERATOR is one of = or !=, it defaults to "=".
1986 VALUE is a string to search for in the type of the ticket.
2001 VALUE => $args{'VALUE'},
2002 OPERATOR => $args{'OPERATOR'},
2003 DESCRIPTION => join( ' ',
2004 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2012 # {{{ Limit by string field
2014 # {{{ sub LimitSubject
2018 Takes a paramhash with the fields OPERATOR and VALUE.
2019 OPERATOR is one of = or !=.
2020 VALUE is a string to search for in the subject of the ticket.
2029 VALUE => $args{'VALUE'},
2030 OPERATOR => $args{'OPERATOR'},
2031 DESCRIPTION => join( ' ',
2032 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2040 # {{{ Limit based on ticket numerical attributes
2041 # Things that can be > < = !=
2047 Takes a paramhash with the fields OPERATOR and VALUE.
2048 OPERATOR is one of =, >, < or !=.
2049 VALUE is a ticket Id to search for
2062 VALUE => $args{'VALUE'},
2063 OPERATOR => $args{'OPERATOR'},
2065 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2071 # {{{ sub LimitPriority
2073 =head2 LimitPriority
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 priority against
2085 FIELD => 'Priority',
2086 VALUE => $args{'VALUE'},
2087 OPERATOR => $args{'OPERATOR'},
2088 DESCRIPTION => join( ' ',
2089 $self->loc('Priority'),
2090 $args{'OPERATOR'}, $args{'VALUE'}, ),
2096 # {{{ sub LimitInitialPriority
2098 =head2 LimitInitialPriority
2100 Takes a paramhash with the fields OPERATOR and VALUE.
2101 OPERATOR is one of =, >, < or !=.
2102 VALUE is a value to match the ticket\'s initial priority against
2107 sub LimitInitialPriority {
2111 FIELD => 'InitialPriority',
2112 VALUE => $args{'VALUE'},
2113 OPERATOR => $args{'OPERATOR'},
2114 DESCRIPTION => join( ' ',
2115 $self->loc('Initial Priority'), $args{'OPERATOR'},
2122 # {{{ sub LimitFinalPriority
2124 =head2 LimitFinalPriority
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 final priority against
2132 sub LimitFinalPriority {
2136 FIELD => 'FinalPriority',
2137 VALUE => $args{'VALUE'},
2138 OPERATOR => $args{'OPERATOR'},
2139 DESCRIPTION => join( ' ',
2140 $self->loc('Final Priority'), $args{'OPERATOR'},
2147 # {{{ sub LimitTimeWorked
2149 =head2 LimitTimeWorked
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 TimeWorked attribute
2157 sub LimitTimeWorked {
2161 FIELD => 'TimeWorked',
2162 VALUE => $args{'VALUE'},
2163 OPERATOR => $args{'OPERATOR'},
2164 DESCRIPTION => join( ' ',
2165 $self->loc('Time Worked'),
2166 $args{'OPERATOR'}, $args{'VALUE'}, ),
2172 # {{{ sub LimitTimeLeft
2174 =head2 LimitTimeLeft
2176 Takes a paramhash with the fields OPERATOR and VALUE.
2177 OPERATOR is one of =, >, < or !=.
2178 VALUE is a value to match the ticket's TimeLeft attribute
2186 FIELD => 'TimeLeft',
2187 VALUE => $args{'VALUE'},
2188 OPERATOR => $args{'OPERATOR'},
2189 DESCRIPTION => join( ' ',
2190 $self->loc('Time Left'),
2191 $args{'OPERATOR'}, $args{'VALUE'}, ),
2199 # {{{ Limiting based on attachment attributes
2201 # {{{ sub LimitContent
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
2216 VALUE => $args{'VALUE'},
2217 OPERATOR => $args{'OPERATOR'},
2218 DESCRIPTION => join( ' ',
2219 $self->loc('Ticket content'), $args{'OPERATOR'},
2226 # {{{ sub LimitFilename
2228 =head2 LimitFilename
2230 Takes a paramhash with the fields OPERATOR and VALUE.
2231 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2232 VALUE is a string to search for in the body of the ticket
2240 FIELD => 'Filename',
2241 VALUE => $args{'VALUE'},
2242 OPERATOR => $args{'OPERATOR'},
2243 DESCRIPTION => join( ' ',
2244 $self->loc('Attachment filename'), $args{'OPERATOR'},
2250 # {{{ sub LimitContentType
2252 =head2 LimitContentType
2254 Takes a paramhash with the fields OPERATOR and VALUE.
2255 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2256 VALUE is a content type to search ticket attachments for
2260 sub LimitContentType {
2264 FIELD => 'ContentType',
2265 VALUE => $args{'VALUE'},
2266 OPERATOR => $args{'OPERATOR'},
2267 DESCRIPTION => join( ' ',
2268 $self->loc('Ticket content type'), $args{'OPERATOR'},
2277 # {{{ Limiting based on people
2279 # {{{ sub LimitOwner
2283 Takes a paramhash with the fields OPERATOR and VALUE.
2284 OPERATOR is one of = or !=.
2296 my $owner = new RT::User( $self->CurrentUser );
2297 $owner->Load( $args{'VALUE'} );
2299 # FIXME: check for a valid $owner
2302 VALUE => $args{'VALUE'},
2303 OPERATOR => $args{'OPERATOR'},
2304 DESCRIPTION => join( ' ',
2305 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2312 # {{{ Limiting watchers
2314 # {{{ sub LimitWatcher
2318 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2319 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2320 VALUE is a value to match the ticket\'s watcher email addresses against
2321 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2335 #build us up a description
2336 my ( $watcher_type, $desc );
2337 if ( $args{'TYPE'} ) {
2338 $watcher_type = $args{'TYPE'};
2341 $watcher_type = "Watcher";
2345 FIELD => $watcher_type,
2346 VALUE => $args{'VALUE'},
2347 OPERATOR => $args{'OPERATOR'},
2348 TYPE => $args{'TYPE'},
2349 DESCRIPTION => join( ' ',
2350 $self->loc($watcher_type),
2351 $args{'OPERATOR'}, $args{'VALUE'}, ),
2361 # {{{ Limiting based on links
2365 =head2 LimitLinkedTo
2367 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2368 TYPE limits the sort of link we want to search on
2370 TYPE = { RefersTo, MemberOf, DependsOn }
2372 TARGET is the id or URI of the TARGET of the link
2386 FIELD => 'LinkedTo',
2388 TARGET => $args{'TARGET'},
2389 TYPE => $args{'TYPE'},
2390 DESCRIPTION => $self->loc(
2391 "Tickets [_1] by [_2]",
2392 $self->loc( $args{'TYPE'} ),
2395 OPERATOR => $args{'OPERATOR'},
2401 # {{{ LimitLinkedFrom
2403 =head2 LimitLinkedFrom
2405 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2406 TYPE limits the sort of link we want to search on
2409 BASE is the id or URI of the BASE of the link
2413 sub LimitLinkedFrom {
2422 # translate RT2 From/To naming to RT3 TicketSQL naming
2423 my %fromToMap = qw(DependsOn DependentOn
2425 RefersTo ReferredToBy);
2427 my $type = $args{'TYPE'};
2428 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2431 FIELD => 'LinkedTo',
2433 BASE => $args{'BASE'},
2435 DESCRIPTION => $self->loc(
2436 "Tickets [_1] [_2]",
2437 $self->loc( $args{'TYPE'} ),
2440 OPERATOR => $args{'OPERATOR'},
2449 my $ticket_id = shift;
2450 return $self->LimitLinkedTo(
2452 TARGET => $ticket_id,
2459 # {{{ LimitHasMember
2460 sub LimitHasMember {
2462 my $ticket_id = shift;
2463 return $self->LimitLinkedFrom(
2465 BASE => "$ticket_id",
2466 TYPE => 'HasMember',
2473 # {{{ LimitDependsOn
2475 sub LimitDependsOn {
2477 my $ticket_id = shift;
2478 return $self->LimitLinkedTo(
2480 TARGET => $ticket_id,
2481 TYPE => 'DependsOn',
2488 # {{{ LimitDependedOnBy
2490 sub LimitDependedOnBy {
2492 my $ticket_id = shift;
2493 return $self->LimitLinkedFrom(
2496 TYPE => 'DependentOn',
2507 my $ticket_id = shift;
2508 return $self->LimitLinkedTo(
2510 TARGET => $ticket_id,
2518 # {{{ LimitReferredToBy
2520 sub LimitReferredToBy {
2522 my $ticket_id = shift;
2523 return $self->LimitLinkedFrom(
2526 TYPE => 'ReferredToBy',
2534 # {{{ limit based on ticket date attribtes
2538 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2540 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2542 OPERATOR is one of > or <
2543 VALUE is a date and time in ISO format in GMT
2544 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2546 There are also helper functions of the form LimitFIELD that eliminate
2547 the need to pass in a FIELD argument.
2561 #Set the description if we didn't get handed it above
2562 unless ( $args{'DESCRIPTION'} ) {
2563 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2564 . $args{'OPERATOR'} . " "
2565 . $args{'VALUE'} . " GMT";
2568 $self->Limit(%args);
2576 $self->LimitDate( FIELD => 'Created', @_ );
2581 $self->LimitDate( FIELD => 'Due', @_ );
2587 $self->LimitDate( FIELD => 'Starts', @_ );
2593 $self->LimitDate( FIELD => 'Started', @_ );
2598 $self->LimitDate( FIELD => 'Resolved', @_ );
2603 $self->LimitDate( FIELD => 'Told', @_ );
2606 sub LimitLastUpdated {
2608 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2612 # {{{ sub LimitTransactionDate
2614 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2616 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2618 OPERATOR is one of > or <
2619 VALUE is a date and time in ISO format in GMT
2624 sub LimitTransactionDate {
2627 FIELD => 'TransactionDate',
2634 # <20021217042756.GK28744@pallas.fsck.com>
2635 # "Kill It" - Jesse.
2637 #Set the description if we didn't get handed it above
2638 unless ( $args{'DESCRIPTION'} ) {
2639 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2640 . $args{'OPERATOR'} . " "
2641 . $args{'VALUE'} . " GMT";
2644 $self->Limit(%args);
2652 # {{{ Limit based on custom fields
2653 # {{{ sub LimitCustomField
2655 =head2 LimitCustomField
2657 Takes a paramhash of key/value pairs with the following keys:
2661 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2663 =item OPERATOR - The usual Limit operators
2665 =item VALUE - The value to compare against
2671 sub LimitCustomField {
2675 CUSTOMFIELD => undef,
2677 DESCRIPTION => undef,
2678 FIELD => 'CustomFieldValue',
2683 my $CF = RT::CustomField->new( $self->CurrentUser );
2684 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2685 $CF->Load( $args{CUSTOMFIELD} );
2688 $CF->LoadByNameAndQueue(
2689 Name => $args{CUSTOMFIELD},
2690 Queue => $args{QUEUE}
2692 $args{CUSTOMFIELD} = $CF->Id;
2695 #If we are looking to compare with a null value.
2696 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2697 $args{'DESCRIPTION'}
2698 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2700 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2701 $args{'DESCRIPTION'}
2702 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2705 # if we're not looking to compare with a null value
2707 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2708 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2711 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2712 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2713 $QueueObj->Load( $args{'QUEUE'} );
2714 $args{'QUEUE'} = $QueueObj->Id;
2716 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2719 @rest = ( ENTRYAGGREGATOR => 'AND' )
2720 if ( $CF->Type eq 'SelectMultiple' );
2723 VALUE => $args{VALUE},
2725 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2726 .".{" . $CF->Name . "}",
2727 OPERATOR => $args{OPERATOR},
2732 $self->{'RecalcTicketLimits'} = 1;
2738 # {{{ sub _NextIndex
2742 Keep track of the counter for the array of restrictions
2748 return ( $self->{'restriction_index'}++ );
2755 # {{{ Core bits to make this a DBIx::SearchBuilder object
2760 $self->{'table'} = "Tickets";
2761 $self->{'RecalcTicketLimits'} = 1;
2762 $self->{'looking_at_effective_id'} = 0;
2763 $self->{'looking_at_type'} = 0;
2764 $self->{'restriction_index'} = 1;
2765 $self->{'primary_key'} = "id";
2766 delete $self->{'items_array'};
2767 delete $self->{'item_map'};
2768 delete $self->{'columns_to_display'};
2769 $self->SUPER::_Init(@_);
2780 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2781 return ( $self->SUPER::Count() );
2789 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2790 return ( $self->SUPER::CountAll() );
2795 # {{{ sub ItemsArrayRef
2797 =head2 ItemsArrayRef
2799 Returns a reference to the set of all items found in this search
2806 return $self->{'items_array'} if $self->{'items_array'};
2808 my $placeholder = $self->_ItemsCounter;
2809 $self->GotoFirstItem();
2810 while ( my $item = $self->Next ) {
2811 push( @{ $self->{'items_array'} }, $item );
2813 $self->GotoItem($placeholder);
2814 $self->{'items_array'}
2815 = $self->ItemsOrderBy( $self->{'items_array'} );
2817 return $self->{'items_array'};
2820 sub ItemsArrayRefWindow {
2824 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2826 $self->RowsPerPage( $window );
2828 $self->GotoFirstItem;
2831 while ( my $item = $self->Next ) {
2835 $self->RowsPerPage( $old[1] );
2836 $self->FirstRow( $old[2] );
2837 $self->GotoItem( $old[0] );
2848 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2850 my $Ticket = $self->SUPER::Next;
2851 return $Ticket unless $Ticket;
2853 if ( $Ticket->__Value('Status') eq 'deleted'
2854 && !$self->{'allow_deleted_search'} )
2858 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2859 # if we found a ticket with this option enabled then
2860 # all tickets we found are ACLed, cache this fact
2861 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2862 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2865 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2870 # If the user doesn't have the right to show this ticket
2877 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2878 return $self->SUPER::_DoSearch( @_ );
2883 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2884 return $self->SUPER::_DoCount( @_ );
2890 my $cache_key = 'RolesHasRight;:;ShowTicket';
2892 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
2896 my $ACL = RT::ACL->new( $RT::SystemUser );
2897 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2898 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
2899 my $principal_alias = $ACL->Join(
2901 FIELD1 => 'PrincipalId',
2902 TABLE2 => 'Principals',
2905 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2908 while ( my $ACE = $ACL->Next ) {
2909 my $role = $ACE->PrincipalType;
2910 my $type = $ACE->ObjectType;
2911 if ( $type eq 'RT::System' ) {
2914 elsif ( $type eq 'RT::Queue' ) {
2915 next if $res{ $role } && !ref $res{ $role };
2916 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
2919 $RT::Logger->error('ShowTicket right is granted on unsupported object');
2922 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
2926 sub _DirectlyCanSeeIn {
2928 my $id = $self->CurrentUser->id;
2930 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
2931 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
2935 my $ACL = RT::ACL->new( $RT::SystemUser );
2936 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2937 my $principal_alias = $ACL->Join(
2939 FIELD1 => 'PrincipalId',
2940 TABLE2 => 'Principals',
2943 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2944 my $cgm_alias = $ACL->Join(
2946 FIELD1 => 'PrincipalId',
2947 TABLE2 => 'CachedGroupMembers',
2948 FIELD2 => 'GroupId',
2950 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
2951 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
2954 while ( my $ACE = $ACL->Next ) {
2955 my $type = $ACE->ObjectType;
2956 if ( $type eq 'RT::System' ) {
2957 # If user is direct member of a group that has the right
2958 # on the system then he can see any ticket
2959 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
2962 elsif ( $type eq 'RT::Queue' ) {
2963 push @res, $ACE->ObjectId;
2966 $RT::Logger->error('ShowTicket right is granted on unsupported object');
2969 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
2973 sub CurrentUserCanSee {
2975 return if $self->{'_sql_current_user_can_see_applied'};
2977 return $self->{'_sql_current_user_can_see_applied'} = 1
2978 if $self->CurrentUser->UserObj->HasRight(
2979 Right => 'SuperUser', Object => $RT::System
2982 my $id = $self->CurrentUser->id;
2984 # directly can see in all queues then we have nothing to do
2985 my @direct_queues = $self->_DirectlyCanSeeIn;
2986 return $self->{'_sql_current_user_can_see_applied'} = 1
2987 if @direct_queues && $direct_queues[0] == -1;
2989 my %roles = $self->_RolesCanSee;
2991 my %skip = map { $_ => 1 } @direct_queues;
2992 foreach my $role ( keys %roles ) {
2993 next unless ref $roles{ $role };
2995 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
2997 $roles{ $role } = \@queues;
2999 delete $roles{ $role };
3004 # there is no global watchers, only queues and tickes, if at
3005 # some point we will add global roles then it's gonna blow
3006 # the idea here is that if the right is set globaly for a role
3007 # and user plays this role for a queue directly not a ticket
3008 # then we have to check in advance
3009 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3011 my $groups = RT::Groups->new( $RT::SystemUser );
3012 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3014 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3016 my $principal_alias = $groups->Join(
3019 TABLE2 => 'Principals',
3022 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3023 my $cgm_alias = $groups->Join(
3026 TABLE2 => 'CachedGroupMembers',
3027 FIELD2 => 'GroupId',
3029 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3030 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3031 while ( my $group = $groups->Next ) {
3032 push @direct_queues, $group->Instance;
3036 unless ( @direct_queues || keys %roles ) {
3037 $self->SUPER::Limit(
3042 ENTRYAGGREGATOR => 'AND',
3044 return $self->{'_sql_current_user_can_see_applied'} = 1;
3048 my $join_roles = keys %roles;
3049 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3050 my ($role_group_alias, $cgm_alias);
3051 if ( $join_roles ) {
3052 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3053 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3054 $self->SUPER::Limit(
3055 LEFTJOIN => $cgm_alias,
3056 FIELD => 'MemberId',
3061 my $limit_queues = sub {
3065 return unless @queues;
3066 if ( @queues == 1 ) {
3067 $self->SUPER::Limit(
3072 ENTRYAGGREGATOR => $ea,
3075 $self->SUPER::_OpenParen('ACL');
3076 foreach my $q ( @queues ) {
3077 $self->SUPER::Limit(
3082 ENTRYAGGREGATOR => $ea,
3086 $self->SUPER::_CloseParen('ACL');
3091 $self->SUPER::_OpenParen('ACL');
3093 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3094 while ( my ($role, $queues) = each %roles ) {
3095 $self->SUPER::_OpenParen('ACL');
3096 if ( $role eq 'Owner' ) {
3097 $self->SUPER::Limit(
3101 ENTRYAGGREGATOR => $ea,
3105 $self->SUPER::Limit(
3107 ALIAS => $cgm_alias,
3108 FIELD => 'MemberId',
3109 OPERATOR => 'IS NOT',
3112 ENTRYAGGREGATOR => $ea,
3114 $self->SUPER::Limit(
3116 ALIAS => $role_group_alias,
3119 ENTRYAGGREGATOR => 'AND',
3122 $limit_queues->( 'AND', @$queues ) if ref $queues;
3123 $ea = 'OR' if $ea eq 'AND';
3124 $self->SUPER::_CloseParen('ACL');
3126 $self->SUPER::_CloseParen('ACL');
3128 return $self->{'_sql_current_user_can_see_applied'} = 1;
3135 # {{{ Deal with storing and restoring restrictions
3137 # {{{ sub LoadRestrictions
3139 =head2 LoadRestrictions
3141 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3142 TODO It is not yet implemented
3148 # {{{ sub DescribeRestrictions
3150 =head2 DescribeRestrictions
3153 Returns a hash keyed by restriction id.
3154 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3155 is a description of the purpose of that TicketRestriction
3159 sub DescribeRestrictions {
3164 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3165 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3172 # {{{ sub RestrictionValues
3174 =head2 RestrictionValues FIELD
3176 Takes a restriction field and returns a list of values this field is restricted
3181 sub RestrictionValues {
3184 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3185 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3186 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3188 keys %{ $self->{'TicketRestrictions'} };
3193 # {{{ sub ClearRestrictions
3195 =head2 ClearRestrictions
3197 Removes all restrictions irretrievably
3201 sub ClearRestrictions {
3203 delete $self->{'TicketRestrictions'};
3204 $self->{'looking_at_effective_id'} = 0;
3205 $self->{'looking_at_type'} = 0;
3206 $self->{'RecalcTicketLimits'} = 1;
3211 # {{{ sub DeleteRestriction
3213 =head2 DeleteRestriction
3215 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3216 Removes that restriction from the session's limits.
3220 sub DeleteRestriction {
3223 delete $self->{'TicketRestrictions'}{$row};
3225 $self->{'RecalcTicketLimits'} = 1;
3227 #make the underlying easysearch object forget all its preconceptions
3232 # {{{ sub _RestrictionsToClauses
3234 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3236 sub _RestrictionsToClauses {
3240 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3241 my $restriction = $self->{'TicketRestrictions'}{$row};
3243 # We need to reimplement the subclause aggregation that SearchBuilder does.
3244 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3245 # Then SB AND's the different Subclauses together.
3247 # So, we want to group things into Subclauses, convert them to
3248 # SQL, and then join them with the appropriate DefaultEA.
3249 # Then join each subclause group with AND.
3251 my $field = $restriction->{'FIELD'};
3252 my $realfield = $field; # CustomFields fake up a fieldname, so
3253 # we need to figure that out
3256 # Rewrite LinkedTo meta field to the real field
3257 if ( $field =~ /LinkedTo/ ) {
3258 $realfield = $field = $restriction->{'TYPE'};
3262 # Handle subkey fields with a different real field
3263 if ( $field =~ /^(\w+)\./ ) {
3267 die "I don't know about $field yet"
3268 unless ( exists $FIELD_METADATA{$realfield}
3269 or $restriction->{CUSTOMFIELD} );
3271 my $type = $FIELD_METADATA{$realfield}->[0];
3272 my $op = $restriction->{'OPERATOR'};
3276 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3279 # this performs the moral equivalent of defined or/dor/C<//>,
3280 # without the short circuiting.You need to use a 'defined or'
3281 # type thing instead of just checking for truth values, because
3282 # VALUE could be 0.(i.e. "false")
3284 # You could also use this, but I find it less aesthetic:
3285 # (although it does short circuit)
3286 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3287 # defined $restriction->{'TICKET'} ?
3288 # $restriction->{TICKET} :
3289 # defined $restriction->{'BASE'} ?
3290 # $restriction->{BASE} :
3291 # defined $restriction->{'TARGET'} ?
3292 # $restriction->{TARGET} )
3294 my $ea = $restriction->{ENTRYAGGREGATOR}
3295 || $DefaultEA{$type}
3298 die "Invalid operator $op for $field ($type)"
3299 unless exists $ea->{$op};
3303 # Each CustomField should be put into a different Clause so they
3304 # are ANDed together.
3305 if ( $restriction->{CUSTOMFIELD} ) {
3306 $realfield = $field;
3309 exists $clause{$realfield} or $clause{$realfield} = [];
3312 $field =~ s!(['"])!\\$1!g;
3313 $value =~ s!(['"])!\\$1!g;
3314 my $data = [ $ea, $type, $field, $op, $value ];
3316 # here is where we store extra data, say if it's a keyword or
3317 # something. (I.e. "TYPE SPECIFIC STUFF")
3319 push @{ $clause{$realfield} }, $data;
3326 # {{{ sub _ProcessRestrictions
3328 =head2 _ProcessRestrictions PARAMHASH
3330 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3331 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3335 sub _ProcessRestrictions {
3338 #Blow away ticket aliases since we'll need to regenerate them for
3340 delete $self->{'TicketAliases'};
3341 delete $self->{'items_array'};
3342 delete $self->{'item_map'};
3343 delete $self->{'raw_rows'};
3344 delete $self->{'rows'};
3345 delete $self->{'count_all'};
3347 my $sql = $self->Query; # Violating the _SQL namespace
3348 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3350 # "Restrictions to Clauses Branch\n";
3351 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3353 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3357 $sql = $self->ClausesToSQL($clauseRef);
3358 $self->FromSQL($sql) if $sql;
3362 $self->{'RecalcTicketLimits'} = 0;
3366 =head2 _BuildItemMap
3368 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3369 display search nav quickly.
3376 my $window = RT->Config->Get('TicketsItemMapSize');
3378 $self->{'item_map'} = {};
3380 my $items = $self->ItemsArrayRefWindow( $window );
3381 return unless $items && @$items;
3384 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3385 for ( my $i = 0; $i < @$items; $i++ ) {
3386 my $item = $items->[$i];
3387 my $id = $item->EffectiveId;
3388 $self->{'item_map'}{$id}{'defined'} = 1;
3389 $self->{'item_map'}{$id}{'prev'} = $prev;
3390 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3394 $self->{'item_map'}{'last'} = $prev
3395 if !$window || @$items < $window;
3400 Returns an a map of all items found by this search. The map is a hash
3404 first => <first ticket id found>,
3405 last => <last ticket id found or undef>,
3408 prev => <the ticket id found before>,
3409 next => <the ticket id found after>,
3421 $self->_BuildItemMap unless $self->{'item_map'};
3422 return $self->{'item_map'};
3430 =head2 PrepForSerialization
3432 You don't want to serialize a big tickets object, as
3433 the {items} hash will be instantly invalid _and_ eat
3438 sub PrepForSerialization {
3440 delete $self->{'items'};
3441 delete $self->{'items_array'};
3442 $self->RedoSearch();
3447 RT::Tickets supports several flags which alter search behavior:
3450 allow_deleted_search (Otherwise never show deleted tickets in search results)
3451 looking_at_type (otherwise limit to type=ticket)
3453 These flags are set by calling
3455 $tickets->{'flagname'} = 1;
3457 BUG: There should be an API for this