1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 # <jesse@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 # Mapping of Field Type to Function
155 ENUM => \&_EnumLimit,
158 LINK => \&_LinkLimit,
159 DATE => \&_DateLimit,
160 STRING => \&_StringLimit,
161 TRANSFIELD => \&_TransLimit,
162 TRANSDATE => \&_TransDateLimit,
163 WATCHERFIELD => \&_WatcherLimit,
164 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
165 CUSTOMFIELD => \&_CustomFieldLimit,
166 HASATTRIBUTE => \&_HasAttributeLimit,
167 FREESIDEFIELD => \&_FreesideFieldLimit,
169 our %can_bundle = ();# WATCHERFIELD => "yes", );
171 # Default EntryAggregator per type
172 # if you specify OP, you must specify all valid OPs
213 # Helper functions for passing the above lexically scoped tables above
214 # into Tickets_Overlay_SQL.
215 sub FIELDS { return \%FIELD_METADATA }
216 sub dispatch { return \%dispatch }
217 sub can_bundle { return \%can_bundle }
219 # Bring in the clowns.
220 require RT::Tickets_Overlay_SQL;
224 our @SORTFIELDS = qw(id Status
226 Owner Created Due Starts Started
228 Resolved LastUpdated Priority TimeWorked TimeLeft);
232 Returns the list of fields that lists of tickets can easily be sorted by
238 return (@SORTFIELDS);
243 # BEGIN SQL STUFF *********************************
248 $self->SUPER::CleanSlate( @_ );
249 delete $self->{$_} foreach qw(
251 _sql_group_members_aliases
252 _sql_object_cfv_alias
253 _sql_role_group_aliases
256 _sql_u_watchers_alias_for_sort
257 _sql_u_watchers_aliases
258 _sql_current_user_can_see_applied
262 =head1 Limit Helper Routines
264 These routines are the targets of a dispatch table depending on the
265 type of field. They all share the same signature:
267 my ($self,$field,$op,$value,@rest) = @_;
269 The values in @rest should be suitable for passing directly to
270 DBIx::SearchBuilder::Limit.
272 Essentially they are an expanded/broken out (and much simplified)
273 version of what ProcessRestrictions used to do. They're also much
274 more clearly delineated by the TYPE of field being processed.
283 my ( $sb, $field, $op, $value, @rest ) = @_;
285 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
287 die "Invalid operator $op for __Bookmarked__ search on $field"
288 unless $op =~ /^(=|!=)$/;
291 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
292 $tmp = $tmp->Content if $tmp;
297 return $sb->_SQLLimit(
304 # as bookmarked tickets can be merged we have to use a join
305 # but it should be pretty lightweight
306 my $tickets_alias = $sb->Join(
311 FIELD2 => 'EffectiveId',
315 my $ea = $op eq '='? 'OR': 'AND';
316 foreach my $id ( sort @bookmarks ) {
318 ALIAS => $tickets_alias,
322 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
330 Handle Fields which are limited to certain values, and potentially
331 need to be looked up from another class.
333 This subroutine actually handles two different kinds of fields. For
334 some the user is responsible for limiting the values. (i.e. Status,
337 For others, the value specified by the user will be looked by via
341 name of class to lookup in (Optional)
346 my ( $sb, $field, $op, $value, @rest ) = @_;
348 # SQL::Statement changes != to <>. (Can we remove this now?)
349 $op = "!=" if $op eq "<>";
351 die "Invalid Operation: $op for $field"
355 my $meta = $FIELD_METADATA{$field};
356 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
357 my $class = "RT::" . $meta->[1];
358 my $o = $class->new( $sb->CurrentUser );
372 Handle fields where the values are limited to integers. (For example,
373 Priority, TimeWorked.)
381 my ( $sb, $field, $op, $value, @rest ) = @_;
383 die "Invalid Operator $op for $field"
384 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
396 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
399 1: Direction (From, To)
400 2: Link Type (MemberOf, DependsOn, RefersTo)
405 my ( $sb, $field, $op, $value, @rest ) = @_;
407 my $meta = $FIELD_METADATA{$field};
408 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
411 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
415 $is_null = 1 if !$value || $value =~ /^null$/io;
417 my $direction = $meta->[1] || '';
418 my ($matchfield, $linkfield) = ('', '');
419 if ( $direction eq 'To' ) {
420 ($matchfield, $linkfield) = ("Target", "Base");
422 elsif ( $direction eq 'From' ) {
423 ($matchfield, $linkfield) = ("Base", "Target");
425 elsif ( $direction ) {
426 die "Invalid link direction '$direction' for $field\n";
429 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
431 'LinkedFrom', $op, $value, @rest,
432 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
440 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
442 elsif ( $value =~ /\D/ ) {
445 $matchfield = "Local$matchfield" if $is_local;
447 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
448 # SELECT main.* FROM Tickets main
449 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
450 # AND(main.id = Links_1.LocalTarget))
451 # WHERE Links_1.LocalBase IS NULL;
454 my $linkalias = $sb->Join(
459 FIELD2 => 'Local' . $linkfield
462 LEFTJOIN => $linkalias,
470 FIELD => $matchfield,
477 my $linkalias = $sb->Join(
482 FIELD2 => 'Local' . $linkfield
485 LEFTJOIN => $linkalias,
491 LEFTJOIN => $linkalias,
492 FIELD => $matchfield,
499 FIELD => $matchfield,
500 OPERATOR => $is_negative? 'IS': 'IS NOT',
509 Handle date fields. (Created, LastTold..)
512 1: type of link. (Probably not necessary.)
517 my ( $sb, $field, $op, $value, @rest ) = @_;
519 die "Invalid Date Op: $op"
520 unless $op =~ /^(=|>|<|>=|<=)$/;
522 my $meta = $FIELD_METADATA{$field};
523 die "Incorrect Meta Data for $field"
524 unless ( defined $meta->[1] );
526 $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest );
529 # Factor this out for use by custom fields
531 sub _DateFieldLimit {
532 my ( $sb, $field, $op, $value, @rest ) = @_;
534 my $date = RT::Date->new( $sb->CurrentUser );
535 $date->Set( Format => 'unknown', Value => $value );
539 # if we're specifying =, that means we want everything on a
540 # particular single day. in the database, we need to check for >
541 # and < the edges of that day.
543 $date->SetToMidnight( Timezone => 'server' );
544 my $daystart = $date->ISO;
546 my $dayend = $date->ISO;
562 ENTRYAGGREGATOR => 'AND',
580 Handle simple fields which are just strings. (Subject,Type)
588 my ( $sb, $field, $op, $value, @rest ) = @_;
592 # =, !=, LIKE, NOT LIKE
593 if ( (!defined $value || !length $value)
594 && lc($op) ne 'is' && lc($op) ne 'is not'
595 && RT->Config->Get('DatabaseType') eq 'Oracle'
597 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
598 $op = $negative? 'IS NOT': 'IS';
611 =head2 _TransDateLimit
613 Handle fields limiting based on Transaction Date.
615 The inpupt value must be in a format parseable by Time::ParseDate
622 # This routine should really be factored into translimit.
623 sub _TransDateLimit {
624 my ( $sb, $field, $op, $value, @rest ) = @_;
626 # See the comments for TransLimit, they apply here too
628 unless ( $sb->{_sql_transalias} ) {
629 $sb->{_sql_transalias} = $sb->Join(
632 TABLE2 => 'Transactions',
633 FIELD2 => 'ObjectId',
636 ALIAS => $sb->{_sql_transalias},
637 FIELD => 'ObjectType',
638 VALUE => 'RT::Ticket',
639 ENTRYAGGREGATOR => 'AND',
643 my $date = RT::Date->new( $sb->CurrentUser );
644 $date->Set( Format => 'unknown', Value => $value );
649 # if we're specifying =, that means we want everything on a
650 # particular single day. in the database, we need to check for >
651 # and < the edges of that day.
653 $date->SetToMidnight( Timezone => 'server' );
654 my $daystart = $date->ISO;
656 my $dayend = $date->ISO;
659 ALIAS => $sb->{_sql_transalias},
667 ALIAS => $sb->{_sql_transalias},
673 ENTRYAGGREGATOR => 'AND',
678 # not searching for a single day
681 #Search for the right field
683 ALIAS => $sb->{_sql_transalias},
697 Limit based on the Content of a transaction or the ContentType.
706 # Content, ContentType, Filename
708 # If only this was this simple. We've got to do something
711 #Basically, we want to make sure that the limits apply to
712 #the same attachment, rather than just another attachment
713 #for the same ticket, no matter how many clauses we lump
714 #on. We put them in TicketAliases so that they get nuked
715 #when we redo the join.
717 # In the SQL, we might have
718 # (( Content = foo ) or ( Content = bar AND Content = baz ))
719 # The AND group should share the same Alias.
721 # Actually, maybe it doesn't matter. We use the same alias and it
722 # works itself out? (er.. different.)
724 # Steal more from _ProcessRestrictions
726 # FIXME: Maybe look at the previous FooLimit call, and if it was a
727 # TransLimit and EntryAggregator == AND, reuse the Aliases?
729 # Or better - store the aliases on a per subclause basis - since
730 # those are going to be the things we want to relate to each other,
733 # maybe we should not allow certain kinds of aggregation of these
734 # clauses and do a psuedo regex instead? - the problem is getting
735 # them all into the same subclause when you have (A op B op C) - the
736 # way they get parsed in the tree they're in different subclauses.
738 my ( $self, $field, $op, $value, %rest ) = @_;
740 unless ( $self->{_sql_transalias} ) {
741 $self->{_sql_transalias} = $self->Join(
744 TABLE2 => 'Transactions',
745 FIELD2 => 'ObjectId',
748 ALIAS => $self->{_sql_transalias},
749 FIELD => 'ObjectType',
750 VALUE => 'RT::Ticket',
751 ENTRYAGGREGATOR => 'AND',
754 unless ( defined $self->{_sql_trattachalias} ) {
755 $self->{_sql_trattachalias} = $self->_SQLJoin(
756 TYPE => 'LEFT', # not all txns have an attachment
757 ALIAS1 => $self->{_sql_transalias},
759 TABLE2 => 'Attachments',
760 FIELD2 => 'TransactionId',
764 #Search for the right field
765 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
769 ALIAS => $self->{_sql_trattachalias},
776 ENTRYAGGREGATOR => 'AND',
777 ALIAS => $self->{_sql_trattachalias},
786 ALIAS => $self->{_sql_trattachalias},
799 Handle watcher limits. (Requestor, CC, etc..)
815 my $meta = $FIELD_METADATA{ $field };
816 my $type = $meta->[1] || '';
817 my $class = $meta->[2] || 'Ticket';
819 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
820 # search by id and Name at the same time, this is workaround
821 # to preserve backward compatibility
822 if ( $field eq 'Owner' ) {
823 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
824 my $o = RT::User->new( $self->CurrentUser );
825 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
826 $o->$method( $value );
835 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
845 $rest{SUBKEY} ||= 'EmailAddress';
847 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
850 if ( $op =~ /^IS(?: NOT)?$/ ) {
851 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
852 # to avoid joining the table Users into the query, we just join GM
853 # and make sure we don't match records where group is member of itself
855 LEFTJOIN => $group_members,
858 VALUE => "$group_members.MemberId",
862 ALIAS => $group_members,
869 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
871 $op =~ s/!|NOT\s+//i;
873 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
874 # "X = 'Y'" matches more then one user so we try to fetch two records and
875 # do the right thing when there is only one exist and semi-working solution
877 my $users_obj = RT::Users->new( $self->CurrentUser );
879 FIELD => $rest{SUBKEY},
884 $users_obj->RowsPerPage(2);
885 my @users = @{ $users_obj->ItemsArrayRef };
887 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
890 $uid = $users[0]->id if @users;
892 LEFTJOIN => $group_members,
893 ALIAS => $group_members,
899 ALIAS => $group_members,
906 LEFTJOIN => $group_members,
909 VALUE => "$group_members.MemberId",
912 my $users = $self->Join(
914 ALIAS1 => $group_members,
915 FIELD1 => 'MemberId',
922 FIELD => $rest{SUBKEY},
936 my $group_members = $self->_GroupMembersJoin(
937 GroupsAlias => $groups,
941 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
943 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
944 $self->NewAlias('Users');
946 LEFTJOIN => $group_members,
947 ALIAS => $group_members,
949 VALUE => "$users.id",
954 # we join users table without adding some join condition between tables,
955 # the only conditions we have are conditions on the table iteslf,
956 # for example Users.EmailAddress = 'x'. We should add this condition to
957 # the top level of the query and bundle it with another similar conditions,
958 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
959 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
962 SUBCLAUSE => '_sql_u_watchers_'. $users,
964 FIELD => $rest{'SUBKEY'},
969 # A condition which ties Users and Groups (role groups) is a left join condition
970 # of CachedGroupMembers table. To get correct results of the query we check
971 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
974 ALIAS => $group_members,
976 OPERATOR => 'IS NOT',
983 sub _RoleGroupsJoin {
985 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
986 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
987 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
990 # we always have watcher groups for ticket, so we use INNER join
991 my $groups = $self->Join(
993 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
995 FIELD2 => 'Instance',
996 ENTRYAGGREGATOR => 'AND',
1002 VALUE => 'RT::'. $args{'Class'} .'-Role',
1004 $self->SUPER::Limit(
1005 LEFTJOIN => $groups,
1008 VALUE => $args{'Type'},
1011 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1012 unless $args{'New'};
1017 sub _GroupMembersJoin {
1019 my %args = (New => 1, GroupsAlias => undef, @_);
1021 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1022 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1025 my $alias = $self->Join(
1027 ALIAS1 => $args{'GroupsAlias'},
1029 TABLE2 => 'CachedGroupMembers',
1030 FIELD2 => 'GroupId',
1031 ENTRYAGGREGATOR => 'AND',
1034 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1035 unless $args{'New'};
1042 Helper function which provides joins to a watchers table both for limits
1049 my $type = shift || '';
1052 my $groups = $self->_RoleGroupsJoin( Type => $type );
1053 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1054 # XXX: work around, we must hide groups that
1055 # are members of the role group we search in,
1056 # otherwise them result in wrong NULLs in Users
1057 # table and break ordering. Now, we know that
1058 # RT doesn't allow to add groups as members of the
1059 # ticket roles, so we just hide entries in CGM table
1060 # with MemberId == GroupId from results
1061 $self->SUPER::Limit(
1062 LEFTJOIN => $group_members,
1065 VALUE => "$group_members.MemberId",
1068 my $users = $self->Join(
1070 ALIAS1 => $group_members,
1071 FIELD1 => 'MemberId',
1075 return ($groups, $group_members, $users);
1078 =head2 _WatcherMembershipLimit
1080 Handle watcher membership limits, i.e. whether the watcher belongs to a
1081 specific group or not.
1084 1: Field to query on
1086 SELECT DISTINCT main.*
1090 CachedGroupMembers CachedGroupMembers_2,
1093 (main.EffectiveId = main.id)
1095 (main.Status != 'deleted')
1097 (main.Type = 'ticket')
1100 (Users_3.EmailAddress = '22')
1102 (Groups_1.Domain = 'RT::Ticket-Role')
1104 (Groups_1.Type = 'RequestorGroup')
1107 Groups_1.Instance = main.id
1109 Groups_1.id = CachedGroupMembers_2.GroupId
1111 CachedGroupMembers_2.MemberId = Users_3.id
1112 ORDER BY main.id ASC
1117 sub _WatcherMembershipLimit {
1118 my ( $self, $field, $op, $value, @rest ) = @_;
1123 my $groups = $self->NewAlias('Groups');
1124 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1125 my $users = $self->NewAlias('Users');
1126 my $memberships = $self->NewAlias('CachedGroupMembers');
1128 if ( ref $field ) { # gross hack
1129 my @bundle = @$field;
1131 for my $chunk (@bundle) {
1132 ( $field, $op, $value, @rest ) = @$chunk;
1134 ALIAS => $memberships,
1145 ALIAS => $memberships,
1153 # {{{ Tie to groups for tickets we care about
1157 VALUE => 'RT::Ticket-Role',
1158 ENTRYAGGREGATOR => 'AND'
1163 FIELD1 => 'Instance',
1170 # If we care about which sort of watcher
1171 my $meta = $FIELD_METADATA{$field};
1172 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1179 ENTRYAGGREGATOR => 'AND'
1186 ALIAS2 => $groupmembers,
1191 ALIAS1 => $groupmembers,
1192 FIELD1 => 'MemberId',
1198 ALIAS1 => $memberships,
1199 FIELD1 => 'MemberId',
1208 =head2 _CustomFieldDecipher
1210 Try and turn a CF descriptor into (cfid, cfname) object pair.
1214 sub _CustomFieldDecipher {
1215 my ($self, $string) = @_;
1217 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
1218 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1222 my $q = RT::Queue->new( $self->CurrentUser );
1226 # $queue = $q->Name; # should we normalize the queue?
1227 $cf = $q->CustomField( $field );
1230 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1234 elsif ( $field =~ /\D/ ) {
1236 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1237 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1238 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1240 # if there is more then one field the current user can
1241 # see with the same name then we shouldn't return cf object
1242 # as we don't know which one to use
1245 $cf = undef if $cfs->Next;
1249 $cf = RT::CustomField->new( $self->CurrentUser );
1250 $cf->Load( $field );
1253 return ($queue, $field, $cf, $column);
1256 =head2 _CustomFieldJoin
1258 Factor out the Join of custom fields so we can use it for sorting too
1262 sub _CustomFieldJoin {
1263 my ($self, $cfkey, $cfid, $field) = @_;
1264 # Perform one Join per CustomField
1265 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1266 $self->{_sql_cf_alias}{$cfkey} )
1268 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1269 $self->{_sql_cf_alias}{$cfkey} );
1272 my ($TicketCFs, $CFs);
1274 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1278 TABLE2 => 'ObjectCustomFieldValues',
1279 FIELD2 => 'ObjectId',
1281 $self->SUPER::Limit(
1282 LEFTJOIN => $TicketCFs,
1283 FIELD => 'CustomField',
1285 ENTRYAGGREGATOR => 'AND'
1289 my $ocfalias = $self->Join(
1292 TABLE2 => 'ObjectCustomFields',
1293 FIELD2 => 'ObjectId',
1296 $self->SUPER::Limit(
1297 LEFTJOIN => $ocfalias,
1298 ENTRYAGGREGATOR => 'OR',
1299 FIELD => 'ObjectId',
1303 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1305 ALIAS1 => $ocfalias,
1306 FIELD1 => 'CustomField',
1307 TABLE2 => 'CustomFields',
1310 $self->SUPER::Limit(
1312 ENTRYAGGREGATOR => 'AND',
1313 FIELD => 'LookupType',
1314 VALUE => 'RT::Queue-RT::Ticket',
1316 $self->SUPER::Limit(
1318 ENTRYAGGREGATOR => 'AND',
1323 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1327 TABLE2 => 'ObjectCustomFieldValues',
1328 FIELD2 => 'CustomField',
1330 $self->SUPER::Limit(
1331 LEFTJOIN => $TicketCFs,
1332 FIELD => 'ObjectId',
1335 ENTRYAGGREGATOR => 'AND',
1338 $self->SUPER::Limit(
1339 LEFTJOIN => $TicketCFs,
1340 FIELD => 'ObjectType',
1341 VALUE => 'RT::Ticket',
1342 ENTRYAGGREGATOR => 'AND'
1344 $self->SUPER::Limit(
1345 LEFTJOIN => $TicketCFs,
1346 FIELD => 'Disabled',
1349 ENTRYAGGREGATOR => 'AND'
1352 return ($TicketCFs, $CFs);
1355 =head2 _CustomFieldLimit
1357 Limit based on CustomFields
1364 sub _CustomFieldLimit {
1365 my ( $self, $_field, $op, $value, %rest ) = @_;
1367 my $field = $rest{'SUBKEY'} || die "No field specified";
1369 # For our sanity, we can only limit on one queue at a time
1371 my ($queue, $cfid, $cf, $column);
1372 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1373 $cfid = $cf ? $cf->id : 0 ;
1375 # Handle date custom fields specially
1376 if ( $cf->Type eq 'Date' ) {
1377 return $self->_DateCustomFieldLimit($_field, $op, $value, %rest);
1380 # If we're trying to find custom fields that don't match something, we
1381 # want tickets where the custom field has no value at all. Note that
1382 # we explicitly don't include the "IS NULL" case, since we would
1383 # otherwise end up with a redundant clause.
1385 my ($negative_op, $null_op, $inv_op, $range_op)
1386 = $self->ClassifySQLOperation( $op );
1390 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1391 return 'MATCHES' if $op eq '=';
1392 return 'NOT MATCHES' if $op eq '!=';
1396 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1398 my $cfkey = $cfid ? $cfid : "$queue.$field";
1400 if ( $null_op && !$column ) {
1401 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1402 # we can reuse our default joins for this operation
1403 # with column specified we have different situation
1404 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1407 ALIAS => $TicketCFs,
1416 OPERATOR => 'IS NOT',
1419 ENTRYAGGREGATOR => 'AND',
1423 elsif ( !$negative_op || $single_value ) {
1424 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1425 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1432 # if column is defined then deal only with it
1433 # otherwise search in Content and in LargeContent
1436 ALIAS => $TicketCFs,
1438 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1443 elsif ( $cf->Type eq 'Date' ) {
1444 $self->_DateFieldLimit(
1448 ALIAS => $TicketCFs,
1452 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1453 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1455 ALIAS => $TicketCFs,
1464 ALIAS => $TicketCFs,
1468 ENTRYAGGREGATOR => 'OR'
1471 ALIAS => $TicketCFs,
1475 ENTRYAGGREGATOR => 'OR'
1479 ALIAS => $TicketCFs,
1480 FIELD => 'LargeContent',
1481 OPERATOR => $fix_op->($op),
1483 ENTRYAGGREGATOR => 'AND',
1489 ALIAS => $TicketCFs,
1499 ALIAS => $TicketCFs,
1503 ENTRYAGGREGATOR => 'OR'
1506 ALIAS => $TicketCFs,
1510 ENTRYAGGREGATOR => 'OR'
1514 ALIAS => $TicketCFs,
1515 FIELD => 'LargeContent',
1516 OPERATOR => $fix_op->($op),
1518 ENTRYAGGREGATOR => 'AND',
1524 # XXX: if we join via CustomFields table then
1525 # because of order of left joins we get NULLs in
1526 # CF table and then get nulls for those records
1527 # in OCFVs table what result in wrong results
1528 # as decifer method now tries to load a CF then
1529 # we fall into this situation only when there
1530 # are more than one CF with the name in the DB.
1531 # the same thing applies to order by call.
1532 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1533 # we want treat IS NULL as (not applies or has
1538 OPERATOR => 'IS NOT',
1541 ENTRYAGGREGATOR => 'AND',
1547 ALIAS => $TicketCFs,
1548 FIELD => $column || 'Content',
1552 ENTRYAGGREGATOR => 'OR',
1559 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1560 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1563 $op =~ s/!|NOT\s+//i;
1565 # if column is defined then deal only with it
1566 # otherwise search in Content and in LargeContent
1568 $self->SUPER::Limit(
1569 LEFTJOIN => $TicketCFs,
1570 ALIAS => $TicketCFs,
1572 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1577 $self->SUPER::Limit(
1578 LEFTJOIN => $TicketCFs,
1579 ALIAS => $TicketCFs,
1587 ALIAS => $TicketCFs,
1596 sub _HasAttributeLimit {
1597 my ( $self, $field, $op, $value, %rest ) = @_;
1599 my $alias = $self->Join(
1603 TABLE2 => 'Attributes',
1604 FIELD2 => 'ObjectId',
1606 $self->SUPER::Limit(
1608 FIELD => 'ObjectType',
1609 VALUE => 'RT::Ticket',
1610 ENTRYAGGREGATOR => 'AND'
1612 $self->SUPER::Limit(
1617 ENTRYAGGREGATOR => 'AND'
1623 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1629 # End Helper Functions
1631 # End of SQL Stuff -------------------------------------------------
1633 # {{{ Allow sorting on watchers
1635 =head2 OrderByCols ARRAY
1637 A modified version of the OrderBy method which automatically joins where
1638 C<ALIAS> is set to the name of a watcher type.
1649 foreach my $row (@args) {
1650 if ( $row->{ALIAS} ) {
1654 if ( $row->{FIELD} !~ /\./ ) {
1655 my $meta = $self->FIELDS->{ $row->{FIELD} };
1661 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1662 my $alias = $self->Join(
1665 FIELD1 => $row->{'FIELD'},
1669 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1670 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1671 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1673 my $alias = $self->Join(
1676 FIELD1 => $row->{'FIELD'},
1680 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1687 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1688 my $meta = $self->FIELDS->{$field};
1689 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1690 # cache alias as we want to use one alias per watcher type for sorting
1691 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1693 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1694 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1696 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1697 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1698 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1699 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1700 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1701 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1702 # this is described in _CustomFieldLimit
1706 OPERATOR => 'IS NOT',
1709 ENTRYAGGREGATOR => 'AND',
1712 # For those cases where we are doing a join against the
1713 # CF name, and don't have a CFid, use Unique to make sure
1714 # we don't show duplicate tickets. NOTE: I'm pretty sure
1715 # this will stay mixed in for the life of the
1716 # class/package, and not just for the life of the object.
1717 # Potential performance issue.
1718 require DBIx::SearchBuilder::Unique;
1719 DBIx::SearchBuilder::Unique->import;
1721 my $CFvs = $self->Join(
1723 ALIAS1 => $TicketCFs,
1724 FIELD1 => 'CustomField',
1725 TABLE2 => 'CustomFieldValues',
1726 FIELD2 => 'CustomField',
1728 $self->SUPER::Limit(
1732 VALUE => $TicketCFs . ".Content",
1733 ENTRYAGGREGATOR => 'AND'
1736 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1737 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1738 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1739 # PAW logic is "reversed"
1741 if (exists $row->{ORDER} ) {
1742 my $o = $row->{ORDER};
1743 delete $row->{ORDER};
1744 $order = "DESC" if $o =~ /asc/i;
1747 # Ticket.Owner 1 0 X
1748 # Unowned Tickets 0 1 X
1751 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1752 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1753 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1754 push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
1756 push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
1760 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1762 } elsif ( $field eq 'Customer' ) { #Freeside
1763 if ( $subkey eq 'Number' ) {
1764 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1767 FIELD => $custnum_sql,
1771 my $custalias = $self->JoinToCustomer;
1773 if ( $subkey eq 'Name' ) {
1774 $field = "COALESCE( $custalias.company,
1775 $custalias.last || ', ' || $custalias.first
1779 # no other cases exist yet, but for obviousness:
1782 push @res, { %$row, ALIAS => '', FIELD => $field };
1791 return $self->SUPER::OrderByCols(@res);
1796 sub JoinToCustLinks {
1797 # Set up join to links (id = localbase),
1798 # limit link type to 'MemberOf',
1799 # and target value to any Freeside custnum URI.
1800 # Return the linkalias for further join/limit action,
1801 # and an sql expression to retrieve the custnum.
1803 my $linkalias = $self->Join(
1808 FIELD2 => 'LocalBase',
1811 $self->SUPER::Limit(
1812 LEFTJOIN => $linkalias,
1815 VALUE => 'MemberOf',
1817 $self->SUPER::Limit(
1818 LEFTJOIN => $linkalias,
1820 OPERATOR => 'STARTSWITH',
1821 VALUE => 'freeside://freeside/cust_main/',
1823 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1824 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1825 $custnum_sql .= 'SIGNED INTEGER)';
1828 $custnum_sql .= 'INTEGER)';
1830 return ($linkalias, $custnum_sql);
1833 sub JoinToCustomer {
1835 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1837 my $custalias = $self->Join(
1839 EXPRESSION => $custnum_sql,
1840 TABLE2 => 'cust_main',
1841 FIELD2 => 'custnum',
1846 sub _FreesideFieldLimit {
1847 my ( $self, $field, $op, $value, %rest ) = @_;
1848 my $alias = $self->JoinToCustomer;
1849 my $is_negative = 0;
1850 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1851 # if the op is negative, do the join as though
1852 # the op were positive, then accept only records
1853 # where the right-side join key is null.
1855 $op = '=' if $op eq '!=';
1858 my $meta = $FIELD_METADATA{$field};
1860 $alias = $self->Join(
1863 FIELD1 => 'custnum',
1864 TABLE2 => $meta->[1],
1865 FIELD2 => 'custnum',
1869 $self->SUPER::Limit(
1871 FIELD => lc($field),
1874 ENTRYAGGREGATOR => 'AND',
1879 FIELD => lc($field),
1880 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1890 # {{{ Limit the result set based on content
1896 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1897 Generally best called from LimitFoo methods
1907 DESCRIPTION => undef,
1910 $args{'DESCRIPTION'} = $self->loc(
1911 "[_1] [_2] [_3]", $args{'FIELD'},
1912 $args{'OPERATOR'}, $args{'VALUE'}
1914 if ( !defined $args{'DESCRIPTION'} );
1916 my $index = $self->_NextIndex;
1918 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1920 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1922 $self->{'RecalcTicketLimits'} = 1;
1924 # If we're looking at the effective id, we don't want to append the other clause
1925 # which limits us to tickets where id = effective id
1926 if ( $args{'FIELD'} eq 'EffectiveId'
1927 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1929 $self->{'looking_at_effective_id'} = 1;
1932 if ( $args{'FIELD'} eq 'Type'
1933 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1935 $self->{'looking_at_type'} = 1;
1945 Returns a frozen string suitable for handing back to ThawLimits.
1949 sub _FreezeThawKeys {
1950 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1954 # {{{ sub FreezeLimits
1959 require MIME::Base64;
1960 MIME::Base64::base64_encode(
1961 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1968 Take a frozen Limits string generated by FreezeLimits and make this tickets
1969 object have that set of limits.
1973 # {{{ sub ThawLimits
1979 #if we don't have $in, get outta here.
1980 return undef unless ($in);
1982 $self->{'RecalcTicketLimits'} = 1;
1985 require MIME::Base64;
1987 #We don't need to die if the thaw fails.
1988 @{$self}{ $self->_FreezeThawKeys }
1989 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1991 $RT::Logger->error($@) if $@;
1997 # {{{ Limit by enum or foreign key
1999 # {{{ sub LimitQueue
2003 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2004 OPERATOR is one of = or !=. (It defaults to =).
2005 VALUE is a queue id or Name.
2018 #TODO VALUE should also take queue objects
2019 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2020 my $queue = new RT::Queue( $self->CurrentUser );
2021 $queue->Load( $args{'VALUE'} );
2022 $args{'VALUE'} = $queue->Id;
2025 # What if they pass in an Id? Check for isNum() and convert to
2028 #TODO check for a valid queue here
2032 VALUE => $args{'VALUE'},
2033 OPERATOR => $args{'OPERATOR'},
2034 DESCRIPTION => join(
2035 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2043 # {{{ sub LimitStatus
2047 Takes a paramhash with the fields OPERATOR and VALUE.
2048 OPERATOR is one of = or !=.
2051 RT adds Status != 'deleted' until object has
2052 allow_deleted_search internal property set.
2053 $tickets->{'allow_deleted_search'} = 1;
2054 $tickets->LimitStatus( VALUE => 'deleted' );
2066 VALUE => $args{'VALUE'},
2067 OPERATOR => $args{'OPERATOR'},
2068 DESCRIPTION => join( ' ',
2069 $self->loc('Status'), $args{'OPERATOR'},
2070 $self->loc( $args{'VALUE'} ) ),
2076 # {{{ sub IgnoreType
2080 If called, this search will not automatically limit the set of results found
2081 to tickets of type "Ticket". Tickets of other types, such as "project" and
2082 "approval" will be found.
2089 # Instead of faking a Limit that later gets ignored, fake up the
2090 # fact that we're already looking at type, so that the check in
2091 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2093 # $self->LimitType(VALUE => '__any');
2094 $self->{looking_at_type} = 1;
2103 Takes a paramhash with the fields OPERATOR and VALUE.
2104 OPERATOR is one of = or !=, it defaults to "=".
2105 VALUE is a string to search for in the type of the ticket.
2120 VALUE => $args{'VALUE'},
2121 OPERATOR => $args{'OPERATOR'},
2122 DESCRIPTION => join( ' ',
2123 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2131 # {{{ Limit by string field
2133 # {{{ sub LimitSubject
2137 Takes a paramhash with the fields OPERATOR and VALUE.
2138 OPERATOR is one of = or !=.
2139 VALUE is a string to search for in the subject of the ticket.
2148 VALUE => $args{'VALUE'},
2149 OPERATOR => $args{'OPERATOR'},
2150 DESCRIPTION => join( ' ',
2151 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2159 # {{{ Limit based on ticket numerical attributes
2160 # Things that can be > < = !=
2166 Takes a paramhash with the fields OPERATOR and VALUE.
2167 OPERATOR is one of =, >, < or !=.
2168 VALUE is a ticket Id to search for
2181 VALUE => $args{'VALUE'},
2182 OPERATOR => $args{'OPERATOR'},
2184 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2190 # {{{ sub LimitPriority
2192 =head2 LimitPriority
2194 Takes a paramhash with the fields OPERATOR and VALUE.
2195 OPERATOR is one of =, >, < or !=.
2196 VALUE is a value to match the ticket\'s priority against
2204 FIELD => 'Priority',
2205 VALUE => $args{'VALUE'},
2206 OPERATOR => $args{'OPERATOR'},
2207 DESCRIPTION => join( ' ',
2208 $self->loc('Priority'),
2209 $args{'OPERATOR'}, $args{'VALUE'}, ),
2215 # {{{ sub LimitInitialPriority
2217 =head2 LimitInitialPriority
2219 Takes a paramhash with the fields OPERATOR and VALUE.
2220 OPERATOR is one of =, >, < or !=.
2221 VALUE is a value to match the ticket\'s initial priority against
2226 sub LimitInitialPriority {
2230 FIELD => 'InitialPriority',
2231 VALUE => $args{'VALUE'},
2232 OPERATOR => $args{'OPERATOR'},
2233 DESCRIPTION => join( ' ',
2234 $self->loc('Initial Priority'), $args{'OPERATOR'},
2241 # {{{ sub LimitFinalPriority
2243 =head2 LimitFinalPriority
2245 Takes a paramhash with the fields OPERATOR and VALUE.
2246 OPERATOR is one of =, >, < or !=.
2247 VALUE is a value to match the ticket\'s final priority against
2251 sub LimitFinalPriority {
2255 FIELD => 'FinalPriority',
2256 VALUE => $args{'VALUE'},
2257 OPERATOR => $args{'OPERATOR'},
2258 DESCRIPTION => join( ' ',
2259 $self->loc('Final Priority'), $args{'OPERATOR'},
2266 # {{{ sub LimitTimeWorked
2268 =head2 LimitTimeWorked
2270 Takes a paramhash with the fields OPERATOR and VALUE.
2271 OPERATOR is one of =, >, < or !=.
2272 VALUE is a value to match the ticket's TimeWorked attribute
2276 sub LimitTimeWorked {
2280 FIELD => 'TimeWorked',
2281 VALUE => $args{'VALUE'},
2282 OPERATOR => $args{'OPERATOR'},
2283 DESCRIPTION => join( ' ',
2284 $self->loc('Time Worked'),
2285 $args{'OPERATOR'}, $args{'VALUE'}, ),
2291 # {{{ sub LimitTimeLeft
2293 =head2 LimitTimeLeft
2295 Takes a paramhash with the fields OPERATOR and VALUE.
2296 OPERATOR is one of =, >, < or !=.
2297 VALUE is a value to match the ticket's TimeLeft attribute
2305 FIELD => 'TimeLeft',
2306 VALUE => $args{'VALUE'},
2307 OPERATOR => $args{'OPERATOR'},
2308 DESCRIPTION => join( ' ',
2309 $self->loc('Time Left'),
2310 $args{'OPERATOR'}, $args{'VALUE'}, ),
2318 # {{{ Limiting based on attachment attributes
2320 # {{{ sub LimitContent
2324 Takes a paramhash with the fields OPERATOR and VALUE.
2325 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2326 VALUE is a string to search for in the body of the ticket
2335 VALUE => $args{'VALUE'},
2336 OPERATOR => $args{'OPERATOR'},
2337 DESCRIPTION => join( ' ',
2338 $self->loc('Ticket content'), $args{'OPERATOR'},
2345 # {{{ sub LimitFilename
2347 =head2 LimitFilename
2349 Takes a paramhash with the fields OPERATOR and VALUE.
2350 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2351 VALUE is a string to search for in the body of the ticket
2359 FIELD => 'Filename',
2360 VALUE => $args{'VALUE'},
2361 OPERATOR => $args{'OPERATOR'},
2362 DESCRIPTION => join( ' ',
2363 $self->loc('Attachment filename'), $args{'OPERATOR'},
2369 # {{{ sub LimitContentType
2371 =head2 LimitContentType
2373 Takes a paramhash with the fields OPERATOR and VALUE.
2374 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2375 VALUE is a content type to search ticket attachments for
2379 sub LimitContentType {
2383 FIELD => 'ContentType',
2384 VALUE => $args{'VALUE'},
2385 OPERATOR => $args{'OPERATOR'},
2386 DESCRIPTION => join( ' ',
2387 $self->loc('Ticket content type'), $args{'OPERATOR'},
2396 # {{{ Limiting based on people
2398 # {{{ sub LimitOwner
2402 Takes a paramhash with the fields OPERATOR and VALUE.
2403 OPERATOR is one of = or !=.
2415 my $owner = new RT::User( $self->CurrentUser );
2416 $owner->Load( $args{'VALUE'} );
2418 # FIXME: check for a valid $owner
2421 VALUE => $args{'VALUE'},
2422 OPERATOR => $args{'OPERATOR'},
2423 DESCRIPTION => join( ' ',
2424 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2431 # {{{ Limiting watchers
2433 # {{{ sub LimitWatcher
2437 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2438 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2439 VALUE is a value to match the ticket\'s watcher email addresses against
2440 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2454 #build us up a description
2455 my ( $watcher_type, $desc );
2456 if ( $args{'TYPE'} ) {
2457 $watcher_type = $args{'TYPE'};
2460 $watcher_type = "Watcher";
2464 FIELD => $watcher_type,
2465 VALUE => $args{'VALUE'},
2466 OPERATOR => $args{'OPERATOR'},
2467 TYPE => $args{'TYPE'},
2468 DESCRIPTION => join( ' ',
2469 $self->loc($watcher_type),
2470 $args{'OPERATOR'}, $args{'VALUE'}, ),
2480 # {{{ Limiting based on links
2484 =head2 LimitLinkedTo
2486 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2487 TYPE limits the sort of link we want to search on
2489 TYPE = { RefersTo, MemberOf, DependsOn }
2491 TARGET is the id or URI of the TARGET of the link
2505 FIELD => 'LinkedTo',
2507 TARGET => $args{'TARGET'},
2508 TYPE => $args{'TYPE'},
2509 DESCRIPTION => $self->loc(
2510 "Tickets [_1] by [_2]",
2511 $self->loc( $args{'TYPE'} ),
2514 OPERATOR => $args{'OPERATOR'},
2520 # {{{ LimitLinkedFrom
2522 =head2 LimitLinkedFrom
2524 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2525 TYPE limits the sort of link we want to search on
2528 BASE is the id or URI of the BASE of the link
2532 sub LimitLinkedFrom {
2541 # translate RT2 From/To naming to RT3 TicketSQL naming
2542 my %fromToMap = qw(DependsOn DependentOn
2544 RefersTo ReferredToBy);
2546 my $type = $args{'TYPE'};
2547 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2550 FIELD => 'LinkedTo',
2552 BASE => $args{'BASE'},
2554 DESCRIPTION => $self->loc(
2555 "Tickets [_1] [_2]",
2556 $self->loc( $args{'TYPE'} ),
2559 OPERATOR => $args{'OPERATOR'},
2568 my $ticket_id = shift;
2569 return $self->LimitLinkedTo(
2571 TARGET => $ticket_id,
2578 # {{{ LimitHasMember
2579 sub LimitHasMember {
2581 my $ticket_id = shift;
2582 return $self->LimitLinkedFrom(
2584 BASE => "$ticket_id",
2585 TYPE => 'HasMember',
2592 # {{{ LimitDependsOn
2594 sub LimitDependsOn {
2596 my $ticket_id = shift;
2597 return $self->LimitLinkedTo(
2599 TARGET => $ticket_id,
2600 TYPE => 'DependsOn',
2607 # {{{ LimitDependedOnBy
2609 sub LimitDependedOnBy {
2611 my $ticket_id = shift;
2612 return $self->LimitLinkedFrom(
2615 TYPE => 'DependentOn',
2626 my $ticket_id = shift;
2627 return $self->LimitLinkedTo(
2629 TARGET => $ticket_id,
2637 # {{{ LimitReferredToBy
2639 sub LimitReferredToBy {
2641 my $ticket_id = shift;
2642 return $self->LimitLinkedFrom(
2645 TYPE => 'ReferredToBy',
2653 # {{{ limit based on ticket date attribtes
2657 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2659 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2661 OPERATOR is one of > or <
2662 VALUE is a date and time in ISO format in GMT
2663 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2665 There are also helper functions of the form LimitFIELD that eliminate
2666 the need to pass in a FIELD argument.
2680 #Set the description if we didn't get handed it above
2681 unless ( $args{'DESCRIPTION'} ) {
2682 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2683 . $args{'OPERATOR'} . " "
2684 . $args{'VALUE'} . " GMT";
2687 $self->Limit(%args);
2695 $self->LimitDate( FIELD => 'Created', @_ );
2700 $self->LimitDate( FIELD => 'Due', @_ );
2706 $self->LimitDate( FIELD => 'Starts', @_ );
2712 $self->LimitDate( FIELD => 'Started', @_ );
2717 $self->LimitDate( FIELD => 'Resolved', @_ );
2722 $self->LimitDate( FIELD => 'Told', @_ );
2725 sub LimitLastUpdated {
2727 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2731 # {{{ sub LimitTransactionDate
2733 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2735 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2737 OPERATOR is one of > or <
2738 VALUE is a date and time in ISO format in GMT
2743 sub LimitTransactionDate {
2746 FIELD => 'TransactionDate',
2753 # <20021217042756.GK28744@pallas.fsck.com>
2754 # "Kill It" - Jesse.
2756 #Set the description if we didn't get handed it above
2757 unless ( $args{'DESCRIPTION'} ) {
2758 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2759 . $args{'OPERATOR'} . " "
2760 . $args{'VALUE'} . " GMT";
2763 $self->Limit(%args);
2771 # {{{ Limit based on custom fields
2772 # {{{ sub LimitCustomField
2774 =head2 LimitCustomField
2776 Takes a paramhash of key/value pairs with the following keys:
2780 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2782 =item OPERATOR - The usual Limit operators
2784 =item VALUE - The value to compare against
2790 sub LimitCustomField {
2794 CUSTOMFIELD => undef,
2796 DESCRIPTION => undef,
2797 FIELD => 'CustomFieldValue',
2802 my $CF = RT::CustomField->new( $self->CurrentUser );
2803 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2804 $CF->Load( $args{CUSTOMFIELD} );
2807 $CF->LoadByNameAndQueue(
2808 Name => $args{CUSTOMFIELD},
2809 Queue => $args{QUEUE}
2811 $args{CUSTOMFIELD} = $CF->Id;
2814 # Handle special customfields types
2815 if ($CF->Type eq 'Date') {
2816 $args{FIELD} = 'DateCustomFieldValue';
2819 #If we are looking to compare with a null value.
2820 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2821 $args{'DESCRIPTION'}
2822 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2824 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2825 $args{'DESCRIPTION'}
2826 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2829 # if we're not looking to compare with a null value
2831 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2832 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2835 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2836 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2837 $QueueObj->Load( $args{'QUEUE'} );
2838 $args{'QUEUE'} = $QueueObj->Id;
2840 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2843 @rest = ( ENTRYAGGREGATOR => 'AND' )
2844 if ( $CF->Type eq 'SelectMultiple' );
2847 VALUE => $args{VALUE},
2849 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2850 .".{" . $CF->Name . "}",
2851 OPERATOR => $args{OPERATOR},
2856 $self->{'RecalcTicketLimits'} = 1;
2862 # {{{ sub _NextIndex
2866 Keep track of the counter for the array of restrictions
2872 return ( $self->{'restriction_index'}++ );
2879 # {{{ Core bits to make this a DBIx::SearchBuilder object
2884 $self->{'table'} = "Tickets";
2885 $self->{'RecalcTicketLimits'} = 1;
2886 $self->{'looking_at_effective_id'} = 0;
2887 $self->{'looking_at_type'} = 0;
2888 $self->{'restriction_index'} = 1;
2889 $self->{'primary_key'} = "id";
2890 delete $self->{'items_array'};
2891 delete $self->{'item_map'};
2892 delete $self->{'columns_to_display'};
2893 $self->SUPER::_Init(@_);
2904 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2905 return ( $self->SUPER::Count() );
2913 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2914 return ( $self->SUPER::CountAll() );
2919 # {{{ sub ItemsArrayRef
2921 =head2 ItemsArrayRef
2923 Returns a reference to the set of all items found in this search
2930 return $self->{'items_array'} if $self->{'items_array'};
2932 my $placeholder = $self->_ItemsCounter;
2933 $self->GotoFirstItem();
2934 while ( my $item = $self->Next ) {
2935 push( @{ $self->{'items_array'} }, $item );
2937 $self->GotoItem($placeholder);
2938 $self->{'items_array'}
2939 = $self->ItemsOrderBy( $self->{'items_array'} );
2941 return $self->{'items_array'};
2944 sub ItemsArrayRefWindow {
2948 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2950 $self->RowsPerPage( $window );
2952 $self->GotoFirstItem;
2955 while ( my $item = $self->Next ) {
2959 $self->RowsPerPage( $old[1] );
2960 $self->FirstRow( $old[2] );
2961 $self->GotoItem( $old[0] );
2972 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2974 my $Ticket = $self->SUPER::Next;
2975 return $Ticket unless $Ticket;
2977 if ( $Ticket->__Value('Status') eq 'deleted'
2978 && !$self->{'allow_deleted_search'} )
2982 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2983 # if we found a ticket with this option enabled then
2984 # all tickets we found are ACLed, cache this fact
2985 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2986 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2989 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2994 # If the user doesn't have the right to show this ticket
3001 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3002 return $self->SUPER::_DoSearch( @_ );
3007 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3008 return $self->SUPER::_DoCount( @_ );
3014 my $cache_key = 'RolesHasRight;:;ShowTicket';
3016 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3020 my $ACL = RT::ACL->new( $RT::SystemUser );
3021 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3022 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3023 my $principal_alias = $ACL->Join(
3025 FIELD1 => 'PrincipalId',
3026 TABLE2 => 'Principals',
3029 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3032 while ( my $ACE = $ACL->Next ) {
3033 my $role = $ACE->PrincipalType;
3034 my $type = $ACE->ObjectType;
3035 if ( $type eq 'RT::System' ) {
3038 elsif ( $type eq 'RT::Queue' ) {
3039 next if $res{ $role } && !ref $res{ $role };
3040 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3043 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3046 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3050 sub _DirectlyCanSeeIn {
3052 my $id = $self->CurrentUser->id;
3054 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3055 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3059 my $ACL = RT::ACL->new( $RT::SystemUser );
3060 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3061 my $principal_alias = $ACL->Join(
3063 FIELD1 => 'PrincipalId',
3064 TABLE2 => 'Principals',
3067 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3068 my $cgm_alias = $ACL->Join(
3070 FIELD1 => 'PrincipalId',
3071 TABLE2 => 'CachedGroupMembers',
3072 FIELD2 => 'GroupId',
3074 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3075 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3078 while ( my $ACE = $ACL->Next ) {
3079 my $type = $ACE->ObjectType;
3080 if ( $type eq 'RT::System' ) {
3081 # If user is direct member of a group that has the right
3082 # on the system then he can see any ticket
3083 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3086 elsif ( $type eq 'RT::Queue' ) {
3087 push @res, $ACE->ObjectId;
3090 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3093 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3097 sub CurrentUserCanSee {
3099 return if $self->{'_sql_current_user_can_see_applied'};
3101 return $self->{'_sql_current_user_can_see_applied'} = 1
3102 if $self->CurrentUser->UserObj->HasRight(
3103 Right => 'SuperUser', Object => $RT::System
3106 my $id = $self->CurrentUser->id;
3108 # directly can see in all queues then we have nothing to do
3109 my @direct_queues = $self->_DirectlyCanSeeIn;
3110 return $self->{'_sql_current_user_can_see_applied'} = 1
3111 if @direct_queues && $direct_queues[0] == -1;
3113 my %roles = $self->_RolesCanSee;
3115 my %skip = map { $_ => 1 } @direct_queues;
3116 foreach my $role ( keys %roles ) {
3117 next unless ref $roles{ $role };
3119 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3121 $roles{ $role } = \@queues;
3123 delete $roles{ $role };
3128 # there is no global watchers, only queues and tickes, if at
3129 # some point we will add global roles then it's gonna blow
3130 # the idea here is that if the right is set globaly for a role
3131 # and user plays this role for a queue directly not a ticket
3132 # then we have to check in advance
3133 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3135 my $groups = RT::Groups->new( $RT::SystemUser );
3136 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3138 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3140 my $principal_alias = $groups->Join(
3143 TABLE2 => 'Principals',
3146 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3147 my $cgm_alias = $groups->Join(
3150 TABLE2 => 'CachedGroupMembers',
3151 FIELD2 => 'GroupId',
3153 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3154 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3155 while ( my $group = $groups->Next ) {
3156 push @direct_queues, $group->Instance;
3160 unless ( @direct_queues || keys %roles ) {
3161 $self->SUPER::Limit(
3166 ENTRYAGGREGATOR => 'AND',
3168 return $self->{'_sql_current_user_can_see_applied'} = 1;
3172 my $join_roles = keys %roles;
3173 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3174 my ($role_group_alias, $cgm_alias);
3175 if ( $join_roles ) {
3176 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3177 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3178 $self->SUPER::Limit(
3179 LEFTJOIN => $cgm_alias,
3180 FIELD => 'MemberId',
3185 my $limit_queues = sub {
3189 return unless @queues;
3190 if ( @queues == 1 ) {
3191 $self->SUPER::Limit(
3196 ENTRYAGGREGATOR => $ea,
3199 $self->SUPER::_OpenParen('ACL');
3200 foreach my $q ( @queues ) {
3201 $self->SUPER::Limit(
3206 ENTRYAGGREGATOR => $ea,
3210 $self->SUPER::_CloseParen('ACL');
3215 $self->SUPER::_OpenParen('ACL');
3217 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3218 while ( my ($role, $queues) = each %roles ) {
3219 $self->SUPER::_OpenParen('ACL');
3220 if ( $role eq 'Owner' ) {
3221 $self->SUPER::Limit(
3225 ENTRYAGGREGATOR => $ea,
3229 $self->SUPER::Limit(
3231 ALIAS => $cgm_alias,
3232 FIELD => 'MemberId',
3233 OPERATOR => 'IS NOT',
3236 ENTRYAGGREGATOR => $ea,
3238 $self->SUPER::Limit(
3240 ALIAS => $role_group_alias,
3243 ENTRYAGGREGATOR => 'AND',
3246 $limit_queues->( 'AND', @$queues ) if ref $queues;
3247 $ea = 'OR' if $ea eq 'AND';
3248 $self->SUPER::_CloseParen('ACL');
3250 $self->SUPER::_CloseParen('ACL');
3252 return $self->{'_sql_current_user_can_see_applied'} = 1;
3259 # {{{ Deal with storing and restoring restrictions
3261 # {{{ sub LoadRestrictions
3263 =head2 LoadRestrictions
3265 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3266 TODO It is not yet implemented
3272 # {{{ sub DescribeRestrictions
3274 =head2 DescribeRestrictions
3277 Returns a hash keyed by restriction id.
3278 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3279 is a description of the purpose of that TicketRestriction
3283 sub DescribeRestrictions {
3286 my ( $row, %listing );
3288 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3289 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3296 # {{{ sub RestrictionValues
3298 =head2 RestrictionValues FIELD
3300 Takes a restriction field and returns a list of values this field is restricted
3305 sub RestrictionValues {
3308 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3309 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3310 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3312 keys %{ $self->{'TicketRestrictions'} };
3317 # {{{ sub ClearRestrictions
3319 =head2 ClearRestrictions
3321 Removes all restrictions irretrievably
3325 sub ClearRestrictions {
3327 delete $self->{'TicketRestrictions'};
3328 $self->{'looking_at_effective_id'} = 0;
3329 $self->{'looking_at_type'} = 0;
3330 $self->{'RecalcTicketLimits'} = 1;
3335 # {{{ sub DeleteRestriction
3337 =head2 DeleteRestriction
3339 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3340 Removes that restriction from the session's limits.
3344 sub DeleteRestriction {
3347 delete $self->{'TicketRestrictions'}{$row};
3349 $self->{'RecalcTicketLimits'} = 1;
3351 #make the underlying easysearch object forget all its preconceptions
3356 # {{{ sub _RestrictionsToClauses
3358 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3360 sub _RestrictionsToClauses {
3365 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3366 my $restriction = $self->{'TicketRestrictions'}{$row};
3368 # We need to reimplement the subclause aggregation that SearchBuilder does.
3369 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3370 # Then SB AND's the different Subclauses together.
3372 # So, we want to group things into Subclauses, convert them to
3373 # SQL, and then join them with the appropriate DefaultEA.
3374 # Then join each subclause group with AND.
3376 my $field = $restriction->{'FIELD'};
3377 my $realfield = $field; # CustomFields fake up a fieldname, so
3378 # we need to figure that out
3381 # Rewrite LinkedTo meta field to the real field
3382 if ( $field =~ /LinkedTo/ ) {
3383 $realfield = $field = $restriction->{'TYPE'};
3387 # Handle subkey fields with a different real field
3388 if ( $field =~ /^(\w+)\./ ) {
3392 die "I don't know about $field yet"
3393 unless ( exists $FIELD_METADATA{$realfield}
3394 or $restriction->{CUSTOMFIELD} );
3396 my $type = $FIELD_METADATA{$realfield}->[0];
3397 my $op = $restriction->{'OPERATOR'};
3401 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3404 # this performs the moral equivalent of defined or/dor/C<//>,
3405 # without the short circuiting.You need to use a 'defined or'
3406 # type thing instead of just checking for truth values, because
3407 # VALUE could be 0.(i.e. "false")
3409 # You could also use this, but I find it less aesthetic:
3410 # (although it does short circuit)
3411 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3412 # defined $restriction->{'TICKET'} ?
3413 # $restriction->{TICKET} :
3414 # defined $restriction->{'BASE'} ?
3415 # $restriction->{BASE} :
3416 # defined $restriction->{'TARGET'} ?
3417 # $restriction->{TARGET} )
3419 my $ea = $restriction->{ENTRYAGGREGATOR}
3420 || $DefaultEA{$type}
3423 die "Invalid operator $op for $field ($type)"
3424 unless exists $ea->{$op};
3428 # Each CustomField should be put into a different Clause so they
3429 # are ANDed together.
3430 if ( $restriction->{CUSTOMFIELD} ) {
3431 $realfield = $field;
3434 exists $clause{$realfield} or $clause{$realfield} = [];
3437 $field =~ s!(['"])!\\$1!g;
3438 $value =~ s!(['"])!\\$1!g;
3439 my $data = [ $ea, $type, $field, $op, $value ];
3441 # here is where we store extra data, say if it's a keyword or
3442 # something. (I.e. "TYPE SPECIFIC STUFF")
3444 push @{ $clause{$realfield} }, $data;
3451 # {{{ sub _ProcessRestrictions
3453 =head2 _ProcessRestrictions PARAMHASH
3455 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3456 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3460 sub _ProcessRestrictions {
3463 #Blow away ticket aliases since we'll need to regenerate them for
3465 delete $self->{'TicketAliases'};
3466 delete $self->{'items_array'};
3467 delete $self->{'item_map'};
3468 delete $self->{'raw_rows'};
3469 delete $self->{'rows'};
3470 delete $self->{'count_all'};
3472 my $sql = $self->Query; # Violating the _SQL namespace
3473 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3475 # "Restrictions to Clauses Branch\n";
3476 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3478 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3482 $sql = $self->ClausesToSQL($clauseRef);
3483 $self->FromSQL($sql) if $sql;
3487 $self->{'RecalcTicketLimits'} = 0;
3491 =head2 _BuildItemMap
3493 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3494 display search nav quickly.
3501 my $window = RT->Config->Get('TicketsItemMapSize');
3503 $self->{'item_map'} = {};
3505 my $items = $self->ItemsArrayRefWindow( $window );
3506 return unless $items && @$items;
3509 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3510 for ( my $i = 0; $i < @$items; $i++ ) {
3511 my $item = $items->[$i];
3512 my $id = $item->EffectiveId;
3513 $self->{'item_map'}{$id}{'defined'} = 1;
3514 $self->{'item_map'}{$id}{'prev'} = $prev;
3515 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3519 $self->{'item_map'}{'last'} = $prev
3520 if !$window || @$items < $window;
3525 Returns an a map of all items found by this search. The map is a hash
3529 first => <first ticket id found>,
3530 last => <last ticket id found or undef>,
3533 prev => <the ticket id found before>,
3534 next => <the ticket id found after>,
3546 $self->_BuildItemMap unless $self->{'item_map'};
3547 return $self->{'item_map'};
3555 =head2 PrepForSerialization
3557 You don't want to serialize a big tickets object, as
3558 the {items} hash will be instantly invalid _and_ eat
3563 sub PrepForSerialization {
3565 delete $self->{'items'};
3566 delete $self->{'items_array'};
3567 $self->RedoSearch();
3572 RT::Tickets supports several flags which alter search behavior:
3575 allow_deleted_search (Otherwise never show deleted tickets in search results)
3576 looking_at_type (otherwise limit to type=ticket)
3578 These flags are set by calling
3580 $tickets->{'flagname'} = 1;
3582 BUG: There should be an API for this