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 INTEGER)";
1860 if ( $subkey eq 'Number' ) {
1864 FIELD => $custnum_sql,
1867 } elsif ( $subkey eq 'Name' ) {
1869 my $custalias = $self->Join(
1871 EXPRESSION => $custnum_sql,
1872 TABLE2 => 'cust_main',
1873 FIELD2 => 'custnum',
1877 my $field = "COALESCE( $custalias.company,
1878 $custalias.last || ', ' || $custalias.first
1881 push @res, { %$row, ALIAS => '', FIELD => $field };
1891 return $self->SUPER::OrderByCols(@res);
1896 # {{{ Limit the result set based on content
1902 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1903 Generally best called from LimitFoo methods
1913 DESCRIPTION => undef,
1916 $args{'DESCRIPTION'} = $self->loc(
1917 "[_1] [_2] [_3]", $args{'FIELD'},
1918 $args{'OPERATOR'}, $args{'VALUE'}
1920 if ( !defined $args{'DESCRIPTION'} );
1922 my $index = $self->_NextIndex;
1924 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1926 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1928 $self->{'RecalcTicketLimits'} = 1;
1930 # If we're looking at the effective id, we don't want to append the other clause
1931 # which limits us to tickets where id = effective id
1932 if ( $args{'FIELD'} eq 'EffectiveId'
1933 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1935 $self->{'looking_at_effective_id'} = 1;
1938 if ( $args{'FIELD'} eq 'Type'
1939 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1941 $self->{'looking_at_type'} = 1;
1951 Returns a frozen string suitable for handing back to ThawLimits.
1955 sub _FreezeThawKeys {
1956 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1960 # {{{ sub FreezeLimits
1965 require MIME::Base64;
1966 MIME::Base64::base64_encode(
1967 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1974 Take a frozen Limits string generated by FreezeLimits and make this tickets
1975 object have that set of limits.
1979 # {{{ sub ThawLimits
1985 #if we don't have $in, get outta here.
1986 return undef unless ($in);
1988 $self->{'RecalcTicketLimits'} = 1;
1991 require MIME::Base64;
1993 #We don't need to die if the thaw fails.
1994 @{$self}{ $self->_FreezeThawKeys }
1995 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1997 $RT::Logger->error($@) if $@;
2003 # {{{ Limit by enum or foreign key
2005 # {{{ sub LimitQueue
2009 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2010 OPERATOR is one of = or !=. (It defaults to =).
2011 VALUE is a queue id or Name.
2024 #TODO VALUE should also take queue objects
2025 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2026 my $queue = new RT::Queue( $self->CurrentUser );
2027 $queue->Load( $args{'VALUE'} );
2028 $args{'VALUE'} = $queue->Id;
2031 # What if they pass in an Id? Check for isNum() and convert to
2034 #TODO check for a valid queue here
2038 VALUE => $args{'VALUE'},
2039 OPERATOR => $args{'OPERATOR'},
2040 DESCRIPTION => join(
2041 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2049 # {{{ sub LimitStatus
2053 Takes a paramhash with the fields OPERATOR and VALUE.
2054 OPERATOR is one of = or !=.
2057 RT adds Status != 'deleted' until object has
2058 allow_deleted_search internal property set.
2059 $tickets->{'allow_deleted_search'} = 1;
2060 $tickets->LimitStatus( VALUE => 'deleted' );
2072 VALUE => $args{'VALUE'},
2073 OPERATOR => $args{'OPERATOR'},
2074 DESCRIPTION => join( ' ',
2075 $self->loc('Status'), $args{'OPERATOR'},
2076 $self->loc( $args{'VALUE'} ) ),
2082 # {{{ sub IgnoreType
2086 If called, this search will not automatically limit the set of results found
2087 to tickets of type "Ticket". Tickets of other types, such as "project" and
2088 "approval" will be found.
2095 # Instead of faking a Limit that later gets ignored, fake up the
2096 # fact that we're already looking at type, so that the check in
2097 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2099 # $self->LimitType(VALUE => '__any');
2100 $self->{looking_at_type} = 1;
2109 Takes a paramhash with the fields OPERATOR and VALUE.
2110 OPERATOR is one of = or !=, it defaults to "=".
2111 VALUE is a string to search for in the type of the ticket.
2126 VALUE => $args{'VALUE'},
2127 OPERATOR => $args{'OPERATOR'},
2128 DESCRIPTION => join( ' ',
2129 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2137 # {{{ Limit by string field
2139 # {{{ sub LimitSubject
2143 Takes a paramhash with the fields OPERATOR and VALUE.
2144 OPERATOR is one of = or !=.
2145 VALUE is a string to search for in the subject of the ticket.
2154 VALUE => $args{'VALUE'},
2155 OPERATOR => $args{'OPERATOR'},
2156 DESCRIPTION => join( ' ',
2157 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2165 # {{{ Limit based on ticket numerical attributes
2166 # Things that can be > < = !=
2172 Takes a paramhash with the fields OPERATOR and VALUE.
2173 OPERATOR is one of =, >, < or !=.
2174 VALUE is a ticket Id to search for
2187 VALUE => $args{'VALUE'},
2188 OPERATOR => $args{'OPERATOR'},
2190 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2196 # {{{ sub LimitPriority
2198 =head2 LimitPriority
2200 Takes a paramhash with the fields OPERATOR and VALUE.
2201 OPERATOR is one of =, >, < or !=.
2202 VALUE is a value to match the ticket\'s priority against
2210 FIELD => 'Priority',
2211 VALUE => $args{'VALUE'},
2212 OPERATOR => $args{'OPERATOR'},
2213 DESCRIPTION => join( ' ',
2214 $self->loc('Priority'),
2215 $args{'OPERATOR'}, $args{'VALUE'}, ),
2221 # {{{ sub LimitInitialPriority
2223 =head2 LimitInitialPriority
2225 Takes a paramhash with the fields OPERATOR and VALUE.
2226 OPERATOR is one of =, >, < or !=.
2227 VALUE is a value to match the ticket\'s initial priority against
2232 sub LimitInitialPriority {
2236 FIELD => 'InitialPriority',
2237 VALUE => $args{'VALUE'},
2238 OPERATOR => $args{'OPERATOR'},
2239 DESCRIPTION => join( ' ',
2240 $self->loc('Initial Priority'), $args{'OPERATOR'},
2247 # {{{ sub LimitFinalPriority
2249 =head2 LimitFinalPriority
2251 Takes a paramhash with the fields OPERATOR and VALUE.
2252 OPERATOR is one of =, >, < or !=.
2253 VALUE is a value to match the ticket\'s final priority against
2257 sub LimitFinalPriority {
2261 FIELD => 'FinalPriority',
2262 VALUE => $args{'VALUE'},
2263 OPERATOR => $args{'OPERATOR'},
2264 DESCRIPTION => join( ' ',
2265 $self->loc('Final Priority'), $args{'OPERATOR'},
2272 # {{{ sub LimitTimeWorked
2274 =head2 LimitTimeWorked
2276 Takes a paramhash with the fields OPERATOR and VALUE.
2277 OPERATOR is one of =, >, < or !=.
2278 VALUE is a value to match the ticket's TimeWorked attribute
2282 sub LimitTimeWorked {
2286 FIELD => 'TimeWorked',
2287 VALUE => $args{'VALUE'},
2288 OPERATOR => $args{'OPERATOR'},
2289 DESCRIPTION => join( ' ',
2290 $self->loc('Time Worked'),
2291 $args{'OPERATOR'}, $args{'VALUE'}, ),
2297 # {{{ sub LimitTimeLeft
2299 =head2 LimitTimeLeft
2301 Takes a paramhash with the fields OPERATOR and VALUE.
2302 OPERATOR is one of =, >, < or !=.
2303 VALUE is a value to match the ticket's TimeLeft attribute
2311 FIELD => 'TimeLeft',
2312 VALUE => $args{'VALUE'},
2313 OPERATOR => $args{'OPERATOR'},
2314 DESCRIPTION => join( ' ',
2315 $self->loc('Time Left'),
2316 $args{'OPERATOR'}, $args{'VALUE'}, ),
2324 # {{{ Limiting based on attachment attributes
2326 # {{{ sub LimitContent
2330 Takes a paramhash with the fields OPERATOR and VALUE.
2331 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2332 VALUE is a string to search for in the body of the ticket
2341 VALUE => $args{'VALUE'},
2342 OPERATOR => $args{'OPERATOR'},
2343 DESCRIPTION => join( ' ',
2344 $self->loc('Ticket content'), $args{'OPERATOR'},
2351 # {{{ sub LimitFilename
2353 =head2 LimitFilename
2355 Takes a paramhash with the fields OPERATOR and VALUE.
2356 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2357 VALUE is a string to search for in the body of the ticket
2365 FIELD => 'Filename',
2366 VALUE => $args{'VALUE'},
2367 OPERATOR => $args{'OPERATOR'},
2368 DESCRIPTION => join( ' ',
2369 $self->loc('Attachment filename'), $args{'OPERATOR'},
2375 # {{{ sub LimitContentType
2377 =head2 LimitContentType
2379 Takes a paramhash with the fields OPERATOR and VALUE.
2380 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2381 VALUE is a content type to search ticket attachments for
2385 sub LimitContentType {
2389 FIELD => 'ContentType',
2390 VALUE => $args{'VALUE'},
2391 OPERATOR => $args{'OPERATOR'},
2392 DESCRIPTION => join( ' ',
2393 $self->loc('Ticket content type'), $args{'OPERATOR'},
2402 # {{{ Limiting based on people
2404 # {{{ sub LimitOwner
2408 Takes a paramhash with the fields OPERATOR and VALUE.
2409 OPERATOR is one of = or !=.
2421 my $owner = new RT::User( $self->CurrentUser );
2422 $owner->Load( $args{'VALUE'} );
2424 # FIXME: check for a valid $owner
2427 VALUE => $args{'VALUE'},
2428 OPERATOR => $args{'OPERATOR'},
2429 DESCRIPTION => join( ' ',
2430 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2437 # {{{ Limiting watchers
2439 # {{{ sub LimitWatcher
2443 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2444 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2445 VALUE is a value to match the ticket\'s watcher email addresses against
2446 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2460 #build us up a description
2461 my ( $watcher_type, $desc );
2462 if ( $args{'TYPE'} ) {
2463 $watcher_type = $args{'TYPE'};
2466 $watcher_type = "Watcher";
2470 FIELD => $watcher_type,
2471 VALUE => $args{'VALUE'},
2472 OPERATOR => $args{'OPERATOR'},
2473 TYPE => $args{'TYPE'},
2474 DESCRIPTION => join( ' ',
2475 $self->loc($watcher_type),
2476 $args{'OPERATOR'}, $args{'VALUE'}, ),
2486 # {{{ Limiting based on links
2490 =head2 LimitLinkedTo
2492 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2493 TYPE limits the sort of link we want to search on
2495 TYPE = { RefersTo, MemberOf, DependsOn }
2497 TARGET is the id or URI of the TARGET of the link
2511 FIELD => 'LinkedTo',
2513 TARGET => $args{'TARGET'},
2514 TYPE => $args{'TYPE'},
2515 DESCRIPTION => $self->loc(
2516 "Tickets [_1] by [_2]",
2517 $self->loc( $args{'TYPE'} ),
2520 OPERATOR => $args{'OPERATOR'},
2526 # {{{ LimitLinkedFrom
2528 =head2 LimitLinkedFrom
2530 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2531 TYPE limits the sort of link we want to search on
2534 BASE is the id or URI of the BASE of the link
2538 sub LimitLinkedFrom {
2547 # translate RT2 From/To naming to RT3 TicketSQL naming
2548 my %fromToMap = qw(DependsOn DependentOn
2550 RefersTo ReferredToBy);
2552 my $type = $args{'TYPE'};
2553 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2556 FIELD => 'LinkedTo',
2558 BASE => $args{'BASE'},
2560 DESCRIPTION => $self->loc(
2561 "Tickets [_1] [_2]",
2562 $self->loc( $args{'TYPE'} ),
2565 OPERATOR => $args{'OPERATOR'},
2574 my $ticket_id = shift;
2575 return $self->LimitLinkedTo(
2577 TARGET => $ticket_id,
2584 # {{{ LimitHasMember
2585 sub LimitHasMember {
2587 my $ticket_id = shift;
2588 return $self->LimitLinkedFrom(
2590 BASE => "$ticket_id",
2591 TYPE => 'HasMember',
2598 # {{{ LimitDependsOn
2600 sub LimitDependsOn {
2602 my $ticket_id = shift;
2603 return $self->LimitLinkedTo(
2605 TARGET => $ticket_id,
2606 TYPE => 'DependsOn',
2613 # {{{ LimitDependedOnBy
2615 sub LimitDependedOnBy {
2617 my $ticket_id = shift;
2618 return $self->LimitLinkedFrom(
2621 TYPE => 'DependentOn',
2632 my $ticket_id = shift;
2633 return $self->LimitLinkedTo(
2635 TARGET => $ticket_id,
2643 # {{{ LimitReferredToBy
2645 sub LimitReferredToBy {
2647 my $ticket_id = shift;
2648 return $self->LimitLinkedFrom(
2651 TYPE => 'ReferredToBy',
2659 # {{{ limit based on ticket date attribtes
2663 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2665 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2667 OPERATOR is one of > or <
2668 VALUE is a date and time in ISO format in GMT
2669 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2671 There are also helper functions of the form LimitFIELD that eliminate
2672 the need to pass in a FIELD argument.
2686 #Set the description if we didn't get handed it above
2687 unless ( $args{'DESCRIPTION'} ) {
2688 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2689 . $args{'OPERATOR'} . " "
2690 . $args{'VALUE'} . " GMT";
2693 $self->Limit(%args);
2701 $self->LimitDate( FIELD => 'Created', @_ );
2706 $self->LimitDate( FIELD => 'Due', @_ );
2712 $self->LimitDate( FIELD => 'Starts', @_ );
2718 $self->LimitDate( FIELD => 'Started', @_ );
2723 $self->LimitDate( FIELD => 'Resolved', @_ );
2728 $self->LimitDate( FIELD => 'Told', @_ );
2731 sub LimitLastUpdated {
2733 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2737 # {{{ sub LimitTransactionDate
2739 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2741 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2743 OPERATOR is one of > or <
2744 VALUE is a date and time in ISO format in GMT
2749 sub LimitTransactionDate {
2752 FIELD => 'TransactionDate',
2759 # <20021217042756.GK28744@pallas.fsck.com>
2760 # "Kill It" - Jesse.
2762 #Set the description if we didn't get handed it above
2763 unless ( $args{'DESCRIPTION'} ) {
2764 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2765 . $args{'OPERATOR'} . " "
2766 . $args{'VALUE'} . " GMT";
2769 $self->Limit(%args);
2777 # {{{ Limit based on custom fields
2778 # {{{ sub LimitCustomField
2780 =head2 LimitCustomField
2782 Takes a paramhash of key/value pairs with the following keys:
2786 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2788 =item OPERATOR - The usual Limit operators
2790 =item VALUE - The value to compare against
2796 sub LimitCustomField {
2800 CUSTOMFIELD => undef,
2802 DESCRIPTION => undef,
2803 FIELD => 'CustomFieldValue',
2808 my $CF = RT::CustomField->new( $self->CurrentUser );
2809 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2810 $CF->Load( $args{CUSTOMFIELD} );
2813 $CF->LoadByNameAndQueue(
2814 Name => $args{CUSTOMFIELD},
2815 Queue => $args{QUEUE}
2817 $args{CUSTOMFIELD} = $CF->Id;
2820 # Handle special customfields types
2821 if ($CF->Type eq 'Date') {
2822 $args{FIELD} = 'DateCustomFieldValue';
2825 #If we are looking to compare with a null value.
2826 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2827 $args{'DESCRIPTION'}
2828 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2830 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2831 $args{'DESCRIPTION'}
2832 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2835 # if we're not looking to compare with a null value
2837 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2838 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2841 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2842 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2843 $QueueObj->Load( $args{'QUEUE'} );
2844 $args{'QUEUE'} = $QueueObj->Id;
2846 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2849 @rest = ( ENTRYAGGREGATOR => 'AND' )
2850 if ( $CF->Type eq 'SelectMultiple' );
2853 VALUE => $args{VALUE},
2855 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2856 .".{" . $CF->Name . "}",
2857 OPERATOR => $args{OPERATOR},
2862 $self->{'RecalcTicketLimits'} = 1;
2868 # {{{ sub _NextIndex
2872 Keep track of the counter for the array of restrictions
2878 return ( $self->{'restriction_index'}++ );
2885 # {{{ Core bits to make this a DBIx::SearchBuilder object
2890 $self->{'table'} = "Tickets";
2891 $self->{'RecalcTicketLimits'} = 1;
2892 $self->{'looking_at_effective_id'} = 0;
2893 $self->{'looking_at_type'} = 0;
2894 $self->{'restriction_index'} = 1;
2895 $self->{'primary_key'} = "id";
2896 delete $self->{'items_array'};
2897 delete $self->{'item_map'};
2898 delete $self->{'columns_to_display'};
2899 $self->SUPER::_Init(@_);
2910 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2911 return ( $self->SUPER::Count() );
2919 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2920 return ( $self->SUPER::CountAll() );
2925 # {{{ sub ItemsArrayRef
2927 =head2 ItemsArrayRef
2929 Returns a reference to the set of all items found in this search
2936 return $self->{'items_array'} if $self->{'items_array'};
2938 my $placeholder = $self->_ItemsCounter;
2939 $self->GotoFirstItem();
2940 while ( my $item = $self->Next ) {
2941 push( @{ $self->{'items_array'} }, $item );
2943 $self->GotoItem($placeholder);
2944 $self->{'items_array'}
2945 = $self->ItemsOrderBy( $self->{'items_array'} );
2947 return $self->{'items_array'};
2950 sub ItemsArrayRefWindow {
2954 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2956 $self->RowsPerPage( $window );
2958 $self->GotoFirstItem;
2961 while ( my $item = $self->Next ) {
2965 $self->RowsPerPage( $old[1] );
2966 $self->FirstRow( $old[2] );
2967 $self->GotoItem( $old[0] );
2978 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2980 my $Ticket = $self->SUPER::Next;
2981 return $Ticket unless $Ticket;
2983 if ( $Ticket->__Value('Status') eq 'deleted'
2984 && !$self->{'allow_deleted_search'} )
2988 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2989 # if we found a ticket with this option enabled then
2990 # all tickets we found are ACLed, cache this fact
2991 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2992 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2995 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3000 # If the user doesn't have the right to show this ticket
3007 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3008 return $self->SUPER::_DoSearch( @_ );
3013 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3014 return $self->SUPER::_DoCount( @_ );
3020 my $cache_key = 'RolesHasRight;:;ShowTicket';
3022 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3026 my $ACL = RT::ACL->new( $RT::SystemUser );
3027 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3028 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3029 my $principal_alias = $ACL->Join(
3031 FIELD1 => 'PrincipalId',
3032 TABLE2 => 'Principals',
3035 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3038 while ( my $ACE = $ACL->Next ) {
3039 my $role = $ACE->PrincipalType;
3040 my $type = $ACE->ObjectType;
3041 if ( $type eq 'RT::System' ) {
3044 elsif ( $type eq 'RT::Queue' ) {
3045 next if $res{ $role } && !ref $res{ $role };
3046 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3049 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3052 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3056 sub _DirectlyCanSeeIn {
3058 my $id = $self->CurrentUser->id;
3060 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3061 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3065 my $ACL = RT::ACL->new( $RT::SystemUser );
3066 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3067 my $principal_alias = $ACL->Join(
3069 FIELD1 => 'PrincipalId',
3070 TABLE2 => 'Principals',
3073 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3074 my $cgm_alias = $ACL->Join(
3076 FIELD1 => 'PrincipalId',
3077 TABLE2 => 'CachedGroupMembers',
3078 FIELD2 => 'GroupId',
3080 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3081 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3084 while ( my $ACE = $ACL->Next ) {
3085 my $type = $ACE->ObjectType;
3086 if ( $type eq 'RT::System' ) {
3087 # If user is direct member of a group that has the right
3088 # on the system then he can see any ticket
3089 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3092 elsif ( $type eq 'RT::Queue' ) {
3093 push @res, $ACE->ObjectId;
3096 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3099 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3103 sub CurrentUserCanSee {
3105 return if $self->{'_sql_current_user_can_see_applied'};
3107 return $self->{'_sql_current_user_can_see_applied'} = 1
3108 if $self->CurrentUser->UserObj->HasRight(
3109 Right => 'SuperUser', Object => $RT::System
3112 my $id = $self->CurrentUser->id;
3114 # directly can see in all queues then we have nothing to do
3115 my @direct_queues = $self->_DirectlyCanSeeIn;
3116 return $self->{'_sql_current_user_can_see_applied'} = 1
3117 if @direct_queues && $direct_queues[0] == -1;
3119 my %roles = $self->_RolesCanSee;
3121 my %skip = map { $_ => 1 } @direct_queues;
3122 foreach my $role ( keys %roles ) {
3123 next unless ref $roles{ $role };
3125 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3127 $roles{ $role } = \@queues;
3129 delete $roles{ $role };
3134 # there is no global watchers, only queues and tickes, if at
3135 # some point we will add global roles then it's gonna blow
3136 # the idea here is that if the right is set globaly for a role
3137 # and user plays this role for a queue directly not a ticket
3138 # then we have to check in advance
3139 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3141 my $groups = RT::Groups->new( $RT::SystemUser );
3142 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3144 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3146 my $principal_alias = $groups->Join(
3149 TABLE2 => 'Principals',
3152 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3153 my $cgm_alias = $groups->Join(
3156 TABLE2 => 'CachedGroupMembers',
3157 FIELD2 => 'GroupId',
3159 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3160 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3161 while ( my $group = $groups->Next ) {
3162 push @direct_queues, $group->Instance;
3166 unless ( @direct_queues || keys %roles ) {
3167 $self->SUPER::Limit(
3172 ENTRYAGGREGATOR => 'AND',
3174 return $self->{'_sql_current_user_can_see_applied'} = 1;
3178 my $join_roles = keys %roles;
3179 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3180 my ($role_group_alias, $cgm_alias);
3181 if ( $join_roles ) {
3182 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3183 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3184 $self->SUPER::Limit(
3185 LEFTJOIN => $cgm_alias,
3186 FIELD => 'MemberId',
3191 my $limit_queues = sub {
3195 return unless @queues;
3196 if ( @queues == 1 ) {
3197 $self->SUPER::Limit(
3202 ENTRYAGGREGATOR => $ea,
3205 $self->SUPER::_OpenParen('ACL');
3206 foreach my $q ( @queues ) {
3207 $self->SUPER::Limit(
3212 ENTRYAGGREGATOR => $ea,
3216 $self->SUPER::_CloseParen('ACL');
3221 $self->SUPER::_OpenParen('ACL');
3223 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3224 while ( my ($role, $queues) = each %roles ) {
3225 $self->SUPER::_OpenParen('ACL');
3226 if ( $role eq 'Owner' ) {
3227 $self->SUPER::Limit(
3231 ENTRYAGGREGATOR => $ea,
3235 $self->SUPER::Limit(
3237 ALIAS => $cgm_alias,
3238 FIELD => 'MemberId',
3239 OPERATOR => 'IS NOT',
3242 ENTRYAGGREGATOR => $ea,
3244 $self->SUPER::Limit(
3246 ALIAS => $role_group_alias,
3249 ENTRYAGGREGATOR => 'AND',
3252 $limit_queues->( 'AND', @$queues ) if ref $queues;
3253 $ea = 'OR' if $ea eq 'AND';
3254 $self->SUPER::_CloseParen('ACL');
3256 $self->SUPER::_CloseParen('ACL');
3258 return $self->{'_sql_current_user_can_see_applied'} = 1;
3265 # {{{ Deal with storing and restoring restrictions
3267 # {{{ sub LoadRestrictions
3269 =head2 LoadRestrictions
3271 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3272 TODO It is not yet implemented
3278 # {{{ sub DescribeRestrictions
3280 =head2 DescribeRestrictions
3283 Returns a hash keyed by restriction id.
3284 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3285 is a description of the purpose of that TicketRestriction
3289 sub DescribeRestrictions {
3292 my ( $row, %listing );
3294 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3295 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3302 # {{{ sub RestrictionValues
3304 =head2 RestrictionValues FIELD
3306 Takes a restriction field and returns a list of values this field is restricted
3311 sub RestrictionValues {
3314 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3315 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3316 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3318 keys %{ $self->{'TicketRestrictions'} };
3323 # {{{ sub ClearRestrictions
3325 =head2 ClearRestrictions
3327 Removes all restrictions irretrievably
3331 sub ClearRestrictions {
3333 delete $self->{'TicketRestrictions'};
3334 $self->{'looking_at_effective_id'} = 0;
3335 $self->{'looking_at_type'} = 0;
3336 $self->{'RecalcTicketLimits'} = 1;
3341 # {{{ sub DeleteRestriction
3343 =head2 DeleteRestriction
3345 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3346 Removes that restriction from the session's limits.
3350 sub DeleteRestriction {
3353 delete $self->{'TicketRestrictions'}{$row};
3355 $self->{'RecalcTicketLimits'} = 1;
3357 #make the underlying easysearch object forget all its preconceptions
3362 # {{{ sub _RestrictionsToClauses
3364 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3366 sub _RestrictionsToClauses {
3371 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3372 my $restriction = $self->{'TicketRestrictions'}{$row};
3374 # We need to reimplement the subclause aggregation that SearchBuilder does.
3375 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3376 # Then SB AND's the different Subclauses together.
3378 # So, we want to group things into Subclauses, convert them to
3379 # SQL, and then join them with the appropriate DefaultEA.
3380 # Then join each subclause group with AND.
3382 my $field = $restriction->{'FIELD'};
3383 my $realfield = $field; # CustomFields fake up a fieldname, so
3384 # we need to figure that out
3387 # Rewrite LinkedTo meta field to the real field
3388 if ( $field =~ /LinkedTo/ ) {
3389 $realfield = $field = $restriction->{'TYPE'};
3393 # Handle subkey fields with a different real field
3394 if ( $field =~ /^(\w+)\./ ) {
3398 die "I don't know about $field yet"
3399 unless ( exists $FIELD_METADATA{$realfield}
3400 or $restriction->{CUSTOMFIELD} );
3402 my $type = $FIELD_METADATA{$realfield}->[0];
3403 my $op = $restriction->{'OPERATOR'};
3407 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3410 # this performs the moral equivalent of defined or/dor/C<//>,
3411 # without the short circuiting.You need to use a 'defined or'
3412 # type thing instead of just checking for truth values, because
3413 # VALUE could be 0.(i.e. "false")
3415 # You could also use this, but I find it less aesthetic:
3416 # (although it does short circuit)
3417 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3418 # defined $restriction->{'TICKET'} ?
3419 # $restriction->{TICKET} :
3420 # defined $restriction->{'BASE'} ?
3421 # $restriction->{BASE} :
3422 # defined $restriction->{'TARGET'} ?
3423 # $restriction->{TARGET} )
3425 my $ea = $restriction->{ENTRYAGGREGATOR}
3426 || $DefaultEA{$type}
3429 die "Invalid operator $op for $field ($type)"
3430 unless exists $ea->{$op};
3434 # Each CustomField should be put into a different Clause so they
3435 # are ANDed together.
3436 if ( $restriction->{CUSTOMFIELD} ) {
3437 $realfield = $field;
3440 exists $clause{$realfield} or $clause{$realfield} = [];
3443 $field =~ s!(['"])!\\$1!g;
3444 $value =~ s!(['"])!\\$1!g;
3445 my $data = [ $ea, $type, $field, $op, $value ];
3447 # here is where we store extra data, say if it's a keyword or
3448 # something. (I.e. "TYPE SPECIFIC STUFF")
3450 push @{ $clause{$realfield} }, $data;
3457 # {{{ sub _ProcessRestrictions
3459 =head2 _ProcessRestrictions PARAMHASH
3461 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3462 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3466 sub _ProcessRestrictions {
3469 #Blow away ticket aliases since we'll need to regenerate them for
3471 delete $self->{'TicketAliases'};
3472 delete $self->{'items_array'};
3473 delete $self->{'item_map'};
3474 delete $self->{'raw_rows'};
3475 delete $self->{'rows'};
3476 delete $self->{'count_all'};
3478 my $sql = $self->Query; # Violating the _SQL namespace
3479 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3481 # "Restrictions to Clauses Branch\n";
3482 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3484 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3488 $sql = $self->ClausesToSQL($clauseRef);
3489 $self->FromSQL($sql) if $sql;
3493 $self->{'RecalcTicketLimits'} = 0;
3497 =head2 _BuildItemMap
3499 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3500 display search nav quickly.
3507 my $window = RT->Config->Get('TicketsItemMapSize');
3509 $self->{'item_map'} = {};
3511 my $items = $self->ItemsArrayRefWindow( $window );
3512 return unless $items && @$items;
3515 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3516 for ( my $i = 0; $i < @$items; $i++ ) {
3517 my $item = $items->[$i];
3518 my $id = $item->EffectiveId;
3519 $self->{'item_map'}{$id}{'defined'} = 1;
3520 $self->{'item_map'}{$id}{'prev'} = $prev;
3521 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3525 $self->{'item_map'}{'last'} = $prev
3526 if !$window || @$items < $window;
3531 Returns an a map of all items found by this search. The map is a hash
3535 first => <first ticket id found>,
3536 last => <last ticket id found or undef>,
3539 prev => <the ticket id found before>,
3540 next => <the ticket id found after>,
3552 $self->_BuildItemMap unless $self->{'item_map'};
3553 return $self->{'item_map'};
3561 =head2 PrepForSerialization
3563 You don't want to serialize a big tickets object, as
3564 the {items} hash will be instantly invalid _and_ eat
3569 sub PrepForSerialization {
3571 delete $self->{'items'};
3572 delete $self->{'items_array'};
3573 $self->RedoSearch();
3578 RT::Tickets supports several flags which alter search behavior:
3581 allow_deleted_search (Otherwise never show deleted tickets in search results)
3582 looking_at_type (otherwise limit to type=ticket)
3584 These flags are set by calling
3586 $tickets->{'flagname'} = 1;
3588 BUG: There should be an API for this