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',
1072 $self->SUPER::Limit(
1075 FIELD => 'Disabled',
1079 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1080 unless $args{'New'};
1087 Helper function which provides joins to a watchers table both for limits
1094 my $type = shift || '';
1097 my $groups = $self->_RoleGroupsJoin( Type => $type );
1098 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1099 # XXX: work around, we must hide groups that
1100 # are members of the role group we search in,
1101 # otherwise them result in wrong NULLs in Users
1102 # table and break ordering. Now, we know that
1103 # RT doesn't allow to add groups as members of the
1104 # ticket roles, so we just hide entries in CGM table
1105 # with MemberId == GroupId from results
1106 $self->SUPER::Limit(
1107 LEFTJOIN => $group_members,
1110 VALUE => "$group_members.MemberId",
1113 my $users = $self->Join(
1115 ALIAS1 => $group_members,
1116 FIELD1 => 'MemberId',
1120 return ($groups, $group_members, $users);
1123 =head2 _WatcherMembershipLimit
1125 Handle watcher membership limits, i.e. whether the watcher belongs to a
1126 specific group or not.
1129 1: Field to query on
1131 SELECT DISTINCT main.*
1135 CachedGroupMembers CachedGroupMembers_2,
1138 (main.EffectiveId = main.id)
1140 (main.Status != 'deleted')
1142 (main.Type = 'ticket')
1145 (Users_3.EmailAddress = '22')
1147 (Groups_1.Domain = 'RT::Ticket-Role')
1149 (Groups_1.Type = 'RequestorGroup')
1152 Groups_1.Instance = main.id
1154 Groups_1.id = CachedGroupMembers_2.GroupId
1156 CachedGroupMembers_2.MemberId = Users_3.id
1157 ORDER BY main.id ASC
1162 sub _WatcherMembershipLimit {
1163 my ( $self, $field, $op, $value, @rest ) = @_;
1168 my $groups = $self->NewAlias('Groups');
1169 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1170 my $users = $self->NewAlias('Users');
1171 my $memberships = $self->NewAlias('CachedGroupMembers');
1173 if ( ref $field ) { # gross hack
1174 my @bundle = @$field;
1176 for my $chunk (@bundle) {
1177 ( $field, $op, $value, @rest ) = @$chunk;
1179 ALIAS => $memberships,
1190 ALIAS => $memberships,
1198 # {{{ Tie to groups for tickets we care about
1202 VALUE => 'RT::Ticket-Role',
1203 ENTRYAGGREGATOR => 'AND'
1208 FIELD1 => 'Instance',
1215 # If we care about which sort of watcher
1216 my $meta = $FIELD_METADATA{$field};
1217 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1224 ENTRYAGGREGATOR => 'AND'
1231 ALIAS2 => $groupmembers,
1236 ALIAS1 => $groupmembers,
1237 FIELD1 => 'MemberId',
1243 ALIAS => $groupmembers,
1244 FIELD => 'Disabled',
1249 ALIAS1 => $memberships,
1250 FIELD1 => 'MemberId',
1256 ALIAS => $memberships,
1257 FIELD => 'Disabled',
1266 =head2 _CustomFieldDecipher
1268 Try and turn a CF descriptor into (cfid, cfname) object pair.
1272 sub _CustomFieldDecipher {
1273 my ($self, $string) = @_;
1275 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1276 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1280 my $q = RT::Queue->new( $self->CurrentUser );
1284 # $queue = $q->Name; # should we normalize the queue?
1285 $cf = $q->CustomField( $field );
1288 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1292 elsif ( $field =~ /\D/ ) {
1294 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1295 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1296 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1298 # if there is more then one field the current user can
1299 # see with the same name then we shouldn't return cf object
1300 # as we don't know which one to use
1303 $cf = undef if $cfs->Next;
1307 $cf = RT::CustomField->new( $self->CurrentUser );
1308 $cf->Load( $field );
1311 return ($queue, $field, $cf, $column);
1314 =head2 _CustomFieldJoin
1316 Factor out the Join of custom fields so we can use it for sorting too
1320 sub _CustomFieldJoin {
1321 my ($self, $cfkey, $cfid, $field) = @_;
1322 # Perform one Join per CustomField
1323 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1324 $self->{_sql_cf_alias}{$cfkey} )
1326 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1327 $self->{_sql_cf_alias}{$cfkey} );
1330 my ($TicketCFs, $CFs);
1332 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1336 TABLE2 => 'ObjectCustomFieldValues',
1337 FIELD2 => 'ObjectId',
1339 $self->SUPER::Limit(
1340 LEFTJOIN => $TicketCFs,
1341 FIELD => 'CustomField',
1343 ENTRYAGGREGATOR => 'AND'
1347 my $ocfalias = $self->Join(
1350 TABLE2 => 'ObjectCustomFields',
1351 FIELD2 => 'ObjectId',
1354 $self->SUPER::Limit(
1355 LEFTJOIN => $ocfalias,
1356 ENTRYAGGREGATOR => 'OR',
1357 FIELD => 'ObjectId',
1361 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1363 ALIAS1 => $ocfalias,
1364 FIELD1 => 'CustomField',
1365 TABLE2 => 'CustomFields',
1368 $self->SUPER::Limit(
1370 ENTRYAGGREGATOR => 'AND',
1371 FIELD => 'LookupType',
1372 VALUE => 'RT::Queue-RT::Ticket',
1374 $self->SUPER::Limit(
1376 ENTRYAGGREGATOR => 'AND',
1381 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1385 TABLE2 => 'ObjectCustomFieldValues',
1386 FIELD2 => 'CustomField',
1388 $self->SUPER::Limit(
1389 LEFTJOIN => $TicketCFs,
1390 FIELD => 'ObjectId',
1393 ENTRYAGGREGATOR => 'AND',
1396 $self->SUPER::Limit(
1397 LEFTJOIN => $TicketCFs,
1398 FIELD => 'ObjectType',
1399 VALUE => 'RT::Ticket',
1400 ENTRYAGGREGATOR => 'AND'
1402 $self->SUPER::Limit(
1403 LEFTJOIN => $TicketCFs,
1404 FIELD => 'Disabled',
1407 ENTRYAGGREGATOR => 'AND'
1410 return ($TicketCFs, $CFs);
1413 =head2 _CustomFieldLimit
1415 Limit based on CustomFields
1422 sub _CustomFieldLimit {
1423 my ( $self, $_field, $op, $value, %rest ) = @_;
1425 my $field = $rest{'SUBKEY'} || die "No field specified";
1427 # For our sanity, we can only limit on one queue at a time
1429 my ($queue, $cfid, $cf, $column);
1430 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1431 $cfid = $cf ? $cf->id : 0 ;
1433 # If we're trying to find custom fields that don't match something, we
1434 # want tickets where the custom field has no value at all. Note that
1435 # we explicitly don't include the "IS NULL" case, since we would
1436 # otherwise end up with a redundant clause.
1438 my ($negative_op, $null_op, $inv_op, $range_op)
1439 = $self->ClassifySQLOperation( $op );
1443 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1444 return 'MATCHES' if $op eq '=';
1445 return 'NOT MATCHES' if $op eq '!=';
1449 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1451 my $cfkey = $cfid ? $cfid : "$queue.$field";
1453 if ( $null_op && !$column ) {
1454 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1455 # we can reuse our default joins for this operation
1456 # with column specified we have different situation
1457 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1460 ALIAS => $TicketCFs,
1469 OPERATOR => 'IS NOT',
1472 ENTRYAGGREGATOR => 'AND',
1476 elsif ( !$negative_op || $single_value ) {
1477 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1478 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1485 # if column is defined then deal only with it
1486 # otherwise search in Content and in LargeContent
1489 ALIAS => $TicketCFs,
1491 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1496 elsif ( $cfid and $cf->Type eq 'Date' ) {
1497 $self->_DateFieldLimit(
1501 ALIAS => $TicketCFs,
1505 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1506 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1508 ALIAS => $TicketCFs,
1517 ALIAS => $TicketCFs,
1521 ENTRYAGGREGATOR => 'OR'
1524 ALIAS => $TicketCFs,
1528 ENTRYAGGREGATOR => 'OR'
1532 ALIAS => $TicketCFs,
1533 FIELD => 'LargeContent',
1534 OPERATOR => $fix_op->($op),
1536 ENTRYAGGREGATOR => 'AND',
1542 ALIAS => $TicketCFs,
1552 ALIAS => $TicketCFs,
1556 ENTRYAGGREGATOR => 'OR'
1559 ALIAS => $TicketCFs,
1563 ENTRYAGGREGATOR => 'OR'
1567 ALIAS => $TicketCFs,
1568 FIELD => 'LargeContent',
1569 OPERATOR => $fix_op->($op),
1571 ENTRYAGGREGATOR => 'AND',
1577 # XXX: if we join via CustomFields table then
1578 # because of order of left joins we get NULLs in
1579 # CF table and then get nulls for those records
1580 # in OCFVs table what result in wrong results
1581 # as decifer method now tries to load a CF then
1582 # we fall into this situation only when there
1583 # are more than one CF with the name in the DB.
1584 # the same thing applies to order by call.
1585 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1586 # we want treat IS NULL as (not applies or has
1591 OPERATOR => 'IS NOT',
1594 ENTRYAGGREGATOR => 'AND',
1600 ALIAS => $TicketCFs,
1601 FIELD => $column || 'Content',
1605 ENTRYAGGREGATOR => 'OR',
1612 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1613 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1616 $op =~ s/!|NOT\s+//i;
1618 # if column is defined then deal only with it
1619 # otherwise search in Content and in LargeContent
1621 $self->SUPER::Limit(
1622 LEFTJOIN => $TicketCFs,
1623 ALIAS => $TicketCFs,
1625 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1630 $self->SUPER::Limit(
1631 LEFTJOIN => $TicketCFs,
1632 ALIAS => $TicketCFs,
1640 ALIAS => $TicketCFs,
1649 sub _HasAttributeLimit {
1650 my ( $self, $field, $op, $value, %rest ) = @_;
1652 my $alias = $self->Join(
1656 TABLE2 => 'Attributes',
1657 FIELD2 => 'ObjectId',
1659 $self->SUPER::Limit(
1661 FIELD => 'ObjectType',
1662 VALUE => 'RT::Ticket',
1663 ENTRYAGGREGATOR => 'AND'
1665 $self->SUPER::Limit(
1670 ENTRYAGGREGATOR => 'AND'
1676 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1682 # End Helper Functions
1684 # End of SQL Stuff -------------------------------------------------
1686 # {{{ Allow sorting on watchers
1688 =head2 OrderByCols ARRAY
1690 A modified version of the OrderBy method which automatically joins where
1691 C<ALIAS> is set to the name of a watcher type.
1702 foreach my $row (@args) {
1703 if ( $row->{ALIAS} ) {
1707 if ( $row->{FIELD} !~ /\./ ) {
1708 my $meta = $self->FIELDS->{ $row->{FIELD} };
1714 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1715 my $alias = $self->Join(
1718 FIELD1 => $row->{'FIELD'},
1722 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1723 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1724 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1726 my $alias = $self->Join(
1729 FIELD1 => $row->{'FIELD'},
1733 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1740 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1741 my $meta = $self->FIELDS->{$field};
1742 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1743 # cache alias as we want to use one alias per watcher type for sorting
1744 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1746 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1747 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1749 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1750 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1751 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1752 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1753 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1754 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1755 # this is described in _CustomFieldLimit
1759 OPERATOR => 'IS NOT',
1762 ENTRYAGGREGATOR => 'AND',
1765 # For those cases where we are doing a join against the
1766 # CF name, and don't have a CFid, use Unique to make sure
1767 # we don't show duplicate tickets. NOTE: I'm pretty sure
1768 # this will stay mixed in for the life of the
1769 # class/package, and not just for the life of the object.
1770 # Potential performance issue.
1771 require DBIx::SearchBuilder::Unique;
1772 DBIx::SearchBuilder::Unique->import;
1774 my $CFvs = $self->Join(
1776 ALIAS1 => $TicketCFs,
1777 FIELD1 => 'CustomField',
1778 TABLE2 => 'CustomFieldValues',
1779 FIELD2 => 'CustomField',
1781 $self->SUPER::Limit(
1785 VALUE => $TicketCFs . ".Content",
1786 ENTRYAGGREGATOR => 'AND'
1789 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1790 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1791 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1792 # PAW logic is "reversed"
1794 if (exists $row->{ORDER} ) {
1795 my $o = $row->{ORDER};
1796 delete $row->{ORDER};
1797 $order = "DESC" if $o =~ /asc/i;
1800 # Ticket.Owner 1 0 X
1801 # Unowned Tickets 0 1 X
1804 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1805 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1806 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1811 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1818 FUNCTION => "Owner=$uid",
1824 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1826 } elsif ( $field eq 'Customer' ) { #Freeside
1827 # OrderBy(FIELD => expression) doesn't work, it has to be
1828 # an actual field, so we have to do the join even if sorting
1830 my $custalias = $self->JoinToCustomer;
1831 my $cust_field = lc($subkey);
1832 if ( !$cust_field or $cust_field eq 'number' ) {
1833 $cust_field = 'custnum';
1835 elsif ( $cust_field eq 'name' ) {
1836 $cust_field = "COALESCE( $custalias.company,
1837 $custalias.last || ', ' || $custalias.first
1840 else { # order by cust_main fields directly: 'Customer.agentnum'
1841 $cust_field = $subkey;
1843 push @res, { %$row, ALIAS => $custalias, FIELD => $cust_field };
1851 return $self->SUPER::OrderByCols(@res);
1856 sub JoinToCustLinks {
1857 # Set up join to links (id = localbase),
1858 # limit link type to 'MemberOf',
1859 # and target value to any Freeside custnum URI.
1860 # Return the linkalias for further join/limit action,
1861 # and an sql expression to retrieve the custnum.
1863 # only join once for each RT::Tickets object
1864 my $linkalias = $self->{cust_linkalias};
1866 $linkalias = $self->Join(
1871 FIELD2 => 'LocalBase',
1873 $self->SUPER::Limit(
1874 LEFTJOIN => $linkalias,
1877 VALUE => 'fsck.com-rt://%/ticket/%',
1879 $self->SUPER::Limit(
1880 LEFTJOIN => $linkalias,
1883 VALUE => 'MemberOf',
1885 $self->SUPER::Limit(
1886 LEFTJOIN => $linkalias,
1888 OPERATOR => 'STARTSWITH',
1889 VALUE => 'freeside://freeside/cust_main/',
1891 $self->{cust_linkalias} = $linkalias;
1893 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1894 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1895 $custnum_sql .= 'SIGNED INTEGER)';
1898 $custnum_sql .= 'INTEGER)';
1900 return ($linkalias, $custnum_sql);
1903 sub JoinToCustomer {
1905 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1906 # don't reuse this join, though--negative queries need
1908 my $custalias = $self->Join(
1910 EXPRESSION => $custnum_sql,
1911 TABLE2 => 'cust_main',
1912 FIELD2 => 'custnum',
1917 sub _FreesideFieldLimit {
1918 my ( $self, $field, $op, $value, %rest ) = @_;
1919 my $alias = $self->JoinToCustomer;
1920 my $is_negative = 0;
1921 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1922 # if the op is negative, do the join as though
1923 # the op were positive, then accept only records
1924 # where the right-side join key is null.
1926 $op = '=' if $op eq '!=';
1930 my $cust_field = $rest{SUBKEY} || 'custnum';
1932 # compound subkey: separate into table name and field in that table
1933 # (must be linked by custnum)
1934 ($table2, $cust_field) = ($1, $2) if $cust_field =~ /^(\w+)?\.(\w+)$/;
1936 $cust_field = lc($cust_field);
1937 $cust_field = 'custnum' if !$cust_field or $cust_field eq 'number';
1940 $alias = $self->Join(
1943 FIELD1 => 'custnum',
1945 FIELD2 => 'custnum',
1949 $self->SUPER::Limit(
1951 FIELD => $cust_field,
1954 ENTRYAGGREGATOR => 'AND',
1960 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1970 # {{{ Limit the result set based on content
1976 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1977 Generally best called from LimitFoo methods
1987 DESCRIPTION => undef,
1990 $args{'DESCRIPTION'} = $self->loc(
1991 "[_1] [_2] [_3]", $args{'FIELD'},
1992 $args{'OPERATOR'}, $args{'VALUE'}
1994 if ( !defined $args{'DESCRIPTION'} );
1996 my $index = $self->_NextIndex;
1998 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2000 %{ $self->{'TicketRestrictions'}{$index} } = %args;
2002 $self->{'RecalcTicketLimits'} = 1;
2004 # If we're looking at the effective id, we don't want to append the other clause
2005 # which limits us to tickets where id = effective id
2006 if ( $args{'FIELD'} eq 'EffectiveId'
2007 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2009 $self->{'looking_at_effective_id'} = 1;
2012 if ( $args{'FIELD'} eq 'Type'
2013 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2015 $self->{'looking_at_type'} = 1;
2025 Returns a frozen string suitable for handing back to ThawLimits.
2029 sub _FreezeThawKeys {
2030 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2034 # {{{ sub FreezeLimits
2039 require MIME::Base64;
2040 MIME::Base64::base64_encode(
2041 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2048 Take a frozen Limits string generated by FreezeLimits and make this tickets
2049 object have that set of limits.
2053 # {{{ sub ThawLimits
2059 #if we don't have $in, get outta here.
2060 return undef unless ($in);
2062 $self->{'RecalcTicketLimits'} = 1;
2065 require MIME::Base64;
2067 #We don't need to die if the thaw fails.
2068 @{$self}{ $self->_FreezeThawKeys }
2069 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2071 $RT::Logger->error($@) if $@;
2077 # {{{ Limit by enum or foreign key
2079 # {{{ sub LimitQueue
2083 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2084 OPERATOR is one of = or !=. (It defaults to =).
2085 VALUE is a queue id or Name.
2098 #TODO VALUE should also take queue objects
2099 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2100 my $queue = new RT::Queue( $self->CurrentUser );
2101 $queue->Load( $args{'VALUE'} );
2102 $args{'VALUE'} = $queue->Id;
2105 # What if they pass in an Id? Check for isNum() and convert to
2108 #TODO check for a valid queue here
2112 VALUE => $args{'VALUE'},
2113 OPERATOR => $args{'OPERATOR'},
2114 DESCRIPTION => join(
2115 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2123 # {{{ sub LimitStatus
2127 Takes a paramhash with the fields OPERATOR and VALUE.
2128 OPERATOR is one of = or !=.
2131 RT adds Status != 'deleted' until object has
2132 allow_deleted_search internal property set.
2133 $tickets->{'allow_deleted_search'} = 1;
2134 $tickets->LimitStatus( VALUE => 'deleted' );
2146 VALUE => $args{'VALUE'},
2147 OPERATOR => $args{'OPERATOR'},
2148 DESCRIPTION => join( ' ',
2149 $self->loc('Status'), $args{'OPERATOR'},
2150 $self->loc( $args{'VALUE'} ) ),
2156 # {{{ sub IgnoreType
2160 If called, this search will not automatically limit the set of results found
2161 to tickets of type "Ticket". Tickets of other types, such as "project" and
2162 "approval" will be found.
2169 # Instead of faking a Limit that later gets ignored, fake up the
2170 # fact that we're already looking at type, so that the check in
2171 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2173 # $self->LimitType(VALUE => '__any');
2174 $self->{looking_at_type} = 1;
2183 Takes a paramhash with the fields OPERATOR and VALUE.
2184 OPERATOR is one of = or !=, it defaults to "=".
2185 VALUE is a string to search for in the type of the ticket.
2200 VALUE => $args{'VALUE'},
2201 OPERATOR => $args{'OPERATOR'},
2202 DESCRIPTION => join( ' ',
2203 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2211 # {{{ Limit by string field
2213 # {{{ sub LimitSubject
2217 Takes a paramhash with the fields OPERATOR and VALUE.
2218 OPERATOR is one of = or !=.
2219 VALUE is a string to search for in the subject of the ticket.
2228 VALUE => $args{'VALUE'},
2229 OPERATOR => $args{'OPERATOR'},
2230 DESCRIPTION => join( ' ',
2231 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2239 # {{{ Limit based on ticket numerical attributes
2240 # Things that can be > < = !=
2246 Takes a paramhash with the fields OPERATOR and VALUE.
2247 OPERATOR is one of =, >, < or !=.
2248 VALUE is a ticket Id to search for
2261 VALUE => $args{'VALUE'},
2262 OPERATOR => $args{'OPERATOR'},
2264 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2270 # {{{ sub LimitPriority
2272 =head2 LimitPriority
2274 Takes a paramhash with the fields OPERATOR and VALUE.
2275 OPERATOR is one of =, >, < or !=.
2276 VALUE is a value to match the ticket\'s priority against
2284 FIELD => 'Priority',
2285 VALUE => $args{'VALUE'},
2286 OPERATOR => $args{'OPERATOR'},
2287 DESCRIPTION => join( ' ',
2288 $self->loc('Priority'),
2289 $args{'OPERATOR'}, $args{'VALUE'}, ),
2295 # {{{ sub LimitInitialPriority
2297 =head2 LimitInitialPriority
2299 Takes a paramhash with the fields OPERATOR and VALUE.
2300 OPERATOR is one of =, >, < or !=.
2301 VALUE is a value to match the ticket\'s initial priority against
2306 sub LimitInitialPriority {
2310 FIELD => 'InitialPriority',
2311 VALUE => $args{'VALUE'},
2312 OPERATOR => $args{'OPERATOR'},
2313 DESCRIPTION => join( ' ',
2314 $self->loc('Initial Priority'), $args{'OPERATOR'},
2321 # {{{ sub LimitFinalPriority
2323 =head2 LimitFinalPriority
2325 Takes a paramhash with the fields OPERATOR and VALUE.
2326 OPERATOR is one of =, >, < or !=.
2327 VALUE is a value to match the ticket\'s final priority against
2331 sub LimitFinalPriority {
2335 FIELD => 'FinalPriority',
2336 VALUE => $args{'VALUE'},
2337 OPERATOR => $args{'OPERATOR'},
2338 DESCRIPTION => join( ' ',
2339 $self->loc('Final Priority'), $args{'OPERATOR'},
2346 # {{{ sub LimitTimeWorked
2348 =head2 LimitTimeWorked
2350 Takes a paramhash with the fields OPERATOR and VALUE.
2351 OPERATOR is one of =, >, < or !=.
2352 VALUE is a value to match the ticket's TimeWorked attribute
2356 sub LimitTimeWorked {
2360 FIELD => 'TimeWorked',
2361 VALUE => $args{'VALUE'},
2362 OPERATOR => $args{'OPERATOR'},
2363 DESCRIPTION => join( ' ',
2364 $self->loc('Time Worked'),
2365 $args{'OPERATOR'}, $args{'VALUE'}, ),
2371 # {{{ sub LimitTimeLeft
2373 =head2 LimitTimeLeft
2375 Takes a paramhash with the fields OPERATOR and VALUE.
2376 OPERATOR is one of =, >, < or !=.
2377 VALUE is a value to match the ticket's TimeLeft attribute
2385 FIELD => 'TimeLeft',
2386 VALUE => $args{'VALUE'},
2387 OPERATOR => $args{'OPERATOR'},
2388 DESCRIPTION => join( ' ',
2389 $self->loc('Time Left'),
2390 $args{'OPERATOR'}, $args{'VALUE'}, ),
2398 # {{{ Limiting based on attachment attributes
2400 # {{{ sub LimitContent
2404 Takes a paramhash with the fields OPERATOR and VALUE.
2405 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2406 VALUE is a string to search for in the body of the ticket
2415 VALUE => $args{'VALUE'},
2416 OPERATOR => $args{'OPERATOR'},
2417 DESCRIPTION => join( ' ',
2418 $self->loc('Ticket content'), $args{'OPERATOR'},
2425 # {{{ sub LimitFilename
2427 =head2 LimitFilename
2429 Takes a paramhash with the fields OPERATOR and VALUE.
2430 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2431 VALUE is a string to search for in the body of the ticket
2439 FIELD => 'Filename',
2440 VALUE => $args{'VALUE'},
2441 OPERATOR => $args{'OPERATOR'},
2442 DESCRIPTION => join( ' ',
2443 $self->loc('Attachment filename'), $args{'OPERATOR'},
2449 # {{{ sub LimitContentType
2451 =head2 LimitContentType
2453 Takes a paramhash with the fields OPERATOR and VALUE.
2454 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2455 VALUE is a content type to search ticket attachments for
2459 sub LimitContentType {
2463 FIELD => 'ContentType',
2464 VALUE => $args{'VALUE'},
2465 OPERATOR => $args{'OPERATOR'},
2466 DESCRIPTION => join( ' ',
2467 $self->loc('Ticket content type'), $args{'OPERATOR'},
2476 # {{{ Limiting based on people
2478 # {{{ sub LimitOwner
2482 Takes a paramhash with the fields OPERATOR and VALUE.
2483 OPERATOR is one of = or !=.
2495 my $owner = new RT::User( $self->CurrentUser );
2496 $owner->Load( $args{'VALUE'} );
2498 # FIXME: check for a valid $owner
2501 VALUE => $args{'VALUE'},
2502 OPERATOR => $args{'OPERATOR'},
2503 DESCRIPTION => join( ' ',
2504 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2511 # {{{ Limiting watchers
2513 # {{{ sub LimitWatcher
2517 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2518 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2519 VALUE is a value to match the ticket\'s watcher email addresses against
2520 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2534 #build us up a description
2535 my ( $watcher_type, $desc );
2536 if ( $args{'TYPE'} ) {
2537 $watcher_type = $args{'TYPE'};
2540 $watcher_type = "Watcher";
2544 FIELD => $watcher_type,
2545 VALUE => $args{'VALUE'},
2546 OPERATOR => $args{'OPERATOR'},
2547 TYPE => $args{'TYPE'},
2548 DESCRIPTION => join( ' ',
2549 $self->loc($watcher_type),
2550 $args{'OPERATOR'}, $args{'VALUE'}, ),
2560 # {{{ Limiting based on links
2564 =head2 LimitLinkedTo
2566 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2567 TYPE limits the sort of link we want to search on
2569 TYPE = { RefersTo, MemberOf, DependsOn }
2571 TARGET is the id or URI of the TARGET of the link
2585 FIELD => 'LinkedTo',
2587 TARGET => $args{'TARGET'},
2588 TYPE => $args{'TYPE'},
2589 DESCRIPTION => $self->loc(
2590 "Tickets [_1] by [_2]",
2591 $self->loc( $args{'TYPE'} ),
2594 OPERATOR => $args{'OPERATOR'},
2600 # {{{ LimitLinkedFrom
2602 =head2 LimitLinkedFrom
2604 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2605 TYPE limits the sort of link we want to search on
2608 BASE is the id or URI of the BASE of the link
2612 sub LimitLinkedFrom {
2621 # translate RT2 From/To naming to RT3 TicketSQL naming
2622 my %fromToMap = qw(DependsOn DependentOn
2624 RefersTo ReferredToBy);
2626 my $type = $args{'TYPE'};
2627 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2630 FIELD => 'LinkedTo',
2632 BASE => $args{'BASE'},
2634 DESCRIPTION => $self->loc(
2635 "Tickets [_1] [_2]",
2636 $self->loc( $args{'TYPE'} ),
2639 OPERATOR => $args{'OPERATOR'},
2648 my $ticket_id = shift;
2649 return $self->LimitLinkedTo(
2651 TARGET => $ticket_id,
2658 # {{{ LimitHasMember
2659 sub LimitHasMember {
2661 my $ticket_id = shift;
2662 return $self->LimitLinkedFrom(
2664 BASE => "$ticket_id",
2665 TYPE => 'HasMember',
2672 # {{{ LimitDependsOn
2674 sub LimitDependsOn {
2676 my $ticket_id = shift;
2677 return $self->LimitLinkedTo(
2679 TARGET => $ticket_id,
2680 TYPE => 'DependsOn',
2687 # {{{ LimitDependedOnBy
2689 sub LimitDependedOnBy {
2691 my $ticket_id = shift;
2692 return $self->LimitLinkedFrom(
2695 TYPE => 'DependentOn',
2706 my $ticket_id = shift;
2707 return $self->LimitLinkedTo(
2709 TARGET => $ticket_id,
2717 # {{{ LimitReferredToBy
2719 sub LimitReferredToBy {
2721 my $ticket_id = shift;
2722 return $self->LimitLinkedFrom(
2725 TYPE => 'ReferredToBy',
2733 # {{{ limit based on ticket date attribtes
2737 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2739 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2741 OPERATOR is one of > or <
2742 VALUE is a date and time in ISO format in GMT
2743 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2745 There are also helper functions of the form LimitFIELD that eliminate
2746 the need to pass in a FIELD argument.
2760 #Set the description if we didn't get handed it above
2761 unless ( $args{'DESCRIPTION'} ) {
2762 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2763 . $args{'OPERATOR'} . " "
2764 . $args{'VALUE'} . " GMT";
2767 $self->Limit(%args);
2775 $self->LimitDate( FIELD => 'Created', @_ );
2780 $self->LimitDate( FIELD => 'Due', @_ );
2786 $self->LimitDate( FIELD => 'Starts', @_ );
2792 $self->LimitDate( FIELD => 'Started', @_ );
2797 $self->LimitDate( FIELD => 'Resolved', @_ );
2802 $self->LimitDate( FIELD => 'Told', @_ );
2805 sub LimitLastUpdated {
2807 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2811 # {{{ sub LimitTransactionDate
2813 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2815 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2817 OPERATOR is one of > or <
2818 VALUE is a date and time in ISO format in GMT
2823 sub LimitTransactionDate {
2826 FIELD => 'TransactionDate',
2833 # <20021217042756.GK28744@pallas.fsck.com>
2834 # "Kill It" - Jesse.
2836 #Set the description if we didn't get handed it above
2837 unless ( $args{'DESCRIPTION'} ) {
2838 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2839 . $args{'OPERATOR'} . " "
2840 . $args{'VALUE'} . " GMT";
2843 $self->Limit(%args);
2851 # {{{ Limit based on custom fields
2852 # {{{ sub LimitCustomField
2854 =head2 LimitCustomField
2856 Takes a paramhash of key/value pairs with the following keys:
2860 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2862 =item OPERATOR - The usual Limit operators
2864 =item VALUE - The value to compare against
2870 sub LimitCustomField {
2874 CUSTOMFIELD => undef,
2876 DESCRIPTION => undef,
2877 FIELD => 'CustomFieldValue',
2882 my $CF = RT::CustomField->new( $self->CurrentUser );
2883 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2884 $CF->Load( $args{CUSTOMFIELD} );
2887 $CF->LoadByNameAndQueue(
2888 Name => $args{CUSTOMFIELD},
2889 Queue => $args{QUEUE}
2891 $args{CUSTOMFIELD} = $CF->Id;
2894 #If we are looking to compare with a null value.
2895 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2896 $args{'DESCRIPTION'}
2897 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2899 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2900 $args{'DESCRIPTION'}
2901 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2904 # if we're not looking to compare with a null value
2906 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2907 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2910 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2911 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2912 $QueueObj->Load( $args{'QUEUE'} );
2913 $args{'QUEUE'} = $QueueObj->Id;
2915 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2918 @rest = ( ENTRYAGGREGATOR => 'AND' )
2919 if ( $CF->Type eq 'SelectMultiple' );
2922 VALUE => $args{VALUE},
2924 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2925 .".{" . $CF->Name . "}",
2926 OPERATOR => $args{OPERATOR},
2931 $self->{'RecalcTicketLimits'} = 1;
2937 # {{{ sub _NextIndex
2941 Keep track of the counter for the array of restrictions
2947 return ( $self->{'restriction_index'}++ );
2954 # {{{ Core bits to make this a DBIx::SearchBuilder object
2959 $self->{'table'} = "Tickets";
2960 $self->{'RecalcTicketLimits'} = 1;
2961 $self->{'looking_at_effective_id'} = 0;
2962 $self->{'looking_at_type'} = 0;
2963 $self->{'restriction_index'} = 1;
2964 $self->{'primary_key'} = "id";
2965 delete $self->{'items_array'};
2966 delete $self->{'item_map'};
2967 delete $self->{'columns_to_display'};
2968 $self->SUPER::_Init(@_);
2979 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2980 return ( $self->SUPER::Count() );
2988 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2989 return ( $self->SUPER::CountAll() );
2994 # {{{ sub ItemsArrayRef
2996 =head2 ItemsArrayRef
2998 Returns a reference to the set of all items found in this search
3005 return $self->{'items_array'} if $self->{'items_array'};
3007 my $placeholder = $self->_ItemsCounter;
3008 $self->GotoFirstItem();
3009 while ( my $item = $self->Next ) {
3010 push( @{ $self->{'items_array'} }, $item );
3012 $self->GotoItem($placeholder);
3013 $self->{'items_array'}
3014 = $self->ItemsOrderBy( $self->{'items_array'} );
3016 return $self->{'items_array'};
3019 sub ItemsArrayRefWindow {
3023 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3025 $self->RowsPerPage( $window );
3027 $self->GotoFirstItem;
3030 while ( my $item = $self->Next ) {
3034 $self->RowsPerPage( $old[1] );
3035 $self->FirstRow( $old[2] );
3036 $self->GotoItem( $old[0] );
3047 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3049 my $Ticket = $self->SUPER::Next;
3050 return $Ticket unless $Ticket;
3052 if ( $Ticket->__Value('Status') eq 'deleted'
3053 && !$self->{'allow_deleted_search'} )
3057 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3058 # if we found a ticket with this option enabled then
3059 # all tickets we found are ACLed, cache this fact
3060 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3061 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3064 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3069 # If the user doesn't have the right to show this ticket
3076 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3077 return $self->SUPER::_DoSearch( @_ );
3082 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3083 return $self->SUPER::_DoCount( @_ );
3089 my $cache_key = 'RolesHasRight;:;ShowTicket';
3091 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3095 my $ACL = RT::ACL->new( $RT::SystemUser );
3096 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3097 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3098 my $principal_alias = $ACL->Join(
3100 FIELD1 => 'PrincipalId',
3101 TABLE2 => 'Principals',
3104 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3107 while ( my $ACE = $ACL->Next ) {
3108 my $role = $ACE->PrincipalType;
3109 my $type = $ACE->ObjectType;
3110 if ( $type eq 'RT::System' ) {
3113 elsif ( $type eq 'RT::Queue' ) {
3114 next if $res{ $role } && !ref $res{ $role };
3115 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3118 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3121 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3125 sub _DirectlyCanSeeIn {
3127 my $id = $self->CurrentUser->id;
3129 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3130 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3134 my $ACL = RT::ACL->new( $RT::SystemUser );
3135 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3136 my $principal_alias = $ACL->Join(
3138 FIELD1 => 'PrincipalId',
3139 TABLE2 => 'Principals',
3142 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3143 my $cgm_alias = $ACL->Join(
3145 FIELD1 => 'PrincipalId',
3146 TABLE2 => 'CachedGroupMembers',
3147 FIELD2 => 'GroupId',
3149 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3150 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3153 while ( my $ACE = $ACL->Next ) {
3154 my $type = $ACE->ObjectType;
3155 if ( $type eq 'RT::System' ) {
3156 # If user is direct member of a group that has the right
3157 # on the system then he can see any ticket
3158 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3161 elsif ( $type eq 'RT::Queue' ) {
3162 push @res, $ACE->ObjectId;
3165 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3168 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3172 sub CurrentUserCanSee {
3174 return if $self->{'_sql_current_user_can_see_applied'};
3176 return $self->{'_sql_current_user_can_see_applied'} = 1
3177 if $self->CurrentUser->UserObj->HasRight(
3178 Right => 'SuperUser', Object => $RT::System
3181 my $id = $self->CurrentUser->id;
3183 # directly can see in all queues then we have nothing to do
3184 my @direct_queues = $self->_DirectlyCanSeeIn;
3185 return $self->{'_sql_current_user_can_see_applied'} = 1
3186 if @direct_queues && $direct_queues[0] == -1;
3188 my %roles = $self->_RolesCanSee;
3190 my %skip = map { $_ => 1 } @direct_queues;
3191 foreach my $role ( keys %roles ) {
3192 next unless ref $roles{ $role };
3194 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3196 $roles{ $role } = \@queues;
3198 delete $roles{ $role };
3203 # there is no global watchers, only queues and tickes, if at
3204 # some point we will add global roles then it's gonna blow
3205 # the idea here is that if the right is set globaly for a role
3206 # and user plays this role for a queue directly not a ticket
3207 # then we have to check in advance
3208 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3210 my $groups = RT::Groups->new( $RT::SystemUser );
3211 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3213 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3215 my $principal_alias = $groups->Join(
3218 TABLE2 => 'Principals',
3221 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3222 my $cgm_alias = $groups->Join(
3225 TABLE2 => 'CachedGroupMembers',
3226 FIELD2 => 'GroupId',
3228 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3229 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3230 while ( my $group = $groups->Next ) {
3231 push @direct_queues, $group->Instance;
3235 unless ( @direct_queues || keys %roles ) {
3236 $self->SUPER::Limit(
3241 ENTRYAGGREGATOR => 'AND',
3243 return $self->{'_sql_current_user_can_see_applied'} = 1;
3247 my $join_roles = keys %roles;
3248 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3249 my ($role_group_alias, $cgm_alias);
3250 if ( $join_roles ) {
3251 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3252 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3253 $self->SUPER::Limit(
3254 LEFTJOIN => $cgm_alias,
3255 FIELD => 'MemberId',
3260 my $limit_queues = sub {
3264 return unless @queues;
3265 if ( @queues == 1 ) {
3266 $self->SUPER::Limit(
3271 ENTRYAGGREGATOR => $ea,
3274 $self->SUPER::_OpenParen('ACL');
3275 foreach my $q ( @queues ) {
3276 $self->SUPER::Limit(
3281 ENTRYAGGREGATOR => $ea,
3285 $self->SUPER::_CloseParen('ACL');
3290 $self->SUPER::_OpenParen('ACL');
3292 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3293 while ( my ($role, $queues) = each %roles ) {
3294 $self->SUPER::_OpenParen('ACL');
3295 if ( $role eq 'Owner' ) {
3296 $self->SUPER::Limit(
3300 ENTRYAGGREGATOR => $ea,
3304 $self->SUPER::Limit(
3306 ALIAS => $cgm_alias,
3307 FIELD => 'MemberId',
3308 OPERATOR => 'IS NOT',
3311 ENTRYAGGREGATOR => $ea,
3313 $self->SUPER::Limit(
3315 ALIAS => $role_group_alias,
3318 ENTRYAGGREGATOR => 'AND',
3321 $limit_queues->( 'AND', @$queues ) if ref $queues;
3322 $ea = 'OR' if $ea eq 'AND';
3323 $self->SUPER::_CloseParen('ACL');
3325 $self->SUPER::_CloseParen('ACL');
3327 return $self->{'_sql_current_user_can_see_applied'} = 1;
3334 # {{{ Deal with storing and restoring restrictions
3336 # {{{ sub LoadRestrictions
3338 =head2 LoadRestrictions
3340 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3341 TODO It is not yet implemented
3347 # {{{ sub DescribeRestrictions
3349 =head2 DescribeRestrictions
3352 Returns a hash keyed by restriction id.
3353 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3354 is a description of the purpose of that TicketRestriction
3358 sub DescribeRestrictions {
3363 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3364 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3371 # {{{ sub RestrictionValues
3373 =head2 RestrictionValues FIELD
3375 Takes a restriction field and returns a list of values this field is restricted
3380 sub RestrictionValues {
3383 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3384 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3385 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3387 keys %{ $self->{'TicketRestrictions'} };
3392 # {{{ sub ClearRestrictions
3394 =head2 ClearRestrictions
3396 Removes all restrictions irretrievably
3400 sub ClearRestrictions {
3402 delete $self->{'TicketRestrictions'};
3403 $self->{'looking_at_effective_id'} = 0;
3404 $self->{'looking_at_type'} = 0;
3405 $self->{'RecalcTicketLimits'} = 1;
3410 # {{{ sub DeleteRestriction
3412 =head2 DeleteRestriction
3414 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3415 Removes that restriction from the session's limits.
3419 sub DeleteRestriction {
3422 delete $self->{'TicketRestrictions'}{$row};
3424 $self->{'RecalcTicketLimits'} = 1;
3426 #make the underlying easysearch object forget all its preconceptions
3431 # {{{ sub _RestrictionsToClauses
3433 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3435 sub _RestrictionsToClauses {
3439 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3440 my $restriction = $self->{'TicketRestrictions'}{$row};
3442 # We need to reimplement the subclause aggregation that SearchBuilder does.
3443 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3444 # Then SB AND's the different Subclauses together.
3446 # So, we want to group things into Subclauses, convert them to
3447 # SQL, and then join them with the appropriate DefaultEA.
3448 # Then join each subclause group with AND.
3450 my $field = $restriction->{'FIELD'};
3451 my $realfield = $field; # CustomFields fake up a fieldname, so
3452 # we need to figure that out
3455 # Rewrite LinkedTo meta field to the real field
3456 if ( $field =~ /LinkedTo/ ) {
3457 $realfield = $field = $restriction->{'TYPE'};
3461 # Handle subkey fields with a different real field
3462 if ( $field =~ /^(\w+)\./ ) {
3466 die "I don't know about $field yet"
3467 unless ( exists $FIELD_METADATA{$realfield}
3468 or $restriction->{CUSTOMFIELD} );
3470 my $type = $FIELD_METADATA{$realfield}->[0];
3471 my $op = $restriction->{'OPERATOR'};
3475 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3478 # this performs the moral equivalent of defined or/dor/C<//>,
3479 # without the short circuiting.You need to use a 'defined or'
3480 # type thing instead of just checking for truth values, because
3481 # VALUE could be 0.(i.e. "false")
3483 # You could also use this, but I find it less aesthetic:
3484 # (although it does short circuit)
3485 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3486 # defined $restriction->{'TICKET'} ?
3487 # $restriction->{TICKET} :
3488 # defined $restriction->{'BASE'} ?
3489 # $restriction->{BASE} :
3490 # defined $restriction->{'TARGET'} ?
3491 # $restriction->{TARGET} )
3493 my $ea = $restriction->{ENTRYAGGREGATOR}
3494 || $DefaultEA{$type}
3497 die "Invalid operator $op for $field ($type)"
3498 unless exists $ea->{$op};
3502 # Each CustomField should be put into a different Clause so they
3503 # are ANDed together.
3504 if ( $restriction->{CUSTOMFIELD} ) {
3505 $realfield = $field;
3508 exists $clause{$realfield} or $clause{$realfield} = [];
3511 $field =~ s!(['"])!\\$1!g;
3512 $value =~ s!(['"])!\\$1!g;
3513 my $data = [ $ea, $type, $field, $op, $value ];
3515 # here is where we store extra data, say if it's a keyword or
3516 # something. (I.e. "TYPE SPECIFIC STUFF")
3518 push @{ $clause{$realfield} }, $data;
3525 # {{{ sub _ProcessRestrictions
3527 =head2 _ProcessRestrictions PARAMHASH
3529 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3530 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3534 sub _ProcessRestrictions {
3537 #Blow away ticket aliases since we'll need to regenerate them for
3539 delete $self->{'TicketAliases'};
3540 delete $self->{'items_array'};
3541 delete $self->{'item_map'};
3542 delete $self->{'raw_rows'};
3543 delete $self->{'rows'};
3544 delete $self->{'count_all'};
3546 my $sql = $self->Query; # Violating the _SQL namespace
3547 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3549 # "Restrictions to Clauses Branch\n";
3550 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3552 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3556 $sql = $self->ClausesToSQL($clauseRef);
3557 $self->FromSQL($sql) if $sql;
3561 $self->{'RecalcTicketLimits'} = 0;
3565 =head2 _BuildItemMap
3567 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3568 display search nav quickly.
3575 my $window = RT->Config->Get('TicketsItemMapSize');
3577 $self->{'item_map'} = {};
3579 my $items = $self->ItemsArrayRefWindow( $window );
3580 return unless $items && @$items;
3583 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3584 for ( my $i = 0; $i < @$items; $i++ ) {
3585 my $item = $items->[$i];
3586 my $id = $item->EffectiveId;
3587 $self->{'item_map'}{$id}{'defined'} = 1;
3588 $self->{'item_map'}{$id}{'prev'} = $prev;
3589 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3593 $self->{'item_map'}{'last'} = $prev
3594 if !$window || @$items < $window;
3599 Returns an a map of all items found by this search. The map is a hash
3603 first => <first ticket id found>,
3604 last => <last ticket id found or undef>,
3607 prev => <the ticket id found before>,
3608 next => <the ticket id found after>,
3620 $self->_BuildItemMap unless $self->{'item_map'};
3621 return $self->{'item_map'};
3629 =head2 PrepForSerialization
3631 You don't want to serialize a big tickets object, as
3632 the {items} hash will be instantly invalid _and_ eat
3637 sub PrepForSerialization {
3639 delete $self->{'items'};
3640 delete $self->{'items_array'};
3641 $self->RedoSearch();
3646 RT::Tickets supports several flags which alter search behavior:
3649 allow_deleted_search (Otherwise never show deleted tickets in search results)
3650 looking_at_type (otherwise limit to type=ticket)
3652 These flags are set by calling
3654 $tickets->{'flagname'} = 1;
3656 BUG: There should be an API for this