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 ],
148 Agentnum => [ 'FREESIDEFIELD', ],
149 Classnum => [ 'FREESIDEFIELD', ],
150 Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ],
153 our %SEARCHABLE_SUBFIELDS = (
155 EmailAddress Name RealName Nickname Organization Address1 Address2
156 WorkPhone HomePhone MobilePhone PagerPhone id
160 # Mapping of Field Type to Function
162 ENUM => \&_EnumLimit,
165 LINK => \&_LinkLimit,
166 DATE => \&_DateLimit,
167 STRING => \&_StringLimit,
168 TRANSFIELD => \&_TransLimit,
169 TRANSDATE => \&_TransDateLimit,
170 WATCHERFIELD => \&_WatcherLimit,
171 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
172 CUSTOMFIELD => \&_CustomFieldLimit,
173 HASATTRIBUTE => \&_HasAttributeLimit,
174 FREESIDEFIELD => \&_FreesideFieldLimit,
176 our %can_bundle = ();# WATCHERFIELD => "yes", );
178 # Default EntryAggregator per type
179 # if you specify OP, you must specify all valid OPs
220 # Helper functions for passing the above lexically scoped tables above
221 # into Tickets_Overlay_SQL.
222 sub FIELDS { return \%FIELD_METADATA }
223 sub dispatch { return \%dispatch }
224 sub can_bundle { return \%can_bundle }
226 # Bring in the clowns.
227 require RT::Tickets_Overlay_SQL;
231 our @SORTFIELDS = qw(id Status
233 Owner Created Due Starts Started
235 Resolved LastUpdated Priority TimeWorked TimeLeft);
239 Returns the list of fields that lists of tickets can easily be sorted by
245 return (@SORTFIELDS);
250 # BEGIN SQL STUFF *********************************
255 $self->SUPER::CleanSlate( @_ );
256 delete $self->{$_} foreach qw(
258 _sql_group_members_aliases
259 _sql_object_cfv_alias
260 _sql_role_group_aliases
263 _sql_u_watchers_alias_for_sort
264 _sql_u_watchers_aliases
265 _sql_current_user_can_see_applied
269 =head1 Limit Helper Routines
271 These routines are the targets of a dispatch table depending on the
272 type of field. They all share the same signature:
274 my ($self,$field,$op,$value,@rest) = @_;
276 The values in @rest should be suitable for passing directly to
277 DBIx::SearchBuilder::Limit.
279 Essentially they are an expanded/broken out (and much simplified)
280 version of what ProcessRestrictions used to do. They're also much
281 more clearly delineated by the TYPE of field being processed.
290 my ( $sb, $field, $op, $value, @rest ) = @_;
292 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
294 die "Invalid operator $op for __Bookmarked__ search on $field"
295 unless $op =~ /^(=|!=)$/;
298 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
299 $tmp = $tmp->Content if $tmp;
304 return $sb->_SQLLimit(
311 # as bookmarked tickets can be merged we have to use a join
312 # but it should be pretty lightweight
313 my $tickets_alias = $sb->Join(
318 FIELD2 => 'EffectiveId',
322 my $ea = $op eq '='? 'OR': 'AND';
323 foreach my $id ( sort @bookmarks ) {
325 ALIAS => $tickets_alias,
329 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
337 Handle Fields which are limited to certain values, and potentially
338 need to be looked up from another class.
340 This subroutine actually handles two different kinds of fields. For
341 some the user is responsible for limiting the values. (i.e. Status,
344 For others, the value specified by the user will be looked by via
348 name of class to lookup in (Optional)
353 my ( $sb, $field, $op, $value, @rest ) = @_;
355 # SQL::Statement changes != to <>. (Can we remove this now?)
356 $op = "!=" if $op eq "<>";
358 die "Invalid Operation: $op for $field"
362 my $meta = $FIELD_METADATA{$field};
363 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
364 my $class = "RT::" . $meta->[1];
365 my $o = $class->new( $sb->CurrentUser );
379 Handle fields where the values are limited to integers. (For example,
380 Priority, TimeWorked.)
388 my ( $sb, $field, $op, $value, @rest ) = @_;
390 die "Invalid Operator $op for $field"
391 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
403 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
406 1: Direction (From, To)
407 2: Link Type (MemberOf, DependsOn, RefersTo)
412 my ( $sb, $field, $op, $value, @rest ) = @_;
414 my $meta = $FIELD_METADATA{$field};
415 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
418 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
422 $is_null = 1 if !$value || $value =~ /^null$/io;
424 my $direction = $meta->[1] || '';
425 my ($matchfield, $linkfield) = ('', '');
426 if ( $direction eq 'To' ) {
427 ($matchfield, $linkfield) = ("Target", "Base");
429 elsif ( $direction eq 'From' ) {
430 ($matchfield, $linkfield) = ("Base", "Target");
432 elsif ( $direction ) {
433 die "Invalid link direction '$direction' for $field\n";
436 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
438 'LinkedFrom', $op, $value, @rest,
439 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
447 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
449 elsif ( $value =~ /\D/ ) {
452 $matchfield = "Local$matchfield" if $is_local;
454 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
455 # SELECT main.* FROM Tickets main
456 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
457 # AND(main.id = Links_1.LocalTarget))
458 # WHERE Links_1.LocalBase IS NULL;
461 my $linkalias = $sb->Join(
466 FIELD2 => 'Local' . $linkfield
469 LEFTJOIN => $linkalias,
477 FIELD => $matchfield,
484 my $linkalias = $sb->Join(
489 FIELD2 => 'Local' . $linkfield
492 LEFTJOIN => $linkalias,
498 LEFTJOIN => $linkalias,
499 FIELD => $matchfield,
506 FIELD => $matchfield,
507 OPERATOR => $is_negative? 'IS': 'IS NOT',
516 Handle date fields. (Created, LastTold..)
519 1: type of link. (Probably not necessary.)
524 my ( $sb, $field, $op, $value, @rest ) = @_;
526 die "Invalid Date Op: $op"
527 unless $op =~ /^(=|>|<|>=|<=)$/;
529 my $meta = $FIELD_METADATA{$field};
530 die "Incorrect Meta Data for $field"
531 unless ( defined $meta->[1] );
533 $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest );
536 # Factor this out for use by custom fields
538 sub _DateFieldLimit {
539 my ( $sb, $field, $op, $value, @rest ) = @_;
541 my $date = RT::Date->new( $sb->CurrentUser );
542 $date->Set( Format => 'unknown', Value => $value );
546 # if we're specifying =, that means we want everything on a
547 # particular single day. in the database, we need to check for >
548 # and < the edges of that day.
550 # Except if the value is 'this month' or 'last month', check
551 # > and < the edges of the month.
553 my ($daystart, $dayend);
554 if ( lc($value) eq 'this month' ) {
556 $date->SetToStart('month', Timezone => 'server');
557 $daystart = $date->ISO;
558 $date->AddMonth(Timezone => 'server');
559 $dayend = $date->ISO;
561 elsif ( lc($value) eq 'last month' ) {
563 $date->SetToStart('month', Timezone => 'server');
564 $dayend = $date->ISO;
566 $date->SetToStart('month', Timezone => 'server');
567 $daystart = $date->ISO;
570 $date->SetToMidnight( Timezone => 'server' );
571 $daystart = $date->ISO;
573 $dayend = $date->ISO;
590 ENTRYAGGREGATOR => 'AND',
608 Handle simple fields which are just strings. (Subject,Type)
616 my ( $sb, $field, $op, $value, @rest ) = @_;
620 # =, !=, LIKE, NOT LIKE
621 if ( (!defined $value || !length $value)
622 && lc($op) ne 'is' && lc($op) ne 'is not'
623 && RT->Config->Get('DatabaseType') eq 'Oracle'
625 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
626 $op = $negative? 'IS NOT': 'IS';
639 =head2 _TransDateLimit
641 Handle fields limiting based on Transaction Date.
643 The inpupt value must be in a format parseable by Time::ParseDate
650 # This routine should really be factored into translimit.
651 sub _TransDateLimit {
652 my ( $sb, $field, $op, $value, @rest ) = @_;
654 # See the comments for TransLimit, they apply here too
656 unless ( $sb->{_sql_transalias} ) {
657 $sb->{_sql_transalias} = $sb->Join(
660 TABLE2 => 'Transactions',
661 FIELD2 => 'ObjectId',
664 ALIAS => $sb->{_sql_transalias},
665 FIELD => 'ObjectType',
666 VALUE => 'RT::Ticket',
667 ENTRYAGGREGATOR => 'AND',
671 my $date = RT::Date->new( $sb->CurrentUser );
672 $date->Set( Format => 'unknown', Value => $value );
677 # if we're specifying =, that means we want everything on a
678 # particular single day. in the database, we need to check for >
679 # and < the edges of that day.
681 $date->SetToMidnight( Timezone => 'server' );
682 my $daystart = $date->ISO;
684 my $dayend = $date->ISO;
687 ALIAS => $sb->{_sql_transalias},
695 ALIAS => $sb->{_sql_transalias},
701 ENTRYAGGREGATOR => 'AND',
706 # not searching for a single day
709 #Search for the right field
711 ALIAS => $sb->{_sql_transalias},
725 Limit based on the Content of a transaction or the ContentType.
734 # Content, ContentType, Filename
736 # If only this was this simple. We've got to do something
739 #Basically, we want to make sure that the limits apply to
740 #the same attachment, rather than just another attachment
741 #for the same ticket, no matter how many clauses we lump
742 #on. We put them in TicketAliases so that they get nuked
743 #when we redo the join.
745 # In the SQL, we might have
746 # (( Content = foo ) or ( Content = bar AND Content = baz ))
747 # The AND group should share the same Alias.
749 # Actually, maybe it doesn't matter. We use the same alias and it
750 # works itself out? (er.. different.)
752 # Steal more from _ProcessRestrictions
754 # FIXME: Maybe look at the previous FooLimit call, and if it was a
755 # TransLimit and EntryAggregator == AND, reuse the Aliases?
757 # Or better - store the aliases on a per subclause basis - since
758 # those are going to be the things we want to relate to each other,
761 # maybe we should not allow certain kinds of aggregation of these
762 # clauses and do a psuedo regex instead? - the problem is getting
763 # them all into the same subclause when you have (A op B op C) - the
764 # way they get parsed in the tree they're in different subclauses.
766 my ( $self, $field, $op, $value, %rest ) = @_;
768 unless ( $self->{_sql_transalias} ) {
769 $self->{_sql_transalias} = $self->Join(
772 TABLE2 => 'Transactions',
773 FIELD2 => 'ObjectId',
776 ALIAS => $self->{_sql_transalias},
777 FIELD => 'ObjectType',
778 VALUE => 'RT::Ticket',
779 ENTRYAGGREGATOR => 'AND',
782 unless ( defined $self->{_sql_trattachalias} ) {
783 $self->{_sql_trattachalias} = $self->_SQLJoin(
784 TYPE => 'LEFT', # not all txns have an attachment
785 ALIAS1 => $self->{_sql_transalias},
787 TABLE2 => 'Attachments',
788 FIELD2 => 'TransactionId',
792 #Search for the right field
793 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
797 ALIAS => $self->{_sql_trattachalias},
804 ENTRYAGGREGATOR => 'AND',
805 ALIAS => $self->{_sql_trattachalias},
814 ALIAS => $self->{_sql_trattachalias},
827 Handle watcher limits. (Requestor, CC, etc..)
843 my $meta = $FIELD_METADATA{ $field };
844 my $type = $meta->[1] || '';
845 my $class = $meta->[2] || 'Ticket';
847 # Bail if the subfield is not allowed
849 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
851 die "Invalid watcher subfield: '$rest{SUBKEY}'";
854 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
855 # search by id and Name at the same time, this is workaround
856 # to preserve backward compatibility
857 if ( $field eq 'Owner' ) {
858 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
859 my $o = RT::User->new( $self->CurrentUser );
860 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
861 $o->$method( $value );
870 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
880 $rest{SUBKEY} ||= 'EmailAddress';
882 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
885 if ( $op =~ /^IS(?: NOT)?$/ ) {
886 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
887 # to avoid joining the table Users into the query, we just join GM
888 # and make sure we don't match records where group is member of itself
890 LEFTJOIN => $group_members,
893 VALUE => "$group_members.MemberId",
897 ALIAS => $group_members,
904 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
906 $op =~ s/!|NOT\s+//i;
908 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
909 # "X = 'Y'" matches more then one user so we try to fetch two records and
910 # do the right thing when there is only one exist and semi-working solution
912 my $users_obj = RT::Users->new( $self->CurrentUser );
914 FIELD => $rest{SUBKEY},
919 $users_obj->RowsPerPage(2);
920 my @users = @{ $users_obj->ItemsArrayRef };
922 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
925 $uid = $users[0]->id if @users;
927 LEFTJOIN => $group_members,
928 ALIAS => $group_members,
934 ALIAS => $group_members,
941 LEFTJOIN => $group_members,
944 VALUE => "$group_members.MemberId",
947 my $users = $self->Join(
949 ALIAS1 => $group_members,
950 FIELD1 => 'MemberId',
957 FIELD => $rest{SUBKEY},
971 my $group_members = $self->_GroupMembersJoin(
972 GroupsAlias => $groups,
976 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
978 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
979 $self->NewAlias('Users');
981 LEFTJOIN => $group_members,
982 ALIAS => $group_members,
984 VALUE => "$users.id",
989 # we join users table without adding some join condition between tables,
990 # the only conditions we have are conditions on the table iteslf,
991 # for example Users.EmailAddress = 'x'. We should add this condition to
992 # the top level of the query and bundle it with another similar conditions,
993 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
994 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
997 SUBCLAUSE => '_sql_u_watchers_'. $users,
999 FIELD => $rest{'SUBKEY'},
1004 # A condition which ties Users and Groups (role groups) is a left join condition
1005 # of CachedGroupMembers table. To get correct results of the query we check
1006 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
1009 ALIAS => $group_members,
1011 OPERATOR => 'IS NOT',
1018 sub _RoleGroupsJoin {
1020 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1021 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1022 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1025 # we always have watcher groups for ticket, so we use INNER join
1026 my $groups = $self->Join(
1028 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1030 FIELD2 => 'Instance',
1031 ENTRYAGGREGATOR => 'AND',
1033 $self->SUPER::Limit(
1034 LEFTJOIN => $groups,
1037 VALUE => 'RT::'. $args{'Class'} .'-Role',
1039 $self->SUPER::Limit(
1040 LEFTJOIN => $groups,
1043 VALUE => $args{'Type'},
1046 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1047 unless $args{'New'};
1052 sub _GroupMembersJoin {
1054 my %args = (New => 1, GroupsAlias => undef, @_);
1056 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1057 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1060 my $alias = $self->Join(
1062 ALIAS1 => $args{'GroupsAlias'},
1064 TABLE2 => 'CachedGroupMembers',
1065 FIELD2 => 'GroupId',
1066 ENTRYAGGREGATOR => 'AND',
1069 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1070 unless $args{'New'};
1077 Helper function which provides joins to a watchers table both for limits
1084 my $type = shift || '';
1087 my $groups = $self->_RoleGroupsJoin( Type => $type );
1088 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1089 # XXX: work around, we must hide groups that
1090 # are members of the role group we search in,
1091 # otherwise them result in wrong NULLs in Users
1092 # table and break ordering. Now, we know that
1093 # RT doesn't allow to add groups as members of the
1094 # ticket roles, so we just hide entries in CGM table
1095 # with MemberId == GroupId from results
1096 $self->SUPER::Limit(
1097 LEFTJOIN => $group_members,
1100 VALUE => "$group_members.MemberId",
1103 my $users = $self->Join(
1105 ALIAS1 => $group_members,
1106 FIELD1 => 'MemberId',
1110 return ($groups, $group_members, $users);
1113 =head2 _WatcherMembershipLimit
1115 Handle watcher membership limits, i.e. whether the watcher belongs to a
1116 specific group or not.
1119 1: Field to query on
1121 SELECT DISTINCT main.*
1125 CachedGroupMembers CachedGroupMembers_2,
1128 (main.EffectiveId = main.id)
1130 (main.Status != 'deleted')
1132 (main.Type = 'ticket')
1135 (Users_3.EmailAddress = '22')
1137 (Groups_1.Domain = 'RT::Ticket-Role')
1139 (Groups_1.Type = 'RequestorGroup')
1142 Groups_1.Instance = main.id
1144 Groups_1.id = CachedGroupMembers_2.GroupId
1146 CachedGroupMembers_2.MemberId = Users_3.id
1147 ORDER BY main.id ASC
1152 sub _WatcherMembershipLimit {
1153 my ( $self, $field, $op, $value, @rest ) = @_;
1158 my $groups = $self->NewAlias('Groups');
1159 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1160 my $users = $self->NewAlias('Users');
1161 my $memberships = $self->NewAlias('CachedGroupMembers');
1163 if ( ref $field ) { # gross hack
1164 my @bundle = @$field;
1166 for my $chunk (@bundle) {
1167 ( $field, $op, $value, @rest ) = @$chunk;
1169 ALIAS => $memberships,
1180 ALIAS => $memberships,
1188 # {{{ Tie to groups for tickets we care about
1192 VALUE => 'RT::Ticket-Role',
1193 ENTRYAGGREGATOR => 'AND'
1198 FIELD1 => 'Instance',
1205 # If we care about which sort of watcher
1206 my $meta = $FIELD_METADATA{$field};
1207 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1214 ENTRYAGGREGATOR => 'AND'
1221 ALIAS2 => $groupmembers,
1226 ALIAS1 => $groupmembers,
1227 FIELD1 => 'MemberId',
1233 ALIAS1 => $memberships,
1234 FIELD1 => 'MemberId',
1243 =head2 _CustomFieldDecipher
1245 Try and turn a CF descriptor into (cfid, cfname) object pair.
1249 sub _CustomFieldDecipher {
1250 my ($self, $string) = @_;
1252 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1253 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1257 my $q = RT::Queue->new( $self->CurrentUser );
1261 # $queue = $q->Name; # should we normalize the queue?
1262 $cf = $q->CustomField( $field );
1265 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1269 elsif ( $field =~ /\D/ ) {
1271 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1272 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1273 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1275 # if there is more then one field the current user can
1276 # see with the same name then we shouldn't return cf object
1277 # as we don't know which one to use
1280 $cf = undef if $cfs->Next;
1284 $cf = RT::CustomField->new( $self->CurrentUser );
1285 $cf->Load( $field );
1288 return ($queue, $field, $cf, $column);
1291 =head2 _CustomFieldJoin
1293 Factor out the Join of custom fields so we can use it for sorting too
1297 sub _CustomFieldJoin {
1298 my ($self, $cfkey, $cfid, $field) = @_;
1299 # Perform one Join per CustomField
1300 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1301 $self->{_sql_cf_alias}{$cfkey} )
1303 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1304 $self->{_sql_cf_alias}{$cfkey} );
1307 my ($TicketCFs, $CFs);
1309 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1313 TABLE2 => 'ObjectCustomFieldValues',
1314 FIELD2 => 'ObjectId',
1316 $self->SUPER::Limit(
1317 LEFTJOIN => $TicketCFs,
1318 FIELD => 'CustomField',
1320 ENTRYAGGREGATOR => 'AND'
1324 my $ocfalias = $self->Join(
1327 TABLE2 => 'ObjectCustomFields',
1328 FIELD2 => 'ObjectId',
1331 $self->SUPER::Limit(
1332 LEFTJOIN => $ocfalias,
1333 ENTRYAGGREGATOR => 'OR',
1334 FIELD => 'ObjectId',
1338 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1340 ALIAS1 => $ocfalias,
1341 FIELD1 => 'CustomField',
1342 TABLE2 => 'CustomFields',
1345 $self->SUPER::Limit(
1347 ENTRYAGGREGATOR => 'AND',
1348 FIELD => 'LookupType',
1349 VALUE => 'RT::Queue-RT::Ticket',
1351 $self->SUPER::Limit(
1353 ENTRYAGGREGATOR => 'AND',
1358 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1362 TABLE2 => 'ObjectCustomFieldValues',
1363 FIELD2 => 'CustomField',
1365 $self->SUPER::Limit(
1366 LEFTJOIN => $TicketCFs,
1367 FIELD => 'ObjectId',
1370 ENTRYAGGREGATOR => 'AND',
1373 $self->SUPER::Limit(
1374 LEFTJOIN => $TicketCFs,
1375 FIELD => 'ObjectType',
1376 VALUE => 'RT::Ticket',
1377 ENTRYAGGREGATOR => 'AND'
1379 $self->SUPER::Limit(
1380 LEFTJOIN => $TicketCFs,
1381 FIELD => 'Disabled',
1384 ENTRYAGGREGATOR => 'AND'
1387 return ($TicketCFs, $CFs);
1390 =head2 _CustomFieldLimit
1392 Limit based on CustomFields
1399 sub _CustomFieldLimit {
1400 my ( $self, $_field, $op, $value, %rest ) = @_;
1402 my $field = $rest{'SUBKEY'} || die "No field specified";
1404 # For our sanity, we can only limit on one queue at a time
1406 my ($queue, $cfid, $cf, $column);
1407 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1408 $cfid = $cf ? $cf->id : 0 ;
1410 # If we're trying to find custom fields that don't match something, we
1411 # want tickets where the custom field has no value at all. Note that
1412 # we explicitly don't include the "IS NULL" case, since we would
1413 # otherwise end up with a redundant clause.
1415 my ($negative_op, $null_op, $inv_op, $range_op)
1416 = $self->ClassifySQLOperation( $op );
1420 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1421 return 'MATCHES' if $op eq '=';
1422 return 'NOT MATCHES' if $op eq '!=';
1426 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1428 my $cfkey = $cfid ? $cfid : "$queue.$field";
1430 if ( $null_op && !$column ) {
1431 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1432 # we can reuse our default joins for this operation
1433 # with column specified we have different situation
1434 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1437 ALIAS => $TicketCFs,
1446 OPERATOR => 'IS NOT',
1449 ENTRYAGGREGATOR => 'AND',
1453 elsif ( !$negative_op || $single_value ) {
1454 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1455 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1462 # if column is defined then deal only with it
1463 # otherwise search in Content and in LargeContent
1466 ALIAS => $TicketCFs,
1468 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1473 elsif ( $cfid and $cf->Type eq 'Date' ) {
1474 $self->_DateFieldLimit(
1478 ALIAS => $TicketCFs,
1482 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1483 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1485 ALIAS => $TicketCFs,
1494 ALIAS => $TicketCFs,
1498 ENTRYAGGREGATOR => 'OR'
1501 ALIAS => $TicketCFs,
1505 ENTRYAGGREGATOR => 'OR'
1509 ALIAS => $TicketCFs,
1510 FIELD => 'LargeContent',
1511 OPERATOR => $fix_op->($op),
1513 ENTRYAGGREGATOR => 'AND',
1519 ALIAS => $TicketCFs,
1529 ALIAS => $TicketCFs,
1533 ENTRYAGGREGATOR => 'OR'
1536 ALIAS => $TicketCFs,
1540 ENTRYAGGREGATOR => 'OR'
1544 ALIAS => $TicketCFs,
1545 FIELD => 'LargeContent',
1546 OPERATOR => $fix_op->($op),
1548 ENTRYAGGREGATOR => 'AND',
1554 # XXX: if we join via CustomFields table then
1555 # because of order of left joins we get NULLs in
1556 # CF table and then get nulls for those records
1557 # in OCFVs table what result in wrong results
1558 # as decifer method now tries to load a CF then
1559 # we fall into this situation only when there
1560 # are more than one CF with the name in the DB.
1561 # the same thing applies to order by call.
1562 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1563 # we want treat IS NULL as (not applies or has
1568 OPERATOR => 'IS NOT',
1571 ENTRYAGGREGATOR => 'AND',
1577 ALIAS => $TicketCFs,
1578 FIELD => $column || 'Content',
1582 ENTRYAGGREGATOR => 'OR',
1589 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1590 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1593 $op =~ s/!|NOT\s+//i;
1595 # if column is defined then deal only with it
1596 # otherwise search in Content and in LargeContent
1598 $self->SUPER::Limit(
1599 LEFTJOIN => $TicketCFs,
1600 ALIAS => $TicketCFs,
1602 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1607 $self->SUPER::Limit(
1608 LEFTJOIN => $TicketCFs,
1609 ALIAS => $TicketCFs,
1617 ALIAS => $TicketCFs,
1626 sub _HasAttributeLimit {
1627 my ( $self, $field, $op, $value, %rest ) = @_;
1629 my $alias = $self->Join(
1633 TABLE2 => 'Attributes',
1634 FIELD2 => 'ObjectId',
1636 $self->SUPER::Limit(
1638 FIELD => 'ObjectType',
1639 VALUE => 'RT::Ticket',
1640 ENTRYAGGREGATOR => 'AND'
1642 $self->SUPER::Limit(
1647 ENTRYAGGREGATOR => 'AND'
1653 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1659 # End Helper Functions
1661 # End of SQL Stuff -------------------------------------------------
1663 # {{{ Allow sorting on watchers
1665 =head2 OrderByCols ARRAY
1667 A modified version of the OrderBy method which automatically joins where
1668 C<ALIAS> is set to the name of a watcher type.
1679 foreach my $row (@args) {
1680 if ( $row->{ALIAS} ) {
1684 if ( $row->{FIELD} !~ /\./ ) {
1685 my $meta = $self->FIELDS->{ $row->{FIELD} };
1691 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1692 my $alias = $self->Join(
1695 FIELD1 => $row->{'FIELD'},
1699 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1700 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1701 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1703 my $alias = $self->Join(
1706 FIELD1 => $row->{'FIELD'},
1710 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1717 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1718 my $meta = $self->FIELDS->{$field};
1719 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1720 # cache alias as we want to use one alias per watcher type for sorting
1721 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1723 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1724 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1726 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1727 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1728 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1729 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1730 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1731 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1732 # this is described in _CustomFieldLimit
1736 OPERATOR => 'IS NOT',
1739 ENTRYAGGREGATOR => 'AND',
1742 # For those cases where we are doing a join against the
1743 # CF name, and don't have a CFid, use Unique to make sure
1744 # we don't show duplicate tickets. NOTE: I'm pretty sure
1745 # this will stay mixed in for the life of the
1746 # class/package, and not just for the life of the object.
1747 # Potential performance issue.
1748 require DBIx::SearchBuilder::Unique;
1749 DBIx::SearchBuilder::Unique->import;
1751 my $CFvs = $self->Join(
1753 ALIAS1 => $TicketCFs,
1754 FIELD1 => 'CustomField',
1755 TABLE2 => 'CustomFieldValues',
1756 FIELD2 => 'CustomField',
1758 $self->SUPER::Limit(
1762 VALUE => $TicketCFs . ".Content",
1763 ENTRYAGGREGATOR => 'AND'
1766 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1767 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1768 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1769 # PAW logic is "reversed"
1771 if (exists $row->{ORDER} ) {
1772 my $o = $row->{ORDER};
1773 delete $row->{ORDER};
1774 $order = "DESC" if $o =~ /asc/i;
1777 # Ticket.Owner 1 0 X
1778 # Unowned Tickets 0 1 X
1781 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1782 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1783 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1788 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1795 FUNCTION => "Owner=$uid",
1801 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1803 } elsif ( $field eq 'Customer' ) { #Freeside
1804 if ( $subkey eq 'Number' ) {
1805 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1808 FIELD => $custnum_sql,
1812 my $custalias = $self->JoinToCustomer;
1814 if ( $subkey eq 'Name' ) {
1815 $field = "COALESCE( $custalias.company,
1816 $custalias.last || ', ' || $custalias.first
1819 elsif ( $subkey eq 'Class' ) {
1820 $field = "$custalias.classnum";
1822 elsif ( $subkey eq 'Agent' ) {
1823 $field = "$custalias.agentnum";
1826 # no other cases exist yet, but for obviousness:
1829 push @res, { %$row, ALIAS => '', FIELD => $field };
1838 return $self->SUPER::OrderByCols(@res);
1843 sub JoinToCustLinks {
1844 # Set up join to links (id = localbase),
1845 # limit link type to 'MemberOf',
1846 # and target value to any Freeside custnum URI.
1847 # Return the linkalias for further join/limit action,
1848 # and an sql expression to retrieve the custnum.
1850 my $linkalias = $self->Join(
1855 FIELD2 => 'LocalBase',
1858 $self->SUPER::Limit(
1859 LEFTJOIN => $linkalias,
1862 VALUE => 'MemberOf',
1864 $self->SUPER::Limit(
1865 LEFTJOIN => $linkalias,
1867 OPERATOR => 'STARTSWITH',
1868 VALUE => 'freeside://freeside/cust_main/',
1870 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1871 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1872 $custnum_sql .= 'SIGNED INTEGER)';
1875 $custnum_sql .= 'INTEGER)';
1877 return ($linkalias, $custnum_sql);
1880 sub JoinToCustomer {
1882 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1884 my $custalias = $self->Join(
1886 EXPRESSION => $custnum_sql,
1887 TABLE2 => 'cust_main',
1888 FIELD2 => 'custnum',
1893 sub _FreesideFieldLimit {
1894 my ( $self, $field, $op, $value, %rest ) = @_;
1895 my $alias = $self->JoinToCustomer;
1896 my $is_negative = 0;
1897 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1898 # if the op is negative, do the join as though
1899 # the op were positive, then accept only records
1900 # where the right-side join key is null.
1902 $op = '=' if $op eq '!=';
1905 my $meta = $FIELD_METADATA{$field};
1907 $alias = $self->Join(
1910 FIELD1 => 'custnum',
1911 TABLE2 => $meta->[1],
1912 FIELD2 => 'custnum',
1916 $self->SUPER::Limit(
1918 FIELD => lc($field),
1921 ENTRYAGGREGATOR => 'AND',
1926 FIELD => lc($field),
1927 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1937 # {{{ Limit the result set based on content
1943 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1944 Generally best called from LimitFoo methods
1954 DESCRIPTION => undef,
1957 $args{'DESCRIPTION'} = $self->loc(
1958 "[_1] [_2] [_3]", $args{'FIELD'},
1959 $args{'OPERATOR'}, $args{'VALUE'}
1961 if ( !defined $args{'DESCRIPTION'} );
1963 my $index = $self->_NextIndex;
1965 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1967 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1969 $self->{'RecalcTicketLimits'} = 1;
1971 # If we're looking at the effective id, we don't want to append the other clause
1972 # which limits us to tickets where id = effective id
1973 if ( $args{'FIELD'} eq 'EffectiveId'
1974 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1976 $self->{'looking_at_effective_id'} = 1;
1979 if ( $args{'FIELD'} eq 'Type'
1980 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1982 $self->{'looking_at_type'} = 1;
1992 Returns a frozen string suitable for handing back to ThawLimits.
1996 sub _FreezeThawKeys {
1997 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2001 # {{{ sub FreezeLimits
2006 require MIME::Base64;
2007 MIME::Base64::base64_encode(
2008 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2015 Take a frozen Limits string generated by FreezeLimits and make this tickets
2016 object have that set of limits.
2020 # {{{ sub ThawLimits
2026 #if we don't have $in, get outta here.
2027 return undef unless ($in);
2029 $self->{'RecalcTicketLimits'} = 1;
2032 require MIME::Base64;
2034 #We don't need to die if the thaw fails.
2035 @{$self}{ $self->_FreezeThawKeys }
2036 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2038 $RT::Logger->error($@) if $@;
2044 # {{{ Limit by enum or foreign key
2046 # {{{ sub LimitQueue
2050 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2051 OPERATOR is one of = or !=. (It defaults to =).
2052 VALUE is a queue id or Name.
2065 #TODO VALUE should also take queue objects
2066 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2067 my $queue = new RT::Queue( $self->CurrentUser );
2068 $queue->Load( $args{'VALUE'} );
2069 $args{'VALUE'} = $queue->Id;
2072 # What if they pass in an Id? Check for isNum() and convert to
2075 #TODO check for a valid queue here
2079 VALUE => $args{'VALUE'},
2080 OPERATOR => $args{'OPERATOR'},
2081 DESCRIPTION => join(
2082 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2090 # {{{ sub LimitStatus
2094 Takes a paramhash with the fields OPERATOR and VALUE.
2095 OPERATOR is one of = or !=.
2098 RT adds Status != 'deleted' until object has
2099 allow_deleted_search internal property set.
2100 $tickets->{'allow_deleted_search'} = 1;
2101 $tickets->LimitStatus( VALUE => 'deleted' );
2113 VALUE => $args{'VALUE'},
2114 OPERATOR => $args{'OPERATOR'},
2115 DESCRIPTION => join( ' ',
2116 $self->loc('Status'), $args{'OPERATOR'},
2117 $self->loc( $args{'VALUE'} ) ),
2123 # {{{ sub IgnoreType
2127 If called, this search will not automatically limit the set of results found
2128 to tickets of type "Ticket". Tickets of other types, such as "project" and
2129 "approval" will be found.
2136 # Instead of faking a Limit that later gets ignored, fake up the
2137 # fact that we're already looking at type, so that the check in
2138 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2140 # $self->LimitType(VALUE => '__any');
2141 $self->{looking_at_type} = 1;
2150 Takes a paramhash with the fields OPERATOR and VALUE.
2151 OPERATOR is one of = or !=, it defaults to "=".
2152 VALUE is a string to search for in the type of the ticket.
2167 VALUE => $args{'VALUE'},
2168 OPERATOR => $args{'OPERATOR'},
2169 DESCRIPTION => join( ' ',
2170 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2178 # {{{ Limit by string field
2180 # {{{ sub LimitSubject
2184 Takes a paramhash with the fields OPERATOR and VALUE.
2185 OPERATOR is one of = or !=.
2186 VALUE is a string to search for in the subject of the ticket.
2195 VALUE => $args{'VALUE'},
2196 OPERATOR => $args{'OPERATOR'},
2197 DESCRIPTION => join( ' ',
2198 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2206 # {{{ Limit based on ticket numerical attributes
2207 # Things that can be > < = !=
2213 Takes a paramhash with the fields OPERATOR and VALUE.
2214 OPERATOR is one of =, >, < or !=.
2215 VALUE is a ticket Id to search for
2228 VALUE => $args{'VALUE'},
2229 OPERATOR => $args{'OPERATOR'},
2231 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2237 # {{{ sub LimitPriority
2239 =head2 LimitPriority
2241 Takes a paramhash with the fields OPERATOR and VALUE.
2242 OPERATOR is one of =, >, < or !=.
2243 VALUE is a value to match the ticket\'s priority against
2251 FIELD => 'Priority',
2252 VALUE => $args{'VALUE'},
2253 OPERATOR => $args{'OPERATOR'},
2254 DESCRIPTION => join( ' ',
2255 $self->loc('Priority'),
2256 $args{'OPERATOR'}, $args{'VALUE'}, ),
2262 # {{{ sub LimitInitialPriority
2264 =head2 LimitInitialPriority
2266 Takes a paramhash with the fields OPERATOR and VALUE.
2267 OPERATOR is one of =, >, < or !=.
2268 VALUE is a value to match the ticket\'s initial priority against
2273 sub LimitInitialPriority {
2277 FIELD => 'InitialPriority',
2278 VALUE => $args{'VALUE'},
2279 OPERATOR => $args{'OPERATOR'},
2280 DESCRIPTION => join( ' ',
2281 $self->loc('Initial Priority'), $args{'OPERATOR'},
2288 # {{{ sub LimitFinalPriority
2290 =head2 LimitFinalPriority
2292 Takes a paramhash with the fields OPERATOR and VALUE.
2293 OPERATOR is one of =, >, < or !=.
2294 VALUE is a value to match the ticket\'s final priority against
2298 sub LimitFinalPriority {
2302 FIELD => 'FinalPriority',
2303 VALUE => $args{'VALUE'},
2304 OPERATOR => $args{'OPERATOR'},
2305 DESCRIPTION => join( ' ',
2306 $self->loc('Final Priority'), $args{'OPERATOR'},
2313 # {{{ sub LimitTimeWorked
2315 =head2 LimitTimeWorked
2317 Takes a paramhash with the fields OPERATOR and VALUE.
2318 OPERATOR is one of =, >, < or !=.
2319 VALUE is a value to match the ticket's TimeWorked attribute
2323 sub LimitTimeWorked {
2327 FIELD => 'TimeWorked',
2328 VALUE => $args{'VALUE'},
2329 OPERATOR => $args{'OPERATOR'},
2330 DESCRIPTION => join( ' ',
2331 $self->loc('Time Worked'),
2332 $args{'OPERATOR'}, $args{'VALUE'}, ),
2338 # {{{ sub LimitTimeLeft
2340 =head2 LimitTimeLeft
2342 Takes a paramhash with the fields OPERATOR and VALUE.
2343 OPERATOR is one of =, >, < or !=.
2344 VALUE is a value to match the ticket's TimeLeft attribute
2352 FIELD => 'TimeLeft',
2353 VALUE => $args{'VALUE'},
2354 OPERATOR => $args{'OPERATOR'},
2355 DESCRIPTION => join( ' ',
2356 $self->loc('Time Left'),
2357 $args{'OPERATOR'}, $args{'VALUE'}, ),
2365 # {{{ Limiting based on attachment attributes
2367 # {{{ sub LimitContent
2371 Takes a paramhash with the fields OPERATOR and VALUE.
2372 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2373 VALUE is a string to search for in the body of the ticket
2382 VALUE => $args{'VALUE'},
2383 OPERATOR => $args{'OPERATOR'},
2384 DESCRIPTION => join( ' ',
2385 $self->loc('Ticket content'), $args{'OPERATOR'},
2392 # {{{ sub LimitFilename
2394 =head2 LimitFilename
2396 Takes a paramhash with the fields OPERATOR and VALUE.
2397 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2398 VALUE is a string to search for in the body of the ticket
2406 FIELD => 'Filename',
2407 VALUE => $args{'VALUE'},
2408 OPERATOR => $args{'OPERATOR'},
2409 DESCRIPTION => join( ' ',
2410 $self->loc('Attachment filename'), $args{'OPERATOR'},
2416 # {{{ sub LimitContentType
2418 =head2 LimitContentType
2420 Takes a paramhash with the fields OPERATOR and VALUE.
2421 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2422 VALUE is a content type to search ticket attachments for
2426 sub LimitContentType {
2430 FIELD => 'ContentType',
2431 VALUE => $args{'VALUE'},
2432 OPERATOR => $args{'OPERATOR'},
2433 DESCRIPTION => join( ' ',
2434 $self->loc('Ticket content type'), $args{'OPERATOR'},
2443 # {{{ Limiting based on people
2445 # {{{ sub LimitOwner
2449 Takes a paramhash with the fields OPERATOR and VALUE.
2450 OPERATOR is one of = or !=.
2462 my $owner = new RT::User( $self->CurrentUser );
2463 $owner->Load( $args{'VALUE'} );
2465 # FIXME: check for a valid $owner
2468 VALUE => $args{'VALUE'},
2469 OPERATOR => $args{'OPERATOR'},
2470 DESCRIPTION => join( ' ',
2471 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2478 # {{{ Limiting watchers
2480 # {{{ sub LimitWatcher
2484 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2485 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2486 VALUE is a value to match the ticket\'s watcher email addresses against
2487 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2501 #build us up a description
2502 my ( $watcher_type, $desc );
2503 if ( $args{'TYPE'} ) {
2504 $watcher_type = $args{'TYPE'};
2507 $watcher_type = "Watcher";
2511 FIELD => $watcher_type,
2512 VALUE => $args{'VALUE'},
2513 OPERATOR => $args{'OPERATOR'},
2514 TYPE => $args{'TYPE'},
2515 DESCRIPTION => join( ' ',
2516 $self->loc($watcher_type),
2517 $args{'OPERATOR'}, $args{'VALUE'}, ),
2527 # {{{ Limiting based on links
2531 =head2 LimitLinkedTo
2533 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2534 TYPE limits the sort of link we want to search on
2536 TYPE = { RefersTo, MemberOf, DependsOn }
2538 TARGET is the id or URI of the TARGET of the link
2552 FIELD => 'LinkedTo',
2554 TARGET => $args{'TARGET'},
2555 TYPE => $args{'TYPE'},
2556 DESCRIPTION => $self->loc(
2557 "Tickets [_1] by [_2]",
2558 $self->loc( $args{'TYPE'} ),
2561 OPERATOR => $args{'OPERATOR'},
2567 # {{{ LimitLinkedFrom
2569 =head2 LimitLinkedFrom
2571 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2572 TYPE limits the sort of link we want to search on
2575 BASE is the id or URI of the BASE of the link
2579 sub LimitLinkedFrom {
2588 # translate RT2 From/To naming to RT3 TicketSQL naming
2589 my %fromToMap = qw(DependsOn DependentOn
2591 RefersTo ReferredToBy);
2593 my $type = $args{'TYPE'};
2594 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2597 FIELD => 'LinkedTo',
2599 BASE => $args{'BASE'},
2601 DESCRIPTION => $self->loc(
2602 "Tickets [_1] [_2]",
2603 $self->loc( $args{'TYPE'} ),
2606 OPERATOR => $args{'OPERATOR'},
2615 my $ticket_id = shift;
2616 return $self->LimitLinkedTo(
2618 TARGET => $ticket_id,
2625 # {{{ LimitHasMember
2626 sub LimitHasMember {
2628 my $ticket_id = shift;
2629 return $self->LimitLinkedFrom(
2631 BASE => "$ticket_id",
2632 TYPE => 'HasMember',
2639 # {{{ LimitDependsOn
2641 sub LimitDependsOn {
2643 my $ticket_id = shift;
2644 return $self->LimitLinkedTo(
2646 TARGET => $ticket_id,
2647 TYPE => 'DependsOn',
2654 # {{{ LimitDependedOnBy
2656 sub LimitDependedOnBy {
2658 my $ticket_id = shift;
2659 return $self->LimitLinkedFrom(
2662 TYPE => 'DependentOn',
2673 my $ticket_id = shift;
2674 return $self->LimitLinkedTo(
2676 TARGET => $ticket_id,
2684 # {{{ LimitReferredToBy
2686 sub LimitReferredToBy {
2688 my $ticket_id = shift;
2689 return $self->LimitLinkedFrom(
2692 TYPE => 'ReferredToBy',
2700 # {{{ limit based on ticket date attribtes
2704 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2706 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2708 OPERATOR is one of > or <
2709 VALUE is a date and time in ISO format in GMT
2710 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2712 There are also helper functions of the form LimitFIELD that eliminate
2713 the need to pass in a FIELD argument.
2727 #Set the description if we didn't get handed it above
2728 unless ( $args{'DESCRIPTION'} ) {
2729 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2730 . $args{'OPERATOR'} . " "
2731 . $args{'VALUE'} . " GMT";
2734 $self->Limit(%args);
2742 $self->LimitDate( FIELD => 'Created', @_ );
2747 $self->LimitDate( FIELD => 'Due', @_ );
2753 $self->LimitDate( FIELD => 'Starts', @_ );
2759 $self->LimitDate( FIELD => 'Started', @_ );
2764 $self->LimitDate( FIELD => 'Resolved', @_ );
2769 $self->LimitDate( FIELD => 'Told', @_ );
2772 sub LimitLastUpdated {
2774 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2778 # {{{ sub LimitTransactionDate
2780 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2782 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2784 OPERATOR is one of > or <
2785 VALUE is a date and time in ISO format in GMT
2790 sub LimitTransactionDate {
2793 FIELD => 'TransactionDate',
2800 # <20021217042756.GK28744@pallas.fsck.com>
2801 # "Kill It" - Jesse.
2803 #Set the description if we didn't get handed it above
2804 unless ( $args{'DESCRIPTION'} ) {
2805 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2806 . $args{'OPERATOR'} . " "
2807 . $args{'VALUE'} . " GMT";
2810 $self->Limit(%args);
2818 # {{{ Limit based on custom fields
2819 # {{{ sub LimitCustomField
2821 =head2 LimitCustomField
2823 Takes a paramhash of key/value pairs with the following keys:
2827 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2829 =item OPERATOR - The usual Limit operators
2831 =item VALUE - The value to compare against
2837 sub LimitCustomField {
2841 CUSTOMFIELD => undef,
2843 DESCRIPTION => undef,
2844 FIELD => 'CustomFieldValue',
2849 my $CF = RT::CustomField->new( $self->CurrentUser );
2850 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2851 $CF->Load( $args{CUSTOMFIELD} );
2854 $CF->LoadByNameAndQueue(
2855 Name => $args{CUSTOMFIELD},
2856 Queue => $args{QUEUE}
2858 $args{CUSTOMFIELD} = $CF->Id;
2861 #If we are looking to compare with a null value.
2862 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2863 $args{'DESCRIPTION'}
2864 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2866 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2867 $args{'DESCRIPTION'}
2868 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2871 # if we're not looking to compare with a null value
2873 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2874 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2877 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2878 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2879 $QueueObj->Load( $args{'QUEUE'} );
2880 $args{'QUEUE'} = $QueueObj->Id;
2882 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2885 @rest = ( ENTRYAGGREGATOR => 'AND' )
2886 if ( $CF->Type eq 'SelectMultiple' );
2889 VALUE => $args{VALUE},
2891 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2892 .".{" . $CF->Name . "}",
2893 OPERATOR => $args{OPERATOR},
2898 $self->{'RecalcTicketLimits'} = 1;
2904 # {{{ sub _NextIndex
2908 Keep track of the counter for the array of restrictions
2914 return ( $self->{'restriction_index'}++ );
2921 # {{{ Core bits to make this a DBIx::SearchBuilder object
2926 $self->{'table'} = "Tickets";
2927 $self->{'RecalcTicketLimits'} = 1;
2928 $self->{'looking_at_effective_id'} = 0;
2929 $self->{'looking_at_type'} = 0;
2930 $self->{'restriction_index'} = 1;
2931 $self->{'primary_key'} = "id";
2932 delete $self->{'items_array'};
2933 delete $self->{'item_map'};
2934 delete $self->{'columns_to_display'};
2935 $self->SUPER::_Init(@_);
2946 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2947 return ( $self->SUPER::Count() );
2955 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2956 return ( $self->SUPER::CountAll() );
2961 # {{{ sub ItemsArrayRef
2963 =head2 ItemsArrayRef
2965 Returns a reference to the set of all items found in this search
2972 return $self->{'items_array'} if $self->{'items_array'};
2974 my $placeholder = $self->_ItemsCounter;
2975 $self->GotoFirstItem();
2976 while ( my $item = $self->Next ) {
2977 push( @{ $self->{'items_array'} }, $item );
2979 $self->GotoItem($placeholder);
2980 $self->{'items_array'}
2981 = $self->ItemsOrderBy( $self->{'items_array'} );
2983 return $self->{'items_array'};
2986 sub ItemsArrayRefWindow {
2990 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2992 $self->RowsPerPage( $window );
2994 $self->GotoFirstItem;
2997 while ( my $item = $self->Next ) {
3001 $self->RowsPerPage( $old[1] );
3002 $self->FirstRow( $old[2] );
3003 $self->GotoItem( $old[0] );
3014 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3016 my $Ticket = $self->SUPER::Next;
3017 return $Ticket unless $Ticket;
3019 if ( $Ticket->__Value('Status') eq 'deleted'
3020 && !$self->{'allow_deleted_search'} )
3024 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3025 # if we found a ticket with this option enabled then
3026 # all tickets we found are ACLed, cache this fact
3027 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3028 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3031 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3036 # If the user doesn't have the right to show this ticket
3043 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3044 return $self->SUPER::_DoSearch( @_ );
3049 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3050 return $self->SUPER::_DoCount( @_ );
3056 my $cache_key = 'RolesHasRight;:;ShowTicket';
3058 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3062 my $ACL = RT::ACL->new( $RT::SystemUser );
3063 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3064 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3065 my $principal_alias = $ACL->Join(
3067 FIELD1 => 'PrincipalId',
3068 TABLE2 => 'Principals',
3071 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3074 while ( my $ACE = $ACL->Next ) {
3075 my $role = $ACE->PrincipalType;
3076 my $type = $ACE->ObjectType;
3077 if ( $type eq 'RT::System' ) {
3080 elsif ( $type eq 'RT::Queue' ) {
3081 next if $res{ $role } && !ref $res{ $role };
3082 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3085 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3088 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3092 sub _DirectlyCanSeeIn {
3094 my $id = $self->CurrentUser->id;
3096 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3097 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3101 my $ACL = RT::ACL->new( $RT::SystemUser );
3102 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3103 my $principal_alias = $ACL->Join(
3105 FIELD1 => 'PrincipalId',
3106 TABLE2 => 'Principals',
3109 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3110 my $cgm_alias = $ACL->Join(
3112 FIELD1 => 'PrincipalId',
3113 TABLE2 => 'CachedGroupMembers',
3114 FIELD2 => 'GroupId',
3116 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3117 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3120 while ( my $ACE = $ACL->Next ) {
3121 my $type = $ACE->ObjectType;
3122 if ( $type eq 'RT::System' ) {
3123 # If user is direct member of a group that has the right
3124 # on the system then he can see any ticket
3125 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3128 elsif ( $type eq 'RT::Queue' ) {
3129 push @res, $ACE->ObjectId;
3132 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3135 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3139 sub CurrentUserCanSee {
3141 return if $self->{'_sql_current_user_can_see_applied'};
3143 return $self->{'_sql_current_user_can_see_applied'} = 1
3144 if $self->CurrentUser->UserObj->HasRight(
3145 Right => 'SuperUser', Object => $RT::System
3148 my $id = $self->CurrentUser->id;
3150 # directly can see in all queues then we have nothing to do
3151 my @direct_queues = $self->_DirectlyCanSeeIn;
3152 return $self->{'_sql_current_user_can_see_applied'} = 1
3153 if @direct_queues && $direct_queues[0] == -1;
3155 my %roles = $self->_RolesCanSee;
3157 my %skip = map { $_ => 1 } @direct_queues;
3158 foreach my $role ( keys %roles ) {
3159 next unless ref $roles{ $role };
3161 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3163 $roles{ $role } = \@queues;
3165 delete $roles{ $role };
3170 # there is no global watchers, only queues and tickes, if at
3171 # some point we will add global roles then it's gonna blow
3172 # the idea here is that if the right is set globaly for a role
3173 # and user plays this role for a queue directly not a ticket
3174 # then we have to check in advance
3175 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3177 my $groups = RT::Groups->new( $RT::SystemUser );
3178 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3180 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3182 my $principal_alias = $groups->Join(
3185 TABLE2 => 'Principals',
3188 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3189 my $cgm_alias = $groups->Join(
3192 TABLE2 => 'CachedGroupMembers',
3193 FIELD2 => 'GroupId',
3195 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3196 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3197 while ( my $group = $groups->Next ) {
3198 push @direct_queues, $group->Instance;
3202 unless ( @direct_queues || keys %roles ) {
3203 $self->SUPER::Limit(
3208 ENTRYAGGREGATOR => 'AND',
3210 return $self->{'_sql_current_user_can_see_applied'} = 1;
3214 my $join_roles = keys %roles;
3215 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3216 my ($role_group_alias, $cgm_alias);
3217 if ( $join_roles ) {
3218 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3219 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3220 $self->SUPER::Limit(
3221 LEFTJOIN => $cgm_alias,
3222 FIELD => 'MemberId',
3227 my $limit_queues = sub {
3231 return unless @queues;
3232 if ( @queues == 1 ) {
3233 $self->SUPER::Limit(
3238 ENTRYAGGREGATOR => $ea,
3241 $self->SUPER::_OpenParen('ACL');
3242 foreach my $q ( @queues ) {
3243 $self->SUPER::Limit(
3248 ENTRYAGGREGATOR => $ea,
3252 $self->SUPER::_CloseParen('ACL');
3257 $self->SUPER::_OpenParen('ACL');
3259 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3260 while ( my ($role, $queues) = each %roles ) {
3261 $self->SUPER::_OpenParen('ACL');
3262 if ( $role eq 'Owner' ) {
3263 $self->SUPER::Limit(
3267 ENTRYAGGREGATOR => $ea,
3271 $self->SUPER::Limit(
3273 ALIAS => $cgm_alias,
3274 FIELD => 'MemberId',
3275 OPERATOR => 'IS NOT',
3278 ENTRYAGGREGATOR => $ea,
3280 $self->SUPER::Limit(
3282 ALIAS => $role_group_alias,
3285 ENTRYAGGREGATOR => 'AND',
3288 $limit_queues->( 'AND', @$queues ) if ref $queues;
3289 $ea = 'OR' if $ea eq 'AND';
3290 $self->SUPER::_CloseParen('ACL');
3292 $self->SUPER::_CloseParen('ACL');
3294 return $self->{'_sql_current_user_can_see_applied'} = 1;
3301 # {{{ Deal with storing and restoring restrictions
3303 # {{{ sub LoadRestrictions
3305 =head2 LoadRestrictions
3307 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3308 TODO It is not yet implemented
3314 # {{{ sub DescribeRestrictions
3316 =head2 DescribeRestrictions
3319 Returns a hash keyed by restriction id.
3320 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3321 is a description of the purpose of that TicketRestriction
3325 sub DescribeRestrictions {
3330 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3331 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3338 # {{{ sub RestrictionValues
3340 =head2 RestrictionValues FIELD
3342 Takes a restriction field and returns a list of values this field is restricted
3347 sub RestrictionValues {
3350 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3351 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3352 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3354 keys %{ $self->{'TicketRestrictions'} };
3359 # {{{ sub ClearRestrictions
3361 =head2 ClearRestrictions
3363 Removes all restrictions irretrievably
3367 sub ClearRestrictions {
3369 delete $self->{'TicketRestrictions'};
3370 $self->{'looking_at_effective_id'} = 0;
3371 $self->{'looking_at_type'} = 0;
3372 $self->{'RecalcTicketLimits'} = 1;
3377 # {{{ sub DeleteRestriction
3379 =head2 DeleteRestriction
3381 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3382 Removes that restriction from the session's limits.
3386 sub DeleteRestriction {
3389 delete $self->{'TicketRestrictions'}{$row};
3391 $self->{'RecalcTicketLimits'} = 1;
3393 #make the underlying easysearch object forget all its preconceptions
3398 # {{{ sub _RestrictionsToClauses
3400 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3402 sub _RestrictionsToClauses {
3406 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3407 my $restriction = $self->{'TicketRestrictions'}{$row};
3409 # We need to reimplement the subclause aggregation that SearchBuilder does.
3410 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3411 # Then SB AND's the different Subclauses together.
3413 # So, we want to group things into Subclauses, convert them to
3414 # SQL, and then join them with the appropriate DefaultEA.
3415 # Then join each subclause group with AND.
3417 my $field = $restriction->{'FIELD'};
3418 my $realfield = $field; # CustomFields fake up a fieldname, so
3419 # we need to figure that out
3422 # Rewrite LinkedTo meta field to the real field
3423 if ( $field =~ /LinkedTo/ ) {
3424 $realfield = $field = $restriction->{'TYPE'};
3428 # Handle subkey fields with a different real field
3429 if ( $field =~ /^(\w+)\./ ) {
3433 die "I don't know about $field yet"
3434 unless ( exists $FIELD_METADATA{$realfield}
3435 or $restriction->{CUSTOMFIELD} );
3437 my $type = $FIELD_METADATA{$realfield}->[0];
3438 my $op = $restriction->{'OPERATOR'};
3442 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3445 # this performs the moral equivalent of defined or/dor/C<//>,
3446 # without the short circuiting.You need to use a 'defined or'
3447 # type thing instead of just checking for truth values, because
3448 # VALUE could be 0.(i.e. "false")
3450 # You could also use this, but I find it less aesthetic:
3451 # (although it does short circuit)
3452 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3453 # defined $restriction->{'TICKET'} ?
3454 # $restriction->{TICKET} :
3455 # defined $restriction->{'BASE'} ?
3456 # $restriction->{BASE} :
3457 # defined $restriction->{'TARGET'} ?
3458 # $restriction->{TARGET} )
3460 my $ea = $restriction->{ENTRYAGGREGATOR}
3461 || $DefaultEA{$type}
3464 die "Invalid operator $op for $field ($type)"
3465 unless exists $ea->{$op};
3469 # Each CustomField should be put into a different Clause so they
3470 # are ANDed together.
3471 if ( $restriction->{CUSTOMFIELD} ) {
3472 $realfield = $field;
3475 exists $clause{$realfield} or $clause{$realfield} = [];
3478 $field =~ s!(['"])!\\$1!g;
3479 $value =~ s!(['"])!\\$1!g;
3480 my $data = [ $ea, $type, $field, $op, $value ];
3482 # here is where we store extra data, say if it's a keyword or
3483 # something. (I.e. "TYPE SPECIFIC STUFF")
3485 push @{ $clause{$realfield} }, $data;
3492 # {{{ sub _ProcessRestrictions
3494 =head2 _ProcessRestrictions PARAMHASH
3496 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3497 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3501 sub _ProcessRestrictions {
3504 #Blow away ticket aliases since we'll need to regenerate them for
3506 delete $self->{'TicketAliases'};
3507 delete $self->{'items_array'};
3508 delete $self->{'item_map'};
3509 delete $self->{'raw_rows'};
3510 delete $self->{'rows'};
3511 delete $self->{'count_all'};
3513 my $sql = $self->Query; # Violating the _SQL namespace
3514 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3516 # "Restrictions to Clauses Branch\n";
3517 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3519 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3523 $sql = $self->ClausesToSQL($clauseRef);
3524 $self->FromSQL($sql) if $sql;
3528 $self->{'RecalcTicketLimits'} = 0;
3532 =head2 _BuildItemMap
3534 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3535 display search nav quickly.
3542 my $window = RT->Config->Get('TicketsItemMapSize');
3544 $self->{'item_map'} = {};
3546 my $items = $self->ItemsArrayRefWindow( $window );
3547 return unless $items && @$items;
3550 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3551 for ( my $i = 0; $i < @$items; $i++ ) {
3552 my $item = $items->[$i];
3553 my $id = $item->EffectiveId;
3554 $self->{'item_map'}{$id}{'defined'} = 1;
3555 $self->{'item_map'}{$id}{'prev'} = $prev;
3556 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3560 $self->{'item_map'}{'last'} = $prev
3561 if !$window || @$items < $window;
3566 Returns an a map of all items found by this search. The map is a hash
3570 first => <first ticket id found>,
3571 last => <last ticket id found or undef>,
3574 prev => <the ticket id found before>,
3575 next => <the ticket id found after>,
3587 $self->_BuildItemMap unless $self->{'item_map'};
3588 return $self->{'item_map'};
3596 =head2 PrepForSerialization
3598 You don't want to serialize a big tickets object, as
3599 the {items} hash will be instantly invalid _and_ eat
3604 sub PrepForSerialization {
3606 delete $self->{'items'};
3607 delete $self->{'items_array'};
3608 $self->RedoSearch();
3613 RT::Tickets supports several flags which alter search behavior:
3616 allow_deleted_search (Otherwise never show deleted tickets in search results)
3617 looking_at_type (otherwise limit to type=ticket)
3619 These flags are set by calling
3621 $tickets->{'flagname'} = 1;
3623 BUG: There should be an API for this