1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
51 # - Decimated ProcessRestrictions and broke it into multiple
52 # functions joined by a LUT
53 # - Semi-Generic SQL stuff moved to another file
55 # Known Issues: FIXME!
57 # - ClearRestrictions and Reinitialization is messy and unclear. The
58 # only good way to do it is to create a new RT::Tickets object.
62 RT::Tickets - A collection of Ticket objects
68 my $tickets = new RT::Tickets($CurrentUser);
72 A collection of RT::Tickets.
82 no warnings qw(redefine);
85 use DBIx::SearchBuilder::Unique;
87 # Configuration Tables:
89 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
92 our %FIELD_METADATA = (
93 Status => [ 'ENUM', ], #loc_left_pair
94 Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
95 Type => [ 'ENUM', ], #loc_left_pair
96 Creator => [ 'ENUM' => 'User', ], #loc_left_pair
97 LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
98 Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
99 EffectiveId => [ 'INT', ], #loc_left_pair
100 id => [ 'ID', ], #loc_left_pair
101 InitialPriority => [ 'INT', ], #loc_left_pair
102 FinalPriority => [ 'INT', ], #loc_left_pair
103 Priority => [ 'INT', ], #loc_left_pair
104 TimeLeft => [ 'INT', ], #loc_left_pair
105 TimeWorked => [ 'INT', ], #loc_left_pair
106 TimeEstimated => [ 'INT', ], #loc_left_pair
108 Linked => [ 'LINK' ], #loc_left_pair
109 LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
110 LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
111 MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
112 DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
113 RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
114 HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
115 DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
116 DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
117 ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
118 Told => [ 'DATE' => 'Told', ], #loc_left_pair
119 Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
120 Started => [ 'DATE' => 'Started', ], #loc_left_pair
121 Due => [ 'DATE' => 'Due', ], #loc_left_pair
122 Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
123 LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
124 Created => [ 'DATE' => 'Created', ], #loc_left_pair
125 Subject => [ 'STRING', ], #loc_left_pair
126 Content => [ 'TRANSFIELD', ], #loc_left_pair
127 ContentType => [ 'TRANSFIELD', ], #loc_left_pair
128 Filename => [ 'TRANSFIELD', ], #loc_left_pair
129 TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
130 Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
131 Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
132 Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
133 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
134 Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
135 QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
136 QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
137 QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
138 CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
139 CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
140 CF => [ 'CUSTOMFIELD', ], #loc_left_pair
141 Updated => [ 'TRANSDATE', ], #loc_left_pair
142 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
143 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
144 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
145 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
146 HasAttribute => [ 'HASATTRIBUTE', 1 ],
147 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
148 Agentnum => [ 'FREESIDEFIELD', ],
149 Classnum => [ 'FREESIDEFIELD', ],
150 Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ],
153 # Mapping of Field Type to Function
155 ENUM => \&_EnumLimit,
158 LINK => \&_LinkLimit,
159 DATE => \&_DateLimit,
160 STRING => \&_StringLimit,
161 TRANSFIELD => \&_TransLimit,
162 TRANSDATE => \&_TransDateLimit,
163 WATCHERFIELD => \&_WatcherLimit,
164 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
165 CUSTOMFIELD => \&_CustomFieldLimit,
166 HASATTRIBUTE => \&_HasAttributeLimit,
167 FREESIDEFIELD => \&_FreesideFieldLimit,
169 our %can_bundle = ();# WATCHERFIELD => "yes", );
171 # Default EntryAggregator per type
172 # if you specify OP, you must specify all valid OPs
213 # Helper functions for passing the above lexically scoped tables above
214 # into Tickets_Overlay_SQL.
215 sub FIELDS { return \%FIELD_METADATA }
216 sub dispatch { return \%dispatch }
217 sub can_bundle { return \%can_bundle }
219 # Bring in the clowns.
220 require RT::Tickets_Overlay_SQL;
224 our @SORTFIELDS = qw(id Status
226 Owner Created Due Starts Started
228 Resolved LastUpdated Priority TimeWorked TimeLeft);
232 Returns the list of fields that lists of tickets can easily be sorted by
238 return (@SORTFIELDS);
243 # BEGIN SQL STUFF *********************************
248 $self->SUPER::CleanSlate( @_ );
249 delete $self->{$_} foreach qw(
251 _sql_group_members_aliases
252 _sql_object_cfv_alias
253 _sql_role_group_aliases
256 _sql_u_watchers_alias_for_sort
257 _sql_u_watchers_aliases
258 _sql_current_user_can_see_applied
262 =head1 Limit Helper Routines
264 These routines are the targets of a dispatch table depending on the
265 type of field. They all share the same signature:
267 my ($self,$field,$op,$value,@rest) = @_;
269 The values in @rest should be suitable for passing directly to
270 DBIx::SearchBuilder::Limit.
272 Essentially they are an expanded/broken out (and much simplified)
273 version of what ProcessRestrictions used to do. They're also much
274 more clearly delineated by the TYPE of field being processed.
283 my ( $sb, $field, $op, $value, @rest ) = @_;
285 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
287 die "Invalid operator $op for __Bookmarked__ search on $field"
288 unless $op =~ /^(=|!=)$/;
291 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
292 $tmp = $tmp->Content if $tmp;
297 return $sb->_SQLLimit(
304 # as bookmarked tickets can be merged we have to use a join
305 # but it should be pretty lightweight
306 my $tickets_alias = $sb->Join(
311 FIELD2 => 'EffectiveId',
315 my $ea = $op eq '='? 'OR': 'AND';
316 foreach my $id ( sort @bookmarks ) {
318 ALIAS => $tickets_alias,
322 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
330 Handle Fields which are limited to certain values, and potentially
331 need to be looked up from another class.
333 This subroutine actually handles two different kinds of fields. For
334 some the user is responsible for limiting the values. (i.e. Status,
337 For others, the value specified by the user will be looked by via
341 name of class to lookup in (Optional)
346 my ( $sb, $field, $op, $value, @rest ) = @_;
348 # SQL::Statement changes != to <>. (Can we remove this now?)
349 $op = "!=" if $op eq "<>";
351 die "Invalid Operation: $op for $field"
355 my $meta = $FIELD_METADATA{$field};
356 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
357 my $class = "RT::" . $meta->[1];
358 my $o = $class->new( $sb->CurrentUser );
372 Handle fields where the values are limited to integers. (For example,
373 Priority, TimeWorked.)
381 my ( $sb, $field, $op, $value, @rest ) = @_;
383 die "Invalid Operator $op for $field"
384 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
396 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
399 1: Direction (From, To)
400 2: Link Type (MemberOf, DependsOn, RefersTo)
405 my ( $sb, $field, $op, $value, @rest ) = @_;
407 my $meta = $FIELD_METADATA{$field};
408 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
411 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
415 $is_null = 1 if !$value || $value =~ /^null$/io;
417 my $direction = $meta->[1] || '';
418 my ($matchfield, $linkfield) = ('', '');
419 if ( $direction eq 'To' ) {
420 ($matchfield, $linkfield) = ("Target", "Base");
422 elsif ( $direction eq 'From' ) {
423 ($matchfield, $linkfield) = ("Base", "Target");
425 elsif ( $direction ) {
426 die "Invalid link direction '$direction' for $field\n";
429 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
431 'LinkedFrom', $op, $value, @rest,
432 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
440 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
442 elsif ( $value =~ /\D/ ) {
445 $matchfield = "Local$matchfield" if $is_local;
447 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
448 # SELECT main.* FROM Tickets main
449 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
450 # AND(main.id = Links_1.LocalTarget))
451 # WHERE Links_1.LocalBase IS NULL;
454 my $linkalias = $sb->Join(
459 FIELD2 => 'Local' . $linkfield
462 LEFTJOIN => $linkalias,
470 FIELD => $matchfield,
477 my $linkalias = $sb->Join(
482 FIELD2 => 'Local' . $linkfield
485 LEFTJOIN => $linkalias,
491 LEFTJOIN => $linkalias,
492 FIELD => $matchfield,
499 FIELD => $matchfield,
500 OPERATOR => $is_negative? 'IS': 'IS NOT',
509 Handle date fields. (Created, LastTold..)
512 1: type of link. (Probably not necessary.)
517 my ( $sb, $field, $op, $value, @rest ) = @_;
519 die "Invalid Date Op: $op"
520 unless $op =~ /^(=|>|<|>=|<=)$/;
522 my $meta = $FIELD_METADATA{$field};
523 die "Incorrect Meta Data for $field"
524 unless ( defined $meta->[1] );
526 $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest );
529 # Factor this out for use by custom fields
531 sub _DateFieldLimit {
532 my ( $sb, $field, $op, $value, @rest ) = @_;
534 my $date = RT::Date->new( $sb->CurrentUser );
535 $date->Set( Format => 'unknown', Value => $value );
539 # if we're specifying =, that means we want everything on a
540 # particular single day. in the database, we need to check for >
541 # and < the edges of that day.
543 my ($daystart, $dayend);
544 if ( lc($value) eq 'this month' ) {
545 # special case: > and < the edges of this month
547 $date->SetToStart('month');
548 $daystart = $date->ISO;
550 $dayend = $date->ISO;
553 $date->SetToMidnight( Timezone => 'server' );
554 $daystart = $date->ISO;
556 $dayend = $date->ISO;
573 ENTRYAGGREGATOR => 'AND',
591 Handle simple fields which are just strings. (Subject,Type)
599 my ( $sb, $field, $op, $value, @rest ) = @_;
603 # =, !=, LIKE, NOT LIKE
604 if ( (!defined $value || !length $value)
605 && lc($op) ne 'is' && lc($op) ne 'is not'
606 && RT->Config->Get('DatabaseType') eq 'Oracle'
608 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
609 $op = $negative? 'IS NOT': 'IS';
622 =head2 _TransDateLimit
624 Handle fields limiting based on Transaction Date.
626 The inpupt value must be in a format parseable by Time::ParseDate
633 # This routine should really be factored into translimit.
634 sub _TransDateLimit {
635 my ( $sb, $field, $op, $value, @rest ) = @_;
637 # See the comments for TransLimit, they apply here too
639 unless ( $sb->{_sql_transalias} ) {
640 $sb->{_sql_transalias} = $sb->Join(
643 TABLE2 => 'Transactions',
644 FIELD2 => 'ObjectId',
647 ALIAS => $sb->{_sql_transalias},
648 FIELD => 'ObjectType',
649 VALUE => 'RT::Ticket',
650 ENTRYAGGREGATOR => 'AND',
654 my $date = RT::Date->new( $sb->CurrentUser );
655 $date->Set( Format => 'unknown', Value => $value );
660 # if we're specifying =, that means we want everything on a
661 # particular single day. in the database, we need to check for >
662 # and < the edges of that day.
664 $date->SetToMidnight( Timezone => 'server' );
665 my $daystart = $date->ISO;
667 my $dayend = $date->ISO;
670 ALIAS => $sb->{_sql_transalias},
678 ALIAS => $sb->{_sql_transalias},
684 ENTRYAGGREGATOR => 'AND',
689 # not searching for a single day
692 #Search for the right field
694 ALIAS => $sb->{_sql_transalias},
708 Limit based on the Content of a transaction or the ContentType.
717 # Content, ContentType, Filename
719 # If only this was this simple. We've got to do something
722 #Basically, we want to make sure that the limits apply to
723 #the same attachment, rather than just another attachment
724 #for the same ticket, no matter how many clauses we lump
725 #on. We put them in TicketAliases so that they get nuked
726 #when we redo the join.
728 # In the SQL, we might have
729 # (( Content = foo ) or ( Content = bar AND Content = baz ))
730 # The AND group should share the same Alias.
732 # Actually, maybe it doesn't matter. We use the same alias and it
733 # works itself out? (er.. different.)
735 # Steal more from _ProcessRestrictions
737 # FIXME: Maybe look at the previous FooLimit call, and if it was a
738 # TransLimit and EntryAggregator == AND, reuse the Aliases?
740 # Or better - store the aliases on a per subclause basis - since
741 # those are going to be the things we want to relate to each other,
744 # maybe we should not allow certain kinds of aggregation of these
745 # clauses and do a psuedo regex instead? - the problem is getting
746 # them all into the same subclause when you have (A op B op C) - the
747 # way they get parsed in the tree they're in different subclauses.
749 my ( $self, $field, $op, $value, %rest ) = @_;
751 unless ( $self->{_sql_transalias} ) {
752 $self->{_sql_transalias} = $self->Join(
755 TABLE2 => 'Transactions',
756 FIELD2 => 'ObjectId',
759 ALIAS => $self->{_sql_transalias},
760 FIELD => 'ObjectType',
761 VALUE => 'RT::Ticket',
762 ENTRYAGGREGATOR => 'AND',
765 unless ( defined $self->{_sql_trattachalias} ) {
766 $self->{_sql_trattachalias} = $self->_SQLJoin(
767 TYPE => 'LEFT', # not all txns have an attachment
768 ALIAS1 => $self->{_sql_transalias},
770 TABLE2 => 'Attachments',
771 FIELD2 => 'TransactionId',
775 #Search for the right field
776 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
780 ALIAS => $self->{_sql_trattachalias},
787 ENTRYAGGREGATOR => 'AND',
788 ALIAS => $self->{_sql_trattachalias},
797 ALIAS => $self->{_sql_trattachalias},
810 Handle watcher limits. (Requestor, CC, etc..)
826 my $meta = $FIELD_METADATA{ $field };
827 my $type = $meta->[1] || '';
828 my $class = $meta->[2] || 'Ticket';
830 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
831 # search by id and Name at the same time, this is workaround
832 # to preserve backward compatibility
833 if ( $field eq 'Owner' ) {
834 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
835 my $o = RT::User->new( $self->CurrentUser );
836 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
837 $o->$method( $value );
846 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
856 $rest{SUBKEY} ||= 'EmailAddress';
858 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
861 if ( $op =~ /^IS(?: NOT)?$/ ) {
862 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
863 # to avoid joining the table Users into the query, we just join GM
864 # and make sure we don't match records where group is member of itself
866 LEFTJOIN => $group_members,
869 VALUE => "$group_members.MemberId",
873 ALIAS => $group_members,
880 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
882 $op =~ s/!|NOT\s+//i;
884 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
885 # "X = 'Y'" matches more then one user so we try to fetch two records and
886 # do the right thing when there is only one exist and semi-working solution
888 my $users_obj = RT::Users->new( $self->CurrentUser );
890 FIELD => $rest{SUBKEY},
895 $users_obj->RowsPerPage(2);
896 my @users = @{ $users_obj->ItemsArrayRef };
898 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
901 $uid = $users[0]->id if @users;
903 LEFTJOIN => $group_members,
904 ALIAS => $group_members,
910 ALIAS => $group_members,
917 LEFTJOIN => $group_members,
920 VALUE => "$group_members.MemberId",
923 my $users = $self->Join(
925 ALIAS1 => $group_members,
926 FIELD1 => 'MemberId',
933 FIELD => $rest{SUBKEY},
947 my $group_members = $self->_GroupMembersJoin(
948 GroupsAlias => $groups,
952 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
954 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
955 $self->NewAlias('Users');
957 LEFTJOIN => $group_members,
958 ALIAS => $group_members,
960 VALUE => "$users.id",
965 # we join users table without adding some join condition between tables,
966 # the only conditions we have are conditions on the table iteslf,
967 # for example Users.EmailAddress = 'x'. We should add this condition to
968 # the top level of the query and bundle it with another similar conditions,
969 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
970 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
973 SUBCLAUSE => '_sql_u_watchers_'. $users,
975 FIELD => $rest{'SUBKEY'},
980 # A condition which ties Users and Groups (role groups) is a left join condition
981 # of CachedGroupMembers table. To get correct results of the query we check
982 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
985 ALIAS => $group_members,
987 OPERATOR => 'IS NOT',
994 sub _RoleGroupsJoin {
996 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
997 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
998 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1001 # we always have watcher groups for ticket, so we use INNER join
1002 my $groups = $self->Join(
1004 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1006 FIELD2 => 'Instance',
1007 ENTRYAGGREGATOR => 'AND',
1009 $self->SUPER::Limit(
1010 LEFTJOIN => $groups,
1013 VALUE => 'RT::'. $args{'Class'} .'-Role',
1015 $self->SUPER::Limit(
1016 LEFTJOIN => $groups,
1019 VALUE => $args{'Type'},
1022 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1023 unless $args{'New'};
1028 sub _GroupMembersJoin {
1030 my %args = (New => 1, GroupsAlias => undef, @_);
1032 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1033 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1036 my $alias = $self->Join(
1038 ALIAS1 => $args{'GroupsAlias'},
1040 TABLE2 => 'CachedGroupMembers',
1041 FIELD2 => 'GroupId',
1042 ENTRYAGGREGATOR => 'AND',
1045 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1046 unless $args{'New'};
1053 Helper function which provides joins to a watchers table both for limits
1060 my $type = shift || '';
1063 my $groups = $self->_RoleGroupsJoin( Type => $type );
1064 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1065 # XXX: work around, we must hide groups that
1066 # are members of the role group we search in,
1067 # otherwise them result in wrong NULLs in Users
1068 # table and break ordering. Now, we know that
1069 # RT doesn't allow to add groups as members of the
1070 # ticket roles, so we just hide entries in CGM table
1071 # with MemberId == GroupId from results
1072 $self->SUPER::Limit(
1073 LEFTJOIN => $group_members,
1076 VALUE => "$group_members.MemberId",
1079 my $users = $self->Join(
1081 ALIAS1 => $group_members,
1082 FIELD1 => 'MemberId',
1086 return ($groups, $group_members, $users);
1089 =head2 _WatcherMembershipLimit
1091 Handle watcher membership limits, i.e. whether the watcher belongs to a
1092 specific group or not.
1095 1: Field to query on
1097 SELECT DISTINCT main.*
1101 CachedGroupMembers CachedGroupMembers_2,
1104 (main.EffectiveId = main.id)
1106 (main.Status != 'deleted')
1108 (main.Type = 'ticket')
1111 (Users_3.EmailAddress = '22')
1113 (Groups_1.Domain = 'RT::Ticket-Role')
1115 (Groups_1.Type = 'RequestorGroup')
1118 Groups_1.Instance = main.id
1120 Groups_1.id = CachedGroupMembers_2.GroupId
1122 CachedGroupMembers_2.MemberId = Users_3.id
1123 ORDER BY main.id ASC
1128 sub _WatcherMembershipLimit {
1129 my ( $self, $field, $op, $value, @rest ) = @_;
1134 my $groups = $self->NewAlias('Groups');
1135 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1136 my $users = $self->NewAlias('Users');
1137 my $memberships = $self->NewAlias('CachedGroupMembers');
1139 if ( ref $field ) { # gross hack
1140 my @bundle = @$field;
1142 for my $chunk (@bundle) {
1143 ( $field, $op, $value, @rest ) = @$chunk;
1145 ALIAS => $memberships,
1156 ALIAS => $memberships,
1164 # {{{ Tie to groups for tickets we care about
1168 VALUE => 'RT::Ticket-Role',
1169 ENTRYAGGREGATOR => 'AND'
1174 FIELD1 => 'Instance',
1181 # If we care about which sort of watcher
1182 my $meta = $FIELD_METADATA{$field};
1183 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1190 ENTRYAGGREGATOR => 'AND'
1197 ALIAS2 => $groupmembers,
1202 ALIAS1 => $groupmembers,
1203 FIELD1 => 'MemberId',
1209 ALIAS1 => $memberships,
1210 FIELD1 => 'MemberId',
1219 =head2 _CustomFieldDecipher
1221 Try and turn a CF descriptor into (cfid, cfname) object pair.
1225 sub _CustomFieldDecipher {
1226 my ($self, $string) = @_;
1228 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
1229 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1233 my $q = RT::Queue->new( $self->CurrentUser );
1237 # $queue = $q->Name; # should we normalize the queue?
1238 $cf = $q->CustomField( $field );
1241 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1245 elsif ( $field =~ /\D/ ) {
1247 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1248 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1249 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1251 # if there is more then one field the current user can
1252 # see with the same name then we shouldn't return cf object
1253 # as we don't know which one to use
1256 $cf = undef if $cfs->Next;
1260 $cf = RT::CustomField->new( $self->CurrentUser );
1261 $cf->Load( $field );
1264 return ($queue, $field, $cf, $column);
1267 =head2 _CustomFieldJoin
1269 Factor out the Join of custom fields so we can use it for sorting too
1273 sub _CustomFieldJoin {
1274 my ($self, $cfkey, $cfid, $field) = @_;
1275 # Perform one Join per CustomField
1276 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1277 $self->{_sql_cf_alias}{$cfkey} )
1279 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1280 $self->{_sql_cf_alias}{$cfkey} );
1283 my ($TicketCFs, $CFs);
1285 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1289 TABLE2 => 'ObjectCustomFieldValues',
1290 FIELD2 => 'ObjectId',
1292 $self->SUPER::Limit(
1293 LEFTJOIN => $TicketCFs,
1294 FIELD => 'CustomField',
1296 ENTRYAGGREGATOR => 'AND'
1300 my $ocfalias = $self->Join(
1303 TABLE2 => 'ObjectCustomFields',
1304 FIELD2 => 'ObjectId',
1307 $self->SUPER::Limit(
1308 LEFTJOIN => $ocfalias,
1309 ENTRYAGGREGATOR => 'OR',
1310 FIELD => 'ObjectId',
1314 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1316 ALIAS1 => $ocfalias,
1317 FIELD1 => 'CustomField',
1318 TABLE2 => 'CustomFields',
1321 $self->SUPER::Limit(
1323 ENTRYAGGREGATOR => 'AND',
1324 FIELD => 'LookupType',
1325 VALUE => 'RT::Queue-RT::Ticket',
1327 $self->SUPER::Limit(
1329 ENTRYAGGREGATOR => 'AND',
1334 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1338 TABLE2 => 'ObjectCustomFieldValues',
1339 FIELD2 => 'CustomField',
1341 $self->SUPER::Limit(
1342 LEFTJOIN => $TicketCFs,
1343 FIELD => 'ObjectId',
1346 ENTRYAGGREGATOR => 'AND',
1349 $self->SUPER::Limit(
1350 LEFTJOIN => $TicketCFs,
1351 FIELD => 'ObjectType',
1352 VALUE => 'RT::Ticket',
1353 ENTRYAGGREGATOR => 'AND'
1355 $self->SUPER::Limit(
1356 LEFTJOIN => $TicketCFs,
1357 FIELD => 'Disabled',
1360 ENTRYAGGREGATOR => 'AND'
1363 return ($TicketCFs, $CFs);
1366 =head2 _CustomFieldLimit
1368 Limit based on CustomFields
1375 sub _CustomFieldLimit {
1376 my ( $self, $_field, $op, $value, %rest ) = @_;
1378 my $field = $rest{'SUBKEY'} || die "No field specified";
1380 # For our sanity, we can only limit on one queue at a time
1382 my ($queue, $cfid, $cf, $column);
1383 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1384 $cfid = $cf ? $cf->id : 0 ;
1386 # If we're trying to find custom fields that don't match something, we
1387 # want tickets where the custom field has no value at all. Note that
1388 # we explicitly don't include the "IS NULL" case, since we would
1389 # otherwise end up with a redundant clause.
1391 my ($negative_op, $null_op, $inv_op, $range_op)
1392 = $self->ClassifySQLOperation( $op );
1396 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1397 return 'MATCHES' if $op eq '=';
1398 return 'NOT MATCHES' if $op eq '!=';
1402 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1404 my $cfkey = $cfid ? $cfid : "$queue.$field";
1406 if ( $null_op && !$column ) {
1407 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1408 # we can reuse our default joins for this operation
1409 # with column specified we have different situation
1410 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1413 ALIAS => $TicketCFs,
1422 OPERATOR => 'IS NOT',
1425 ENTRYAGGREGATOR => 'AND',
1429 elsif ( !$negative_op || $single_value ) {
1430 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1431 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1438 # if column is defined then deal only with it
1439 # otherwise search in Content and in LargeContent
1442 ALIAS => $TicketCFs,
1444 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1449 elsif ( $cf->Type eq 'Date' ) {
1450 $self->_DateFieldLimit(
1454 ALIAS => $TicketCFs,
1458 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1459 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1461 ALIAS => $TicketCFs,
1470 ALIAS => $TicketCFs,
1474 ENTRYAGGREGATOR => 'OR'
1477 ALIAS => $TicketCFs,
1481 ENTRYAGGREGATOR => 'OR'
1485 ALIAS => $TicketCFs,
1486 FIELD => 'LargeContent',
1487 OPERATOR => $fix_op->($op),
1489 ENTRYAGGREGATOR => 'AND',
1495 ALIAS => $TicketCFs,
1505 ALIAS => $TicketCFs,
1509 ENTRYAGGREGATOR => 'OR'
1512 ALIAS => $TicketCFs,
1516 ENTRYAGGREGATOR => 'OR'
1520 ALIAS => $TicketCFs,
1521 FIELD => 'LargeContent',
1522 OPERATOR => $fix_op->($op),
1524 ENTRYAGGREGATOR => 'AND',
1530 # XXX: if we join via CustomFields table then
1531 # because of order of left joins we get NULLs in
1532 # CF table and then get nulls for those records
1533 # in OCFVs table what result in wrong results
1534 # as decifer method now tries to load a CF then
1535 # we fall into this situation only when there
1536 # are more than one CF with the name in the DB.
1537 # the same thing applies to order by call.
1538 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1539 # we want treat IS NULL as (not applies or has
1544 OPERATOR => 'IS NOT',
1547 ENTRYAGGREGATOR => 'AND',
1553 ALIAS => $TicketCFs,
1554 FIELD => $column || 'Content',
1558 ENTRYAGGREGATOR => 'OR',
1565 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1566 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1569 $op =~ s/!|NOT\s+//i;
1571 # if column is defined then deal only with it
1572 # otherwise search in Content and in LargeContent
1574 $self->SUPER::Limit(
1575 LEFTJOIN => $TicketCFs,
1576 ALIAS => $TicketCFs,
1578 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1583 $self->SUPER::Limit(
1584 LEFTJOIN => $TicketCFs,
1585 ALIAS => $TicketCFs,
1593 ALIAS => $TicketCFs,
1602 sub _HasAttributeLimit {
1603 my ( $self, $field, $op, $value, %rest ) = @_;
1605 my $alias = $self->Join(
1609 TABLE2 => 'Attributes',
1610 FIELD2 => 'ObjectId',
1612 $self->SUPER::Limit(
1614 FIELD => 'ObjectType',
1615 VALUE => 'RT::Ticket',
1616 ENTRYAGGREGATOR => 'AND'
1618 $self->SUPER::Limit(
1623 ENTRYAGGREGATOR => 'AND'
1629 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1635 # End Helper Functions
1637 # End of SQL Stuff -------------------------------------------------
1639 # {{{ Allow sorting on watchers
1641 =head2 OrderByCols ARRAY
1643 A modified version of the OrderBy method which automatically joins where
1644 C<ALIAS> is set to the name of a watcher type.
1655 foreach my $row (@args) {
1656 if ( $row->{ALIAS} ) {
1660 if ( $row->{FIELD} !~ /\./ ) {
1661 my $meta = $self->FIELDS->{ $row->{FIELD} };
1667 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1668 my $alias = $self->Join(
1671 FIELD1 => $row->{'FIELD'},
1675 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1676 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1677 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1679 my $alias = $self->Join(
1682 FIELD1 => $row->{'FIELD'},
1686 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1693 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1694 my $meta = $self->FIELDS->{$field};
1695 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1696 # cache alias as we want to use one alias per watcher type for sorting
1697 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1699 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1700 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1702 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1703 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1704 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1705 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1706 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1707 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1708 # this is described in _CustomFieldLimit
1712 OPERATOR => 'IS NOT',
1715 ENTRYAGGREGATOR => 'AND',
1718 # For those cases where we are doing a join against the
1719 # CF name, and don't have a CFid, use Unique to make sure
1720 # we don't show duplicate tickets. NOTE: I'm pretty sure
1721 # this will stay mixed in for the life of the
1722 # class/package, and not just for the life of the object.
1723 # Potential performance issue.
1724 require DBIx::SearchBuilder::Unique;
1725 DBIx::SearchBuilder::Unique->import;
1727 my $CFvs = $self->Join(
1729 ALIAS1 => $TicketCFs,
1730 FIELD1 => 'CustomField',
1731 TABLE2 => 'CustomFieldValues',
1732 FIELD2 => 'CustomField',
1734 $self->SUPER::Limit(
1738 VALUE => $TicketCFs . ".Content",
1739 ENTRYAGGREGATOR => 'AND'
1742 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1743 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1744 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1745 # PAW logic is "reversed"
1747 if (exists $row->{ORDER} ) {
1748 my $o = $row->{ORDER};
1749 delete $row->{ORDER};
1750 $order = "DESC" if $o =~ /asc/i;
1753 # Ticket.Owner 1 0 X
1754 # Unowned Tickets 0 1 X
1757 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1758 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1759 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1760 push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
1762 push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
1766 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1768 } elsif ( $field eq 'Customer' ) { #Freeside
1769 if ( $subkey eq 'Number' ) {
1770 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1773 FIELD => $custnum_sql,
1777 my $custalias = $self->JoinToCustomer;
1779 if ( $subkey eq 'Name' ) {
1780 $field = "COALESCE( $custalias.company,
1781 $custalias.last || ', ' || $custalias.first
1785 # no other cases exist yet, but for obviousness:
1788 push @res, { %$row, ALIAS => '', FIELD => $field };
1797 return $self->SUPER::OrderByCols(@res);
1802 sub JoinToCustLinks {
1803 # Set up join to links (id = localbase),
1804 # limit link type to 'MemberOf',
1805 # and target value to any Freeside custnum URI.
1806 # Return the linkalias for further join/limit action,
1807 # and an sql expression to retrieve the custnum.
1809 my $linkalias = $self->Join(
1814 FIELD2 => 'LocalBase',
1817 $self->SUPER::Limit(
1818 LEFTJOIN => $linkalias,
1821 VALUE => 'MemberOf',
1823 $self->SUPER::Limit(
1824 LEFTJOIN => $linkalias,
1826 OPERATOR => 'STARTSWITH',
1827 VALUE => 'freeside://freeside/cust_main/',
1829 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1830 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1831 $custnum_sql .= 'SIGNED INTEGER)';
1834 $custnum_sql .= 'INTEGER)';
1836 return ($linkalias, $custnum_sql);
1839 sub JoinToCustomer {
1841 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1843 my $custalias = $self->Join(
1845 EXPRESSION => $custnum_sql,
1846 TABLE2 => 'cust_main',
1847 FIELD2 => 'custnum',
1852 sub _FreesideFieldLimit {
1853 my ( $self, $field, $op, $value, %rest ) = @_;
1854 my $alias = $self->JoinToCustomer;
1855 my $is_negative = 0;
1856 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1857 # if the op is negative, do the join as though
1858 # the op were positive, then accept only records
1859 # where the right-side join key is null.
1861 $op = '=' if $op eq '!=';
1864 my $meta = $FIELD_METADATA{$field};
1866 $alias = $self->Join(
1869 FIELD1 => 'custnum',
1870 TABLE2 => $meta->[1],
1871 FIELD2 => 'custnum',
1875 $self->SUPER::Limit(
1877 FIELD => lc($field),
1880 ENTRYAGGREGATOR => 'AND',
1885 FIELD => lc($field),
1886 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
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 #If we are looking to compare with a null value.
2821 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2822 $args{'DESCRIPTION'}
2823 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2825 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2826 $args{'DESCRIPTION'}
2827 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2830 # if we're not looking to compare with a null value
2832 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2833 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2836 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2837 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2838 $QueueObj->Load( $args{'QUEUE'} );
2839 $args{'QUEUE'} = $QueueObj->Id;
2841 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2844 @rest = ( ENTRYAGGREGATOR => 'AND' )
2845 if ( $CF->Type eq 'SelectMultiple' );
2848 VALUE => $args{VALUE},
2850 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2851 .".{" . $CF->Name . "}",
2852 OPERATOR => $args{OPERATOR},
2857 $self->{'RecalcTicketLimits'} = 1;
2863 # {{{ sub _NextIndex
2867 Keep track of the counter for the array of restrictions
2873 return ( $self->{'restriction_index'}++ );
2880 # {{{ Core bits to make this a DBIx::SearchBuilder object
2885 $self->{'table'} = "Tickets";
2886 $self->{'RecalcTicketLimits'} = 1;
2887 $self->{'looking_at_effective_id'} = 0;
2888 $self->{'looking_at_type'} = 0;
2889 $self->{'restriction_index'} = 1;
2890 $self->{'primary_key'} = "id";
2891 delete $self->{'items_array'};
2892 delete $self->{'item_map'};
2893 delete $self->{'columns_to_display'};
2894 $self->SUPER::_Init(@_);
2905 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2906 return ( $self->SUPER::Count() );
2914 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2915 return ( $self->SUPER::CountAll() );
2920 # {{{ sub ItemsArrayRef
2922 =head2 ItemsArrayRef
2924 Returns a reference to the set of all items found in this search
2931 return $self->{'items_array'} if $self->{'items_array'};
2933 my $placeholder = $self->_ItemsCounter;
2934 $self->GotoFirstItem();
2935 while ( my $item = $self->Next ) {
2936 push( @{ $self->{'items_array'} }, $item );
2938 $self->GotoItem($placeholder);
2939 $self->{'items_array'}
2940 = $self->ItemsOrderBy( $self->{'items_array'} );
2942 return $self->{'items_array'};
2945 sub ItemsArrayRefWindow {
2949 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2951 $self->RowsPerPage( $window );
2953 $self->GotoFirstItem;
2956 while ( my $item = $self->Next ) {
2960 $self->RowsPerPage( $old[1] );
2961 $self->FirstRow( $old[2] );
2962 $self->GotoItem( $old[0] );
2973 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2975 my $Ticket = $self->SUPER::Next;
2976 return $Ticket unless $Ticket;
2978 if ( $Ticket->__Value('Status') eq 'deleted'
2979 && !$self->{'allow_deleted_search'} )
2983 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2984 # if we found a ticket with this option enabled then
2985 # all tickets we found are ACLed, cache this fact
2986 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2987 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2990 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2995 # If the user doesn't have the right to show this ticket
3002 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3003 return $self->SUPER::_DoSearch( @_ );
3008 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3009 return $self->SUPER::_DoCount( @_ );
3015 my $cache_key = 'RolesHasRight;:;ShowTicket';
3017 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3021 my $ACL = RT::ACL->new( $RT::SystemUser );
3022 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3023 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3024 my $principal_alias = $ACL->Join(
3026 FIELD1 => 'PrincipalId',
3027 TABLE2 => 'Principals',
3030 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3033 while ( my $ACE = $ACL->Next ) {
3034 my $role = $ACE->PrincipalType;
3035 my $type = $ACE->ObjectType;
3036 if ( $type eq 'RT::System' ) {
3039 elsif ( $type eq 'RT::Queue' ) {
3040 next if $res{ $role } && !ref $res{ $role };
3041 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3044 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3047 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3051 sub _DirectlyCanSeeIn {
3053 my $id = $self->CurrentUser->id;
3055 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3056 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3060 my $ACL = RT::ACL->new( $RT::SystemUser );
3061 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3062 my $principal_alias = $ACL->Join(
3064 FIELD1 => 'PrincipalId',
3065 TABLE2 => 'Principals',
3068 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3069 my $cgm_alias = $ACL->Join(
3071 FIELD1 => 'PrincipalId',
3072 TABLE2 => 'CachedGroupMembers',
3073 FIELD2 => 'GroupId',
3075 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3076 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3079 while ( my $ACE = $ACL->Next ) {
3080 my $type = $ACE->ObjectType;
3081 if ( $type eq 'RT::System' ) {
3082 # If user is direct member of a group that has the right
3083 # on the system then he can see any ticket
3084 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3087 elsif ( $type eq 'RT::Queue' ) {
3088 push @res, $ACE->ObjectId;
3091 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3094 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3098 sub CurrentUserCanSee {
3100 return if $self->{'_sql_current_user_can_see_applied'};
3102 return $self->{'_sql_current_user_can_see_applied'} = 1
3103 if $self->CurrentUser->UserObj->HasRight(
3104 Right => 'SuperUser', Object => $RT::System
3107 my $id = $self->CurrentUser->id;
3109 # directly can see in all queues then we have nothing to do
3110 my @direct_queues = $self->_DirectlyCanSeeIn;
3111 return $self->{'_sql_current_user_can_see_applied'} = 1
3112 if @direct_queues && $direct_queues[0] == -1;
3114 my %roles = $self->_RolesCanSee;
3116 my %skip = map { $_ => 1 } @direct_queues;
3117 foreach my $role ( keys %roles ) {
3118 next unless ref $roles{ $role };
3120 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3122 $roles{ $role } = \@queues;
3124 delete $roles{ $role };
3129 # there is no global watchers, only queues and tickes, if at
3130 # some point we will add global roles then it's gonna blow
3131 # the idea here is that if the right is set globaly for a role
3132 # and user plays this role for a queue directly not a ticket
3133 # then we have to check in advance
3134 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3136 my $groups = RT::Groups->new( $RT::SystemUser );
3137 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3139 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3141 my $principal_alias = $groups->Join(
3144 TABLE2 => 'Principals',
3147 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3148 my $cgm_alias = $groups->Join(
3151 TABLE2 => 'CachedGroupMembers',
3152 FIELD2 => 'GroupId',
3154 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3155 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3156 while ( my $group = $groups->Next ) {
3157 push @direct_queues, $group->Instance;
3161 unless ( @direct_queues || keys %roles ) {
3162 $self->SUPER::Limit(
3167 ENTRYAGGREGATOR => 'AND',
3169 return $self->{'_sql_current_user_can_see_applied'} = 1;
3173 my $join_roles = keys %roles;
3174 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3175 my ($role_group_alias, $cgm_alias);
3176 if ( $join_roles ) {
3177 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3178 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3179 $self->SUPER::Limit(
3180 LEFTJOIN => $cgm_alias,
3181 FIELD => 'MemberId',
3186 my $limit_queues = sub {
3190 return unless @queues;
3191 if ( @queues == 1 ) {
3192 $self->SUPER::Limit(
3197 ENTRYAGGREGATOR => $ea,
3200 $self->SUPER::_OpenParen('ACL');
3201 foreach my $q ( @queues ) {
3202 $self->SUPER::Limit(
3207 ENTRYAGGREGATOR => $ea,
3211 $self->SUPER::_CloseParen('ACL');
3216 $self->SUPER::_OpenParen('ACL');
3218 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3219 while ( my ($role, $queues) = each %roles ) {
3220 $self->SUPER::_OpenParen('ACL');
3221 if ( $role eq 'Owner' ) {
3222 $self->SUPER::Limit(
3226 ENTRYAGGREGATOR => $ea,
3230 $self->SUPER::Limit(
3232 ALIAS => $cgm_alias,
3233 FIELD => 'MemberId',
3234 OPERATOR => 'IS NOT',
3237 ENTRYAGGREGATOR => $ea,
3239 $self->SUPER::Limit(
3241 ALIAS => $role_group_alias,
3244 ENTRYAGGREGATOR => 'AND',
3247 $limit_queues->( 'AND', @$queues ) if ref $queues;
3248 $ea = 'OR' if $ea eq 'AND';
3249 $self->SUPER::_CloseParen('ACL');
3251 $self->SUPER::_CloseParen('ACL');
3253 return $self->{'_sql_current_user_can_see_applied'} = 1;
3260 # {{{ Deal with storing and restoring restrictions
3262 # {{{ sub LoadRestrictions
3264 =head2 LoadRestrictions
3266 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3267 TODO It is not yet implemented
3273 # {{{ sub DescribeRestrictions
3275 =head2 DescribeRestrictions
3278 Returns a hash keyed by restriction id.
3279 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3280 is a description of the purpose of that TicketRestriction
3284 sub DescribeRestrictions {
3287 my ( $row, %listing );
3289 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3290 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3297 # {{{ sub RestrictionValues
3299 =head2 RestrictionValues FIELD
3301 Takes a restriction field and returns a list of values this field is restricted
3306 sub RestrictionValues {
3309 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3310 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3311 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3313 keys %{ $self->{'TicketRestrictions'} };
3318 # {{{ sub ClearRestrictions
3320 =head2 ClearRestrictions
3322 Removes all restrictions irretrievably
3326 sub ClearRestrictions {
3328 delete $self->{'TicketRestrictions'};
3329 $self->{'looking_at_effective_id'} = 0;
3330 $self->{'looking_at_type'} = 0;
3331 $self->{'RecalcTicketLimits'} = 1;
3336 # {{{ sub DeleteRestriction
3338 =head2 DeleteRestriction
3340 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3341 Removes that restriction from the session's limits.
3345 sub DeleteRestriction {
3348 delete $self->{'TicketRestrictions'}{$row};
3350 $self->{'RecalcTicketLimits'} = 1;
3352 #make the underlying easysearch object forget all its preconceptions
3357 # {{{ sub _RestrictionsToClauses
3359 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3361 sub _RestrictionsToClauses {
3366 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3367 my $restriction = $self->{'TicketRestrictions'}{$row};
3369 # We need to reimplement the subclause aggregation that SearchBuilder does.
3370 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3371 # Then SB AND's the different Subclauses together.
3373 # So, we want to group things into Subclauses, convert them to
3374 # SQL, and then join them with the appropriate DefaultEA.
3375 # Then join each subclause group with AND.
3377 my $field = $restriction->{'FIELD'};
3378 my $realfield = $field; # CustomFields fake up a fieldname, so
3379 # we need to figure that out
3382 # Rewrite LinkedTo meta field to the real field
3383 if ( $field =~ /LinkedTo/ ) {
3384 $realfield = $field = $restriction->{'TYPE'};
3388 # Handle subkey fields with a different real field
3389 if ( $field =~ /^(\w+)\./ ) {
3393 die "I don't know about $field yet"
3394 unless ( exists $FIELD_METADATA{$realfield}
3395 or $restriction->{CUSTOMFIELD} );
3397 my $type = $FIELD_METADATA{$realfield}->[0];
3398 my $op = $restriction->{'OPERATOR'};
3402 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3405 # this performs the moral equivalent of defined or/dor/C<//>,
3406 # without the short circuiting.You need to use a 'defined or'
3407 # type thing instead of just checking for truth values, because
3408 # VALUE could be 0.(i.e. "false")
3410 # You could also use this, but I find it less aesthetic:
3411 # (although it does short circuit)
3412 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3413 # defined $restriction->{'TICKET'} ?
3414 # $restriction->{TICKET} :
3415 # defined $restriction->{'BASE'} ?
3416 # $restriction->{BASE} :
3417 # defined $restriction->{'TARGET'} ?
3418 # $restriction->{TARGET} )
3420 my $ea = $restriction->{ENTRYAGGREGATOR}
3421 || $DefaultEA{$type}
3424 die "Invalid operator $op for $field ($type)"
3425 unless exists $ea->{$op};
3429 # Each CustomField should be put into a different Clause so they
3430 # are ANDed together.
3431 if ( $restriction->{CUSTOMFIELD} ) {
3432 $realfield = $field;
3435 exists $clause{$realfield} or $clause{$realfield} = [];
3438 $field =~ s!(['"])!\\$1!g;
3439 $value =~ s!(['"])!\\$1!g;
3440 my $data = [ $ea, $type, $field, $op, $value ];
3442 # here is where we store extra data, say if it's a keyword or
3443 # something. (I.e. "TYPE SPECIFIC STUFF")
3445 push @{ $clause{$realfield} }, $data;
3452 # {{{ sub _ProcessRestrictions
3454 =head2 _ProcessRestrictions PARAMHASH
3456 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3457 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3461 sub _ProcessRestrictions {
3464 #Blow away ticket aliases since we'll need to regenerate them for
3466 delete $self->{'TicketAliases'};
3467 delete $self->{'items_array'};
3468 delete $self->{'item_map'};
3469 delete $self->{'raw_rows'};
3470 delete $self->{'rows'};
3471 delete $self->{'count_all'};
3473 my $sql = $self->Query; # Violating the _SQL namespace
3474 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3476 # "Restrictions to Clauses Branch\n";
3477 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3479 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3483 $sql = $self->ClausesToSQL($clauseRef);
3484 $self->FromSQL($sql) if $sql;
3488 $self->{'RecalcTicketLimits'} = 0;
3492 =head2 _BuildItemMap
3494 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3495 display search nav quickly.
3502 my $window = RT->Config->Get('TicketsItemMapSize');
3504 $self->{'item_map'} = {};
3506 my $items = $self->ItemsArrayRefWindow( $window );
3507 return unless $items && @$items;
3510 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3511 for ( my $i = 0; $i < @$items; $i++ ) {
3512 my $item = $items->[$i];
3513 my $id = $item->EffectiveId;
3514 $self->{'item_map'}{$id}{'defined'} = 1;
3515 $self->{'item_map'}{$id}{'prev'} = $prev;
3516 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3520 $self->{'item_map'}{'last'} = $prev
3521 if !$window || @$items < $window;
3526 Returns an a map of all items found by this search. The map is a hash
3530 first => <first ticket id found>,
3531 last => <last ticket id found or undef>,
3534 prev => <the ticket id found before>,
3535 next => <the ticket id found after>,
3547 $self->_BuildItemMap unless $self->{'item_map'};
3548 return $self->{'item_map'};
3556 =head2 PrepForSerialization
3558 You don't want to serialize a big tickets object, as
3559 the {items} hash will be instantly invalid _and_ eat
3564 sub PrepForSerialization {
3566 delete $self->{'items'};
3567 delete $self->{'items_array'};
3568 $self->RedoSearch();
3573 RT::Tickets supports several flags which alter search behavior:
3576 allow_deleted_search (Otherwise never show deleted tickets in search results)
3577 looking_at_type (otherwise limit to type=ticket)
3579 These flags are set by calling
3581 $tickets->{'flagname'} = 1;
3583 BUG: There should be an API for this