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
1795 # no other cases exist yet, but for obviousness:
1798 push @res, { %$row, ALIAS => '', FIELD => $field };
1807 return $self->SUPER::OrderByCols(@res);
1812 sub JoinToCustLinks {
1813 # Set up join to links (id = localbase),
1814 # limit link type to 'MemberOf',
1815 # and target value to any Freeside custnum URI.
1816 # Return the linkalias for further join/limit action,
1817 # and an sql expression to retrieve the custnum.
1819 my $linkalias = $self->Join(
1824 FIELD2 => 'LocalBase',
1827 $self->SUPER::Limit(
1828 LEFTJOIN => $linkalias,
1831 VALUE => 'MemberOf',
1833 $self->SUPER::Limit(
1834 LEFTJOIN => $linkalias,
1836 OPERATOR => 'STARTSWITH',
1837 VALUE => 'freeside://freeside/cust_main/',
1839 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1840 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1841 $custnum_sql .= 'SIGNED INTEGER)';
1844 $custnum_sql .= 'INTEGER)';
1846 return ($linkalias, $custnum_sql);
1849 sub JoinToCustomer {
1851 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1853 my $custalias = $self->Join(
1855 EXPRESSION => $custnum_sql,
1856 TABLE2 => 'cust_main',
1857 FIELD2 => 'custnum',
1862 sub _FreesideFieldLimit {
1863 my ( $self, $field, $op, $value, %rest ) = @_;
1864 my $alias = $self->JoinToCustomer;
1865 my $is_negative = 0;
1866 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1867 # if the op is negative, do the join as though
1868 # the op were positive, then accept only records
1869 # where the right-side join key is null.
1871 $op = '=' if $op eq '!=';
1874 my $meta = $FIELD_METADATA{$field};
1876 $alias = $self->Join(
1879 FIELD1 => 'custnum',
1880 TABLE2 => $meta->[1],
1881 FIELD2 => 'custnum',
1885 $self->SUPER::Limit(
1887 FIELD => lc($field),
1890 ENTRYAGGREGATOR => 'AND',
1895 FIELD => lc($field),
1896 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1906 # {{{ Limit the result set based on content
1912 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1913 Generally best called from LimitFoo methods
1923 DESCRIPTION => undef,
1926 $args{'DESCRIPTION'} = $self->loc(
1927 "[_1] [_2] [_3]", $args{'FIELD'},
1928 $args{'OPERATOR'}, $args{'VALUE'}
1930 if ( !defined $args{'DESCRIPTION'} );
1932 my $index = $self->_NextIndex;
1934 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1936 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1938 $self->{'RecalcTicketLimits'} = 1;
1940 # If we're looking at the effective id, we don't want to append the other clause
1941 # which limits us to tickets where id = effective id
1942 if ( $args{'FIELD'} eq 'EffectiveId'
1943 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1945 $self->{'looking_at_effective_id'} = 1;
1948 if ( $args{'FIELD'} eq 'Type'
1949 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1951 $self->{'looking_at_type'} = 1;
1961 Returns a frozen string suitable for handing back to ThawLimits.
1965 sub _FreezeThawKeys {
1966 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1970 # {{{ sub FreezeLimits
1975 require MIME::Base64;
1976 MIME::Base64::base64_encode(
1977 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1984 Take a frozen Limits string generated by FreezeLimits and make this tickets
1985 object have that set of limits.
1989 # {{{ sub ThawLimits
1995 #if we don't have $in, get outta here.
1996 return undef unless ($in);
1998 $self->{'RecalcTicketLimits'} = 1;
2001 require MIME::Base64;
2003 #We don't need to die if the thaw fails.
2004 @{$self}{ $self->_FreezeThawKeys }
2005 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2007 $RT::Logger->error($@) if $@;
2013 # {{{ Limit by enum or foreign key
2015 # {{{ sub LimitQueue
2019 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2020 OPERATOR is one of = or !=. (It defaults to =).
2021 VALUE is a queue id or Name.
2034 #TODO VALUE should also take queue objects
2035 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2036 my $queue = new RT::Queue( $self->CurrentUser );
2037 $queue->Load( $args{'VALUE'} );
2038 $args{'VALUE'} = $queue->Id;
2041 # What if they pass in an Id? Check for isNum() and convert to
2044 #TODO check for a valid queue here
2048 VALUE => $args{'VALUE'},
2049 OPERATOR => $args{'OPERATOR'},
2050 DESCRIPTION => join(
2051 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2059 # {{{ sub LimitStatus
2063 Takes a paramhash with the fields OPERATOR and VALUE.
2064 OPERATOR is one of = or !=.
2067 RT adds Status != 'deleted' until object has
2068 allow_deleted_search internal property set.
2069 $tickets->{'allow_deleted_search'} = 1;
2070 $tickets->LimitStatus( VALUE => 'deleted' );
2082 VALUE => $args{'VALUE'},
2083 OPERATOR => $args{'OPERATOR'},
2084 DESCRIPTION => join( ' ',
2085 $self->loc('Status'), $args{'OPERATOR'},
2086 $self->loc( $args{'VALUE'} ) ),
2092 # {{{ sub IgnoreType
2096 If called, this search will not automatically limit the set of results found
2097 to tickets of type "Ticket". Tickets of other types, such as "project" and
2098 "approval" will be found.
2105 # Instead of faking a Limit that later gets ignored, fake up the
2106 # fact that we're already looking at type, so that the check in
2107 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2109 # $self->LimitType(VALUE => '__any');
2110 $self->{looking_at_type} = 1;
2119 Takes a paramhash with the fields OPERATOR and VALUE.
2120 OPERATOR is one of = or !=, it defaults to "=".
2121 VALUE is a string to search for in the type of the ticket.
2136 VALUE => $args{'VALUE'},
2137 OPERATOR => $args{'OPERATOR'},
2138 DESCRIPTION => join( ' ',
2139 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2147 # {{{ Limit by string field
2149 # {{{ sub LimitSubject
2153 Takes a paramhash with the fields OPERATOR and VALUE.
2154 OPERATOR is one of = or !=.
2155 VALUE is a string to search for in the subject of the ticket.
2164 VALUE => $args{'VALUE'},
2165 OPERATOR => $args{'OPERATOR'},
2166 DESCRIPTION => join( ' ',
2167 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2175 # {{{ Limit based on ticket numerical attributes
2176 # Things that can be > < = !=
2182 Takes a paramhash with the fields OPERATOR and VALUE.
2183 OPERATOR is one of =, >, < or !=.
2184 VALUE is a ticket Id to search for
2197 VALUE => $args{'VALUE'},
2198 OPERATOR => $args{'OPERATOR'},
2200 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2206 # {{{ sub LimitPriority
2208 =head2 LimitPriority
2210 Takes a paramhash with the fields OPERATOR and VALUE.
2211 OPERATOR is one of =, >, < or !=.
2212 VALUE is a value to match the ticket\'s priority against
2220 FIELD => 'Priority',
2221 VALUE => $args{'VALUE'},
2222 OPERATOR => $args{'OPERATOR'},
2223 DESCRIPTION => join( ' ',
2224 $self->loc('Priority'),
2225 $args{'OPERATOR'}, $args{'VALUE'}, ),
2231 # {{{ sub LimitInitialPriority
2233 =head2 LimitInitialPriority
2235 Takes a paramhash with the fields OPERATOR and VALUE.
2236 OPERATOR is one of =, >, < or !=.
2237 VALUE is a value to match the ticket\'s initial priority against
2242 sub LimitInitialPriority {
2246 FIELD => 'InitialPriority',
2247 VALUE => $args{'VALUE'},
2248 OPERATOR => $args{'OPERATOR'},
2249 DESCRIPTION => join( ' ',
2250 $self->loc('Initial Priority'), $args{'OPERATOR'},
2257 # {{{ sub LimitFinalPriority
2259 =head2 LimitFinalPriority
2261 Takes a paramhash with the fields OPERATOR and VALUE.
2262 OPERATOR is one of =, >, < or !=.
2263 VALUE is a value to match the ticket\'s final priority against
2267 sub LimitFinalPriority {
2271 FIELD => 'FinalPriority',
2272 VALUE => $args{'VALUE'},
2273 OPERATOR => $args{'OPERATOR'},
2274 DESCRIPTION => join( ' ',
2275 $self->loc('Final Priority'), $args{'OPERATOR'},
2282 # {{{ sub LimitTimeWorked
2284 =head2 LimitTimeWorked
2286 Takes a paramhash with the fields OPERATOR and VALUE.
2287 OPERATOR is one of =, >, < or !=.
2288 VALUE is a value to match the ticket's TimeWorked attribute
2292 sub LimitTimeWorked {
2296 FIELD => 'TimeWorked',
2297 VALUE => $args{'VALUE'},
2298 OPERATOR => $args{'OPERATOR'},
2299 DESCRIPTION => join( ' ',
2300 $self->loc('Time Worked'),
2301 $args{'OPERATOR'}, $args{'VALUE'}, ),
2307 # {{{ sub LimitTimeLeft
2309 =head2 LimitTimeLeft
2311 Takes a paramhash with the fields OPERATOR and VALUE.
2312 OPERATOR is one of =, >, < or !=.
2313 VALUE is a value to match the ticket's TimeLeft attribute
2321 FIELD => 'TimeLeft',
2322 VALUE => $args{'VALUE'},
2323 OPERATOR => $args{'OPERATOR'},
2324 DESCRIPTION => join( ' ',
2325 $self->loc('Time Left'),
2326 $args{'OPERATOR'}, $args{'VALUE'}, ),
2334 # {{{ Limiting based on attachment attributes
2336 # {{{ sub LimitContent
2340 Takes a paramhash with the fields OPERATOR and VALUE.
2341 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2342 VALUE is a string to search for in the body of the ticket
2351 VALUE => $args{'VALUE'},
2352 OPERATOR => $args{'OPERATOR'},
2353 DESCRIPTION => join( ' ',
2354 $self->loc('Ticket content'), $args{'OPERATOR'},
2361 # {{{ sub LimitFilename
2363 =head2 LimitFilename
2365 Takes a paramhash with the fields OPERATOR and VALUE.
2366 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2367 VALUE is a string to search for in the body of the ticket
2375 FIELD => 'Filename',
2376 VALUE => $args{'VALUE'},
2377 OPERATOR => $args{'OPERATOR'},
2378 DESCRIPTION => join( ' ',
2379 $self->loc('Attachment filename'), $args{'OPERATOR'},
2385 # {{{ sub LimitContentType
2387 =head2 LimitContentType
2389 Takes a paramhash with the fields OPERATOR and VALUE.
2390 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2391 VALUE is a content type to search ticket attachments for
2395 sub LimitContentType {
2399 FIELD => 'ContentType',
2400 VALUE => $args{'VALUE'},
2401 OPERATOR => $args{'OPERATOR'},
2402 DESCRIPTION => join( ' ',
2403 $self->loc('Ticket content type'), $args{'OPERATOR'},
2412 # {{{ Limiting based on people
2414 # {{{ sub LimitOwner
2418 Takes a paramhash with the fields OPERATOR and VALUE.
2419 OPERATOR is one of = or !=.
2431 my $owner = new RT::User( $self->CurrentUser );
2432 $owner->Load( $args{'VALUE'} );
2434 # FIXME: check for a valid $owner
2437 VALUE => $args{'VALUE'},
2438 OPERATOR => $args{'OPERATOR'},
2439 DESCRIPTION => join( ' ',
2440 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2447 # {{{ Limiting watchers
2449 # {{{ sub LimitWatcher
2453 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2454 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2455 VALUE is a value to match the ticket\'s watcher email addresses against
2456 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2470 #build us up a description
2471 my ( $watcher_type, $desc );
2472 if ( $args{'TYPE'} ) {
2473 $watcher_type = $args{'TYPE'};
2476 $watcher_type = "Watcher";
2480 FIELD => $watcher_type,
2481 VALUE => $args{'VALUE'},
2482 OPERATOR => $args{'OPERATOR'},
2483 TYPE => $args{'TYPE'},
2484 DESCRIPTION => join( ' ',
2485 $self->loc($watcher_type),
2486 $args{'OPERATOR'}, $args{'VALUE'}, ),
2496 # {{{ Limiting based on links
2500 =head2 LimitLinkedTo
2502 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2503 TYPE limits the sort of link we want to search on
2505 TYPE = { RefersTo, MemberOf, DependsOn }
2507 TARGET is the id or URI of the TARGET of the link
2521 FIELD => 'LinkedTo',
2523 TARGET => $args{'TARGET'},
2524 TYPE => $args{'TYPE'},
2525 DESCRIPTION => $self->loc(
2526 "Tickets [_1] by [_2]",
2527 $self->loc( $args{'TYPE'} ),
2530 OPERATOR => $args{'OPERATOR'},
2536 # {{{ LimitLinkedFrom
2538 =head2 LimitLinkedFrom
2540 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2541 TYPE limits the sort of link we want to search on
2544 BASE is the id or URI of the BASE of the link
2548 sub LimitLinkedFrom {
2557 # translate RT2 From/To naming to RT3 TicketSQL naming
2558 my %fromToMap = qw(DependsOn DependentOn
2560 RefersTo ReferredToBy);
2562 my $type = $args{'TYPE'};
2563 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2566 FIELD => 'LinkedTo',
2568 BASE => $args{'BASE'},
2570 DESCRIPTION => $self->loc(
2571 "Tickets [_1] [_2]",
2572 $self->loc( $args{'TYPE'} ),
2575 OPERATOR => $args{'OPERATOR'},
2584 my $ticket_id = shift;
2585 return $self->LimitLinkedTo(
2587 TARGET => $ticket_id,
2594 # {{{ LimitHasMember
2595 sub LimitHasMember {
2597 my $ticket_id = shift;
2598 return $self->LimitLinkedFrom(
2600 BASE => "$ticket_id",
2601 TYPE => 'HasMember',
2608 # {{{ LimitDependsOn
2610 sub LimitDependsOn {
2612 my $ticket_id = shift;
2613 return $self->LimitLinkedTo(
2615 TARGET => $ticket_id,
2616 TYPE => 'DependsOn',
2623 # {{{ LimitDependedOnBy
2625 sub LimitDependedOnBy {
2627 my $ticket_id = shift;
2628 return $self->LimitLinkedFrom(
2631 TYPE => 'DependentOn',
2642 my $ticket_id = shift;
2643 return $self->LimitLinkedTo(
2645 TARGET => $ticket_id,
2653 # {{{ LimitReferredToBy
2655 sub LimitReferredToBy {
2657 my $ticket_id = shift;
2658 return $self->LimitLinkedFrom(
2661 TYPE => 'ReferredToBy',
2669 # {{{ limit based on ticket date attribtes
2673 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2675 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2677 OPERATOR is one of > or <
2678 VALUE is a date and time in ISO format in GMT
2679 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2681 There are also helper functions of the form LimitFIELD that eliminate
2682 the need to pass in a FIELD argument.
2696 #Set the description if we didn't get handed it above
2697 unless ( $args{'DESCRIPTION'} ) {
2698 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2699 . $args{'OPERATOR'} . " "
2700 . $args{'VALUE'} . " GMT";
2703 $self->Limit(%args);
2711 $self->LimitDate( FIELD => 'Created', @_ );
2716 $self->LimitDate( FIELD => 'Due', @_ );
2722 $self->LimitDate( FIELD => 'Starts', @_ );
2728 $self->LimitDate( FIELD => 'Started', @_ );
2733 $self->LimitDate( FIELD => 'Resolved', @_ );
2738 $self->LimitDate( FIELD => 'Told', @_ );
2741 sub LimitLastUpdated {
2743 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2747 # {{{ sub LimitTransactionDate
2749 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2751 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2753 OPERATOR is one of > or <
2754 VALUE is a date and time in ISO format in GMT
2759 sub LimitTransactionDate {
2762 FIELD => 'TransactionDate',
2769 # <20021217042756.GK28744@pallas.fsck.com>
2770 # "Kill It" - Jesse.
2772 #Set the description if we didn't get handed it above
2773 unless ( $args{'DESCRIPTION'} ) {
2774 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2775 . $args{'OPERATOR'} . " "
2776 . $args{'VALUE'} . " GMT";
2779 $self->Limit(%args);
2787 # {{{ Limit based on custom fields
2788 # {{{ sub LimitCustomField
2790 =head2 LimitCustomField
2792 Takes a paramhash of key/value pairs with the following keys:
2796 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2798 =item OPERATOR - The usual Limit operators
2800 =item VALUE - The value to compare against
2806 sub LimitCustomField {
2810 CUSTOMFIELD => undef,
2812 DESCRIPTION => undef,
2813 FIELD => 'CustomFieldValue',
2818 my $CF = RT::CustomField->new( $self->CurrentUser );
2819 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2820 $CF->Load( $args{CUSTOMFIELD} );
2823 $CF->LoadByNameAndQueue(
2824 Name => $args{CUSTOMFIELD},
2825 Queue => $args{QUEUE}
2827 $args{CUSTOMFIELD} = $CF->Id;
2830 #If we are looking to compare with a null value.
2831 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2832 $args{'DESCRIPTION'}
2833 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2835 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2836 $args{'DESCRIPTION'}
2837 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2840 # if we're not looking to compare with a null value
2842 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2843 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2846 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2847 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2848 $QueueObj->Load( $args{'QUEUE'} );
2849 $args{'QUEUE'} = $QueueObj->Id;
2851 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2854 @rest = ( ENTRYAGGREGATOR => 'AND' )
2855 if ( $CF->Type eq 'SelectMultiple' );
2858 VALUE => $args{VALUE},
2860 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2861 .".{" . $CF->Name . "}",
2862 OPERATOR => $args{OPERATOR},
2867 $self->{'RecalcTicketLimits'} = 1;
2873 # {{{ sub _NextIndex
2877 Keep track of the counter for the array of restrictions
2883 return ( $self->{'restriction_index'}++ );
2890 # {{{ Core bits to make this a DBIx::SearchBuilder object
2895 $self->{'table'} = "Tickets";
2896 $self->{'RecalcTicketLimits'} = 1;
2897 $self->{'looking_at_effective_id'} = 0;
2898 $self->{'looking_at_type'} = 0;
2899 $self->{'restriction_index'} = 1;
2900 $self->{'primary_key'} = "id";
2901 delete $self->{'items_array'};
2902 delete $self->{'item_map'};
2903 delete $self->{'columns_to_display'};
2904 $self->SUPER::_Init(@_);
2915 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2916 return ( $self->SUPER::Count() );
2924 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2925 return ( $self->SUPER::CountAll() );
2930 # {{{ sub ItemsArrayRef
2932 =head2 ItemsArrayRef
2934 Returns a reference to the set of all items found in this search
2941 return $self->{'items_array'} if $self->{'items_array'};
2943 my $placeholder = $self->_ItemsCounter;
2944 $self->GotoFirstItem();
2945 while ( my $item = $self->Next ) {
2946 push( @{ $self->{'items_array'} }, $item );
2948 $self->GotoItem($placeholder);
2949 $self->{'items_array'}
2950 = $self->ItemsOrderBy( $self->{'items_array'} );
2952 return $self->{'items_array'};
2955 sub ItemsArrayRefWindow {
2959 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2961 $self->RowsPerPage( $window );
2963 $self->GotoFirstItem;
2966 while ( my $item = $self->Next ) {
2970 $self->RowsPerPage( $old[1] );
2971 $self->FirstRow( $old[2] );
2972 $self->GotoItem( $old[0] );
2983 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2985 my $Ticket = $self->SUPER::Next;
2986 return $Ticket unless $Ticket;
2988 if ( $Ticket->__Value('Status') eq 'deleted'
2989 && !$self->{'allow_deleted_search'} )
2993 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2994 # if we found a ticket with this option enabled then
2995 # all tickets we found are ACLed, cache this fact
2996 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2997 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3000 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3005 # If the user doesn't have the right to show this ticket
3012 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3013 return $self->SUPER::_DoSearch( @_ );
3018 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3019 return $self->SUPER::_DoCount( @_ );
3025 my $cache_key = 'RolesHasRight;:;ShowTicket';
3027 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3031 my $ACL = RT::ACL->new( $RT::SystemUser );
3032 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3033 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3034 my $principal_alias = $ACL->Join(
3036 FIELD1 => 'PrincipalId',
3037 TABLE2 => 'Principals',
3040 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3043 while ( my $ACE = $ACL->Next ) {
3044 my $role = $ACE->PrincipalType;
3045 my $type = $ACE->ObjectType;
3046 if ( $type eq 'RT::System' ) {
3049 elsif ( $type eq 'RT::Queue' ) {
3050 next if $res{ $role } && !ref $res{ $role };
3051 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3054 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3057 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3061 sub _DirectlyCanSeeIn {
3063 my $id = $self->CurrentUser->id;
3065 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3066 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3070 my $ACL = RT::ACL->new( $RT::SystemUser );
3071 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3072 my $principal_alias = $ACL->Join(
3074 FIELD1 => 'PrincipalId',
3075 TABLE2 => 'Principals',
3078 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3079 my $cgm_alias = $ACL->Join(
3081 FIELD1 => 'PrincipalId',
3082 TABLE2 => 'CachedGroupMembers',
3083 FIELD2 => 'GroupId',
3085 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3086 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3089 while ( my $ACE = $ACL->Next ) {
3090 my $type = $ACE->ObjectType;
3091 if ( $type eq 'RT::System' ) {
3092 # If user is direct member of a group that has the right
3093 # on the system then he can see any ticket
3094 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3097 elsif ( $type eq 'RT::Queue' ) {
3098 push @res, $ACE->ObjectId;
3101 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3104 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3108 sub CurrentUserCanSee {
3110 return if $self->{'_sql_current_user_can_see_applied'};
3112 return $self->{'_sql_current_user_can_see_applied'} = 1
3113 if $self->CurrentUser->UserObj->HasRight(
3114 Right => 'SuperUser', Object => $RT::System
3117 my $id = $self->CurrentUser->id;
3119 # directly can see in all queues then we have nothing to do
3120 my @direct_queues = $self->_DirectlyCanSeeIn;
3121 return $self->{'_sql_current_user_can_see_applied'} = 1
3122 if @direct_queues && $direct_queues[0] == -1;
3124 my %roles = $self->_RolesCanSee;
3126 my %skip = map { $_ => 1 } @direct_queues;
3127 foreach my $role ( keys %roles ) {
3128 next unless ref $roles{ $role };
3130 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3132 $roles{ $role } = \@queues;
3134 delete $roles{ $role };
3139 # there is no global watchers, only queues and tickes, if at
3140 # some point we will add global roles then it's gonna blow
3141 # the idea here is that if the right is set globaly for a role
3142 # and user plays this role for a queue directly not a ticket
3143 # then we have to check in advance
3144 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3146 my $groups = RT::Groups->new( $RT::SystemUser );
3147 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3149 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3151 my $principal_alias = $groups->Join(
3154 TABLE2 => 'Principals',
3157 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3158 my $cgm_alias = $groups->Join(
3161 TABLE2 => 'CachedGroupMembers',
3162 FIELD2 => 'GroupId',
3164 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3165 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3166 while ( my $group = $groups->Next ) {
3167 push @direct_queues, $group->Instance;
3171 unless ( @direct_queues || keys %roles ) {
3172 $self->SUPER::Limit(
3177 ENTRYAGGREGATOR => 'AND',
3179 return $self->{'_sql_current_user_can_see_applied'} = 1;
3183 my $join_roles = keys %roles;
3184 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3185 my ($role_group_alias, $cgm_alias);
3186 if ( $join_roles ) {
3187 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3188 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3189 $self->SUPER::Limit(
3190 LEFTJOIN => $cgm_alias,
3191 FIELD => 'MemberId',
3196 my $limit_queues = sub {
3200 return unless @queues;
3201 if ( @queues == 1 ) {
3202 $self->SUPER::Limit(
3207 ENTRYAGGREGATOR => $ea,
3210 $self->SUPER::_OpenParen('ACL');
3211 foreach my $q ( @queues ) {
3212 $self->SUPER::Limit(
3217 ENTRYAGGREGATOR => $ea,
3221 $self->SUPER::_CloseParen('ACL');
3226 $self->SUPER::_OpenParen('ACL');
3228 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3229 while ( my ($role, $queues) = each %roles ) {
3230 $self->SUPER::_OpenParen('ACL');
3231 if ( $role eq 'Owner' ) {
3232 $self->SUPER::Limit(
3236 ENTRYAGGREGATOR => $ea,
3240 $self->SUPER::Limit(
3242 ALIAS => $cgm_alias,
3243 FIELD => 'MemberId',
3244 OPERATOR => 'IS NOT',
3247 ENTRYAGGREGATOR => $ea,
3249 $self->SUPER::Limit(
3251 ALIAS => $role_group_alias,
3254 ENTRYAGGREGATOR => 'AND',
3257 $limit_queues->( 'AND', @$queues ) if ref $queues;
3258 $ea = 'OR' if $ea eq 'AND';
3259 $self->SUPER::_CloseParen('ACL');
3261 $self->SUPER::_CloseParen('ACL');
3263 return $self->{'_sql_current_user_can_see_applied'} = 1;
3270 # {{{ Deal with storing and restoring restrictions
3272 # {{{ sub LoadRestrictions
3274 =head2 LoadRestrictions
3276 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3277 TODO It is not yet implemented
3283 # {{{ sub DescribeRestrictions
3285 =head2 DescribeRestrictions
3288 Returns a hash keyed by restriction id.
3289 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3290 is a description of the purpose of that TicketRestriction
3294 sub DescribeRestrictions {
3297 my ( $row, %listing );
3299 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3300 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3307 # {{{ sub RestrictionValues
3309 =head2 RestrictionValues FIELD
3311 Takes a restriction field and returns a list of values this field is restricted
3316 sub RestrictionValues {
3319 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3320 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3321 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3323 keys %{ $self->{'TicketRestrictions'} };
3328 # {{{ sub ClearRestrictions
3330 =head2 ClearRestrictions
3332 Removes all restrictions irretrievably
3336 sub ClearRestrictions {
3338 delete $self->{'TicketRestrictions'};
3339 $self->{'looking_at_effective_id'} = 0;
3340 $self->{'looking_at_type'} = 0;
3341 $self->{'RecalcTicketLimits'} = 1;
3346 # {{{ sub DeleteRestriction
3348 =head2 DeleteRestriction
3350 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3351 Removes that restriction from the session's limits.
3355 sub DeleteRestriction {
3358 delete $self->{'TicketRestrictions'}{$row};
3360 $self->{'RecalcTicketLimits'} = 1;
3362 #make the underlying easysearch object forget all its preconceptions
3367 # {{{ sub _RestrictionsToClauses
3369 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3371 sub _RestrictionsToClauses {
3376 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3377 my $restriction = $self->{'TicketRestrictions'}{$row};
3379 # We need to reimplement the subclause aggregation that SearchBuilder does.
3380 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3381 # Then SB AND's the different Subclauses together.
3383 # So, we want to group things into Subclauses, convert them to
3384 # SQL, and then join them with the appropriate DefaultEA.
3385 # Then join each subclause group with AND.
3387 my $field = $restriction->{'FIELD'};
3388 my $realfield = $field; # CustomFields fake up a fieldname, so
3389 # we need to figure that out
3392 # Rewrite LinkedTo meta field to the real field
3393 if ( $field =~ /LinkedTo/ ) {
3394 $realfield = $field = $restriction->{'TYPE'};
3398 # Handle subkey fields with a different real field
3399 if ( $field =~ /^(\w+)\./ ) {
3403 die "I don't know about $field yet"
3404 unless ( exists $FIELD_METADATA{$realfield}
3405 or $restriction->{CUSTOMFIELD} );
3407 my $type = $FIELD_METADATA{$realfield}->[0];
3408 my $op = $restriction->{'OPERATOR'};
3412 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3415 # this performs the moral equivalent of defined or/dor/C<//>,
3416 # without the short circuiting.You need to use a 'defined or'
3417 # type thing instead of just checking for truth values, because
3418 # VALUE could be 0.(i.e. "false")
3420 # You could also use this, but I find it less aesthetic:
3421 # (although it does short circuit)
3422 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3423 # defined $restriction->{'TICKET'} ?
3424 # $restriction->{TICKET} :
3425 # defined $restriction->{'BASE'} ?
3426 # $restriction->{BASE} :
3427 # defined $restriction->{'TARGET'} ?
3428 # $restriction->{TARGET} )
3430 my $ea = $restriction->{ENTRYAGGREGATOR}
3431 || $DefaultEA{$type}
3434 die "Invalid operator $op for $field ($type)"
3435 unless exists $ea->{$op};
3439 # Each CustomField should be put into a different Clause so they
3440 # are ANDed together.
3441 if ( $restriction->{CUSTOMFIELD} ) {
3442 $realfield = $field;
3445 exists $clause{$realfield} or $clause{$realfield} = [];
3448 $field =~ s!(['"])!\\$1!g;
3449 $value =~ s!(['"])!\\$1!g;
3450 my $data = [ $ea, $type, $field, $op, $value ];
3452 # here is where we store extra data, say if it's a keyword or
3453 # something. (I.e. "TYPE SPECIFIC STUFF")
3455 push @{ $clause{$realfield} }, $data;
3462 # {{{ sub _ProcessRestrictions
3464 =head2 _ProcessRestrictions PARAMHASH
3466 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3467 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3471 sub _ProcessRestrictions {
3474 #Blow away ticket aliases since we'll need to regenerate them for
3476 delete $self->{'TicketAliases'};
3477 delete $self->{'items_array'};
3478 delete $self->{'item_map'};
3479 delete $self->{'raw_rows'};
3480 delete $self->{'rows'};
3481 delete $self->{'count_all'};
3483 my $sql = $self->Query; # Violating the _SQL namespace
3484 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3486 # "Restrictions to Clauses Branch\n";
3487 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3489 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3493 $sql = $self->ClausesToSQL($clauseRef);
3494 $self->FromSQL($sql) if $sql;
3498 $self->{'RecalcTicketLimits'} = 0;
3502 =head2 _BuildItemMap
3504 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3505 display search nav quickly.
3512 my $window = RT->Config->Get('TicketsItemMapSize');
3514 $self->{'item_map'} = {};
3516 my $items = $self->ItemsArrayRefWindow( $window );
3517 return unless $items && @$items;
3520 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3521 for ( my $i = 0; $i < @$items; $i++ ) {
3522 my $item = $items->[$i];
3523 my $id = $item->EffectiveId;
3524 $self->{'item_map'}{$id}{'defined'} = 1;
3525 $self->{'item_map'}{$id}{'prev'} = $prev;
3526 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3530 $self->{'item_map'}{'last'} = $prev
3531 if !$window || @$items < $window;
3536 Returns an a map of all items found by this search. The map is a hash
3540 first => <first ticket id found>,
3541 last => <last ticket id found or undef>,
3544 prev => <the ticket id found before>,
3545 next => <the ticket id found after>,
3557 $self->_BuildItemMap unless $self->{'item_map'};
3558 return $self->{'item_map'};
3566 =head2 PrepForSerialization
3568 You don't want to serialize a big tickets object, as
3569 the {items} hash will be instantly invalid _and_ eat
3574 sub PrepForSerialization {
3576 delete $self->{'items'};
3577 delete $self->{'items_array'};
3578 $self->RedoSearch();
3583 RT::Tickets supports several flags which alter search behavior:
3586 allow_deleted_search (Otherwise never show deleted tickets in search results)
3587 looking_at_type (otherwise limit to type=ticket)
3589 These flags are set by calling
3591 $tickets->{'flagname'} = 1;
3593 BUG: There should be an API for this