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 Customer => [ 'FREESIDEFIELD', ],
150 # Agentnum => [ 'FREESIDEFIELD', ],
151 # Classnum => [ 'FREESIDEFIELD', ],
152 # Refnum => [ 'FREESIDEFIELD', ],
153 # Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ],
154 WillResolve => [ 'DATE' => 'WillResolve', ], #loc_left_pair
157 our %SEARCHABLE_SUBFIELDS = (
159 EmailAddress Name RealName Nickname Organization Address1 Address2
160 WorkPhone HomePhone MobilePhone PagerPhone id
164 # Mapping of Field Type to Function
166 ENUM => \&_EnumLimit,
169 LINK => \&_LinkLimit,
170 DATE => \&_DateLimit,
171 STRING => \&_StringLimit,
172 TRANSFIELD => \&_TransLimit,
173 TRANSDATE => \&_TransDateLimit,
174 WATCHERFIELD => \&_WatcherLimit,
175 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
176 CUSTOMFIELD => \&_CustomFieldLimit,
177 HASATTRIBUTE => \&_HasAttributeLimit,
178 FREESIDEFIELD => \&_FreesideFieldLimit,
180 our %can_bundle = ();# WATCHERFIELD => "yes", );
182 # Default EntryAggregator per type
183 # if you specify OP, you must specify all valid OPs
224 # Helper functions for passing the above lexically scoped tables above
225 # into Tickets_Overlay_SQL.
226 sub FIELDS { return \%FIELD_METADATA }
227 sub dispatch { return \%dispatch }
228 sub can_bundle { return \%can_bundle }
230 # Bring in the clowns.
231 require RT::Tickets_Overlay_SQL;
235 our @SORTFIELDS = qw(id Status
237 Owner Created Due Starts Started
239 Resolved LastUpdated Priority TimeWorked TimeLeft);
243 Returns the list of fields that lists of tickets can easily be sorted by
249 return (@SORTFIELDS);
254 # BEGIN SQL STUFF *********************************
259 $self->SUPER::CleanSlate( @_ );
260 delete $self->{$_} foreach qw(
262 _sql_group_members_aliases
263 _sql_object_cfv_alias
264 _sql_role_group_aliases
267 _sql_u_watchers_alias_for_sort
268 _sql_u_watchers_aliases
269 _sql_current_user_can_see_applied
273 =head1 Limit Helper Routines
275 These routines are the targets of a dispatch table depending on the
276 type of field. They all share the same signature:
278 my ($self,$field,$op,$value,@rest) = @_;
280 The values in @rest should be suitable for passing directly to
281 DBIx::SearchBuilder::Limit.
283 Essentially they are an expanded/broken out (and much simplified)
284 version of what ProcessRestrictions used to do. They're also much
285 more clearly delineated by the TYPE of field being processed.
294 my ( $sb, $field, $op, $value, @rest ) = @_;
296 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
298 die "Invalid operator $op for __Bookmarked__ search on $field"
299 unless $op =~ /^(=|!=)$/;
302 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
303 $tmp = $tmp->Content if $tmp;
308 return $sb->_SQLLimit(
315 # as bookmarked tickets can be merged we have to use a join
316 # but it should be pretty lightweight
317 my $tickets_alias = $sb->Join(
322 FIELD2 => 'EffectiveId',
326 my $ea = $op eq '='? 'OR': 'AND';
327 foreach my $id ( sort @bookmarks ) {
329 ALIAS => $tickets_alias,
333 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
341 Handle Fields which are limited to certain values, and potentially
342 need to be looked up from another class.
344 This subroutine actually handles two different kinds of fields. For
345 some the user is responsible for limiting the values. (i.e. Status,
348 For others, the value specified by the user will be looked by via
352 name of class to lookup in (Optional)
357 my ( $sb, $field, $op, $value, @rest ) = @_;
359 # SQL::Statement changes != to <>. (Can we remove this now?)
360 $op = "!=" if $op eq "<>";
362 die "Invalid Operation: $op for $field"
366 my $meta = $FIELD_METADATA{$field};
367 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
368 my $class = "RT::" . $meta->[1];
369 my $o = $class->new( $sb->CurrentUser );
383 Handle fields where the values are limited to integers. (For example,
384 Priority, TimeWorked.)
392 my ( $sb, $field, $op, $value, @rest ) = @_;
394 die "Invalid Operator $op for $field"
395 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
407 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
410 1: Direction (From, To)
411 2: Link Type (MemberOf, DependsOn, RefersTo)
416 my ( $sb, $field, $op, $value, @rest ) = @_;
418 my $meta = $FIELD_METADATA{$field};
419 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
422 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
426 $is_null = 1 if !$value || $value =~ /^null$/io;
428 my $direction = $meta->[1] || '';
429 my ($matchfield, $linkfield) = ('', '');
430 if ( $direction eq 'To' ) {
431 ($matchfield, $linkfield) = ("Target", "Base");
433 elsif ( $direction eq 'From' ) {
434 ($matchfield, $linkfield) = ("Base", "Target");
436 elsif ( $direction ) {
437 die "Invalid link direction '$direction' for $field\n";
440 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
442 'LinkedFrom', $op, $value, @rest,
443 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
451 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
453 elsif ( $value =~ /\D/ ) {
456 $matchfield = "Local$matchfield" if $is_local;
458 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
459 # SELECT main.* FROM Tickets main
460 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
461 # AND(main.id = Links_1.LocalTarget))
462 # WHERE Links_1.LocalBase IS NULL;
465 my $linkalias = $sb->Join(
470 FIELD2 => 'Local' . $linkfield
473 LEFTJOIN => $linkalias,
481 FIELD => $matchfield,
488 my $linkalias = $sb->Join(
493 FIELD2 => 'Local' . $linkfield
496 LEFTJOIN => $linkalias,
502 LEFTJOIN => $linkalias,
503 FIELD => $matchfield,
510 FIELD => $matchfield,
511 OPERATOR => $is_negative? 'IS': 'IS NOT',
520 Handle date fields. (Created, LastTold..)
523 1: type of link. (Probably not necessary.)
528 my ( $sb, $field, $op, $value, @rest ) = @_;
530 die "Invalid Date Op: $op"
531 unless $op =~ /^(=|>|<|>=|<=)$/;
533 my $meta = $FIELD_METADATA{$field};
534 die "Incorrect Meta Data for $field"
535 unless ( defined $meta->[1] );
537 $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest );
540 # Factor this out for use by custom fields
542 sub _DateFieldLimit {
543 my ( $sb, $field, $op, $value, @rest ) = @_;
545 my $date = RT::Date->new( $sb->CurrentUser );
546 $date->Set( Format => 'unknown', Value => $value );
550 # if we're specifying =, that means we want everything on a
551 # particular single day. in the database, we need to check for >
552 # and < the edges of that day.
554 # Except if the value is 'this month' or 'last month', check
555 # > and < the edges of the month.
557 my ($daystart, $dayend);
558 if ( lc($value) eq 'this month' ) {
560 $date->SetToStart('month', Timezone => 'server');
561 $daystart = $date->ISO;
562 $date->AddMonth(Timezone => 'server');
563 $dayend = $date->ISO;
565 elsif ( lc($value) eq 'last month' ) {
567 $date->SetToStart('month', Timezone => 'server');
568 $dayend = $date->ISO;
570 $date->SetToStart('month', Timezone => 'server');
571 $daystart = $date->ISO;
574 $date->SetToMidnight( Timezone => 'server' );
575 $daystart = $date->ISO;
577 $dayend = $date->ISO;
594 ENTRYAGGREGATOR => 'AND',
612 Handle simple fields which are just strings. (Subject,Type)
620 my ( $sb, $field, $op, $value, @rest ) = @_;
624 # =, !=, LIKE, NOT LIKE
625 if ( (!defined $value || !length $value)
626 && lc($op) ne 'is' && lc($op) ne 'is not'
627 && RT->Config->Get('DatabaseType') eq 'Oracle'
629 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
630 $op = $negative? 'IS NOT': 'IS';
643 =head2 _TransDateLimit
645 Handle fields limiting based on Transaction Date.
647 The inpupt value must be in a format parseable by Time::ParseDate
654 # This routine should really be factored into translimit.
655 sub _TransDateLimit {
656 my ( $sb, $field, $op, $value, @rest ) = @_;
658 # See the comments for TransLimit, they apply here too
660 unless ( $sb->{_sql_transalias} ) {
661 $sb->{_sql_transalias} = $sb->Join(
664 TABLE2 => 'Transactions',
665 FIELD2 => 'ObjectId',
668 ALIAS => $sb->{_sql_transalias},
669 FIELD => 'ObjectType',
670 VALUE => 'RT::Ticket',
671 ENTRYAGGREGATOR => 'AND',
675 my $date = RT::Date->new( $sb->CurrentUser );
676 $date->Set( Format => 'unknown', Value => $value );
681 # if we're specifying =, that means we want everything on a
682 # particular single day. in the database, we need to check for >
683 # and < the edges of that day.
685 $date->SetToMidnight( Timezone => 'server' );
686 my $daystart = $date->ISO;
688 my $dayend = $date->ISO;
691 ALIAS => $sb->{_sql_transalias},
699 ALIAS => $sb->{_sql_transalias},
705 ENTRYAGGREGATOR => 'AND',
710 # not searching for a single day
713 #Search for the right field
715 ALIAS => $sb->{_sql_transalias},
729 Limit based on the Content of a transaction or the ContentType.
738 # Content, ContentType, Filename
740 # If only this was this simple. We've got to do something
743 #Basically, we want to make sure that the limits apply to
744 #the same attachment, rather than just another attachment
745 #for the same ticket, no matter how many clauses we lump
746 #on. We put them in TicketAliases so that they get nuked
747 #when we redo the join.
749 # In the SQL, we might have
750 # (( Content = foo ) or ( Content = bar AND Content = baz ))
751 # The AND group should share the same Alias.
753 # Actually, maybe it doesn't matter. We use the same alias and it
754 # works itself out? (er.. different.)
756 # Steal more from _ProcessRestrictions
758 # FIXME: Maybe look at the previous FooLimit call, and if it was a
759 # TransLimit and EntryAggregator == AND, reuse the Aliases?
761 # Or better - store the aliases on a per subclause basis - since
762 # those are going to be the things we want to relate to each other,
765 # maybe we should not allow certain kinds of aggregation of these
766 # clauses and do a psuedo regex instead? - the problem is getting
767 # them all into the same subclause when you have (A op B op C) - the
768 # way they get parsed in the tree they're in different subclauses.
770 my ( $self, $field, $op, $value, %rest ) = @_;
772 unless ( $self->{_sql_transalias} ) {
773 $self->{_sql_transalias} = $self->Join(
776 TABLE2 => 'Transactions',
777 FIELD2 => 'ObjectId',
780 ALIAS => $self->{_sql_transalias},
781 FIELD => 'ObjectType',
782 VALUE => 'RT::Ticket',
783 ENTRYAGGREGATOR => 'AND',
786 unless ( defined $self->{_sql_trattachalias} ) {
787 $self->{_sql_trattachalias} = $self->_SQLJoin(
788 TYPE => 'LEFT', # not all txns have an attachment
789 ALIAS1 => $self->{_sql_transalias},
791 TABLE2 => 'Attachments',
792 FIELD2 => 'TransactionId',
796 #Search for the right field
797 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
801 ALIAS => $self->{_sql_trattachalias},
808 ENTRYAGGREGATOR => 'AND',
809 ALIAS => $self->{_sql_trattachalias},
818 ALIAS => $self->{_sql_trattachalias},
831 Handle watcher limits. (Requestor, CC, etc..)
847 my $meta = $FIELD_METADATA{ $field };
848 my $type = $meta->[1] || '';
849 my $class = $meta->[2] || 'Ticket';
851 # Bail if the subfield is not allowed
853 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
855 die "Invalid watcher subfield: '$rest{SUBKEY}'";
858 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
859 # search by id and Name at the same time, this is workaround
860 # to preserve backward compatibility
861 if ( $field eq 'Owner' ) {
862 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
863 my $o = RT::User->new( $self->CurrentUser );
864 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
865 $o->$method( $value );
874 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
884 $rest{SUBKEY} ||= 'EmailAddress';
886 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
889 if ( $op =~ /^IS(?: NOT)?$/ ) {
890 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
891 # to avoid joining the table Users into the query, we just join GM
892 # and make sure we don't match records where group is member of itself
894 LEFTJOIN => $group_members,
897 VALUE => "$group_members.MemberId",
901 ALIAS => $group_members,
908 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
910 $op =~ s/!|NOT\s+//i;
912 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
913 # "X = 'Y'" matches more then one user so we try to fetch two records and
914 # do the right thing when there is only one exist and semi-working solution
916 my $users_obj = RT::Users->new( $self->CurrentUser );
918 FIELD => $rest{SUBKEY},
923 $users_obj->RowsPerPage(2);
924 my @users = @{ $users_obj->ItemsArrayRef };
926 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
929 $uid = $users[0]->id if @users;
931 LEFTJOIN => $group_members,
932 ALIAS => $group_members,
938 ALIAS => $group_members,
945 LEFTJOIN => $group_members,
948 VALUE => "$group_members.MemberId",
951 my $users = $self->Join(
953 ALIAS1 => $group_members,
954 FIELD1 => 'MemberId',
961 FIELD => $rest{SUBKEY},
975 my $group_members = $self->_GroupMembersJoin(
976 GroupsAlias => $groups,
980 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
982 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
983 $self->NewAlias('Users');
985 LEFTJOIN => $group_members,
986 ALIAS => $group_members,
988 VALUE => "$users.id",
993 # we join users table without adding some join condition between tables,
994 # the only conditions we have are conditions on the table iteslf,
995 # for example Users.EmailAddress = 'x'. We should add this condition to
996 # the top level of the query and bundle it with another similar conditions,
997 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
998 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
1001 SUBCLAUSE => '_sql_u_watchers_'. $users,
1003 FIELD => $rest{'SUBKEY'},
1008 # A condition which ties Users and Groups (role groups) is a left join condition
1009 # of CachedGroupMembers table. To get correct results of the query we check
1010 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
1013 ALIAS => $group_members,
1015 OPERATOR => 'IS NOT',
1022 sub _RoleGroupsJoin {
1024 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1025 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1026 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1029 # we always have watcher groups for ticket, so we use INNER join
1030 my $groups = $self->Join(
1032 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1034 FIELD2 => 'Instance',
1035 ENTRYAGGREGATOR => 'AND',
1037 $self->SUPER::Limit(
1038 LEFTJOIN => $groups,
1041 VALUE => 'RT::'. $args{'Class'} .'-Role',
1043 $self->SUPER::Limit(
1044 LEFTJOIN => $groups,
1047 VALUE => $args{'Type'},
1050 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1051 unless $args{'New'};
1056 sub _GroupMembersJoin {
1058 my %args = (New => 1, GroupsAlias => undef, @_);
1060 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1061 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1064 my $alias = $self->Join(
1066 ALIAS1 => $args{'GroupsAlias'},
1068 TABLE2 => 'CachedGroupMembers',
1069 FIELD2 => 'GroupId',
1070 ENTRYAGGREGATOR => 'AND',
1073 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1074 unless $args{'New'};
1081 Helper function which provides joins to a watchers table both for limits
1088 my $type = shift || '';
1091 my $groups = $self->_RoleGroupsJoin( Type => $type );
1092 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1093 # XXX: work around, we must hide groups that
1094 # are members of the role group we search in,
1095 # otherwise them result in wrong NULLs in Users
1096 # table and break ordering. Now, we know that
1097 # RT doesn't allow to add groups as members of the
1098 # ticket roles, so we just hide entries in CGM table
1099 # with MemberId == GroupId from results
1100 $self->SUPER::Limit(
1101 LEFTJOIN => $group_members,
1104 VALUE => "$group_members.MemberId",
1107 my $users = $self->Join(
1109 ALIAS1 => $group_members,
1110 FIELD1 => 'MemberId',
1114 return ($groups, $group_members, $users);
1117 =head2 _WatcherMembershipLimit
1119 Handle watcher membership limits, i.e. whether the watcher belongs to a
1120 specific group or not.
1123 1: Field to query on
1125 SELECT DISTINCT main.*
1129 CachedGroupMembers CachedGroupMembers_2,
1132 (main.EffectiveId = main.id)
1134 (main.Status != 'deleted')
1136 (main.Type = 'ticket')
1139 (Users_3.EmailAddress = '22')
1141 (Groups_1.Domain = 'RT::Ticket-Role')
1143 (Groups_1.Type = 'RequestorGroup')
1146 Groups_1.Instance = main.id
1148 Groups_1.id = CachedGroupMembers_2.GroupId
1150 CachedGroupMembers_2.MemberId = Users_3.id
1151 ORDER BY main.id ASC
1156 sub _WatcherMembershipLimit {
1157 my ( $self, $field, $op, $value, @rest ) = @_;
1162 my $groups = $self->NewAlias('Groups');
1163 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1164 my $users = $self->NewAlias('Users');
1165 my $memberships = $self->NewAlias('CachedGroupMembers');
1167 if ( ref $field ) { # gross hack
1168 my @bundle = @$field;
1170 for my $chunk (@bundle) {
1171 ( $field, $op, $value, @rest ) = @$chunk;
1173 ALIAS => $memberships,
1184 ALIAS => $memberships,
1192 # {{{ Tie to groups for tickets we care about
1196 VALUE => 'RT::Ticket-Role',
1197 ENTRYAGGREGATOR => 'AND'
1202 FIELD1 => 'Instance',
1209 # If we care about which sort of watcher
1210 my $meta = $FIELD_METADATA{$field};
1211 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1218 ENTRYAGGREGATOR => 'AND'
1225 ALIAS2 => $groupmembers,
1230 ALIAS1 => $groupmembers,
1231 FIELD1 => 'MemberId',
1237 ALIAS1 => $memberships,
1238 FIELD1 => 'MemberId',
1247 =head2 _CustomFieldDecipher
1249 Try and turn a CF descriptor into (cfid, cfname) object pair.
1253 sub _CustomFieldDecipher {
1254 my ($self, $string) = @_;
1256 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1257 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1261 my $q = RT::Queue->new( $self->CurrentUser );
1265 # $queue = $q->Name; # should we normalize the queue?
1266 $cf = $q->CustomField( $field );
1269 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1273 elsif ( $field =~ /\D/ ) {
1275 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1276 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1277 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1279 # if there is more then one field the current user can
1280 # see with the same name then we shouldn't return cf object
1281 # as we don't know which one to use
1284 $cf = undef if $cfs->Next;
1288 $cf = RT::CustomField->new( $self->CurrentUser );
1289 $cf->Load( $field );
1292 return ($queue, $field, $cf, $column);
1295 =head2 _CustomFieldJoin
1297 Factor out the Join of custom fields so we can use it for sorting too
1301 sub _CustomFieldJoin {
1302 my ($self, $cfkey, $cfid, $field) = @_;
1303 # Perform one Join per CustomField
1304 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1305 $self->{_sql_cf_alias}{$cfkey} )
1307 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1308 $self->{_sql_cf_alias}{$cfkey} );
1311 my ($TicketCFs, $CFs);
1313 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1317 TABLE2 => 'ObjectCustomFieldValues',
1318 FIELD2 => 'ObjectId',
1320 $self->SUPER::Limit(
1321 LEFTJOIN => $TicketCFs,
1322 FIELD => 'CustomField',
1324 ENTRYAGGREGATOR => 'AND'
1328 my $ocfalias = $self->Join(
1331 TABLE2 => 'ObjectCustomFields',
1332 FIELD2 => 'ObjectId',
1335 $self->SUPER::Limit(
1336 LEFTJOIN => $ocfalias,
1337 ENTRYAGGREGATOR => 'OR',
1338 FIELD => 'ObjectId',
1342 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1344 ALIAS1 => $ocfalias,
1345 FIELD1 => 'CustomField',
1346 TABLE2 => 'CustomFields',
1349 $self->SUPER::Limit(
1351 ENTRYAGGREGATOR => 'AND',
1352 FIELD => 'LookupType',
1353 VALUE => 'RT::Queue-RT::Ticket',
1355 $self->SUPER::Limit(
1357 ENTRYAGGREGATOR => 'AND',
1362 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1366 TABLE2 => 'ObjectCustomFieldValues',
1367 FIELD2 => 'CustomField',
1369 $self->SUPER::Limit(
1370 LEFTJOIN => $TicketCFs,
1371 FIELD => 'ObjectId',
1374 ENTRYAGGREGATOR => 'AND',
1377 $self->SUPER::Limit(
1378 LEFTJOIN => $TicketCFs,
1379 FIELD => 'ObjectType',
1380 VALUE => 'RT::Ticket',
1381 ENTRYAGGREGATOR => 'AND'
1383 $self->SUPER::Limit(
1384 LEFTJOIN => $TicketCFs,
1385 FIELD => 'Disabled',
1388 ENTRYAGGREGATOR => 'AND'
1391 return ($TicketCFs, $CFs);
1394 =head2 _CustomFieldLimit
1396 Limit based on CustomFields
1403 sub _CustomFieldLimit {
1404 my ( $self, $_field, $op, $value, %rest ) = @_;
1406 my $field = $rest{'SUBKEY'} || die "No field specified";
1408 # For our sanity, we can only limit on one queue at a time
1410 my ($queue, $cfid, $cf, $column);
1411 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1412 $cfid = $cf ? $cf->id : 0 ;
1414 # If we're trying to find custom fields that don't match something, we
1415 # want tickets where the custom field has no value at all. Note that
1416 # we explicitly don't include the "IS NULL" case, since we would
1417 # otherwise end up with a redundant clause.
1419 my ($negative_op, $null_op, $inv_op, $range_op)
1420 = $self->ClassifySQLOperation( $op );
1424 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1425 return 'MATCHES' if $op eq '=';
1426 return 'NOT MATCHES' if $op eq '!=';
1430 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1432 my $cfkey = $cfid ? $cfid : "$queue.$field";
1434 if ( $null_op && !$column ) {
1435 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1436 # we can reuse our default joins for this operation
1437 # with column specified we have different situation
1438 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1441 ALIAS => $TicketCFs,
1450 OPERATOR => 'IS NOT',
1453 ENTRYAGGREGATOR => 'AND',
1457 elsif ( !$negative_op || $single_value ) {
1458 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1459 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1466 # if column is defined then deal only with it
1467 # otherwise search in Content and in LargeContent
1470 ALIAS => $TicketCFs,
1472 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1477 elsif ( $cfid and $cf->Type eq 'Date' ) {
1478 $self->_DateFieldLimit(
1482 ALIAS => $TicketCFs,
1486 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1487 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1489 ALIAS => $TicketCFs,
1498 ALIAS => $TicketCFs,
1502 ENTRYAGGREGATOR => 'OR'
1505 ALIAS => $TicketCFs,
1509 ENTRYAGGREGATOR => 'OR'
1513 ALIAS => $TicketCFs,
1514 FIELD => 'LargeContent',
1515 OPERATOR => $fix_op->($op),
1517 ENTRYAGGREGATOR => 'AND',
1523 ALIAS => $TicketCFs,
1533 ALIAS => $TicketCFs,
1537 ENTRYAGGREGATOR => 'OR'
1540 ALIAS => $TicketCFs,
1544 ENTRYAGGREGATOR => 'OR'
1548 ALIAS => $TicketCFs,
1549 FIELD => 'LargeContent',
1550 OPERATOR => $fix_op->($op),
1552 ENTRYAGGREGATOR => 'AND',
1558 # XXX: if we join via CustomFields table then
1559 # because of order of left joins we get NULLs in
1560 # CF table and then get nulls for those records
1561 # in OCFVs table what result in wrong results
1562 # as decifer method now tries to load a CF then
1563 # we fall into this situation only when there
1564 # are more than one CF with the name in the DB.
1565 # the same thing applies to order by call.
1566 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1567 # we want treat IS NULL as (not applies or has
1572 OPERATOR => 'IS NOT',
1575 ENTRYAGGREGATOR => 'AND',
1581 ALIAS => $TicketCFs,
1582 FIELD => $column || 'Content',
1586 ENTRYAGGREGATOR => 'OR',
1593 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1594 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1597 $op =~ s/!|NOT\s+//i;
1599 # if column is defined then deal only with it
1600 # otherwise search in Content and in LargeContent
1602 $self->SUPER::Limit(
1603 LEFTJOIN => $TicketCFs,
1604 ALIAS => $TicketCFs,
1606 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1611 $self->SUPER::Limit(
1612 LEFTJOIN => $TicketCFs,
1613 ALIAS => $TicketCFs,
1621 ALIAS => $TicketCFs,
1630 sub _HasAttributeLimit {
1631 my ( $self, $field, $op, $value, %rest ) = @_;
1633 my $alias = $self->Join(
1637 TABLE2 => 'Attributes',
1638 FIELD2 => 'ObjectId',
1640 $self->SUPER::Limit(
1642 FIELD => 'ObjectType',
1643 VALUE => 'RT::Ticket',
1644 ENTRYAGGREGATOR => 'AND'
1646 $self->SUPER::Limit(
1651 ENTRYAGGREGATOR => 'AND'
1657 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1663 # End Helper Functions
1665 # End of SQL Stuff -------------------------------------------------
1667 # {{{ Allow sorting on watchers
1669 =head2 OrderByCols ARRAY
1671 A modified version of the OrderBy method which automatically joins where
1672 C<ALIAS> is set to the name of a watcher type.
1683 foreach my $row (@args) {
1684 if ( $row->{ALIAS} ) {
1688 if ( $row->{FIELD} !~ /\./ ) {
1689 my $meta = $self->FIELDS->{ $row->{FIELD} };
1695 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1696 my $alias = $self->Join(
1699 FIELD1 => $row->{'FIELD'},
1703 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1704 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1705 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1707 my $alias = $self->Join(
1710 FIELD1 => $row->{'FIELD'},
1714 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1721 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1722 my $meta = $self->FIELDS->{$field};
1723 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1724 # cache alias as we want to use one alias per watcher type for sorting
1725 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1727 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1728 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1730 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1731 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1732 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1733 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1734 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1735 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1736 # this is described in _CustomFieldLimit
1740 OPERATOR => 'IS NOT',
1743 ENTRYAGGREGATOR => 'AND',
1746 # For those cases where we are doing a join against the
1747 # CF name, and don't have a CFid, use Unique to make sure
1748 # we don't show duplicate tickets. NOTE: I'm pretty sure
1749 # this will stay mixed in for the life of the
1750 # class/package, and not just for the life of the object.
1751 # Potential performance issue.
1752 require DBIx::SearchBuilder::Unique;
1753 DBIx::SearchBuilder::Unique->import;
1755 my $CFvs = $self->Join(
1757 ALIAS1 => $TicketCFs,
1758 FIELD1 => 'CustomField',
1759 TABLE2 => 'CustomFieldValues',
1760 FIELD2 => 'CustomField',
1762 $self->SUPER::Limit(
1766 VALUE => $TicketCFs . ".Content",
1767 ENTRYAGGREGATOR => 'AND'
1770 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1771 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1772 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1773 # PAW logic is "reversed"
1775 if (exists $row->{ORDER} ) {
1776 my $o = $row->{ORDER};
1777 delete $row->{ORDER};
1778 $order = "DESC" if $o =~ /asc/i;
1781 # Ticket.Owner 1 0 X
1782 # Unowned Tickets 0 1 X
1785 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1786 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1787 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1792 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1799 FUNCTION => "Owner=$uid",
1805 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1807 } elsif ( $field eq 'Customer' ) { #Freeside
1808 # OrderBy(FIELD => expression) doesn't work, it has to be
1809 # an actual field, so we have to do the join even if sorting
1811 my $custalias = $self->JoinToCustomer;
1812 my $cust_field = lc($subkey);
1813 if ( !$cust_field or $cust_field eq 'number' ) {
1814 $cust_field = 'custnum';
1816 elsif ( $cust_field eq 'name' ) {
1817 $cust_field = "COALESCE( $custalias.company,
1818 $custalias.last || ', ' || $custalias.first
1821 else { # order by cust_main fields directly: 'Customer.agentnum'
1822 $cust_field = $subkey;
1824 push @res, { %$row, ALIAS => $custalias, FIELD => $cust_field };
1832 return $self->SUPER::OrderByCols(@res);
1837 sub JoinToCustLinks {
1838 # Set up join to links (id = localbase),
1839 # limit link type to 'MemberOf',
1840 # and target value to any Freeside custnum URI.
1841 # Return the linkalias for further join/limit action,
1842 # and an sql expression to retrieve the custnum.
1844 # only join once for each RT::Tickets object
1845 my $linkalias = $self->{cust_linkalias};
1847 $linkalias = $self->Join(
1852 FIELD2 => 'LocalBase',
1855 $self->SUPER::Limit(
1856 LEFTJOIN => $linkalias,
1859 VALUE => 'MemberOf',
1861 $self->SUPER::Limit(
1862 LEFTJOIN => $linkalias,
1864 OPERATOR => 'STARTSWITH',
1865 VALUE => 'freeside://freeside/cust_main/',
1867 $self->{cust_linkalias} = $linkalias;
1869 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1870 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1871 $custnum_sql .= 'SIGNED INTEGER)';
1874 $custnum_sql .= 'INTEGER)';
1876 return ($linkalias, $custnum_sql);
1879 sub JoinToCustomer {
1881 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1882 # don't reuse this join, though--negative queries need
1884 my $custalias = $self->Join(
1886 EXPRESSION => $custnum_sql,
1887 TABLE2 => 'cust_main',
1888 FIELD2 => 'custnum',
1893 sub _FreesideFieldLimit {
1894 my ( $self, $field, $op, $value, %rest ) = @_;
1895 my $alias = $self->JoinToCustomer;
1896 my $is_negative = 0;
1897 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1898 # if the op is negative, do the join as though
1899 # the op were positive, then accept only records
1900 # where the right-side join key is null.
1902 $op = '=' if $op eq '!=';
1906 my $cust_field = $rest{SUBKEY} || 'custnum';
1908 # compound subkey: separate into table name and field in that table
1909 # (must be linked by custnum)
1910 ($table2, $cust_field) = ($1, $2) if $cust_field =~ /^(\w+)?\.(\w+)$/;
1912 $cust_field = lc($cust_field);
1913 $cust_field = 'custnum' if !$cust_field or $cust_field eq 'number';
1916 $alias = $self->Join(
1919 FIELD1 => 'custnum',
1921 FIELD2 => 'custnum',
1925 $self->SUPER::Limit(
1927 FIELD => $cust_field,
1930 ENTRYAGGREGATOR => 'AND',
1936 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1946 # {{{ Limit the result set based on content
1952 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1953 Generally best called from LimitFoo methods
1963 DESCRIPTION => undef,
1966 $args{'DESCRIPTION'} = $self->loc(
1967 "[_1] [_2] [_3]", $args{'FIELD'},
1968 $args{'OPERATOR'}, $args{'VALUE'}
1970 if ( !defined $args{'DESCRIPTION'} );
1972 my $index = $self->_NextIndex;
1974 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1976 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1978 $self->{'RecalcTicketLimits'} = 1;
1980 # If we're looking at the effective id, we don't want to append the other clause
1981 # which limits us to tickets where id = effective id
1982 if ( $args{'FIELD'} eq 'EffectiveId'
1983 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1985 $self->{'looking_at_effective_id'} = 1;
1988 if ( $args{'FIELD'} eq 'Type'
1989 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1991 $self->{'looking_at_type'} = 1;
2001 Returns a frozen string suitable for handing back to ThawLimits.
2005 sub _FreezeThawKeys {
2006 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2010 # {{{ sub FreezeLimits
2015 require MIME::Base64;
2016 MIME::Base64::base64_encode(
2017 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2024 Take a frozen Limits string generated by FreezeLimits and make this tickets
2025 object have that set of limits.
2029 # {{{ sub ThawLimits
2035 #if we don't have $in, get outta here.
2036 return undef unless ($in);
2038 $self->{'RecalcTicketLimits'} = 1;
2041 require MIME::Base64;
2043 #We don't need to die if the thaw fails.
2044 @{$self}{ $self->_FreezeThawKeys }
2045 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2047 $RT::Logger->error($@) if $@;
2053 # {{{ Limit by enum or foreign key
2055 # {{{ sub LimitQueue
2059 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2060 OPERATOR is one of = or !=. (It defaults to =).
2061 VALUE is a queue id or Name.
2074 #TODO VALUE should also take queue objects
2075 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2076 my $queue = new RT::Queue( $self->CurrentUser );
2077 $queue->Load( $args{'VALUE'} );
2078 $args{'VALUE'} = $queue->Id;
2081 # What if they pass in an Id? Check for isNum() and convert to
2084 #TODO check for a valid queue here
2088 VALUE => $args{'VALUE'},
2089 OPERATOR => $args{'OPERATOR'},
2090 DESCRIPTION => join(
2091 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2099 # {{{ sub LimitStatus
2103 Takes a paramhash with the fields OPERATOR and VALUE.
2104 OPERATOR is one of = or !=.
2107 RT adds Status != 'deleted' until object has
2108 allow_deleted_search internal property set.
2109 $tickets->{'allow_deleted_search'} = 1;
2110 $tickets->LimitStatus( VALUE => 'deleted' );
2122 VALUE => $args{'VALUE'},
2123 OPERATOR => $args{'OPERATOR'},
2124 DESCRIPTION => join( ' ',
2125 $self->loc('Status'), $args{'OPERATOR'},
2126 $self->loc( $args{'VALUE'} ) ),
2132 # {{{ sub IgnoreType
2136 If called, this search will not automatically limit the set of results found
2137 to tickets of type "Ticket". Tickets of other types, such as "project" and
2138 "approval" will be found.
2145 # Instead of faking a Limit that later gets ignored, fake up the
2146 # fact that we're already looking at type, so that the check in
2147 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2149 # $self->LimitType(VALUE => '__any');
2150 $self->{looking_at_type} = 1;
2159 Takes a paramhash with the fields OPERATOR and VALUE.
2160 OPERATOR is one of = or !=, it defaults to "=".
2161 VALUE is a string to search for in the type of the ticket.
2176 VALUE => $args{'VALUE'},
2177 OPERATOR => $args{'OPERATOR'},
2178 DESCRIPTION => join( ' ',
2179 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2187 # {{{ Limit by string field
2189 # {{{ sub LimitSubject
2193 Takes a paramhash with the fields OPERATOR and VALUE.
2194 OPERATOR is one of = or !=.
2195 VALUE is a string to search for in the subject of the ticket.
2204 VALUE => $args{'VALUE'},
2205 OPERATOR => $args{'OPERATOR'},
2206 DESCRIPTION => join( ' ',
2207 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2215 # {{{ Limit based on ticket numerical attributes
2216 # Things that can be > < = !=
2222 Takes a paramhash with the fields OPERATOR and VALUE.
2223 OPERATOR is one of =, >, < or !=.
2224 VALUE is a ticket Id to search for
2237 VALUE => $args{'VALUE'},
2238 OPERATOR => $args{'OPERATOR'},
2240 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2246 # {{{ sub LimitPriority
2248 =head2 LimitPriority
2250 Takes a paramhash with the fields OPERATOR and VALUE.
2251 OPERATOR is one of =, >, < or !=.
2252 VALUE is a value to match the ticket\'s priority against
2260 FIELD => 'Priority',
2261 VALUE => $args{'VALUE'},
2262 OPERATOR => $args{'OPERATOR'},
2263 DESCRIPTION => join( ' ',
2264 $self->loc('Priority'),
2265 $args{'OPERATOR'}, $args{'VALUE'}, ),
2271 # {{{ sub LimitInitialPriority
2273 =head2 LimitInitialPriority
2275 Takes a paramhash with the fields OPERATOR and VALUE.
2276 OPERATOR is one of =, >, < or !=.
2277 VALUE is a value to match the ticket\'s initial priority against
2282 sub LimitInitialPriority {
2286 FIELD => 'InitialPriority',
2287 VALUE => $args{'VALUE'},
2288 OPERATOR => $args{'OPERATOR'},
2289 DESCRIPTION => join( ' ',
2290 $self->loc('Initial Priority'), $args{'OPERATOR'},
2297 # {{{ sub LimitFinalPriority
2299 =head2 LimitFinalPriority
2301 Takes a paramhash with the fields OPERATOR and VALUE.
2302 OPERATOR is one of =, >, < or !=.
2303 VALUE is a value to match the ticket\'s final priority against
2307 sub LimitFinalPriority {
2311 FIELD => 'FinalPriority',
2312 VALUE => $args{'VALUE'},
2313 OPERATOR => $args{'OPERATOR'},
2314 DESCRIPTION => join( ' ',
2315 $self->loc('Final Priority'), $args{'OPERATOR'},
2322 # {{{ sub LimitTimeWorked
2324 =head2 LimitTimeWorked
2326 Takes a paramhash with the fields OPERATOR and VALUE.
2327 OPERATOR is one of =, >, < or !=.
2328 VALUE is a value to match the ticket's TimeWorked attribute
2332 sub LimitTimeWorked {
2336 FIELD => 'TimeWorked',
2337 VALUE => $args{'VALUE'},
2338 OPERATOR => $args{'OPERATOR'},
2339 DESCRIPTION => join( ' ',
2340 $self->loc('Time Worked'),
2341 $args{'OPERATOR'}, $args{'VALUE'}, ),
2347 # {{{ sub LimitTimeLeft
2349 =head2 LimitTimeLeft
2351 Takes a paramhash with the fields OPERATOR and VALUE.
2352 OPERATOR is one of =, >, < or !=.
2353 VALUE is a value to match the ticket's TimeLeft attribute
2361 FIELD => 'TimeLeft',
2362 VALUE => $args{'VALUE'},
2363 OPERATOR => $args{'OPERATOR'},
2364 DESCRIPTION => join( ' ',
2365 $self->loc('Time Left'),
2366 $args{'OPERATOR'}, $args{'VALUE'}, ),
2374 # {{{ Limiting based on attachment attributes
2376 # {{{ sub LimitContent
2380 Takes a paramhash with the fields OPERATOR and VALUE.
2381 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2382 VALUE is a string to search for in the body of the ticket
2391 VALUE => $args{'VALUE'},
2392 OPERATOR => $args{'OPERATOR'},
2393 DESCRIPTION => join( ' ',
2394 $self->loc('Ticket content'), $args{'OPERATOR'},
2401 # {{{ sub LimitFilename
2403 =head2 LimitFilename
2405 Takes a paramhash with the fields OPERATOR and VALUE.
2406 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2407 VALUE is a string to search for in the body of the ticket
2415 FIELD => 'Filename',
2416 VALUE => $args{'VALUE'},
2417 OPERATOR => $args{'OPERATOR'},
2418 DESCRIPTION => join( ' ',
2419 $self->loc('Attachment filename'), $args{'OPERATOR'},
2425 # {{{ sub LimitContentType
2427 =head2 LimitContentType
2429 Takes a paramhash with the fields OPERATOR and VALUE.
2430 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2431 VALUE is a content type to search ticket attachments for
2435 sub LimitContentType {
2439 FIELD => 'ContentType',
2440 VALUE => $args{'VALUE'},
2441 OPERATOR => $args{'OPERATOR'},
2442 DESCRIPTION => join( ' ',
2443 $self->loc('Ticket content type'), $args{'OPERATOR'},
2452 # {{{ Limiting based on people
2454 # {{{ sub LimitOwner
2458 Takes a paramhash with the fields OPERATOR and VALUE.
2459 OPERATOR is one of = or !=.
2471 my $owner = new RT::User( $self->CurrentUser );
2472 $owner->Load( $args{'VALUE'} );
2474 # FIXME: check for a valid $owner
2477 VALUE => $args{'VALUE'},
2478 OPERATOR => $args{'OPERATOR'},
2479 DESCRIPTION => join( ' ',
2480 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2487 # {{{ Limiting watchers
2489 # {{{ sub LimitWatcher
2493 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2494 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2495 VALUE is a value to match the ticket\'s watcher email addresses against
2496 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2510 #build us up a description
2511 my ( $watcher_type, $desc );
2512 if ( $args{'TYPE'} ) {
2513 $watcher_type = $args{'TYPE'};
2516 $watcher_type = "Watcher";
2520 FIELD => $watcher_type,
2521 VALUE => $args{'VALUE'},
2522 OPERATOR => $args{'OPERATOR'},
2523 TYPE => $args{'TYPE'},
2524 DESCRIPTION => join( ' ',
2525 $self->loc($watcher_type),
2526 $args{'OPERATOR'}, $args{'VALUE'}, ),
2536 # {{{ Limiting based on links
2540 =head2 LimitLinkedTo
2542 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2543 TYPE limits the sort of link we want to search on
2545 TYPE = { RefersTo, MemberOf, DependsOn }
2547 TARGET is the id or URI of the TARGET of the link
2561 FIELD => 'LinkedTo',
2563 TARGET => $args{'TARGET'},
2564 TYPE => $args{'TYPE'},
2565 DESCRIPTION => $self->loc(
2566 "Tickets [_1] by [_2]",
2567 $self->loc( $args{'TYPE'} ),
2570 OPERATOR => $args{'OPERATOR'},
2576 # {{{ LimitLinkedFrom
2578 =head2 LimitLinkedFrom
2580 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2581 TYPE limits the sort of link we want to search on
2584 BASE is the id or URI of the BASE of the link
2588 sub LimitLinkedFrom {
2597 # translate RT2 From/To naming to RT3 TicketSQL naming
2598 my %fromToMap = qw(DependsOn DependentOn
2600 RefersTo ReferredToBy);
2602 my $type = $args{'TYPE'};
2603 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2606 FIELD => 'LinkedTo',
2608 BASE => $args{'BASE'},
2610 DESCRIPTION => $self->loc(
2611 "Tickets [_1] [_2]",
2612 $self->loc( $args{'TYPE'} ),
2615 OPERATOR => $args{'OPERATOR'},
2624 my $ticket_id = shift;
2625 return $self->LimitLinkedTo(
2627 TARGET => $ticket_id,
2634 # {{{ LimitHasMember
2635 sub LimitHasMember {
2637 my $ticket_id = shift;
2638 return $self->LimitLinkedFrom(
2640 BASE => "$ticket_id",
2641 TYPE => 'HasMember',
2648 # {{{ LimitDependsOn
2650 sub LimitDependsOn {
2652 my $ticket_id = shift;
2653 return $self->LimitLinkedTo(
2655 TARGET => $ticket_id,
2656 TYPE => 'DependsOn',
2663 # {{{ LimitDependedOnBy
2665 sub LimitDependedOnBy {
2667 my $ticket_id = shift;
2668 return $self->LimitLinkedFrom(
2671 TYPE => 'DependentOn',
2682 my $ticket_id = shift;
2683 return $self->LimitLinkedTo(
2685 TARGET => $ticket_id,
2693 # {{{ LimitReferredToBy
2695 sub LimitReferredToBy {
2697 my $ticket_id = shift;
2698 return $self->LimitLinkedFrom(
2701 TYPE => 'ReferredToBy',
2709 # {{{ limit based on ticket date attribtes
2713 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2715 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2717 OPERATOR is one of > or <
2718 VALUE is a date and time in ISO format in GMT
2719 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2721 There are also helper functions of the form LimitFIELD that eliminate
2722 the need to pass in a FIELD argument.
2736 #Set the description if we didn't get handed it above
2737 unless ( $args{'DESCRIPTION'} ) {
2738 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2739 . $args{'OPERATOR'} . " "
2740 . $args{'VALUE'} . " GMT";
2743 $self->Limit(%args);
2751 $self->LimitDate( FIELD => 'Created', @_ );
2756 $self->LimitDate( FIELD => 'Due', @_ );
2762 $self->LimitDate( FIELD => 'Starts', @_ );
2768 $self->LimitDate( FIELD => 'Started', @_ );
2773 $self->LimitDate( FIELD => 'Resolved', @_ );
2778 $self->LimitDate( FIELD => 'Told', @_ );
2781 sub LimitLastUpdated {
2783 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2787 # {{{ sub LimitTransactionDate
2789 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2791 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2793 OPERATOR is one of > or <
2794 VALUE is a date and time in ISO format in GMT
2799 sub LimitTransactionDate {
2802 FIELD => 'TransactionDate',
2809 # <20021217042756.GK28744@pallas.fsck.com>
2810 # "Kill It" - Jesse.
2812 #Set the description if we didn't get handed it above
2813 unless ( $args{'DESCRIPTION'} ) {
2814 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2815 . $args{'OPERATOR'} . " "
2816 . $args{'VALUE'} . " GMT";
2819 $self->Limit(%args);
2827 # {{{ Limit based on custom fields
2828 # {{{ sub LimitCustomField
2830 =head2 LimitCustomField
2832 Takes a paramhash of key/value pairs with the following keys:
2836 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2838 =item OPERATOR - The usual Limit operators
2840 =item VALUE - The value to compare against
2846 sub LimitCustomField {
2850 CUSTOMFIELD => undef,
2852 DESCRIPTION => undef,
2853 FIELD => 'CustomFieldValue',
2858 my $CF = RT::CustomField->new( $self->CurrentUser );
2859 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2860 $CF->Load( $args{CUSTOMFIELD} );
2863 $CF->LoadByNameAndQueue(
2864 Name => $args{CUSTOMFIELD},
2865 Queue => $args{QUEUE}
2867 $args{CUSTOMFIELD} = $CF->Id;
2870 #If we are looking to compare with a null value.
2871 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2872 $args{'DESCRIPTION'}
2873 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2875 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2876 $args{'DESCRIPTION'}
2877 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2880 # if we're not looking to compare with a null value
2882 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2883 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2886 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2887 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2888 $QueueObj->Load( $args{'QUEUE'} );
2889 $args{'QUEUE'} = $QueueObj->Id;
2891 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2894 @rest = ( ENTRYAGGREGATOR => 'AND' )
2895 if ( $CF->Type eq 'SelectMultiple' );
2898 VALUE => $args{VALUE},
2900 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2901 .".{" . $CF->Name . "}",
2902 OPERATOR => $args{OPERATOR},
2907 $self->{'RecalcTicketLimits'} = 1;
2913 # {{{ sub _NextIndex
2917 Keep track of the counter for the array of restrictions
2923 return ( $self->{'restriction_index'}++ );
2930 # {{{ Core bits to make this a DBIx::SearchBuilder object
2935 $self->{'table'} = "Tickets";
2936 $self->{'RecalcTicketLimits'} = 1;
2937 $self->{'looking_at_effective_id'} = 0;
2938 $self->{'looking_at_type'} = 0;
2939 $self->{'restriction_index'} = 1;
2940 $self->{'primary_key'} = "id";
2941 delete $self->{'items_array'};
2942 delete $self->{'item_map'};
2943 delete $self->{'columns_to_display'};
2944 $self->SUPER::_Init(@_);
2955 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2956 return ( $self->SUPER::Count() );
2964 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2965 return ( $self->SUPER::CountAll() );
2970 # {{{ sub ItemsArrayRef
2972 =head2 ItemsArrayRef
2974 Returns a reference to the set of all items found in this search
2981 return $self->{'items_array'} if $self->{'items_array'};
2983 my $placeholder = $self->_ItemsCounter;
2984 $self->GotoFirstItem();
2985 while ( my $item = $self->Next ) {
2986 push( @{ $self->{'items_array'} }, $item );
2988 $self->GotoItem($placeholder);
2989 $self->{'items_array'}
2990 = $self->ItemsOrderBy( $self->{'items_array'} );
2992 return $self->{'items_array'};
2995 sub ItemsArrayRefWindow {
2999 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3001 $self->RowsPerPage( $window );
3003 $self->GotoFirstItem;
3006 while ( my $item = $self->Next ) {
3010 $self->RowsPerPage( $old[1] );
3011 $self->FirstRow( $old[2] );
3012 $self->GotoItem( $old[0] );
3023 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3025 my $Ticket = $self->SUPER::Next;
3026 return $Ticket unless $Ticket;
3028 if ( $Ticket->__Value('Status') eq 'deleted'
3029 && !$self->{'allow_deleted_search'} )
3033 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3034 # if we found a ticket with this option enabled then
3035 # all tickets we found are ACLed, cache this fact
3036 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3037 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3040 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3045 # If the user doesn't have the right to show this ticket
3052 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3053 return $self->SUPER::_DoSearch( @_ );
3058 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3059 return $self->SUPER::_DoCount( @_ );
3065 my $cache_key = 'RolesHasRight;:;ShowTicket';
3067 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3071 my $ACL = RT::ACL->new( $RT::SystemUser );
3072 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3073 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3074 my $principal_alias = $ACL->Join(
3076 FIELD1 => 'PrincipalId',
3077 TABLE2 => 'Principals',
3080 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3083 while ( my $ACE = $ACL->Next ) {
3084 my $role = $ACE->PrincipalType;
3085 my $type = $ACE->ObjectType;
3086 if ( $type eq 'RT::System' ) {
3089 elsif ( $type eq 'RT::Queue' ) {
3090 next if $res{ $role } && !ref $res{ $role };
3091 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3094 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3097 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3101 sub _DirectlyCanSeeIn {
3103 my $id = $self->CurrentUser->id;
3105 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3106 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3110 my $ACL = RT::ACL->new( $RT::SystemUser );
3111 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3112 my $principal_alias = $ACL->Join(
3114 FIELD1 => 'PrincipalId',
3115 TABLE2 => 'Principals',
3118 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3119 my $cgm_alias = $ACL->Join(
3121 FIELD1 => 'PrincipalId',
3122 TABLE2 => 'CachedGroupMembers',
3123 FIELD2 => 'GroupId',
3125 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3126 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3129 while ( my $ACE = $ACL->Next ) {
3130 my $type = $ACE->ObjectType;
3131 if ( $type eq 'RT::System' ) {
3132 # If user is direct member of a group that has the right
3133 # on the system then he can see any ticket
3134 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3137 elsif ( $type eq 'RT::Queue' ) {
3138 push @res, $ACE->ObjectId;
3141 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3144 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3148 sub CurrentUserCanSee {
3150 return if $self->{'_sql_current_user_can_see_applied'};
3152 return $self->{'_sql_current_user_can_see_applied'} = 1
3153 if $self->CurrentUser->UserObj->HasRight(
3154 Right => 'SuperUser', Object => $RT::System
3157 my $id = $self->CurrentUser->id;
3159 # directly can see in all queues then we have nothing to do
3160 my @direct_queues = $self->_DirectlyCanSeeIn;
3161 return $self->{'_sql_current_user_can_see_applied'} = 1
3162 if @direct_queues && $direct_queues[0] == -1;
3164 my %roles = $self->_RolesCanSee;
3166 my %skip = map { $_ => 1 } @direct_queues;
3167 foreach my $role ( keys %roles ) {
3168 next unless ref $roles{ $role };
3170 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3172 $roles{ $role } = \@queues;
3174 delete $roles{ $role };
3179 # there is no global watchers, only queues and tickes, if at
3180 # some point we will add global roles then it's gonna blow
3181 # the idea here is that if the right is set globaly for a role
3182 # and user plays this role for a queue directly not a ticket
3183 # then we have to check in advance
3184 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3186 my $groups = RT::Groups->new( $RT::SystemUser );
3187 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3189 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3191 my $principal_alias = $groups->Join(
3194 TABLE2 => 'Principals',
3197 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3198 my $cgm_alias = $groups->Join(
3201 TABLE2 => 'CachedGroupMembers',
3202 FIELD2 => 'GroupId',
3204 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3205 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3206 while ( my $group = $groups->Next ) {
3207 push @direct_queues, $group->Instance;
3211 unless ( @direct_queues || keys %roles ) {
3212 $self->SUPER::Limit(
3217 ENTRYAGGREGATOR => 'AND',
3219 return $self->{'_sql_current_user_can_see_applied'} = 1;
3223 my $join_roles = keys %roles;
3224 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3225 my ($role_group_alias, $cgm_alias);
3226 if ( $join_roles ) {
3227 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3228 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3229 $self->SUPER::Limit(
3230 LEFTJOIN => $cgm_alias,
3231 FIELD => 'MemberId',
3236 my $limit_queues = sub {
3240 return unless @queues;
3241 if ( @queues == 1 ) {
3242 $self->SUPER::Limit(
3247 ENTRYAGGREGATOR => $ea,
3250 $self->SUPER::_OpenParen('ACL');
3251 foreach my $q ( @queues ) {
3252 $self->SUPER::Limit(
3257 ENTRYAGGREGATOR => $ea,
3261 $self->SUPER::_CloseParen('ACL');
3266 $self->SUPER::_OpenParen('ACL');
3268 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3269 while ( my ($role, $queues) = each %roles ) {
3270 $self->SUPER::_OpenParen('ACL');
3271 if ( $role eq 'Owner' ) {
3272 $self->SUPER::Limit(
3276 ENTRYAGGREGATOR => $ea,
3280 $self->SUPER::Limit(
3282 ALIAS => $cgm_alias,
3283 FIELD => 'MemberId',
3284 OPERATOR => 'IS NOT',
3287 ENTRYAGGREGATOR => $ea,
3289 $self->SUPER::Limit(
3291 ALIAS => $role_group_alias,
3294 ENTRYAGGREGATOR => 'AND',
3297 $limit_queues->( 'AND', @$queues ) if ref $queues;
3298 $ea = 'OR' if $ea eq 'AND';
3299 $self->SUPER::_CloseParen('ACL');
3301 $self->SUPER::_CloseParen('ACL');
3303 return $self->{'_sql_current_user_can_see_applied'} = 1;
3310 # {{{ Deal with storing and restoring restrictions
3312 # {{{ sub LoadRestrictions
3314 =head2 LoadRestrictions
3316 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3317 TODO It is not yet implemented
3323 # {{{ sub DescribeRestrictions
3325 =head2 DescribeRestrictions
3328 Returns a hash keyed by restriction id.
3329 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3330 is a description of the purpose of that TicketRestriction
3334 sub DescribeRestrictions {
3339 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3340 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3347 # {{{ sub RestrictionValues
3349 =head2 RestrictionValues FIELD
3351 Takes a restriction field and returns a list of values this field is restricted
3356 sub RestrictionValues {
3359 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3360 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3361 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3363 keys %{ $self->{'TicketRestrictions'} };
3368 # {{{ sub ClearRestrictions
3370 =head2 ClearRestrictions
3372 Removes all restrictions irretrievably
3376 sub ClearRestrictions {
3378 delete $self->{'TicketRestrictions'};
3379 $self->{'looking_at_effective_id'} = 0;
3380 $self->{'looking_at_type'} = 0;
3381 $self->{'RecalcTicketLimits'} = 1;
3386 # {{{ sub DeleteRestriction
3388 =head2 DeleteRestriction
3390 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3391 Removes that restriction from the session's limits.
3395 sub DeleteRestriction {
3398 delete $self->{'TicketRestrictions'}{$row};
3400 $self->{'RecalcTicketLimits'} = 1;
3402 #make the underlying easysearch object forget all its preconceptions
3407 # {{{ sub _RestrictionsToClauses
3409 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3411 sub _RestrictionsToClauses {
3415 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3416 my $restriction = $self->{'TicketRestrictions'}{$row};
3418 # We need to reimplement the subclause aggregation that SearchBuilder does.
3419 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3420 # Then SB AND's the different Subclauses together.
3422 # So, we want to group things into Subclauses, convert them to
3423 # SQL, and then join them with the appropriate DefaultEA.
3424 # Then join each subclause group with AND.
3426 my $field = $restriction->{'FIELD'};
3427 my $realfield = $field; # CustomFields fake up a fieldname, so
3428 # we need to figure that out
3431 # Rewrite LinkedTo meta field to the real field
3432 if ( $field =~ /LinkedTo/ ) {
3433 $realfield = $field = $restriction->{'TYPE'};
3437 # Handle subkey fields with a different real field
3438 if ( $field =~ /^(\w+)\./ ) {
3442 die "I don't know about $field yet"
3443 unless ( exists $FIELD_METADATA{$realfield}
3444 or $restriction->{CUSTOMFIELD} );
3446 my $type = $FIELD_METADATA{$realfield}->[0];
3447 my $op = $restriction->{'OPERATOR'};
3451 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3454 # this performs the moral equivalent of defined or/dor/C<//>,
3455 # without the short circuiting.You need to use a 'defined or'
3456 # type thing instead of just checking for truth values, because
3457 # VALUE could be 0.(i.e. "false")
3459 # You could also use this, but I find it less aesthetic:
3460 # (although it does short circuit)
3461 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3462 # defined $restriction->{'TICKET'} ?
3463 # $restriction->{TICKET} :
3464 # defined $restriction->{'BASE'} ?
3465 # $restriction->{BASE} :
3466 # defined $restriction->{'TARGET'} ?
3467 # $restriction->{TARGET} )
3469 my $ea = $restriction->{ENTRYAGGREGATOR}
3470 || $DefaultEA{$type}
3473 die "Invalid operator $op for $field ($type)"
3474 unless exists $ea->{$op};
3478 # Each CustomField should be put into a different Clause so they
3479 # are ANDed together.
3480 if ( $restriction->{CUSTOMFIELD} ) {
3481 $realfield = $field;
3484 exists $clause{$realfield} or $clause{$realfield} = [];
3487 $field =~ s!(['"])!\\$1!g;
3488 $value =~ s!(['"])!\\$1!g;
3489 my $data = [ $ea, $type, $field, $op, $value ];
3491 # here is where we store extra data, say if it's a keyword or
3492 # something. (I.e. "TYPE SPECIFIC STUFF")
3494 push @{ $clause{$realfield} }, $data;
3501 # {{{ sub _ProcessRestrictions
3503 =head2 _ProcessRestrictions PARAMHASH
3505 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3506 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3510 sub _ProcessRestrictions {
3513 #Blow away ticket aliases since we'll need to regenerate them for
3515 delete $self->{'TicketAliases'};
3516 delete $self->{'items_array'};
3517 delete $self->{'item_map'};
3518 delete $self->{'raw_rows'};
3519 delete $self->{'rows'};
3520 delete $self->{'count_all'};
3522 my $sql = $self->Query; # Violating the _SQL namespace
3523 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3525 # "Restrictions to Clauses Branch\n";
3526 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3528 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3532 $sql = $self->ClausesToSQL($clauseRef);
3533 $self->FromSQL($sql) if $sql;
3537 $self->{'RecalcTicketLimits'} = 0;
3541 =head2 _BuildItemMap
3543 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3544 display search nav quickly.
3551 my $window = RT->Config->Get('TicketsItemMapSize');
3553 $self->{'item_map'} = {};
3555 my $items = $self->ItemsArrayRefWindow( $window );
3556 return unless $items && @$items;
3559 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3560 for ( my $i = 0; $i < @$items; $i++ ) {
3561 my $item = $items->[$i];
3562 my $id = $item->EffectiveId;
3563 $self->{'item_map'}{$id}{'defined'} = 1;
3564 $self->{'item_map'}{$id}{'prev'} = $prev;
3565 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3569 $self->{'item_map'}{'last'} = $prev
3570 if !$window || @$items < $window;
3575 Returns an a map of all items found by this search. The map is a hash
3579 first => <first ticket id found>,
3580 last => <last ticket id found or undef>,
3583 prev => <the ticket id found before>,
3584 next => <the ticket id found after>,
3596 $self->_BuildItemMap unless $self->{'item_map'};
3597 return $self->{'item_map'};
3605 =head2 PrepForSerialization
3607 You don't want to serialize a big tickets object, as
3608 the {items} hash will be instantly invalid _and_ eat
3613 sub PrepForSerialization {
3615 delete $self->{'items'};
3616 delete $self->{'items_array'};
3617 $self->RedoSearch();
3622 RT::Tickets supports several flags which alter search behavior:
3625 allow_deleted_search (Otherwise never show deleted tickets in search results)
3626 looking_at_type (otherwise limit to type=ticket)
3628 These flags are set by calling
3630 $tickets->{'flagname'} = 1;
3632 BUG: There should be an API for this