1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
51 # - Decimated ProcessRestrictions and broke it into multiple
52 # functions joined by a LUT
53 # - Semi-Generic SQL stuff moved to another file
55 # Known Issues: FIXME!
57 # - ClearRestrictions and Reinitialization is messy and unclear. The
58 # only good way to do it is to create a new RT::Tickets object.
62 RT::Tickets - A collection of Ticket objects
68 my $tickets = new RT::Tickets($CurrentUser);
72 A collection of RT::Tickets.
82 no warnings qw(redefine);
85 use DBIx::SearchBuilder::Unique;
87 # Configuration Tables:
89 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
92 our %FIELD_METADATA = (
93 Status => [ 'ENUM', ], #loc_left_pair
94 Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
95 Type => [ 'ENUM', ], #loc_left_pair
96 Creator => [ 'ENUM' => 'User', ], #loc_left_pair
97 LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
98 Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
99 EffectiveId => [ 'INT', ], #loc_left_pair
100 id => [ 'ID', ], #loc_left_pair
101 InitialPriority => [ 'INT', ], #loc_left_pair
102 FinalPriority => [ 'INT', ], #loc_left_pair
103 Priority => [ 'INT', ], #loc_left_pair
104 TimeLeft => [ 'INT', ], #loc_left_pair
105 TimeWorked => [ 'INT', ], #loc_left_pair
106 TimeEstimated => [ 'INT', ], #loc_left_pair
108 Linked => [ 'LINK' ], #loc_left_pair
109 LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
110 LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
111 MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
112 DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
113 RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
114 HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
115 DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
116 DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
117 ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
118 Told => [ 'DATE' => 'Told', ], #loc_left_pair
119 Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
120 Started => [ 'DATE' => 'Started', ], #loc_left_pair
121 Due => [ 'DATE' => 'Due', ], #loc_left_pair
122 Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
123 LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
124 Created => [ 'DATE' => 'Created', ], #loc_left_pair
125 Subject => [ 'STRING', ], #loc_left_pair
126 Content => [ 'TRANSFIELD', ], #loc_left_pair
127 ContentType => [ 'TRANSFIELD', ], #loc_left_pair
128 Filename => [ 'TRANSFIELD', ], #loc_left_pair
129 TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
130 Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
131 Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
132 Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
133 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
134 Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
135 QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
136 QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
137 QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
138 CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
139 DateCustomFieldValue => [ 'DATECUSTOMFIELD', ],
140 CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
141 CF => [ 'CUSTOMFIELD', ], #loc_left_pair
142 Updated => [ 'TRANSDATE', ], #loc_left_pair
143 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
144 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
145 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
146 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
147 HasAttribute => [ 'HASATTRIBUTE', 1 ],
148 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
151 # Mapping of Field Type to Function
153 ENUM => \&_EnumLimit,
156 LINK => \&_LinkLimit,
157 DATE => \&_DateLimit,
158 STRING => \&_StringLimit,
159 TRANSFIELD => \&_TransLimit,
160 TRANSDATE => \&_TransDateLimit,
161 WATCHERFIELD => \&_WatcherLimit,
162 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
163 CUSTOMFIELD => \&_CustomFieldLimit,
164 DATECUSTOMFIELD => \&_DateCustomFieldLimit,
165 HASATTRIBUTE => \&_HasAttributeLimit,
167 our %can_bundle = ();# WATCHERFIELD => "yes", );
169 # Default EntryAggregator per type
170 # if you specify OP, you must specify all valid OPs
211 # Helper functions for passing the above lexically scoped tables above
212 # into Tickets_Overlay_SQL.
213 sub FIELDS { return \%FIELD_METADATA }
214 sub dispatch { return \%dispatch }
215 sub can_bundle { return \%can_bundle }
217 # Bring in the clowns.
218 require RT::Tickets_Overlay_SQL;
222 our @SORTFIELDS = qw(id Status
224 Owner Created Due Starts Started
226 Resolved LastUpdated Priority TimeWorked TimeLeft);
230 Returns the list of fields that lists of tickets can easily be sorted by
236 return (@SORTFIELDS);
241 # BEGIN SQL STUFF *********************************
246 $self->SUPER::CleanSlate( @_ );
247 delete $self->{$_} foreach qw(
249 _sql_group_members_aliases
250 _sql_object_cfv_alias
251 _sql_role_group_aliases
254 _sql_u_watchers_alias_for_sort
255 _sql_u_watchers_aliases
256 _sql_current_user_can_see_applied
260 =head1 Limit Helper Routines
262 These routines are the targets of a dispatch table depending on the
263 type of field. They all share the same signature:
265 my ($self,$field,$op,$value,@rest) = @_;
267 The values in @rest should be suitable for passing directly to
268 DBIx::SearchBuilder::Limit.
270 Essentially they are an expanded/broken out (and much simplified)
271 version of what ProcessRestrictions used to do. They're also much
272 more clearly delineated by the TYPE of field being processed.
281 my ( $sb, $field, $op, $value, @rest ) = @_;
283 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
285 die "Invalid operator $op for __Bookmarked__ search on $field"
286 unless $op =~ /^(=|!=)$/;
289 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
290 $tmp = $tmp->Content if $tmp;
295 return $sb->_SQLLimit(
302 # as bookmarked tickets can be merged we have to use a join
303 # but it should be pretty lightweight
304 my $tickets_alias = $sb->Join(
309 FIELD2 => 'EffectiveId',
313 my $ea = $op eq '='? 'OR': 'AND';
314 foreach my $id ( sort @bookmarks ) {
316 ALIAS => $tickets_alias,
320 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
328 Handle Fields which are limited to certain values, and potentially
329 need to be looked up from another class.
331 This subroutine actually handles two different kinds of fields. For
332 some the user is responsible for limiting the values. (i.e. Status,
335 For others, the value specified by the user will be looked by via
339 name of class to lookup in (Optional)
344 my ( $sb, $field, $op, $value, @rest ) = @_;
346 # SQL::Statement changes != to <>. (Can we remove this now?)
347 $op = "!=" if $op eq "<>";
349 die "Invalid Operation: $op for $field"
353 my $meta = $FIELD_METADATA{$field};
354 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
355 my $class = "RT::" . $meta->[1];
356 my $o = $class->new( $sb->CurrentUser );
370 Handle fields where the values are limited to integers. (For example,
371 Priority, TimeWorked.)
379 my ( $sb, $field, $op, $value, @rest ) = @_;
381 die "Invalid Operator $op for $field"
382 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
394 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
397 1: Direction (From, To)
398 2: Link Type (MemberOf, DependsOn, RefersTo)
403 my ( $sb, $field, $op, $value, @rest ) = @_;
405 my $meta = $FIELD_METADATA{$field};
406 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
409 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
413 $is_null = 1 if !$value || $value =~ /^null$/io;
415 my $direction = $meta->[1] || '';
416 my ($matchfield, $linkfield) = ('', '');
417 if ( $direction eq 'To' ) {
418 ($matchfield, $linkfield) = ("Target", "Base");
420 elsif ( $direction eq 'From' ) {
421 ($matchfield, $linkfield) = ("Base", "Target");
423 elsif ( $direction ) {
424 die "Invalid link direction '$direction' for $field\n";
427 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
429 'LinkedFrom', $op, $value, @rest,
430 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
438 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
440 elsif ( $value =~ /\D/ ) {
443 $matchfield = "Local$matchfield" if $is_local;
445 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
446 # SELECT main.* FROM Tickets main
447 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
448 # AND(main.id = Links_1.LocalTarget))
449 # WHERE Links_1.LocalBase IS NULL;
452 my $linkalias = $sb->Join(
457 FIELD2 => 'Local' . $linkfield
460 LEFTJOIN => $linkalias,
468 FIELD => $matchfield,
475 my $linkalias = $sb->Join(
480 FIELD2 => 'Local' . $linkfield
483 LEFTJOIN => $linkalias,
489 LEFTJOIN => $linkalias,
490 FIELD => $matchfield,
497 FIELD => $matchfield,
498 OPERATOR => $is_negative? 'IS': 'IS NOT',
507 Handle date fields. (Created, LastTold..)
510 1: type of link. (Probably not necessary.)
515 my ( $sb, $field, $op, $value, @rest ) = @_;
517 die "Invalid Date Op: $op"
518 unless $op =~ /^(=|>|<|>=|<=)$/;
520 my $meta = $FIELD_METADATA{$field};
521 die "Incorrect Meta Data for $field"
522 unless ( defined $meta->[1] );
524 my $date = RT::Date->new( $sb->CurrentUser );
525 $date->Set( Format => 'unknown', Value => $value );
529 # if we're specifying =, that means we want everything on a
530 # particular single day. in the database, we need to check for >
531 # and < the edges of that day.
533 $date->SetToMidnight( Timezone => 'server' );
534 my $daystart = $date->ISO;
536 my $dayend = $date->ISO;
552 ENTRYAGGREGATOR => 'AND',
570 Handle simple fields which are just strings. (Subject,Type)
578 my ( $sb, $field, $op, $value, @rest ) = @_;
582 # =, !=, LIKE, NOT LIKE
583 if ( (!defined $value || !length $value)
584 && lc($op) ne 'is' && lc($op) ne 'is not'
585 && RT->Config->Get('DatabaseType') eq 'Oracle'
587 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
588 $op = $negative? 'IS NOT': 'IS';
601 =head2 _TransDateLimit
603 Handle fields limiting based on Transaction Date.
605 The inpupt value must be in a format parseable by Time::ParseDate
612 # This routine should really be factored into translimit.
613 sub _TransDateLimit {
614 my ( $sb, $field, $op, $value, @rest ) = @_;
616 # See the comments for TransLimit, they apply here too
618 unless ( $sb->{_sql_transalias} ) {
619 $sb->{_sql_transalias} = $sb->Join(
622 TABLE2 => 'Transactions',
623 FIELD2 => 'ObjectId',
626 ALIAS => $sb->{_sql_transalias},
627 FIELD => 'ObjectType',
628 VALUE => 'RT::Ticket',
629 ENTRYAGGREGATOR => 'AND',
633 my $date = RT::Date->new( $sb->CurrentUser );
634 $date->Set( Format => 'unknown', Value => $value );
639 # if we're specifying =, that means we want everything on a
640 # particular single day. in the database, we need to check for >
641 # and < the edges of that day.
643 $date->SetToMidnight( Timezone => 'server' );
644 my $daystart = $date->ISO;
646 my $dayend = $date->ISO;
649 ALIAS => $sb->{_sql_transalias},
657 ALIAS => $sb->{_sql_transalias},
663 ENTRYAGGREGATOR => 'AND',
668 # not searching for a single day
671 #Search for the right field
673 ALIAS => $sb->{_sql_transalias},
687 Limit based on the Content of a transaction or the ContentType.
696 # Content, ContentType, Filename
698 # If only this was this simple. We've got to do something
701 #Basically, we want to make sure that the limits apply to
702 #the same attachment, rather than just another attachment
703 #for the same ticket, no matter how many clauses we lump
704 #on. We put them in TicketAliases so that they get nuked
705 #when we redo the join.
707 # In the SQL, we might have
708 # (( Content = foo ) or ( Content = bar AND Content = baz ))
709 # The AND group should share the same Alias.
711 # Actually, maybe it doesn't matter. We use the same alias and it
712 # works itself out? (er.. different.)
714 # Steal more from _ProcessRestrictions
716 # FIXME: Maybe look at the previous FooLimit call, and if it was a
717 # TransLimit and EntryAggregator == AND, reuse the Aliases?
719 # Or better - store the aliases on a per subclause basis - since
720 # those are going to be the things we want to relate to each other,
723 # maybe we should not allow certain kinds of aggregation of these
724 # clauses and do a psuedo regex instead? - the problem is getting
725 # them all into the same subclause when you have (A op B op C) - the
726 # way they get parsed in the tree they're in different subclauses.
728 my ( $self, $field, $op, $value, %rest ) = @_;
730 unless ( $self->{_sql_transalias} ) {
731 $self->{_sql_transalias} = $self->Join(
734 TABLE2 => 'Transactions',
735 FIELD2 => 'ObjectId',
738 ALIAS => $self->{_sql_transalias},
739 FIELD => 'ObjectType',
740 VALUE => 'RT::Ticket',
741 ENTRYAGGREGATOR => 'AND',
744 unless ( defined $self->{_sql_trattachalias} ) {
745 $self->{_sql_trattachalias} = $self->_SQLJoin(
746 TYPE => 'LEFT', # not all txns have an attachment
747 ALIAS1 => $self->{_sql_transalias},
749 TABLE2 => 'Attachments',
750 FIELD2 => 'TransactionId',
754 #Search for the right field
755 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
759 ALIAS => $self->{_sql_trattachalias},
766 ENTRYAGGREGATOR => 'AND',
767 ALIAS => $self->{_sql_trattachalias},
776 ALIAS => $self->{_sql_trattachalias},
789 Handle watcher limits. (Requestor, CC, etc..)
805 my $meta = $FIELD_METADATA{ $field };
806 my $type = $meta->[1] || '';
807 my $class = $meta->[2] || 'Ticket';
809 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
810 # search by id and Name at the same time, this is workaround
811 # to preserve backward compatibility
812 if ( $field eq 'Owner' ) {
813 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
814 my $o = RT::User->new( $self->CurrentUser );
815 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
816 $o->$method( $value );
825 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
835 $rest{SUBKEY} ||= 'EmailAddress';
837 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
840 if ( $op =~ /^IS(?: NOT)?$/ ) {
841 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
842 # to avoid joining the table Users into the query, we just join GM
843 # and make sure we don't match records where group is member of itself
845 LEFTJOIN => $group_members,
848 VALUE => "$group_members.MemberId",
852 ALIAS => $group_members,
859 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
861 $op =~ s/!|NOT\s+//i;
863 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
864 # "X = 'Y'" matches more then one user so we try to fetch two records and
865 # do the right thing when there is only one exist and semi-working solution
867 my $users_obj = RT::Users->new( $self->CurrentUser );
869 FIELD => $rest{SUBKEY},
874 $users_obj->RowsPerPage(2);
875 my @users = @{ $users_obj->ItemsArrayRef };
877 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
880 $uid = $users[0]->id if @users;
882 LEFTJOIN => $group_members,
883 ALIAS => $group_members,
889 ALIAS => $group_members,
896 LEFTJOIN => $group_members,
899 VALUE => "$group_members.MemberId",
902 my $users = $self->Join(
904 ALIAS1 => $group_members,
905 FIELD1 => 'MemberId',
912 FIELD => $rest{SUBKEY},
926 my $group_members = $self->_GroupMembersJoin(
927 GroupsAlias => $groups,
931 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
933 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
934 $self->NewAlias('Users');
936 LEFTJOIN => $group_members,
937 ALIAS => $group_members,
939 VALUE => "$users.id",
944 # we join users table without adding some join condition between tables,
945 # the only conditions we have are conditions on the table iteslf,
946 # for example Users.EmailAddress = 'x'. We should add this condition to
947 # the top level of the query and bundle it with another similar conditions,
948 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
949 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
952 SUBCLAUSE => '_sql_u_watchers_'. $users,
954 FIELD => $rest{'SUBKEY'},
959 # A condition which ties Users and Groups (role groups) is a left join condition
960 # of CachedGroupMembers table. To get correct results of the query we check
961 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
964 ALIAS => $group_members,
966 OPERATOR => 'IS NOT',
973 sub _RoleGroupsJoin {
975 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
976 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
977 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
980 # we always have watcher groups for ticket, so we use INNER join
981 my $groups = $self->Join(
983 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
985 FIELD2 => 'Instance',
986 ENTRYAGGREGATOR => 'AND',
992 VALUE => 'RT::'. $args{'Class'} .'-Role',
998 VALUE => $args{'Type'},
1001 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1002 unless $args{'New'};
1007 sub _GroupMembersJoin {
1009 my %args = (New => 1, GroupsAlias => undef, @_);
1011 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1012 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1015 my $alias = $self->Join(
1017 ALIAS1 => $args{'GroupsAlias'},
1019 TABLE2 => 'CachedGroupMembers',
1020 FIELD2 => 'GroupId',
1021 ENTRYAGGREGATOR => 'AND',
1024 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1025 unless $args{'New'};
1032 Helper function which provides joins to a watchers table both for limits
1039 my $type = shift || '';
1042 my $groups = $self->_RoleGroupsJoin( Type => $type );
1043 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1044 # XXX: work around, we must hide groups that
1045 # are members of the role group we search in,
1046 # otherwise them result in wrong NULLs in Users
1047 # table and break ordering. Now, we know that
1048 # RT doesn't allow to add groups as members of the
1049 # ticket roles, so we just hide entries in CGM table
1050 # with MemberId == GroupId from results
1051 $self->SUPER::Limit(
1052 LEFTJOIN => $group_members,
1055 VALUE => "$group_members.MemberId",
1058 my $users = $self->Join(
1060 ALIAS1 => $group_members,
1061 FIELD1 => 'MemberId',
1065 return ($groups, $group_members, $users);
1068 =head2 _WatcherMembershipLimit
1070 Handle watcher membership limits, i.e. whether the watcher belongs to a
1071 specific group or not.
1074 1: Field to query on
1076 SELECT DISTINCT main.*
1080 CachedGroupMembers CachedGroupMembers_2,
1083 (main.EffectiveId = main.id)
1085 (main.Status != 'deleted')
1087 (main.Type = 'ticket')
1090 (Users_3.EmailAddress = '22')
1092 (Groups_1.Domain = 'RT::Ticket-Role')
1094 (Groups_1.Type = 'RequestorGroup')
1097 Groups_1.Instance = main.id
1099 Groups_1.id = CachedGroupMembers_2.GroupId
1101 CachedGroupMembers_2.MemberId = Users_3.id
1102 ORDER BY main.id ASC
1107 sub _WatcherMembershipLimit {
1108 my ( $self, $field, $op, $value, @rest ) = @_;
1113 my $groups = $self->NewAlias('Groups');
1114 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1115 my $users = $self->NewAlias('Users');
1116 my $memberships = $self->NewAlias('CachedGroupMembers');
1118 if ( ref $field ) { # gross hack
1119 my @bundle = @$field;
1121 for my $chunk (@bundle) {
1122 ( $field, $op, $value, @rest ) = @$chunk;
1124 ALIAS => $memberships,
1135 ALIAS => $memberships,
1143 # {{{ Tie to groups for tickets we care about
1147 VALUE => 'RT::Ticket-Role',
1148 ENTRYAGGREGATOR => 'AND'
1153 FIELD1 => 'Instance',
1160 # If we care about which sort of watcher
1161 my $meta = $FIELD_METADATA{$field};
1162 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1169 ENTRYAGGREGATOR => 'AND'
1176 ALIAS2 => $groupmembers,
1181 ALIAS1 => $groupmembers,
1182 FIELD1 => 'MemberId',
1188 ALIAS1 => $memberships,
1189 FIELD1 => 'MemberId',
1198 =head2 _CustomFieldDecipher
1200 Try and turn a CF descriptor into (cfid, cfname) object pair.
1204 sub _CustomFieldDecipher {
1205 my ($self, $string) = @_;
1207 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
1208 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1212 my $q = RT::Queue->new( $self->CurrentUser );
1216 # $queue = $q->Name; # should we normalize the queue?
1217 $cf = $q->CustomField( $field );
1220 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1224 elsif ( $field =~ /\D/ ) {
1226 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1227 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1228 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1230 # if there is more then one field the current user can
1231 # see with the same name then we shouldn't return cf object
1232 # as we don't know which one to use
1235 $cf = undef if $cfs->Next;
1239 $cf = RT::CustomField->new( $self->CurrentUser );
1240 $cf->Load( $field );
1243 return ($queue, $field, $cf, $column);
1246 =head2 _CustomFieldJoin
1248 Factor out the Join of custom fields so we can use it for sorting too
1252 sub _CustomFieldJoin {
1253 my ($self, $cfkey, $cfid, $field) = @_;
1254 # Perform one Join per CustomField
1255 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1256 $self->{_sql_cf_alias}{$cfkey} )
1258 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1259 $self->{_sql_cf_alias}{$cfkey} );
1262 my ($TicketCFs, $CFs);
1264 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1268 TABLE2 => 'ObjectCustomFieldValues',
1269 FIELD2 => 'ObjectId',
1271 $self->SUPER::Limit(
1272 LEFTJOIN => $TicketCFs,
1273 FIELD => 'CustomField',
1275 ENTRYAGGREGATOR => 'AND'
1279 my $ocfalias = $self->Join(
1282 TABLE2 => 'ObjectCustomFields',
1283 FIELD2 => 'ObjectId',
1286 $self->SUPER::Limit(
1287 LEFTJOIN => $ocfalias,
1288 ENTRYAGGREGATOR => 'OR',
1289 FIELD => 'ObjectId',
1293 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1295 ALIAS1 => $ocfalias,
1296 FIELD1 => 'CustomField',
1297 TABLE2 => 'CustomFields',
1300 $self->SUPER::Limit(
1302 ENTRYAGGREGATOR => 'AND',
1303 FIELD => 'LookupType',
1304 VALUE => 'RT::Queue-RT::Ticket',
1306 $self->SUPER::Limit(
1308 ENTRYAGGREGATOR => 'AND',
1313 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1317 TABLE2 => 'ObjectCustomFieldValues',
1318 FIELD2 => 'CustomField',
1320 $self->SUPER::Limit(
1321 LEFTJOIN => $TicketCFs,
1322 FIELD => 'ObjectId',
1325 ENTRYAGGREGATOR => 'AND',
1328 $self->SUPER::Limit(
1329 LEFTJOIN => $TicketCFs,
1330 FIELD => 'ObjectType',
1331 VALUE => 'RT::Ticket',
1332 ENTRYAGGREGATOR => 'AND'
1334 $self->SUPER::Limit(
1335 LEFTJOIN => $TicketCFs,
1336 FIELD => 'Disabled',
1339 ENTRYAGGREGATOR => 'AND'
1342 return ($TicketCFs, $CFs);
1345 =head2 _DateCustomFieldLimit
1347 Limit based on CustomFields of type Date
1354 sub _DateCustomFieldLimit {
1355 my ( $self, $_field, $op, $value, %rest ) = @_;
1357 my $field = $rest{'SUBKEY'} || die "No field specified";
1359 # For our sanity, we can only limit on one queue at a time
1361 my ($queue, $cfid, $column);
1362 ($queue, $field, $cfid, $column) = $self->_CustomFieldDecipher( $field );
1364 # If we're trying to find custom fields that don't match something, we
1365 # want tickets where the custom field has no value at all. Note that
1366 # we explicitly don't include the "IS NULL" case, since we would
1367 # otherwise end up with a redundant clause.
1369 my $null_columns_ok;
1370 if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
1371 $null_columns_ok = 1;
1374 my $cfkey = $cfid ? $cfid : "$queue.$field";
1375 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1379 if ( $CFs && !$cfid ) {
1380 $self->SUPER::Limit(
1384 ENTRYAGGREGATOR => 'AND',
1388 $self->_OpenParen if $null_columns_ok;
1390 my $date = RT::Date->new( $self->CurrentUser );
1391 $date->Set( Format => 'unknown', Value => $value );
1395 # if we're specifying =, that means we want everything on a
1396 # particular single day. in the database, we need to check for >
1397 # and < the edges of that day.
1399 $date->SetToMidnight( Timezone => 'server' );
1400 my $daystart = $date->ISO;
1402 my $dayend = $date->ISO;
1407 ALIAS => $TicketCFs,
1415 ALIAS => $TicketCFs,
1420 ENTRYAGGREGATOR => 'AND',
1428 ALIAS => $TicketCFs,
1431 VALUE => $date->ISO,
1440 =head2 _CustomFieldLimit
1442 Limit based on CustomFields
1449 sub _CustomFieldLimit {
1450 my ( $self, $_field, $op, $value, %rest ) = @_;
1452 my $field = $rest{'SUBKEY'} || die "No field specified";
1454 # For our sanity, we can only limit on one queue at a time
1456 my ($queue, $cfid, $cf, $column);
1457 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1458 $cfid = $cf ? $cf->id : 0 ;
1460 # If we're trying to find custom fields that don't match something, we
1461 # want tickets where the custom field has no value at all. Note that
1462 # we explicitly don't include the "IS NULL" case, since we would
1463 # otherwise end up with a redundant clause.
1465 my ($negative_op, $null_op, $inv_op, $range_op)
1466 = $self->ClassifySQLOperation( $op );
1470 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1471 return 'MATCHES' if $op eq '=';
1472 return 'NOT MATCHES' if $op eq '!=';
1476 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1478 my $cfkey = $cfid ? $cfid : "$queue.$field";
1480 if ( $null_op && !$column ) {
1481 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1482 # we can reuse our default joins for this operation
1483 # with column specified we have different situation
1484 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1487 ALIAS => $TicketCFs,
1496 OPERATOR => 'IS NOT',
1499 ENTRYAGGREGATOR => 'AND',
1503 elsif ( !$negative_op || $single_value ) {
1504 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1505 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1512 # if column is defined then deal only with it
1513 # otherwise search in Content and in LargeContent
1516 ALIAS => $TicketCFs,
1518 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1523 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1524 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1526 ALIAS => $TicketCFs,
1535 ALIAS => $TicketCFs,
1539 ENTRYAGGREGATOR => 'OR'
1542 ALIAS => $TicketCFs,
1546 ENTRYAGGREGATOR => 'OR'
1550 ALIAS => $TicketCFs,
1551 FIELD => 'LargeContent',
1552 OPERATOR => $fix_op->($op),
1554 ENTRYAGGREGATOR => 'AND',
1560 ALIAS => $TicketCFs,
1570 ALIAS => $TicketCFs,
1574 ENTRYAGGREGATOR => 'OR'
1577 ALIAS => $TicketCFs,
1581 ENTRYAGGREGATOR => 'OR'
1585 ALIAS => $TicketCFs,
1586 FIELD => 'LargeContent',
1587 OPERATOR => $fix_op->($op),
1589 ENTRYAGGREGATOR => 'AND',
1595 # XXX: if we join via CustomFields table then
1596 # because of order of left joins we get NULLs in
1597 # CF table and then get nulls for those records
1598 # in OCFVs table what result in wrong results
1599 # as decifer method now tries to load a CF then
1600 # we fall into this situation only when there
1601 # are more than one CF with the name in the DB.
1602 # the same thing applies to order by call.
1603 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1604 # we want treat IS NULL as (not applies or has
1609 OPERATOR => 'IS NOT',
1612 ENTRYAGGREGATOR => 'AND',
1618 ALIAS => $TicketCFs,
1619 FIELD => $column || 'Content',
1623 ENTRYAGGREGATOR => 'OR',
1630 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1631 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1634 $op =~ s/!|NOT\s+//i;
1636 # if column is defined then deal only with it
1637 # otherwise search in Content and in LargeContent
1639 $self->SUPER::Limit(
1640 LEFTJOIN => $TicketCFs,
1641 ALIAS => $TicketCFs,
1643 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1648 $self->SUPER::Limit(
1649 LEFTJOIN => $TicketCFs,
1650 ALIAS => $TicketCFs,
1658 ALIAS => $TicketCFs,
1667 sub _HasAttributeLimit {
1668 my ( $self, $field, $op, $value, %rest ) = @_;
1670 my $alias = $self->Join(
1674 TABLE2 => 'Attributes',
1675 FIELD2 => 'ObjectId',
1677 $self->SUPER::Limit(
1679 FIELD => 'ObjectType',
1680 VALUE => 'RT::Ticket',
1681 ENTRYAGGREGATOR => 'AND'
1683 $self->SUPER::Limit(
1688 ENTRYAGGREGATOR => 'AND'
1694 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1701 # End Helper Functions
1703 # End of SQL Stuff -------------------------------------------------
1705 # {{{ Allow sorting on watchers
1707 =head2 OrderByCols ARRAY
1709 A modified version of the OrderBy method which automatically joins where
1710 C<ALIAS> is set to the name of a watcher type.
1721 foreach my $row (@args) {
1722 if ( $row->{ALIAS} ) {
1726 if ( $row->{FIELD} !~ /\./ ) {
1727 my $meta = $self->FIELDS->{ $row->{FIELD} };
1733 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1734 my $alias = $self->Join(
1737 FIELD1 => $row->{'FIELD'},
1741 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1742 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1743 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1745 my $alias = $self->Join(
1748 FIELD1 => $row->{'FIELD'},
1752 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1759 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1760 my $meta = $self->FIELDS->{$field};
1761 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1762 # cache alias as we want to use one alias per watcher type for sorting
1763 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1765 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1766 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1768 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1769 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1770 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1771 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1772 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1773 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1774 # this is described in _CustomFieldLimit
1778 OPERATOR => 'IS NOT',
1781 ENTRYAGGREGATOR => 'AND',
1784 # For those cases where we are doing a join against the
1785 # CF name, and don't have a CFid, use Unique to make sure
1786 # we don't show duplicate tickets. NOTE: I'm pretty sure
1787 # this will stay mixed in for the life of the
1788 # class/package, and not just for the life of the object.
1789 # Potential performance issue.
1790 require DBIx::SearchBuilder::Unique;
1791 DBIx::SearchBuilder::Unique->import;
1793 my $CFvs = $self->Join(
1795 ALIAS1 => $TicketCFs,
1796 FIELD1 => 'CustomField',
1797 TABLE2 => 'CustomFieldValues',
1798 FIELD2 => 'CustomField',
1800 $self->SUPER::Limit(
1804 VALUE => $TicketCFs . ".Content",
1805 ENTRYAGGREGATOR => 'AND'
1808 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1809 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1810 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1811 # PAW logic is "reversed"
1813 if (exists $row->{ORDER} ) {
1814 my $o = $row->{ORDER};
1815 delete $row->{ORDER};
1816 $order = "DESC" if $o =~ /asc/i;
1819 # Ticket.Owner 1 0 X
1820 # Unowned Tickets 0 1 X
1823 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1824 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1825 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1826 push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
1828 push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
1832 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1834 } elsif ( $field eq 'Customer' ) { #Freeside
1836 my $linkalias = $self->Join(
1841 FIELD2 => 'LocalBase'
1844 $self->SUPER::Limit(
1845 LEFTJOIN => $linkalias,
1848 VALUE => 'MemberOf',
1850 $self->SUPER::Limit(
1851 LEFTJOIN => $linkalias,
1853 OPERATOR => 'STARTSWITH',
1854 VALUE => 'freeside://freeside/cust_main/',
1857 #if there was a Links.RemoteTarget int, this bs wouldn't be necessary
1858 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1859 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1860 $custnum_sql .= 'SIGNED INTEGER)';
1863 $custnum_sql .= 'INTEGER)';
1866 if ( $subkey eq 'Number' ) {
1870 FIELD => $custnum_sql,
1873 } elsif ( $subkey eq 'Name' ) {
1875 my $custalias = $self->Join(
1877 EXPRESSION => $custnum_sql,
1878 TABLE2 => 'cust_main',
1879 FIELD2 => 'custnum',
1883 my $field = "COALESCE( $custalias.company,
1884 $custalias.last || ', ' || $custalias.first
1887 push @res, { %$row, ALIAS => '', FIELD => $field };
1897 return $self->SUPER::OrderByCols(@res);
1902 # {{{ Limit the result set based on content
1908 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1909 Generally best called from LimitFoo methods
1919 DESCRIPTION => undef,
1922 $args{'DESCRIPTION'} = $self->loc(
1923 "[_1] [_2] [_3]", $args{'FIELD'},
1924 $args{'OPERATOR'}, $args{'VALUE'}
1926 if ( !defined $args{'DESCRIPTION'} );
1928 my $index = $self->_NextIndex;
1930 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1932 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1934 $self->{'RecalcTicketLimits'} = 1;
1936 # If we're looking at the effective id, we don't want to append the other clause
1937 # which limits us to tickets where id = effective id
1938 if ( $args{'FIELD'} eq 'EffectiveId'
1939 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1941 $self->{'looking_at_effective_id'} = 1;
1944 if ( $args{'FIELD'} eq 'Type'
1945 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1947 $self->{'looking_at_type'} = 1;
1957 Returns a frozen string suitable for handing back to ThawLimits.
1961 sub _FreezeThawKeys {
1962 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1966 # {{{ sub FreezeLimits
1971 require MIME::Base64;
1972 MIME::Base64::base64_encode(
1973 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1980 Take a frozen Limits string generated by FreezeLimits and make this tickets
1981 object have that set of limits.
1985 # {{{ sub ThawLimits
1991 #if we don't have $in, get outta here.
1992 return undef unless ($in);
1994 $self->{'RecalcTicketLimits'} = 1;
1997 require MIME::Base64;
1999 #We don't need to die if the thaw fails.
2000 @{$self}{ $self->_FreezeThawKeys }
2001 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2003 $RT::Logger->error($@) if $@;
2009 # {{{ Limit by enum or foreign key
2011 # {{{ sub LimitQueue
2015 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2016 OPERATOR is one of = or !=. (It defaults to =).
2017 VALUE is a queue id or Name.
2030 #TODO VALUE should also take queue objects
2031 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2032 my $queue = new RT::Queue( $self->CurrentUser );
2033 $queue->Load( $args{'VALUE'} );
2034 $args{'VALUE'} = $queue->Id;
2037 # What if they pass in an Id? Check for isNum() and convert to
2040 #TODO check for a valid queue here
2044 VALUE => $args{'VALUE'},
2045 OPERATOR => $args{'OPERATOR'},
2046 DESCRIPTION => join(
2047 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2055 # {{{ sub LimitStatus
2059 Takes a paramhash with the fields OPERATOR and VALUE.
2060 OPERATOR is one of = or !=.
2063 RT adds Status != 'deleted' until object has
2064 allow_deleted_search internal property set.
2065 $tickets->{'allow_deleted_search'} = 1;
2066 $tickets->LimitStatus( VALUE => 'deleted' );
2078 VALUE => $args{'VALUE'},
2079 OPERATOR => $args{'OPERATOR'},
2080 DESCRIPTION => join( ' ',
2081 $self->loc('Status'), $args{'OPERATOR'},
2082 $self->loc( $args{'VALUE'} ) ),
2088 # {{{ sub IgnoreType
2092 If called, this search will not automatically limit the set of results found
2093 to tickets of type "Ticket". Tickets of other types, such as "project" and
2094 "approval" will be found.
2101 # Instead of faking a Limit that later gets ignored, fake up the
2102 # fact that we're already looking at type, so that the check in
2103 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2105 # $self->LimitType(VALUE => '__any');
2106 $self->{looking_at_type} = 1;
2115 Takes a paramhash with the fields OPERATOR and VALUE.
2116 OPERATOR is one of = or !=, it defaults to "=".
2117 VALUE is a string to search for in the type of the ticket.
2132 VALUE => $args{'VALUE'},
2133 OPERATOR => $args{'OPERATOR'},
2134 DESCRIPTION => join( ' ',
2135 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2143 # {{{ Limit by string field
2145 # {{{ sub LimitSubject
2149 Takes a paramhash with the fields OPERATOR and VALUE.
2150 OPERATOR is one of = or !=.
2151 VALUE is a string to search for in the subject of the ticket.
2160 VALUE => $args{'VALUE'},
2161 OPERATOR => $args{'OPERATOR'},
2162 DESCRIPTION => join( ' ',
2163 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2171 # {{{ Limit based on ticket numerical attributes
2172 # Things that can be > < = !=
2178 Takes a paramhash with the fields OPERATOR and VALUE.
2179 OPERATOR is one of =, >, < or !=.
2180 VALUE is a ticket Id to search for
2193 VALUE => $args{'VALUE'},
2194 OPERATOR => $args{'OPERATOR'},
2196 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2202 # {{{ sub LimitPriority
2204 =head2 LimitPriority
2206 Takes a paramhash with the fields OPERATOR and VALUE.
2207 OPERATOR is one of =, >, < or !=.
2208 VALUE is a value to match the ticket\'s priority against
2216 FIELD => 'Priority',
2217 VALUE => $args{'VALUE'},
2218 OPERATOR => $args{'OPERATOR'},
2219 DESCRIPTION => join( ' ',
2220 $self->loc('Priority'),
2221 $args{'OPERATOR'}, $args{'VALUE'}, ),
2227 # {{{ sub LimitInitialPriority
2229 =head2 LimitInitialPriority
2231 Takes a paramhash with the fields OPERATOR and VALUE.
2232 OPERATOR is one of =, >, < or !=.
2233 VALUE is a value to match the ticket\'s initial priority against
2238 sub LimitInitialPriority {
2242 FIELD => 'InitialPriority',
2243 VALUE => $args{'VALUE'},
2244 OPERATOR => $args{'OPERATOR'},
2245 DESCRIPTION => join( ' ',
2246 $self->loc('Initial Priority'), $args{'OPERATOR'},
2253 # {{{ sub LimitFinalPriority
2255 =head2 LimitFinalPriority
2257 Takes a paramhash with the fields OPERATOR and VALUE.
2258 OPERATOR is one of =, >, < or !=.
2259 VALUE is a value to match the ticket\'s final priority against
2263 sub LimitFinalPriority {
2267 FIELD => 'FinalPriority',
2268 VALUE => $args{'VALUE'},
2269 OPERATOR => $args{'OPERATOR'},
2270 DESCRIPTION => join( ' ',
2271 $self->loc('Final Priority'), $args{'OPERATOR'},
2278 # {{{ sub LimitTimeWorked
2280 =head2 LimitTimeWorked
2282 Takes a paramhash with the fields OPERATOR and VALUE.
2283 OPERATOR is one of =, >, < or !=.
2284 VALUE is a value to match the ticket's TimeWorked attribute
2288 sub LimitTimeWorked {
2292 FIELD => 'TimeWorked',
2293 VALUE => $args{'VALUE'},
2294 OPERATOR => $args{'OPERATOR'},
2295 DESCRIPTION => join( ' ',
2296 $self->loc('Time Worked'),
2297 $args{'OPERATOR'}, $args{'VALUE'}, ),
2303 # {{{ sub LimitTimeLeft
2305 =head2 LimitTimeLeft
2307 Takes a paramhash with the fields OPERATOR and VALUE.
2308 OPERATOR is one of =, >, < or !=.
2309 VALUE is a value to match the ticket's TimeLeft attribute
2317 FIELD => 'TimeLeft',
2318 VALUE => $args{'VALUE'},
2319 OPERATOR => $args{'OPERATOR'},
2320 DESCRIPTION => join( ' ',
2321 $self->loc('Time Left'),
2322 $args{'OPERATOR'}, $args{'VALUE'}, ),
2330 # {{{ Limiting based on attachment attributes
2332 # {{{ sub LimitContent
2336 Takes a paramhash with the fields OPERATOR and VALUE.
2337 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2338 VALUE is a string to search for in the body of the ticket
2347 VALUE => $args{'VALUE'},
2348 OPERATOR => $args{'OPERATOR'},
2349 DESCRIPTION => join( ' ',
2350 $self->loc('Ticket content'), $args{'OPERATOR'},
2357 # {{{ sub LimitFilename
2359 =head2 LimitFilename
2361 Takes a paramhash with the fields OPERATOR and VALUE.
2362 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2363 VALUE is a string to search for in the body of the ticket
2371 FIELD => 'Filename',
2372 VALUE => $args{'VALUE'},
2373 OPERATOR => $args{'OPERATOR'},
2374 DESCRIPTION => join( ' ',
2375 $self->loc('Attachment filename'), $args{'OPERATOR'},
2381 # {{{ sub LimitContentType
2383 =head2 LimitContentType
2385 Takes a paramhash with the fields OPERATOR and VALUE.
2386 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2387 VALUE is a content type to search ticket attachments for
2391 sub LimitContentType {
2395 FIELD => 'ContentType',
2396 VALUE => $args{'VALUE'},
2397 OPERATOR => $args{'OPERATOR'},
2398 DESCRIPTION => join( ' ',
2399 $self->loc('Ticket content type'), $args{'OPERATOR'},
2408 # {{{ Limiting based on people
2410 # {{{ sub LimitOwner
2414 Takes a paramhash with the fields OPERATOR and VALUE.
2415 OPERATOR is one of = or !=.
2427 my $owner = new RT::User( $self->CurrentUser );
2428 $owner->Load( $args{'VALUE'} );
2430 # FIXME: check for a valid $owner
2433 VALUE => $args{'VALUE'},
2434 OPERATOR => $args{'OPERATOR'},
2435 DESCRIPTION => join( ' ',
2436 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2443 # {{{ Limiting watchers
2445 # {{{ sub LimitWatcher
2449 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2450 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2451 VALUE is a value to match the ticket\'s watcher email addresses against
2452 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2466 #build us up a description
2467 my ( $watcher_type, $desc );
2468 if ( $args{'TYPE'} ) {
2469 $watcher_type = $args{'TYPE'};
2472 $watcher_type = "Watcher";
2476 FIELD => $watcher_type,
2477 VALUE => $args{'VALUE'},
2478 OPERATOR => $args{'OPERATOR'},
2479 TYPE => $args{'TYPE'},
2480 DESCRIPTION => join( ' ',
2481 $self->loc($watcher_type),
2482 $args{'OPERATOR'}, $args{'VALUE'}, ),
2492 # {{{ Limiting based on links
2496 =head2 LimitLinkedTo
2498 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2499 TYPE limits the sort of link we want to search on
2501 TYPE = { RefersTo, MemberOf, DependsOn }
2503 TARGET is the id or URI of the TARGET of the link
2517 FIELD => 'LinkedTo',
2519 TARGET => $args{'TARGET'},
2520 TYPE => $args{'TYPE'},
2521 DESCRIPTION => $self->loc(
2522 "Tickets [_1] by [_2]",
2523 $self->loc( $args{'TYPE'} ),
2526 OPERATOR => $args{'OPERATOR'},
2532 # {{{ LimitLinkedFrom
2534 =head2 LimitLinkedFrom
2536 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2537 TYPE limits the sort of link we want to search on
2540 BASE is the id or URI of the BASE of the link
2544 sub LimitLinkedFrom {
2553 # translate RT2 From/To naming to RT3 TicketSQL naming
2554 my %fromToMap = qw(DependsOn DependentOn
2556 RefersTo ReferredToBy);
2558 my $type = $args{'TYPE'};
2559 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2562 FIELD => 'LinkedTo',
2564 BASE => $args{'BASE'},
2566 DESCRIPTION => $self->loc(
2567 "Tickets [_1] [_2]",
2568 $self->loc( $args{'TYPE'} ),
2571 OPERATOR => $args{'OPERATOR'},
2580 my $ticket_id = shift;
2581 return $self->LimitLinkedTo(
2583 TARGET => $ticket_id,
2590 # {{{ LimitHasMember
2591 sub LimitHasMember {
2593 my $ticket_id = shift;
2594 return $self->LimitLinkedFrom(
2596 BASE => "$ticket_id",
2597 TYPE => 'HasMember',
2604 # {{{ LimitDependsOn
2606 sub LimitDependsOn {
2608 my $ticket_id = shift;
2609 return $self->LimitLinkedTo(
2611 TARGET => $ticket_id,
2612 TYPE => 'DependsOn',
2619 # {{{ LimitDependedOnBy
2621 sub LimitDependedOnBy {
2623 my $ticket_id = shift;
2624 return $self->LimitLinkedFrom(
2627 TYPE => 'DependentOn',
2638 my $ticket_id = shift;
2639 return $self->LimitLinkedTo(
2641 TARGET => $ticket_id,
2649 # {{{ LimitReferredToBy
2651 sub LimitReferredToBy {
2653 my $ticket_id = shift;
2654 return $self->LimitLinkedFrom(
2657 TYPE => 'ReferredToBy',
2665 # {{{ limit based on ticket date attribtes
2669 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2671 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2673 OPERATOR is one of > or <
2674 VALUE is a date and time in ISO format in GMT
2675 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2677 There are also helper functions of the form LimitFIELD that eliminate
2678 the need to pass in a FIELD argument.
2692 #Set the description if we didn't get handed it above
2693 unless ( $args{'DESCRIPTION'} ) {
2694 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2695 . $args{'OPERATOR'} . " "
2696 . $args{'VALUE'} . " GMT";
2699 $self->Limit(%args);
2707 $self->LimitDate( FIELD => 'Created', @_ );
2712 $self->LimitDate( FIELD => 'Due', @_ );
2718 $self->LimitDate( FIELD => 'Starts', @_ );
2724 $self->LimitDate( FIELD => 'Started', @_ );
2729 $self->LimitDate( FIELD => 'Resolved', @_ );
2734 $self->LimitDate( FIELD => 'Told', @_ );
2737 sub LimitLastUpdated {
2739 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2743 # {{{ sub LimitTransactionDate
2745 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2747 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2749 OPERATOR is one of > or <
2750 VALUE is a date and time in ISO format in GMT
2755 sub LimitTransactionDate {
2758 FIELD => 'TransactionDate',
2765 # <20021217042756.GK28744@pallas.fsck.com>
2766 # "Kill It" - Jesse.
2768 #Set the description if we didn't get handed it above
2769 unless ( $args{'DESCRIPTION'} ) {
2770 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2771 . $args{'OPERATOR'} . " "
2772 . $args{'VALUE'} . " GMT";
2775 $self->Limit(%args);
2783 # {{{ Limit based on custom fields
2784 # {{{ sub LimitCustomField
2786 =head2 LimitCustomField
2788 Takes a paramhash of key/value pairs with the following keys:
2792 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2794 =item OPERATOR - The usual Limit operators
2796 =item VALUE - The value to compare against
2802 sub LimitCustomField {
2806 CUSTOMFIELD => undef,
2808 DESCRIPTION => undef,
2809 FIELD => 'CustomFieldValue',
2814 my $CF = RT::CustomField->new( $self->CurrentUser );
2815 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2816 $CF->Load( $args{CUSTOMFIELD} );
2819 $CF->LoadByNameAndQueue(
2820 Name => $args{CUSTOMFIELD},
2821 Queue => $args{QUEUE}
2823 $args{CUSTOMFIELD} = $CF->Id;
2826 # Handle special customfields types
2827 if ($CF->Type eq 'Date') {
2828 $args{FIELD} = 'DateCustomFieldValue';
2831 #If we are looking to compare with a null value.
2832 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2833 $args{'DESCRIPTION'}
2834 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2836 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2837 $args{'DESCRIPTION'}
2838 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2841 # if we're not looking to compare with a null value
2843 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2844 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2847 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2848 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2849 $QueueObj->Load( $args{'QUEUE'} );
2850 $args{'QUEUE'} = $QueueObj->Id;
2852 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2855 @rest = ( ENTRYAGGREGATOR => 'AND' )
2856 if ( $CF->Type eq 'SelectMultiple' );
2859 VALUE => $args{VALUE},
2861 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2862 .".{" . $CF->Name . "}",
2863 OPERATOR => $args{OPERATOR},
2868 $self->{'RecalcTicketLimits'} = 1;
2874 # {{{ sub _NextIndex
2878 Keep track of the counter for the array of restrictions
2884 return ( $self->{'restriction_index'}++ );
2891 # {{{ Core bits to make this a DBIx::SearchBuilder object
2896 $self->{'table'} = "Tickets";
2897 $self->{'RecalcTicketLimits'} = 1;
2898 $self->{'looking_at_effective_id'} = 0;
2899 $self->{'looking_at_type'} = 0;
2900 $self->{'restriction_index'} = 1;
2901 $self->{'primary_key'} = "id";
2902 delete $self->{'items_array'};
2903 delete $self->{'item_map'};
2904 delete $self->{'columns_to_display'};
2905 $self->SUPER::_Init(@_);
2916 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2917 return ( $self->SUPER::Count() );
2925 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2926 return ( $self->SUPER::CountAll() );
2931 # {{{ sub ItemsArrayRef
2933 =head2 ItemsArrayRef
2935 Returns a reference to the set of all items found in this search
2942 return $self->{'items_array'} if $self->{'items_array'};
2944 my $placeholder = $self->_ItemsCounter;
2945 $self->GotoFirstItem();
2946 while ( my $item = $self->Next ) {
2947 push( @{ $self->{'items_array'} }, $item );
2949 $self->GotoItem($placeholder);
2950 $self->{'items_array'}
2951 = $self->ItemsOrderBy( $self->{'items_array'} );
2953 return $self->{'items_array'};
2956 sub ItemsArrayRefWindow {
2960 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2962 $self->RowsPerPage( $window );
2964 $self->GotoFirstItem;
2967 while ( my $item = $self->Next ) {
2971 $self->RowsPerPage( $old[1] );
2972 $self->FirstRow( $old[2] );
2973 $self->GotoItem( $old[0] );
2984 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2986 my $Ticket = $self->SUPER::Next;
2987 return $Ticket unless $Ticket;
2989 if ( $Ticket->__Value('Status') eq 'deleted'
2990 && !$self->{'allow_deleted_search'} )
2994 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2995 # if we found a ticket with this option enabled then
2996 # all tickets we found are ACLed, cache this fact
2997 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2998 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3001 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3006 # If the user doesn't have the right to show this ticket
3013 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3014 return $self->SUPER::_DoSearch( @_ );
3019 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3020 return $self->SUPER::_DoCount( @_ );
3026 my $cache_key = 'RolesHasRight;:;ShowTicket';
3028 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3032 my $ACL = RT::ACL->new( $RT::SystemUser );
3033 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3034 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3035 my $principal_alias = $ACL->Join(
3037 FIELD1 => 'PrincipalId',
3038 TABLE2 => 'Principals',
3041 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3044 while ( my $ACE = $ACL->Next ) {
3045 my $role = $ACE->PrincipalType;
3046 my $type = $ACE->ObjectType;
3047 if ( $type eq 'RT::System' ) {
3050 elsif ( $type eq 'RT::Queue' ) {
3051 next if $res{ $role } && !ref $res{ $role };
3052 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3055 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3058 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3062 sub _DirectlyCanSeeIn {
3064 my $id = $self->CurrentUser->id;
3066 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3067 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3071 my $ACL = RT::ACL->new( $RT::SystemUser );
3072 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3073 my $principal_alias = $ACL->Join(
3075 FIELD1 => 'PrincipalId',
3076 TABLE2 => 'Principals',
3079 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3080 my $cgm_alias = $ACL->Join(
3082 FIELD1 => 'PrincipalId',
3083 TABLE2 => 'CachedGroupMembers',
3084 FIELD2 => 'GroupId',
3086 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3087 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3090 while ( my $ACE = $ACL->Next ) {
3091 my $type = $ACE->ObjectType;
3092 if ( $type eq 'RT::System' ) {
3093 # If user is direct member of a group that has the right
3094 # on the system then he can see any ticket
3095 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3098 elsif ( $type eq 'RT::Queue' ) {
3099 push @res, $ACE->ObjectId;
3102 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3105 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3109 sub CurrentUserCanSee {
3111 return if $self->{'_sql_current_user_can_see_applied'};
3113 return $self->{'_sql_current_user_can_see_applied'} = 1
3114 if $self->CurrentUser->UserObj->HasRight(
3115 Right => 'SuperUser', Object => $RT::System
3118 my $id = $self->CurrentUser->id;
3120 # directly can see in all queues then we have nothing to do
3121 my @direct_queues = $self->_DirectlyCanSeeIn;
3122 return $self->{'_sql_current_user_can_see_applied'} = 1
3123 if @direct_queues && $direct_queues[0] == -1;
3125 my %roles = $self->_RolesCanSee;
3127 my %skip = map { $_ => 1 } @direct_queues;
3128 foreach my $role ( keys %roles ) {
3129 next unless ref $roles{ $role };
3131 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3133 $roles{ $role } = \@queues;
3135 delete $roles{ $role };
3140 # there is no global watchers, only queues and tickes, if at
3141 # some point we will add global roles then it's gonna blow
3142 # the idea here is that if the right is set globaly for a role
3143 # and user plays this role for a queue directly not a ticket
3144 # then we have to check in advance
3145 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3147 my $groups = RT::Groups->new( $RT::SystemUser );
3148 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3150 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3152 my $principal_alias = $groups->Join(
3155 TABLE2 => 'Principals',
3158 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3159 my $cgm_alias = $groups->Join(
3162 TABLE2 => 'CachedGroupMembers',
3163 FIELD2 => 'GroupId',
3165 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3166 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3167 while ( my $group = $groups->Next ) {
3168 push @direct_queues, $group->Instance;
3172 unless ( @direct_queues || keys %roles ) {
3173 $self->SUPER::Limit(
3178 ENTRYAGGREGATOR => 'AND',
3180 return $self->{'_sql_current_user_can_see_applied'} = 1;
3184 my $join_roles = keys %roles;
3185 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3186 my ($role_group_alias, $cgm_alias);
3187 if ( $join_roles ) {
3188 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3189 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3190 $self->SUPER::Limit(
3191 LEFTJOIN => $cgm_alias,
3192 FIELD => 'MemberId',
3197 my $limit_queues = sub {
3201 return unless @queues;
3202 if ( @queues == 1 ) {
3203 $self->SUPER::Limit(
3208 ENTRYAGGREGATOR => $ea,
3211 $self->SUPER::_OpenParen('ACL');
3212 foreach my $q ( @queues ) {
3213 $self->SUPER::Limit(
3218 ENTRYAGGREGATOR => $ea,
3222 $self->SUPER::_CloseParen('ACL');
3227 $self->SUPER::_OpenParen('ACL');
3229 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3230 while ( my ($role, $queues) = each %roles ) {
3231 $self->SUPER::_OpenParen('ACL');
3232 if ( $role eq 'Owner' ) {
3233 $self->SUPER::Limit(
3237 ENTRYAGGREGATOR => $ea,
3241 $self->SUPER::Limit(
3243 ALIAS => $cgm_alias,
3244 FIELD => 'MemberId',
3245 OPERATOR => 'IS NOT',
3248 ENTRYAGGREGATOR => $ea,
3250 $self->SUPER::Limit(
3252 ALIAS => $role_group_alias,
3255 ENTRYAGGREGATOR => 'AND',
3258 $limit_queues->( 'AND', @$queues ) if ref $queues;
3259 $ea = 'OR' if $ea eq 'AND';
3260 $self->SUPER::_CloseParen('ACL');
3262 $self->SUPER::_CloseParen('ACL');
3264 return $self->{'_sql_current_user_can_see_applied'} = 1;
3271 # {{{ Deal with storing and restoring restrictions
3273 # {{{ sub LoadRestrictions
3275 =head2 LoadRestrictions
3277 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3278 TODO It is not yet implemented
3284 # {{{ sub DescribeRestrictions
3286 =head2 DescribeRestrictions
3289 Returns a hash keyed by restriction id.
3290 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3291 is a description of the purpose of that TicketRestriction
3295 sub DescribeRestrictions {
3298 my ( $row, %listing );
3300 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3301 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3308 # {{{ sub RestrictionValues
3310 =head2 RestrictionValues FIELD
3312 Takes a restriction field and returns a list of values this field is restricted
3317 sub RestrictionValues {
3320 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3321 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3322 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3324 keys %{ $self->{'TicketRestrictions'} };
3329 # {{{ sub ClearRestrictions
3331 =head2 ClearRestrictions
3333 Removes all restrictions irretrievably
3337 sub ClearRestrictions {
3339 delete $self->{'TicketRestrictions'};
3340 $self->{'looking_at_effective_id'} = 0;
3341 $self->{'looking_at_type'} = 0;
3342 $self->{'RecalcTicketLimits'} = 1;
3347 # {{{ sub DeleteRestriction
3349 =head2 DeleteRestriction
3351 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3352 Removes that restriction from the session's limits.
3356 sub DeleteRestriction {
3359 delete $self->{'TicketRestrictions'}{$row};
3361 $self->{'RecalcTicketLimits'} = 1;
3363 #make the underlying easysearch object forget all its preconceptions
3368 # {{{ sub _RestrictionsToClauses
3370 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3372 sub _RestrictionsToClauses {
3377 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3378 my $restriction = $self->{'TicketRestrictions'}{$row};
3380 # We need to reimplement the subclause aggregation that SearchBuilder does.
3381 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3382 # Then SB AND's the different Subclauses together.
3384 # So, we want to group things into Subclauses, convert them to
3385 # SQL, and then join them with the appropriate DefaultEA.
3386 # Then join each subclause group with AND.
3388 my $field = $restriction->{'FIELD'};
3389 my $realfield = $field; # CustomFields fake up a fieldname, so
3390 # we need to figure that out
3393 # Rewrite LinkedTo meta field to the real field
3394 if ( $field =~ /LinkedTo/ ) {
3395 $realfield = $field = $restriction->{'TYPE'};
3399 # Handle subkey fields with a different real field
3400 if ( $field =~ /^(\w+)\./ ) {
3404 die "I don't know about $field yet"
3405 unless ( exists $FIELD_METADATA{$realfield}
3406 or $restriction->{CUSTOMFIELD} );
3408 my $type = $FIELD_METADATA{$realfield}->[0];
3409 my $op = $restriction->{'OPERATOR'};
3413 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3416 # this performs the moral equivalent of defined or/dor/C<//>,
3417 # without the short circuiting.You need to use a 'defined or'
3418 # type thing instead of just checking for truth values, because
3419 # VALUE could be 0.(i.e. "false")
3421 # You could also use this, but I find it less aesthetic:
3422 # (although it does short circuit)
3423 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3424 # defined $restriction->{'TICKET'} ?
3425 # $restriction->{TICKET} :
3426 # defined $restriction->{'BASE'} ?
3427 # $restriction->{BASE} :
3428 # defined $restriction->{'TARGET'} ?
3429 # $restriction->{TARGET} )
3431 my $ea = $restriction->{ENTRYAGGREGATOR}
3432 || $DefaultEA{$type}
3435 die "Invalid operator $op for $field ($type)"
3436 unless exists $ea->{$op};
3440 # Each CustomField should be put into a different Clause so they
3441 # are ANDed together.
3442 if ( $restriction->{CUSTOMFIELD} ) {
3443 $realfield = $field;
3446 exists $clause{$realfield} or $clause{$realfield} = [];
3449 $field =~ s!(['"])!\\$1!g;
3450 $value =~ s!(['"])!\\$1!g;
3451 my $data = [ $ea, $type, $field, $op, $value ];
3453 # here is where we store extra data, say if it's a keyword or
3454 # something. (I.e. "TYPE SPECIFIC STUFF")
3456 push @{ $clause{$realfield} }, $data;
3463 # {{{ sub _ProcessRestrictions
3465 =head2 _ProcessRestrictions PARAMHASH
3467 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3468 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3472 sub _ProcessRestrictions {
3475 #Blow away ticket aliases since we'll need to regenerate them for
3477 delete $self->{'TicketAliases'};
3478 delete $self->{'items_array'};
3479 delete $self->{'item_map'};
3480 delete $self->{'raw_rows'};
3481 delete $self->{'rows'};
3482 delete $self->{'count_all'};
3484 my $sql = $self->Query; # Violating the _SQL namespace
3485 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3487 # "Restrictions to Clauses Branch\n";
3488 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3490 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3494 $sql = $self->ClausesToSQL($clauseRef);
3495 $self->FromSQL($sql) if $sql;
3499 $self->{'RecalcTicketLimits'} = 0;
3503 =head2 _BuildItemMap
3505 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3506 display search nav quickly.
3513 my $window = RT->Config->Get('TicketsItemMapSize');
3515 $self->{'item_map'} = {};
3517 my $items = $self->ItemsArrayRefWindow( $window );
3518 return unless $items && @$items;
3521 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3522 for ( my $i = 0; $i < @$items; $i++ ) {
3523 my $item = $items->[$i];
3524 my $id = $item->EffectiveId;
3525 $self->{'item_map'}{$id}{'defined'} = 1;
3526 $self->{'item_map'}{$id}{'prev'} = $prev;
3527 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3531 $self->{'item_map'}{'last'} = $prev
3532 if !$window || @$items < $window;
3537 Returns an a map of all items found by this search. The map is a hash
3541 first => <first ticket id found>,
3542 last => <last ticket id found or undef>,
3545 prev => <the ticket id found before>,
3546 next => <the ticket id found after>,
3558 $self->_BuildItemMap unless $self->{'item_map'};
3559 return $self->{'item_map'};
3567 =head2 PrepForSerialization
3569 You don't want to serialize a big tickets object, as
3570 the {items} hash will be instantly invalid _and_ eat
3575 sub PrepForSerialization {
3577 delete $self->{'items'};
3578 delete $self->{'items_array'};
3579 $self->RedoSearch();
3584 RT::Tickets supports several flags which alter search behavior:
3587 allow_deleted_search (Otherwise never show deleted tickets in search results)
3588 looking_at_type (otherwise limit to type=ticket)
3590 These flags are set by calling
3592 $tickets->{'flagname'} = 1;
3594 BUG: There should be an API for this