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 # If we're trying to find custom fields that don't match something, we
1376 # want tickets where the custom field has no value at all. Note that
1377 # we explicitly don't include the "IS NULL" case, since we would
1378 # otherwise end up with a redundant clause.
1380 my ($negative_op, $null_op, $inv_op, $range_op)
1381 = $self->ClassifySQLOperation( $op );
1385 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1386 return 'MATCHES' if $op eq '=';
1387 return 'NOT MATCHES' if $op eq '!=';
1391 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1393 my $cfkey = $cfid ? $cfid : "$queue.$field";
1395 if ( $null_op && !$column ) {
1396 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1397 # we can reuse our default joins for this operation
1398 # with column specified we have different situation
1399 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1402 ALIAS => $TicketCFs,
1411 OPERATOR => 'IS NOT',
1414 ENTRYAGGREGATOR => 'AND',
1418 elsif ( !$negative_op || $single_value ) {
1419 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1420 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1427 # if column is defined then deal only with it
1428 # otherwise search in Content and in LargeContent
1431 ALIAS => $TicketCFs,
1433 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1438 elsif ( $cf->Type eq 'Date' ) {
1439 $self->_DateFieldLimit(
1443 ALIAS => $TicketCFs,
1447 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1448 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1450 ALIAS => $TicketCFs,
1459 ALIAS => $TicketCFs,
1463 ENTRYAGGREGATOR => 'OR'
1466 ALIAS => $TicketCFs,
1470 ENTRYAGGREGATOR => 'OR'
1474 ALIAS => $TicketCFs,
1475 FIELD => 'LargeContent',
1476 OPERATOR => $fix_op->($op),
1478 ENTRYAGGREGATOR => 'AND',
1484 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 # XXX: if we join via CustomFields table then
1520 # because of order of left joins we get NULLs in
1521 # CF table and then get nulls for those records
1522 # in OCFVs table what result in wrong results
1523 # as decifer method now tries to load a CF then
1524 # we fall into this situation only when there
1525 # are more than one CF with the name in the DB.
1526 # the same thing applies to order by call.
1527 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1528 # we want treat IS NULL as (not applies or has
1533 OPERATOR => 'IS NOT',
1536 ENTRYAGGREGATOR => 'AND',
1542 ALIAS => $TicketCFs,
1543 FIELD => $column || 'Content',
1547 ENTRYAGGREGATOR => 'OR',
1554 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1555 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1558 $op =~ s/!|NOT\s+//i;
1560 # if column is defined then deal only with it
1561 # otherwise search in Content and in LargeContent
1563 $self->SUPER::Limit(
1564 LEFTJOIN => $TicketCFs,
1565 ALIAS => $TicketCFs,
1567 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1572 $self->SUPER::Limit(
1573 LEFTJOIN => $TicketCFs,
1574 ALIAS => $TicketCFs,
1582 ALIAS => $TicketCFs,
1591 sub _HasAttributeLimit {
1592 my ( $self, $field, $op, $value, %rest ) = @_;
1594 my $alias = $self->Join(
1598 TABLE2 => 'Attributes',
1599 FIELD2 => 'ObjectId',
1601 $self->SUPER::Limit(
1603 FIELD => 'ObjectType',
1604 VALUE => 'RT::Ticket',
1605 ENTRYAGGREGATOR => 'AND'
1607 $self->SUPER::Limit(
1612 ENTRYAGGREGATOR => 'AND'
1618 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1624 # End Helper Functions
1626 # End of SQL Stuff -------------------------------------------------
1628 # {{{ Allow sorting on watchers
1630 =head2 OrderByCols ARRAY
1632 A modified version of the OrderBy method which automatically joins where
1633 C<ALIAS> is set to the name of a watcher type.
1644 foreach my $row (@args) {
1645 if ( $row->{ALIAS} ) {
1649 if ( $row->{FIELD} !~ /\./ ) {
1650 my $meta = $self->FIELDS->{ $row->{FIELD} };
1656 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1657 my $alias = $self->Join(
1660 FIELD1 => $row->{'FIELD'},
1664 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1665 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1666 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1668 my $alias = $self->Join(
1671 FIELD1 => $row->{'FIELD'},
1675 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1682 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1683 my $meta = $self->FIELDS->{$field};
1684 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1685 # cache alias as we want to use one alias per watcher type for sorting
1686 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1688 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1689 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1691 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1692 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1693 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1694 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1695 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1696 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1697 # this is described in _CustomFieldLimit
1701 OPERATOR => 'IS NOT',
1704 ENTRYAGGREGATOR => 'AND',
1707 # For those cases where we are doing a join against the
1708 # CF name, and don't have a CFid, use Unique to make sure
1709 # we don't show duplicate tickets. NOTE: I'm pretty sure
1710 # this will stay mixed in for the life of the
1711 # class/package, and not just for the life of the object.
1712 # Potential performance issue.
1713 require DBIx::SearchBuilder::Unique;
1714 DBIx::SearchBuilder::Unique->import;
1716 my $CFvs = $self->Join(
1718 ALIAS1 => $TicketCFs,
1719 FIELD1 => 'CustomField',
1720 TABLE2 => 'CustomFieldValues',
1721 FIELD2 => 'CustomField',
1723 $self->SUPER::Limit(
1727 VALUE => $TicketCFs . ".Content",
1728 ENTRYAGGREGATOR => 'AND'
1731 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1732 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1733 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1734 # PAW logic is "reversed"
1736 if (exists $row->{ORDER} ) {
1737 my $o = $row->{ORDER};
1738 delete $row->{ORDER};
1739 $order = "DESC" if $o =~ /asc/i;
1742 # Ticket.Owner 1 0 X
1743 # Unowned Tickets 0 1 X
1746 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1747 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1748 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1749 push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
1751 push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
1755 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1757 } elsif ( $field eq 'Customer' ) { #Freeside
1758 if ( $subkey eq 'Number' ) {
1759 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1762 FIELD => $custnum_sql,
1766 my $custalias = $self->JoinToCustomer;
1768 if ( $subkey eq 'Name' ) {
1769 $field = "COALESCE( $custalias.company,
1770 $custalias.last || ', ' || $custalias.first
1774 # no other cases exist yet, but for obviousness:
1777 push @res, { %$row, ALIAS => '', FIELD => $field };
1786 return $self->SUPER::OrderByCols(@res);
1791 sub JoinToCustLinks {
1792 # Set up join to links (id = localbase),
1793 # limit link type to 'MemberOf',
1794 # and target value to any Freeside custnum URI.
1795 # Return the linkalias for further join/limit action,
1796 # and an sql expression to retrieve the custnum.
1798 my $linkalias = $self->Join(
1803 FIELD2 => 'LocalBase',
1806 $self->SUPER::Limit(
1807 LEFTJOIN => $linkalias,
1810 VALUE => 'MemberOf',
1812 $self->SUPER::Limit(
1813 LEFTJOIN => $linkalias,
1815 OPERATOR => 'STARTSWITH',
1816 VALUE => 'freeside://freeside/cust_main/',
1818 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1819 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1820 $custnum_sql .= 'SIGNED INTEGER)';
1823 $custnum_sql .= 'INTEGER)';
1825 return ($linkalias, $custnum_sql);
1828 sub JoinToCustomer {
1830 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1832 my $custalias = $self->Join(
1834 EXPRESSION => $custnum_sql,
1835 TABLE2 => 'cust_main',
1836 FIELD2 => 'custnum',
1841 sub _FreesideFieldLimit {
1842 my ( $self, $field, $op, $value, %rest ) = @_;
1843 my $alias = $self->JoinToCustomer;
1844 my $is_negative = 0;
1845 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1846 # if the op is negative, do the join as though
1847 # the op were positive, then accept only records
1848 # where the right-side join key is null.
1850 $op = '=' if $op eq '!=';
1853 my $meta = $FIELD_METADATA{$field};
1855 $alias = $self->Join(
1858 FIELD1 => 'custnum',
1859 TABLE2 => $meta->[1],
1860 FIELD2 => 'custnum',
1864 $self->SUPER::Limit(
1866 FIELD => lc($field),
1869 ENTRYAGGREGATOR => 'AND',
1874 FIELD => lc($field),
1875 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1885 # {{{ Limit the result set based on content
1891 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1892 Generally best called from LimitFoo methods
1902 DESCRIPTION => undef,
1905 $args{'DESCRIPTION'} = $self->loc(
1906 "[_1] [_2] [_3]", $args{'FIELD'},
1907 $args{'OPERATOR'}, $args{'VALUE'}
1909 if ( !defined $args{'DESCRIPTION'} );
1911 my $index = $self->_NextIndex;
1913 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1915 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1917 $self->{'RecalcTicketLimits'} = 1;
1919 # If we're looking at the effective id, we don't want to append the other clause
1920 # which limits us to tickets where id = effective id
1921 if ( $args{'FIELD'} eq 'EffectiveId'
1922 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1924 $self->{'looking_at_effective_id'} = 1;
1927 if ( $args{'FIELD'} eq 'Type'
1928 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1930 $self->{'looking_at_type'} = 1;
1940 Returns a frozen string suitable for handing back to ThawLimits.
1944 sub _FreezeThawKeys {
1945 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1949 # {{{ sub FreezeLimits
1954 require MIME::Base64;
1955 MIME::Base64::base64_encode(
1956 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1963 Take a frozen Limits string generated by FreezeLimits and make this tickets
1964 object have that set of limits.
1968 # {{{ sub ThawLimits
1974 #if we don't have $in, get outta here.
1975 return undef unless ($in);
1977 $self->{'RecalcTicketLimits'} = 1;
1980 require MIME::Base64;
1982 #We don't need to die if the thaw fails.
1983 @{$self}{ $self->_FreezeThawKeys }
1984 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1986 $RT::Logger->error($@) if $@;
1992 # {{{ Limit by enum or foreign key
1994 # {{{ sub LimitQueue
1998 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1999 OPERATOR is one of = or !=. (It defaults to =).
2000 VALUE is a queue id or Name.
2013 #TODO VALUE should also take queue objects
2014 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2015 my $queue = new RT::Queue( $self->CurrentUser );
2016 $queue->Load( $args{'VALUE'} );
2017 $args{'VALUE'} = $queue->Id;
2020 # What if they pass in an Id? Check for isNum() and convert to
2023 #TODO check for a valid queue here
2027 VALUE => $args{'VALUE'},
2028 OPERATOR => $args{'OPERATOR'},
2029 DESCRIPTION => join(
2030 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2038 # {{{ sub LimitStatus
2042 Takes a paramhash with the fields OPERATOR and VALUE.
2043 OPERATOR is one of = or !=.
2046 RT adds Status != 'deleted' until object has
2047 allow_deleted_search internal property set.
2048 $tickets->{'allow_deleted_search'} = 1;
2049 $tickets->LimitStatus( VALUE => 'deleted' );
2061 VALUE => $args{'VALUE'},
2062 OPERATOR => $args{'OPERATOR'},
2063 DESCRIPTION => join( ' ',
2064 $self->loc('Status'), $args{'OPERATOR'},
2065 $self->loc( $args{'VALUE'} ) ),
2071 # {{{ sub IgnoreType
2075 If called, this search will not automatically limit the set of results found
2076 to tickets of type "Ticket". Tickets of other types, such as "project" and
2077 "approval" will be found.
2084 # Instead of faking a Limit that later gets ignored, fake up the
2085 # fact that we're already looking at type, so that the check in
2086 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2088 # $self->LimitType(VALUE => '__any');
2089 $self->{looking_at_type} = 1;
2098 Takes a paramhash with the fields OPERATOR and VALUE.
2099 OPERATOR is one of = or !=, it defaults to "=".
2100 VALUE is a string to search for in the type of the ticket.
2115 VALUE => $args{'VALUE'},
2116 OPERATOR => $args{'OPERATOR'},
2117 DESCRIPTION => join( ' ',
2118 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2126 # {{{ Limit by string field
2128 # {{{ sub LimitSubject
2132 Takes a paramhash with the fields OPERATOR and VALUE.
2133 OPERATOR is one of = or !=.
2134 VALUE is a string to search for in the subject of the ticket.
2143 VALUE => $args{'VALUE'},
2144 OPERATOR => $args{'OPERATOR'},
2145 DESCRIPTION => join( ' ',
2146 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2154 # {{{ Limit based on ticket numerical attributes
2155 # Things that can be > < = !=
2161 Takes a paramhash with the fields OPERATOR and VALUE.
2162 OPERATOR is one of =, >, < or !=.
2163 VALUE is a ticket Id to search for
2176 VALUE => $args{'VALUE'},
2177 OPERATOR => $args{'OPERATOR'},
2179 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2185 # {{{ sub LimitPriority
2187 =head2 LimitPriority
2189 Takes a paramhash with the fields OPERATOR and VALUE.
2190 OPERATOR is one of =, >, < or !=.
2191 VALUE is a value to match the ticket\'s priority against
2199 FIELD => 'Priority',
2200 VALUE => $args{'VALUE'},
2201 OPERATOR => $args{'OPERATOR'},
2202 DESCRIPTION => join( ' ',
2203 $self->loc('Priority'),
2204 $args{'OPERATOR'}, $args{'VALUE'}, ),
2210 # {{{ sub LimitInitialPriority
2212 =head2 LimitInitialPriority
2214 Takes a paramhash with the fields OPERATOR and VALUE.
2215 OPERATOR is one of =, >, < or !=.
2216 VALUE is a value to match the ticket\'s initial priority against
2221 sub LimitInitialPriority {
2225 FIELD => 'InitialPriority',
2226 VALUE => $args{'VALUE'},
2227 OPERATOR => $args{'OPERATOR'},
2228 DESCRIPTION => join( ' ',
2229 $self->loc('Initial Priority'), $args{'OPERATOR'},
2236 # {{{ sub LimitFinalPriority
2238 =head2 LimitFinalPriority
2240 Takes a paramhash with the fields OPERATOR and VALUE.
2241 OPERATOR is one of =, >, < or !=.
2242 VALUE is a value to match the ticket\'s final priority against
2246 sub LimitFinalPriority {
2250 FIELD => 'FinalPriority',
2251 VALUE => $args{'VALUE'},
2252 OPERATOR => $args{'OPERATOR'},
2253 DESCRIPTION => join( ' ',
2254 $self->loc('Final Priority'), $args{'OPERATOR'},
2261 # {{{ sub LimitTimeWorked
2263 =head2 LimitTimeWorked
2265 Takes a paramhash with the fields OPERATOR and VALUE.
2266 OPERATOR is one of =, >, < or !=.
2267 VALUE is a value to match the ticket's TimeWorked attribute
2271 sub LimitTimeWorked {
2275 FIELD => 'TimeWorked',
2276 VALUE => $args{'VALUE'},
2277 OPERATOR => $args{'OPERATOR'},
2278 DESCRIPTION => join( ' ',
2279 $self->loc('Time Worked'),
2280 $args{'OPERATOR'}, $args{'VALUE'}, ),
2286 # {{{ sub LimitTimeLeft
2288 =head2 LimitTimeLeft
2290 Takes a paramhash with the fields OPERATOR and VALUE.
2291 OPERATOR is one of =, >, < or !=.
2292 VALUE is a value to match the ticket's TimeLeft attribute
2300 FIELD => 'TimeLeft',
2301 VALUE => $args{'VALUE'},
2302 OPERATOR => $args{'OPERATOR'},
2303 DESCRIPTION => join( ' ',
2304 $self->loc('Time Left'),
2305 $args{'OPERATOR'}, $args{'VALUE'}, ),
2313 # {{{ Limiting based on attachment attributes
2315 # {{{ sub LimitContent
2319 Takes a paramhash with the fields OPERATOR and VALUE.
2320 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2321 VALUE is a string to search for in the body of the ticket
2330 VALUE => $args{'VALUE'},
2331 OPERATOR => $args{'OPERATOR'},
2332 DESCRIPTION => join( ' ',
2333 $self->loc('Ticket content'), $args{'OPERATOR'},
2340 # {{{ sub LimitFilename
2342 =head2 LimitFilename
2344 Takes a paramhash with the fields OPERATOR and VALUE.
2345 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2346 VALUE is a string to search for in the body of the ticket
2354 FIELD => 'Filename',
2355 VALUE => $args{'VALUE'},
2356 OPERATOR => $args{'OPERATOR'},
2357 DESCRIPTION => join( ' ',
2358 $self->loc('Attachment filename'), $args{'OPERATOR'},
2364 # {{{ sub LimitContentType
2366 =head2 LimitContentType
2368 Takes a paramhash with the fields OPERATOR and VALUE.
2369 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2370 VALUE is a content type to search ticket attachments for
2374 sub LimitContentType {
2378 FIELD => 'ContentType',
2379 VALUE => $args{'VALUE'},
2380 OPERATOR => $args{'OPERATOR'},
2381 DESCRIPTION => join( ' ',
2382 $self->loc('Ticket content type'), $args{'OPERATOR'},
2391 # {{{ Limiting based on people
2393 # {{{ sub LimitOwner
2397 Takes a paramhash with the fields OPERATOR and VALUE.
2398 OPERATOR is one of = or !=.
2410 my $owner = new RT::User( $self->CurrentUser );
2411 $owner->Load( $args{'VALUE'} );
2413 # FIXME: check for a valid $owner
2416 VALUE => $args{'VALUE'},
2417 OPERATOR => $args{'OPERATOR'},
2418 DESCRIPTION => join( ' ',
2419 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2426 # {{{ Limiting watchers
2428 # {{{ sub LimitWatcher
2432 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2433 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2434 VALUE is a value to match the ticket\'s watcher email addresses against
2435 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2449 #build us up a description
2450 my ( $watcher_type, $desc );
2451 if ( $args{'TYPE'} ) {
2452 $watcher_type = $args{'TYPE'};
2455 $watcher_type = "Watcher";
2459 FIELD => $watcher_type,
2460 VALUE => $args{'VALUE'},
2461 OPERATOR => $args{'OPERATOR'},
2462 TYPE => $args{'TYPE'},
2463 DESCRIPTION => join( ' ',
2464 $self->loc($watcher_type),
2465 $args{'OPERATOR'}, $args{'VALUE'}, ),
2475 # {{{ Limiting based on links
2479 =head2 LimitLinkedTo
2481 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2482 TYPE limits the sort of link we want to search on
2484 TYPE = { RefersTo, MemberOf, DependsOn }
2486 TARGET is the id or URI of the TARGET of the link
2500 FIELD => 'LinkedTo',
2502 TARGET => $args{'TARGET'},
2503 TYPE => $args{'TYPE'},
2504 DESCRIPTION => $self->loc(
2505 "Tickets [_1] by [_2]",
2506 $self->loc( $args{'TYPE'} ),
2509 OPERATOR => $args{'OPERATOR'},
2515 # {{{ LimitLinkedFrom
2517 =head2 LimitLinkedFrom
2519 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2520 TYPE limits the sort of link we want to search on
2523 BASE is the id or URI of the BASE of the link
2527 sub LimitLinkedFrom {
2536 # translate RT2 From/To naming to RT3 TicketSQL naming
2537 my %fromToMap = qw(DependsOn DependentOn
2539 RefersTo ReferredToBy);
2541 my $type = $args{'TYPE'};
2542 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2545 FIELD => 'LinkedTo',
2547 BASE => $args{'BASE'},
2549 DESCRIPTION => $self->loc(
2550 "Tickets [_1] [_2]",
2551 $self->loc( $args{'TYPE'} ),
2554 OPERATOR => $args{'OPERATOR'},
2563 my $ticket_id = shift;
2564 return $self->LimitLinkedTo(
2566 TARGET => $ticket_id,
2573 # {{{ LimitHasMember
2574 sub LimitHasMember {
2576 my $ticket_id = shift;
2577 return $self->LimitLinkedFrom(
2579 BASE => "$ticket_id",
2580 TYPE => 'HasMember',
2587 # {{{ LimitDependsOn
2589 sub LimitDependsOn {
2591 my $ticket_id = shift;
2592 return $self->LimitLinkedTo(
2594 TARGET => $ticket_id,
2595 TYPE => 'DependsOn',
2602 # {{{ LimitDependedOnBy
2604 sub LimitDependedOnBy {
2606 my $ticket_id = shift;
2607 return $self->LimitLinkedFrom(
2610 TYPE => 'DependentOn',
2621 my $ticket_id = shift;
2622 return $self->LimitLinkedTo(
2624 TARGET => $ticket_id,
2632 # {{{ LimitReferredToBy
2634 sub LimitReferredToBy {
2636 my $ticket_id = shift;
2637 return $self->LimitLinkedFrom(
2640 TYPE => 'ReferredToBy',
2648 # {{{ limit based on ticket date attribtes
2652 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2654 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2656 OPERATOR is one of > or <
2657 VALUE is a date and time in ISO format in GMT
2658 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2660 There are also helper functions of the form LimitFIELD that eliminate
2661 the need to pass in a FIELD argument.
2675 #Set the description if we didn't get handed it above
2676 unless ( $args{'DESCRIPTION'} ) {
2677 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2678 . $args{'OPERATOR'} . " "
2679 . $args{'VALUE'} . " GMT";
2682 $self->Limit(%args);
2690 $self->LimitDate( FIELD => 'Created', @_ );
2695 $self->LimitDate( FIELD => 'Due', @_ );
2701 $self->LimitDate( FIELD => 'Starts', @_ );
2707 $self->LimitDate( FIELD => 'Started', @_ );
2712 $self->LimitDate( FIELD => 'Resolved', @_ );
2717 $self->LimitDate( FIELD => 'Told', @_ );
2720 sub LimitLastUpdated {
2722 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2726 # {{{ sub LimitTransactionDate
2728 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2730 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2732 OPERATOR is one of > or <
2733 VALUE is a date and time in ISO format in GMT
2738 sub LimitTransactionDate {
2741 FIELD => 'TransactionDate',
2748 # <20021217042756.GK28744@pallas.fsck.com>
2749 # "Kill It" - Jesse.
2751 #Set the description if we didn't get handed it above
2752 unless ( $args{'DESCRIPTION'} ) {
2753 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2754 . $args{'OPERATOR'} . " "
2755 . $args{'VALUE'} . " GMT";
2758 $self->Limit(%args);
2766 # {{{ Limit based on custom fields
2767 # {{{ sub LimitCustomField
2769 =head2 LimitCustomField
2771 Takes a paramhash of key/value pairs with the following keys:
2775 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2777 =item OPERATOR - The usual Limit operators
2779 =item VALUE - The value to compare against
2785 sub LimitCustomField {
2789 CUSTOMFIELD => undef,
2791 DESCRIPTION => undef,
2792 FIELD => 'CustomFieldValue',
2797 my $CF = RT::CustomField->new( $self->CurrentUser );
2798 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2799 $CF->Load( $args{CUSTOMFIELD} );
2802 $CF->LoadByNameAndQueue(
2803 Name => $args{CUSTOMFIELD},
2804 Queue => $args{QUEUE}
2806 $args{CUSTOMFIELD} = $CF->Id;
2809 #If we are looking to compare with a null value.
2810 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2811 $args{'DESCRIPTION'}
2812 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2814 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2815 $args{'DESCRIPTION'}
2816 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2819 # if we're not looking to compare with a null value
2821 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2822 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2825 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2826 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2827 $QueueObj->Load( $args{'QUEUE'} );
2828 $args{'QUEUE'} = $QueueObj->Id;
2830 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2833 @rest = ( ENTRYAGGREGATOR => 'AND' )
2834 if ( $CF->Type eq 'SelectMultiple' );
2837 VALUE => $args{VALUE},
2839 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2840 .".{" . $CF->Name . "}",
2841 OPERATOR => $args{OPERATOR},
2846 $self->{'RecalcTicketLimits'} = 1;
2852 # {{{ sub _NextIndex
2856 Keep track of the counter for the array of restrictions
2862 return ( $self->{'restriction_index'}++ );
2869 # {{{ Core bits to make this a DBIx::SearchBuilder object
2874 $self->{'table'} = "Tickets";
2875 $self->{'RecalcTicketLimits'} = 1;
2876 $self->{'looking_at_effective_id'} = 0;
2877 $self->{'looking_at_type'} = 0;
2878 $self->{'restriction_index'} = 1;
2879 $self->{'primary_key'} = "id";
2880 delete $self->{'items_array'};
2881 delete $self->{'item_map'};
2882 delete $self->{'columns_to_display'};
2883 $self->SUPER::_Init(@_);
2894 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2895 return ( $self->SUPER::Count() );
2903 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2904 return ( $self->SUPER::CountAll() );
2909 # {{{ sub ItemsArrayRef
2911 =head2 ItemsArrayRef
2913 Returns a reference to the set of all items found in this search
2920 return $self->{'items_array'} if $self->{'items_array'};
2922 my $placeholder = $self->_ItemsCounter;
2923 $self->GotoFirstItem();
2924 while ( my $item = $self->Next ) {
2925 push( @{ $self->{'items_array'} }, $item );
2927 $self->GotoItem($placeholder);
2928 $self->{'items_array'}
2929 = $self->ItemsOrderBy( $self->{'items_array'} );
2931 return $self->{'items_array'};
2934 sub ItemsArrayRefWindow {
2938 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2940 $self->RowsPerPage( $window );
2942 $self->GotoFirstItem;
2945 while ( my $item = $self->Next ) {
2949 $self->RowsPerPage( $old[1] );
2950 $self->FirstRow( $old[2] );
2951 $self->GotoItem( $old[0] );
2962 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2964 my $Ticket = $self->SUPER::Next;
2965 return $Ticket unless $Ticket;
2967 if ( $Ticket->__Value('Status') eq 'deleted'
2968 && !$self->{'allow_deleted_search'} )
2972 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2973 # if we found a ticket with this option enabled then
2974 # all tickets we found are ACLed, cache this fact
2975 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2976 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2979 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2984 # If the user doesn't have the right to show this ticket
2991 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2992 return $self->SUPER::_DoSearch( @_ );
2997 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2998 return $self->SUPER::_DoCount( @_ );
3004 my $cache_key = 'RolesHasRight;:;ShowTicket';
3006 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3010 my $ACL = RT::ACL->new( $RT::SystemUser );
3011 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3012 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3013 my $principal_alias = $ACL->Join(
3015 FIELD1 => 'PrincipalId',
3016 TABLE2 => 'Principals',
3019 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3022 while ( my $ACE = $ACL->Next ) {
3023 my $role = $ACE->PrincipalType;
3024 my $type = $ACE->ObjectType;
3025 if ( $type eq 'RT::System' ) {
3028 elsif ( $type eq 'RT::Queue' ) {
3029 next if $res{ $role } && !ref $res{ $role };
3030 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3033 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3036 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3040 sub _DirectlyCanSeeIn {
3042 my $id = $self->CurrentUser->id;
3044 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3045 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3049 my $ACL = RT::ACL->new( $RT::SystemUser );
3050 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3051 my $principal_alias = $ACL->Join(
3053 FIELD1 => 'PrincipalId',
3054 TABLE2 => 'Principals',
3057 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3058 my $cgm_alias = $ACL->Join(
3060 FIELD1 => 'PrincipalId',
3061 TABLE2 => 'CachedGroupMembers',
3062 FIELD2 => 'GroupId',
3064 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3065 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3068 while ( my $ACE = $ACL->Next ) {
3069 my $type = $ACE->ObjectType;
3070 if ( $type eq 'RT::System' ) {
3071 # If user is direct member of a group that has the right
3072 # on the system then he can see any ticket
3073 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3076 elsif ( $type eq 'RT::Queue' ) {
3077 push @res, $ACE->ObjectId;
3080 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3083 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3087 sub CurrentUserCanSee {
3089 return if $self->{'_sql_current_user_can_see_applied'};
3091 return $self->{'_sql_current_user_can_see_applied'} = 1
3092 if $self->CurrentUser->UserObj->HasRight(
3093 Right => 'SuperUser', Object => $RT::System
3096 my $id = $self->CurrentUser->id;
3098 # directly can see in all queues then we have nothing to do
3099 my @direct_queues = $self->_DirectlyCanSeeIn;
3100 return $self->{'_sql_current_user_can_see_applied'} = 1
3101 if @direct_queues && $direct_queues[0] == -1;
3103 my %roles = $self->_RolesCanSee;
3105 my %skip = map { $_ => 1 } @direct_queues;
3106 foreach my $role ( keys %roles ) {
3107 next unless ref $roles{ $role };
3109 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3111 $roles{ $role } = \@queues;
3113 delete $roles{ $role };
3118 # there is no global watchers, only queues and tickes, if at
3119 # some point we will add global roles then it's gonna blow
3120 # the idea here is that if the right is set globaly for a role
3121 # and user plays this role for a queue directly not a ticket
3122 # then we have to check in advance
3123 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3125 my $groups = RT::Groups->new( $RT::SystemUser );
3126 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3128 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3130 my $principal_alias = $groups->Join(
3133 TABLE2 => 'Principals',
3136 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3137 my $cgm_alias = $groups->Join(
3140 TABLE2 => 'CachedGroupMembers',
3141 FIELD2 => 'GroupId',
3143 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3144 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3145 while ( my $group = $groups->Next ) {
3146 push @direct_queues, $group->Instance;
3150 unless ( @direct_queues || keys %roles ) {
3151 $self->SUPER::Limit(
3156 ENTRYAGGREGATOR => 'AND',
3158 return $self->{'_sql_current_user_can_see_applied'} = 1;
3162 my $join_roles = keys %roles;
3163 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3164 my ($role_group_alias, $cgm_alias);
3165 if ( $join_roles ) {
3166 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3167 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3168 $self->SUPER::Limit(
3169 LEFTJOIN => $cgm_alias,
3170 FIELD => 'MemberId',
3175 my $limit_queues = sub {
3179 return unless @queues;
3180 if ( @queues == 1 ) {
3181 $self->SUPER::Limit(
3186 ENTRYAGGREGATOR => $ea,
3189 $self->SUPER::_OpenParen('ACL');
3190 foreach my $q ( @queues ) {
3191 $self->SUPER::Limit(
3196 ENTRYAGGREGATOR => $ea,
3200 $self->SUPER::_CloseParen('ACL');
3205 $self->SUPER::_OpenParen('ACL');
3207 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3208 while ( my ($role, $queues) = each %roles ) {
3209 $self->SUPER::_OpenParen('ACL');
3210 if ( $role eq 'Owner' ) {
3211 $self->SUPER::Limit(
3215 ENTRYAGGREGATOR => $ea,
3219 $self->SUPER::Limit(
3221 ALIAS => $cgm_alias,
3222 FIELD => 'MemberId',
3223 OPERATOR => 'IS NOT',
3226 ENTRYAGGREGATOR => $ea,
3228 $self->SUPER::Limit(
3230 ALIAS => $role_group_alias,
3233 ENTRYAGGREGATOR => 'AND',
3236 $limit_queues->( 'AND', @$queues ) if ref $queues;
3237 $ea = 'OR' if $ea eq 'AND';
3238 $self->SUPER::_CloseParen('ACL');
3240 $self->SUPER::_CloseParen('ACL');
3242 return $self->{'_sql_current_user_can_see_applied'} = 1;
3249 # {{{ Deal with storing and restoring restrictions
3251 # {{{ sub LoadRestrictions
3253 =head2 LoadRestrictions
3255 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3256 TODO It is not yet implemented
3262 # {{{ sub DescribeRestrictions
3264 =head2 DescribeRestrictions
3267 Returns a hash keyed by restriction id.
3268 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3269 is a description of the purpose of that TicketRestriction
3273 sub DescribeRestrictions {
3276 my ( $row, %listing );
3278 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3279 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3286 # {{{ sub RestrictionValues
3288 =head2 RestrictionValues FIELD
3290 Takes a restriction field and returns a list of values this field is restricted
3295 sub RestrictionValues {
3298 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3299 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3300 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3302 keys %{ $self->{'TicketRestrictions'} };
3307 # {{{ sub ClearRestrictions
3309 =head2 ClearRestrictions
3311 Removes all restrictions irretrievably
3315 sub ClearRestrictions {
3317 delete $self->{'TicketRestrictions'};
3318 $self->{'looking_at_effective_id'} = 0;
3319 $self->{'looking_at_type'} = 0;
3320 $self->{'RecalcTicketLimits'} = 1;
3325 # {{{ sub DeleteRestriction
3327 =head2 DeleteRestriction
3329 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3330 Removes that restriction from the session's limits.
3334 sub DeleteRestriction {
3337 delete $self->{'TicketRestrictions'}{$row};
3339 $self->{'RecalcTicketLimits'} = 1;
3341 #make the underlying easysearch object forget all its preconceptions
3346 # {{{ sub _RestrictionsToClauses
3348 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3350 sub _RestrictionsToClauses {
3355 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3356 my $restriction = $self->{'TicketRestrictions'}{$row};
3358 # We need to reimplement the subclause aggregation that SearchBuilder does.
3359 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3360 # Then SB AND's the different Subclauses together.
3362 # So, we want to group things into Subclauses, convert them to
3363 # SQL, and then join them with the appropriate DefaultEA.
3364 # Then join each subclause group with AND.
3366 my $field = $restriction->{'FIELD'};
3367 my $realfield = $field; # CustomFields fake up a fieldname, so
3368 # we need to figure that out
3371 # Rewrite LinkedTo meta field to the real field
3372 if ( $field =~ /LinkedTo/ ) {
3373 $realfield = $field = $restriction->{'TYPE'};
3377 # Handle subkey fields with a different real field
3378 if ( $field =~ /^(\w+)\./ ) {
3382 die "I don't know about $field yet"
3383 unless ( exists $FIELD_METADATA{$realfield}
3384 or $restriction->{CUSTOMFIELD} );
3386 my $type = $FIELD_METADATA{$realfield}->[0];
3387 my $op = $restriction->{'OPERATOR'};
3391 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3394 # this performs the moral equivalent of defined or/dor/C<//>,
3395 # without the short circuiting.You need to use a 'defined or'
3396 # type thing instead of just checking for truth values, because
3397 # VALUE could be 0.(i.e. "false")
3399 # You could also use this, but I find it less aesthetic:
3400 # (although it does short circuit)
3401 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3402 # defined $restriction->{'TICKET'} ?
3403 # $restriction->{TICKET} :
3404 # defined $restriction->{'BASE'} ?
3405 # $restriction->{BASE} :
3406 # defined $restriction->{'TARGET'} ?
3407 # $restriction->{TARGET} )
3409 my $ea = $restriction->{ENTRYAGGREGATOR}
3410 || $DefaultEA{$type}
3413 die "Invalid operator $op for $field ($type)"
3414 unless exists $ea->{$op};
3418 # Each CustomField should be put into a different Clause so they
3419 # are ANDed together.
3420 if ( $restriction->{CUSTOMFIELD} ) {
3421 $realfield = $field;
3424 exists $clause{$realfield} or $clause{$realfield} = [];
3427 $field =~ s!(['"])!\\$1!g;
3428 $value =~ s!(['"])!\\$1!g;
3429 my $data = [ $ea, $type, $field, $op, $value ];
3431 # here is where we store extra data, say if it's a keyword or
3432 # something. (I.e. "TYPE SPECIFIC STUFF")
3434 push @{ $clause{$realfield} }, $data;
3441 # {{{ sub _ProcessRestrictions
3443 =head2 _ProcessRestrictions PARAMHASH
3445 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3446 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3450 sub _ProcessRestrictions {
3453 #Blow away ticket aliases since we'll need to regenerate them for
3455 delete $self->{'TicketAliases'};
3456 delete $self->{'items_array'};
3457 delete $self->{'item_map'};
3458 delete $self->{'raw_rows'};
3459 delete $self->{'rows'};
3460 delete $self->{'count_all'};
3462 my $sql = $self->Query; # Violating the _SQL namespace
3463 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3465 # "Restrictions to Clauses Branch\n";
3466 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3468 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3472 $sql = $self->ClausesToSQL($clauseRef);
3473 $self->FromSQL($sql) if $sql;
3477 $self->{'RecalcTicketLimits'} = 0;
3481 =head2 _BuildItemMap
3483 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3484 display search nav quickly.
3491 my $window = RT->Config->Get('TicketsItemMapSize');
3493 $self->{'item_map'} = {};
3495 my $items = $self->ItemsArrayRefWindow( $window );
3496 return unless $items && @$items;
3499 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3500 for ( my $i = 0; $i < @$items; $i++ ) {
3501 my $item = $items->[$i];
3502 my $id = $item->EffectiveId;
3503 $self->{'item_map'}{$id}{'defined'} = 1;
3504 $self->{'item_map'}{$id}{'prev'} = $prev;
3505 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3509 $self->{'item_map'}{'last'} = $prev
3510 if !$window || @$items < $window;
3515 Returns an a map of all items found by this search. The map is a hash
3519 first => <first ticket id found>,
3520 last => <last ticket id found or undef>,
3523 prev => <the ticket id found before>,
3524 next => <the ticket id found after>,
3536 $self->_BuildItemMap unless $self->{'item_map'};
3537 return $self->{'item_map'};
3545 =head2 PrepForSerialization
3547 You don't want to serialize a big tickets object, as
3548 the {items} hash will be instantly invalid _and_ eat
3553 sub PrepForSerialization {
3555 delete $self->{'items'};
3556 delete $self->{'items_array'};
3557 $self->RedoSearch();
3562 RT::Tickets supports several flags which alter search behavior:
3565 allow_deleted_search (Otherwise never show deleted tickets in search results)
3566 looking_at_type (otherwise limit to type=ticket)
3568 These flags are set by calling
3570 $tickets->{'flagname'} = 1;
3572 BUG: There should be an API for this