1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
51 # - Decimated ProcessRestrictions and broke it into multiple
52 # functions joined by a LUT
53 # - Semi-Generic SQL stuff moved to another file
55 # Known Issues: FIXME!
57 # - ClearRestrictions and Reinitialization is messy and unclear. The
58 # only good way to do it is to create a new RT::Tickets object.
62 RT::Tickets - A collection of Ticket objects
68 my $tickets = new RT::Tickets($CurrentUser);
72 A collection of RT::Tickets.
82 no warnings qw(redefine);
85 use DBIx::SearchBuilder::Unique;
87 # Configuration Tables:
89 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
92 our %FIELD_METADATA = (
93 Status => [ 'ENUM', ], #loc_left_pair
94 Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
95 Type => [ 'ENUM', ], #loc_left_pair
96 Creator => [ 'ENUM' => 'User', ], #loc_left_pair
97 LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
98 Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
99 EffectiveId => [ 'INT', ], #loc_left_pair
100 id => [ 'ID', ], #loc_left_pair
101 InitialPriority => [ 'INT', ], #loc_left_pair
102 FinalPriority => [ 'INT', ], #loc_left_pair
103 Priority => [ 'INT', ], #loc_left_pair
104 TimeLeft => [ 'INT', ], #loc_left_pair
105 TimeWorked => [ 'INT', ], #loc_left_pair
106 TimeEstimated => [ 'INT', ], #loc_left_pair
108 Linked => [ 'LINK' ], #loc_left_pair
109 LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
110 LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
111 MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
112 DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
113 RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
114 HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
115 DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
116 DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
117 ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
118 Told => [ 'DATE' => 'Told', ], #loc_left_pair
119 Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
120 Started => [ 'DATE' => 'Started', ], #loc_left_pair
121 Due => [ 'DATE' => 'Due', ], #loc_left_pair
122 Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
123 LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
124 Created => [ 'DATE' => 'Created', ], #loc_left_pair
125 Subject => [ 'STRING', ], #loc_left_pair
126 Content => [ 'TRANSFIELD', ], #loc_left_pair
127 ContentType => [ 'TRANSFIELD', ], #loc_left_pair
128 Filename => [ 'TRANSFIELD', ], #loc_left_pair
129 TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
130 Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
131 Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
132 Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
133 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
134 Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
135 QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
136 QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
137 QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
138 CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
139 CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
140 CF => [ 'CUSTOMFIELD', ], #loc_left_pair
141 Updated => [ 'TRANSDATE', ], #loc_left_pair
142 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
143 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
144 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
145 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
146 HasAttribute => [ 'HASATTRIBUTE', 1 ],
147 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
149 Agentnum => [ 'FREESIDEFIELD', ],
150 Classnum => [ 'FREESIDEFIELD', ],
151 Refnum => [ 'FREESIDEFIELD', ],
152 Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ],
153 WillResolve => [ 'DATE' => 'WillResolve', ], #loc_left_pair
156 our %SEARCHABLE_SUBFIELDS = (
158 EmailAddress Name RealName Nickname Organization Address1 Address2
159 WorkPhone HomePhone MobilePhone PagerPhone id
163 # Mapping of Field Type to Function
165 ENUM => \&_EnumLimit,
168 LINK => \&_LinkLimit,
169 DATE => \&_DateLimit,
170 STRING => \&_StringLimit,
171 TRANSFIELD => \&_TransLimit,
172 TRANSDATE => \&_TransDateLimit,
173 WATCHERFIELD => \&_WatcherLimit,
174 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
175 CUSTOMFIELD => \&_CustomFieldLimit,
176 HASATTRIBUTE => \&_HasAttributeLimit,
177 FREESIDEFIELD => \&_FreesideFieldLimit,
179 our %can_bundle = ();# WATCHERFIELD => "yes", );
181 # Default EntryAggregator per type
182 # if you specify OP, you must specify all valid OPs
223 # Helper functions for passing the above lexically scoped tables above
224 # into Tickets_Overlay_SQL.
225 sub FIELDS { return \%FIELD_METADATA }
226 sub dispatch { return \%dispatch }
227 sub can_bundle { return \%can_bundle }
229 # Bring in the clowns.
230 require RT::Tickets_Overlay_SQL;
234 our @SORTFIELDS = qw(id Status
236 Owner Created Due Starts Started
238 Resolved LastUpdated Priority TimeWorked TimeLeft);
242 Returns the list of fields that lists of tickets can easily be sorted by
248 return (@SORTFIELDS);
253 # BEGIN SQL STUFF *********************************
258 $self->SUPER::CleanSlate( @_ );
259 delete $self->{$_} foreach qw(
261 _sql_group_members_aliases
262 _sql_object_cfv_alias
263 _sql_role_group_aliases
266 _sql_u_watchers_alias_for_sort
267 _sql_u_watchers_aliases
268 _sql_current_user_can_see_applied
272 =head1 Limit Helper Routines
274 These routines are the targets of a dispatch table depending on the
275 type of field. They all share the same signature:
277 my ($self,$field,$op,$value,@rest) = @_;
279 The values in @rest should be suitable for passing directly to
280 DBIx::SearchBuilder::Limit.
282 Essentially they are an expanded/broken out (and much simplified)
283 version of what ProcessRestrictions used to do. They're also much
284 more clearly delineated by the TYPE of field being processed.
293 my ( $sb, $field, $op, $value, @rest ) = @_;
295 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
297 die "Invalid operator $op for __Bookmarked__ search on $field"
298 unless $op =~ /^(=|!=)$/;
301 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
302 $tmp = $tmp->Content if $tmp;
307 return $sb->_SQLLimit(
314 # as bookmarked tickets can be merged we have to use a join
315 # but it should be pretty lightweight
316 my $tickets_alias = $sb->Join(
321 FIELD2 => 'EffectiveId',
325 my $ea = $op eq '='? 'OR': 'AND';
326 foreach my $id ( sort @bookmarks ) {
328 ALIAS => $tickets_alias,
332 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
340 Handle Fields which are limited to certain values, and potentially
341 need to be looked up from another class.
343 This subroutine actually handles two different kinds of fields. For
344 some the user is responsible for limiting the values. (i.e. Status,
347 For others, the value specified by the user will be looked by via
351 name of class to lookup in (Optional)
356 my ( $sb, $field, $op, $value, @rest ) = @_;
358 # SQL::Statement changes != to <>. (Can we remove this now?)
359 $op = "!=" if $op eq "<>";
361 die "Invalid Operation: $op for $field"
365 my $meta = $FIELD_METADATA{$field};
366 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
367 my $class = "RT::" . $meta->[1];
368 my $o = $class->new( $sb->CurrentUser );
382 Handle fields where the values are limited to integers. (For example,
383 Priority, TimeWorked.)
391 my ( $sb, $field, $op, $value, @rest ) = @_;
393 die "Invalid Operator $op for $field"
394 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
406 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
409 1: Direction (From, To)
410 2: Link Type (MemberOf, DependsOn, RefersTo)
415 my ( $sb, $field, $op, $value, @rest ) = @_;
417 my $meta = $FIELD_METADATA{$field};
418 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
421 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
425 $is_null = 1 if !$value || $value =~ /^null$/io;
427 my $direction = $meta->[1] || '';
428 my ($matchfield, $linkfield) = ('', '');
429 if ( $direction eq 'To' ) {
430 ($matchfield, $linkfield) = ("Target", "Base");
432 elsif ( $direction eq 'From' ) {
433 ($matchfield, $linkfield) = ("Base", "Target");
435 elsif ( $direction ) {
436 die "Invalid link direction '$direction' for $field\n";
439 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
441 'LinkedFrom', $op, $value, @rest,
442 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
450 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
452 elsif ( $value =~ /\D/ ) {
455 $matchfield = "Local$matchfield" if $is_local;
457 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
458 # SELECT main.* FROM Tickets main
459 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
460 # AND(main.id = Links_1.LocalTarget))
461 # WHERE Links_1.LocalBase IS NULL;
464 my $linkalias = $sb->Join(
469 FIELD2 => 'Local' . $linkfield
472 LEFTJOIN => $linkalias,
480 FIELD => $matchfield,
487 my $linkalias = $sb->Join(
492 FIELD2 => 'Local' . $linkfield
495 LEFTJOIN => $linkalias,
501 LEFTJOIN => $linkalias,
502 FIELD => $matchfield,
509 FIELD => $matchfield,
510 OPERATOR => $is_negative? 'IS': 'IS NOT',
519 Handle date fields. (Created, LastTold..)
522 1: type of link. (Probably not necessary.)
527 my ( $sb, $field, $op, $value, @rest ) = @_;
529 die "Invalid Date Op: $op"
530 unless $op =~ /^(=|>|<|>=|<=)$/;
532 my $meta = $FIELD_METADATA{$field};
533 die "Incorrect Meta Data for $field"
534 unless ( defined $meta->[1] );
536 $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest );
539 # Factor this out for use by custom fields
541 sub _DateFieldLimit {
542 my ( $sb, $field, $op, $value, @rest ) = @_;
544 my $date = RT::Date->new( $sb->CurrentUser );
545 $date->Set( Format => 'unknown', Value => $value );
549 # if we're specifying =, that means we want everything on a
550 # particular single day. in the database, we need to check for >
551 # and < the edges of that day.
553 # Except if the value is 'this month' or 'last month', check
554 # > and < the edges of the month.
556 my ($daystart, $dayend);
557 if ( lc($value) eq 'this month' ) {
559 $date->SetToStart('month', Timezone => 'server');
560 $daystart = $date->ISO;
561 $date->AddMonth(Timezone => 'server');
562 $dayend = $date->ISO;
564 elsif ( lc($value) eq 'last month' ) {
566 $date->SetToStart('month', Timezone => 'server');
567 $dayend = $date->ISO;
569 $date->SetToStart('month', Timezone => 'server');
570 $daystart = $date->ISO;
573 $date->SetToMidnight( Timezone => 'server' );
574 $daystart = $date->ISO;
576 $dayend = $date->ISO;
593 ENTRYAGGREGATOR => 'AND',
611 Handle simple fields which are just strings. (Subject,Type)
619 my ( $sb, $field, $op, $value, @rest ) = @_;
623 # =, !=, LIKE, NOT LIKE
624 if ( (!defined $value || !length $value)
625 && lc($op) ne 'is' && lc($op) ne 'is not'
626 && RT->Config->Get('DatabaseType') eq 'Oracle'
628 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
629 $op = $negative? 'IS NOT': 'IS';
642 =head2 _TransDateLimit
644 Handle fields limiting based on Transaction Date.
646 The inpupt value must be in a format parseable by Time::ParseDate
653 # This routine should really be factored into translimit.
654 sub _TransDateLimit {
655 my ( $sb, $field, $op, $value, @rest ) = @_;
657 # See the comments for TransLimit, they apply here too
659 unless ( $sb->{_sql_transalias} ) {
660 $sb->{_sql_transalias} = $sb->Join(
663 TABLE2 => 'Transactions',
664 FIELD2 => 'ObjectId',
667 ALIAS => $sb->{_sql_transalias},
668 FIELD => 'ObjectType',
669 VALUE => 'RT::Ticket',
670 ENTRYAGGREGATOR => 'AND',
674 my $date = RT::Date->new( $sb->CurrentUser );
675 $date->Set( Format => 'unknown', Value => $value );
680 # if we're specifying =, that means we want everything on a
681 # particular single day. in the database, we need to check for >
682 # and < the edges of that day.
684 $date->SetToMidnight( Timezone => 'server' );
685 my $daystart = $date->ISO;
687 my $dayend = $date->ISO;
690 ALIAS => $sb->{_sql_transalias},
698 ALIAS => $sb->{_sql_transalias},
704 ENTRYAGGREGATOR => 'AND',
709 # not searching for a single day
712 #Search for the right field
714 ALIAS => $sb->{_sql_transalias},
728 Limit based on the Content of a transaction or the ContentType.
737 # Content, ContentType, Filename
739 # If only this was this simple. We've got to do something
742 #Basically, we want to make sure that the limits apply to
743 #the same attachment, rather than just another attachment
744 #for the same ticket, no matter how many clauses we lump
745 #on. We put them in TicketAliases so that they get nuked
746 #when we redo the join.
748 # In the SQL, we might have
749 # (( Content = foo ) or ( Content = bar AND Content = baz ))
750 # The AND group should share the same Alias.
752 # Actually, maybe it doesn't matter. We use the same alias and it
753 # works itself out? (er.. different.)
755 # Steal more from _ProcessRestrictions
757 # FIXME: Maybe look at the previous FooLimit call, and if it was a
758 # TransLimit and EntryAggregator == AND, reuse the Aliases?
760 # Or better - store the aliases on a per subclause basis - since
761 # those are going to be the things we want to relate to each other,
764 # maybe we should not allow certain kinds of aggregation of these
765 # clauses and do a psuedo regex instead? - the problem is getting
766 # them all into the same subclause when you have (A op B op C) - the
767 # way they get parsed in the tree they're in different subclauses.
769 my ( $self, $field, $op, $value, %rest ) = @_;
771 unless ( $self->{_sql_transalias} ) {
772 $self->{_sql_transalias} = $self->Join(
775 TABLE2 => 'Transactions',
776 FIELD2 => 'ObjectId',
779 ALIAS => $self->{_sql_transalias},
780 FIELD => 'ObjectType',
781 VALUE => 'RT::Ticket',
782 ENTRYAGGREGATOR => 'AND',
785 unless ( defined $self->{_sql_trattachalias} ) {
786 $self->{_sql_trattachalias} = $self->_SQLJoin(
787 TYPE => 'LEFT', # not all txns have an attachment
788 ALIAS1 => $self->{_sql_transalias},
790 TABLE2 => 'Attachments',
791 FIELD2 => 'TransactionId',
795 #Search for the right field
796 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
800 ALIAS => $self->{_sql_trattachalias},
807 ENTRYAGGREGATOR => 'AND',
808 ALIAS => $self->{_sql_trattachalias},
817 ALIAS => $self->{_sql_trattachalias},
830 Handle watcher limits. (Requestor, CC, etc..)
846 my $meta = $FIELD_METADATA{ $field };
847 my $type = $meta->[1] || '';
848 my $class = $meta->[2] || 'Ticket';
850 # Bail if the subfield is not allowed
852 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
854 die "Invalid watcher subfield: '$rest{SUBKEY}'";
857 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
858 # search by id and Name at the same time, this is workaround
859 # to preserve backward compatibility
860 if ( $field eq 'Owner' ) {
861 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
862 my $o = RT::User->new( $self->CurrentUser );
863 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
864 $o->$method( $value );
873 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
883 $rest{SUBKEY} ||= 'EmailAddress';
885 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
888 if ( $op =~ /^IS(?: NOT)?$/ ) {
889 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
890 # to avoid joining the table Users into the query, we just join GM
891 # and make sure we don't match records where group is member of itself
893 LEFTJOIN => $group_members,
896 VALUE => "$group_members.MemberId",
900 ALIAS => $group_members,
907 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
909 $op =~ s/!|NOT\s+//i;
911 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
912 # "X = 'Y'" matches more then one user so we try to fetch two records and
913 # do the right thing when there is only one exist and semi-working solution
915 my $users_obj = RT::Users->new( $self->CurrentUser );
917 FIELD => $rest{SUBKEY},
922 $users_obj->RowsPerPage(2);
923 my @users = @{ $users_obj->ItemsArrayRef };
925 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
928 $uid = $users[0]->id if @users;
930 LEFTJOIN => $group_members,
931 ALIAS => $group_members,
937 ALIAS => $group_members,
944 LEFTJOIN => $group_members,
947 VALUE => "$group_members.MemberId",
950 my $users = $self->Join(
952 ALIAS1 => $group_members,
953 FIELD1 => 'MemberId',
960 FIELD => $rest{SUBKEY},
974 my $group_members = $self->_GroupMembersJoin(
975 GroupsAlias => $groups,
979 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
981 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
982 $self->NewAlias('Users');
984 LEFTJOIN => $group_members,
985 ALIAS => $group_members,
987 VALUE => "$users.id",
992 # we join users table without adding some join condition between tables,
993 # the only conditions we have are conditions on the table iteslf,
994 # for example Users.EmailAddress = 'x'. We should add this condition to
995 # the top level of the query and bundle it with another similar conditions,
996 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
997 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
1000 SUBCLAUSE => '_sql_u_watchers_'. $users,
1002 FIELD => $rest{'SUBKEY'},
1007 # A condition which ties Users and Groups (role groups) is a left join condition
1008 # of CachedGroupMembers table. To get correct results of the query we check
1009 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
1012 ALIAS => $group_members,
1014 OPERATOR => 'IS NOT',
1021 sub _RoleGroupsJoin {
1023 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1024 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1025 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1028 # we always have watcher groups for ticket, so we use INNER join
1029 my $groups = $self->Join(
1031 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1033 FIELD2 => 'Instance',
1034 ENTRYAGGREGATOR => 'AND',
1036 $self->SUPER::Limit(
1037 LEFTJOIN => $groups,
1040 VALUE => 'RT::'. $args{'Class'} .'-Role',
1042 $self->SUPER::Limit(
1043 LEFTJOIN => $groups,
1046 VALUE => $args{'Type'},
1049 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1050 unless $args{'New'};
1055 sub _GroupMembersJoin {
1057 my %args = (New => 1, GroupsAlias => undef, @_);
1059 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1060 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1063 my $alias = $self->Join(
1065 ALIAS1 => $args{'GroupsAlias'},
1067 TABLE2 => 'CachedGroupMembers',
1068 FIELD2 => 'GroupId',
1069 ENTRYAGGREGATOR => 'AND',
1072 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1073 unless $args{'New'};
1080 Helper function which provides joins to a watchers table both for limits
1087 my $type = shift || '';
1090 my $groups = $self->_RoleGroupsJoin( Type => $type );
1091 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1092 # XXX: work around, we must hide groups that
1093 # are members of the role group we search in,
1094 # otherwise them result in wrong NULLs in Users
1095 # table and break ordering. Now, we know that
1096 # RT doesn't allow to add groups as members of the
1097 # ticket roles, so we just hide entries in CGM table
1098 # with MemberId == GroupId from results
1099 $self->SUPER::Limit(
1100 LEFTJOIN => $group_members,
1103 VALUE => "$group_members.MemberId",
1106 my $users = $self->Join(
1108 ALIAS1 => $group_members,
1109 FIELD1 => 'MemberId',
1113 return ($groups, $group_members, $users);
1116 =head2 _WatcherMembershipLimit
1118 Handle watcher membership limits, i.e. whether the watcher belongs to a
1119 specific group or not.
1122 1: Field to query on
1124 SELECT DISTINCT main.*
1128 CachedGroupMembers CachedGroupMembers_2,
1131 (main.EffectiveId = main.id)
1133 (main.Status != 'deleted')
1135 (main.Type = 'ticket')
1138 (Users_3.EmailAddress = '22')
1140 (Groups_1.Domain = 'RT::Ticket-Role')
1142 (Groups_1.Type = 'RequestorGroup')
1145 Groups_1.Instance = main.id
1147 Groups_1.id = CachedGroupMembers_2.GroupId
1149 CachedGroupMembers_2.MemberId = Users_3.id
1150 ORDER BY main.id ASC
1155 sub _WatcherMembershipLimit {
1156 my ( $self, $field, $op, $value, @rest ) = @_;
1161 my $groups = $self->NewAlias('Groups');
1162 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1163 my $users = $self->NewAlias('Users');
1164 my $memberships = $self->NewAlias('CachedGroupMembers');
1166 if ( ref $field ) { # gross hack
1167 my @bundle = @$field;
1169 for my $chunk (@bundle) {
1170 ( $field, $op, $value, @rest ) = @$chunk;
1172 ALIAS => $memberships,
1183 ALIAS => $memberships,
1191 # {{{ Tie to groups for tickets we care about
1195 VALUE => 'RT::Ticket-Role',
1196 ENTRYAGGREGATOR => 'AND'
1201 FIELD1 => 'Instance',
1208 # If we care about which sort of watcher
1209 my $meta = $FIELD_METADATA{$field};
1210 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1217 ENTRYAGGREGATOR => 'AND'
1224 ALIAS2 => $groupmembers,
1229 ALIAS1 => $groupmembers,
1230 FIELD1 => 'MemberId',
1236 ALIAS1 => $memberships,
1237 FIELD1 => 'MemberId',
1246 =head2 _CustomFieldDecipher
1248 Try and turn a CF descriptor into (cfid, cfname) object pair.
1252 sub _CustomFieldDecipher {
1253 my ($self, $string) = @_;
1255 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1256 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1260 my $q = RT::Queue->new( $self->CurrentUser );
1264 # $queue = $q->Name; # should we normalize the queue?
1265 $cf = $q->CustomField( $field );
1268 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1272 elsif ( $field =~ /\D/ ) {
1274 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1275 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1276 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1278 # if there is more then one field the current user can
1279 # see with the same name then we shouldn't return cf object
1280 # as we don't know which one to use
1283 $cf = undef if $cfs->Next;
1287 $cf = RT::CustomField->new( $self->CurrentUser );
1288 $cf->Load( $field );
1291 return ($queue, $field, $cf, $column);
1294 =head2 _CustomFieldJoin
1296 Factor out the Join of custom fields so we can use it for sorting too
1300 sub _CustomFieldJoin {
1301 my ($self, $cfkey, $cfid, $field) = @_;
1302 # Perform one Join per CustomField
1303 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1304 $self->{_sql_cf_alias}{$cfkey} )
1306 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1307 $self->{_sql_cf_alias}{$cfkey} );
1310 my ($TicketCFs, $CFs);
1312 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1316 TABLE2 => 'ObjectCustomFieldValues',
1317 FIELD2 => 'ObjectId',
1319 $self->SUPER::Limit(
1320 LEFTJOIN => $TicketCFs,
1321 FIELD => 'CustomField',
1323 ENTRYAGGREGATOR => 'AND'
1327 my $ocfalias = $self->Join(
1330 TABLE2 => 'ObjectCustomFields',
1331 FIELD2 => 'ObjectId',
1334 $self->SUPER::Limit(
1335 LEFTJOIN => $ocfalias,
1336 ENTRYAGGREGATOR => 'OR',
1337 FIELD => 'ObjectId',
1341 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1343 ALIAS1 => $ocfalias,
1344 FIELD1 => 'CustomField',
1345 TABLE2 => 'CustomFields',
1348 $self->SUPER::Limit(
1350 ENTRYAGGREGATOR => 'AND',
1351 FIELD => 'LookupType',
1352 VALUE => 'RT::Queue-RT::Ticket',
1354 $self->SUPER::Limit(
1356 ENTRYAGGREGATOR => 'AND',
1361 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1365 TABLE2 => 'ObjectCustomFieldValues',
1366 FIELD2 => 'CustomField',
1368 $self->SUPER::Limit(
1369 LEFTJOIN => $TicketCFs,
1370 FIELD => 'ObjectId',
1373 ENTRYAGGREGATOR => 'AND',
1376 $self->SUPER::Limit(
1377 LEFTJOIN => $TicketCFs,
1378 FIELD => 'ObjectType',
1379 VALUE => 'RT::Ticket',
1380 ENTRYAGGREGATOR => 'AND'
1382 $self->SUPER::Limit(
1383 LEFTJOIN => $TicketCFs,
1384 FIELD => 'Disabled',
1387 ENTRYAGGREGATOR => 'AND'
1390 return ($TicketCFs, $CFs);
1393 =head2 _CustomFieldLimit
1395 Limit based on CustomFields
1402 sub _CustomFieldLimit {
1403 my ( $self, $_field, $op, $value, %rest ) = @_;
1405 my $field = $rest{'SUBKEY'} || die "No field specified";
1407 # For our sanity, we can only limit on one queue at a time
1409 my ($queue, $cfid, $cf, $column);
1410 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1411 $cfid = $cf ? $cf->id : 0 ;
1413 # If we're trying to find custom fields that don't match something, we
1414 # want tickets where the custom field has no value at all. Note that
1415 # we explicitly don't include the "IS NULL" case, since we would
1416 # otherwise end up with a redundant clause.
1418 my ($negative_op, $null_op, $inv_op, $range_op)
1419 = $self->ClassifySQLOperation( $op );
1423 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1424 return 'MATCHES' if $op eq '=';
1425 return 'NOT MATCHES' if $op eq '!=';
1429 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1431 my $cfkey = $cfid ? $cfid : "$queue.$field";
1433 if ( $null_op && !$column ) {
1434 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1435 # we can reuse our default joins for this operation
1436 # with column specified we have different situation
1437 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1440 ALIAS => $TicketCFs,
1449 OPERATOR => 'IS NOT',
1452 ENTRYAGGREGATOR => 'AND',
1456 elsif ( !$negative_op || $single_value ) {
1457 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1458 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1465 # if column is defined then deal only with it
1466 # otherwise search in Content and in LargeContent
1469 ALIAS => $TicketCFs,
1471 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1476 elsif ( $cfid and $cf->Type eq 'Date' ) {
1477 $self->_DateFieldLimit(
1481 ALIAS => $TicketCFs,
1485 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1486 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1488 ALIAS => $TicketCFs,
1497 ALIAS => $TicketCFs,
1501 ENTRYAGGREGATOR => 'OR'
1504 ALIAS => $TicketCFs,
1508 ENTRYAGGREGATOR => 'OR'
1512 ALIAS => $TicketCFs,
1513 FIELD => 'LargeContent',
1514 OPERATOR => $fix_op->($op),
1516 ENTRYAGGREGATOR => 'AND',
1522 ALIAS => $TicketCFs,
1532 ALIAS => $TicketCFs,
1536 ENTRYAGGREGATOR => 'OR'
1539 ALIAS => $TicketCFs,
1543 ENTRYAGGREGATOR => 'OR'
1547 ALIAS => $TicketCFs,
1548 FIELD => 'LargeContent',
1549 OPERATOR => $fix_op->($op),
1551 ENTRYAGGREGATOR => 'AND',
1557 # XXX: if we join via CustomFields table then
1558 # because of order of left joins we get NULLs in
1559 # CF table and then get nulls for those records
1560 # in OCFVs table what result in wrong results
1561 # as decifer method now tries to load a CF then
1562 # we fall into this situation only when there
1563 # are more than one CF with the name in the DB.
1564 # the same thing applies to order by call.
1565 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1566 # we want treat IS NULL as (not applies or has
1571 OPERATOR => 'IS NOT',
1574 ENTRYAGGREGATOR => 'AND',
1580 ALIAS => $TicketCFs,
1581 FIELD => $column || 'Content',
1585 ENTRYAGGREGATOR => 'OR',
1592 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1593 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1596 $op =~ s/!|NOT\s+//i;
1598 # if column is defined then deal only with it
1599 # otherwise search in Content and in LargeContent
1601 $self->SUPER::Limit(
1602 LEFTJOIN => $TicketCFs,
1603 ALIAS => $TicketCFs,
1605 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1610 $self->SUPER::Limit(
1611 LEFTJOIN => $TicketCFs,
1612 ALIAS => $TicketCFs,
1620 ALIAS => $TicketCFs,
1629 sub _HasAttributeLimit {
1630 my ( $self, $field, $op, $value, %rest ) = @_;
1632 my $alias = $self->Join(
1636 TABLE2 => 'Attributes',
1637 FIELD2 => 'ObjectId',
1639 $self->SUPER::Limit(
1641 FIELD => 'ObjectType',
1642 VALUE => 'RT::Ticket',
1643 ENTRYAGGREGATOR => 'AND'
1645 $self->SUPER::Limit(
1650 ENTRYAGGREGATOR => 'AND'
1656 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1662 # End Helper Functions
1664 # End of SQL Stuff -------------------------------------------------
1666 # {{{ Allow sorting on watchers
1668 =head2 OrderByCols ARRAY
1670 A modified version of the OrderBy method which automatically joins where
1671 C<ALIAS> is set to the name of a watcher type.
1682 foreach my $row (@args) {
1683 if ( $row->{ALIAS} ) {
1687 if ( $row->{FIELD} !~ /\./ ) {
1688 my $meta = $self->FIELDS->{ $row->{FIELD} };
1694 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1695 my $alias = $self->Join(
1698 FIELD1 => $row->{'FIELD'},
1702 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1703 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1704 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1706 my $alias = $self->Join(
1709 FIELD1 => $row->{'FIELD'},
1713 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1720 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1721 my $meta = $self->FIELDS->{$field};
1722 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1723 # cache alias as we want to use one alias per watcher type for sorting
1724 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1726 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1727 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1729 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1730 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1731 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1732 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1733 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1734 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1735 # this is described in _CustomFieldLimit
1739 OPERATOR => 'IS NOT',
1742 ENTRYAGGREGATOR => 'AND',
1745 # For those cases where we are doing a join against the
1746 # CF name, and don't have a CFid, use Unique to make sure
1747 # we don't show duplicate tickets. NOTE: I'm pretty sure
1748 # this will stay mixed in for the life of the
1749 # class/package, and not just for the life of the object.
1750 # Potential performance issue.
1751 require DBIx::SearchBuilder::Unique;
1752 DBIx::SearchBuilder::Unique->import;
1754 my $CFvs = $self->Join(
1756 ALIAS1 => $TicketCFs,
1757 FIELD1 => 'CustomField',
1758 TABLE2 => 'CustomFieldValues',
1759 FIELD2 => 'CustomField',
1761 $self->SUPER::Limit(
1765 VALUE => $TicketCFs . ".Content",
1766 ENTRYAGGREGATOR => 'AND'
1769 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1770 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1771 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1772 # PAW logic is "reversed"
1774 if (exists $row->{ORDER} ) {
1775 my $o = $row->{ORDER};
1776 delete $row->{ORDER};
1777 $order = "DESC" if $o =~ /asc/i;
1780 # Ticket.Owner 1 0 X
1781 # Unowned Tickets 0 1 X
1784 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1785 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1786 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1791 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1798 FUNCTION => "Owner=$uid",
1804 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1806 } elsif ( $field eq 'Customer' ) { #Freeside
1807 if ( $subkey eq 'Number' ) {
1808 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1811 FIELD => $custnum_sql,
1815 my $custalias = $self->JoinToCustomer;
1817 if ( $subkey eq 'Name' ) {
1818 $field = "COALESCE( $custalias.company,
1819 $custalias.last || ', ' || $custalias.first
1822 elsif ( $subkey eq 'Class' ) {
1823 $field = "$custalias.classnum";
1825 elsif ( $subkey eq 'Agent' ) {
1826 $field = "$custalias.agentnum";
1828 elsif ( $subkey eq 'Referral' ) {
1829 $field = "$custalias.refnum";
1832 # no other cases exist yet, but for obviousness:
1835 push @res, { %$row, ALIAS => '', FIELD => $field };
1844 return $self->SUPER::OrderByCols(@res);
1849 sub JoinToCustLinks {
1850 # Set up join to links (id = localbase),
1851 # limit link type to 'MemberOf',
1852 # and target value to any Freeside custnum URI.
1853 # Return the linkalias for further join/limit action,
1854 # and an sql expression to retrieve the custnum.
1856 my $linkalias = $self->Join(
1861 FIELD2 => 'LocalBase',
1864 $self->SUPER::Limit(
1865 LEFTJOIN => $linkalias,
1868 VALUE => 'MemberOf',
1870 $self->SUPER::Limit(
1871 LEFTJOIN => $linkalias,
1873 OPERATOR => 'STARTSWITH',
1874 VALUE => 'freeside://freeside/cust_main/',
1876 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1877 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1878 $custnum_sql .= 'SIGNED INTEGER)';
1881 $custnum_sql .= 'INTEGER)';
1883 return ($linkalias, $custnum_sql);
1886 sub JoinToCustomer {
1888 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1890 my $custalias = $self->Join(
1892 EXPRESSION => $custnum_sql,
1893 TABLE2 => 'cust_main',
1894 FIELD2 => 'custnum',
1899 sub _FreesideFieldLimit {
1900 my ( $self, $field, $op, $value, %rest ) = @_;
1901 my $alias = $self->JoinToCustomer;
1902 my $is_negative = 0;
1903 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1904 # if the op is negative, do the join as though
1905 # the op were positive, then accept only records
1906 # where the right-side join key is null.
1908 $op = '=' if $op eq '!=';
1911 my $meta = $FIELD_METADATA{$field};
1913 $alias = $self->Join(
1916 FIELD1 => 'custnum',
1917 TABLE2 => $meta->[1],
1918 FIELD2 => 'custnum',
1922 $self->SUPER::Limit(
1924 FIELD => lc($field),
1927 ENTRYAGGREGATOR => 'AND',
1932 FIELD => lc($field),
1933 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1943 # {{{ Limit the result set based on content
1949 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1950 Generally best called from LimitFoo methods
1960 DESCRIPTION => undef,
1963 $args{'DESCRIPTION'} = $self->loc(
1964 "[_1] [_2] [_3]", $args{'FIELD'},
1965 $args{'OPERATOR'}, $args{'VALUE'}
1967 if ( !defined $args{'DESCRIPTION'} );
1969 my $index = $self->_NextIndex;
1971 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1973 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1975 $self->{'RecalcTicketLimits'} = 1;
1977 # If we're looking at the effective id, we don't want to append the other clause
1978 # which limits us to tickets where id = effective id
1979 if ( $args{'FIELD'} eq 'EffectiveId'
1980 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1982 $self->{'looking_at_effective_id'} = 1;
1985 if ( $args{'FIELD'} eq 'Type'
1986 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1988 $self->{'looking_at_type'} = 1;
1998 Returns a frozen string suitable for handing back to ThawLimits.
2002 sub _FreezeThawKeys {
2003 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2007 # {{{ sub FreezeLimits
2012 require MIME::Base64;
2013 MIME::Base64::base64_encode(
2014 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2021 Take a frozen Limits string generated by FreezeLimits and make this tickets
2022 object have that set of limits.
2026 # {{{ sub ThawLimits
2032 #if we don't have $in, get outta here.
2033 return undef unless ($in);
2035 $self->{'RecalcTicketLimits'} = 1;
2038 require MIME::Base64;
2040 #We don't need to die if the thaw fails.
2041 @{$self}{ $self->_FreezeThawKeys }
2042 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2044 $RT::Logger->error($@) if $@;
2050 # {{{ Limit by enum or foreign key
2052 # {{{ sub LimitQueue
2056 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2057 OPERATOR is one of = or !=. (It defaults to =).
2058 VALUE is a queue id or Name.
2071 #TODO VALUE should also take queue objects
2072 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2073 my $queue = new RT::Queue( $self->CurrentUser );
2074 $queue->Load( $args{'VALUE'} );
2075 $args{'VALUE'} = $queue->Id;
2078 # What if they pass in an Id? Check for isNum() and convert to
2081 #TODO check for a valid queue here
2085 VALUE => $args{'VALUE'},
2086 OPERATOR => $args{'OPERATOR'},
2087 DESCRIPTION => join(
2088 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2096 # {{{ sub LimitStatus
2100 Takes a paramhash with the fields OPERATOR and VALUE.
2101 OPERATOR is one of = or !=.
2104 RT adds Status != 'deleted' until object has
2105 allow_deleted_search internal property set.
2106 $tickets->{'allow_deleted_search'} = 1;
2107 $tickets->LimitStatus( VALUE => 'deleted' );
2119 VALUE => $args{'VALUE'},
2120 OPERATOR => $args{'OPERATOR'},
2121 DESCRIPTION => join( ' ',
2122 $self->loc('Status'), $args{'OPERATOR'},
2123 $self->loc( $args{'VALUE'} ) ),
2129 # {{{ sub IgnoreType
2133 If called, this search will not automatically limit the set of results found
2134 to tickets of type "Ticket". Tickets of other types, such as "project" and
2135 "approval" will be found.
2142 # Instead of faking a Limit that later gets ignored, fake up the
2143 # fact that we're already looking at type, so that the check in
2144 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2146 # $self->LimitType(VALUE => '__any');
2147 $self->{looking_at_type} = 1;
2156 Takes a paramhash with the fields OPERATOR and VALUE.
2157 OPERATOR is one of = or !=, it defaults to "=".
2158 VALUE is a string to search for in the type of the ticket.
2173 VALUE => $args{'VALUE'},
2174 OPERATOR => $args{'OPERATOR'},
2175 DESCRIPTION => join( ' ',
2176 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2184 # {{{ Limit by string field
2186 # {{{ sub LimitSubject
2190 Takes a paramhash with the fields OPERATOR and VALUE.
2191 OPERATOR is one of = or !=.
2192 VALUE is a string to search for in the subject of the ticket.
2201 VALUE => $args{'VALUE'},
2202 OPERATOR => $args{'OPERATOR'},
2203 DESCRIPTION => join( ' ',
2204 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2212 # {{{ Limit based on ticket numerical attributes
2213 # Things that can be > < = !=
2219 Takes a paramhash with the fields OPERATOR and VALUE.
2220 OPERATOR is one of =, >, < or !=.
2221 VALUE is a ticket Id to search for
2234 VALUE => $args{'VALUE'},
2235 OPERATOR => $args{'OPERATOR'},
2237 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2243 # {{{ sub LimitPriority
2245 =head2 LimitPriority
2247 Takes a paramhash with the fields OPERATOR and VALUE.
2248 OPERATOR is one of =, >, < or !=.
2249 VALUE is a value to match the ticket\'s priority against
2257 FIELD => 'Priority',
2258 VALUE => $args{'VALUE'},
2259 OPERATOR => $args{'OPERATOR'},
2260 DESCRIPTION => join( ' ',
2261 $self->loc('Priority'),
2262 $args{'OPERATOR'}, $args{'VALUE'}, ),
2268 # {{{ sub LimitInitialPriority
2270 =head2 LimitInitialPriority
2272 Takes a paramhash with the fields OPERATOR and VALUE.
2273 OPERATOR is one of =, >, < or !=.
2274 VALUE is a value to match the ticket\'s initial priority against
2279 sub LimitInitialPriority {
2283 FIELD => 'InitialPriority',
2284 VALUE => $args{'VALUE'},
2285 OPERATOR => $args{'OPERATOR'},
2286 DESCRIPTION => join( ' ',
2287 $self->loc('Initial Priority'), $args{'OPERATOR'},
2294 # {{{ sub LimitFinalPriority
2296 =head2 LimitFinalPriority
2298 Takes a paramhash with the fields OPERATOR and VALUE.
2299 OPERATOR is one of =, >, < or !=.
2300 VALUE is a value to match the ticket\'s final priority against
2304 sub LimitFinalPriority {
2308 FIELD => 'FinalPriority',
2309 VALUE => $args{'VALUE'},
2310 OPERATOR => $args{'OPERATOR'},
2311 DESCRIPTION => join( ' ',
2312 $self->loc('Final Priority'), $args{'OPERATOR'},
2319 # {{{ sub LimitTimeWorked
2321 =head2 LimitTimeWorked
2323 Takes a paramhash with the fields OPERATOR and VALUE.
2324 OPERATOR is one of =, >, < or !=.
2325 VALUE is a value to match the ticket's TimeWorked attribute
2329 sub LimitTimeWorked {
2333 FIELD => 'TimeWorked',
2334 VALUE => $args{'VALUE'},
2335 OPERATOR => $args{'OPERATOR'},
2336 DESCRIPTION => join( ' ',
2337 $self->loc('Time Worked'),
2338 $args{'OPERATOR'}, $args{'VALUE'}, ),
2344 # {{{ sub LimitTimeLeft
2346 =head2 LimitTimeLeft
2348 Takes a paramhash with the fields OPERATOR and VALUE.
2349 OPERATOR is one of =, >, < or !=.
2350 VALUE is a value to match the ticket's TimeLeft attribute
2358 FIELD => 'TimeLeft',
2359 VALUE => $args{'VALUE'},
2360 OPERATOR => $args{'OPERATOR'},
2361 DESCRIPTION => join( ' ',
2362 $self->loc('Time Left'),
2363 $args{'OPERATOR'}, $args{'VALUE'}, ),
2371 # {{{ Limiting based on attachment attributes
2373 # {{{ sub LimitContent
2377 Takes a paramhash with the fields OPERATOR and VALUE.
2378 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2379 VALUE is a string to search for in the body of the ticket
2388 VALUE => $args{'VALUE'},
2389 OPERATOR => $args{'OPERATOR'},
2390 DESCRIPTION => join( ' ',
2391 $self->loc('Ticket content'), $args{'OPERATOR'},
2398 # {{{ sub LimitFilename
2400 =head2 LimitFilename
2402 Takes a paramhash with the fields OPERATOR and VALUE.
2403 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2404 VALUE is a string to search for in the body of the ticket
2412 FIELD => 'Filename',
2413 VALUE => $args{'VALUE'},
2414 OPERATOR => $args{'OPERATOR'},
2415 DESCRIPTION => join( ' ',
2416 $self->loc('Attachment filename'), $args{'OPERATOR'},
2422 # {{{ sub LimitContentType
2424 =head2 LimitContentType
2426 Takes a paramhash with the fields OPERATOR and VALUE.
2427 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2428 VALUE is a content type to search ticket attachments for
2432 sub LimitContentType {
2436 FIELD => 'ContentType',
2437 VALUE => $args{'VALUE'},
2438 OPERATOR => $args{'OPERATOR'},
2439 DESCRIPTION => join( ' ',
2440 $self->loc('Ticket content type'), $args{'OPERATOR'},
2449 # {{{ Limiting based on people
2451 # {{{ sub LimitOwner
2455 Takes a paramhash with the fields OPERATOR and VALUE.
2456 OPERATOR is one of = or !=.
2468 my $owner = new RT::User( $self->CurrentUser );
2469 $owner->Load( $args{'VALUE'} );
2471 # FIXME: check for a valid $owner
2474 VALUE => $args{'VALUE'},
2475 OPERATOR => $args{'OPERATOR'},
2476 DESCRIPTION => join( ' ',
2477 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2484 # {{{ Limiting watchers
2486 # {{{ sub LimitWatcher
2490 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2491 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2492 VALUE is a value to match the ticket\'s watcher email addresses against
2493 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2507 #build us up a description
2508 my ( $watcher_type, $desc );
2509 if ( $args{'TYPE'} ) {
2510 $watcher_type = $args{'TYPE'};
2513 $watcher_type = "Watcher";
2517 FIELD => $watcher_type,
2518 VALUE => $args{'VALUE'},
2519 OPERATOR => $args{'OPERATOR'},
2520 TYPE => $args{'TYPE'},
2521 DESCRIPTION => join( ' ',
2522 $self->loc($watcher_type),
2523 $args{'OPERATOR'}, $args{'VALUE'}, ),
2533 # {{{ Limiting based on links
2537 =head2 LimitLinkedTo
2539 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2540 TYPE limits the sort of link we want to search on
2542 TYPE = { RefersTo, MemberOf, DependsOn }
2544 TARGET is the id or URI of the TARGET of the link
2558 FIELD => 'LinkedTo',
2560 TARGET => $args{'TARGET'},
2561 TYPE => $args{'TYPE'},
2562 DESCRIPTION => $self->loc(
2563 "Tickets [_1] by [_2]",
2564 $self->loc( $args{'TYPE'} ),
2567 OPERATOR => $args{'OPERATOR'},
2573 # {{{ LimitLinkedFrom
2575 =head2 LimitLinkedFrom
2577 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2578 TYPE limits the sort of link we want to search on
2581 BASE is the id or URI of the BASE of the link
2585 sub LimitLinkedFrom {
2594 # translate RT2 From/To naming to RT3 TicketSQL naming
2595 my %fromToMap = qw(DependsOn DependentOn
2597 RefersTo ReferredToBy);
2599 my $type = $args{'TYPE'};
2600 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2603 FIELD => 'LinkedTo',
2605 BASE => $args{'BASE'},
2607 DESCRIPTION => $self->loc(
2608 "Tickets [_1] [_2]",
2609 $self->loc( $args{'TYPE'} ),
2612 OPERATOR => $args{'OPERATOR'},
2621 my $ticket_id = shift;
2622 return $self->LimitLinkedTo(
2624 TARGET => $ticket_id,
2631 # {{{ LimitHasMember
2632 sub LimitHasMember {
2634 my $ticket_id = shift;
2635 return $self->LimitLinkedFrom(
2637 BASE => "$ticket_id",
2638 TYPE => 'HasMember',
2645 # {{{ LimitDependsOn
2647 sub LimitDependsOn {
2649 my $ticket_id = shift;
2650 return $self->LimitLinkedTo(
2652 TARGET => $ticket_id,
2653 TYPE => 'DependsOn',
2660 # {{{ LimitDependedOnBy
2662 sub LimitDependedOnBy {
2664 my $ticket_id = shift;
2665 return $self->LimitLinkedFrom(
2668 TYPE => 'DependentOn',
2679 my $ticket_id = shift;
2680 return $self->LimitLinkedTo(
2682 TARGET => $ticket_id,
2690 # {{{ LimitReferredToBy
2692 sub LimitReferredToBy {
2694 my $ticket_id = shift;
2695 return $self->LimitLinkedFrom(
2698 TYPE => 'ReferredToBy',
2706 # {{{ limit based on ticket date attribtes
2710 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2712 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2714 OPERATOR is one of > or <
2715 VALUE is a date and time in ISO format in GMT
2716 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2718 There are also helper functions of the form LimitFIELD that eliminate
2719 the need to pass in a FIELD argument.
2733 #Set the description if we didn't get handed it above
2734 unless ( $args{'DESCRIPTION'} ) {
2735 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2736 . $args{'OPERATOR'} . " "
2737 . $args{'VALUE'} . " GMT";
2740 $self->Limit(%args);
2748 $self->LimitDate( FIELD => 'Created', @_ );
2753 $self->LimitDate( FIELD => 'Due', @_ );
2759 $self->LimitDate( FIELD => 'Starts', @_ );
2765 $self->LimitDate( FIELD => 'Started', @_ );
2770 $self->LimitDate( FIELD => 'Resolved', @_ );
2775 $self->LimitDate( FIELD => 'Told', @_ );
2778 sub LimitLastUpdated {
2780 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2784 # {{{ sub LimitTransactionDate
2786 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2788 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2790 OPERATOR is one of > or <
2791 VALUE is a date and time in ISO format in GMT
2796 sub LimitTransactionDate {
2799 FIELD => 'TransactionDate',
2806 # <20021217042756.GK28744@pallas.fsck.com>
2807 # "Kill It" - Jesse.
2809 #Set the description if we didn't get handed it above
2810 unless ( $args{'DESCRIPTION'} ) {
2811 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2812 . $args{'OPERATOR'} . " "
2813 . $args{'VALUE'} . " GMT";
2816 $self->Limit(%args);
2824 # {{{ Limit based on custom fields
2825 # {{{ sub LimitCustomField
2827 =head2 LimitCustomField
2829 Takes a paramhash of key/value pairs with the following keys:
2833 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2835 =item OPERATOR - The usual Limit operators
2837 =item VALUE - The value to compare against
2843 sub LimitCustomField {
2847 CUSTOMFIELD => undef,
2849 DESCRIPTION => undef,
2850 FIELD => 'CustomFieldValue',
2855 my $CF = RT::CustomField->new( $self->CurrentUser );
2856 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2857 $CF->Load( $args{CUSTOMFIELD} );
2860 $CF->LoadByNameAndQueue(
2861 Name => $args{CUSTOMFIELD},
2862 Queue => $args{QUEUE}
2864 $args{CUSTOMFIELD} = $CF->Id;
2867 #If we are looking to compare with a null value.
2868 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2869 $args{'DESCRIPTION'}
2870 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2872 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2873 $args{'DESCRIPTION'}
2874 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2877 # if we're not looking to compare with a null value
2879 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2880 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2883 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2884 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2885 $QueueObj->Load( $args{'QUEUE'} );
2886 $args{'QUEUE'} = $QueueObj->Id;
2888 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2891 @rest = ( ENTRYAGGREGATOR => 'AND' )
2892 if ( $CF->Type eq 'SelectMultiple' );
2895 VALUE => $args{VALUE},
2897 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2898 .".{" . $CF->Name . "}",
2899 OPERATOR => $args{OPERATOR},
2904 $self->{'RecalcTicketLimits'} = 1;
2910 # {{{ sub _NextIndex
2914 Keep track of the counter for the array of restrictions
2920 return ( $self->{'restriction_index'}++ );
2927 # {{{ Core bits to make this a DBIx::SearchBuilder object
2932 $self->{'table'} = "Tickets";
2933 $self->{'RecalcTicketLimits'} = 1;
2934 $self->{'looking_at_effective_id'} = 0;
2935 $self->{'looking_at_type'} = 0;
2936 $self->{'restriction_index'} = 1;
2937 $self->{'primary_key'} = "id";
2938 delete $self->{'items_array'};
2939 delete $self->{'item_map'};
2940 delete $self->{'columns_to_display'};
2941 $self->SUPER::_Init(@_);
2952 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2953 return ( $self->SUPER::Count() );
2961 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2962 return ( $self->SUPER::CountAll() );
2967 # {{{ sub ItemsArrayRef
2969 =head2 ItemsArrayRef
2971 Returns a reference to the set of all items found in this search
2978 return $self->{'items_array'} if $self->{'items_array'};
2980 my $placeholder = $self->_ItemsCounter;
2981 $self->GotoFirstItem();
2982 while ( my $item = $self->Next ) {
2983 push( @{ $self->{'items_array'} }, $item );
2985 $self->GotoItem($placeholder);
2986 $self->{'items_array'}
2987 = $self->ItemsOrderBy( $self->{'items_array'} );
2989 return $self->{'items_array'};
2992 sub ItemsArrayRefWindow {
2996 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2998 $self->RowsPerPage( $window );
3000 $self->GotoFirstItem;
3003 while ( my $item = $self->Next ) {
3007 $self->RowsPerPage( $old[1] );
3008 $self->FirstRow( $old[2] );
3009 $self->GotoItem( $old[0] );
3020 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3022 my $Ticket = $self->SUPER::Next;
3023 return $Ticket unless $Ticket;
3025 if ( $Ticket->__Value('Status') eq 'deleted'
3026 && !$self->{'allow_deleted_search'} )
3030 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3031 # if we found a ticket with this option enabled then
3032 # all tickets we found are ACLed, cache this fact
3033 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3034 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3037 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3042 # If the user doesn't have the right to show this ticket
3049 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3050 return $self->SUPER::_DoSearch( @_ );
3055 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3056 return $self->SUPER::_DoCount( @_ );
3062 my $cache_key = 'RolesHasRight;:;ShowTicket';
3064 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3068 my $ACL = RT::ACL->new( $RT::SystemUser );
3069 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3070 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3071 my $principal_alias = $ACL->Join(
3073 FIELD1 => 'PrincipalId',
3074 TABLE2 => 'Principals',
3077 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3080 while ( my $ACE = $ACL->Next ) {
3081 my $role = $ACE->PrincipalType;
3082 my $type = $ACE->ObjectType;
3083 if ( $type eq 'RT::System' ) {
3086 elsif ( $type eq 'RT::Queue' ) {
3087 next if $res{ $role } && !ref $res{ $role };
3088 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3091 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3094 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3098 sub _DirectlyCanSeeIn {
3100 my $id = $self->CurrentUser->id;
3102 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3103 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3107 my $ACL = RT::ACL->new( $RT::SystemUser );
3108 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3109 my $principal_alias = $ACL->Join(
3111 FIELD1 => 'PrincipalId',
3112 TABLE2 => 'Principals',
3115 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3116 my $cgm_alias = $ACL->Join(
3118 FIELD1 => 'PrincipalId',
3119 TABLE2 => 'CachedGroupMembers',
3120 FIELD2 => 'GroupId',
3122 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3123 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3126 while ( my $ACE = $ACL->Next ) {
3127 my $type = $ACE->ObjectType;
3128 if ( $type eq 'RT::System' ) {
3129 # If user is direct member of a group that has the right
3130 # on the system then he can see any ticket
3131 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3134 elsif ( $type eq 'RT::Queue' ) {
3135 push @res, $ACE->ObjectId;
3138 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3141 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3145 sub CurrentUserCanSee {
3147 return if $self->{'_sql_current_user_can_see_applied'};
3149 return $self->{'_sql_current_user_can_see_applied'} = 1
3150 if $self->CurrentUser->UserObj->HasRight(
3151 Right => 'SuperUser', Object => $RT::System
3154 my $id = $self->CurrentUser->id;
3156 # directly can see in all queues then we have nothing to do
3157 my @direct_queues = $self->_DirectlyCanSeeIn;
3158 return $self->{'_sql_current_user_can_see_applied'} = 1
3159 if @direct_queues && $direct_queues[0] == -1;
3161 my %roles = $self->_RolesCanSee;
3163 my %skip = map { $_ => 1 } @direct_queues;
3164 foreach my $role ( keys %roles ) {
3165 next unless ref $roles{ $role };
3167 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3169 $roles{ $role } = \@queues;
3171 delete $roles{ $role };
3176 # there is no global watchers, only queues and tickes, if at
3177 # some point we will add global roles then it's gonna blow
3178 # the idea here is that if the right is set globaly for a role
3179 # and user plays this role for a queue directly not a ticket
3180 # then we have to check in advance
3181 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3183 my $groups = RT::Groups->new( $RT::SystemUser );
3184 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3186 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3188 my $principal_alias = $groups->Join(
3191 TABLE2 => 'Principals',
3194 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3195 my $cgm_alias = $groups->Join(
3198 TABLE2 => 'CachedGroupMembers',
3199 FIELD2 => 'GroupId',
3201 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3202 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3203 while ( my $group = $groups->Next ) {
3204 push @direct_queues, $group->Instance;
3208 unless ( @direct_queues || keys %roles ) {
3209 $self->SUPER::Limit(
3214 ENTRYAGGREGATOR => 'AND',
3216 return $self->{'_sql_current_user_can_see_applied'} = 1;
3220 my $join_roles = keys %roles;
3221 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3222 my ($role_group_alias, $cgm_alias);
3223 if ( $join_roles ) {
3224 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3225 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3226 $self->SUPER::Limit(
3227 LEFTJOIN => $cgm_alias,
3228 FIELD => 'MemberId',
3233 my $limit_queues = sub {
3237 return unless @queues;
3238 if ( @queues == 1 ) {
3239 $self->SUPER::Limit(
3244 ENTRYAGGREGATOR => $ea,
3247 $self->SUPER::_OpenParen('ACL');
3248 foreach my $q ( @queues ) {
3249 $self->SUPER::Limit(
3254 ENTRYAGGREGATOR => $ea,
3258 $self->SUPER::_CloseParen('ACL');
3263 $self->SUPER::_OpenParen('ACL');
3265 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3266 while ( my ($role, $queues) = each %roles ) {
3267 $self->SUPER::_OpenParen('ACL');
3268 if ( $role eq 'Owner' ) {
3269 $self->SUPER::Limit(
3273 ENTRYAGGREGATOR => $ea,
3277 $self->SUPER::Limit(
3279 ALIAS => $cgm_alias,
3280 FIELD => 'MemberId',
3281 OPERATOR => 'IS NOT',
3284 ENTRYAGGREGATOR => $ea,
3286 $self->SUPER::Limit(
3288 ALIAS => $role_group_alias,
3291 ENTRYAGGREGATOR => 'AND',
3294 $limit_queues->( 'AND', @$queues ) if ref $queues;
3295 $ea = 'OR' if $ea eq 'AND';
3296 $self->SUPER::_CloseParen('ACL');
3298 $self->SUPER::_CloseParen('ACL');
3300 return $self->{'_sql_current_user_can_see_applied'} = 1;
3307 # {{{ Deal with storing and restoring restrictions
3309 # {{{ sub LoadRestrictions
3311 =head2 LoadRestrictions
3313 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3314 TODO It is not yet implemented
3320 # {{{ sub DescribeRestrictions
3322 =head2 DescribeRestrictions
3325 Returns a hash keyed by restriction id.
3326 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3327 is a description of the purpose of that TicketRestriction
3331 sub DescribeRestrictions {
3336 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3337 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3344 # {{{ sub RestrictionValues
3346 =head2 RestrictionValues FIELD
3348 Takes a restriction field and returns a list of values this field is restricted
3353 sub RestrictionValues {
3356 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3357 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3358 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3360 keys %{ $self->{'TicketRestrictions'} };
3365 # {{{ sub ClearRestrictions
3367 =head2 ClearRestrictions
3369 Removes all restrictions irretrievably
3373 sub ClearRestrictions {
3375 delete $self->{'TicketRestrictions'};
3376 $self->{'looking_at_effective_id'} = 0;
3377 $self->{'looking_at_type'} = 0;
3378 $self->{'RecalcTicketLimits'} = 1;
3383 # {{{ sub DeleteRestriction
3385 =head2 DeleteRestriction
3387 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3388 Removes that restriction from the session's limits.
3392 sub DeleteRestriction {
3395 delete $self->{'TicketRestrictions'}{$row};
3397 $self->{'RecalcTicketLimits'} = 1;
3399 #make the underlying easysearch object forget all its preconceptions
3404 # {{{ sub _RestrictionsToClauses
3406 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3408 sub _RestrictionsToClauses {
3412 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3413 my $restriction = $self->{'TicketRestrictions'}{$row};
3415 # We need to reimplement the subclause aggregation that SearchBuilder does.
3416 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3417 # Then SB AND's the different Subclauses together.
3419 # So, we want to group things into Subclauses, convert them to
3420 # SQL, and then join them with the appropriate DefaultEA.
3421 # Then join each subclause group with AND.
3423 my $field = $restriction->{'FIELD'};
3424 my $realfield = $field; # CustomFields fake up a fieldname, so
3425 # we need to figure that out
3428 # Rewrite LinkedTo meta field to the real field
3429 if ( $field =~ /LinkedTo/ ) {
3430 $realfield = $field = $restriction->{'TYPE'};
3434 # Handle subkey fields with a different real field
3435 if ( $field =~ /^(\w+)\./ ) {
3439 die "I don't know about $field yet"
3440 unless ( exists $FIELD_METADATA{$realfield}
3441 or $restriction->{CUSTOMFIELD} );
3443 my $type = $FIELD_METADATA{$realfield}->[0];
3444 my $op = $restriction->{'OPERATOR'};
3448 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3451 # this performs the moral equivalent of defined or/dor/C<//>,
3452 # without the short circuiting.You need to use a 'defined or'
3453 # type thing instead of just checking for truth values, because
3454 # VALUE could be 0.(i.e. "false")
3456 # You could also use this, but I find it less aesthetic:
3457 # (although it does short circuit)
3458 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3459 # defined $restriction->{'TICKET'} ?
3460 # $restriction->{TICKET} :
3461 # defined $restriction->{'BASE'} ?
3462 # $restriction->{BASE} :
3463 # defined $restriction->{'TARGET'} ?
3464 # $restriction->{TARGET} )
3466 my $ea = $restriction->{ENTRYAGGREGATOR}
3467 || $DefaultEA{$type}
3470 die "Invalid operator $op for $field ($type)"
3471 unless exists $ea->{$op};
3475 # Each CustomField should be put into a different Clause so they
3476 # are ANDed together.
3477 if ( $restriction->{CUSTOMFIELD} ) {
3478 $realfield = $field;
3481 exists $clause{$realfield} or $clause{$realfield} = [];
3484 $field =~ s!(['"])!\\$1!g;
3485 $value =~ s!(['"])!\\$1!g;
3486 my $data = [ $ea, $type, $field, $op, $value ];
3488 # here is where we store extra data, say if it's a keyword or
3489 # something. (I.e. "TYPE SPECIFIC STUFF")
3491 push @{ $clause{$realfield} }, $data;
3498 # {{{ sub _ProcessRestrictions
3500 =head2 _ProcessRestrictions PARAMHASH
3502 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3503 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3507 sub _ProcessRestrictions {
3510 #Blow away ticket aliases since we'll need to regenerate them for
3512 delete $self->{'TicketAliases'};
3513 delete $self->{'items_array'};
3514 delete $self->{'item_map'};
3515 delete $self->{'raw_rows'};
3516 delete $self->{'rows'};
3517 delete $self->{'count_all'};
3519 my $sql = $self->Query; # Violating the _SQL namespace
3520 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3522 # "Restrictions to Clauses Branch\n";
3523 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3525 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3529 $sql = $self->ClausesToSQL($clauseRef);
3530 $self->FromSQL($sql) if $sql;
3534 $self->{'RecalcTicketLimits'} = 0;
3538 =head2 _BuildItemMap
3540 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3541 display search nav quickly.
3548 my $window = RT->Config->Get('TicketsItemMapSize');
3550 $self->{'item_map'} = {};
3552 my $items = $self->ItemsArrayRefWindow( $window );
3553 return unless $items && @$items;
3556 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3557 for ( my $i = 0; $i < @$items; $i++ ) {
3558 my $item = $items->[$i];
3559 my $id = $item->EffectiveId;
3560 $self->{'item_map'}{$id}{'defined'} = 1;
3561 $self->{'item_map'}{$id}{'prev'} = $prev;
3562 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3566 $self->{'item_map'}{'last'} = $prev
3567 if !$window || @$items < $window;
3572 Returns an a map of all items found by this search. The map is a hash
3576 first => <first ticket id found>,
3577 last => <last ticket id found or undef>,
3580 prev => <the ticket id found before>,
3581 next => <the ticket id found after>,
3593 $self->_BuildItemMap unless $self->{'item_map'};
3594 return $self->{'item_map'};
3602 =head2 PrepForSerialization
3604 You don't want to serialize a big tickets object, as
3605 the {items} hash will be instantly invalid _and_ eat
3610 sub PrepForSerialization {
3612 delete $self->{'items'};
3613 delete $self->{'items_array'};
3614 $self->RedoSearch();
3619 RT::Tickets supports several flags which alter search behavior:
3622 allow_deleted_search (Otherwise never show deleted tickets in search results)
3623 looking_at_type (otherwise limit to type=ticket)
3625 These flags are set by calling
3627 $tickets->{'flagname'} = 1;
3629 BUG: There should be an API for this