1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
51 # - Decimated ProcessRestrictions and broke it into multiple
52 # functions joined by a LUT
53 # - Semi-Generic SQL stuff moved to another file
55 # Known Issues: FIXME!
57 # - ClearRestrictions and Reinitialization is messy and unclear. The
58 # only good way to do it is to create a new RT::Tickets object.
62 RT::Tickets - A collection of Ticket objects
68 my $tickets = new RT::Tickets($CurrentUser);
72 A collection of RT::Tickets.
82 no warnings qw(redefine);
85 use DBIx::SearchBuilder::Unique;
87 # Configuration Tables:
89 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
92 our %FIELD_METADATA = (
93 Status => [ 'ENUM', ], #loc_left_pair
94 Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
95 Type => [ 'ENUM', ], #loc_left_pair
96 Creator => [ 'ENUM' => 'User', ], #loc_left_pair
97 LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
98 Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
99 EffectiveId => [ 'INT', ], #loc_left_pair
100 id => [ 'ID', ], #loc_left_pair
101 InitialPriority => [ 'INT', ], #loc_left_pair
102 FinalPriority => [ 'INT', ], #loc_left_pair
103 Priority => [ 'INT', ], #loc_left_pair
104 TimeLeft => [ 'INT', ], #loc_left_pair
105 TimeWorked => [ 'INT', ], #loc_left_pair
106 TimeEstimated => [ 'INT', ], #loc_left_pair
108 Linked => [ 'LINK' ], #loc_left_pair
109 LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
110 LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
111 MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
112 DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
113 RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
114 HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
115 DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
116 DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
117 ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
118 Told => [ 'DATE' => 'Told', ], #loc_left_pair
119 Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
120 Started => [ 'DATE' => 'Started', ], #loc_left_pair
121 Due => [ 'DATE' => 'Due', ], #loc_left_pair
122 Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
123 LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
124 Created => [ 'DATE' => 'Created', ], #loc_left_pair
125 Subject => [ 'STRING', ], #loc_left_pair
126 Content => [ 'TRANSFIELD', ], #loc_left_pair
127 ContentType => [ 'TRANSFIELD', ], #loc_left_pair
128 Filename => [ 'TRANSFIELD', ], #loc_left_pair
129 TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
130 Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
131 Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
132 Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
133 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
134 Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
135 QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
136 QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
137 QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
138 CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
139 CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
140 CF => [ 'CUSTOMFIELD', ], #loc_left_pair
141 Updated => [ 'TRANSDATE', ], #loc_left_pair
142 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
143 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
144 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
145 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
146 HasAttribute => [ 'HASATTRIBUTE', 1 ],
147 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
149 Agentnum => [ 'FREESIDEFIELD', ],
150 Classnum => [ 'FREESIDEFIELD', ],
151 Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ],
152 WillResolve => [ 'DATE' => 'WillResolve', ], #loc_left_pair
155 our %SEARCHABLE_SUBFIELDS = (
157 EmailAddress Name RealName Nickname Organization Address1 Address2
158 WorkPhone HomePhone MobilePhone PagerPhone id
162 # Mapping of Field Type to Function
164 ENUM => \&_EnumLimit,
167 LINK => \&_LinkLimit,
168 DATE => \&_DateLimit,
169 STRING => \&_StringLimit,
170 TRANSFIELD => \&_TransLimit,
171 TRANSDATE => \&_TransDateLimit,
172 WATCHERFIELD => \&_WatcherLimit,
173 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
174 CUSTOMFIELD => \&_CustomFieldLimit,
175 HASATTRIBUTE => \&_HasAttributeLimit,
176 FREESIDEFIELD => \&_FreesideFieldLimit,
178 our %can_bundle = ();# WATCHERFIELD => "yes", );
180 # Default EntryAggregator per type
181 # if you specify OP, you must specify all valid OPs
222 # Helper functions for passing the above lexically scoped tables above
223 # into Tickets_Overlay_SQL.
224 sub FIELDS { return \%FIELD_METADATA }
225 sub dispatch { return \%dispatch }
226 sub can_bundle { return \%can_bundle }
228 # Bring in the clowns.
229 require RT::Tickets_Overlay_SQL;
233 our @SORTFIELDS = qw(id Status
235 Owner Created Due Starts Started
237 Resolved LastUpdated Priority TimeWorked TimeLeft);
241 Returns the list of fields that lists of tickets can easily be sorted by
247 return (@SORTFIELDS);
252 # BEGIN SQL STUFF *********************************
257 $self->SUPER::CleanSlate( @_ );
258 delete $self->{$_} foreach qw(
260 _sql_group_members_aliases
261 _sql_object_cfv_alias
262 _sql_role_group_aliases
265 _sql_u_watchers_alias_for_sort
266 _sql_u_watchers_aliases
267 _sql_current_user_can_see_applied
271 =head1 Limit Helper Routines
273 These routines are the targets of a dispatch table depending on the
274 type of field. They all share the same signature:
276 my ($self,$field,$op,$value,@rest) = @_;
278 The values in @rest should be suitable for passing directly to
279 DBIx::SearchBuilder::Limit.
281 Essentially they are an expanded/broken out (and much simplified)
282 version of what ProcessRestrictions used to do. They're also much
283 more clearly delineated by the TYPE of field being processed.
292 my ( $sb, $field, $op, $value, @rest ) = @_;
294 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
296 die "Invalid operator $op for __Bookmarked__ search on $field"
297 unless $op =~ /^(=|!=)$/;
300 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
301 $tmp = $tmp->Content if $tmp;
306 return $sb->_SQLLimit(
313 # as bookmarked tickets can be merged we have to use a join
314 # but it should be pretty lightweight
315 my $tickets_alias = $sb->Join(
320 FIELD2 => 'EffectiveId',
324 my $ea = $op eq '='? 'OR': 'AND';
325 foreach my $id ( sort @bookmarks ) {
327 ALIAS => $tickets_alias,
331 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
339 Handle Fields which are limited to certain values, and potentially
340 need to be looked up from another class.
342 This subroutine actually handles two different kinds of fields. For
343 some the user is responsible for limiting the values. (i.e. Status,
346 For others, the value specified by the user will be looked by via
350 name of class to lookup in (Optional)
355 my ( $sb, $field, $op, $value, @rest ) = @_;
357 # SQL::Statement changes != to <>. (Can we remove this now?)
358 $op = "!=" if $op eq "<>";
360 die "Invalid Operation: $op for $field"
364 my $meta = $FIELD_METADATA{$field};
365 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
366 my $class = "RT::" . $meta->[1];
367 my $o = $class->new( $sb->CurrentUser );
381 Handle fields where the values are limited to integers. (For example,
382 Priority, TimeWorked.)
390 my ( $sb, $field, $op, $value, @rest ) = @_;
392 die "Invalid Operator $op for $field"
393 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
405 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
408 1: Direction (From, To)
409 2: Link Type (MemberOf, DependsOn, RefersTo)
414 my ( $sb, $field, $op, $value, @rest ) = @_;
416 my $meta = $FIELD_METADATA{$field};
417 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
420 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
424 $is_null = 1 if !$value || $value =~ /^null$/io;
426 my $direction = $meta->[1] || '';
427 my ($matchfield, $linkfield) = ('', '');
428 if ( $direction eq 'To' ) {
429 ($matchfield, $linkfield) = ("Target", "Base");
431 elsif ( $direction eq 'From' ) {
432 ($matchfield, $linkfield) = ("Base", "Target");
434 elsif ( $direction ) {
435 die "Invalid link direction '$direction' for $field\n";
438 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
440 'LinkedFrom', $op, $value, @rest,
441 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
449 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
451 elsif ( $value =~ /\D/ ) {
454 $matchfield = "Local$matchfield" if $is_local;
456 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
457 # SELECT main.* FROM Tickets main
458 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
459 # AND(main.id = Links_1.LocalTarget))
460 # WHERE Links_1.LocalBase IS NULL;
463 my $linkalias = $sb->Join(
468 FIELD2 => 'Local' . $linkfield
471 LEFTJOIN => $linkalias,
479 FIELD => $matchfield,
486 my $linkalias = $sb->Join(
491 FIELD2 => 'Local' . $linkfield
494 LEFTJOIN => $linkalias,
500 LEFTJOIN => $linkalias,
501 FIELD => $matchfield,
508 FIELD => $matchfield,
509 OPERATOR => $is_negative? 'IS': 'IS NOT',
518 Handle date fields. (Created, LastTold..)
521 1: type of link. (Probably not necessary.)
526 my ( $sb, $field, $op, $value, @rest ) = @_;
528 die "Invalid Date Op: $op"
529 unless $op =~ /^(=|>|<|>=|<=)$/;
531 my $meta = $FIELD_METADATA{$field};
532 die "Incorrect Meta Data for $field"
533 unless ( defined $meta->[1] );
535 $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest );
538 # Factor this out for use by custom fields
540 sub _DateFieldLimit {
541 my ( $sb, $field, $op, $value, @rest ) = @_;
543 my $date = RT::Date->new( $sb->CurrentUser );
544 $date->Set( Format => 'unknown', Value => $value );
548 # if we're specifying =, that means we want everything on a
549 # particular single day. in the database, we need to check for >
550 # and < the edges of that day.
552 # Except if the value is 'this month' or 'last month', check
553 # > and < the edges of the month.
555 my ($daystart, $dayend);
556 if ( lc($value) eq 'this month' ) {
558 $date->SetToStart('month', Timezone => 'server');
559 $daystart = $date->ISO;
560 $date->AddMonth(Timezone => 'server');
561 $dayend = $date->ISO;
563 elsif ( lc($value) eq 'last month' ) {
565 $date->SetToStart('month', Timezone => 'server');
566 $dayend = $date->ISO;
568 $date->SetToStart('month', Timezone => 'server');
569 $daystart = $date->ISO;
572 $date->SetToMidnight( Timezone => 'server' );
573 $daystart = $date->ISO;
575 $dayend = $date->ISO;
592 ENTRYAGGREGATOR => 'AND',
610 Handle simple fields which are just strings. (Subject,Type)
618 my ( $sb, $field, $op, $value, @rest ) = @_;
622 # =, !=, LIKE, NOT LIKE
623 if ( (!defined $value || !length $value)
624 && lc($op) ne 'is' && lc($op) ne 'is not'
625 && RT->Config->Get('DatabaseType') eq 'Oracle'
627 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
628 $op = $negative? 'IS NOT': 'IS';
641 =head2 _TransDateLimit
643 Handle fields limiting based on Transaction Date.
645 The inpupt value must be in a format parseable by Time::ParseDate
652 # This routine should really be factored into translimit.
653 sub _TransDateLimit {
654 my ( $sb, $field, $op, $value, @rest ) = @_;
656 # See the comments for TransLimit, they apply here too
658 unless ( $sb->{_sql_transalias} ) {
659 $sb->{_sql_transalias} = $sb->Join(
662 TABLE2 => 'Transactions',
663 FIELD2 => 'ObjectId',
666 ALIAS => $sb->{_sql_transalias},
667 FIELD => 'ObjectType',
668 VALUE => 'RT::Ticket',
669 ENTRYAGGREGATOR => 'AND',
673 my $date = RT::Date->new( $sb->CurrentUser );
674 $date->Set( Format => 'unknown', Value => $value );
679 # if we're specifying =, that means we want everything on a
680 # particular single day. in the database, we need to check for >
681 # and < the edges of that day.
683 $date->SetToMidnight( Timezone => 'server' );
684 my $daystart = $date->ISO;
686 my $dayend = $date->ISO;
689 ALIAS => $sb->{_sql_transalias},
697 ALIAS => $sb->{_sql_transalias},
703 ENTRYAGGREGATOR => 'AND',
708 # not searching for a single day
711 #Search for the right field
713 ALIAS => $sb->{_sql_transalias},
727 Limit based on the Content of a transaction or the ContentType.
736 # Content, ContentType, Filename
738 # If only this was this simple. We've got to do something
741 #Basically, we want to make sure that the limits apply to
742 #the same attachment, rather than just another attachment
743 #for the same ticket, no matter how many clauses we lump
744 #on. We put them in TicketAliases so that they get nuked
745 #when we redo the join.
747 # In the SQL, we might have
748 # (( Content = foo ) or ( Content = bar AND Content = baz ))
749 # The AND group should share the same Alias.
751 # Actually, maybe it doesn't matter. We use the same alias and it
752 # works itself out? (er.. different.)
754 # Steal more from _ProcessRestrictions
756 # FIXME: Maybe look at the previous FooLimit call, and if it was a
757 # TransLimit and EntryAggregator == AND, reuse the Aliases?
759 # Or better - store the aliases on a per subclause basis - since
760 # those are going to be the things we want to relate to each other,
763 # maybe we should not allow certain kinds of aggregation of these
764 # clauses and do a psuedo regex instead? - the problem is getting
765 # them all into the same subclause when you have (A op B op C) - the
766 # way they get parsed in the tree they're in different subclauses.
768 my ( $self, $field, $op, $value, %rest ) = @_;
770 unless ( $self->{_sql_transalias} ) {
771 $self->{_sql_transalias} = $self->Join(
774 TABLE2 => 'Transactions',
775 FIELD2 => 'ObjectId',
778 ALIAS => $self->{_sql_transalias},
779 FIELD => 'ObjectType',
780 VALUE => 'RT::Ticket',
781 ENTRYAGGREGATOR => 'AND',
784 unless ( defined $self->{_sql_trattachalias} ) {
785 $self->{_sql_trattachalias} = $self->_SQLJoin(
786 TYPE => 'LEFT', # not all txns have an attachment
787 ALIAS1 => $self->{_sql_transalias},
789 TABLE2 => 'Attachments',
790 FIELD2 => 'TransactionId',
794 #Search for the right field
795 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
799 ALIAS => $self->{_sql_trattachalias},
806 ENTRYAGGREGATOR => 'AND',
807 ALIAS => $self->{_sql_trattachalias},
816 ALIAS => $self->{_sql_trattachalias},
829 Handle watcher limits. (Requestor, CC, etc..)
845 my $meta = $FIELD_METADATA{ $field };
846 my $type = $meta->[1] || '';
847 my $class = $meta->[2] || 'Ticket';
849 # Bail if the subfield is not allowed
851 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
853 die "Invalid watcher subfield: '$rest{SUBKEY}'";
856 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
857 # search by id and Name at the same time, this is workaround
858 # to preserve backward compatibility
859 if ( $field eq 'Owner' ) {
860 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
861 my $o = RT::User->new( $self->CurrentUser );
862 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
863 $o->$method( $value );
872 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
882 $rest{SUBKEY} ||= 'EmailAddress';
884 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
887 if ( $op =~ /^IS(?: NOT)?$/ ) {
888 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
889 # to avoid joining the table Users into the query, we just join GM
890 # and make sure we don't match records where group is member of itself
892 LEFTJOIN => $group_members,
895 VALUE => "$group_members.MemberId",
899 ALIAS => $group_members,
906 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
908 $op =~ s/!|NOT\s+//i;
910 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
911 # "X = 'Y'" matches more then one user so we try to fetch two records and
912 # do the right thing when there is only one exist and semi-working solution
914 my $users_obj = RT::Users->new( $self->CurrentUser );
916 FIELD => $rest{SUBKEY},
921 $users_obj->RowsPerPage(2);
922 my @users = @{ $users_obj->ItemsArrayRef };
924 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
927 $uid = $users[0]->id if @users;
929 LEFTJOIN => $group_members,
930 ALIAS => $group_members,
936 ALIAS => $group_members,
943 LEFTJOIN => $group_members,
946 VALUE => "$group_members.MemberId",
949 my $users = $self->Join(
951 ALIAS1 => $group_members,
952 FIELD1 => 'MemberId',
959 FIELD => $rest{SUBKEY},
973 my $group_members = $self->_GroupMembersJoin(
974 GroupsAlias => $groups,
978 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
980 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
981 $self->NewAlias('Users');
983 LEFTJOIN => $group_members,
984 ALIAS => $group_members,
986 VALUE => "$users.id",
991 # we join users table without adding some join condition between tables,
992 # the only conditions we have are conditions on the table iteslf,
993 # for example Users.EmailAddress = 'x'. We should add this condition to
994 # the top level of the query and bundle it with another similar conditions,
995 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
996 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
999 SUBCLAUSE => '_sql_u_watchers_'. $users,
1001 FIELD => $rest{'SUBKEY'},
1006 # A condition which ties Users and Groups (role groups) is a left join condition
1007 # of CachedGroupMembers table. To get correct results of the query we check
1008 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
1011 ALIAS => $group_members,
1013 OPERATOR => 'IS NOT',
1020 sub _RoleGroupsJoin {
1022 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1023 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1024 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1027 # we always have watcher groups for ticket, so we use INNER join
1028 my $groups = $self->Join(
1030 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1032 FIELD2 => 'Instance',
1033 ENTRYAGGREGATOR => 'AND',
1035 $self->SUPER::Limit(
1036 LEFTJOIN => $groups,
1039 VALUE => 'RT::'. $args{'Class'} .'-Role',
1041 $self->SUPER::Limit(
1042 LEFTJOIN => $groups,
1045 VALUE => $args{'Type'},
1048 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1049 unless $args{'New'};
1054 sub _GroupMembersJoin {
1056 my %args = (New => 1, GroupsAlias => undef, @_);
1058 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1059 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1062 my $alias = $self->Join(
1064 ALIAS1 => $args{'GroupsAlias'},
1066 TABLE2 => 'CachedGroupMembers',
1067 FIELD2 => 'GroupId',
1068 ENTRYAGGREGATOR => 'AND',
1071 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1072 unless $args{'New'};
1079 Helper function which provides joins to a watchers table both for limits
1086 my $type = shift || '';
1089 my $groups = $self->_RoleGroupsJoin( Type => $type );
1090 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1091 # XXX: work around, we must hide groups that
1092 # are members of the role group we search in,
1093 # otherwise them result in wrong NULLs in Users
1094 # table and break ordering. Now, we know that
1095 # RT doesn't allow to add groups as members of the
1096 # ticket roles, so we just hide entries in CGM table
1097 # with MemberId == GroupId from results
1098 $self->SUPER::Limit(
1099 LEFTJOIN => $group_members,
1102 VALUE => "$group_members.MemberId",
1105 my $users = $self->Join(
1107 ALIAS1 => $group_members,
1108 FIELD1 => 'MemberId',
1112 return ($groups, $group_members, $users);
1115 =head2 _WatcherMembershipLimit
1117 Handle watcher membership limits, i.e. whether the watcher belongs to a
1118 specific group or not.
1121 1: Field to query on
1123 SELECT DISTINCT main.*
1127 CachedGroupMembers CachedGroupMembers_2,
1130 (main.EffectiveId = main.id)
1132 (main.Status != 'deleted')
1134 (main.Type = 'ticket')
1137 (Users_3.EmailAddress = '22')
1139 (Groups_1.Domain = 'RT::Ticket-Role')
1141 (Groups_1.Type = 'RequestorGroup')
1144 Groups_1.Instance = main.id
1146 Groups_1.id = CachedGroupMembers_2.GroupId
1148 CachedGroupMembers_2.MemberId = Users_3.id
1149 ORDER BY main.id ASC
1154 sub _WatcherMembershipLimit {
1155 my ( $self, $field, $op, $value, @rest ) = @_;
1160 my $groups = $self->NewAlias('Groups');
1161 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1162 my $users = $self->NewAlias('Users');
1163 my $memberships = $self->NewAlias('CachedGroupMembers');
1165 if ( ref $field ) { # gross hack
1166 my @bundle = @$field;
1168 for my $chunk (@bundle) {
1169 ( $field, $op, $value, @rest ) = @$chunk;
1171 ALIAS => $memberships,
1182 ALIAS => $memberships,
1190 # {{{ Tie to groups for tickets we care about
1194 VALUE => 'RT::Ticket-Role',
1195 ENTRYAGGREGATOR => 'AND'
1200 FIELD1 => 'Instance',
1207 # If we care about which sort of watcher
1208 my $meta = $FIELD_METADATA{$field};
1209 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1216 ENTRYAGGREGATOR => 'AND'
1223 ALIAS2 => $groupmembers,
1228 ALIAS1 => $groupmembers,
1229 FIELD1 => 'MemberId',
1235 ALIAS1 => $memberships,
1236 FIELD1 => 'MemberId',
1245 =head2 _CustomFieldDecipher
1247 Try and turn a CF descriptor into (cfid, cfname) object pair.
1251 sub _CustomFieldDecipher {
1252 my ($self, $string) = @_;
1254 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1255 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1259 my $q = RT::Queue->new( $self->CurrentUser );
1263 # $queue = $q->Name; # should we normalize the queue?
1264 $cf = $q->CustomField( $field );
1267 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1271 elsif ( $field =~ /\D/ ) {
1273 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1274 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1275 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1277 # if there is more then one field the current user can
1278 # see with the same name then we shouldn't return cf object
1279 # as we don't know which one to use
1282 $cf = undef if $cfs->Next;
1286 $cf = RT::CustomField->new( $self->CurrentUser );
1287 $cf->Load( $field );
1290 return ($queue, $field, $cf, $column);
1293 =head2 _CustomFieldJoin
1295 Factor out the Join of custom fields so we can use it for sorting too
1299 sub _CustomFieldJoin {
1300 my ($self, $cfkey, $cfid, $field) = @_;
1301 # Perform one Join per CustomField
1302 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1303 $self->{_sql_cf_alias}{$cfkey} )
1305 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1306 $self->{_sql_cf_alias}{$cfkey} );
1309 my ($TicketCFs, $CFs);
1311 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1315 TABLE2 => 'ObjectCustomFieldValues',
1316 FIELD2 => 'ObjectId',
1318 $self->SUPER::Limit(
1319 LEFTJOIN => $TicketCFs,
1320 FIELD => 'CustomField',
1322 ENTRYAGGREGATOR => 'AND'
1326 my $ocfalias = $self->Join(
1329 TABLE2 => 'ObjectCustomFields',
1330 FIELD2 => 'ObjectId',
1333 $self->SUPER::Limit(
1334 LEFTJOIN => $ocfalias,
1335 ENTRYAGGREGATOR => 'OR',
1336 FIELD => 'ObjectId',
1340 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1342 ALIAS1 => $ocfalias,
1343 FIELD1 => 'CustomField',
1344 TABLE2 => 'CustomFields',
1347 $self->SUPER::Limit(
1349 ENTRYAGGREGATOR => 'AND',
1350 FIELD => 'LookupType',
1351 VALUE => 'RT::Queue-RT::Ticket',
1353 $self->SUPER::Limit(
1355 ENTRYAGGREGATOR => 'AND',
1360 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1364 TABLE2 => 'ObjectCustomFieldValues',
1365 FIELD2 => 'CustomField',
1367 $self->SUPER::Limit(
1368 LEFTJOIN => $TicketCFs,
1369 FIELD => 'ObjectId',
1372 ENTRYAGGREGATOR => 'AND',
1375 $self->SUPER::Limit(
1376 LEFTJOIN => $TicketCFs,
1377 FIELD => 'ObjectType',
1378 VALUE => 'RT::Ticket',
1379 ENTRYAGGREGATOR => 'AND'
1381 $self->SUPER::Limit(
1382 LEFTJOIN => $TicketCFs,
1383 FIELD => 'Disabled',
1386 ENTRYAGGREGATOR => 'AND'
1389 return ($TicketCFs, $CFs);
1392 =head2 _CustomFieldLimit
1394 Limit based on CustomFields
1401 sub _CustomFieldLimit {
1402 my ( $self, $_field, $op, $value, %rest ) = @_;
1404 my $field = $rest{'SUBKEY'} || die "No field specified";
1406 # For our sanity, we can only limit on one queue at a time
1408 my ($queue, $cfid, $cf, $column);
1409 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1410 $cfid = $cf ? $cf->id : 0 ;
1412 # If we're trying to find custom fields that don't match something, we
1413 # want tickets where the custom field has no value at all. Note that
1414 # we explicitly don't include the "IS NULL" case, since we would
1415 # otherwise end up with a redundant clause.
1417 my ($negative_op, $null_op, $inv_op, $range_op)
1418 = $self->ClassifySQLOperation( $op );
1422 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1423 return 'MATCHES' if $op eq '=';
1424 return 'NOT MATCHES' if $op eq '!=';
1428 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1430 my $cfkey = $cfid ? $cfid : "$queue.$field";
1432 if ( $null_op && !$column ) {
1433 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1434 # we can reuse our default joins for this operation
1435 # with column specified we have different situation
1436 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1439 ALIAS => $TicketCFs,
1448 OPERATOR => 'IS NOT',
1451 ENTRYAGGREGATOR => 'AND',
1455 elsif ( !$negative_op || $single_value ) {
1456 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1457 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1464 # if column is defined then deal only with it
1465 # otherwise search in Content and in LargeContent
1468 ALIAS => $TicketCFs,
1470 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1475 elsif ( $cfid and $cf->Type eq 'Date' ) {
1476 $self->_DateFieldLimit(
1480 ALIAS => $TicketCFs,
1484 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1485 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1487 ALIAS => $TicketCFs,
1496 ALIAS => $TicketCFs,
1500 ENTRYAGGREGATOR => 'OR'
1503 ALIAS => $TicketCFs,
1507 ENTRYAGGREGATOR => 'OR'
1511 ALIAS => $TicketCFs,
1512 FIELD => 'LargeContent',
1513 OPERATOR => $fix_op->($op),
1515 ENTRYAGGREGATOR => 'AND',
1521 ALIAS => $TicketCFs,
1531 ALIAS => $TicketCFs,
1535 ENTRYAGGREGATOR => 'OR'
1538 ALIAS => $TicketCFs,
1542 ENTRYAGGREGATOR => 'OR'
1546 ALIAS => $TicketCFs,
1547 FIELD => 'LargeContent',
1548 OPERATOR => $fix_op->($op),
1550 ENTRYAGGREGATOR => 'AND',
1556 # XXX: if we join via CustomFields table then
1557 # because of order of left joins we get NULLs in
1558 # CF table and then get nulls for those records
1559 # in OCFVs table what result in wrong results
1560 # as decifer method now tries to load a CF then
1561 # we fall into this situation only when there
1562 # are more than one CF with the name in the DB.
1563 # the same thing applies to order by call.
1564 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1565 # we want treat IS NULL as (not applies or has
1570 OPERATOR => 'IS NOT',
1573 ENTRYAGGREGATOR => 'AND',
1579 ALIAS => $TicketCFs,
1580 FIELD => $column || 'Content',
1584 ENTRYAGGREGATOR => 'OR',
1591 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1592 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1595 $op =~ s/!|NOT\s+//i;
1597 # if column is defined then deal only with it
1598 # otherwise search in Content and in LargeContent
1600 $self->SUPER::Limit(
1601 LEFTJOIN => $TicketCFs,
1602 ALIAS => $TicketCFs,
1604 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1609 $self->SUPER::Limit(
1610 LEFTJOIN => $TicketCFs,
1611 ALIAS => $TicketCFs,
1619 ALIAS => $TicketCFs,
1628 sub _HasAttributeLimit {
1629 my ( $self, $field, $op, $value, %rest ) = @_;
1631 my $alias = $self->Join(
1635 TABLE2 => 'Attributes',
1636 FIELD2 => 'ObjectId',
1638 $self->SUPER::Limit(
1640 FIELD => 'ObjectType',
1641 VALUE => 'RT::Ticket',
1642 ENTRYAGGREGATOR => 'AND'
1644 $self->SUPER::Limit(
1649 ENTRYAGGREGATOR => 'AND'
1655 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1661 # End Helper Functions
1663 # End of SQL Stuff -------------------------------------------------
1665 # {{{ Allow sorting on watchers
1667 =head2 OrderByCols ARRAY
1669 A modified version of the OrderBy method which automatically joins where
1670 C<ALIAS> is set to the name of a watcher type.
1681 foreach my $row (@args) {
1682 if ( $row->{ALIAS} ) {
1686 if ( $row->{FIELD} !~ /\./ ) {
1687 my $meta = $self->FIELDS->{ $row->{FIELD} };
1693 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1694 my $alias = $self->Join(
1697 FIELD1 => $row->{'FIELD'},
1701 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1702 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1703 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1705 my $alias = $self->Join(
1708 FIELD1 => $row->{'FIELD'},
1712 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1719 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1720 my $meta = $self->FIELDS->{$field};
1721 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1722 # cache alias as we want to use one alias per watcher type for sorting
1723 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1725 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1726 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1728 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1729 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1730 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1731 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1732 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1733 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1734 # this is described in _CustomFieldLimit
1738 OPERATOR => 'IS NOT',
1741 ENTRYAGGREGATOR => 'AND',
1744 # For those cases where we are doing a join against the
1745 # CF name, and don't have a CFid, use Unique to make sure
1746 # we don't show duplicate tickets. NOTE: I'm pretty sure
1747 # this will stay mixed in for the life of the
1748 # class/package, and not just for the life of the object.
1749 # Potential performance issue.
1750 require DBIx::SearchBuilder::Unique;
1751 DBIx::SearchBuilder::Unique->import;
1753 my $CFvs = $self->Join(
1755 ALIAS1 => $TicketCFs,
1756 FIELD1 => 'CustomField',
1757 TABLE2 => 'CustomFieldValues',
1758 FIELD2 => 'CustomField',
1760 $self->SUPER::Limit(
1764 VALUE => $TicketCFs . ".Content",
1765 ENTRYAGGREGATOR => 'AND'
1768 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1769 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1770 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1771 # PAW logic is "reversed"
1773 if (exists $row->{ORDER} ) {
1774 my $o = $row->{ORDER};
1775 delete $row->{ORDER};
1776 $order = "DESC" if $o =~ /asc/i;
1779 # Ticket.Owner 1 0 X
1780 # Unowned Tickets 0 1 X
1783 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1784 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1785 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1790 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1797 FUNCTION => "Owner=$uid",
1803 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1805 } elsif ( $field eq 'Customer' ) { #Freeside
1806 if ( $subkey eq 'Number' ) {
1807 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1810 FIELD => $custnum_sql,
1814 my $custalias = $self->JoinToCustomer;
1816 if ( $subkey eq 'Name' ) {
1817 $field = "COALESCE( $custalias.company,
1818 $custalias.last || ', ' || $custalias.first
1821 elsif ( $subkey eq 'Class' ) {
1822 $field = "$custalias.classnum";
1824 elsif ( $subkey eq 'Agent' ) {
1825 $field = "$custalias.agentnum";
1828 # no other cases exist yet, but for obviousness:
1831 push @res, { %$row, ALIAS => '', FIELD => $field };
1840 return $self->SUPER::OrderByCols(@res);
1845 sub JoinToCustLinks {
1846 # Set up join to links (id = localbase),
1847 # limit link type to 'MemberOf',
1848 # and target value to any Freeside custnum URI.
1849 # Return the linkalias for further join/limit action,
1850 # and an sql expression to retrieve the custnum.
1852 my $linkalias = $self->Join(
1857 FIELD2 => 'LocalBase',
1860 $self->SUPER::Limit(
1861 LEFTJOIN => $linkalias,
1864 VALUE => 'MemberOf',
1866 $self->SUPER::Limit(
1867 LEFTJOIN => $linkalias,
1869 OPERATOR => 'STARTSWITH',
1870 VALUE => 'freeside://freeside/cust_main/',
1872 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1873 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1874 $custnum_sql .= 'SIGNED INTEGER)';
1877 $custnum_sql .= 'INTEGER)';
1879 return ($linkalias, $custnum_sql);
1882 sub JoinToCustomer {
1884 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1886 my $custalias = $self->Join(
1888 EXPRESSION => $custnum_sql,
1889 TABLE2 => 'cust_main',
1890 FIELD2 => 'custnum',
1895 sub _FreesideFieldLimit {
1896 my ( $self, $field, $op, $value, %rest ) = @_;
1897 my $alias = $self->JoinToCustomer;
1898 my $is_negative = 0;
1899 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1900 # if the op is negative, do the join as though
1901 # the op were positive, then accept only records
1902 # where the right-side join key is null.
1904 $op = '=' if $op eq '!=';
1907 my $meta = $FIELD_METADATA{$field};
1909 $alias = $self->Join(
1912 FIELD1 => 'custnum',
1913 TABLE2 => $meta->[1],
1914 FIELD2 => 'custnum',
1918 $self->SUPER::Limit(
1920 FIELD => lc($field),
1923 ENTRYAGGREGATOR => 'AND',
1928 FIELD => lc($field),
1929 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1939 # {{{ Limit the result set based on content
1945 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1946 Generally best called from LimitFoo methods
1956 DESCRIPTION => undef,
1959 $args{'DESCRIPTION'} = $self->loc(
1960 "[_1] [_2] [_3]", $args{'FIELD'},
1961 $args{'OPERATOR'}, $args{'VALUE'}
1963 if ( !defined $args{'DESCRIPTION'} );
1965 my $index = $self->_NextIndex;
1967 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1969 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1971 $self->{'RecalcTicketLimits'} = 1;
1973 # If we're looking at the effective id, we don't want to append the other clause
1974 # which limits us to tickets where id = effective id
1975 if ( $args{'FIELD'} eq 'EffectiveId'
1976 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1978 $self->{'looking_at_effective_id'} = 1;
1981 if ( $args{'FIELD'} eq 'Type'
1982 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1984 $self->{'looking_at_type'} = 1;
1994 Returns a frozen string suitable for handing back to ThawLimits.
1998 sub _FreezeThawKeys {
1999 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2003 # {{{ sub FreezeLimits
2008 require MIME::Base64;
2009 MIME::Base64::base64_encode(
2010 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2017 Take a frozen Limits string generated by FreezeLimits and make this tickets
2018 object have that set of limits.
2022 # {{{ sub ThawLimits
2028 #if we don't have $in, get outta here.
2029 return undef unless ($in);
2031 $self->{'RecalcTicketLimits'} = 1;
2034 require MIME::Base64;
2036 #We don't need to die if the thaw fails.
2037 @{$self}{ $self->_FreezeThawKeys }
2038 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2040 $RT::Logger->error($@) if $@;
2046 # {{{ Limit by enum or foreign key
2048 # {{{ sub LimitQueue
2052 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2053 OPERATOR is one of = or !=. (It defaults to =).
2054 VALUE is a queue id or Name.
2067 #TODO VALUE should also take queue objects
2068 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2069 my $queue = new RT::Queue( $self->CurrentUser );
2070 $queue->Load( $args{'VALUE'} );
2071 $args{'VALUE'} = $queue->Id;
2074 # What if they pass in an Id? Check for isNum() and convert to
2077 #TODO check for a valid queue here
2081 VALUE => $args{'VALUE'},
2082 OPERATOR => $args{'OPERATOR'},
2083 DESCRIPTION => join(
2084 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2092 # {{{ sub LimitStatus
2096 Takes a paramhash with the fields OPERATOR and VALUE.
2097 OPERATOR is one of = or !=.
2100 RT adds Status != 'deleted' until object has
2101 allow_deleted_search internal property set.
2102 $tickets->{'allow_deleted_search'} = 1;
2103 $tickets->LimitStatus( VALUE => 'deleted' );
2115 VALUE => $args{'VALUE'},
2116 OPERATOR => $args{'OPERATOR'},
2117 DESCRIPTION => join( ' ',
2118 $self->loc('Status'), $args{'OPERATOR'},
2119 $self->loc( $args{'VALUE'} ) ),
2125 # {{{ sub IgnoreType
2129 If called, this search will not automatically limit the set of results found
2130 to tickets of type "Ticket". Tickets of other types, such as "project" and
2131 "approval" will be found.
2138 # Instead of faking a Limit that later gets ignored, fake up the
2139 # fact that we're already looking at type, so that the check in
2140 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2142 # $self->LimitType(VALUE => '__any');
2143 $self->{looking_at_type} = 1;
2152 Takes a paramhash with the fields OPERATOR and VALUE.
2153 OPERATOR is one of = or !=, it defaults to "=".
2154 VALUE is a string to search for in the type of the ticket.
2169 VALUE => $args{'VALUE'},
2170 OPERATOR => $args{'OPERATOR'},
2171 DESCRIPTION => join( ' ',
2172 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2180 # {{{ Limit by string field
2182 # {{{ sub LimitSubject
2186 Takes a paramhash with the fields OPERATOR and VALUE.
2187 OPERATOR is one of = or !=.
2188 VALUE is a string to search for in the subject of the ticket.
2197 VALUE => $args{'VALUE'},
2198 OPERATOR => $args{'OPERATOR'},
2199 DESCRIPTION => join( ' ',
2200 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2208 # {{{ Limit based on ticket numerical attributes
2209 # Things that can be > < = !=
2215 Takes a paramhash with the fields OPERATOR and VALUE.
2216 OPERATOR is one of =, >, < or !=.
2217 VALUE is a ticket Id to search for
2230 VALUE => $args{'VALUE'},
2231 OPERATOR => $args{'OPERATOR'},
2233 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2239 # {{{ sub LimitPriority
2241 =head2 LimitPriority
2243 Takes a paramhash with the fields OPERATOR and VALUE.
2244 OPERATOR is one of =, >, < or !=.
2245 VALUE is a value to match the ticket\'s priority against
2253 FIELD => 'Priority',
2254 VALUE => $args{'VALUE'},
2255 OPERATOR => $args{'OPERATOR'},
2256 DESCRIPTION => join( ' ',
2257 $self->loc('Priority'),
2258 $args{'OPERATOR'}, $args{'VALUE'}, ),
2264 # {{{ sub LimitInitialPriority
2266 =head2 LimitInitialPriority
2268 Takes a paramhash with the fields OPERATOR and VALUE.
2269 OPERATOR is one of =, >, < or !=.
2270 VALUE is a value to match the ticket\'s initial priority against
2275 sub LimitInitialPriority {
2279 FIELD => 'InitialPriority',
2280 VALUE => $args{'VALUE'},
2281 OPERATOR => $args{'OPERATOR'},
2282 DESCRIPTION => join( ' ',
2283 $self->loc('Initial Priority'), $args{'OPERATOR'},
2290 # {{{ sub LimitFinalPriority
2292 =head2 LimitFinalPriority
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 final priority against
2300 sub LimitFinalPriority {
2304 FIELD => 'FinalPriority',
2305 VALUE => $args{'VALUE'},
2306 OPERATOR => $args{'OPERATOR'},
2307 DESCRIPTION => join( ' ',
2308 $self->loc('Final Priority'), $args{'OPERATOR'},
2315 # {{{ sub LimitTimeWorked
2317 =head2 LimitTimeWorked
2319 Takes a paramhash with the fields OPERATOR and VALUE.
2320 OPERATOR is one of =, >, < or !=.
2321 VALUE is a value to match the ticket's TimeWorked attribute
2325 sub LimitTimeWorked {
2329 FIELD => 'TimeWorked',
2330 VALUE => $args{'VALUE'},
2331 OPERATOR => $args{'OPERATOR'},
2332 DESCRIPTION => join( ' ',
2333 $self->loc('Time Worked'),
2334 $args{'OPERATOR'}, $args{'VALUE'}, ),
2340 # {{{ sub LimitTimeLeft
2342 =head2 LimitTimeLeft
2344 Takes a paramhash with the fields OPERATOR and VALUE.
2345 OPERATOR is one of =, >, < or !=.
2346 VALUE is a value to match the ticket's TimeLeft attribute
2354 FIELD => 'TimeLeft',
2355 VALUE => $args{'VALUE'},
2356 OPERATOR => $args{'OPERATOR'},
2357 DESCRIPTION => join( ' ',
2358 $self->loc('Time Left'),
2359 $args{'OPERATOR'}, $args{'VALUE'}, ),
2367 # {{{ Limiting based on attachment attributes
2369 # {{{ sub LimitContent
2373 Takes a paramhash with the fields OPERATOR and VALUE.
2374 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2375 VALUE is a string to search for in the body of the ticket
2384 VALUE => $args{'VALUE'},
2385 OPERATOR => $args{'OPERATOR'},
2386 DESCRIPTION => join( ' ',
2387 $self->loc('Ticket content'), $args{'OPERATOR'},
2394 # {{{ sub LimitFilename
2396 =head2 LimitFilename
2398 Takes a paramhash with the fields OPERATOR and VALUE.
2399 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2400 VALUE is a string to search for in the body of the ticket
2408 FIELD => 'Filename',
2409 VALUE => $args{'VALUE'},
2410 OPERATOR => $args{'OPERATOR'},
2411 DESCRIPTION => join( ' ',
2412 $self->loc('Attachment filename'), $args{'OPERATOR'},
2418 # {{{ sub LimitContentType
2420 =head2 LimitContentType
2422 Takes a paramhash with the fields OPERATOR and VALUE.
2423 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2424 VALUE is a content type to search ticket attachments for
2428 sub LimitContentType {
2432 FIELD => 'ContentType',
2433 VALUE => $args{'VALUE'},
2434 OPERATOR => $args{'OPERATOR'},
2435 DESCRIPTION => join( ' ',
2436 $self->loc('Ticket content type'), $args{'OPERATOR'},
2445 # {{{ Limiting based on people
2447 # {{{ sub LimitOwner
2451 Takes a paramhash with the fields OPERATOR and VALUE.
2452 OPERATOR is one of = or !=.
2464 my $owner = new RT::User( $self->CurrentUser );
2465 $owner->Load( $args{'VALUE'} );
2467 # FIXME: check for a valid $owner
2470 VALUE => $args{'VALUE'},
2471 OPERATOR => $args{'OPERATOR'},
2472 DESCRIPTION => join( ' ',
2473 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2480 # {{{ Limiting watchers
2482 # {{{ sub LimitWatcher
2486 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2487 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2488 VALUE is a value to match the ticket\'s watcher email addresses against
2489 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2503 #build us up a description
2504 my ( $watcher_type, $desc );
2505 if ( $args{'TYPE'} ) {
2506 $watcher_type = $args{'TYPE'};
2509 $watcher_type = "Watcher";
2513 FIELD => $watcher_type,
2514 VALUE => $args{'VALUE'},
2515 OPERATOR => $args{'OPERATOR'},
2516 TYPE => $args{'TYPE'},
2517 DESCRIPTION => join( ' ',
2518 $self->loc($watcher_type),
2519 $args{'OPERATOR'}, $args{'VALUE'}, ),
2529 # {{{ Limiting based on links
2533 =head2 LimitLinkedTo
2535 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2536 TYPE limits the sort of link we want to search on
2538 TYPE = { RefersTo, MemberOf, DependsOn }
2540 TARGET is the id or URI of the TARGET of the link
2554 FIELD => 'LinkedTo',
2556 TARGET => $args{'TARGET'},
2557 TYPE => $args{'TYPE'},
2558 DESCRIPTION => $self->loc(
2559 "Tickets [_1] by [_2]",
2560 $self->loc( $args{'TYPE'} ),
2563 OPERATOR => $args{'OPERATOR'},
2569 # {{{ LimitLinkedFrom
2571 =head2 LimitLinkedFrom
2573 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2574 TYPE limits the sort of link we want to search on
2577 BASE is the id or URI of the BASE of the link
2581 sub LimitLinkedFrom {
2590 # translate RT2 From/To naming to RT3 TicketSQL naming
2591 my %fromToMap = qw(DependsOn DependentOn
2593 RefersTo ReferredToBy);
2595 my $type = $args{'TYPE'};
2596 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2599 FIELD => 'LinkedTo',
2601 BASE => $args{'BASE'},
2603 DESCRIPTION => $self->loc(
2604 "Tickets [_1] [_2]",
2605 $self->loc( $args{'TYPE'} ),
2608 OPERATOR => $args{'OPERATOR'},
2617 my $ticket_id = shift;
2618 return $self->LimitLinkedTo(
2620 TARGET => $ticket_id,
2627 # {{{ LimitHasMember
2628 sub LimitHasMember {
2630 my $ticket_id = shift;
2631 return $self->LimitLinkedFrom(
2633 BASE => "$ticket_id",
2634 TYPE => 'HasMember',
2641 # {{{ LimitDependsOn
2643 sub LimitDependsOn {
2645 my $ticket_id = shift;
2646 return $self->LimitLinkedTo(
2648 TARGET => $ticket_id,
2649 TYPE => 'DependsOn',
2656 # {{{ LimitDependedOnBy
2658 sub LimitDependedOnBy {
2660 my $ticket_id = shift;
2661 return $self->LimitLinkedFrom(
2664 TYPE => 'DependentOn',
2675 my $ticket_id = shift;
2676 return $self->LimitLinkedTo(
2678 TARGET => $ticket_id,
2686 # {{{ LimitReferredToBy
2688 sub LimitReferredToBy {
2690 my $ticket_id = shift;
2691 return $self->LimitLinkedFrom(
2694 TYPE => 'ReferredToBy',
2702 # {{{ limit based on ticket date attribtes
2706 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2708 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2710 OPERATOR is one of > or <
2711 VALUE is a date and time in ISO format in GMT
2712 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2714 There are also helper functions of the form LimitFIELD that eliminate
2715 the need to pass in a FIELD argument.
2729 #Set the description if we didn't get handed it above
2730 unless ( $args{'DESCRIPTION'} ) {
2731 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2732 . $args{'OPERATOR'} . " "
2733 . $args{'VALUE'} . " GMT";
2736 $self->Limit(%args);
2744 $self->LimitDate( FIELD => 'Created', @_ );
2749 $self->LimitDate( FIELD => 'Due', @_ );
2755 $self->LimitDate( FIELD => 'Starts', @_ );
2761 $self->LimitDate( FIELD => 'Started', @_ );
2766 $self->LimitDate( FIELD => 'Resolved', @_ );
2771 $self->LimitDate( FIELD => 'Told', @_ );
2774 sub LimitLastUpdated {
2776 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2780 # {{{ sub LimitTransactionDate
2782 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2784 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2786 OPERATOR is one of > or <
2787 VALUE is a date and time in ISO format in GMT
2792 sub LimitTransactionDate {
2795 FIELD => 'TransactionDate',
2802 # <20021217042756.GK28744@pallas.fsck.com>
2803 # "Kill It" - Jesse.
2805 #Set the description if we didn't get handed it above
2806 unless ( $args{'DESCRIPTION'} ) {
2807 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2808 . $args{'OPERATOR'} . " "
2809 . $args{'VALUE'} . " GMT";
2812 $self->Limit(%args);
2820 # {{{ Limit based on custom fields
2821 # {{{ sub LimitCustomField
2823 =head2 LimitCustomField
2825 Takes a paramhash of key/value pairs with the following keys:
2829 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2831 =item OPERATOR - The usual Limit operators
2833 =item VALUE - The value to compare against
2839 sub LimitCustomField {
2843 CUSTOMFIELD => undef,
2845 DESCRIPTION => undef,
2846 FIELD => 'CustomFieldValue',
2851 my $CF = RT::CustomField->new( $self->CurrentUser );
2852 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2853 $CF->Load( $args{CUSTOMFIELD} );
2856 $CF->LoadByNameAndQueue(
2857 Name => $args{CUSTOMFIELD},
2858 Queue => $args{QUEUE}
2860 $args{CUSTOMFIELD} = $CF->Id;
2863 #If we are looking to compare with a null value.
2864 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2865 $args{'DESCRIPTION'}
2866 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2868 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2869 $args{'DESCRIPTION'}
2870 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2873 # if we're not looking to compare with a null value
2875 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2876 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2879 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2880 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2881 $QueueObj->Load( $args{'QUEUE'} );
2882 $args{'QUEUE'} = $QueueObj->Id;
2884 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2887 @rest = ( ENTRYAGGREGATOR => 'AND' )
2888 if ( $CF->Type eq 'SelectMultiple' );
2891 VALUE => $args{VALUE},
2893 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2894 .".{" . $CF->Name . "}",
2895 OPERATOR => $args{OPERATOR},
2900 $self->{'RecalcTicketLimits'} = 1;
2906 # {{{ sub _NextIndex
2910 Keep track of the counter for the array of restrictions
2916 return ( $self->{'restriction_index'}++ );
2923 # {{{ Core bits to make this a DBIx::SearchBuilder object
2928 $self->{'table'} = "Tickets";
2929 $self->{'RecalcTicketLimits'} = 1;
2930 $self->{'looking_at_effective_id'} = 0;
2931 $self->{'looking_at_type'} = 0;
2932 $self->{'restriction_index'} = 1;
2933 $self->{'primary_key'} = "id";
2934 delete $self->{'items_array'};
2935 delete $self->{'item_map'};
2936 delete $self->{'columns_to_display'};
2937 $self->SUPER::_Init(@_);
2948 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2949 return ( $self->SUPER::Count() );
2957 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2958 return ( $self->SUPER::CountAll() );
2963 # {{{ sub ItemsArrayRef
2965 =head2 ItemsArrayRef
2967 Returns a reference to the set of all items found in this search
2974 return $self->{'items_array'} if $self->{'items_array'};
2976 my $placeholder = $self->_ItemsCounter;
2977 $self->GotoFirstItem();
2978 while ( my $item = $self->Next ) {
2979 push( @{ $self->{'items_array'} }, $item );
2981 $self->GotoItem($placeholder);
2982 $self->{'items_array'}
2983 = $self->ItemsOrderBy( $self->{'items_array'} );
2985 return $self->{'items_array'};
2988 sub ItemsArrayRefWindow {
2992 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2994 $self->RowsPerPage( $window );
2996 $self->GotoFirstItem;
2999 while ( my $item = $self->Next ) {
3003 $self->RowsPerPage( $old[1] );
3004 $self->FirstRow( $old[2] );
3005 $self->GotoItem( $old[0] );
3016 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3018 my $Ticket = $self->SUPER::Next;
3019 return $Ticket unless $Ticket;
3021 if ( $Ticket->__Value('Status') eq 'deleted'
3022 && !$self->{'allow_deleted_search'} )
3026 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3027 # if we found a ticket with this option enabled then
3028 # all tickets we found are ACLed, cache this fact
3029 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3030 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3033 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3038 # If the user doesn't have the right to show this ticket
3045 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3046 return $self->SUPER::_DoSearch( @_ );
3051 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3052 return $self->SUPER::_DoCount( @_ );
3058 my $cache_key = 'RolesHasRight;:;ShowTicket';
3060 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3064 my $ACL = RT::ACL->new( $RT::SystemUser );
3065 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3066 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3067 my $principal_alias = $ACL->Join(
3069 FIELD1 => 'PrincipalId',
3070 TABLE2 => 'Principals',
3073 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3076 while ( my $ACE = $ACL->Next ) {
3077 my $role = $ACE->PrincipalType;
3078 my $type = $ACE->ObjectType;
3079 if ( $type eq 'RT::System' ) {
3082 elsif ( $type eq 'RT::Queue' ) {
3083 next if $res{ $role } && !ref $res{ $role };
3084 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3087 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3090 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3094 sub _DirectlyCanSeeIn {
3096 my $id = $self->CurrentUser->id;
3098 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3099 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3103 my $ACL = RT::ACL->new( $RT::SystemUser );
3104 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3105 my $principal_alias = $ACL->Join(
3107 FIELD1 => 'PrincipalId',
3108 TABLE2 => 'Principals',
3111 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3112 my $cgm_alias = $ACL->Join(
3114 FIELD1 => 'PrincipalId',
3115 TABLE2 => 'CachedGroupMembers',
3116 FIELD2 => 'GroupId',
3118 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3119 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3122 while ( my $ACE = $ACL->Next ) {
3123 my $type = $ACE->ObjectType;
3124 if ( $type eq 'RT::System' ) {
3125 # If user is direct member of a group that has the right
3126 # on the system then he can see any ticket
3127 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3130 elsif ( $type eq 'RT::Queue' ) {
3131 push @res, $ACE->ObjectId;
3134 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3137 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3141 sub CurrentUserCanSee {
3143 return if $self->{'_sql_current_user_can_see_applied'};
3145 return $self->{'_sql_current_user_can_see_applied'} = 1
3146 if $self->CurrentUser->UserObj->HasRight(
3147 Right => 'SuperUser', Object => $RT::System
3150 my $id = $self->CurrentUser->id;
3152 # directly can see in all queues then we have nothing to do
3153 my @direct_queues = $self->_DirectlyCanSeeIn;
3154 return $self->{'_sql_current_user_can_see_applied'} = 1
3155 if @direct_queues && $direct_queues[0] == -1;
3157 my %roles = $self->_RolesCanSee;
3159 my %skip = map { $_ => 1 } @direct_queues;
3160 foreach my $role ( keys %roles ) {
3161 next unless ref $roles{ $role };
3163 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3165 $roles{ $role } = \@queues;
3167 delete $roles{ $role };
3172 # there is no global watchers, only queues and tickes, if at
3173 # some point we will add global roles then it's gonna blow
3174 # the idea here is that if the right is set globaly for a role
3175 # and user plays this role for a queue directly not a ticket
3176 # then we have to check in advance
3177 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3179 my $groups = RT::Groups->new( $RT::SystemUser );
3180 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3182 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3184 my $principal_alias = $groups->Join(
3187 TABLE2 => 'Principals',
3190 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3191 my $cgm_alias = $groups->Join(
3194 TABLE2 => 'CachedGroupMembers',
3195 FIELD2 => 'GroupId',
3197 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3198 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3199 while ( my $group = $groups->Next ) {
3200 push @direct_queues, $group->Instance;
3204 unless ( @direct_queues || keys %roles ) {
3205 $self->SUPER::Limit(
3210 ENTRYAGGREGATOR => 'AND',
3212 return $self->{'_sql_current_user_can_see_applied'} = 1;
3216 my $join_roles = keys %roles;
3217 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3218 my ($role_group_alias, $cgm_alias);
3219 if ( $join_roles ) {
3220 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3221 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3222 $self->SUPER::Limit(
3223 LEFTJOIN => $cgm_alias,
3224 FIELD => 'MemberId',
3229 my $limit_queues = sub {
3233 return unless @queues;
3234 if ( @queues == 1 ) {
3235 $self->SUPER::Limit(
3240 ENTRYAGGREGATOR => $ea,
3243 $self->SUPER::_OpenParen('ACL');
3244 foreach my $q ( @queues ) {
3245 $self->SUPER::Limit(
3250 ENTRYAGGREGATOR => $ea,
3254 $self->SUPER::_CloseParen('ACL');
3259 $self->SUPER::_OpenParen('ACL');
3261 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3262 while ( my ($role, $queues) = each %roles ) {
3263 $self->SUPER::_OpenParen('ACL');
3264 if ( $role eq 'Owner' ) {
3265 $self->SUPER::Limit(
3269 ENTRYAGGREGATOR => $ea,
3273 $self->SUPER::Limit(
3275 ALIAS => $cgm_alias,
3276 FIELD => 'MemberId',
3277 OPERATOR => 'IS NOT',
3280 ENTRYAGGREGATOR => $ea,
3282 $self->SUPER::Limit(
3284 ALIAS => $role_group_alias,
3287 ENTRYAGGREGATOR => 'AND',
3290 $limit_queues->( 'AND', @$queues ) if ref $queues;
3291 $ea = 'OR' if $ea eq 'AND';
3292 $self->SUPER::_CloseParen('ACL');
3294 $self->SUPER::_CloseParen('ACL');
3296 return $self->{'_sql_current_user_can_see_applied'} = 1;
3303 # {{{ Deal with storing and restoring restrictions
3305 # {{{ sub LoadRestrictions
3307 =head2 LoadRestrictions
3309 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3310 TODO It is not yet implemented
3316 # {{{ sub DescribeRestrictions
3318 =head2 DescribeRestrictions
3321 Returns a hash keyed by restriction id.
3322 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3323 is a description of the purpose of that TicketRestriction
3327 sub DescribeRestrictions {
3332 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3333 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3340 # {{{ sub RestrictionValues
3342 =head2 RestrictionValues FIELD
3344 Takes a restriction field and returns a list of values this field is restricted
3349 sub RestrictionValues {
3352 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3353 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3354 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3356 keys %{ $self->{'TicketRestrictions'} };
3361 # {{{ sub ClearRestrictions
3363 =head2 ClearRestrictions
3365 Removes all restrictions irretrievably
3369 sub ClearRestrictions {
3371 delete $self->{'TicketRestrictions'};
3372 $self->{'looking_at_effective_id'} = 0;
3373 $self->{'looking_at_type'} = 0;
3374 $self->{'RecalcTicketLimits'} = 1;
3379 # {{{ sub DeleteRestriction
3381 =head2 DeleteRestriction
3383 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3384 Removes that restriction from the session's limits.
3388 sub DeleteRestriction {
3391 delete $self->{'TicketRestrictions'}{$row};
3393 $self->{'RecalcTicketLimits'} = 1;
3395 #make the underlying easysearch object forget all its preconceptions
3400 # {{{ sub _RestrictionsToClauses
3402 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3404 sub _RestrictionsToClauses {
3408 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3409 my $restriction = $self->{'TicketRestrictions'}{$row};
3411 # We need to reimplement the subclause aggregation that SearchBuilder does.
3412 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3413 # Then SB AND's the different Subclauses together.
3415 # So, we want to group things into Subclauses, convert them to
3416 # SQL, and then join them with the appropriate DefaultEA.
3417 # Then join each subclause group with AND.
3419 my $field = $restriction->{'FIELD'};
3420 my $realfield = $field; # CustomFields fake up a fieldname, so
3421 # we need to figure that out
3424 # Rewrite LinkedTo meta field to the real field
3425 if ( $field =~ /LinkedTo/ ) {
3426 $realfield = $field = $restriction->{'TYPE'};
3430 # Handle subkey fields with a different real field
3431 if ( $field =~ /^(\w+)\./ ) {
3435 die "I don't know about $field yet"
3436 unless ( exists $FIELD_METADATA{$realfield}
3437 or $restriction->{CUSTOMFIELD} );
3439 my $type = $FIELD_METADATA{$realfield}->[0];
3440 my $op = $restriction->{'OPERATOR'};
3444 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3447 # this performs the moral equivalent of defined or/dor/C<//>,
3448 # without the short circuiting.You need to use a 'defined or'
3449 # type thing instead of just checking for truth values, because
3450 # VALUE could be 0.(i.e. "false")
3452 # You could also use this, but I find it less aesthetic:
3453 # (although it does short circuit)
3454 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3455 # defined $restriction->{'TICKET'} ?
3456 # $restriction->{TICKET} :
3457 # defined $restriction->{'BASE'} ?
3458 # $restriction->{BASE} :
3459 # defined $restriction->{'TARGET'} ?
3460 # $restriction->{TARGET} )
3462 my $ea = $restriction->{ENTRYAGGREGATOR}
3463 || $DefaultEA{$type}
3466 die "Invalid operator $op for $field ($type)"
3467 unless exists $ea->{$op};
3471 # Each CustomField should be put into a different Clause so they
3472 # are ANDed together.
3473 if ( $restriction->{CUSTOMFIELD} ) {
3474 $realfield = $field;
3477 exists $clause{$realfield} or $clause{$realfield} = [];
3480 $field =~ s!(['"])!\\$1!g;
3481 $value =~ s!(['"])!\\$1!g;
3482 my $data = [ $ea, $type, $field, $op, $value ];
3484 # here is where we store extra data, say if it's a keyword or
3485 # something. (I.e. "TYPE SPECIFIC STUFF")
3487 push @{ $clause{$realfield} }, $data;
3494 # {{{ sub _ProcessRestrictions
3496 =head2 _ProcessRestrictions PARAMHASH
3498 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3499 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3503 sub _ProcessRestrictions {
3506 #Blow away ticket aliases since we'll need to regenerate them for
3508 delete $self->{'TicketAliases'};
3509 delete $self->{'items_array'};
3510 delete $self->{'item_map'};
3511 delete $self->{'raw_rows'};
3512 delete $self->{'rows'};
3513 delete $self->{'count_all'};
3515 my $sql = $self->Query; # Violating the _SQL namespace
3516 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3518 # "Restrictions to Clauses Branch\n";
3519 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3521 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3525 $sql = $self->ClausesToSQL($clauseRef);
3526 $self->FromSQL($sql) if $sql;
3530 $self->{'RecalcTicketLimits'} = 0;
3534 =head2 _BuildItemMap
3536 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3537 display search nav quickly.
3544 my $window = RT->Config->Get('TicketsItemMapSize');
3546 $self->{'item_map'} = {};
3548 my $items = $self->ItemsArrayRefWindow( $window );
3549 return unless $items && @$items;
3552 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3553 for ( my $i = 0; $i < @$items; $i++ ) {
3554 my $item = $items->[$i];
3555 my $id = $item->EffectiveId;
3556 $self->{'item_map'}{$id}{'defined'} = 1;
3557 $self->{'item_map'}{$id}{'prev'} = $prev;
3558 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3562 $self->{'item_map'}{'last'} = $prev
3563 if !$window || @$items < $window;
3568 Returns an a map of all items found by this search. The map is a hash
3572 first => <first ticket id found>,
3573 last => <last ticket id found or undef>,
3576 prev => <the ticket id found before>,
3577 next => <the ticket id found after>,
3589 $self->_BuildItemMap unless $self->{'item_map'};
3590 return $self->{'item_map'};
3598 =head2 PrepForSerialization
3600 You don't want to serialize a big tickets object, as
3601 the {items} hash will be instantly invalid _and_ eat
3606 sub PrepForSerialization {
3608 delete $self->{'items'};
3609 delete $self->{'items_array'};
3610 $self->RedoSearch();
3615 RT::Tickets supports several flags which alter search behavior:
3618 allow_deleted_search (Otherwise never show deleted tickets in search results)
3619 looking_at_type (otherwise limit to type=ticket)
3621 These flags are set by calling
3623 $tickets->{'flagname'} = 1;
3625 BUG: There should be an API for this