1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
51 # - Decimated ProcessRestrictions and broke it into multiple
52 # functions joined by a LUT
53 # - Semi-Generic SQL stuff moved to another file
55 # Known Issues: FIXME!
57 # - ClearRestrictions and Reinitialization is messy and unclear. The
58 # only good way to do it is to create a new RT::Tickets object.
62 RT::Tickets - A collection of Ticket objects
68 my $tickets = new RT::Tickets($CurrentUser);
72 A collection of RT::Tickets.
82 no warnings qw(redefine);
85 use DBIx::SearchBuilder::Unique;
87 # Configuration Tables:
89 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
92 our %FIELD_METADATA = (
93 Status => [ 'ENUM', ], #loc_left_pair
94 Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
95 Type => [ 'ENUM', ], #loc_left_pair
96 Creator => [ 'ENUM' => 'User', ], #loc_left_pair
97 LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
98 Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
99 EffectiveId => [ 'INT', ], #loc_left_pair
100 id => [ 'ID', ], #loc_left_pair
101 InitialPriority => [ 'INT', ], #loc_left_pair
102 FinalPriority => [ 'INT', ], #loc_left_pair
103 Priority => [ 'INT', ], #loc_left_pair
104 TimeLeft => [ 'INT', ], #loc_left_pair
105 TimeWorked => [ 'INT', ], #loc_left_pair
106 TimeEstimated => [ 'INT', ], #loc_left_pair
108 Linked => [ 'LINK' ], #loc_left_pair
109 LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
110 LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
111 MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
112 DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
113 RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
114 HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
115 DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
116 DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
117 ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
118 Told => [ 'DATE' => 'Told', ], #loc_left_pair
119 Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
120 Started => [ 'DATE' => 'Started', ], #loc_left_pair
121 Due => [ 'DATE' => 'Due', ], #loc_left_pair
122 Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
123 LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
124 Created => [ 'DATE' => 'Created', ], #loc_left_pair
125 Subject => [ 'STRING', ], #loc_left_pair
126 Content => [ 'TRANSFIELD', ], #loc_left_pair
127 ContentType => [ 'TRANSFIELD', ], #loc_left_pair
128 Filename => [ 'TRANSFIELD', ], #loc_left_pair
129 TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
130 Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
131 Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
132 Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
133 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
134 Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
135 QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
136 QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
137 QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
138 CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
139 CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
140 CF => [ 'CUSTOMFIELD', ], #loc_left_pair
141 Updated => [ 'TRANSDATE', ], #loc_left_pair
142 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
143 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
144 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
145 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
146 HasAttribute => [ 'HASATTRIBUTE', 1 ],
147 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
148 Agentnum => [ 'FREESIDEFIELD', ],
149 Classnum => [ 'FREESIDEFIELD', ],
150 Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ],
153 # 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 # Except if the value is 'this month' or 'last month', check
544 # > and < the edges of the month.
546 my ($daystart, $dayend);
547 if ( lc($value) eq 'this month' ) {
549 $date->SetToStart('month', Timezone => 'server');
550 $daystart = $date->ISO;
552 $dayend = $date->ISO;
554 elsif ( lc($value) eq 'last month' ) {
556 $date->SetToStart('month', Timezone => 'server');
557 $dayend = $date->ISO;
559 $date->SetToStart('month', Timezone => 'server');
560 $daystart = $date->ISO;
563 $date->SetToMidnight( Timezone => 'server' );
564 $daystart = $date->ISO;
566 $dayend = $date->ISO;
583 ENTRYAGGREGATOR => 'AND',
601 Handle simple fields which are just strings. (Subject,Type)
609 my ( $sb, $field, $op, $value, @rest ) = @_;
613 # =, !=, LIKE, NOT LIKE
614 if ( (!defined $value || !length $value)
615 && lc($op) ne 'is' && lc($op) ne 'is not'
616 && RT->Config->Get('DatabaseType') eq 'Oracle'
618 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
619 $op = $negative? 'IS NOT': 'IS';
632 =head2 _TransDateLimit
634 Handle fields limiting based on Transaction Date.
636 The inpupt value must be in a format parseable by Time::ParseDate
643 # This routine should really be factored into translimit.
644 sub _TransDateLimit {
645 my ( $sb, $field, $op, $value, @rest ) = @_;
647 # See the comments for TransLimit, they apply here too
649 unless ( $sb->{_sql_transalias} ) {
650 $sb->{_sql_transalias} = $sb->Join(
653 TABLE2 => 'Transactions',
654 FIELD2 => 'ObjectId',
657 ALIAS => $sb->{_sql_transalias},
658 FIELD => 'ObjectType',
659 VALUE => 'RT::Ticket',
660 ENTRYAGGREGATOR => 'AND',
664 my $date = RT::Date->new( $sb->CurrentUser );
665 $date->Set( Format => 'unknown', Value => $value );
670 # if we're specifying =, that means we want everything on a
671 # particular single day. in the database, we need to check for >
672 # and < the edges of that day.
674 $date->SetToMidnight( Timezone => 'server' );
675 my $daystart = $date->ISO;
677 my $dayend = $date->ISO;
680 ALIAS => $sb->{_sql_transalias},
688 ALIAS => $sb->{_sql_transalias},
694 ENTRYAGGREGATOR => 'AND',
699 # not searching for a single day
702 #Search for the right field
704 ALIAS => $sb->{_sql_transalias},
718 Limit based on the Content of a transaction or the ContentType.
727 # Content, ContentType, Filename
729 # If only this was this simple. We've got to do something
732 #Basically, we want to make sure that the limits apply to
733 #the same attachment, rather than just another attachment
734 #for the same ticket, no matter how many clauses we lump
735 #on. We put them in TicketAliases so that they get nuked
736 #when we redo the join.
738 # In the SQL, we might have
739 # (( Content = foo ) or ( Content = bar AND Content = baz ))
740 # The AND group should share the same Alias.
742 # Actually, maybe it doesn't matter. We use the same alias and it
743 # works itself out? (er.. different.)
745 # Steal more from _ProcessRestrictions
747 # FIXME: Maybe look at the previous FooLimit call, and if it was a
748 # TransLimit and EntryAggregator == AND, reuse the Aliases?
750 # Or better - store the aliases on a per subclause basis - since
751 # those are going to be the things we want to relate to each other,
754 # maybe we should not allow certain kinds of aggregation of these
755 # clauses and do a psuedo regex instead? - the problem is getting
756 # them all into the same subclause when you have (A op B op C) - the
757 # way they get parsed in the tree they're in different subclauses.
759 my ( $self, $field, $op, $value, %rest ) = @_;
761 unless ( $self->{_sql_transalias} ) {
762 $self->{_sql_transalias} = $self->Join(
765 TABLE2 => 'Transactions',
766 FIELD2 => 'ObjectId',
769 ALIAS => $self->{_sql_transalias},
770 FIELD => 'ObjectType',
771 VALUE => 'RT::Ticket',
772 ENTRYAGGREGATOR => 'AND',
775 unless ( defined $self->{_sql_trattachalias} ) {
776 $self->{_sql_trattachalias} = $self->_SQLJoin(
777 TYPE => 'LEFT', # not all txns have an attachment
778 ALIAS1 => $self->{_sql_transalias},
780 TABLE2 => 'Attachments',
781 FIELD2 => 'TransactionId',
785 #Search for the right field
786 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
790 ALIAS => $self->{_sql_trattachalias},
797 ENTRYAGGREGATOR => 'AND',
798 ALIAS => $self->{_sql_trattachalias},
807 ALIAS => $self->{_sql_trattachalias},
820 Handle watcher limits. (Requestor, CC, etc..)
836 my $meta = $FIELD_METADATA{ $field };
837 my $type = $meta->[1] || '';
838 my $class = $meta->[2] || 'Ticket';
840 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
841 # search by id and Name at the same time, this is workaround
842 # to preserve backward compatibility
843 if ( $field eq 'Owner' ) {
844 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
845 my $o = RT::User->new( $self->CurrentUser );
846 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
847 $o->$method( $value );
856 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
866 $rest{SUBKEY} ||= 'EmailAddress';
868 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
871 if ( $op =~ /^IS(?: NOT)?$/ ) {
872 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
873 # to avoid joining the table Users into the query, we just join GM
874 # and make sure we don't match records where group is member of itself
876 LEFTJOIN => $group_members,
879 VALUE => "$group_members.MemberId",
883 ALIAS => $group_members,
890 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
892 $op =~ s/!|NOT\s+//i;
894 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
895 # "X = 'Y'" matches more then one user so we try to fetch two records and
896 # do the right thing when there is only one exist and semi-working solution
898 my $users_obj = RT::Users->new( $self->CurrentUser );
900 FIELD => $rest{SUBKEY},
905 $users_obj->RowsPerPage(2);
906 my @users = @{ $users_obj->ItemsArrayRef };
908 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
911 $uid = $users[0]->id if @users;
913 LEFTJOIN => $group_members,
914 ALIAS => $group_members,
920 ALIAS => $group_members,
927 LEFTJOIN => $group_members,
930 VALUE => "$group_members.MemberId",
933 my $users = $self->Join(
935 ALIAS1 => $group_members,
936 FIELD1 => 'MemberId',
943 FIELD => $rest{SUBKEY},
957 my $group_members = $self->_GroupMembersJoin(
958 GroupsAlias => $groups,
962 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
964 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
965 $self->NewAlias('Users');
967 LEFTJOIN => $group_members,
968 ALIAS => $group_members,
970 VALUE => "$users.id",
975 # we join users table without adding some join condition between tables,
976 # the only conditions we have are conditions on the table iteslf,
977 # for example Users.EmailAddress = 'x'. We should add this condition to
978 # the top level of the query and bundle it with another similar conditions,
979 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
980 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
983 SUBCLAUSE => '_sql_u_watchers_'. $users,
985 FIELD => $rest{'SUBKEY'},
990 # A condition which ties Users and Groups (role groups) is a left join condition
991 # of CachedGroupMembers table. To get correct results of the query we check
992 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
995 ALIAS => $group_members,
997 OPERATOR => 'IS NOT',
1004 sub _RoleGroupsJoin {
1006 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1007 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1008 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1011 # we always have watcher groups for ticket, so we use INNER join
1012 my $groups = $self->Join(
1014 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1016 FIELD2 => 'Instance',
1017 ENTRYAGGREGATOR => 'AND',
1019 $self->SUPER::Limit(
1020 LEFTJOIN => $groups,
1023 VALUE => 'RT::'. $args{'Class'} .'-Role',
1025 $self->SUPER::Limit(
1026 LEFTJOIN => $groups,
1029 VALUE => $args{'Type'},
1032 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1033 unless $args{'New'};
1038 sub _GroupMembersJoin {
1040 my %args = (New => 1, GroupsAlias => undef, @_);
1042 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1043 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1046 my $alias = $self->Join(
1048 ALIAS1 => $args{'GroupsAlias'},
1050 TABLE2 => 'CachedGroupMembers',
1051 FIELD2 => 'GroupId',
1052 ENTRYAGGREGATOR => 'AND',
1055 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1056 unless $args{'New'};
1063 Helper function which provides joins to a watchers table both for limits
1070 my $type = shift || '';
1073 my $groups = $self->_RoleGroupsJoin( Type => $type );
1074 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1075 # XXX: work around, we must hide groups that
1076 # are members of the role group we search in,
1077 # otherwise them result in wrong NULLs in Users
1078 # table and break ordering. Now, we know that
1079 # RT doesn't allow to add groups as members of the
1080 # ticket roles, so we just hide entries in CGM table
1081 # with MemberId == GroupId from results
1082 $self->SUPER::Limit(
1083 LEFTJOIN => $group_members,
1086 VALUE => "$group_members.MemberId",
1089 my $users = $self->Join(
1091 ALIAS1 => $group_members,
1092 FIELD1 => 'MemberId',
1096 return ($groups, $group_members, $users);
1099 =head2 _WatcherMembershipLimit
1101 Handle watcher membership limits, i.e. whether the watcher belongs to a
1102 specific group or not.
1105 1: Field to query on
1107 SELECT DISTINCT main.*
1111 CachedGroupMembers CachedGroupMembers_2,
1114 (main.EffectiveId = main.id)
1116 (main.Status != 'deleted')
1118 (main.Type = 'ticket')
1121 (Users_3.EmailAddress = '22')
1123 (Groups_1.Domain = 'RT::Ticket-Role')
1125 (Groups_1.Type = 'RequestorGroup')
1128 Groups_1.Instance = main.id
1130 Groups_1.id = CachedGroupMembers_2.GroupId
1132 CachedGroupMembers_2.MemberId = Users_3.id
1133 ORDER BY main.id ASC
1138 sub _WatcherMembershipLimit {
1139 my ( $self, $field, $op, $value, @rest ) = @_;
1144 my $groups = $self->NewAlias('Groups');
1145 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1146 my $users = $self->NewAlias('Users');
1147 my $memberships = $self->NewAlias('CachedGroupMembers');
1149 if ( ref $field ) { # gross hack
1150 my @bundle = @$field;
1152 for my $chunk (@bundle) {
1153 ( $field, $op, $value, @rest ) = @$chunk;
1155 ALIAS => $memberships,
1166 ALIAS => $memberships,
1174 # {{{ Tie to groups for tickets we care about
1178 VALUE => 'RT::Ticket-Role',
1179 ENTRYAGGREGATOR => 'AND'
1184 FIELD1 => 'Instance',
1191 # If we care about which sort of watcher
1192 my $meta = $FIELD_METADATA{$field};
1193 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1200 ENTRYAGGREGATOR => 'AND'
1207 ALIAS2 => $groupmembers,
1212 ALIAS1 => $groupmembers,
1213 FIELD1 => 'MemberId',
1219 ALIAS1 => $memberships,
1220 FIELD1 => 'MemberId',
1229 =head2 _CustomFieldDecipher
1231 Try and turn a CF descriptor into (cfid, cfname) object pair.
1235 sub _CustomFieldDecipher {
1236 my ($self, $string) = @_;
1238 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
1239 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1243 my $q = RT::Queue->new( $self->CurrentUser );
1247 # $queue = $q->Name; # should we normalize the queue?
1248 $cf = $q->CustomField( $field );
1251 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1255 elsif ( $field =~ /\D/ ) {
1257 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1258 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1259 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1261 # if there is more then one field the current user can
1262 # see with the same name then we shouldn't return cf object
1263 # as we don't know which one to use
1266 $cf = undef if $cfs->Next;
1270 $cf = RT::CustomField->new( $self->CurrentUser );
1271 $cf->Load( $field );
1274 return ($queue, $field, $cf, $column);
1277 =head2 _CustomFieldJoin
1279 Factor out the Join of custom fields so we can use it for sorting too
1283 sub _CustomFieldJoin {
1284 my ($self, $cfkey, $cfid, $field) = @_;
1285 # Perform one Join per CustomField
1286 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1287 $self->{_sql_cf_alias}{$cfkey} )
1289 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1290 $self->{_sql_cf_alias}{$cfkey} );
1293 my ($TicketCFs, $CFs);
1295 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1299 TABLE2 => 'ObjectCustomFieldValues',
1300 FIELD2 => 'ObjectId',
1302 $self->SUPER::Limit(
1303 LEFTJOIN => $TicketCFs,
1304 FIELD => 'CustomField',
1306 ENTRYAGGREGATOR => 'AND'
1310 my $ocfalias = $self->Join(
1313 TABLE2 => 'ObjectCustomFields',
1314 FIELD2 => 'ObjectId',
1317 $self->SUPER::Limit(
1318 LEFTJOIN => $ocfalias,
1319 ENTRYAGGREGATOR => 'OR',
1320 FIELD => 'ObjectId',
1324 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1326 ALIAS1 => $ocfalias,
1327 FIELD1 => 'CustomField',
1328 TABLE2 => 'CustomFields',
1331 $self->SUPER::Limit(
1333 ENTRYAGGREGATOR => 'AND',
1334 FIELD => 'LookupType',
1335 VALUE => 'RT::Queue-RT::Ticket',
1337 $self->SUPER::Limit(
1339 ENTRYAGGREGATOR => 'AND',
1344 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1348 TABLE2 => 'ObjectCustomFieldValues',
1349 FIELD2 => 'CustomField',
1351 $self->SUPER::Limit(
1352 LEFTJOIN => $TicketCFs,
1353 FIELD => 'ObjectId',
1356 ENTRYAGGREGATOR => 'AND',
1359 $self->SUPER::Limit(
1360 LEFTJOIN => $TicketCFs,
1361 FIELD => 'ObjectType',
1362 VALUE => 'RT::Ticket',
1363 ENTRYAGGREGATOR => 'AND'
1365 $self->SUPER::Limit(
1366 LEFTJOIN => $TicketCFs,
1367 FIELD => 'Disabled',
1370 ENTRYAGGREGATOR => 'AND'
1373 return ($TicketCFs, $CFs);
1376 =head2 _CustomFieldLimit
1378 Limit based on CustomFields
1385 sub _CustomFieldLimit {
1386 my ( $self, $_field, $op, $value, %rest ) = @_;
1388 my $field = $rest{'SUBKEY'} || die "No field specified";
1390 # For our sanity, we can only limit on one queue at a time
1392 my ($queue, $cfid, $cf, $column);
1393 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1394 $cfid = $cf ? $cf->id : 0 ;
1396 # If we're trying to find custom fields that don't match something, we
1397 # want tickets where the custom field has no value at all. Note that
1398 # we explicitly don't include the "IS NULL" case, since we would
1399 # otherwise end up with a redundant clause.
1401 my ($negative_op, $null_op, $inv_op, $range_op)
1402 = $self->ClassifySQLOperation( $op );
1406 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1407 return 'MATCHES' if $op eq '=';
1408 return 'NOT MATCHES' if $op eq '!=';
1412 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1414 my $cfkey = $cfid ? $cfid : "$queue.$field";
1416 if ( $null_op && !$column ) {
1417 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1418 # we can reuse our default joins for this operation
1419 # with column specified we have different situation
1420 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1423 ALIAS => $TicketCFs,
1432 OPERATOR => 'IS NOT',
1435 ENTRYAGGREGATOR => 'AND',
1439 elsif ( !$negative_op || $single_value ) {
1440 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1441 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1448 # if column is defined then deal only with it
1449 # otherwise search in Content and in LargeContent
1452 ALIAS => $TicketCFs,
1454 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1459 elsif ( $cf->Type eq 'Date' ) {
1460 $self->_DateFieldLimit(
1464 ALIAS => $TicketCFs,
1468 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1469 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1471 ALIAS => $TicketCFs,
1480 ALIAS => $TicketCFs,
1484 ENTRYAGGREGATOR => 'OR'
1487 ALIAS => $TicketCFs,
1491 ENTRYAGGREGATOR => 'OR'
1495 ALIAS => $TicketCFs,
1496 FIELD => 'LargeContent',
1497 OPERATOR => $fix_op->($op),
1499 ENTRYAGGREGATOR => 'AND',
1505 ALIAS => $TicketCFs,
1515 ALIAS => $TicketCFs,
1519 ENTRYAGGREGATOR => 'OR'
1522 ALIAS => $TicketCFs,
1526 ENTRYAGGREGATOR => 'OR'
1530 ALIAS => $TicketCFs,
1531 FIELD => 'LargeContent',
1532 OPERATOR => $fix_op->($op),
1534 ENTRYAGGREGATOR => 'AND',
1540 # XXX: if we join via CustomFields table then
1541 # because of order of left joins we get NULLs in
1542 # CF table and then get nulls for those records
1543 # in OCFVs table what result in wrong results
1544 # as decifer method now tries to load a CF then
1545 # we fall into this situation only when there
1546 # are more than one CF with the name in the DB.
1547 # the same thing applies to order by call.
1548 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1549 # we want treat IS NULL as (not applies or has
1554 OPERATOR => 'IS NOT',
1557 ENTRYAGGREGATOR => 'AND',
1563 ALIAS => $TicketCFs,
1564 FIELD => $column || 'Content',
1568 ENTRYAGGREGATOR => 'OR',
1575 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1576 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1579 $op =~ s/!|NOT\s+//i;
1581 # if column is defined then deal only with it
1582 # otherwise search in Content and in LargeContent
1584 $self->SUPER::Limit(
1585 LEFTJOIN => $TicketCFs,
1586 ALIAS => $TicketCFs,
1588 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1593 $self->SUPER::Limit(
1594 LEFTJOIN => $TicketCFs,
1595 ALIAS => $TicketCFs,
1603 ALIAS => $TicketCFs,
1612 sub _HasAttributeLimit {
1613 my ( $self, $field, $op, $value, %rest ) = @_;
1615 my $alias = $self->Join(
1619 TABLE2 => 'Attributes',
1620 FIELD2 => 'ObjectId',
1622 $self->SUPER::Limit(
1624 FIELD => 'ObjectType',
1625 VALUE => 'RT::Ticket',
1626 ENTRYAGGREGATOR => 'AND'
1628 $self->SUPER::Limit(
1633 ENTRYAGGREGATOR => 'AND'
1639 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1645 # End Helper Functions
1647 # End of SQL Stuff -------------------------------------------------
1649 # {{{ Allow sorting on watchers
1651 =head2 OrderByCols ARRAY
1653 A modified version of the OrderBy method which automatically joins where
1654 C<ALIAS> is set to the name of a watcher type.
1665 foreach my $row (@args) {
1666 if ( $row->{ALIAS} ) {
1670 if ( $row->{FIELD} !~ /\./ ) {
1671 my $meta = $self->FIELDS->{ $row->{FIELD} };
1677 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1678 my $alias = $self->Join(
1681 FIELD1 => $row->{'FIELD'},
1685 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1686 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1687 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1689 my $alias = $self->Join(
1692 FIELD1 => $row->{'FIELD'},
1696 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1703 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1704 my $meta = $self->FIELDS->{$field};
1705 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1706 # cache alias as we want to use one alias per watcher type for sorting
1707 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1709 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1710 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1712 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1713 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1714 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1715 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1716 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1717 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1718 # this is described in _CustomFieldLimit
1722 OPERATOR => 'IS NOT',
1725 ENTRYAGGREGATOR => 'AND',
1728 # For those cases where we are doing a join against the
1729 # CF name, and don't have a CFid, use Unique to make sure
1730 # we don't show duplicate tickets. NOTE: I'm pretty sure
1731 # this will stay mixed in for the life of the
1732 # class/package, and not just for the life of the object.
1733 # Potential performance issue.
1734 require DBIx::SearchBuilder::Unique;
1735 DBIx::SearchBuilder::Unique->import;
1737 my $CFvs = $self->Join(
1739 ALIAS1 => $TicketCFs,
1740 FIELD1 => 'CustomField',
1741 TABLE2 => 'CustomFieldValues',
1742 FIELD2 => 'CustomField',
1744 $self->SUPER::Limit(
1748 VALUE => $TicketCFs . ".Content",
1749 ENTRYAGGREGATOR => 'AND'
1752 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1753 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1754 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1755 # PAW logic is "reversed"
1757 if (exists $row->{ORDER} ) {
1758 my $o = $row->{ORDER};
1759 delete $row->{ORDER};
1760 $order = "DESC" if $o =~ /asc/i;
1763 # Ticket.Owner 1 0 X
1764 # Unowned Tickets 0 1 X
1767 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1768 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1769 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1770 push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
1772 push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
1776 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1778 } elsif ( $field eq 'Customer' ) { #Freeside
1779 if ( $subkey eq 'Number' ) {
1780 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1783 FIELD => $custnum_sql,
1787 my $custalias = $self->JoinToCustomer;
1789 if ( $subkey eq 'Name' ) {
1790 $field = "COALESCE( $custalias.company,
1791 $custalias.last || ', ' || $custalias.first
1794 elsif ( $subkey eq 'Class' ) {
1795 $field = "$custalias.classnum";
1797 elsif ( $subkey eq 'Agent' ) {
1798 $field = "$custalias.agentnum";
1801 # no other cases exist yet, but for obviousness:
1804 push @res, { %$row, ALIAS => '', FIELD => $field };
1813 return $self->SUPER::OrderByCols(@res);
1818 sub JoinToCustLinks {
1819 # Set up join to links (id = localbase),
1820 # limit link type to 'MemberOf',
1821 # and target value to any Freeside custnum URI.
1822 # Return the linkalias for further join/limit action,
1823 # and an sql expression to retrieve the custnum.
1825 my $linkalias = $self->Join(
1830 FIELD2 => 'LocalBase',
1833 $self->SUPER::Limit(
1834 LEFTJOIN => $linkalias,
1837 VALUE => 'MemberOf',
1839 $self->SUPER::Limit(
1840 LEFTJOIN => $linkalias,
1842 OPERATOR => 'STARTSWITH',
1843 VALUE => 'freeside://freeside/cust_main/',
1845 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1846 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1847 $custnum_sql .= 'SIGNED INTEGER)';
1850 $custnum_sql .= 'INTEGER)';
1852 return ($linkalias, $custnum_sql);
1855 sub JoinToCustomer {
1857 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1859 my $custalias = $self->Join(
1861 EXPRESSION => $custnum_sql,
1862 TABLE2 => 'cust_main',
1863 FIELD2 => 'custnum',
1868 sub _FreesideFieldLimit {
1869 my ( $self, $field, $op, $value, %rest ) = @_;
1870 my $alias = $self->JoinToCustomer;
1871 my $is_negative = 0;
1872 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1873 # if the op is negative, do the join as though
1874 # the op were positive, then accept only records
1875 # where the right-side join key is null.
1877 $op = '=' if $op eq '!=';
1880 my $meta = $FIELD_METADATA{$field};
1882 $alias = $self->Join(
1885 FIELD1 => 'custnum',
1886 TABLE2 => $meta->[1],
1887 FIELD2 => 'custnum',
1891 $self->SUPER::Limit(
1893 FIELD => lc($field),
1896 ENTRYAGGREGATOR => 'AND',
1901 FIELD => lc($field),
1902 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1912 # {{{ Limit the result set based on content
1918 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1919 Generally best called from LimitFoo methods
1929 DESCRIPTION => undef,
1932 $args{'DESCRIPTION'} = $self->loc(
1933 "[_1] [_2] [_3]", $args{'FIELD'},
1934 $args{'OPERATOR'}, $args{'VALUE'}
1936 if ( !defined $args{'DESCRIPTION'} );
1938 my $index = $self->_NextIndex;
1940 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1942 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1944 $self->{'RecalcTicketLimits'} = 1;
1946 # If we're looking at the effective id, we don't want to append the other clause
1947 # which limits us to tickets where id = effective id
1948 if ( $args{'FIELD'} eq 'EffectiveId'
1949 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1951 $self->{'looking_at_effective_id'} = 1;
1954 if ( $args{'FIELD'} eq 'Type'
1955 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1957 $self->{'looking_at_type'} = 1;
1967 Returns a frozen string suitable for handing back to ThawLimits.
1971 sub _FreezeThawKeys {
1972 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1976 # {{{ sub FreezeLimits
1981 require MIME::Base64;
1982 MIME::Base64::base64_encode(
1983 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1990 Take a frozen Limits string generated by FreezeLimits and make this tickets
1991 object have that set of limits.
1995 # {{{ sub ThawLimits
2001 #if we don't have $in, get outta here.
2002 return undef unless ($in);
2004 $self->{'RecalcTicketLimits'} = 1;
2007 require MIME::Base64;
2009 #We don't need to die if the thaw fails.
2010 @{$self}{ $self->_FreezeThawKeys }
2011 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2013 $RT::Logger->error($@) if $@;
2019 # {{{ Limit by enum or foreign key
2021 # {{{ sub LimitQueue
2025 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2026 OPERATOR is one of = or !=. (It defaults to =).
2027 VALUE is a queue id or Name.
2040 #TODO VALUE should also take queue objects
2041 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2042 my $queue = new RT::Queue( $self->CurrentUser );
2043 $queue->Load( $args{'VALUE'} );
2044 $args{'VALUE'} = $queue->Id;
2047 # What if they pass in an Id? Check for isNum() and convert to
2050 #TODO check for a valid queue here
2054 VALUE => $args{'VALUE'},
2055 OPERATOR => $args{'OPERATOR'},
2056 DESCRIPTION => join(
2057 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2065 # {{{ sub LimitStatus
2069 Takes a paramhash with the fields OPERATOR and VALUE.
2070 OPERATOR is one of = or !=.
2073 RT adds Status != 'deleted' until object has
2074 allow_deleted_search internal property set.
2075 $tickets->{'allow_deleted_search'} = 1;
2076 $tickets->LimitStatus( VALUE => 'deleted' );
2088 VALUE => $args{'VALUE'},
2089 OPERATOR => $args{'OPERATOR'},
2090 DESCRIPTION => join( ' ',
2091 $self->loc('Status'), $args{'OPERATOR'},
2092 $self->loc( $args{'VALUE'} ) ),
2098 # {{{ sub IgnoreType
2102 If called, this search will not automatically limit the set of results found
2103 to tickets of type "Ticket". Tickets of other types, such as "project" and
2104 "approval" will be found.
2111 # Instead of faking a Limit that later gets ignored, fake up the
2112 # fact that we're already looking at type, so that the check in
2113 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2115 # $self->LimitType(VALUE => '__any');
2116 $self->{looking_at_type} = 1;
2125 Takes a paramhash with the fields OPERATOR and VALUE.
2126 OPERATOR is one of = or !=, it defaults to "=".
2127 VALUE is a string to search for in the type of the ticket.
2142 VALUE => $args{'VALUE'},
2143 OPERATOR => $args{'OPERATOR'},
2144 DESCRIPTION => join( ' ',
2145 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2153 # {{{ Limit by string field
2155 # {{{ sub LimitSubject
2159 Takes a paramhash with the fields OPERATOR and VALUE.
2160 OPERATOR is one of = or !=.
2161 VALUE is a string to search for in the subject of the ticket.
2170 VALUE => $args{'VALUE'},
2171 OPERATOR => $args{'OPERATOR'},
2172 DESCRIPTION => join( ' ',
2173 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2181 # {{{ Limit based on ticket numerical attributes
2182 # Things that can be > < = !=
2188 Takes a paramhash with the fields OPERATOR and VALUE.
2189 OPERATOR is one of =, >, < or !=.
2190 VALUE is a ticket Id to search for
2203 VALUE => $args{'VALUE'},
2204 OPERATOR => $args{'OPERATOR'},
2206 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2212 # {{{ sub LimitPriority
2214 =head2 LimitPriority
2216 Takes a paramhash with the fields OPERATOR and VALUE.
2217 OPERATOR is one of =, >, < or !=.
2218 VALUE is a value to match the ticket\'s priority against
2226 FIELD => 'Priority',
2227 VALUE => $args{'VALUE'},
2228 OPERATOR => $args{'OPERATOR'},
2229 DESCRIPTION => join( ' ',
2230 $self->loc('Priority'),
2231 $args{'OPERATOR'}, $args{'VALUE'}, ),
2237 # {{{ sub LimitInitialPriority
2239 =head2 LimitInitialPriority
2241 Takes a paramhash with the fields OPERATOR and VALUE.
2242 OPERATOR is one of =, >, < or !=.
2243 VALUE is a value to match the ticket\'s initial priority against
2248 sub LimitInitialPriority {
2252 FIELD => 'InitialPriority',
2253 VALUE => $args{'VALUE'},
2254 OPERATOR => $args{'OPERATOR'},
2255 DESCRIPTION => join( ' ',
2256 $self->loc('Initial Priority'), $args{'OPERATOR'},
2263 # {{{ sub LimitFinalPriority
2265 =head2 LimitFinalPriority
2267 Takes a paramhash with the fields OPERATOR and VALUE.
2268 OPERATOR is one of =, >, < or !=.
2269 VALUE is a value to match the ticket\'s final priority against
2273 sub LimitFinalPriority {
2277 FIELD => 'FinalPriority',
2278 VALUE => $args{'VALUE'},
2279 OPERATOR => $args{'OPERATOR'},
2280 DESCRIPTION => join( ' ',
2281 $self->loc('Final Priority'), $args{'OPERATOR'},
2288 # {{{ sub LimitTimeWorked
2290 =head2 LimitTimeWorked
2292 Takes a paramhash with the fields OPERATOR and VALUE.
2293 OPERATOR is one of =, >, < or !=.
2294 VALUE is a value to match the ticket's TimeWorked attribute
2298 sub LimitTimeWorked {
2302 FIELD => 'TimeWorked',
2303 VALUE => $args{'VALUE'},
2304 OPERATOR => $args{'OPERATOR'},
2305 DESCRIPTION => join( ' ',
2306 $self->loc('Time Worked'),
2307 $args{'OPERATOR'}, $args{'VALUE'}, ),
2313 # {{{ sub LimitTimeLeft
2315 =head2 LimitTimeLeft
2317 Takes a paramhash with the fields OPERATOR and VALUE.
2318 OPERATOR is one of =, >, < or !=.
2319 VALUE is a value to match the ticket's TimeLeft attribute
2327 FIELD => 'TimeLeft',
2328 VALUE => $args{'VALUE'},
2329 OPERATOR => $args{'OPERATOR'},
2330 DESCRIPTION => join( ' ',
2331 $self->loc('Time Left'),
2332 $args{'OPERATOR'}, $args{'VALUE'}, ),
2340 # {{{ Limiting based on attachment attributes
2342 # {{{ sub LimitContent
2346 Takes a paramhash with the fields OPERATOR and VALUE.
2347 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2348 VALUE is a string to search for in the body of the ticket
2357 VALUE => $args{'VALUE'},
2358 OPERATOR => $args{'OPERATOR'},
2359 DESCRIPTION => join( ' ',
2360 $self->loc('Ticket content'), $args{'OPERATOR'},
2367 # {{{ sub LimitFilename
2369 =head2 LimitFilename
2371 Takes a paramhash with the fields OPERATOR and VALUE.
2372 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2373 VALUE is a string to search for in the body of the ticket
2381 FIELD => 'Filename',
2382 VALUE => $args{'VALUE'},
2383 OPERATOR => $args{'OPERATOR'},
2384 DESCRIPTION => join( ' ',
2385 $self->loc('Attachment filename'), $args{'OPERATOR'},
2391 # {{{ sub LimitContentType
2393 =head2 LimitContentType
2395 Takes a paramhash with the fields OPERATOR and VALUE.
2396 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2397 VALUE is a content type to search ticket attachments for
2401 sub LimitContentType {
2405 FIELD => 'ContentType',
2406 VALUE => $args{'VALUE'},
2407 OPERATOR => $args{'OPERATOR'},
2408 DESCRIPTION => join( ' ',
2409 $self->loc('Ticket content type'), $args{'OPERATOR'},
2418 # {{{ Limiting based on people
2420 # {{{ sub LimitOwner
2424 Takes a paramhash with the fields OPERATOR and VALUE.
2425 OPERATOR is one of = or !=.
2437 my $owner = new RT::User( $self->CurrentUser );
2438 $owner->Load( $args{'VALUE'} );
2440 # FIXME: check for a valid $owner
2443 VALUE => $args{'VALUE'},
2444 OPERATOR => $args{'OPERATOR'},
2445 DESCRIPTION => join( ' ',
2446 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2453 # {{{ Limiting watchers
2455 # {{{ sub LimitWatcher
2459 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2460 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2461 VALUE is a value to match the ticket\'s watcher email addresses against
2462 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2476 #build us up a description
2477 my ( $watcher_type, $desc );
2478 if ( $args{'TYPE'} ) {
2479 $watcher_type = $args{'TYPE'};
2482 $watcher_type = "Watcher";
2486 FIELD => $watcher_type,
2487 VALUE => $args{'VALUE'},
2488 OPERATOR => $args{'OPERATOR'},
2489 TYPE => $args{'TYPE'},
2490 DESCRIPTION => join( ' ',
2491 $self->loc($watcher_type),
2492 $args{'OPERATOR'}, $args{'VALUE'}, ),
2502 # {{{ Limiting based on links
2506 =head2 LimitLinkedTo
2508 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2509 TYPE limits the sort of link we want to search on
2511 TYPE = { RefersTo, MemberOf, DependsOn }
2513 TARGET is the id or URI of the TARGET of the link
2527 FIELD => 'LinkedTo',
2529 TARGET => $args{'TARGET'},
2530 TYPE => $args{'TYPE'},
2531 DESCRIPTION => $self->loc(
2532 "Tickets [_1] by [_2]",
2533 $self->loc( $args{'TYPE'} ),
2536 OPERATOR => $args{'OPERATOR'},
2542 # {{{ LimitLinkedFrom
2544 =head2 LimitLinkedFrom
2546 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2547 TYPE limits the sort of link we want to search on
2550 BASE is the id or URI of the BASE of the link
2554 sub LimitLinkedFrom {
2563 # translate RT2 From/To naming to RT3 TicketSQL naming
2564 my %fromToMap = qw(DependsOn DependentOn
2566 RefersTo ReferredToBy);
2568 my $type = $args{'TYPE'};
2569 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2572 FIELD => 'LinkedTo',
2574 BASE => $args{'BASE'},
2576 DESCRIPTION => $self->loc(
2577 "Tickets [_1] [_2]",
2578 $self->loc( $args{'TYPE'} ),
2581 OPERATOR => $args{'OPERATOR'},
2590 my $ticket_id = shift;
2591 return $self->LimitLinkedTo(
2593 TARGET => $ticket_id,
2600 # {{{ LimitHasMember
2601 sub LimitHasMember {
2603 my $ticket_id = shift;
2604 return $self->LimitLinkedFrom(
2606 BASE => "$ticket_id",
2607 TYPE => 'HasMember',
2614 # {{{ LimitDependsOn
2616 sub LimitDependsOn {
2618 my $ticket_id = shift;
2619 return $self->LimitLinkedTo(
2621 TARGET => $ticket_id,
2622 TYPE => 'DependsOn',
2629 # {{{ LimitDependedOnBy
2631 sub LimitDependedOnBy {
2633 my $ticket_id = shift;
2634 return $self->LimitLinkedFrom(
2637 TYPE => 'DependentOn',
2648 my $ticket_id = shift;
2649 return $self->LimitLinkedTo(
2651 TARGET => $ticket_id,
2659 # {{{ LimitReferredToBy
2661 sub LimitReferredToBy {
2663 my $ticket_id = shift;
2664 return $self->LimitLinkedFrom(
2667 TYPE => 'ReferredToBy',
2675 # {{{ limit based on ticket date attribtes
2679 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2681 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2683 OPERATOR is one of > or <
2684 VALUE is a date and time in ISO format in GMT
2685 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2687 There are also helper functions of the form LimitFIELD that eliminate
2688 the need to pass in a FIELD argument.
2702 #Set the description if we didn't get handed it above
2703 unless ( $args{'DESCRIPTION'} ) {
2704 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2705 . $args{'OPERATOR'} . " "
2706 . $args{'VALUE'} . " GMT";
2709 $self->Limit(%args);
2717 $self->LimitDate( FIELD => 'Created', @_ );
2722 $self->LimitDate( FIELD => 'Due', @_ );
2728 $self->LimitDate( FIELD => 'Starts', @_ );
2734 $self->LimitDate( FIELD => 'Started', @_ );
2739 $self->LimitDate( FIELD => 'Resolved', @_ );
2744 $self->LimitDate( FIELD => 'Told', @_ );
2747 sub LimitLastUpdated {
2749 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2753 # {{{ sub LimitTransactionDate
2755 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2757 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2759 OPERATOR is one of > or <
2760 VALUE is a date and time in ISO format in GMT
2765 sub LimitTransactionDate {
2768 FIELD => 'TransactionDate',
2775 # <20021217042756.GK28744@pallas.fsck.com>
2776 # "Kill It" - Jesse.
2778 #Set the description if we didn't get handed it above
2779 unless ( $args{'DESCRIPTION'} ) {
2780 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2781 . $args{'OPERATOR'} . " "
2782 . $args{'VALUE'} . " GMT";
2785 $self->Limit(%args);
2793 # {{{ Limit based on custom fields
2794 # {{{ sub LimitCustomField
2796 =head2 LimitCustomField
2798 Takes a paramhash of key/value pairs with the following keys:
2802 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2804 =item OPERATOR - The usual Limit operators
2806 =item VALUE - The value to compare against
2812 sub LimitCustomField {
2816 CUSTOMFIELD => undef,
2818 DESCRIPTION => undef,
2819 FIELD => 'CustomFieldValue',
2824 my $CF = RT::CustomField->new( $self->CurrentUser );
2825 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2826 $CF->Load( $args{CUSTOMFIELD} );
2829 $CF->LoadByNameAndQueue(
2830 Name => $args{CUSTOMFIELD},
2831 Queue => $args{QUEUE}
2833 $args{CUSTOMFIELD} = $CF->Id;
2836 #If we are looking to compare with a null value.
2837 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2838 $args{'DESCRIPTION'}
2839 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2841 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2842 $args{'DESCRIPTION'}
2843 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2846 # if we're not looking to compare with a null value
2848 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2849 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2852 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2853 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2854 $QueueObj->Load( $args{'QUEUE'} );
2855 $args{'QUEUE'} = $QueueObj->Id;
2857 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2860 @rest = ( ENTRYAGGREGATOR => 'AND' )
2861 if ( $CF->Type eq 'SelectMultiple' );
2864 VALUE => $args{VALUE},
2866 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2867 .".{" . $CF->Name . "}",
2868 OPERATOR => $args{OPERATOR},
2873 $self->{'RecalcTicketLimits'} = 1;
2879 # {{{ sub _NextIndex
2883 Keep track of the counter for the array of restrictions
2889 return ( $self->{'restriction_index'}++ );
2896 # {{{ Core bits to make this a DBIx::SearchBuilder object
2901 $self->{'table'} = "Tickets";
2902 $self->{'RecalcTicketLimits'} = 1;
2903 $self->{'looking_at_effective_id'} = 0;
2904 $self->{'looking_at_type'} = 0;
2905 $self->{'restriction_index'} = 1;
2906 $self->{'primary_key'} = "id";
2907 delete $self->{'items_array'};
2908 delete $self->{'item_map'};
2909 delete $self->{'columns_to_display'};
2910 $self->SUPER::_Init(@_);
2921 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2922 return ( $self->SUPER::Count() );
2930 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2931 return ( $self->SUPER::CountAll() );
2936 # {{{ sub ItemsArrayRef
2938 =head2 ItemsArrayRef
2940 Returns a reference to the set of all items found in this search
2947 return $self->{'items_array'} if $self->{'items_array'};
2949 my $placeholder = $self->_ItemsCounter;
2950 $self->GotoFirstItem();
2951 while ( my $item = $self->Next ) {
2952 push( @{ $self->{'items_array'} }, $item );
2954 $self->GotoItem($placeholder);
2955 $self->{'items_array'}
2956 = $self->ItemsOrderBy( $self->{'items_array'} );
2958 return $self->{'items_array'};
2961 sub ItemsArrayRefWindow {
2965 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2967 $self->RowsPerPage( $window );
2969 $self->GotoFirstItem;
2972 while ( my $item = $self->Next ) {
2976 $self->RowsPerPage( $old[1] );
2977 $self->FirstRow( $old[2] );
2978 $self->GotoItem( $old[0] );
2989 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2991 my $Ticket = $self->SUPER::Next;
2992 return $Ticket unless $Ticket;
2994 if ( $Ticket->__Value('Status') eq 'deleted'
2995 && !$self->{'allow_deleted_search'} )
2999 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3000 # if we found a ticket with this option enabled then
3001 # all tickets we found are ACLed, cache this fact
3002 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3003 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3006 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3011 # If the user doesn't have the right to show this ticket
3018 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3019 return $self->SUPER::_DoSearch( @_ );
3024 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3025 return $self->SUPER::_DoCount( @_ );
3031 my $cache_key = 'RolesHasRight;:;ShowTicket';
3033 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3037 my $ACL = RT::ACL->new( $RT::SystemUser );
3038 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3039 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3040 my $principal_alias = $ACL->Join(
3042 FIELD1 => 'PrincipalId',
3043 TABLE2 => 'Principals',
3046 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3049 while ( my $ACE = $ACL->Next ) {
3050 my $role = $ACE->PrincipalType;
3051 my $type = $ACE->ObjectType;
3052 if ( $type eq 'RT::System' ) {
3055 elsif ( $type eq 'RT::Queue' ) {
3056 next if $res{ $role } && !ref $res{ $role };
3057 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3060 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3063 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3067 sub _DirectlyCanSeeIn {
3069 my $id = $self->CurrentUser->id;
3071 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3072 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3076 my $ACL = RT::ACL->new( $RT::SystemUser );
3077 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3078 my $principal_alias = $ACL->Join(
3080 FIELD1 => 'PrincipalId',
3081 TABLE2 => 'Principals',
3084 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3085 my $cgm_alias = $ACL->Join(
3087 FIELD1 => 'PrincipalId',
3088 TABLE2 => 'CachedGroupMembers',
3089 FIELD2 => 'GroupId',
3091 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3092 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3095 while ( my $ACE = $ACL->Next ) {
3096 my $type = $ACE->ObjectType;
3097 if ( $type eq 'RT::System' ) {
3098 # If user is direct member of a group that has the right
3099 # on the system then he can see any ticket
3100 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3103 elsif ( $type eq 'RT::Queue' ) {
3104 push @res, $ACE->ObjectId;
3107 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3110 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3114 sub CurrentUserCanSee {
3116 return if $self->{'_sql_current_user_can_see_applied'};
3118 return $self->{'_sql_current_user_can_see_applied'} = 1
3119 if $self->CurrentUser->UserObj->HasRight(
3120 Right => 'SuperUser', Object => $RT::System
3123 my $id = $self->CurrentUser->id;
3125 # directly can see in all queues then we have nothing to do
3126 my @direct_queues = $self->_DirectlyCanSeeIn;
3127 return $self->{'_sql_current_user_can_see_applied'} = 1
3128 if @direct_queues && $direct_queues[0] == -1;
3130 my %roles = $self->_RolesCanSee;
3132 my %skip = map { $_ => 1 } @direct_queues;
3133 foreach my $role ( keys %roles ) {
3134 next unless ref $roles{ $role };
3136 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3138 $roles{ $role } = \@queues;
3140 delete $roles{ $role };
3145 # there is no global watchers, only queues and tickes, if at
3146 # some point we will add global roles then it's gonna blow
3147 # the idea here is that if the right is set globaly for a role
3148 # and user plays this role for a queue directly not a ticket
3149 # then we have to check in advance
3150 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3152 my $groups = RT::Groups->new( $RT::SystemUser );
3153 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3155 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3157 my $principal_alias = $groups->Join(
3160 TABLE2 => 'Principals',
3163 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3164 my $cgm_alias = $groups->Join(
3167 TABLE2 => 'CachedGroupMembers',
3168 FIELD2 => 'GroupId',
3170 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3171 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3172 while ( my $group = $groups->Next ) {
3173 push @direct_queues, $group->Instance;
3177 unless ( @direct_queues || keys %roles ) {
3178 $self->SUPER::Limit(
3183 ENTRYAGGREGATOR => 'AND',
3185 return $self->{'_sql_current_user_can_see_applied'} = 1;
3189 my $join_roles = keys %roles;
3190 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3191 my ($role_group_alias, $cgm_alias);
3192 if ( $join_roles ) {
3193 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3194 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3195 $self->SUPER::Limit(
3196 LEFTJOIN => $cgm_alias,
3197 FIELD => 'MemberId',
3202 my $limit_queues = sub {
3206 return unless @queues;
3207 if ( @queues == 1 ) {
3208 $self->SUPER::Limit(
3213 ENTRYAGGREGATOR => $ea,
3216 $self->SUPER::_OpenParen('ACL');
3217 foreach my $q ( @queues ) {
3218 $self->SUPER::Limit(
3223 ENTRYAGGREGATOR => $ea,
3227 $self->SUPER::_CloseParen('ACL');
3232 $self->SUPER::_OpenParen('ACL');
3234 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3235 while ( my ($role, $queues) = each %roles ) {
3236 $self->SUPER::_OpenParen('ACL');
3237 if ( $role eq 'Owner' ) {
3238 $self->SUPER::Limit(
3242 ENTRYAGGREGATOR => $ea,
3246 $self->SUPER::Limit(
3248 ALIAS => $cgm_alias,
3249 FIELD => 'MemberId',
3250 OPERATOR => 'IS NOT',
3253 ENTRYAGGREGATOR => $ea,
3255 $self->SUPER::Limit(
3257 ALIAS => $role_group_alias,
3260 ENTRYAGGREGATOR => 'AND',
3263 $limit_queues->( 'AND', @$queues ) if ref $queues;
3264 $ea = 'OR' if $ea eq 'AND';
3265 $self->SUPER::_CloseParen('ACL');
3267 $self->SUPER::_CloseParen('ACL');
3269 return $self->{'_sql_current_user_can_see_applied'} = 1;
3276 # {{{ Deal with storing and restoring restrictions
3278 # {{{ sub LoadRestrictions
3280 =head2 LoadRestrictions
3282 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3283 TODO It is not yet implemented
3289 # {{{ sub DescribeRestrictions
3291 =head2 DescribeRestrictions
3294 Returns a hash keyed by restriction id.
3295 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3296 is a description of the purpose of that TicketRestriction
3300 sub DescribeRestrictions {
3303 my ( $row, %listing );
3305 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3306 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3313 # {{{ sub RestrictionValues
3315 =head2 RestrictionValues FIELD
3317 Takes a restriction field and returns a list of values this field is restricted
3322 sub RestrictionValues {
3325 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3326 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3327 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3329 keys %{ $self->{'TicketRestrictions'} };
3334 # {{{ sub ClearRestrictions
3336 =head2 ClearRestrictions
3338 Removes all restrictions irretrievably
3342 sub ClearRestrictions {
3344 delete $self->{'TicketRestrictions'};
3345 $self->{'looking_at_effective_id'} = 0;
3346 $self->{'looking_at_type'} = 0;
3347 $self->{'RecalcTicketLimits'} = 1;
3352 # {{{ sub DeleteRestriction
3354 =head2 DeleteRestriction
3356 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3357 Removes that restriction from the session's limits.
3361 sub DeleteRestriction {
3364 delete $self->{'TicketRestrictions'}{$row};
3366 $self->{'RecalcTicketLimits'} = 1;
3368 #make the underlying easysearch object forget all its preconceptions
3373 # {{{ sub _RestrictionsToClauses
3375 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3377 sub _RestrictionsToClauses {
3382 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3383 my $restriction = $self->{'TicketRestrictions'}{$row};
3385 # We need to reimplement the subclause aggregation that SearchBuilder does.
3386 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3387 # Then SB AND's the different Subclauses together.
3389 # So, we want to group things into Subclauses, convert them to
3390 # SQL, and then join them with the appropriate DefaultEA.
3391 # Then join each subclause group with AND.
3393 my $field = $restriction->{'FIELD'};
3394 my $realfield = $field; # CustomFields fake up a fieldname, so
3395 # we need to figure that out
3398 # Rewrite LinkedTo meta field to the real field
3399 if ( $field =~ /LinkedTo/ ) {
3400 $realfield = $field = $restriction->{'TYPE'};
3404 # Handle subkey fields with a different real field
3405 if ( $field =~ /^(\w+)\./ ) {
3409 die "I don't know about $field yet"
3410 unless ( exists $FIELD_METADATA{$realfield}
3411 or $restriction->{CUSTOMFIELD} );
3413 my $type = $FIELD_METADATA{$realfield}->[0];
3414 my $op = $restriction->{'OPERATOR'};
3418 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3421 # this performs the moral equivalent of defined or/dor/C<//>,
3422 # without the short circuiting.You need to use a 'defined or'
3423 # type thing instead of just checking for truth values, because
3424 # VALUE could be 0.(i.e. "false")
3426 # You could also use this, but I find it less aesthetic:
3427 # (although it does short circuit)
3428 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3429 # defined $restriction->{'TICKET'} ?
3430 # $restriction->{TICKET} :
3431 # defined $restriction->{'BASE'} ?
3432 # $restriction->{BASE} :
3433 # defined $restriction->{'TARGET'} ?
3434 # $restriction->{TARGET} )
3436 my $ea = $restriction->{ENTRYAGGREGATOR}
3437 || $DefaultEA{$type}
3440 die "Invalid operator $op for $field ($type)"
3441 unless exists $ea->{$op};
3445 # Each CustomField should be put into a different Clause so they
3446 # are ANDed together.
3447 if ( $restriction->{CUSTOMFIELD} ) {
3448 $realfield = $field;
3451 exists $clause{$realfield} or $clause{$realfield} = [];
3454 $field =~ s!(['"])!\\$1!g;
3455 $value =~ s!(['"])!\\$1!g;
3456 my $data = [ $ea, $type, $field, $op, $value ];
3458 # here is where we store extra data, say if it's a keyword or
3459 # something. (I.e. "TYPE SPECIFIC STUFF")
3461 push @{ $clause{$realfield} }, $data;
3468 # {{{ sub _ProcessRestrictions
3470 =head2 _ProcessRestrictions PARAMHASH
3472 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3473 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3477 sub _ProcessRestrictions {
3480 #Blow away ticket aliases since we'll need to regenerate them for
3482 delete $self->{'TicketAliases'};
3483 delete $self->{'items_array'};
3484 delete $self->{'item_map'};
3485 delete $self->{'raw_rows'};
3486 delete $self->{'rows'};
3487 delete $self->{'count_all'};
3489 my $sql = $self->Query; # Violating the _SQL namespace
3490 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3492 # "Restrictions to Clauses Branch\n";
3493 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3495 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3499 $sql = $self->ClausesToSQL($clauseRef);
3500 $self->FromSQL($sql) if $sql;
3504 $self->{'RecalcTicketLimits'} = 0;
3508 =head2 _BuildItemMap
3510 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3511 display search nav quickly.
3518 my $window = RT->Config->Get('TicketsItemMapSize');
3520 $self->{'item_map'} = {};
3522 my $items = $self->ItemsArrayRefWindow( $window );
3523 return unless $items && @$items;
3526 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3527 for ( my $i = 0; $i < @$items; $i++ ) {
3528 my $item = $items->[$i];
3529 my $id = $item->EffectiveId;
3530 $self->{'item_map'}{$id}{'defined'} = 1;
3531 $self->{'item_map'}{$id}{'prev'} = $prev;
3532 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3536 $self->{'item_map'}{'last'} = $prev
3537 if !$window || @$items < $window;
3542 Returns an a map of all items found by this search. The map is a hash
3546 first => <first ticket id found>,
3547 last => <last ticket id found or undef>,
3550 prev => <the ticket id found before>,
3551 next => <the ticket id found after>,
3563 $self->_BuildItemMap unless $self->{'item_map'};
3564 return $self->{'item_map'};
3572 =head2 PrepForSerialization
3574 You don't want to serialize a big tickets object, as
3575 the {items} hash will be instantly invalid _and_ eat
3580 sub PrepForSerialization {
3582 delete $self->{'items'};
3583 delete $self->{'items_array'};
3584 $self->RedoSearch();
3589 RT::Tickets supports several flags which alter search behavior:
3592 allow_deleted_search (Otherwise never show deleted tickets in search results)
3593 looking_at_type (otherwise limit to type=ticket)
3595 These flags are set by calling
3597 $tickets->{'flagname'} = 1;
3599 BUG: There should be an API for this