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 ],
149 Agentnum => [ 'FREESIDEFIELD', ],
150 Classnum => [ 'FREESIDEFIELD', ],
151 Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ],
154 # Mapping of Field Type to Function
156 ENUM => \&_EnumLimit,
159 LINK => \&_LinkLimit,
160 DATE => \&_DateLimit,
161 STRING => \&_StringLimit,
162 TRANSFIELD => \&_TransLimit,
163 TRANSDATE => \&_TransDateLimit,
164 WATCHERFIELD => \&_WatcherLimit,
165 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
166 CUSTOMFIELD => \&_CustomFieldLimit,
167 DATECUSTOMFIELD => \&_DateCustomFieldLimit,
168 HASATTRIBUTE => \&_HasAttributeLimit,
169 FREESIDEFIELD => \&_FreesideFieldLimit,
171 our %can_bundle = ();# WATCHERFIELD => "yes", );
173 # Default EntryAggregator per type
174 # if you specify OP, you must specify all valid OPs
215 # Helper functions for passing the above lexically scoped tables above
216 # into Tickets_Overlay_SQL.
217 sub FIELDS { return \%FIELD_METADATA }
218 sub dispatch { return \%dispatch }
219 sub can_bundle { return \%can_bundle }
221 # Bring in the clowns.
222 require RT::Tickets_Overlay_SQL;
226 our @SORTFIELDS = qw(id Status
228 Owner Created Due Starts Started
230 Resolved LastUpdated Priority TimeWorked TimeLeft);
234 Returns the list of fields that lists of tickets can easily be sorted by
240 return (@SORTFIELDS);
245 # BEGIN SQL STUFF *********************************
250 $self->SUPER::CleanSlate( @_ );
251 delete $self->{$_} foreach qw(
253 _sql_group_members_aliases
254 _sql_object_cfv_alias
255 _sql_role_group_aliases
258 _sql_u_watchers_alias_for_sort
259 _sql_u_watchers_aliases
260 _sql_current_user_can_see_applied
264 =head1 Limit Helper Routines
266 These routines are the targets of a dispatch table depending on the
267 type of field. They all share the same signature:
269 my ($self,$field,$op,$value,@rest) = @_;
271 The values in @rest should be suitable for passing directly to
272 DBIx::SearchBuilder::Limit.
274 Essentially they are an expanded/broken out (and much simplified)
275 version of what ProcessRestrictions used to do. They're also much
276 more clearly delineated by the TYPE of field being processed.
285 my ( $sb, $field, $op, $value, @rest ) = @_;
287 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
289 die "Invalid operator $op for __Bookmarked__ search on $field"
290 unless $op =~ /^(=|!=)$/;
293 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
294 $tmp = $tmp->Content if $tmp;
299 return $sb->_SQLLimit(
306 # as bookmarked tickets can be merged we have to use a join
307 # but it should be pretty lightweight
308 my $tickets_alias = $sb->Join(
313 FIELD2 => 'EffectiveId',
317 my $ea = $op eq '='? 'OR': 'AND';
318 foreach my $id ( sort @bookmarks ) {
320 ALIAS => $tickets_alias,
324 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
332 Handle Fields which are limited to certain values, and potentially
333 need to be looked up from another class.
335 This subroutine actually handles two different kinds of fields. For
336 some the user is responsible for limiting the values. (i.e. Status,
339 For others, the value specified by the user will be looked by via
343 name of class to lookup in (Optional)
348 my ( $sb, $field, $op, $value, @rest ) = @_;
350 # SQL::Statement changes != to <>. (Can we remove this now?)
351 $op = "!=" if $op eq "<>";
353 die "Invalid Operation: $op for $field"
357 my $meta = $FIELD_METADATA{$field};
358 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
359 my $class = "RT::" . $meta->[1];
360 my $o = $class->new( $sb->CurrentUser );
374 Handle fields where the values are limited to integers. (For example,
375 Priority, TimeWorked.)
383 my ( $sb, $field, $op, $value, @rest ) = @_;
385 die "Invalid Operator $op for $field"
386 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
398 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
401 1: Direction (From, To)
402 2: Link Type (MemberOf, DependsOn, RefersTo)
407 my ( $sb, $field, $op, $value, @rest ) = @_;
409 my $meta = $FIELD_METADATA{$field};
410 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
413 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
417 $is_null = 1 if !$value || $value =~ /^null$/io;
419 my $direction = $meta->[1] || '';
420 my ($matchfield, $linkfield) = ('', '');
421 if ( $direction eq 'To' ) {
422 ($matchfield, $linkfield) = ("Target", "Base");
424 elsif ( $direction eq 'From' ) {
425 ($matchfield, $linkfield) = ("Base", "Target");
427 elsif ( $direction ) {
428 die "Invalid link direction '$direction' for $field\n";
431 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
433 'LinkedFrom', $op, $value, @rest,
434 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
442 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
444 elsif ( $value =~ /\D/ ) {
447 $matchfield = "Local$matchfield" if $is_local;
449 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
450 # SELECT main.* FROM Tickets main
451 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
452 # AND(main.id = Links_1.LocalTarget))
453 # WHERE Links_1.LocalBase IS NULL;
456 my $linkalias = $sb->Join(
461 FIELD2 => 'Local' . $linkfield
464 LEFTJOIN => $linkalias,
472 FIELD => $matchfield,
479 my $linkalias = $sb->Join(
484 FIELD2 => 'Local' . $linkfield
487 LEFTJOIN => $linkalias,
493 LEFTJOIN => $linkalias,
494 FIELD => $matchfield,
501 FIELD => $matchfield,
502 OPERATOR => $is_negative? 'IS': 'IS NOT',
511 Handle date fields. (Created, LastTold..)
514 1: type of link. (Probably not necessary.)
519 my ( $sb, $field, $op, $value, @rest ) = @_;
521 die "Invalid Date Op: $op"
522 unless $op =~ /^(=|>|<|>=|<=)$/;
524 my $meta = $FIELD_METADATA{$field};
525 die "Incorrect Meta Data for $field"
526 unless ( defined $meta->[1] );
528 my $date = RT::Date->new( $sb->CurrentUser );
529 $date->Set( Format => 'unknown', Value => $value );
533 # if we're specifying =, that means we want everything on a
534 # particular single day. in the database, we need to check for >
535 # and < the edges of that day.
537 $date->SetToMidnight( Timezone => 'server' );
538 my $daystart = $date->ISO;
540 my $dayend = $date->ISO;
556 ENTRYAGGREGATOR => 'AND',
574 Handle simple fields which are just strings. (Subject,Type)
582 my ( $sb, $field, $op, $value, @rest ) = @_;
586 # =, !=, LIKE, NOT LIKE
587 if ( (!defined $value || !length $value)
588 && lc($op) ne 'is' && lc($op) ne 'is not'
589 && RT->Config->Get('DatabaseType') eq 'Oracle'
591 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
592 $op = $negative? 'IS NOT': 'IS';
605 =head2 _TransDateLimit
607 Handle fields limiting based on Transaction Date.
609 The inpupt value must be in a format parseable by Time::ParseDate
616 # This routine should really be factored into translimit.
617 sub _TransDateLimit {
618 my ( $sb, $field, $op, $value, @rest ) = @_;
620 # See the comments for TransLimit, they apply here too
622 unless ( $sb->{_sql_transalias} ) {
623 $sb->{_sql_transalias} = $sb->Join(
626 TABLE2 => 'Transactions',
627 FIELD2 => 'ObjectId',
630 ALIAS => $sb->{_sql_transalias},
631 FIELD => 'ObjectType',
632 VALUE => 'RT::Ticket',
633 ENTRYAGGREGATOR => 'AND',
637 my $date = RT::Date->new( $sb->CurrentUser );
638 $date->Set( Format => 'unknown', Value => $value );
643 # if we're specifying =, that means we want everything on a
644 # particular single day. in the database, we need to check for >
645 # and < the edges of that day.
647 $date->SetToMidnight( Timezone => 'server' );
648 my $daystart = $date->ISO;
650 my $dayend = $date->ISO;
653 ALIAS => $sb->{_sql_transalias},
661 ALIAS => $sb->{_sql_transalias},
667 ENTRYAGGREGATOR => 'AND',
672 # not searching for a single day
675 #Search for the right field
677 ALIAS => $sb->{_sql_transalias},
691 Limit based on the Content of a transaction or the ContentType.
700 # Content, ContentType, Filename
702 # If only this was this simple. We've got to do something
705 #Basically, we want to make sure that the limits apply to
706 #the same attachment, rather than just another attachment
707 #for the same ticket, no matter how many clauses we lump
708 #on. We put them in TicketAliases so that they get nuked
709 #when we redo the join.
711 # In the SQL, we might have
712 # (( Content = foo ) or ( Content = bar AND Content = baz ))
713 # The AND group should share the same Alias.
715 # Actually, maybe it doesn't matter. We use the same alias and it
716 # works itself out? (er.. different.)
718 # Steal more from _ProcessRestrictions
720 # FIXME: Maybe look at the previous FooLimit call, and if it was a
721 # TransLimit and EntryAggregator == AND, reuse the Aliases?
723 # Or better - store the aliases on a per subclause basis - since
724 # those are going to be the things we want to relate to each other,
727 # maybe we should not allow certain kinds of aggregation of these
728 # clauses and do a psuedo regex instead? - the problem is getting
729 # them all into the same subclause when you have (A op B op C) - the
730 # way they get parsed in the tree they're in different subclauses.
732 my ( $self, $field, $op, $value, %rest ) = @_;
734 unless ( $self->{_sql_transalias} ) {
735 $self->{_sql_transalias} = $self->Join(
738 TABLE2 => 'Transactions',
739 FIELD2 => 'ObjectId',
742 ALIAS => $self->{_sql_transalias},
743 FIELD => 'ObjectType',
744 VALUE => 'RT::Ticket',
745 ENTRYAGGREGATOR => 'AND',
748 unless ( defined $self->{_sql_trattachalias} ) {
749 $self->{_sql_trattachalias} = $self->_SQLJoin(
750 TYPE => 'LEFT', # not all txns have an attachment
751 ALIAS1 => $self->{_sql_transalias},
753 TABLE2 => 'Attachments',
754 FIELD2 => 'TransactionId',
758 #Search for the right field
759 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
763 ALIAS => $self->{_sql_trattachalias},
770 ENTRYAGGREGATOR => 'AND',
771 ALIAS => $self->{_sql_trattachalias},
780 ALIAS => $self->{_sql_trattachalias},
793 Handle watcher limits. (Requestor, CC, etc..)
809 my $meta = $FIELD_METADATA{ $field };
810 my $type = $meta->[1] || '';
811 my $class = $meta->[2] || 'Ticket';
813 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
814 # search by id and Name at the same time, this is workaround
815 # to preserve backward compatibility
816 if ( $field eq 'Owner' ) {
817 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
818 my $o = RT::User->new( $self->CurrentUser );
819 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
820 $o->$method( $value );
829 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
839 $rest{SUBKEY} ||= 'EmailAddress';
841 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
844 if ( $op =~ /^IS(?: NOT)?$/ ) {
845 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
846 # to avoid joining the table Users into the query, we just join GM
847 # and make sure we don't match records where group is member of itself
849 LEFTJOIN => $group_members,
852 VALUE => "$group_members.MemberId",
856 ALIAS => $group_members,
863 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
865 $op =~ s/!|NOT\s+//i;
867 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
868 # "X = 'Y'" matches more then one user so we try to fetch two records and
869 # do the right thing when there is only one exist and semi-working solution
871 my $users_obj = RT::Users->new( $self->CurrentUser );
873 FIELD => $rest{SUBKEY},
878 $users_obj->RowsPerPage(2);
879 my @users = @{ $users_obj->ItemsArrayRef };
881 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
884 $uid = $users[0]->id if @users;
886 LEFTJOIN => $group_members,
887 ALIAS => $group_members,
893 ALIAS => $group_members,
900 LEFTJOIN => $group_members,
903 VALUE => "$group_members.MemberId",
906 my $users = $self->Join(
908 ALIAS1 => $group_members,
909 FIELD1 => 'MemberId',
916 FIELD => $rest{SUBKEY},
930 my $group_members = $self->_GroupMembersJoin(
931 GroupsAlias => $groups,
935 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
937 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
938 $self->NewAlias('Users');
940 LEFTJOIN => $group_members,
941 ALIAS => $group_members,
943 VALUE => "$users.id",
948 # we join users table without adding some join condition between tables,
949 # the only conditions we have are conditions on the table iteslf,
950 # for example Users.EmailAddress = 'x'. We should add this condition to
951 # the top level of the query and bundle it with another similar conditions,
952 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
953 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
956 SUBCLAUSE => '_sql_u_watchers_'. $users,
958 FIELD => $rest{'SUBKEY'},
963 # A condition which ties Users and Groups (role groups) is a left join condition
964 # of CachedGroupMembers table. To get correct results of the query we check
965 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
968 ALIAS => $group_members,
970 OPERATOR => 'IS NOT',
977 sub _RoleGroupsJoin {
979 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
980 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
981 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
984 # we always have watcher groups for ticket, so we use INNER join
985 my $groups = $self->Join(
987 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
989 FIELD2 => 'Instance',
990 ENTRYAGGREGATOR => 'AND',
996 VALUE => 'RT::'. $args{'Class'} .'-Role',
1002 VALUE => $args{'Type'},
1005 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1006 unless $args{'New'};
1011 sub _GroupMembersJoin {
1013 my %args = (New => 1, GroupsAlias => undef, @_);
1015 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1016 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1019 my $alias = $self->Join(
1021 ALIAS1 => $args{'GroupsAlias'},
1023 TABLE2 => 'CachedGroupMembers',
1024 FIELD2 => 'GroupId',
1025 ENTRYAGGREGATOR => 'AND',
1028 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1029 unless $args{'New'};
1036 Helper function which provides joins to a watchers table both for limits
1043 my $type = shift || '';
1046 my $groups = $self->_RoleGroupsJoin( Type => $type );
1047 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1048 # XXX: work around, we must hide groups that
1049 # are members of the role group we search in,
1050 # otherwise them result in wrong NULLs in Users
1051 # table and break ordering. Now, we know that
1052 # RT doesn't allow to add groups as members of the
1053 # ticket roles, so we just hide entries in CGM table
1054 # with MemberId == GroupId from results
1055 $self->SUPER::Limit(
1056 LEFTJOIN => $group_members,
1059 VALUE => "$group_members.MemberId",
1062 my $users = $self->Join(
1064 ALIAS1 => $group_members,
1065 FIELD1 => 'MemberId',
1069 return ($groups, $group_members, $users);
1072 =head2 _WatcherMembershipLimit
1074 Handle watcher membership limits, i.e. whether the watcher belongs to a
1075 specific group or not.
1078 1: Field to query on
1080 SELECT DISTINCT main.*
1084 CachedGroupMembers CachedGroupMembers_2,
1087 (main.EffectiveId = main.id)
1089 (main.Status != 'deleted')
1091 (main.Type = 'ticket')
1094 (Users_3.EmailAddress = '22')
1096 (Groups_1.Domain = 'RT::Ticket-Role')
1098 (Groups_1.Type = 'RequestorGroup')
1101 Groups_1.Instance = main.id
1103 Groups_1.id = CachedGroupMembers_2.GroupId
1105 CachedGroupMembers_2.MemberId = Users_3.id
1106 ORDER BY main.id ASC
1111 sub _WatcherMembershipLimit {
1112 my ( $self, $field, $op, $value, @rest ) = @_;
1117 my $groups = $self->NewAlias('Groups');
1118 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1119 my $users = $self->NewAlias('Users');
1120 my $memberships = $self->NewAlias('CachedGroupMembers');
1122 if ( ref $field ) { # gross hack
1123 my @bundle = @$field;
1125 for my $chunk (@bundle) {
1126 ( $field, $op, $value, @rest ) = @$chunk;
1128 ALIAS => $memberships,
1139 ALIAS => $memberships,
1147 # {{{ Tie to groups for tickets we care about
1151 VALUE => 'RT::Ticket-Role',
1152 ENTRYAGGREGATOR => 'AND'
1157 FIELD1 => 'Instance',
1164 # If we care about which sort of watcher
1165 my $meta = $FIELD_METADATA{$field};
1166 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1173 ENTRYAGGREGATOR => 'AND'
1180 ALIAS2 => $groupmembers,
1185 ALIAS1 => $groupmembers,
1186 FIELD1 => 'MemberId',
1192 ALIAS1 => $memberships,
1193 FIELD1 => 'MemberId',
1202 =head2 _CustomFieldDecipher
1204 Try and turn a CF descriptor into (cfid, cfname) object pair.
1208 sub _CustomFieldDecipher {
1209 my ($self, $string) = @_;
1211 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
1212 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1216 my $q = RT::Queue->new( $self->CurrentUser );
1220 # $queue = $q->Name; # should we normalize the queue?
1221 $cf = $q->CustomField( $field );
1224 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1228 elsif ( $field =~ /\D/ ) {
1230 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1231 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1232 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1234 # if there is more then one field the current user can
1235 # see with the same name then we shouldn't return cf object
1236 # as we don't know which one to use
1239 $cf = undef if $cfs->Next;
1243 $cf = RT::CustomField->new( $self->CurrentUser );
1244 $cf->Load( $field );
1247 return ($queue, $field, $cf, $column);
1250 =head2 _CustomFieldJoin
1252 Factor out the Join of custom fields so we can use it for sorting too
1256 sub _CustomFieldJoin {
1257 my ($self, $cfkey, $cfid, $field) = @_;
1258 # Perform one Join per CustomField
1259 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1260 $self->{_sql_cf_alias}{$cfkey} )
1262 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1263 $self->{_sql_cf_alias}{$cfkey} );
1266 my ($TicketCFs, $CFs);
1268 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1272 TABLE2 => 'ObjectCustomFieldValues',
1273 FIELD2 => 'ObjectId',
1275 $self->SUPER::Limit(
1276 LEFTJOIN => $TicketCFs,
1277 FIELD => 'CustomField',
1279 ENTRYAGGREGATOR => 'AND'
1283 my $ocfalias = $self->Join(
1286 TABLE2 => 'ObjectCustomFields',
1287 FIELD2 => 'ObjectId',
1290 $self->SUPER::Limit(
1291 LEFTJOIN => $ocfalias,
1292 ENTRYAGGREGATOR => 'OR',
1293 FIELD => 'ObjectId',
1297 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1299 ALIAS1 => $ocfalias,
1300 FIELD1 => 'CustomField',
1301 TABLE2 => 'CustomFields',
1304 $self->SUPER::Limit(
1306 ENTRYAGGREGATOR => 'AND',
1307 FIELD => 'LookupType',
1308 VALUE => 'RT::Queue-RT::Ticket',
1310 $self->SUPER::Limit(
1312 ENTRYAGGREGATOR => 'AND',
1317 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1321 TABLE2 => 'ObjectCustomFieldValues',
1322 FIELD2 => 'CustomField',
1324 $self->SUPER::Limit(
1325 LEFTJOIN => $TicketCFs,
1326 FIELD => 'ObjectId',
1329 ENTRYAGGREGATOR => 'AND',
1332 $self->SUPER::Limit(
1333 LEFTJOIN => $TicketCFs,
1334 FIELD => 'ObjectType',
1335 VALUE => 'RT::Ticket',
1336 ENTRYAGGREGATOR => 'AND'
1338 $self->SUPER::Limit(
1339 LEFTJOIN => $TicketCFs,
1340 FIELD => 'Disabled',
1343 ENTRYAGGREGATOR => 'AND'
1346 return ($TicketCFs, $CFs);
1349 =head2 _DateCustomFieldLimit
1351 Limit based on CustomFields of type Date
1358 sub _DateCustomFieldLimit {
1359 my ( $self, $_field, $op, $value, %rest ) = @_;
1361 my $field = $rest{'SUBKEY'} || die "No field specified";
1363 # For our sanity, we can only limit on one queue at a time
1365 my ($queue, $cfid, $column);
1366 ($queue, $field, $cfid, $column) = $self->_CustomFieldDecipher( $field );
1368 # If we're trying to find custom fields that don't match something, we
1369 # want tickets where the custom field has no value at all. Note that
1370 # we explicitly don't include the "IS NULL" case, since we would
1371 # otherwise end up with a redundant clause.
1373 my $null_columns_ok;
1374 if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
1375 $null_columns_ok = 1;
1378 my $cfkey = $cfid ? $cfid : "$queue.$field";
1379 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1383 if ( $CFs && !$cfid ) {
1384 $self->SUPER::Limit(
1388 ENTRYAGGREGATOR => 'AND',
1392 $self->_OpenParen if $null_columns_ok;
1394 my $date = RT::Date->new( $self->CurrentUser );
1395 $date->Set( Format => 'unknown', Value => $value );
1399 # if we're specifying =, that means we want everything on a
1400 # particular single day. in the database, we need to check for >
1401 # and < the edges of that day.
1403 $date->SetToMidnight( Timezone => 'server' );
1404 my $daystart = $date->ISO;
1406 my $dayend = $date->ISO;
1411 ALIAS => $TicketCFs,
1419 ALIAS => $TicketCFs,
1424 ENTRYAGGREGATOR => 'AND',
1432 ALIAS => $TicketCFs,
1435 VALUE => $date->ISO,
1444 =head2 _CustomFieldLimit
1446 Limit based on CustomFields
1453 sub _CustomFieldLimit {
1454 my ( $self, $_field, $op, $value, %rest ) = @_;
1456 my $field = $rest{'SUBKEY'} || die "No field specified";
1458 # For our sanity, we can only limit on one queue at a time
1460 my ($queue, $cfid, $cf, $column);
1461 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1462 $cfid = $cf ? $cf->id : 0 ;
1464 # If we're trying to find custom fields that don't match something, we
1465 # want tickets where the custom field has no value at all. Note that
1466 # we explicitly don't include the "IS NULL" case, since we would
1467 # otherwise end up with a redundant clause.
1469 my ($negative_op, $null_op, $inv_op, $range_op)
1470 = $self->ClassifySQLOperation( $op );
1474 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1475 return 'MATCHES' if $op eq '=';
1476 return 'NOT MATCHES' if $op eq '!=';
1480 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1482 my $cfkey = $cfid ? $cfid : "$queue.$field";
1484 if ( $null_op && !$column ) {
1485 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1486 # we can reuse our default joins for this operation
1487 # with column specified we have different situation
1488 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1491 ALIAS => $TicketCFs,
1500 OPERATOR => 'IS NOT',
1503 ENTRYAGGREGATOR => 'AND',
1507 elsif ( !$negative_op || $single_value ) {
1508 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1509 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1516 # if column is defined then deal only with it
1517 # otherwise search in Content and in LargeContent
1520 ALIAS => $TicketCFs,
1522 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1527 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1528 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1530 ALIAS => $TicketCFs,
1539 ALIAS => $TicketCFs,
1543 ENTRYAGGREGATOR => 'OR'
1546 ALIAS => $TicketCFs,
1550 ENTRYAGGREGATOR => 'OR'
1554 ALIAS => $TicketCFs,
1555 FIELD => 'LargeContent',
1556 OPERATOR => $fix_op->($op),
1558 ENTRYAGGREGATOR => 'AND',
1564 ALIAS => $TicketCFs,
1574 ALIAS => $TicketCFs,
1578 ENTRYAGGREGATOR => 'OR'
1581 ALIAS => $TicketCFs,
1585 ENTRYAGGREGATOR => 'OR'
1589 ALIAS => $TicketCFs,
1590 FIELD => 'LargeContent',
1591 OPERATOR => $fix_op->($op),
1593 ENTRYAGGREGATOR => 'AND',
1599 # XXX: if we join via CustomFields table then
1600 # because of order of left joins we get NULLs in
1601 # CF table and then get nulls for those records
1602 # in OCFVs table what result in wrong results
1603 # as decifer method now tries to load a CF then
1604 # we fall into this situation only when there
1605 # are more than one CF with the name in the DB.
1606 # the same thing applies to order by call.
1607 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1608 # we want treat IS NULL as (not applies or has
1613 OPERATOR => 'IS NOT',
1616 ENTRYAGGREGATOR => 'AND',
1622 ALIAS => $TicketCFs,
1623 FIELD => $column || 'Content',
1627 ENTRYAGGREGATOR => 'OR',
1634 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1635 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1638 $op =~ s/!|NOT\s+//i;
1640 # if column is defined then deal only with it
1641 # otherwise search in Content and in LargeContent
1643 $self->SUPER::Limit(
1644 LEFTJOIN => $TicketCFs,
1645 ALIAS => $TicketCFs,
1647 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1652 $self->SUPER::Limit(
1653 LEFTJOIN => $TicketCFs,
1654 ALIAS => $TicketCFs,
1662 ALIAS => $TicketCFs,
1671 sub _HasAttributeLimit {
1672 my ( $self, $field, $op, $value, %rest ) = @_;
1674 my $alias = $self->Join(
1678 TABLE2 => 'Attributes',
1679 FIELD2 => 'ObjectId',
1681 $self->SUPER::Limit(
1683 FIELD => 'ObjectType',
1684 VALUE => 'RT::Ticket',
1685 ENTRYAGGREGATOR => 'AND'
1687 $self->SUPER::Limit(
1692 ENTRYAGGREGATOR => 'AND'
1698 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1704 # End Helper Functions
1706 # End of SQL Stuff -------------------------------------------------
1708 # {{{ Allow sorting on watchers
1710 =head2 OrderByCols ARRAY
1712 A modified version of the OrderBy method which automatically joins where
1713 C<ALIAS> is set to the name of a watcher type.
1724 foreach my $row (@args) {
1725 if ( $row->{ALIAS} ) {
1729 if ( $row->{FIELD} !~ /\./ ) {
1730 my $meta = $self->FIELDS->{ $row->{FIELD} };
1736 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1737 my $alias = $self->Join(
1740 FIELD1 => $row->{'FIELD'},
1744 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1745 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1746 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1748 my $alias = $self->Join(
1751 FIELD1 => $row->{'FIELD'},
1755 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1762 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1763 my $meta = $self->FIELDS->{$field};
1764 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1765 # cache alias as we want to use one alias per watcher type for sorting
1766 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1768 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1769 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1771 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1772 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1773 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1774 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1775 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1776 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1777 # this is described in _CustomFieldLimit
1781 OPERATOR => 'IS NOT',
1784 ENTRYAGGREGATOR => 'AND',
1787 # For those cases where we are doing a join against the
1788 # CF name, and don't have a CFid, use Unique to make sure
1789 # we don't show duplicate tickets. NOTE: I'm pretty sure
1790 # this will stay mixed in for the life of the
1791 # class/package, and not just for the life of the object.
1792 # Potential performance issue.
1793 require DBIx::SearchBuilder::Unique;
1794 DBIx::SearchBuilder::Unique->import;
1796 my $CFvs = $self->Join(
1798 ALIAS1 => $TicketCFs,
1799 FIELD1 => 'CustomField',
1800 TABLE2 => 'CustomFieldValues',
1801 FIELD2 => 'CustomField',
1803 $self->SUPER::Limit(
1807 VALUE => $TicketCFs . ".Content",
1808 ENTRYAGGREGATOR => 'AND'
1811 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1812 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1813 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1814 # PAW logic is "reversed"
1816 if (exists $row->{ORDER} ) {
1817 my $o = $row->{ORDER};
1818 delete $row->{ORDER};
1819 $order = "DESC" if $o =~ /asc/i;
1822 # Ticket.Owner 1 0 X
1823 # Unowned Tickets 0 1 X
1826 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1827 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1828 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1829 push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
1831 push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
1835 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1837 } elsif ( $field eq 'Customer' ) { #Freeside
1838 if ( $subkey eq 'Number' ) {
1839 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1842 FIELD => $custnum_sql,
1846 my $custalias = $self->JoinToCustomer;
1848 if ( $subkey eq 'Name' ) {
1849 $field = "COALESCE( $custalias.company,
1850 $custalias.last || ', ' || $custalias.first
1854 # no other cases exist yet, but for obviousness:
1857 push @res, { %$row, ALIAS => '', FIELD => $field };
1866 return $self->SUPER::OrderByCols(@res);
1871 sub JoinToCustLinks {
1872 # Set up join to links (id = localbase),
1873 # limit link type to 'MemberOf',
1874 # and target value to any Freeside custnum URI.
1875 # Return the linkalias for further join/limit action,
1876 # and an sql expression to retrieve the custnum.
1878 my $linkalias = $self->Join(
1883 FIELD2 => 'LocalBase',
1886 $self->SUPER::Limit(
1887 LEFTJOIN => $linkalias,
1890 VALUE => 'MemberOf',
1892 $self->SUPER::Limit(
1893 LEFTJOIN => $linkalias,
1895 OPERATOR => 'STARTSWITH',
1896 VALUE => 'freeside://freeside/cust_main/',
1898 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1899 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1900 $custnum_sql .= 'SIGNED INTEGER)';
1903 $custnum_sql .= 'INTEGER)';
1905 return ($linkalias, $custnum_sql);
1908 sub JoinToCustomer {
1910 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1912 my $custalias = $self->Join(
1914 EXPRESSION => $custnum_sql,
1915 TABLE2 => 'cust_main',
1916 FIELD2 => 'custnum',
1921 sub _FreesideFieldLimit {
1922 my ( $self, $field, $op, $value, %rest ) = @_;
1923 my $alias = $self->JoinToCustomer;
1924 my $is_negative = 0;
1925 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1926 # if the op is negative, do the join as though
1927 # the op were positive, then accept only records
1928 # where the right-side join key is null.
1930 $op = '=' if $op eq '!=';
1933 my $meta = $FIELD_METADATA{$field};
1935 $alias = $self->Join(
1938 FIELD1 => 'custnum',
1939 TABLE2 => $meta->[1],
1940 FIELD2 => 'custnum',
1944 $self->SUPER::Limit(
1946 FIELD => lc($field),
1949 ENTRYAGGREGATOR => 'AND',
1954 FIELD => lc($field),
1955 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1965 # {{{ Limit the result set based on content
1971 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1972 Generally best called from LimitFoo methods
1982 DESCRIPTION => undef,
1985 $args{'DESCRIPTION'} = $self->loc(
1986 "[_1] [_2] [_3]", $args{'FIELD'},
1987 $args{'OPERATOR'}, $args{'VALUE'}
1989 if ( !defined $args{'DESCRIPTION'} );
1991 my $index = $self->_NextIndex;
1993 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1995 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1997 $self->{'RecalcTicketLimits'} = 1;
1999 # If we're looking at the effective id, we don't want to append the other clause
2000 # which limits us to tickets where id = effective id
2001 if ( $args{'FIELD'} eq 'EffectiveId'
2002 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2004 $self->{'looking_at_effective_id'} = 1;
2007 if ( $args{'FIELD'} eq 'Type'
2008 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2010 $self->{'looking_at_type'} = 1;
2020 Returns a frozen string suitable for handing back to ThawLimits.
2024 sub _FreezeThawKeys {
2025 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2029 # {{{ sub FreezeLimits
2034 require MIME::Base64;
2035 MIME::Base64::base64_encode(
2036 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2043 Take a frozen Limits string generated by FreezeLimits and make this tickets
2044 object have that set of limits.
2048 # {{{ sub ThawLimits
2054 #if we don't have $in, get outta here.
2055 return undef unless ($in);
2057 $self->{'RecalcTicketLimits'} = 1;
2060 require MIME::Base64;
2062 #We don't need to die if the thaw fails.
2063 @{$self}{ $self->_FreezeThawKeys }
2064 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2066 $RT::Logger->error($@) if $@;
2072 # {{{ Limit by enum or foreign key
2074 # {{{ sub LimitQueue
2078 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2079 OPERATOR is one of = or !=. (It defaults to =).
2080 VALUE is a queue id or Name.
2093 #TODO VALUE should also take queue objects
2094 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2095 my $queue = new RT::Queue( $self->CurrentUser );
2096 $queue->Load( $args{'VALUE'} );
2097 $args{'VALUE'} = $queue->Id;
2100 # What if they pass in an Id? Check for isNum() and convert to
2103 #TODO check for a valid queue here
2107 VALUE => $args{'VALUE'},
2108 OPERATOR => $args{'OPERATOR'},
2109 DESCRIPTION => join(
2110 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2118 # {{{ sub LimitStatus
2122 Takes a paramhash with the fields OPERATOR and VALUE.
2123 OPERATOR is one of = or !=.
2126 RT adds Status != 'deleted' until object has
2127 allow_deleted_search internal property set.
2128 $tickets->{'allow_deleted_search'} = 1;
2129 $tickets->LimitStatus( VALUE => 'deleted' );
2141 VALUE => $args{'VALUE'},
2142 OPERATOR => $args{'OPERATOR'},
2143 DESCRIPTION => join( ' ',
2144 $self->loc('Status'), $args{'OPERATOR'},
2145 $self->loc( $args{'VALUE'} ) ),
2151 # {{{ sub IgnoreType
2155 If called, this search will not automatically limit the set of results found
2156 to tickets of type "Ticket". Tickets of other types, such as "project" and
2157 "approval" will be found.
2164 # Instead of faking a Limit that later gets ignored, fake up the
2165 # fact that we're already looking at type, so that the check in
2166 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2168 # $self->LimitType(VALUE => '__any');
2169 $self->{looking_at_type} = 1;
2178 Takes a paramhash with the fields OPERATOR and VALUE.
2179 OPERATOR is one of = or !=, it defaults to "=".
2180 VALUE is a string to search for in the type of the ticket.
2195 VALUE => $args{'VALUE'},
2196 OPERATOR => $args{'OPERATOR'},
2197 DESCRIPTION => join( ' ',
2198 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2206 # {{{ Limit by string field
2208 # {{{ sub LimitSubject
2212 Takes a paramhash with the fields OPERATOR and VALUE.
2213 OPERATOR is one of = or !=.
2214 VALUE is a string to search for in the subject of the ticket.
2223 VALUE => $args{'VALUE'},
2224 OPERATOR => $args{'OPERATOR'},
2225 DESCRIPTION => join( ' ',
2226 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2234 # {{{ Limit based on ticket numerical attributes
2235 # Things that can be > < = !=
2241 Takes a paramhash with the fields OPERATOR and VALUE.
2242 OPERATOR is one of =, >, < or !=.
2243 VALUE is a ticket Id to search for
2256 VALUE => $args{'VALUE'},
2257 OPERATOR => $args{'OPERATOR'},
2259 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2265 # {{{ sub LimitPriority
2267 =head2 LimitPriority
2269 Takes a paramhash with the fields OPERATOR and VALUE.
2270 OPERATOR is one of =, >, < or !=.
2271 VALUE is a value to match the ticket\'s priority against
2279 FIELD => 'Priority',
2280 VALUE => $args{'VALUE'},
2281 OPERATOR => $args{'OPERATOR'},
2282 DESCRIPTION => join( ' ',
2283 $self->loc('Priority'),
2284 $args{'OPERATOR'}, $args{'VALUE'}, ),
2290 # {{{ sub LimitInitialPriority
2292 =head2 LimitInitialPriority
2294 Takes a paramhash with the fields OPERATOR and VALUE.
2295 OPERATOR is one of =, >, < or !=.
2296 VALUE is a value to match the ticket\'s initial priority against
2301 sub LimitInitialPriority {
2305 FIELD => 'InitialPriority',
2306 VALUE => $args{'VALUE'},
2307 OPERATOR => $args{'OPERATOR'},
2308 DESCRIPTION => join( ' ',
2309 $self->loc('Initial Priority'), $args{'OPERATOR'},
2316 # {{{ sub LimitFinalPriority
2318 =head2 LimitFinalPriority
2320 Takes a paramhash with the fields OPERATOR and VALUE.
2321 OPERATOR is one of =, >, < or !=.
2322 VALUE is a value to match the ticket\'s final priority against
2326 sub LimitFinalPriority {
2330 FIELD => 'FinalPriority',
2331 VALUE => $args{'VALUE'},
2332 OPERATOR => $args{'OPERATOR'},
2333 DESCRIPTION => join( ' ',
2334 $self->loc('Final Priority'), $args{'OPERATOR'},
2341 # {{{ sub LimitTimeWorked
2343 =head2 LimitTimeWorked
2345 Takes a paramhash with the fields OPERATOR and VALUE.
2346 OPERATOR is one of =, >, < or !=.
2347 VALUE is a value to match the ticket's TimeWorked attribute
2351 sub LimitTimeWorked {
2355 FIELD => 'TimeWorked',
2356 VALUE => $args{'VALUE'},
2357 OPERATOR => $args{'OPERATOR'},
2358 DESCRIPTION => join( ' ',
2359 $self->loc('Time Worked'),
2360 $args{'OPERATOR'}, $args{'VALUE'}, ),
2366 # {{{ sub LimitTimeLeft
2368 =head2 LimitTimeLeft
2370 Takes a paramhash with the fields OPERATOR and VALUE.
2371 OPERATOR is one of =, >, < or !=.
2372 VALUE is a value to match the ticket's TimeLeft attribute
2380 FIELD => 'TimeLeft',
2381 VALUE => $args{'VALUE'},
2382 OPERATOR => $args{'OPERATOR'},
2383 DESCRIPTION => join( ' ',
2384 $self->loc('Time Left'),
2385 $args{'OPERATOR'}, $args{'VALUE'}, ),
2393 # {{{ Limiting based on attachment attributes
2395 # {{{ sub LimitContent
2399 Takes a paramhash with the fields OPERATOR and VALUE.
2400 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2401 VALUE is a string to search for in the body of the ticket
2410 VALUE => $args{'VALUE'},
2411 OPERATOR => $args{'OPERATOR'},
2412 DESCRIPTION => join( ' ',
2413 $self->loc('Ticket content'), $args{'OPERATOR'},
2420 # {{{ sub LimitFilename
2422 =head2 LimitFilename
2424 Takes a paramhash with the fields OPERATOR and VALUE.
2425 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2426 VALUE is a string to search for in the body of the ticket
2434 FIELD => 'Filename',
2435 VALUE => $args{'VALUE'},
2436 OPERATOR => $args{'OPERATOR'},
2437 DESCRIPTION => join( ' ',
2438 $self->loc('Attachment filename'), $args{'OPERATOR'},
2444 # {{{ sub LimitContentType
2446 =head2 LimitContentType
2448 Takes a paramhash with the fields OPERATOR and VALUE.
2449 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2450 VALUE is a content type to search ticket attachments for
2454 sub LimitContentType {
2458 FIELD => 'ContentType',
2459 VALUE => $args{'VALUE'},
2460 OPERATOR => $args{'OPERATOR'},
2461 DESCRIPTION => join( ' ',
2462 $self->loc('Ticket content type'), $args{'OPERATOR'},
2471 # {{{ Limiting based on people
2473 # {{{ sub LimitOwner
2477 Takes a paramhash with the fields OPERATOR and VALUE.
2478 OPERATOR is one of = or !=.
2490 my $owner = new RT::User( $self->CurrentUser );
2491 $owner->Load( $args{'VALUE'} );
2493 # FIXME: check for a valid $owner
2496 VALUE => $args{'VALUE'},
2497 OPERATOR => $args{'OPERATOR'},
2498 DESCRIPTION => join( ' ',
2499 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2506 # {{{ Limiting watchers
2508 # {{{ sub LimitWatcher
2512 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2513 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2514 VALUE is a value to match the ticket\'s watcher email addresses against
2515 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2529 #build us up a description
2530 my ( $watcher_type, $desc );
2531 if ( $args{'TYPE'} ) {
2532 $watcher_type = $args{'TYPE'};
2535 $watcher_type = "Watcher";
2539 FIELD => $watcher_type,
2540 VALUE => $args{'VALUE'},
2541 OPERATOR => $args{'OPERATOR'},
2542 TYPE => $args{'TYPE'},
2543 DESCRIPTION => join( ' ',
2544 $self->loc($watcher_type),
2545 $args{'OPERATOR'}, $args{'VALUE'}, ),
2555 # {{{ Limiting based on links
2559 =head2 LimitLinkedTo
2561 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2562 TYPE limits the sort of link we want to search on
2564 TYPE = { RefersTo, MemberOf, DependsOn }
2566 TARGET is the id or URI of the TARGET of the link
2580 FIELD => 'LinkedTo',
2582 TARGET => $args{'TARGET'},
2583 TYPE => $args{'TYPE'},
2584 DESCRIPTION => $self->loc(
2585 "Tickets [_1] by [_2]",
2586 $self->loc( $args{'TYPE'} ),
2589 OPERATOR => $args{'OPERATOR'},
2595 # {{{ LimitLinkedFrom
2597 =head2 LimitLinkedFrom
2599 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2600 TYPE limits the sort of link we want to search on
2603 BASE is the id or URI of the BASE of the link
2607 sub LimitLinkedFrom {
2616 # translate RT2 From/To naming to RT3 TicketSQL naming
2617 my %fromToMap = qw(DependsOn DependentOn
2619 RefersTo ReferredToBy);
2621 my $type = $args{'TYPE'};
2622 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2625 FIELD => 'LinkedTo',
2627 BASE => $args{'BASE'},
2629 DESCRIPTION => $self->loc(
2630 "Tickets [_1] [_2]",
2631 $self->loc( $args{'TYPE'} ),
2634 OPERATOR => $args{'OPERATOR'},
2643 my $ticket_id = shift;
2644 return $self->LimitLinkedTo(
2646 TARGET => $ticket_id,
2653 # {{{ LimitHasMember
2654 sub LimitHasMember {
2656 my $ticket_id = shift;
2657 return $self->LimitLinkedFrom(
2659 BASE => "$ticket_id",
2660 TYPE => 'HasMember',
2667 # {{{ LimitDependsOn
2669 sub LimitDependsOn {
2671 my $ticket_id = shift;
2672 return $self->LimitLinkedTo(
2674 TARGET => $ticket_id,
2675 TYPE => 'DependsOn',
2682 # {{{ LimitDependedOnBy
2684 sub LimitDependedOnBy {
2686 my $ticket_id = shift;
2687 return $self->LimitLinkedFrom(
2690 TYPE => 'DependentOn',
2701 my $ticket_id = shift;
2702 return $self->LimitLinkedTo(
2704 TARGET => $ticket_id,
2712 # {{{ LimitReferredToBy
2714 sub LimitReferredToBy {
2716 my $ticket_id = shift;
2717 return $self->LimitLinkedFrom(
2720 TYPE => 'ReferredToBy',
2728 # {{{ limit based on ticket date attribtes
2732 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2734 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2736 OPERATOR is one of > or <
2737 VALUE is a date and time in ISO format in GMT
2738 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2740 There are also helper functions of the form LimitFIELD that eliminate
2741 the need to pass in a FIELD argument.
2755 #Set the description if we didn't get handed it above
2756 unless ( $args{'DESCRIPTION'} ) {
2757 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2758 . $args{'OPERATOR'} . " "
2759 . $args{'VALUE'} . " GMT";
2762 $self->Limit(%args);
2770 $self->LimitDate( FIELD => 'Created', @_ );
2775 $self->LimitDate( FIELD => 'Due', @_ );
2781 $self->LimitDate( FIELD => 'Starts', @_ );
2787 $self->LimitDate( FIELD => 'Started', @_ );
2792 $self->LimitDate( FIELD => 'Resolved', @_ );
2797 $self->LimitDate( FIELD => 'Told', @_ );
2800 sub LimitLastUpdated {
2802 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2806 # {{{ sub LimitTransactionDate
2808 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2810 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2812 OPERATOR is one of > or <
2813 VALUE is a date and time in ISO format in GMT
2818 sub LimitTransactionDate {
2821 FIELD => 'TransactionDate',
2828 # <20021217042756.GK28744@pallas.fsck.com>
2829 # "Kill It" - Jesse.
2831 #Set the description if we didn't get handed it above
2832 unless ( $args{'DESCRIPTION'} ) {
2833 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2834 . $args{'OPERATOR'} . " "
2835 . $args{'VALUE'} . " GMT";
2838 $self->Limit(%args);
2846 # {{{ Limit based on custom fields
2847 # {{{ sub LimitCustomField
2849 =head2 LimitCustomField
2851 Takes a paramhash of key/value pairs with the following keys:
2855 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2857 =item OPERATOR - The usual Limit operators
2859 =item VALUE - The value to compare against
2865 sub LimitCustomField {
2869 CUSTOMFIELD => undef,
2871 DESCRIPTION => undef,
2872 FIELD => 'CustomFieldValue',
2877 my $CF = RT::CustomField->new( $self->CurrentUser );
2878 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2879 $CF->Load( $args{CUSTOMFIELD} );
2882 $CF->LoadByNameAndQueue(
2883 Name => $args{CUSTOMFIELD},
2884 Queue => $args{QUEUE}
2886 $args{CUSTOMFIELD} = $CF->Id;
2889 # Handle special customfields types
2890 if ($CF->Type eq 'Date') {
2891 $args{FIELD} = 'DateCustomFieldValue';
2894 #If we are looking to compare with a null value.
2895 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2896 $args{'DESCRIPTION'}
2897 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2899 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2900 $args{'DESCRIPTION'}
2901 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2904 # if we're not looking to compare with a null value
2906 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2907 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2910 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2911 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2912 $QueueObj->Load( $args{'QUEUE'} );
2913 $args{'QUEUE'} = $QueueObj->Id;
2915 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2918 @rest = ( ENTRYAGGREGATOR => 'AND' )
2919 if ( $CF->Type eq 'SelectMultiple' );
2922 VALUE => $args{VALUE},
2924 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2925 .".{" . $CF->Name . "}",
2926 OPERATOR => $args{OPERATOR},
2931 $self->{'RecalcTicketLimits'} = 1;
2937 # {{{ sub _NextIndex
2941 Keep track of the counter for the array of restrictions
2947 return ( $self->{'restriction_index'}++ );
2954 # {{{ Core bits to make this a DBIx::SearchBuilder object
2959 $self->{'table'} = "Tickets";
2960 $self->{'RecalcTicketLimits'} = 1;
2961 $self->{'looking_at_effective_id'} = 0;
2962 $self->{'looking_at_type'} = 0;
2963 $self->{'restriction_index'} = 1;
2964 $self->{'primary_key'} = "id";
2965 delete $self->{'items_array'};
2966 delete $self->{'item_map'};
2967 delete $self->{'columns_to_display'};
2968 $self->SUPER::_Init(@_);
2979 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2980 return ( $self->SUPER::Count() );
2988 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2989 return ( $self->SUPER::CountAll() );
2994 # {{{ sub ItemsArrayRef
2996 =head2 ItemsArrayRef
2998 Returns a reference to the set of all items found in this search
3005 return $self->{'items_array'} if $self->{'items_array'};
3007 my $placeholder = $self->_ItemsCounter;
3008 $self->GotoFirstItem();
3009 while ( my $item = $self->Next ) {
3010 push( @{ $self->{'items_array'} }, $item );
3012 $self->GotoItem($placeholder);
3013 $self->{'items_array'}
3014 = $self->ItemsOrderBy( $self->{'items_array'} );
3016 return $self->{'items_array'};
3019 sub ItemsArrayRefWindow {
3023 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3025 $self->RowsPerPage( $window );
3027 $self->GotoFirstItem;
3030 while ( my $item = $self->Next ) {
3034 $self->RowsPerPage( $old[1] );
3035 $self->FirstRow( $old[2] );
3036 $self->GotoItem( $old[0] );
3047 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3049 my $Ticket = $self->SUPER::Next;
3050 return $Ticket unless $Ticket;
3052 if ( $Ticket->__Value('Status') eq 'deleted'
3053 && !$self->{'allow_deleted_search'} )
3057 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3058 # if we found a ticket with this option enabled then
3059 # all tickets we found are ACLed, cache this fact
3060 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3061 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3064 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3069 # If the user doesn't have the right to show this ticket
3076 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3077 return $self->SUPER::_DoSearch( @_ );
3082 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3083 return $self->SUPER::_DoCount( @_ );
3089 my $cache_key = 'RolesHasRight;:;ShowTicket';
3091 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3095 my $ACL = RT::ACL->new( $RT::SystemUser );
3096 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3097 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3098 my $principal_alias = $ACL->Join(
3100 FIELD1 => 'PrincipalId',
3101 TABLE2 => 'Principals',
3104 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3107 while ( my $ACE = $ACL->Next ) {
3108 my $role = $ACE->PrincipalType;
3109 my $type = $ACE->ObjectType;
3110 if ( $type eq 'RT::System' ) {
3113 elsif ( $type eq 'RT::Queue' ) {
3114 next if $res{ $role } && !ref $res{ $role };
3115 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3118 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3121 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3125 sub _DirectlyCanSeeIn {
3127 my $id = $self->CurrentUser->id;
3129 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3130 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3134 my $ACL = RT::ACL->new( $RT::SystemUser );
3135 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3136 my $principal_alias = $ACL->Join(
3138 FIELD1 => 'PrincipalId',
3139 TABLE2 => 'Principals',
3142 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3143 my $cgm_alias = $ACL->Join(
3145 FIELD1 => 'PrincipalId',
3146 TABLE2 => 'CachedGroupMembers',
3147 FIELD2 => 'GroupId',
3149 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3150 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3153 while ( my $ACE = $ACL->Next ) {
3154 my $type = $ACE->ObjectType;
3155 if ( $type eq 'RT::System' ) {
3156 # If user is direct member of a group that has the right
3157 # on the system then he can see any ticket
3158 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3161 elsif ( $type eq 'RT::Queue' ) {
3162 push @res, $ACE->ObjectId;
3165 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3168 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3172 sub CurrentUserCanSee {
3174 return if $self->{'_sql_current_user_can_see_applied'};
3176 return $self->{'_sql_current_user_can_see_applied'} = 1
3177 if $self->CurrentUser->UserObj->HasRight(
3178 Right => 'SuperUser', Object => $RT::System
3181 my $id = $self->CurrentUser->id;
3183 # directly can see in all queues then we have nothing to do
3184 my @direct_queues = $self->_DirectlyCanSeeIn;
3185 return $self->{'_sql_current_user_can_see_applied'} = 1
3186 if @direct_queues && $direct_queues[0] == -1;
3188 my %roles = $self->_RolesCanSee;
3190 my %skip = map { $_ => 1 } @direct_queues;
3191 foreach my $role ( keys %roles ) {
3192 next unless ref $roles{ $role };
3194 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3196 $roles{ $role } = \@queues;
3198 delete $roles{ $role };
3203 # there is no global watchers, only queues and tickes, if at
3204 # some point we will add global roles then it's gonna blow
3205 # the idea here is that if the right is set globaly for a role
3206 # and user plays this role for a queue directly not a ticket
3207 # then we have to check in advance
3208 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3210 my $groups = RT::Groups->new( $RT::SystemUser );
3211 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3213 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3215 my $principal_alias = $groups->Join(
3218 TABLE2 => 'Principals',
3221 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3222 my $cgm_alias = $groups->Join(
3225 TABLE2 => 'CachedGroupMembers',
3226 FIELD2 => 'GroupId',
3228 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3229 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3230 while ( my $group = $groups->Next ) {
3231 push @direct_queues, $group->Instance;
3235 unless ( @direct_queues || keys %roles ) {
3236 $self->SUPER::Limit(
3241 ENTRYAGGREGATOR => 'AND',
3243 return $self->{'_sql_current_user_can_see_applied'} = 1;
3247 my $join_roles = keys %roles;
3248 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3249 my ($role_group_alias, $cgm_alias);
3250 if ( $join_roles ) {
3251 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3252 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3253 $self->SUPER::Limit(
3254 LEFTJOIN => $cgm_alias,
3255 FIELD => 'MemberId',
3260 my $limit_queues = sub {
3264 return unless @queues;
3265 if ( @queues == 1 ) {
3266 $self->SUPER::Limit(
3271 ENTRYAGGREGATOR => $ea,
3274 $self->SUPER::_OpenParen('ACL');
3275 foreach my $q ( @queues ) {
3276 $self->SUPER::Limit(
3281 ENTRYAGGREGATOR => $ea,
3285 $self->SUPER::_CloseParen('ACL');
3290 $self->SUPER::_OpenParen('ACL');
3292 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3293 while ( my ($role, $queues) = each %roles ) {
3294 $self->SUPER::_OpenParen('ACL');
3295 if ( $role eq 'Owner' ) {
3296 $self->SUPER::Limit(
3300 ENTRYAGGREGATOR => $ea,
3304 $self->SUPER::Limit(
3306 ALIAS => $cgm_alias,
3307 FIELD => 'MemberId',
3308 OPERATOR => 'IS NOT',
3311 ENTRYAGGREGATOR => $ea,
3313 $self->SUPER::Limit(
3315 ALIAS => $role_group_alias,
3318 ENTRYAGGREGATOR => 'AND',
3321 $limit_queues->( 'AND', @$queues ) if ref $queues;
3322 $ea = 'OR' if $ea eq 'AND';
3323 $self->SUPER::_CloseParen('ACL');
3325 $self->SUPER::_CloseParen('ACL');
3327 return $self->{'_sql_current_user_can_see_applied'} = 1;
3334 # {{{ Deal with storing and restoring restrictions
3336 # {{{ sub LoadRestrictions
3338 =head2 LoadRestrictions
3340 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3341 TODO It is not yet implemented
3347 # {{{ sub DescribeRestrictions
3349 =head2 DescribeRestrictions
3352 Returns a hash keyed by restriction id.
3353 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3354 is a description of the purpose of that TicketRestriction
3358 sub DescribeRestrictions {
3361 my ( $row, %listing );
3363 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3364 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3371 # {{{ sub RestrictionValues
3373 =head2 RestrictionValues FIELD
3375 Takes a restriction field and returns a list of values this field is restricted
3380 sub RestrictionValues {
3383 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3384 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3385 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3387 keys %{ $self->{'TicketRestrictions'} };
3392 # {{{ sub ClearRestrictions
3394 =head2 ClearRestrictions
3396 Removes all restrictions irretrievably
3400 sub ClearRestrictions {
3402 delete $self->{'TicketRestrictions'};
3403 $self->{'looking_at_effective_id'} = 0;
3404 $self->{'looking_at_type'} = 0;
3405 $self->{'RecalcTicketLimits'} = 1;
3410 # {{{ sub DeleteRestriction
3412 =head2 DeleteRestriction
3414 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3415 Removes that restriction from the session's limits.
3419 sub DeleteRestriction {
3422 delete $self->{'TicketRestrictions'}{$row};
3424 $self->{'RecalcTicketLimits'} = 1;
3426 #make the underlying easysearch object forget all its preconceptions
3431 # {{{ sub _RestrictionsToClauses
3433 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3435 sub _RestrictionsToClauses {
3440 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3441 my $restriction = $self->{'TicketRestrictions'}{$row};
3443 # We need to reimplement the subclause aggregation that SearchBuilder does.
3444 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3445 # Then SB AND's the different Subclauses together.
3447 # So, we want to group things into Subclauses, convert them to
3448 # SQL, and then join them with the appropriate DefaultEA.
3449 # Then join each subclause group with AND.
3451 my $field = $restriction->{'FIELD'};
3452 my $realfield = $field; # CustomFields fake up a fieldname, so
3453 # we need to figure that out
3456 # Rewrite LinkedTo meta field to the real field
3457 if ( $field =~ /LinkedTo/ ) {
3458 $realfield = $field = $restriction->{'TYPE'};
3462 # Handle subkey fields with a different real field
3463 if ( $field =~ /^(\w+)\./ ) {
3467 die "I don't know about $field yet"
3468 unless ( exists $FIELD_METADATA{$realfield}
3469 or $restriction->{CUSTOMFIELD} );
3471 my $type = $FIELD_METADATA{$realfield}->[0];
3472 my $op = $restriction->{'OPERATOR'};
3476 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3479 # this performs the moral equivalent of defined or/dor/C<//>,
3480 # without the short circuiting.You need to use a 'defined or'
3481 # type thing instead of just checking for truth values, because
3482 # VALUE could be 0.(i.e. "false")
3484 # You could also use this, but I find it less aesthetic:
3485 # (although it does short circuit)
3486 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3487 # defined $restriction->{'TICKET'} ?
3488 # $restriction->{TICKET} :
3489 # defined $restriction->{'BASE'} ?
3490 # $restriction->{BASE} :
3491 # defined $restriction->{'TARGET'} ?
3492 # $restriction->{TARGET} )
3494 my $ea = $restriction->{ENTRYAGGREGATOR}
3495 || $DefaultEA{$type}
3498 die "Invalid operator $op for $field ($type)"
3499 unless exists $ea->{$op};
3503 # Each CustomField should be put into a different Clause so they
3504 # are ANDed together.
3505 if ( $restriction->{CUSTOMFIELD} ) {
3506 $realfield = $field;
3509 exists $clause{$realfield} or $clause{$realfield} = [];
3512 $field =~ s!(['"])!\\$1!g;
3513 $value =~ s!(['"])!\\$1!g;
3514 my $data = [ $ea, $type, $field, $op, $value ];
3516 # here is where we store extra data, say if it's a keyword or
3517 # something. (I.e. "TYPE SPECIFIC STUFF")
3519 push @{ $clause{$realfield} }, $data;
3526 # {{{ sub _ProcessRestrictions
3528 =head2 _ProcessRestrictions PARAMHASH
3530 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3531 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3535 sub _ProcessRestrictions {
3538 #Blow away ticket aliases since we'll need to regenerate them for
3540 delete $self->{'TicketAliases'};
3541 delete $self->{'items_array'};
3542 delete $self->{'item_map'};
3543 delete $self->{'raw_rows'};
3544 delete $self->{'rows'};
3545 delete $self->{'count_all'};
3547 my $sql = $self->Query; # Violating the _SQL namespace
3548 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3550 # "Restrictions to Clauses Branch\n";
3551 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3553 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3557 $sql = $self->ClausesToSQL($clauseRef);
3558 $self->FromSQL($sql) if $sql;
3562 $self->{'RecalcTicketLimits'} = 0;
3566 =head2 _BuildItemMap
3568 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3569 display search nav quickly.
3576 my $window = RT->Config->Get('TicketsItemMapSize');
3578 $self->{'item_map'} = {};
3580 my $items = $self->ItemsArrayRefWindow( $window );
3581 return unless $items && @$items;
3584 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3585 for ( my $i = 0; $i < @$items; $i++ ) {
3586 my $item = $items->[$i];
3587 my $id = $item->EffectiveId;
3588 $self->{'item_map'}{$id}{'defined'} = 1;
3589 $self->{'item_map'}{$id}{'prev'} = $prev;
3590 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3594 $self->{'item_map'}{'last'} = $prev
3595 if !$window || @$items < $window;
3600 Returns an a map of all items found by this search. The map is a hash
3604 first => <first ticket id found>,
3605 last => <last ticket id found or undef>,
3608 prev => <the ticket id found before>,
3609 next => <the ticket id found after>,
3621 $self->_BuildItemMap unless $self->{'item_map'};
3622 return $self->{'item_map'};
3630 =head2 PrepForSerialization
3632 You don't want to serialize a big tickets object, as
3633 the {items} hash will be instantly invalid _and_ eat
3638 sub PrepForSerialization {
3640 delete $self->{'items'};
3641 delete $self->{'items_array'};
3642 $self->RedoSearch();
3647 RT::Tickets supports several flags which alter search behavior:
3650 allow_deleted_search (Otherwise never show deleted tickets in search results)
3651 looking_at_type (otherwise limit to type=ticket)
3653 These flags are set by calling
3655 $tickets->{'flagname'} = 1;
3657 BUG: There should be an API for this