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 DateCustomFieldValue => [ 'DATECUSTOMFIELD', ],
140 CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
141 CF => [ 'CUSTOMFIELD', ], #loc_left_pair
142 Updated => [ 'TRANSDATE', ], #loc_left_pair
143 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
144 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
145 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
146 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
147 HasAttribute => [ 'HASATTRIBUTE', 1 ],
148 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
151 # Mapping of Field Type to Function
153 ENUM => \&_EnumLimit,
156 LINK => \&_LinkLimit,
157 DATE => \&_DateLimit,
158 STRING => \&_StringLimit,
159 TRANSFIELD => \&_TransLimit,
160 TRANSDATE => \&_TransDateLimit,
161 WATCHERFIELD => \&_WatcherLimit,
162 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
163 CUSTOMFIELD => \&_CustomFieldLimit,
164 DATECUSTOMFIELD => \&_DateCustomFieldLimit,
165 HASATTRIBUTE => \&_HasAttributeLimit,
167 our %can_bundle = ();# WATCHERFIELD => "yes", );
169 # Default EntryAggregator per type
170 # if you specify OP, you must specify all valid OPs
211 # Helper functions for passing the above lexically scoped tables above
212 # into Tickets_Overlay_SQL.
213 sub FIELDS { return \%FIELD_METADATA }
214 sub dispatch { return \%dispatch }
215 sub can_bundle { return \%can_bundle }
217 # Bring in the clowns.
218 require RT::Tickets_Overlay_SQL;
222 our @SORTFIELDS = qw(id Status
224 Owner Created Due Starts Started
226 Resolved LastUpdated Priority TimeWorked TimeLeft);
230 Returns the list of fields that lists of tickets can easily be sorted by
236 return (@SORTFIELDS);
241 # BEGIN SQL STUFF *********************************
246 $self->SUPER::CleanSlate( @_ );
247 delete $self->{$_} foreach qw(
249 _sql_group_members_aliases
250 _sql_object_cfv_alias
251 _sql_role_group_aliases
254 _sql_u_watchers_alias_for_sort
255 _sql_u_watchers_aliases
256 _sql_current_user_can_see_applied
260 =head1 Limit Helper Routines
262 These routines are the targets of a dispatch table depending on the
263 type of field. They all share the same signature:
265 my ($self,$field,$op,$value,@rest) = @_;
267 The values in @rest should be suitable for passing directly to
268 DBIx::SearchBuilder::Limit.
270 Essentially they are an expanded/broken out (and much simplified)
271 version of what ProcessRestrictions used to do. They're also much
272 more clearly delineated by the TYPE of field being processed.
281 my ( $sb, $field, $op, $value, @rest ) = @_;
283 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
285 die "Invalid operator $op for __Bookmarked__ search on $field"
286 unless $op =~ /^(=|!=)$/;
289 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
290 $tmp = $tmp->Content if $tmp;
295 return $sb->_SQLLimit(
302 # as bookmarked tickets can be merged we have to use a join
303 # but it should be pretty lightweight
304 my $tickets_alias = $sb->Join(
309 FIELD2 => 'EffectiveId',
313 my $ea = $op eq '='? 'OR': 'AND';
314 foreach my $id ( sort @bookmarks ) {
316 ALIAS => $tickets_alias,
320 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
328 Handle Fields which are limited to certain values, and potentially
329 need to be looked up from another class.
331 This subroutine actually handles two different kinds of fields. For
332 some the user is responsible for limiting the values. (i.e. Status,
335 For others, the value specified by the user will be looked by via
339 name of class to lookup in (Optional)
344 my ( $sb, $field, $op, $value, @rest ) = @_;
346 # SQL::Statement changes != to <>. (Can we remove this now?)
347 $op = "!=" if $op eq "<>";
349 die "Invalid Operation: $op for $field"
353 my $meta = $FIELD_METADATA{$field};
354 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
355 my $class = "RT::" . $meta->[1];
356 my $o = $class->new( $sb->CurrentUser );
370 Handle fields where the values are limited to integers. (For example,
371 Priority, TimeWorked.)
379 my ( $sb, $field, $op, $value, @rest ) = @_;
381 die "Invalid Operator $op for $field"
382 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
394 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
397 1: Direction (From, To)
398 2: Link Type (MemberOf, DependsOn, RefersTo)
403 my ( $sb, $field, $op, $value, @rest ) = @_;
405 my $meta = $FIELD_METADATA{$field};
406 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
409 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
413 $is_null = 1 if !$value || $value =~ /^null$/io;
415 my $direction = $meta->[1] || '';
416 my ($matchfield, $linkfield) = ('', '');
417 if ( $direction eq 'To' ) {
418 ($matchfield, $linkfield) = ("Target", "Base");
420 elsif ( $direction eq 'From' ) {
421 ($matchfield, $linkfield) = ("Base", "Target");
423 elsif ( $direction ) {
424 die "Invalid link direction '$direction' for $field\n";
427 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
429 'LinkedFrom', $op, $value, @rest,
430 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
438 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
440 elsif ( $value =~ /\D/ ) {
443 $matchfield = "Local$matchfield" if $is_local;
445 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
446 # SELECT main.* FROM Tickets main
447 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
448 # AND(main.id = Links_1.LocalTarget))
449 # WHERE Links_1.LocalBase IS NULL;
452 my $linkalias = $sb->Join(
457 FIELD2 => 'Local' . $linkfield
460 LEFTJOIN => $linkalias,
468 FIELD => $matchfield,
475 my $linkalias = $sb->Join(
480 FIELD2 => 'Local' . $linkfield
483 LEFTJOIN => $linkalias,
489 LEFTJOIN => $linkalias,
490 FIELD => $matchfield,
497 FIELD => $matchfield,
498 OPERATOR => $is_negative? 'IS': 'IS NOT',
507 Handle date fields. (Created, LastTold..)
510 1: type of link. (Probably not necessary.)
515 my ( $sb, $field, $op, $value, @rest ) = @_;
517 die "Invalid Date Op: $op"
518 unless $op =~ /^(=|>|<|>=|<=)$/;
520 my $meta = $FIELD_METADATA{$field};
521 die "Incorrect Meta Data for $field"
522 unless ( defined $meta->[1] );
524 my $date = RT::Date->new( $sb->CurrentUser );
525 $date->Set( Format => 'unknown', Value => $value );
529 # if we're specifying =, that means we want everything on a
530 # particular single day. in the database, we need to check for >
531 # and < the edges of that day.
533 $date->SetToMidnight( Timezone => 'server' );
534 my $daystart = $date->ISO;
536 my $dayend = $date->ISO;
552 ENTRYAGGREGATOR => 'AND',
570 Handle simple fields which are just strings. (Subject,Type)
578 my ( $sb, $field, $op, $value, @rest ) = @_;
582 # =, !=, LIKE, NOT LIKE
583 if ( (!defined $value || !length $value)
584 && lc($op) ne 'is' && lc($op) ne 'is not'
585 && RT->Config->Get('DatabaseType') eq 'Oracle'
587 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
588 $op = $negative? 'IS NOT': 'IS';
601 =head2 _TransDateLimit
603 Handle fields limiting based on Transaction Date.
605 The inpupt value must be in a format parseable by Time::ParseDate
612 # This routine should really be factored into translimit.
613 sub _TransDateLimit {
614 my ( $sb, $field, $op, $value, @rest ) = @_;
616 # See the comments for TransLimit, they apply here too
618 unless ( $sb->{_sql_transalias} ) {
619 $sb->{_sql_transalias} = $sb->Join(
622 TABLE2 => 'Transactions',
623 FIELD2 => 'ObjectId',
626 ALIAS => $sb->{_sql_transalias},
627 FIELD => 'ObjectType',
628 VALUE => 'RT::Ticket',
629 ENTRYAGGREGATOR => 'AND',
633 my $date = RT::Date->new( $sb->CurrentUser );
634 $date->Set( Format => 'unknown', Value => $value );
639 # if we're specifying =, that means we want everything on a
640 # particular single day. in the database, we need to check for >
641 # and < the edges of that day.
643 $date->SetToMidnight( Timezone => 'server' );
644 my $daystart = $date->ISO;
646 my $dayend = $date->ISO;
649 ALIAS => $sb->{_sql_transalias},
657 ALIAS => $sb->{_sql_transalias},
663 ENTRYAGGREGATOR => 'AND',
668 # not searching for a single day
671 #Search for the right field
673 ALIAS => $sb->{_sql_transalias},
687 Limit based on the Content of a transaction or the ContentType.
696 # Content, ContentType, Filename
698 # If only this was this simple. We've got to do something
701 #Basically, we want to make sure that the limits apply to
702 #the same attachment, rather than just another attachment
703 #for the same ticket, no matter how many clauses we lump
704 #on. We put them in TicketAliases so that they get nuked
705 #when we redo the join.
707 # In the SQL, we might have
708 # (( Content = foo ) or ( Content = bar AND Content = baz ))
709 # The AND group should share the same Alias.
711 # Actually, maybe it doesn't matter. We use the same alias and it
712 # works itself out? (er.. different.)
714 # Steal more from _ProcessRestrictions
716 # FIXME: Maybe look at the previous FooLimit call, and if it was a
717 # TransLimit and EntryAggregator == AND, reuse the Aliases?
719 # Or better - store the aliases on a per subclause basis - since
720 # those are going to be the things we want to relate to each other,
723 # maybe we should not allow certain kinds of aggregation of these
724 # clauses and do a psuedo regex instead? - the problem is getting
725 # them all into the same subclause when you have (A op B op C) - the
726 # way they get parsed in the tree they're in different subclauses.
728 my ( $self, $field, $op, $value, %rest ) = @_;
730 unless ( $self->{_sql_transalias} ) {
731 $self->{_sql_transalias} = $self->Join(
734 TABLE2 => 'Transactions',
735 FIELD2 => 'ObjectId',
738 ALIAS => $self->{_sql_transalias},
739 FIELD => 'ObjectType',
740 VALUE => 'RT::Ticket',
741 ENTRYAGGREGATOR => 'AND',
744 unless ( defined $self->{_sql_trattachalias} ) {
745 $self->{_sql_trattachalias} = $self->_SQLJoin(
746 TYPE => 'LEFT', # not all txns have an attachment
747 ALIAS1 => $self->{_sql_transalias},
749 TABLE2 => 'Attachments',
750 FIELD2 => 'TransactionId',
754 #Search for the right field
755 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
759 ALIAS => $self->{_sql_trattachalias},
766 ENTRYAGGREGATOR => 'AND',
767 ALIAS => $self->{_sql_trattachalias},
776 ALIAS => $self->{_sql_trattachalias},
789 Handle watcher limits. (Requestor, CC, etc..)
805 my $meta = $FIELD_METADATA{ $field };
806 my $type = $meta->[1] || '';
807 my $class = $meta->[2] || 'Ticket';
809 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
810 # search by id and Name at the same time, this is workaround
811 # to preserve backward compatibility
812 if ( $field eq 'Owner' ) {
813 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
814 my $o = RT::User->new( $self->CurrentUser );
815 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
816 $o->$method( $value );
825 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
835 $rest{SUBKEY} ||= 'EmailAddress';
837 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
840 if ( $op =~ /^IS(?: NOT)?$/ ) {
841 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
842 # to avoid joining the table Users into the query, we just join GM
843 # and make sure we don't match records where group is member of itself
845 LEFTJOIN => $group_members,
848 VALUE => "$group_members.MemberId",
852 ALIAS => $group_members,
859 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
861 $op =~ s/!|NOT\s+//i;
863 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
864 # "X = 'Y'" matches more then one user so we try to fetch two records and
865 # do the right thing when there is only one exist and semi-working solution
867 my $users_obj = RT::Users->new( $self->CurrentUser );
869 FIELD => $rest{SUBKEY},
874 $users_obj->RowsPerPage(2);
875 my @users = @{ $users_obj->ItemsArrayRef };
877 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
880 $uid = $users[0]->id if @users;
882 LEFTJOIN => $group_members,
883 ALIAS => $group_members,
889 ALIAS => $group_members,
896 LEFTJOIN => $group_members,
899 VALUE => "$group_members.MemberId",
902 my $users = $self->Join(
904 ALIAS1 => $group_members,
905 FIELD1 => 'MemberId',
912 FIELD => $rest{SUBKEY},
926 my $group_members = $self->_GroupMembersJoin(
927 GroupsAlias => $groups,
931 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
933 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
934 $self->NewAlias('Users');
936 LEFTJOIN => $group_members,
937 ALIAS => $group_members,
939 VALUE => "$users.id",
944 # we join users table without adding some join condition between tables,
945 # the only conditions we have are conditions on the table iteslf,
946 # for example Users.EmailAddress = 'x'. We should add this condition to
947 # the top level of the query and bundle it with another similar conditions,
948 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
949 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
952 SUBCLAUSE => '_sql_u_watchers_'. $users,
954 FIELD => $rest{'SUBKEY'},
959 # A condition which ties Users and Groups (role groups) is a left join condition
960 # of CachedGroupMembers table. To get correct results of the query we check
961 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
964 ALIAS => $group_members,
966 OPERATOR => 'IS NOT',
973 sub _RoleGroupsJoin {
975 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
976 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
977 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
980 # we always have watcher groups for ticket, so we use INNER join
981 my $groups = $self->Join(
983 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
985 FIELD2 => 'Instance',
986 ENTRYAGGREGATOR => 'AND',
992 VALUE => 'RT::'. $args{'Class'} .'-Role',
998 VALUE => $args{'Type'},
1001 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1002 unless $args{'New'};
1007 sub _GroupMembersJoin {
1009 my %args = (New => 1, GroupsAlias => undef, @_);
1011 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1012 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1015 my $alias = $self->Join(
1017 ALIAS1 => $args{'GroupsAlias'},
1019 TABLE2 => 'CachedGroupMembers',
1020 FIELD2 => 'GroupId',
1021 ENTRYAGGREGATOR => 'AND',
1024 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1025 unless $args{'New'};
1032 Helper function which provides joins to a watchers table both for limits
1039 my $type = shift || '';
1042 my $groups = $self->_RoleGroupsJoin( Type => $type );
1043 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1044 # XXX: work around, we must hide groups that
1045 # are members of the role group we search in,
1046 # otherwise them result in wrong NULLs in Users
1047 # table and break ordering. Now, we know that
1048 # RT doesn't allow to add groups as members of the
1049 # ticket roles, so we just hide entries in CGM table
1050 # with MemberId == GroupId from results
1051 $self->SUPER::Limit(
1052 LEFTJOIN => $group_members,
1055 VALUE => "$group_members.MemberId",
1058 my $users = $self->Join(
1060 ALIAS1 => $group_members,
1061 FIELD1 => 'MemberId',
1065 return ($groups, $group_members, $users);
1068 =head2 _WatcherMembershipLimit
1070 Handle watcher membership limits, i.e. whether the watcher belongs to a
1071 specific group or not.
1074 1: Field to query on
1076 SELECT DISTINCT main.*
1080 CachedGroupMembers CachedGroupMembers_2,
1083 (main.EffectiveId = main.id)
1085 (main.Status != 'deleted')
1087 (main.Type = 'ticket')
1090 (Users_3.EmailAddress = '22')
1092 (Groups_1.Domain = 'RT::Ticket-Role')
1094 (Groups_1.Type = 'RequestorGroup')
1097 Groups_1.Instance = main.id
1099 Groups_1.id = CachedGroupMembers_2.GroupId
1101 CachedGroupMembers_2.MemberId = Users_3.id
1102 ORDER BY main.id ASC
1107 sub _WatcherMembershipLimit {
1108 my ( $self, $field, $op, $value, @rest ) = @_;
1113 my $groups = $self->NewAlias('Groups');
1114 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1115 my $users = $self->NewAlias('Users');
1116 my $memberships = $self->NewAlias('CachedGroupMembers');
1118 if ( ref $field ) { # gross hack
1119 my @bundle = @$field;
1121 for my $chunk (@bundle) {
1122 ( $field, $op, $value, @rest ) = @$chunk;
1124 ALIAS => $memberships,
1135 ALIAS => $memberships,
1143 # {{{ Tie to groups for tickets we care about
1147 VALUE => 'RT::Ticket-Role',
1148 ENTRYAGGREGATOR => 'AND'
1153 FIELD1 => 'Instance',
1160 # If we care about which sort of watcher
1161 my $meta = $FIELD_METADATA{$field};
1162 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1169 ENTRYAGGREGATOR => 'AND'
1176 ALIAS2 => $groupmembers,
1181 ALIAS1 => $groupmembers,
1182 FIELD1 => 'MemberId',
1188 ALIAS1 => $memberships,
1189 FIELD1 => 'MemberId',
1198 =head2 _CustomFieldDecipher
1200 Try and turn a CF descriptor into (cfid, cfname) object pair.
1204 sub _CustomFieldDecipher {
1205 my ($self, $string) = @_;
1207 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
1208 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1212 my $q = RT::Queue->new( $self->CurrentUser );
1216 # $queue = $q->Name; # should we normalize the queue?
1217 $cf = $q->CustomField( $field );
1220 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1224 elsif ( $field =~ /\D/ ) {
1226 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1227 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1228 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1230 # if there is more then one field the current user can
1231 # see with the same name then we shouldn't return cf object
1232 # as we don't know which one to use
1235 $cf = undef if $cfs->Next;
1239 $cf = RT::CustomField->new( $self->CurrentUser );
1240 $cf->Load( $field );
1243 return ($queue, $field, $cf, $column);
1246 =head2 _CustomFieldJoin
1248 Factor out the Join of custom fields so we can use it for sorting too
1252 sub _CustomFieldJoin {
1253 my ($self, $cfkey, $cfid, $field) = @_;
1254 # Perform one Join per CustomField
1255 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1256 $self->{_sql_cf_alias}{$cfkey} )
1258 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1259 $self->{_sql_cf_alias}{$cfkey} );
1262 my ($TicketCFs, $CFs);
1264 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1268 TABLE2 => 'ObjectCustomFieldValues',
1269 FIELD2 => 'ObjectId',
1271 $self->SUPER::Limit(
1272 LEFTJOIN => $TicketCFs,
1273 FIELD => 'CustomField',
1275 ENTRYAGGREGATOR => 'AND'
1279 my $ocfalias = $self->Join(
1282 TABLE2 => 'ObjectCustomFields',
1283 FIELD2 => 'ObjectId',
1286 $self->SUPER::Limit(
1287 LEFTJOIN => $ocfalias,
1288 ENTRYAGGREGATOR => 'OR',
1289 FIELD => 'ObjectId',
1293 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1295 ALIAS1 => $ocfalias,
1296 FIELD1 => 'CustomField',
1297 TABLE2 => 'CustomFields',
1300 $self->SUPER::Limit(
1302 ENTRYAGGREGATOR => 'AND',
1303 FIELD => 'LookupType',
1304 VALUE => 'RT::Queue-RT::Ticket',
1306 $self->SUPER::Limit(
1308 ENTRYAGGREGATOR => 'AND',
1313 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1317 TABLE2 => 'ObjectCustomFieldValues',
1318 FIELD2 => 'CustomField',
1320 $self->SUPER::Limit(
1321 LEFTJOIN => $TicketCFs,
1322 FIELD => 'ObjectId',
1325 ENTRYAGGREGATOR => 'AND',
1328 $self->SUPER::Limit(
1329 LEFTJOIN => $TicketCFs,
1330 FIELD => 'ObjectType',
1331 VALUE => 'RT::Ticket',
1332 ENTRYAGGREGATOR => 'AND'
1334 $self->SUPER::Limit(
1335 LEFTJOIN => $TicketCFs,
1336 FIELD => 'Disabled',
1339 ENTRYAGGREGATOR => 'AND'
1342 return ($TicketCFs, $CFs);
1345 =head2 _DateCustomFieldLimit
1347 Limit based on CustomFields of type Date
1354 sub _DateCustomFieldLimit {
1355 my ( $self, $_field, $op, $value, %rest ) = @_;
1357 my $field = $rest{'SUBKEY'} || die "No field specified";
1359 # For our sanity, we can only limit on one queue at a time
1361 my ($queue, $cfid, $column);
1362 ($queue, $field, $cfid, $column) = $self->_CustomFieldDecipher( $field );
1364 # If we're trying to find custom fields that don't match something, we
1365 # want tickets where the custom field has no value at all. Note that
1366 # we explicitly don't include the "IS NULL" case, since we would
1367 # otherwise end up with a redundant clause.
1369 my $null_columns_ok;
1370 if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
1371 $null_columns_ok = 1;
1374 my $cfkey = $cfid ? $cfid : "$queue.$field";
1375 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1379 if ( $CFs && !$cfid ) {
1380 $self->SUPER::Limit(
1384 ENTRYAGGREGATOR => 'AND',
1388 $self->_OpenParen if $null_columns_ok;
1390 my $date = RT::Date->new( $self->CurrentUser );
1391 $date->Set( Format => 'unknown', Value => $value );
1395 # if we're specifying =, that means we want everything on a
1396 # particular single day. in the database, we need to check for >
1397 # and < the edges of that day.
1399 $date->SetToMidnight( Timezone => 'server' );
1400 my $daystart = $date->ISO;
1402 my $dayend = $date->ISO;
1407 ALIAS => $TicketCFs,
1415 ALIAS => $TicketCFs,
1420 ENTRYAGGREGATOR => 'AND',
1428 ALIAS => $TicketCFs,
1431 VALUE => $date->ISO,
1440 =head2 _CustomFieldLimit
1442 Limit based on CustomFields
1449 sub _CustomFieldLimit {
1450 my ( $self, $_field, $op, $value, %rest ) = @_;
1452 my $field = $rest{'SUBKEY'} || die "No field specified";
1454 # For our sanity, we can only limit on one queue at a time
1456 my ($queue, $cfid, $cf, $column);
1457 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1458 $cfid = $cf ? $cf->id : 0 ;
1460 # If we're trying to find custom fields that don't match something, we
1461 # want tickets where the custom field has no value at all. Note that
1462 # we explicitly don't include the "IS NULL" case, since we would
1463 # otherwise end up with a redundant clause.
1465 my ($negative_op, $null_op, $inv_op, $range_op)
1466 = $self->ClassifySQLOperation( $op );
1470 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1471 return 'MATCHES' if $op eq '=';
1472 return 'NOT MATCHES' if $op eq '!=';
1476 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1478 my $cfkey = $cfid ? $cfid : "$queue.$field";
1480 if ( $null_op && !$column ) {
1481 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1482 # we can reuse our default joins for this operation
1483 # with column specified we have different situation
1484 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1487 ALIAS => $TicketCFs,
1496 OPERATOR => 'IS NOT',
1499 ENTRYAGGREGATOR => 'AND',
1503 elsif ( !$negative_op || $single_value ) {
1504 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1505 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1512 # if column is defined then deal only with it
1513 # otherwise search in Content and in LargeContent
1516 ALIAS => $TicketCFs,
1518 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1523 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1524 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1526 ALIAS => $TicketCFs,
1535 ALIAS => $TicketCFs,
1539 ENTRYAGGREGATOR => 'OR'
1542 ALIAS => $TicketCFs,
1546 ENTRYAGGREGATOR => 'OR'
1550 ALIAS => $TicketCFs,
1551 FIELD => 'LargeContent',
1552 OPERATOR => $fix_op->($op),
1554 ENTRYAGGREGATOR => 'AND',
1560 ALIAS => $TicketCFs,
1570 ALIAS => $TicketCFs,
1574 ENTRYAGGREGATOR => 'OR'
1577 ALIAS => $TicketCFs,
1581 ENTRYAGGREGATOR => 'OR'
1585 ALIAS => $TicketCFs,
1586 FIELD => 'LargeContent',
1587 OPERATOR => $fix_op->($op),
1589 ENTRYAGGREGATOR => 'AND',
1595 # XXX: if we join via CustomFields table then
1596 # because of order of left joins we get NULLs in
1597 # CF table and then get nulls for those records
1598 # in OCFVs table what result in wrong results
1599 # as decifer method now tries to load a CF then
1600 # we fall into this situation only when there
1601 # are more than one CF with the name in the DB.
1602 # the same thing applies to order by call.
1603 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1604 # we want treat IS NULL as (not applies or has
1609 OPERATOR => 'IS NOT',
1612 ENTRYAGGREGATOR => 'AND',
1618 ALIAS => $TicketCFs,
1619 FIELD => $column || 'Content',
1623 ENTRYAGGREGATOR => 'OR',
1630 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1631 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1634 $op =~ s/!|NOT\s+//i;
1636 # if column is defined then deal only with it
1637 # otherwise search in Content and in LargeContent
1639 $self->SUPER::Limit(
1640 LEFTJOIN => $TicketCFs,
1641 ALIAS => $TicketCFs,
1643 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1648 $self->SUPER::Limit(
1649 LEFTJOIN => $TicketCFs,
1650 ALIAS => $TicketCFs,
1658 ALIAS => $TicketCFs,
1667 sub _HasAttributeLimit {
1668 my ( $self, $field, $op, $value, %rest ) = @_;
1670 my $alias = $self->Join(
1674 TABLE2 => 'Attributes',
1675 FIELD2 => 'ObjectId',
1677 $self->SUPER::Limit(
1679 FIELD => 'ObjectType',
1680 VALUE => 'RT::Ticket',
1681 ENTRYAGGREGATOR => 'AND'
1683 $self->SUPER::Limit(
1688 ENTRYAGGREGATOR => 'AND'
1694 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1701 # End Helper Functions
1703 # End of SQL Stuff -------------------------------------------------
1705 # {{{ Allow sorting on watchers
1707 =head2 OrderByCols ARRAY
1709 A modified version of the OrderBy method which automatically joins where
1710 C<ALIAS> is set to the name of a watcher type.
1721 foreach my $row (@args) {
1722 if ( $row->{ALIAS} ) {
1726 if ( $row->{FIELD} !~ /\./ ) {
1727 my $meta = $self->FIELDS->{ $row->{FIELD} };
1733 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1734 my $alias = $self->Join(
1737 FIELD1 => $row->{'FIELD'},
1741 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1742 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1743 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1745 my $alias = $self->Join(
1748 FIELD1 => $row->{'FIELD'},
1752 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1759 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1760 my $meta = $self->FIELDS->{$field};
1761 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1762 # cache alias as we want to use one alias per watcher type for sorting
1763 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1765 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1766 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1768 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1769 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1770 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1771 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1772 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1773 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1774 # this is described in _CustomFieldLimit
1778 OPERATOR => 'IS NOT',
1781 ENTRYAGGREGATOR => 'AND',
1784 # For those cases where we are doing a join against the
1785 # CF name, and don't have a CFid, use Unique to make sure
1786 # we don't show duplicate tickets. NOTE: I'm pretty sure
1787 # this will stay mixed in for the life of the
1788 # class/package, and not just for the life of the object.
1789 # Potential performance issue.
1790 require DBIx::SearchBuilder::Unique;
1791 DBIx::SearchBuilder::Unique->import;
1793 my $CFvs = $self->Join(
1795 ALIAS1 => $TicketCFs,
1796 FIELD1 => 'CustomField',
1797 TABLE2 => 'CustomFieldValues',
1798 FIELD2 => 'CustomField',
1800 $self->SUPER::Limit(
1804 VALUE => $TicketCFs . ".Content",
1805 ENTRYAGGREGATOR => 'AND'
1808 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1809 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1810 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1811 # PAW logic is "reversed"
1813 if (exists $row->{ORDER} ) {
1814 my $o = $row->{ORDER};
1815 delete $row->{ORDER};
1816 $order = "DESC" if $o =~ /asc/i;
1819 # Ticket.Owner 1 0 X
1820 # Unowned Tickets 0 1 X
1823 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1824 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1825 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1826 push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
1828 push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
1832 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1838 return $self->SUPER::OrderByCols(@res);
1843 # {{{ Limit the result set based on content
1849 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1850 Generally best called from LimitFoo methods
1860 DESCRIPTION => undef,
1863 $args{'DESCRIPTION'} = $self->loc(
1864 "[_1] [_2] [_3]", $args{'FIELD'},
1865 $args{'OPERATOR'}, $args{'VALUE'}
1867 if ( !defined $args{'DESCRIPTION'} );
1869 my $index = $self->_NextIndex;
1871 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1873 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1875 $self->{'RecalcTicketLimits'} = 1;
1877 # If we're looking at the effective id, we don't want to append the other clause
1878 # which limits us to tickets where id = effective id
1879 if ( $args{'FIELD'} eq 'EffectiveId'
1880 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1882 $self->{'looking_at_effective_id'} = 1;
1885 if ( $args{'FIELD'} eq 'Type'
1886 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1888 $self->{'looking_at_type'} = 1;
1898 Returns a frozen string suitable for handing back to ThawLimits.
1902 sub _FreezeThawKeys {
1903 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1907 # {{{ sub FreezeLimits
1912 require MIME::Base64;
1913 MIME::Base64::base64_encode(
1914 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1921 Take a frozen Limits string generated by FreezeLimits and make this tickets
1922 object have that set of limits.
1926 # {{{ sub ThawLimits
1932 #if we don't have $in, get outta here.
1933 return undef unless ($in);
1935 $self->{'RecalcTicketLimits'} = 1;
1938 require MIME::Base64;
1940 #We don't need to die if the thaw fails.
1941 @{$self}{ $self->_FreezeThawKeys }
1942 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1944 $RT::Logger->error($@) if $@;
1950 # {{{ Limit by enum or foreign key
1952 # {{{ sub LimitQueue
1956 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1957 OPERATOR is one of = or !=. (It defaults to =).
1958 VALUE is a queue id or Name.
1971 #TODO VALUE should also take queue objects
1972 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1973 my $queue = new RT::Queue( $self->CurrentUser );
1974 $queue->Load( $args{'VALUE'} );
1975 $args{'VALUE'} = $queue->Id;
1978 # What if they pass in an Id? Check for isNum() and convert to
1981 #TODO check for a valid queue here
1985 VALUE => $args{'VALUE'},
1986 OPERATOR => $args{'OPERATOR'},
1987 DESCRIPTION => join(
1988 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1996 # {{{ sub LimitStatus
2000 Takes a paramhash with the fields OPERATOR and VALUE.
2001 OPERATOR is one of = or !=.
2004 RT adds Status != 'deleted' until object has
2005 allow_deleted_search internal property set.
2006 $tickets->{'allow_deleted_search'} = 1;
2007 $tickets->LimitStatus( VALUE => 'deleted' );
2019 VALUE => $args{'VALUE'},
2020 OPERATOR => $args{'OPERATOR'},
2021 DESCRIPTION => join( ' ',
2022 $self->loc('Status'), $args{'OPERATOR'},
2023 $self->loc( $args{'VALUE'} ) ),
2029 # {{{ sub IgnoreType
2033 If called, this search will not automatically limit the set of results found
2034 to tickets of type "Ticket". Tickets of other types, such as "project" and
2035 "approval" will be found.
2042 # Instead of faking a Limit that later gets ignored, fake up the
2043 # fact that we're already looking at type, so that the check in
2044 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2046 # $self->LimitType(VALUE => '__any');
2047 $self->{looking_at_type} = 1;
2056 Takes a paramhash with the fields OPERATOR and VALUE.
2057 OPERATOR is one of = or !=, it defaults to "=".
2058 VALUE is a string to search for in the type of the ticket.
2073 VALUE => $args{'VALUE'},
2074 OPERATOR => $args{'OPERATOR'},
2075 DESCRIPTION => join( ' ',
2076 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2084 # {{{ Limit by string field
2086 # {{{ sub LimitSubject
2090 Takes a paramhash with the fields OPERATOR and VALUE.
2091 OPERATOR is one of = or !=.
2092 VALUE is a string to search for in the subject of the ticket.
2101 VALUE => $args{'VALUE'},
2102 OPERATOR => $args{'OPERATOR'},
2103 DESCRIPTION => join( ' ',
2104 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2112 # {{{ Limit based on ticket numerical attributes
2113 # Things that can be > < = !=
2119 Takes a paramhash with the fields OPERATOR and VALUE.
2120 OPERATOR is one of =, >, < or !=.
2121 VALUE is a ticket Id to search for
2134 VALUE => $args{'VALUE'},
2135 OPERATOR => $args{'OPERATOR'},
2137 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2143 # {{{ sub LimitPriority
2145 =head2 LimitPriority
2147 Takes a paramhash with the fields OPERATOR and VALUE.
2148 OPERATOR is one of =, >, < or !=.
2149 VALUE is a value to match the ticket\'s priority against
2157 FIELD => 'Priority',
2158 VALUE => $args{'VALUE'},
2159 OPERATOR => $args{'OPERATOR'},
2160 DESCRIPTION => join( ' ',
2161 $self->loc('Priority'),
2162 $args{'OPERATOR'}, $args{'VALUE'}, ),
2168 # {{{ sub LimitInitialPriority
2170 =head2 LimitInitialPriority
2172 Takes a paramhash with the fields OPERATOR and VALUE.
2173 OPERATOR is one of =, >, < or !=.
2174 VALUE is a value to match the ticket\'s initial priority against
2179 sub LimitInitialPriority {
2183 FIELD => 'InitialPriority',
2184 VALUE => $args{'VALUE'},
2185 OPERATOR => $args{'OPERATOR'},
2186 DESCRIPTION => join( ' ',
2187 $self->loc('Initial Priority'), $args{'OPERATOR'},
2194 # {{{ sub LimitFinalPriority
2196 =head2 LimitFinalPriority
2198 Takes a paramhash with the fields OPERATOR and VALUE.
2199 OPERATOR is one of =, >, < or !=.
2200 VALUE is a value to match the ticket\'s final priority against
2204 sub LimitFinalPriority {
2208 FIELD => 'FinalPriority',
2209 VALUE => $args{'VALUE'},
2210 OPERATOR => $args{'OPERATOR'},
2211 DESCRIPTION => join( ' ',
2212 $self->loc('Final Priority'), $args{'OPERATOR'},
2219 # {{{ sub LimitTimeWorked
2221 =head2 LimitTimeWorked
2223 Takes a paramhash with the fields OPERATOR and VALUE.
2224 OPERATOR is one of =, >, < or !=.
2225 VALUE is a value to match the ticket's TimeWorked attribute
2229 sub LimitTimeWorked {
2233 FIELD => 'TimeWorked',
2234 VALUE => $args{'VALUE'},
2235 OPERATOR => $args{'OPERATOR'},
2236 DESCRIPTION => join( ' ',
2237 $self->loc('Time Worked'),
2238 $args{'OPERATOR'}, $args{'VALUE'}, ),
2244 # {{{ sub LimitTimeLeft
2246 =head2 LimitTimeLeft
2248 Takes a paramhash with the fields OPERATOR and VALUE.
2249 OPERATOR is one of =, >, < or !=.
2250 VALUE is a value to match the ticket's TimeLeft attribute
2258 FIELD => 'TimeLeft',
2259 VALUE => $args{'VALUE'},
2260 OPERATOR => $args{'OPERATOR'},
2261 DESCRIPTION => join( ' ',
2262 $self->loc('Time Left'),
2263 $args{'OPERATOR'}, $args{'VALUE'}, ),
2271 # {{{ Limiting based on attachment attributes
2273 # {{{ sub LimitContent
2277 Takes a paramhash with the fields OPERATOR and VALUE.
2278 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2279 VALUE is a string to search for in the body of the ticket
2288 VALUE => $args{'VALUE'},
2289 OPERATOR => $args{'OPERATOR'},
2290 DESCRIPTION => join( ' ',
2291 $self->loc('Ticket content'), $args{'OPERATOR'},
2298 # {{{ sub LimitFilename
2300 =head2 LimitFilename
2302 Takes a paramhash with the fields OPERATOR and VALUE.
2303 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2304 VALUE is a string to search for in the body of the ticket
2312 FIELD => 'Filename',
2313 VALUE => $args{'VALUE'},
2314 OPERATOR => $args{'OPERATOR'},
2315 DESCRIPTION => join( ' ',
2316 $self->loc('Attachment filename'), $args{'OPERATOR'},
2322 # {{{ sub LimitContentType
2324 =head2 LimitContentType
2326 Takes a paramhash with the fields OPERATOR and VALUE.
2327 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2328 VALUE is a content type to search ticket attachments for
2332 sub LimitContentType {
2336 FIELD => 'ContentType',
2337 VALUE => $args{'VALUE'},
2338 OPERATOR => $args{'OPERATOR'},
2339 DESCRIPTION => join( ' ',
2340 $self->loc('Ticket content type'), $args{'OPERATOR'},
2349 # {{{ Limiting based on people
2351 # {{{ sub LimitOwner
2355 Takes a paramhash with the fields OPERATOR and VALUE.
2356 OPERATOR is one of = or !=.
2368 my $owner = new RT::User( $self->CurrentUser );
2369 $owner->Load( $args{'VALUE'} );
2371 # FIXME: check for a valid $owner
2374 VALUE => $args{'VALUE'},
2375 OPERATOR => $args{'OPERATOR'},
2376 DESCRIPTION => join( ' ',
2377 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2384 # {{{ Limiting watchers
2386 # {{{ sub LimitWatcher
2390 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2391 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2392 VALUE is a value to match the ticket\'s watcher email addresses against
2393 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2407 #build us up a description
2408 my ( $watcher_type, $desc );
2409 if ( $args{'TYPE'} ) {
2410 $watcher_type = $args{'TYPE'};
2413 $watcher_type = "Watcher";
2417 FIELD => $watcher_type,
2418 VALUE => $args{'VALUE'},
2419 OPERATOR => $args{'OPERATOR'},
2420 TYPE => $args{'TYPE'},
2421 DESCRIPTION => join( ' ',
2422 $self->loc($watcher_type),
2423 $args{'OPERATOR'}, $args{'VALUE'}, ),
2433 # {{{ Limiting based on links
2437 =head2 LimitLinkedTo
2439 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2440 TYPE limits the sort of link we want to search on
2442 TYPE = { RefersTo, MemberOf, DependsOn }
2444 TARGET is the id or URI of the TARGET of the link
2458 FIELD => 'LinkedTo',
2460 TARGET => $args{'TARGET'},
2461 TYPE => $args{'TYPE'},
2462 DESCRIPTION => $self->loc(
2463 "Tickets [_1] by [_2]",
2464 $self->loc( $args{'TYPE'} ),
2467 OPERATOR => $args{'OPERATOR'},
2473 # {{{ LimitLinkedFrom
2475 =head2 LimitLinkedFrom
2477 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2478 TYPE limits the sort of link we want to search on
2481 BASE is the id or URI of the BASE of the link
2485 sub LimitLinkedFrom {
2494 # translate RT2 From/To naming to RT3 TicketSQL naming
2495 my %fromToMap = qw(DependsOn DependentOn
2497 RefersTo ReferredToBy);
2499 my $type = $args{'TYPE'};
2500 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2503 FIELD => 'LinkedTo',
2505 BASE => $args{'BASE'},
2507 DESCRIPTION => $self->loc(
2508 "Tickets [_1] [_2]",
2509 $self->loc( $args{'TYPE'} ),
2512 OPERATOR => $args{'OPERATOR'},
2521 my $ticket_id = shift;
2522 return $self->LimitLinkedTo(
2524 TARGET => $ticket_id,
2531 # {{{ LimitHasMember
2532 sub LimitHasMember {
2534 my $ticket_id = shift;
2535 return $self->LimitLinkedFrom(
2537 BASE => "$ticket_id",
2538 TYPE => 'HasMember',
2545 # {{{ LimitDependsOn
2547 sub LimitDependsOn {
2549 my $ticket_id = shift;
2550 return $self->LimitLinkedTo(
2552 TARGET => $ticket_id,
2553 TYPE => 'DependsOn',
2560 # {{{ LimitDependedOnBy
2562 sub LimitDependedOnBy {
2564 my $ticket_id = shift;
2565 return $self->LimitLinkedFrom(
2568 TYPE => 'DependentOn',
2579 my $ticket_id = shift;
2580 return $self->LimitLinkedTo(
2582 TARGET => $ticket_id,
2590 # {{{ LimitReferredToBy
2592 sub LimitReferredToBy {
2594 my $ticket_id = shift;
2595 return $self->LimitLinkedFrom(
2598 TYPE => 'ReferredToBy',
2606 # {{{ limit based on ticket date attribtes
2610 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2612 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2614 OPERATOR is one of > or <
2615 VALUE is a date and time in ISO format in GMT
2616 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2618 There are also helper functions of the form LimitFIELD that eliminate
2619 the need to pass in a FIELD argument.
2633 #Set the description if we didn't get handed it above
2634 unless ( $args{'DESCRIPTION'} ) {
2635 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2636 . $args{'OPERATOR'} . " "
2637 . $args{'VALUE'} . " GMT";
2640 $self->Limit(%args);
2648 $self->LimitDate( FIELD => 'Created', @_ );
2653 $self->LimitDate( FIELD => 'Due', @_ );
2659 $self->LimitDate( FIELD => 'Starts', @_ );
2665 $self->LimitDate( FIELD => 'Started', @_ );
2670 $self->LimitDate( FIELD => 'Resolved', @_ );
2675 $self->LimitDate( FIELD => 'Told', @_ );
2678 sub LimitLastUpdated {
2680 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2684 # {{{ sub LimitTransactionDate
2686 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2688 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2690 OPERATOR is one of > or <
2691 VALUE is a date and time in ISO format in GMT
2696 sub LimitTransactionDate {
2699 FIELD => 'TransactionDate',
2706 # <20021217042756.GK28744@pallas.fsck.com>
2707 # "Kill It" - Jesse.
2709 #Set the description if we didn't get handed it above
2710 unless ( $args{'DESCRIPTION'} ) {
2711 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2712 . $args{'OPERATOR'} . " "
2713 . $args{'VALUE'} . " GMT";
2716 $self->Limit(%args);
2724 # {{{ Limit based on custom fields
2725 # {{{ sub LimitCustomField
2727 =head2 LimitCustomField
2729 Takes a paramhash of key/value pairs with the following keys:
2733 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2735 =item OPERATOR - The usual Limit operators
2737 =item VALUE - The value to compare against
2743 sub LimitCustomField {
2747 CUSTOMFIELD => undef,
2749 DESCRIPTION => undef,
2750 FIELD => 'CustomFieldValue',
2755 my $CF = RT::CustomField->new( $self->CurrentUser );
2756 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2757 $CF->Load( $args{CUSTOMFIELD} );
2760 $CF->LoadByNameAndQueue(
2761 Name => $args{CUSTOMFIELD},
2762 Queue => $args{QUEUE}
2764 $args{CUSTOMFIELD} = $CF->Id;
2767 # Handle special customfields types
2768 if ($CF->Type eq 'Date') {
2769 $args{FIELD} = 'DateCustomFieldValue';
2772 #If we are looking to compare with a null value.
2773 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2774 $args{'DESCRIPTION'}
2775 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2777 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2778 $args{'DESCRIPTION'}
2779 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2782 # if we're not looking to compare with a null value
2784 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2785 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2788 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2789 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2790 $QueueObj->Load( $args{'QUEUE'} );
2791 $args{'QUEUE'} = $QueueObj->Id;
2793 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2796 @rest = ( ENTRYAGGREGATOR => 'AND' )
2797 if ( $CF->Type eq 'SelectMultiple' );
2800 VALUE => $args{VALUE},
2802 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2803 .".{" . $CF->Name . "}",
2804 OPERATOR => $args{OPERATOR},
2809 $self->{'RecalcTicketLimits'} = 1;
2815 # {{{ sub _NextIndex
2819 Keep track of the counter for the array of restrictions
2825 return ( $self->{'restriction_index'}++ );
2832 # {{{ Core bits to make this a DBIx::SearchBuilder object
2837 $self->{'table'} = "Tickets";
2838 $self->{'RecalcTicketLimits'} = 1;
2839 $self->{'looking_at_effective_id'} = 0;
2840 $self->{'looking_at_type'} = 0;
2841 $self->{'restriction_index'} = 1;
2842 $self->{'primary_key'} = "id";
2843 delete $self->{'items_array'};
2844 delete $self->{'item_map'};
2845 delete $self->{'columns_to_display'};
2846 $self->SUPER::_Init(@_);
2857 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2858 return ( $self->SUPER::Count() );
2866 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2867 return ( $self->SUPER::CountAll() );
2872 # {{{ sub ItemsArrayRef
2874 =head2 ItemsArrayRef
2876 Returns a reference to the set of all items found in this search
2883 return $self->{'items_array'} if $self->{'items_array'};
2885 my $placeholder = $self->_ItemsCounter;
2886 $self->GotoFirstItem();
2887 while ( my $item = $self->Next ) {
2888 push( @{ $self->{'items_array'} }, $item );
2890 $self->GotoItem($placeholder);
2891 $self->{'items_array'}
2892 = $self->ItemsOrderBy( $self->{'items_array'} );
2894 return $self->{'items_array'};
2897 sub ItemsArrayRefWindow {
2901 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2903 $self->RowsPerPage( $window );
2905 $self->GotoFirstItem;
2908 while ( my $item = $self->Next ) {
2912 $self->RowsPerPage( $old[1] );
2913 $self->FirstRow( $old[2] );
2914 $self->GotoItem( $old[0] );
2925 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2927 my $Ticket = $self->SUPER::Next;
2928 return $Ticket unless $Ticket;
2930 if ( $Ticket->__Value('Status') eq 'deleted'
2931 && !$self->{'allow_deleted_search'} )
2935 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2936 # if we found a ticket with this option enabled then
2937 # all tickets we found are ACLed, cache this fact
2938 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2939 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2942 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2947 # If the user doesn't have the right to show this ticket
2954 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2955 return $self->SUPER::_DoSearch( @_ );
2960 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2961 return $self->SUPER::_DoCount( @_ );
2967 my $cache_key = 'RolesHasRight;:;ShowTicket';
2969 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
2973 my $ACL = RT::ACL->new( $RT::SystemUser );
2974 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2975 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
2976 my $principal_alias = $ACL->Join(
2978 FIELD1 => 'PrincipalId',
2979 TABLE2 => 'Principals',
2982 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2985 while ( my $ACE = $ACL->Next ) {
2986 my $role = $ACE->PrincipalType;
2987 my $type = $ACE->ObjectType;
2988 if ( $type eq 'RT::System' ) {
2991 elsif ( $type eq 'RT::Queue' ) {
2992 next if $res{ $role } && !ref $res{ $role };
2993 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
2996 $RT::Logger->error('ShowTicket right is granted on unsupported object');
2999 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3003 sub _DirectlyCanSeeIn {
3005 my $id = $self->CurrentUser->id;
3007 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3008 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3012 my $ACL = RT::ACL->new( $RT::SystemUser );
3013 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3014 my $principal_alias = $ACL->Join(
3016 FIELD1 => 'PrincipalId',
3017 TABLE2 => 'Principals',
3020 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3021 my $cgm_alias = $ACL->Join(
3023 FIELD1 => 'PrincipalId',
3024 TABLE2 => 'CachedGroupMembers',
3025 FIELD2 => 'GroupId',
3027 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3028 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3031 while ( my $ACE = $ACL->Next ) {
3032 my $type = $ACE->ObjectType;
3033 if ( $type eq 'RT::System' ) {
3034 # If user is direct member of a group that has the right
3035 # on the system then he can see any ticket
3036 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3039 elsif ( $type eq 'RT::Queue' ) {
3040 push @res, $ACE->ObjectId;
3043 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3046 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3050 sub CurrentUserCanSee {
3052 return if $self->{'_sql_current_user_can_see_applied'};
3054 return $self->{'_sql_current_user_can_see_applied'} = 1
3055 if $self->CurrentUser->UserObj->HasRight(
3056 Right => 'SuperUser', Object => $RT::System
3059 my $id = $self->CurrentUser->id;
3061 # directly can see in all queues then we have nothing to do
3062 my @direct_queues = $self->_DirectlyCanSeeIn;
3063 return $self->{'_sql_current_user_can_see_applied'} = 1
3064 if @direct_queues && $direct_queues[0] == -1;
3066 my %roles = $self->_RolesCanSee;
3068 my %skip = map { $_ => 1 } @direct_queues;
3069 foreach my $role ( keys %roles ) {
3070 next unless ref $roles{ $role };
3072 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3074 $roles{ $role } = \@queues;
3076 delete $roles{ $role };
3081 # there is no global watchers, only queues and tickes, if at
3082 # some point we will add global roles then it's gonna blow
3083 # the idea here is that if the right is set globaly for a role
3084 # and user plays this role for a queue directly not a ticket
3085 # then we have to check in advance
3086 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3088 my $groups = RT::Groups->new( $RT::SystemUser );
3089 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3091 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3093 my $principal_alias = $groups->Join(
3096 TABLE2 => 'Principals',
3099 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3100 my $cgm_alias = $groups->Join(
3103 TABLE2 => 'CachedGroupMembers',
3104 FIELD2 => 'GroupId',
3106 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3107 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3108 while ( my $group = $groups->Next ) {
3109 push @direct_queues, $group->Instance;
3113 unless ( @direct_queues || keys %roles ) {
3114 $self->SUPER::Limit(
3119 ENTRYAGGREGATOR => 'AND',
3121 return $self->{'_sql_current_user_can_see_applied'} = 1;
3125 my $join_roles = keys %roles;
3126 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3127 my ($role_group_alias, $cgm_alias);
3128 if ( $join_roles ) {
3129 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3130 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3131 $self->SUPER::Limit(
3132 LEFTJOIN => $cgm_alias,
3133 FIELD => 'MemberId',
3138 my $limit_queues = sub {
3142 return unless @queues;
3143 if ( @queues == 1 ) {
3144 $self->SUPER::Limit(
3149 ENTRYAGGREGATOR => $ea,
3152 $self->SUPER::_OpenParen('ACL');
3153 foreach my $q ( @queues ) {
3154 $self->SUPER::Limit(
3159 ENTRYAGGREGATOR => $ea,
3163 $self->SUPER::_CloseParen('ACL');
3168 $self->SUPER::_OpenParen('ACL');
3170 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3171 while ( my ($role, $queues) = each %roles ) {
3172 $self->SUPER::_OpenParen('ACL');
3173 if ( $role eq 'Owner' ) {
3174 $self->SUPER::Limit(
3178 ENTRYAGGREGATOR => $ea,
3182 $self->SUPER::Limit(
3184 ALIAS => $cgm_alias,
3185 FIELD => 'MemberId',
3186 OPERATOR => 'IS NOT',
3189 ENTRYAGGREGATOR => $ea,
3191 $self->SUPER::Limit(
3193 ALIAS => $role_group_alias,
3196 ENTRYAGGREGATOR => 'AND',
3199 $limit_queues->( 'AND', @$queues ) if ref $queues;
3200 $ea = 'OR' if $ea eq 'AND';
3201 $self->SUPER::_CloseParen('ACL');
3203 $self->SUPER::_CloseParen('ACL');
3205 return $self->{'_sql_current_user_can_see_applied'} = 1;
3212 # {{{ Deal with storing and restoring restrictions
3214 # {{{ sub LoadRestrictions
3216 =head2 LoadRestrictions
3218 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3219 TODO It is not yet implemented
3225 # {{{ sub DescribeRestrictions
3227 =head2 DescribeRestrictions
3230 Returns a hash keyed by restriction id.
3231 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3232 is a description of the purpose of that TicketRestriction
3236 sub DescribeRestrictions {
3239 my ( $row, %listing );
3241 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3242 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3249 # {{{ sub RestrictionValues
3251 =head2 RestrictionValues FIELD
3253 Takes a restriction field and returns a list of values this field is restricted
3258 sub RestrictionValues {
3261 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3262 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3263 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3265 keys %{ $self->{'TicketRestrictions'} };
3270 # {{{ sub ClearRestrictions
3272 =head2 ClearRestrictions
3274 Removes all restrictions irretrievably
3278 sub ClearRestrictions {
3280 delete $self->{'TicketRestrictions'};
3281 $self->{'looking_at_effective_id'} = 0;
3282 $self->{'looking_at_type'} = 0;
3283 $self->{'RecalcTicketLimits'} = 1;
3288 # {{{ sub DeleteRestriction
3290 =head2 DeleteRestriction
3292 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3293 Removes that restriction from the session's limits.
3297 sub DeleteRestriction {
3300 delete $self->{'TicketRestrictions'}{$row};
3302 $self->{'RecalcTicketLimits'} = 1;
3304 #make the underlying easysearch object forget all its preconceptions
3309 # {{{ sub _RestrictionsToClauses
3311 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3313 sub _RestrictionsToClauses {
3318 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3319 my $restriction = $self->{'TicketRestrictions'}{$row};
3321 # We need to reimplement the subclause aggregation that SearchBuilder does.
3322 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3323 # Then SB AND's the different Subclauses together.
3325 # So, we want to group things into Subclauses, convert them to
3326 # SQL, and then join them with the appropriate DefaultEA.
3327 # Then join each subclause group with AND.
3329 my $field = $restriction->{'FIELD'};
3330 my $realfield = $field; # CustomFields fake up a fieldname, so
3331 # we need to figure that out
3334 # Rewrite LinkedTo meta field to the real field
3335 if ( $field =~ /LinkedTo/ ) {
3336 $realfield = $field = $restriction->{'TYPE'};
3340 # Handle subkey fields with a different real field
3341 if ( $field =~ /^(\w+)\./ ) {
3345 die "I don't know about $field yet"
3346 unless ( exists $FIELD_METADATA{$realfield}
3347 or $restriction->{CUSTOMFIELD} );
3349 my $type = $FIELD_METADATA{$realfield}->[0];
3350 my $op = $restriction->{'OPERATOR'};
3354 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3357 # this performs the moral equivalent of defined or/dor/C<//>,
3358 # without the short circuiting.You need to use a 'defined or'
3359 # type thing instead of just checking for truth values, because
3360 # VALUE could be 0.(i.e. "false")
3362 # You could also use this, but I find it less aesthetic:
3363 # (although it does short circuit)
3364 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3365 # defined $restriction->{'TICKET'} ?
3366 # $restriction->{TICKET} :
3367 # defined $restriction->{'BASE'} ?
3368 # $restriction->{BASE} :
3369 # defined $restriction->{'TARGET'} ?
3370 # $restriction->{TARGET} )
3372 my $ea = $restriction->{ENTRYAGGREGATOR}
3373 || $DefaultEA{$type}
3376 die "Invalid operator $op for $field ($type)"
3377 unless exists $ea->{$op};
3381 # Each CustomField should be put into a different Clause so they
3382 # are ANDed together.
3383 if ( $restriction->{CUSTOMFIELD} ) {
3384 $realfield = $field;
3387 exists $clause{$realfield} or $clause{$realfield} = [];
3390 $field =~ s!(['"])!\\$1!g;
3391 $value =~ s!(['"])!\\$1!g;
3392 my $data = [ $ea, $type, $field, $op, $value ];
3394 # here is where we store extra data, say if it's a keyword or
3395 # something. (I.e. "TYPE SPECIFIC STUFF")
3397 push @{ $clause{$realfield} }, $data;
3404 # {{{ sub _ProcessRestrictions
3406 =head2 _ProcessRestrictions PARAMHASH
3408 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3409 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3413 sub _ProcessRestrictions {
3416 #Blow away ticket aliases since we'll need to regenerate them for
3418 delete $self->{'TicketAliases'};
3419 delete $self->{'items_array'};
3420 delete $self->{'item_map'};
3421 delete $self->{'raw_rows'};
3422 delete $self->{'rows'};
3423 delete $self->{'count_all'};
3425 my $sql = $self->Query; # Violating the _SQL namespace
3426 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3428 # "Restrictions to Clauses Branch\n";
3429 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3431 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3435 $sql = $self->ClausesToSQL($clauseRef);
3436 $self->FromSQL($sql) if $sql;
3440 $self->{'RecalcTicketLimits'} = 0;
3444 =head2 _BuildItemMap
3446 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3447 display search nav quickly.
3454 my $window = RT->Config->Get('TicketsItemMapSize');
3456 $self->{'item_map'} = {};
3458 my $items = $self->ItemsArrayRefWindow( $window );
3459 return unless $items && @$items;
3462 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3463 for ( my $i = 0; $i < @$items; $i++ ) {
3464 my $item = $items->[$i];
3465 my $id = $item->EffectiveId;
3466 $self->{'item_map'}{$id}{'defined'} = 1;
3467 $self->{'item_map'}{$id}{'prev'} = $prev;
3468 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3472 $self->{'item_map'}{'last'} = $prev
3473 if !$window || @$items < $window;
3478 Returns an a map of all items found by this search. The map is a hash
3482 first => <first ticket id found>,
3483 last => <last ticket id found or undef>,
3486 prev => <the ticket id found before>,
3487 next => <the ticket id found after>,
3499 $self->_BuildItemMap unless $self->{'item_map'};
3500 return $self->{'item_map'};
3508 =head2 PrepForSerialization
3510 You don't want to serialize a big tickets object, as
3511 the {items} hash will be instantly invalid _and_ eat
3516 sub PrepForSerialization {
3518 delete $self->{'items'};
3519 delete $self->{'items_array'};
3520 $self->RedoSearch();
3525 RT::Tickets supports several flags which alter search behavior:
3528 allow_deleted_search (Otherwise never show deleted tickets in search results)
3529 looking_at_type (otherwise limit to type=ticket)
3531 These flags are set by calling
3533 $tickets->{'flagname'} = 1;
3535 BUG: There should be an API for this