1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2007 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/copyleft/gpl.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 }}}
50 # - Decimated ProcessRestrictions and broke it into multiple
51 # functions joined by a LUT
52 # - Semi-Generic SQL stuff moved to another file
54 # Known Issues: FIXME!
56 # - ClearRestrictions and Reinitialization is messy and unclear. The
57 # only good way to do it is to create a new RT::Tickets object.
61 RT::Tickets - A collection of Ticket objects
67 my $tickets = new RT::Tickets($CurrentUser);
71 A collection of RT::Tickets.
77 ok (require RT::Tickets);
78 ok( my $testtickets = RT::Tickets->new( $RT::SystemUser ) );
79 ok( $testtickets->LimitStatus( VALUE => 'deleted' ) );
80 # Should be zero until 'allow_deleted_search'
81 ok( $testtickets->Count == 0 );
90 no warnings qw(redefine);
93 use DBIx::SearchBuilder::Unique;
95 # Configuration Tables:
97 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
100 my %FIELD_METADATA = (
101 Status => [ 'ENUM', ],
102 Queue => [ 'ENUM' => 'Queue', ],
104 Creator => [ 'ENUM' => 'User', ],
105 LastUpdatedBy => [ 'ENUM' => 'User', ],
106 Owner => [ 'WATCHERFIELD' => 'Owner', ],
107 EffectiveId => [ 'INT', ],
109 InitialPriority => [ 'INT', ],
110 FinalPriority => [ 'INT', ],
111 Priority => [ 'INT', ],
112 TimeLeft => [ 'INT', ],
113 TimeWorked => [ 'INT', ],
114 TimeEstimated => [ 'INT', ],
115 MemberOf => [ 'LINK' => To => 'MemberOf', ],
116 DependsOn => [ 'LINK' => To => 'DependsOn', ],
117 RefersTo => [ 'LINK' => To => 'RefersTo', ],
118 HasMember => [ 'LINK' => From => 'MemberOf', ],
119 DependentOn => [ 'LINK' => From => 'DependsOn', ],
120 DependedOnBy => [ 'LINK' => From => 'DependsOn', ],
121 ReferredToBy => [ 'LINK' => From => 'RefersTo', ],
122 Told => [ 'DATE' => 'Told', ],
123 Starts => [ 'DATE' => 'Starts', ],
124 Started => [ 'DATE' => 'Started', ],
125 Due => [ 'DATE' => 'Due', ],
126 Resolved => [ 'DATE' => 'Resolved', ],
127 LastUpdated => [ 'DATE' => 'LastUpdated', ],
128 Created => [ 'DATE' => 'Created', ],
129 Subject => [ 'STRING', ],
130 Content => [ 'TRANSFIELD', ],
131 ContentType => [ 'TRANSFIELD', ],
132 Filename => [ 'TRANSFIELD', ],
133 TransactionDate => [ 'TRANSDATE', ],
134 Requestor => [ 'WATCHERFIELD' => 'Requestor', ],
135 Requestors => [ 'WATCHERFIELD' => 'Requestor', ],
136 Cc => [ 'WATCHERFIELD' => 'Cc', ],
137 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ],
138 Watcher => [ 'WATCHERFIELD', ],
139 LinkedTo => [ 'LINKFIELD', ],
140 CustomFieldValue => [ 'CUSTOMFIELD', ],
141 CustomField => [ 'CUSTOMFIELD', ],
142 CF => [ 'CUSTOMFIELD', ],
143 Updated => [ 'TRANSDATE', ],
144 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ],
145 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ],
146 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ],
147 WatcherGroup => [ 'MEMBERSHIPFIELD', ],
150 # Mapping of Field Type to Function
152 ENUM => \&_EnumLimit,
154 LINK => \&_LinkLimit,
155 DATE => \&_DateLimit,
156 STRING => \&_StringLimit,
157 TRANSFIELD => \&_TransLimit,
158 TRANSDATE => \&_TransDateLimit,
159 WATCHERFIELD => \&_WatcherLimit,
160 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
161 LINKFIELD => \&_LinkFieldLimit,
162 CUSTOMFIELD => \&_CustomFieldLimit,
164 my %can_bundle = (); # WATCHERFIELD => "yes", );
166 # Default EntryAggregator per type
167 # if you specify OP, you must specify all valid OPs
203 # Helper functions for passing the above lexically scoped tables above
204 # into Tickets_Overlay_SQL.
205 sub FIELDS { return \%FIELD_METADATA }
206 sub dispatch { return \%dispatch }
207 sub can_bundle { return \%can_bundle }
209 # Bring in the clowns.
210 require RT::Tickets_Overlay_SQL;
214 our @SORTFIELDS = qw(id Status
216 Owner Created Due Starts Started
218 Resolved LastUpdated Priority TimeWorked TimeLeft);
222 Returns the list of fields that lists of tickets can easily be sorted by
228 return (@SORTFIELDS);
233 # BEGIN SQL STUFF *********************************
238 $self->SUPER::CleanSlate( @_ );
239 delete $self->{$_} foreach qw(
241 _sql_group_members_aliases
242 _sql_object_cfv_alias
243 _sql_role_group_aliases
246 _sql_u_watchers_alias_for_sort
247 _sql_u_watchers_aliases
251 =head1 Limit Helper Routines
253 These routines are the targets of a dispatch table depending on the
254 type of field. They all share the same signature:
256 my ($self,$field,$op,$value,@rest) = @_;
258 The values in @rest should be suitable for passing directly to
259 DBIx::SearchBuilder::Limit.
261 Essentially they are an expanded/broken out (and much simplified)
262 version of what ProcessRestrictions used to do. They're also much
263 more clearly delineated by the TYPE of field being processed.
267 Handle Fields which are limited to certain values, and potentially
268 need to be looked up from another class.
270 This subroutine actually handles two different kinds of fields. For
271 some the user is responsible for limiting the values. (i.e. Status,
274 For others, the value specified by the user will be looked by via
278 name of class to lookup in (Optional)
283 my ( $sb, $field, $op, $value, @rest ) = @_;
285 # SQL::Statement changes != to <>. (Can we remove this now?)
286 $op = "!=" if $op eq "<>";
288 die "Invalid Operation: $op for $field"
292 my $meta = $FIELD_METADATA{$field};
293 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
294 my $class = "RT::" . $meta->[1];
295 my $o = $class->new( $sb->CurrentUser );
309 Handle fields where the values are limited to integers. (For example,
310 Priority, TimeWorked.)
318 my ( $sb, $field, $op, $value, @rest ) = @_;
320 die "Invalid Operator $op for $field"
321 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
333 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
336 1: Direction (From, To)
337 2: Link Type (MemberOf, DependsOn, RefersTo)
342 my ( $sb, $field, $op, $value, @rest ) = @_;
344 my $meta = $FIELD_METADATA{$field};
345 die "Incorrect Metadata for $field"
346 unless defined $meta->[1] && defined $meta->[2];
348 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
350 my $direction = $meta->[1];
354 if ( $direction eq 'To' ) {
355 $matchfield = "Target";
359 elsif ( $direction eq 'From' ) {
360 $linkfield = "Target";
361 $matchfield = "Base";
365 die "Invalid link direction '$meta->[1]' for $field\n";
368 my ($is_local, $is_null) = (1, 0);
369 if ( !$value || $value =~ /^null$/io ) {
371 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
373 elsif ( $value =~ /\D/o ) {
376 $matchfield = "Local$matchfield" if $is_local;
384 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
385 # SELECT main.* FROM Tickets main
386 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
387 # AND(main.id = Links_1.LocalTarget))
388 # WHERE Links_1.LocalBase IS NULL;
391 my $linkalias = $sb->Join(
396 FIELD2 => 'Local' . $linkfield
399 LEFTJOIN => $linkalias,
407 FIELD => $matchfield,
413 elsif ( $is_negative ) {
414 my $linkalias = $sb->Join(
419 FIELD2 => 'Local' . $linkfield
422 LEFTJOIN => $linkalias,
428 LEFTJOIN => $linkalias,
429 FIELD => $matchfield,
436 FIELD => $matchfield,
443 my $linkalias = $sb->NewAlias('Links');
454 FIELD => 'Local' . $linkfield,
458 ENTRYAGGREGATOR => 'AND',
462 FIELD => $matchfield,
465 ENTRYAGGREGATOR => 'AND',
473 Handle date fields. (Created, LastTold..)
476 1: type of link. (Probably not necessary.)
481 my ( $sb, $field, $op, $value, @rest ) = @_;
483 die "Invalid Date Op: $op"
484 unless $op =~ /^(=|>|<|>=|<=)$/;
486 my $meta = $FIELD_METADATA{$field};
487 die "Incorrect Meta Data for $field"
488 unless ( defined $meta->[1] );
490 my $date = RT::Date->new( $sb->CurrentUser );
491 $date->Set( Format => 'unknown', Value => $value );
495 # if we're specifying =, that means we want everything on a
496 # particular single day. in the database, we need to check for >
497 # and < the edges of that day.
499 $date->SetToMidnight( Timezone => 'server' );
500 my $daystart = $date->ISO;
502 my $dayend = $date->ISO;
518 ENTRYAGGREGATOR => 'AND',
536 Handle simple fields which are just strings. (Subject,Type)
544 my ( $sb, $field, $op, $value, @rest ) = @_;
548 # =, !=, LIKE, NOT LIKE
559 =head2 _TransDateLimit
561 Handle fields limiting based on Transaction Date.
563 The inpupt value must be in a format parseable by Time::ParseDate
570 # This routine should really be factored into translimit.
571 sub _TransDateLimit {
572 my ( $sb, $field, $op, $value, @rest ) = @_;
574 # See the comments for TransLimit, they apply here too
576 unless ( $sb->{_sql_transalias} ) {
577 $sb->{_sql_transalias} = $sb->Join(
580 TABLE2 => 'Transactions',
581 FIELD2 => 'ObjectId',
584 ALIAS => $sb->{_sql_transalias},
585 FIELD => 'ObjectType',
586 VALUE => 'RT::Ticket',
587 ENTRYAGGREGATOR => 'AND',
591 my $date = RT::Date->new( $sb->CurrentUser );
592 $date->Set( Format => 'unknown', Value => $value );
597 # if we're specifying =, that means we want everything on a
598 # particular single day. in the database, we need to check for >
599 # and < the edges of that day.
601 $date->SetToMidnight( Timezone => 'server' );
602 my $daystart = $date->ISO;
604 my $dayend = $date->ISO;
607 ALIAS => $sb->{_sql_transalias},
615 ALIAS => $sb->{_sql_transalias},
621 ENTRYAGGREGATOR => 'AND',
626 # not searching for a single day
629 #Search for the right field
631 ALIAS => $sb->{_sql_transalias},
645 Limit based on the Content of a transaction or the ContentType.
654 # Content, ContentType, Filename
656 # If only this was this simple. We've got to do something
659 #Basically, we want to make sure that the limits apply to
660 #the same attachment, rather than just another attachment
661 #for the same ticket, no matter how many clauses we lump
662 #on. We put them in TicketAliases so that they get nuked
663 #when we redo the join.
665 # In the SQL, we might have
666 # (( Content = foo ) or ( Content = bar AND Content = baz ))
667 # The AND group should share the same Alias.
669 # Actually, maybe it doesn't matter. We use the same alias and it
670 # works itself out? (er.. different.)
672 # Steal more from _ProcessRestrictions
674 # FIXME: Maybe look at the previous FooLimit call, and if it was a
675 # TransLimit and EntryAggregator == AND, reuse the Aliases?
677 # Or better - store the aliases on a per subclause basis - since
678 # those are going to be the things we want to relate to each other,
681 # maybe we should not allow certain kinds of aggregation of these
682 # clauses and do a psuedo regex instead? - the problem is getting
683 # them all into the same subclause when you have (A op B op C) - the
684 # way they get parsed in the tree they're in different subclauses.
686 my ( $self, $field, $op, $value, @rest ) = @_;
688 unless ( $self->{_sql_transalias} ) {
689 $self->{_sql_transalias} = $self->Join(
692 TABLE2 => 'Transactions',
693 FIELD2 => 'ObjectId',
696 ALIAS => $self->{_sql_transalias},
697 FIELD => 'ObjectType',
698 VALUE => 'RT::Ticket',
699 ENTRYAGGREGATOR => 'AND',
702 unless ( defined $self->{_sql_trattachalias} ) {
703 $self->{_sql_trattachalias} = $self->_SQLJoin(
704 TYPE => 'LEFT', # not all txns have an attachment
705 ALIAS1 => $self->{_sql_transalias},
707 TABLE2 => 'Attachments',
708 FIELD2 => 'TransactionId',
714 #Search for the right field
715 if ($field eq 'Content' and $RT::DontSearchFileAttachments) {
717 ALIAS => $self->{_sql_trattachalias},
721 SUBCLAUSE => 'contentquery',
722 ENTRYAGGREGATOR => 'AND',
725 ALIAS => $self->{_sql_trattachalias},
731 ENTRYAGGREGATOR => 'AND',
732 SUBCLAUSE => 'contentquery',
736 ALIAS => $self->{_sql_trattachalias},
741 ENTRYAGGREGATOR => 'AND',
752 Handle watcher limits. (Requestor, CC, etc..)
760 # Test to make sure that you can search for tickets by requestor address and
764 my $u1 = RT::User->new($RT::SystemUser);
765 ($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
767 my $u2 = RT::User->new($RT::SystemUser);
768 ($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
771 my $t1 = RT::Ticket->new($RT::SystemUser);
773 ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
776 my $t2 = RT::Ticket->new($RT::SystemUser);
777 ($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
781 my $t3 = RT::Ticket->new($RT::SystemUser);
782 ($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
786 my $tix1 = RT::Tickets->new($RT::SystemUser);
787 $tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
789 is ($tix1->Count, 3);
791 my $tix2 = RT::Tickets->new($RT::SystemUser);
792 $tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
794 is ($tix2->Count, 3);
797 my $tix3 = RT::Tickets->new($RT::SystemUser);
798 $tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
800 is ($tix3->Count, 2);
802 my $tix4 = RT::Tickets->new($RT::SystemUser);
803 $tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
805 is ($tix4->Count, 2);
807 # Searching for tickets that have two requestors isn't supported
808 # There's no way to differentiate "one requestor name that matches foo and bar"
809 # and "two requestors, one matching foo and one matching bar"
811 # my $tix5 = RT::Tickets->new($RT::SystemUser);
812 # $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
814 # is ($tix5->Count, 1);
816 # my $tix6 = RT::Tickets->new($RT::SystemUser);
817 # $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
819 # is ($tix6->Count, 1);
833 my $meta = $FIELD_METADATA{ $field };
834 my $type = $meta->[1] || '';
836 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
837 # search by id and Name at the same time, this is workaround
838 # to preserve backward compatibility
839 if ( $field eq 'Owner' && !$rest{SUBKEY} && $op =~ /^!?=$/ ) {
840 my $o = RT::User->new( $self->CurrentUser );
850 $rest{SUBKEY} ||= 'EmailAddress';
852 my $groups = $self->_RoleGroupsJoin( Type => $type );
855 if ( $op =~ /^IS(?: NOT)?$/ ) {
856 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
858 LEFTJOIN => $group_members,
861 VALUE => "$group_members.MemberId",
865 ALIAS => $group_members,
872 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
874 $op =~ s/!|NOT\s+//i;
876 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
877 # "X = 'Y'" matches more then one user so we try to fetch two records and
878 # do the right thing when there is only one exist and semi-working solution
880 my $users_obj = RT::Users->new( $self->CurrentUser );
882 FIELD => $rest{SUBKEY},
887 $users_obj->RowsPerPage(2);
888 my @users = @{ $users_obj->ItemsArrayRef };
890 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
893 $uid = $users[0]->id if @users;
895 LEFTJOIN => $group_members,
896 ALIAS => $group_members,
902 ALIAS => $group_members,
909 LEFTJOIN => $group_members,
912 VALUE => "$group_members.MemberId",
915 my $users = $self->Join(
917 ALIAS1 => $group_members,
918 FIELD1 => 'MemberId',
925 FIELD => $rest{SUBKEY},
939 my $group_members = $self->_GroupMembersJoin(
940 GroupsAlias => $groups,
944 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
946 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
947 $self->NewAlias('Users');
949 LEFTJOIN => $group_members,
950 ALIAS => $group_members,
952 VALUE => "$users.id",
959 FIELD => $rest{SUBKEY},
966 ENTRYAGGREGATOR => 'AND',
967 ALIAS => $group_members,
969 OPERATOR => 'IS NOT',
976 sub _RoleGroupsJoin {
978 my %args = (New => 0, Type => '', @_);
979 return $self->{'_sql_role_group_aliases'}{ $args{'Type'} }
980 if $self->{'_sql_role_group_aliases'}{ $args{'Type'} } && !$args{'New'};
982 # XXX: this has been fixed in DBIx::SB-1.48
983 # XXX: if we change this from Join to NewAlias+Limit
984 # then Pg and mysql 5.x will complain because SB build wrong query.
985 # Query looks like "FROM (Tickets LEFT JOIN CGM ON(Groups.id = CGM.GroupId)), Groups"
986 # Pg doesn't like that fact that it doesn't know about Groups table yet when
987 # join CGM table into Tickets. Problem is in Join method which doesn't use
988 # ALIAS1 argument when build braces.
990 # we always have watcher groups for ticket, so we use INNER join
991 my $groups = $self->Join(
995 FIELD2 => 'Instance',
996 ENTRYAGGREGATOR => 'AND',
1002 VALUE => 'RT::Ticket-Role',
1004 $self->SUPER::Limit(
1005 LEFTJOIN => $groups,
1008 VALUE => $args{'Type'},
1011 $self->{'_sql_role_group_aliases'}{ $args{'Type'} } = $groups
1012 unless $args{'New'};
1017 sub _GroupMembersJoin {
1019 my %args = (New => 1, GroupsAlias => undef, @_);
1021 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1022 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1025 my $alias = $self->Join(
1027 ALIAS1 => $args{'GroupsAlias'},
1029 TABLE2 => 'CachedGroupMembers',
1030 FIELD2 => 'GroupId',
1031 ENTRYAGGREGATOR => 'AND',
1034 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1035 unless $args{'New'};
1042 Helper function which provides joins to a watchers table both for limits
1049 my $type = shift || '';
1052 my $groups = $self->_RoleGroupsJoin( Type => $type );
1053 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1054 # XXX: work around, we must hide groups that
1055 # are members of the role group we search in,
1056 # otherwise them result in wrong NULLs in Users
1057 # table and break ordering. Now, we know that
1058 # RT doesn't allow to add groups as members of the
1059 # ticket roles, so we just hide entries in CGM table
1060 # with MemberId == GroupId from results
1061 $self->SUPER::Limit(
1062 LEFTJOIN => $group_members,
1065 VALUE => "$group_members.MemberId",
1068 my $users = $self->Join(
1070 ALIAS1 => $group_members,
1071 FIELD1 => 'MemberId',
1075 return ($groups, $group_members, $users);
1078 =head2 _WatcherMembershipLimit
1080 Handle watcher membership limits, i.e. whether the watcher belongs to a
1081 specific group or not.
1084 1: Field to query on
1086 SELECT DISTINCT main.*
1090 CachedGroupMembers CachedGroupMembers_2,
1093 (main.EffectiveId = main.id)
1095 (main.Status != 'deleted')
1097 (main.Type = 'ticket')
1100 (Users_3.EmailAddress = '22')
1102 (Groups_1.Domain = 'RT::Ticket-Role')
1104 (Groups_1.Type = 'RequestorGroup')
1107 Groups_1.Instance = main.id
1109 Groups_1.id = CachedGroupMembers_2.GroupId
1111 CachedGroupMembers_2.MemberId = Users_3.id
1112 ORDER BY main.id ASC
1117 sub _WatcherMembershipLimit {
1118 my ( $self, $field, $op, $value, @rest ) = @_;
1123 my $groups = $self->NewAlias('Groups');
1124 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1125 my $users = $self->NewAlias('Users');
1126 my $memberships = $self->NewAlias('CachedGroupMembers');
1128 if ( ref $field ) { # gross hack
1129 my @bundle = @$field;
1131 for my $chunk (@bundle) {
1132 ( $field, $op, $value, @rest ) = @$chunk;
1134 ALIAS => $memberships,
1145 ALIAS => $memberships,
1153 # {{{ Tie to groups for tickets we care about
1157 VALUE => 'RT::Ticket-Role',
1158 ENTRYAGGREGATOR => 'AND'
1163 FIELD1 => 'Instance',
1170 # If we care about which sort of watcher
1171 my $meta = $FIELD_METADATA{$field};
1172 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1179 ENTRYAGGREGATOR => 'AND'
1186 ALIAS2 => $groupmembers,
1191 ALIAS1 => $groupmembers,
1192 FIELD1 => 'MemberId',
1198 ALIAS1 => $memberships,
1199 FIELD1 => 'MemberId',
1208 sub _LinkFieldLimit {
1213 if ( $restriction->{'TYPE'} ) {
1214 $self->SUPER::Limit(
1215 ALIAS => $LinkAlias,
1216 ENTRYAGGREGATOR => 'AND',
1219 VALUE => $restriction->{'TYPE'}
1223 #If we're trying to limit it to things that are target of
1224 if ( $restriction->{'TARGET'} ) {
1226 # If the TARGET is an integer that means that we want to look at
1227 # the LocalTarget field. otherwise, we want to look at the
1230 if ( $restriction->{'TARGET'} =~ /^(\d+)$/ ) {
1231 $matchfield = "LocalTarget";
1234 $matchfield = "Target";
1236 $self->SUPER::Limit(
1237 ALIAS => $LinkAlias,
1238 ENTRYAGGREGATOR => 'AND',
1239 FIELD => $matchfield,
1241 VALUE => $restriction->{'TARGET'}
1244 #If we're searching on target, join the base to ticket.id
1247 FIELD1 => $self->{'primary_key'},
1248 ALIAS2 => $LinkAlias,
1249 FIELD2 => 'LocalBase'
1253 #If we're trying to limit it to things that are base of
1254 elsif ( $restriction->{'BASE'} ) {
1256 # If we're trying to match a numeric link, we want to look at
1257 # LocalBase, otherwise we want to look at "Base"
1259 if ( $restriction->{'BASE'} =~ /^(\d+)$/ ) {
1260 $matchfield = "LocalBase";
1263 $matchfield = "Base";
1266 $self->SUPER::Limit(
1267 ALIAS => $LinkAlias,
1268 ENTRYAGGREGATOR => 'AND',
1269 FIELD => $matchfield,
1271 VALUE => $restriction->{'BASE'}
1274 #If we're searching on base, join the target to ticket.id
1277 FIELD1 => $self->{'primary_key'},
1278 ALIAS2 => $LinkAlias,
1279 FIELD2 => 'LocalTarget'
1285 =head2 _CustomFieldDecipher
1287 Try and turn a CF descriptor into (cfid, cfname) object pair.
1291 sub _CustomFieldDecipher {
1292 my ($self, $field) = @_;
1295 if ( $field =~ /^(.+?)\.{(.+)}$/ ) {
1296 ($queue, $field) = ($1, $2);
1298 $field = $1 if $field =~ /^{(.+)}$/; # trim { }
1302 my $q = RT::Queue->new( $self->CurrentUser );
1303 $q->Load( $queue ) if $queue;
1307 # $queue = $q->Name; # should we normalize the queue?
1308 $cf = $q->CustomField( $field );
1311 $cf = RT::CustomField->new( $self->CurrentUser );
1312 $cf->LoadByNameAndQueue( Queue => 0, Name => $field );
1314 $cfid = $cf->id if $cf;
1317 return ($queue, $field, $cfid);
1321 =head2 _CustomFieldJoin
1323 Factor out the Join of custom fields so we can use it for sorting too
1327 sub _CustomFieldJoin {
1328 my ($self, $cfkey, $cfid, $field) = @_;
1329 # Perform one Join per CustomField
1330 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1331 $self->{_sql_cf_alias}{$cfkey} )
1333 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1334 $self->{_sql_cf_alias}{$cfkey} );
1337 my ($TicketCFs, $CFs);
1339 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1343 TABLE2 => 'ObjectCustomFieldValues',
1344 FIELD2 => 'ObjectId',
1346 $self->SUPER::Limit(
1347 LEFTJOIN => $TicketCFs,
1348 FIELD => 'CustomField',
1350 ENTRYAGGREGATOR => 'AND'
1354 my $ocfalias = $self->Join(
1357 TABLE2 => 'ObjectCustomFields',
1358 FIELD2 => 'ObjectId',
1361 $self->SUPER::Limit(
1362 LEFTJOIN => $ocfalias,
1363 ENTRYAGGREGATOR => 'OR',
1364 FIELD => 'ObjectId',
1368 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1370 ALIAS1 => $ocfalias,
1371 FIELD1 => 'CustomField',
1372 TABLE2 => 'CustomFields',
1376 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1380 TABLE2 => 'ObjectCustomFieldValues',
1381 FIELD2 => 'CustomField',
1383 $self->SUPER::Limit(
1384 LEFTJOIN => $TicketCFs,
1385 FIELD => 'ObjectId',
1388 ENTRYAGGREGATOR => 'AND',
1391 $self->SUPER::Limit(
1392 LEFTJOIN => $TicketCFs,
1393 FIELD => 'ObjectType',
1394 VALUE => 'RT::Ticket',
1395 ENTRYAGGREGATOR => 'AND'
1397 $self->SUPER::Limit(
1398 LEFTJOIN => $TicketCFs,
1399 FIELD => 'Disabled',
1402 ENTRYAGGREGATOR => 'AND'
1405 return ($TicketCFs, $CFs);
1408 =head2 _CustomFieldLimit
1410 Limit based on CustomFields
1417 sub _CustomFieldLimit {
1418 my ( $self, $_field, $op, $value, @rest ) = @_;
1421 my $field = $rest{SUBKEY} || die "No field specified";
1423 # For our sanity, we can only limit on one queue at a time
1426 ($queue, $field, $cfid ) = $self->_CustomFieldDecipher( $field );
1428 # If we're trying to find custom fields that don't match something, we
1429 # want tickets where the custom field has no value at all. Note that
1430 # we explicitly don't include the "IS NULL" case, since we would
1431 # otherwise end up with a redundant clause.
1433 my $null_columns_ok;
1434 if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
1435 $null_columns_ok = 1;
1438 my $cfkey = $cfid ? $cfid : "$queue.$field";
1439 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1444 $self->SUPER::Limit(
1448 ENTRYAGGREGATOR => 'AND',
1452 $self->_OpenParen if $null_columns_ok;
1455 ALIAS => $TicketCFs,
1463 if ($null_columns_ok) {
1465 ALIAS => $TicketCFs,
1470 ENTRYAGGREGATOR => 'OR',
1479 # End Helper Functions
1481 # End of SQL Stuff -------------------------------------------------
1483 # {{{ Allow sorting on watchers
1485 =head2 OrderByCols ARRAY
1487 A modified version of the OrderBy method which automatically joins where
1488 C<ALIAS> is set to the name of a watcher type.
1499 foreach my $row (@args) {
1500 if ( $row->{ALIAS} || $row->{FIELD} !~ /\./ ) {
1504 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1505 my $meta = $self->FIELDS->{$field};
1506 if ( $meta->[0] eq 'WATCHERFIELD' ) {
1507 # cache alias as we want to use one alias per watcher type for sorting
1508 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1510 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1511 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1513 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1514 } elsif ( $meta->[0] eq 'CUSTOMFIELD' ) {
1515 my ($queue, $field, $cfid ) = $self->_CustomFieldDecipher( $subkey );
1516 my $cfkey = $cfid ? $cfid : "$queue.$field";
1517 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1519 # For those cases where we are doing a join against the
1520 # CF name, and don't have a CFid, use Unique to make sure
1521 # we don't show duplicate tickets. NOTE: I'm pretty sure
1522 # this will stay mixed in for the life of the
1523 # class/package, and not just for the life of the object.
1524 # Potential performance issue.
1525 require DBIx::SearchBuilder::Unique;
1526 DBIx::SearchBuilder::Unique->import;
1528 my $CFvs = $self->Join(
1530 ALIAS1 => $TicketCFs,
1531 FIELD1 => 'CustomField',
1532 TABLE2 => 'CustomFieldValues',
1533 FIELD2 => 'CustomField',
1535 $self->SUPER::Limit(
1539 VALUE => $TicketCFs . ".Content",
1540 ENTRYAGGREGATOR => 'AND'
1543 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1544 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1545 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1546 # PAW logic is "reversed"
1548 if (exists $row->{ORDER} ) {
1549 my $o = $row->{ORDER};
1550 delete $row->{ORDER};
1551 $order = "DESC" if $o =~ /asc/i;
1557 # Ticket.Owner 1 0 0
1558 my $ownerId = $self->CurrentUser->Id;
1559 push @res, { %$row, FIELD => "Owner=$ownerId", ORDER => $order } ;
1561 # Unowned Tickets 0 1 0
1562 my $nobodyId = $RT::Nobody->Id;
1563 push @res, { %$row, FIELD => "Owner=$nobodyId", ORDER => $order } ;
1565 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1571 return $self->SUPER::OrderByCols(@res);
1576 # {{{ Limit the result set based on content
1582 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1583 Generally best called from LimitFoo methods
1593 DESCRIPTION => undef,
1596 $args{'DESCRIPTION'} = $self->loc(
1597 "[_1] [_2] [_3]", $args{'FIELD'},
1598 $args{'OPERATOR'}, $args{'VALUE'}
1600 if ( !defined $args{'DESCRIPTION'} );
1602 my $index = $self->_NextIndex;
1604 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1606 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1608 $self->{'RecalcTicketLimits'} = 1;
1610 # If we're looking at the effective id, we don't want to append the other clause
1611 # which limits us to tickets where id = effective id
1612 if ( $args{'FIELD'} eq 'EffectiveId'
1613 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1615 $self->{'looking_at_effective_id'} = 1;
1618 if ( $args{'FIELD'} eq 'Type'
1619 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1621 $self->{'looking_at_type'} = 1;
1631 Returns a frozen string suitable for handing back to ThawLimits.
1635 sub _FreezeThawKeys {
1636 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1640 # {{{ sub FreezeLimits
1645 require MIME::Base64;
1646 MIME::Base64::base64_encode(
1647 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1654 Take a frozen Limits string generated by FreezeLimits and make this tickets
1655 object have that set of limits.
1659 # {{{ sub ThawLimits
1665 #if we don't have $in, get outta here.
1666 return undef unless ($in);
1668 $self->{'RecalcTicketLimits'} = 1;
1671 require MIME::Base64;
1673 #We don't need to die if the thaw fails.
1674 @{$self}{ $self->_FreezeThawKeys }
1675 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1677 $RT::Logger->error($@) if $@;
1683 # {{{ Limit by enum or foreign key
1685 # {{{ sub LimitQueue
1689 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1690 OPERATOR is one of = or !=. (It defaults to =).
1691 VALUE is a queue id or Name.
1704 #TODO VALUE should also take queue objects
1705 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1706 my $queue = new RT::Queue( $self->CurrentUser );
1707 $queue->Load( $args{'VALUE'} );
1708 $args{'VALUE'} = $queue->Id;
1711 # What if they pass in an Id? Check for isNum() and convert to
1714 #TODO check for a valid queue here
1718 VALUE => $args{'VALUE'},
1719 OPERATOR => $args{'OPERATOR'},
1720 DESCRIPTION => join(
1721 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1729 # {{{ sub LimitStatus
1733 Takes a paramhash with the fields OPERATOR and VALUE.
1734 OPERATOR is one of = or !=.
1737 RT adds Status != 'deleted' until object has
1738 allow_deleted_search internal property set.
1739 $tickets->{'allow_deleted_search'} = 1;
1740 $tickets->LimitStatus( VALUE => 'deleted' );
1752 VALUE => $args{'VALUE'},
1753 OPERATOR => $args{'OPERATOR'},
1754 DESCRIPTION => join( ' ',
1755 $self->loc('Status'), $args{'OPERATOR'},
1756 $self->loc( $args{'VALUE'} ) ),
1762 # {{{ sub IgnoreType
1766 If called, this search will not automatically limit the set of results found
1767 to tickets of type "Ticket". Tickets of other types, such as "project" and
1768 "approval" will be found.
1775 # Instead of faking a Limit that later gets ignored, fake up the
1776 # fact that we're already looking at type, so that the check in
1777 # Tickets_Overlay_SQL/FromSQL goes down the right branch
1779 # $self->LimitType(VALUE => '__any');
1780 $self->{looking_at_type} = 1;
1789 Takes a paramhash with the fields OPERATOR and VALUE.
1790 OPERATOR is one of = or !=, it defaults to "=".
1791 VALUE is a string to search for in the type of the ticket.
1806 VALUE => $args{'VALUE'},
1807 OPERATOR => $args{'OPERATOR'},
1808 DESCRIPTION => join( ' ',
1809 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
1817 # {{{ Limit by string field
1819 # {{{ sub LimitSubject
1823 Takes a paramhash with the fields OPERATOR and VALUE.
1824 OPERATOR is one of = or !=.
1825 VALUE is a string to search for in the subject of the ticket.
1834 VALUE => $args{'VALUE'},
1835 OPERATOR => $args{'OPERATOR'},
1836 DESCRIPTION => join( ' ',
1837 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1845 # {{{ Limit based on ticket numerical attributes
1846 # Things that can be > < = !=
1852 Takes a paramhash with the fields OPERATOR and VALUE.
1853 OPERATOR is one of =, >, < or !=.
1854 VALUE is a ticket Id to search for
1867 VALUE => $args{'VALUE'},
1868 OPERATOR => $args{'OPERATOR'},
1870 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1876 # {{{ sub LimitPriority
1878 =head2 LimitPriority
1880 Takes a paramhash with the fields OPERATOR and VALUE.
1881 OPERATOR is one of =, >, < or !=.
1882 VALUE is a value to match the ticket\'s priority against
1890 FIELD => 'Priority',
1891 VALUE => $args{'VALUE'},
1892 OPERATOR => $args{'OPERATOR'},
1893 DESCRIPTION => join( ' ',
1894 $self->loc('Priority'),
1895 $args{'OPERATOR'}, $args{'VALUE'}, ),
1901 # {{{ sub LimitInitialPriority
1903 =head2 LimitInitialPriority
1905 Takes a paramhash with the fields OPERATOR and VALUE.
1906 OPERATOR is one of =, >, < or !=.
1907 VALUE is a value to match the ticket\'s initial priority against
1912 sub LimitInitialPriority {
1916 FIELD => 'InitialPriority',
1917 VALUE => $args{'VALUE'},
1918 OPERATOR => $args{'OPERATOR'},
1919 DESCRIPTION => join( ' ',
1920 $self->loc('Initial Priority'), $args{'OPERATOR'},
1927 # {{{ sub LimitFinalPriority
1929 =head2 LimitFinalPriority
1931 Takes a paramhash with the fields OPERATOR and VALUE.
1932 OPERATOR is one of =, >, < or !=.
1933 VALUE is a value to match the ticket\'s final priority against
1937 sub LimitFinalPriority {
1941 FIELD => 'FinalPriority',
1942 VALUE => $args{'VALUE'},
1943 OPERATOR => $args{'OPERATOR'},
1944 DESCRIPTION => join( ' ',
1945 $self->loc('Final Priority'), $args{'OPERATOR'},
1952 # {{{ sub LimitTimeWorked
1954 =head2 LimitTimeWorked
1956 Takes a paramhash with the fields OPERATOR and VALUE.
1957 OPERATOR is one of =, >, < or !=.
1958 VALUE is a value to match the ticket's TimeWorked attribute
1962 sub LimitTimeWorked {
1966 FIELD => 'TimeWorked',
1967 VALUE => $args{'VALUE'},
1968 OPERATOR => $args{'OPERATOR'},
1969 DESCRIPTION => join( ' ',
1970 $self->loc('Time worked'),
1971 $args{'OPERATOR'}, $args{'VALUE'}, ),
1977 # {{{ sub LimitTimeLeft
1979 =head2 LimitTimeLeft
1981 Takes a paramhash with the fields OPERATOR and VALUE.
1982 OPERATOR is one of =, >, < or !=.
1983 VALUE is a value to match the ticket's TimeLeft attribute
1991 FIELD => 'TimeLeft',
1992 VALUE => $args{'VALUE'},
1993 OPERATOR => $args{'OPERATOR'},
1994 DESCRIPTION => join( ' ',
1995 $self->loc('Time left'),
1996 $args{'OPERATOR'}, $args{'VALUE'}, ),
2004 # {{{ Limiting based on attachment attributes
2006 # {{{ sub LimitContent
2010 Takes a paramhash with the fields OPERATOR and VALUE.
2011 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2012 VALUE is a string to search for in the body of the ticket
2021 VALUE => $args{'VALUE'},
2022 OPERATOR => $args{'OPERATOR'},
2023 DESCRIPTION => join( ' ',
2024 $self->loc('Ticket content'), $args{'OPERATOR'},
2031 # {{{ sub LimitFilename
2033 =head2 LimitFilename
2035 Takes a paramhash with the fields OPERATOR and VALUE.
2036 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2037 VALUE is a string to search for in the body of the ticket
2045 FIELD => 'Filename',
2046 VALUE => $args{'VALUE'},
2047 OPERATOR => $args{'OPERATOR'},
2048 DESCRIPTION => join( ' ',
2049 $self->loc('Attachment filename'), $args{'OPERATOR'},
2055 # {{{ sub LimitContentType
2057 =head2 LimitContentType
2059 Takes a paramhash with the fields OPERATOR and VALUE.
2060 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2061 VALUE is a content type to search ticket attachments for
2065 sub LimitContentType {
2069 FIELD => 'ContentType',
2070 VALUE => $args{'VALUE'},
2071 OPERATOR => $args{'OPERATOR'},
2072 DESCRIPTION => join( ' ',
2073 $self->loc('Ticket content type'), $args{'OPERATOR'},
2082 # {{{ Limiting based on people
2084 # {{{ sub LimitOwner
2088 Takes a paramhash with the fields OPERATOR and VALUE.
2089 OPERATOR is one of = or !=.
2101 my $owner = new RT::User( $self->CurrentUser );
2102 $owner->Load( $args{'VALUE'} );
2104 # FIXME: check for a valid $owner
2107 VALUE => $args{'VALUE'},
2108 OPERATOR => $args{'OPERATOR'},
2109 DESCRIPTION => join( ' ',
2110 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2117 # {{{ Limiting watchers
2119 # {{{ sub LimitWatcher
2123 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2124 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2125 VALUE is a value to match the ticket\'s watcher email addresses against
2126 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2130 my $t1 = RT::Ticket->new($RT::SystemUser);
2131 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
2146 #build us up a description
2147 my ( $watcher_type, $desc );
2148 if ( $args{'TYPE'} ) {
2149 $watcher_type = $args{'TYPE'};
2152 $watcher_type = "Watcher";
2156 FIELD => $watcher_type,
2157 VALUE => $args{'VALUE'},
2158 OPERATOR => $args{'OPERATOR'},
2159 TYPE => $args{'TYPE'},
2160 DESCRIPTION => join( ' ',
2161 $self->loc($watcher_type),
2162 $args{'OPERATOR'}, $args{'VALUE'}, ),
2166 sub LimitRequestor {
2169 $RT::Logger->error( "Tickets->LimitRequestor is deprecated at ("
2170 . join( ":", caller )
2172 $self->LimitWatcher( TYPE => 'Requestor', @_ );
2182 # {{{ Limiting based on links
2186 =head2 LimitLinkedTo
2188 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2189 TYPE limits the sort of link we want to search on
2191 TYPE = { RefersTo, MemberOf, DependsOn }
2193 TARGET is the id or URI of the TARGET of the link
2194 (TARGET used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as TARGET
2209 FIELD => 'LinkedTo',
2211 TARGET => ( $args{'TARGET'} || $args{'TICKET'} ),
2212 TYPE => $args{'TYPE'},
2213 DESCRIPTION => $self->loc(
2214 "Tickets [_1] by [_2]",
2215 $self->loc( $args{'TYPE'} ),
2216 ( $args{'TARGET'} || $args{'TICKET'} )
2218 OPERATOR => $args{'OPERATOR'},
2224 # {{{ LimitLinkedFrom
2226 =head2 LimitLinkedFrom
2228 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2229 TYPE limits the sort of link we want to search on
2232 BASE is the id or URI of the BASE of the link
2233 (BASE used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as BASE
2238 sub LimitLinkedFrom {
2248 # translate RT2 From/To naming to RT3 TicketSQL naming
2249 my %fromToMap = qw(DependsOn DependentOn
2251 RefersTo ReferredToBy);
2253 my $type = $args{'TYPE'};
2254 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2257 FIELD => 'LinkedTo',
2259 BASE => ( $args{'BASE'} || $args{'TICKET'} ),
2261 DESCRIPTION => $self->loc(
2262 "Tickets [_1] [_2]",
2263 $self->loc( $args{'TYPE'} ),
2264 ( $args{'BASE'} || $args{'TICKET'} )
2266 OPERATOR => $args{'OPERATOR'},
2275 my $ticket_id = shift;
2276 return $self->LimitLinkedTo(
2278 TARGET => $ticket_id,
2285 # {{{ LimitHasMember
2286 sub LimitHasMember {
2288 my $ticket_id = shift;
2289 return $self->LimitLinkedFrom(
2291 BASE => "$ticket_id",
2292 TYPE => 'HasMember',
2299 # {{{ LimitDependsOn
2301 sub LimitDependsOn {
2303 my $ticket_id = shift;
2304 return $self->LimitLinkedTo(
2306 TARGET => $ticket_id,
2307 TYPE => 'DependsOn',
2314 # {{{ LimitDependedOnBy
2316 sub LimitDependedOnBy {
2318 my $ticket_id = shift;
2319 return $self->LimitLinkedFrom(
2322 TYPE => 'DependentOn',
2333 my $ticket_id = shift;
2334 return $self->LimitLinkedTo(
2336 TARGET => $ticket_id,
2344 # {{{ LimitReferredToBy
2346 sub LimitReferredToBy {
2348 my $ticket_id = shift;
2349 return $self->LimitLinkedFrom(
2352 TYPE => 'ReferredToBy',
2360 # {{{ limit based on ticket date attribtes
2364 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2366 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2368 OPERATOR is one of > or <
2369 VALUE is a date and time in ISO format in GMT
2370 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2372 There are also helper functions of the form LimitFIELD that eliminate
2373 the need to pass in a FIELD argument.
2387 #Set the description if we didn't get handed it above
2388 unless ( $args{'DESCRIPTION'} ) {
2389 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2390 . $args{'OPERATOR'} . " "
2391 . $args{'VALUE'} . " GMT";
2394 $self->Limit(%args);
2402 $self->LimitDate( FIELD => 'Created', @_ );
2407 $self->LimitDate( FIELD => 'Due', @_ );
2413 $self->LimitDate( FIELD => 'Starts', @_ );
2419 $self->LimitDate( FIELD => 'Started', @_ );
2424 $self->LimitDate( FIELD => 'Resolved', @_ );
2429 $self->LimitDate( FIELD => 'Told', @_ );
2432 sub LimitLastUpdated {
2434 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2438 # {{{ sub LimitTransactionDate
2440 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2442 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2444 OPERATOR is one of > or <
2445 VALUE is a date and time in ISO format in GMT
2450 sub LimitTransactionDate {
2453 FIELD => 'TransactionDate',
2460 # <20021217042756.GK28744@pallas.fsck.com>
2461 # "Kill It" - Jesse.
2463 #Set the description if we didn't get handed it above
2464 unless ( $args{'DESCRIPTION'} ) {
2465 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2466 . $args{'OPERATOR'} . " "
2467 . $args{'VALUE'} . " GMT";
2470 $self->Limit(%args);
2478 # {{{ Limit based on custom fields
2479 # {{{ sub LimitCustomField
2481 =head2 LimitCustomField
2483 Takes a paramhash of key/value pairs with the following keys:
2487 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2489 =item OPERATOR - The usual Limit operators
2491 =item VALUE - The value to compare against
2497 sub LimitCustomField {
2501 CUSTOMFIELD => undef,
2503 DESCRIPTION => undef,
2504 FIELD => 'CustomFieldValue',
2509 my $CF = RT::CustomField->new( $self->CurrentUser );
2510 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2511 $CF->Load( $args{CUSTOMFIELD} );
2514 $CF->LoadByNameAndQueue(
2515 Name => $args{CUSTOMFIELD},
2516 Queue => $args{QUEUE}
2518 $args{CUSTOMFIELD} = $CF->Id;
2521 #If we are looking to compare with a null value.
2522 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2523 $args{'DESCRIPTION'}
2524 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2526 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2527 $args{'DESCRIPTION'}
2528 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2531 # if we're not looking to compare with a null value
2533 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2534 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2539 my $qo = new RT::Queue( $self->CurrentUser );
2540 $qo->Load( $CF->Queue );
2545 @rest = ( ENTRYAGGREGATOR => 'AND' )
2546 if ( $CF->Type eq 'SelectMultiple' );
2549 VALUE => $args{VALUE},
2553 ? $q . ".{" . $CF->Name . "}"
2556 OPERATOR => $args{OPERATOR},
2561 $self->{'RecalcTicketLimits'} = 1;
2567 # {{{ sub _NextIndex
2571 Keep track of the counter for the array of restrictions
2577 return ( $self->{'restriction_index'}++ );
2584 # {{{ Core bits to make this a DBIx::SearchBuilder object
2589 $self->{'table'} = "Tickets";
2590 $self->{'RecalcTicketLimits'} = 1;
2591 $self->{'looking_at_effective_id'} = 0;
2592 $self->{'looking_at_type'} = 0;
2593 $self->{'restriction_index'} = 1;
2594 $self->{'primary_key'} = "id";
2595 delete $self->{'items_array'};
2596 delete $self->{'item_map'};
2597 delete $self->{'columns_to_display'};
2598 $self->SUPER::_Init(@_);
2609 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2610 return ( $self->SUPER::Count() );
2618 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2619 return ( $self->SUPER::CountAll() );
2624 # {{{ sub ItemsArrayRef
2626 =head2 ItemsArrayRef
2628 Returns a reference to the set of all items found in this search
2636 unless ( $self->{'items_array'} ) {
2638 my $placeholder = $self->_ItemsCounter;
2639 $self->GotoFirstItem();
2640 while ( my $item = $self->Next ) {
2641 push( @{ $self->{'items_array'} }, $item );
2643 $self->GotoItem($placeholder);
2644 $self->{'items_array'}
2645 = $self->ItemsOrderBy( $self->{'items_array'} );
2647 return ( $self->{'items_array'} );
2656 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2658 my $Ticket = $self->SUPER::Next();
2659 if ( ( defined($Ticket) ) and ( ref($Ticket) ) ) {
2661 if ( $Ticket->__Value('Status') eq 'deleted'
2662 && !$self->{'allow_deleted_search'} )
2664 return ( $self->Next() );
2667 # Since Ticket could be granted with more rights instead
2668 # of being revoked, it's ok if queue rights allow
2669 # ShowTicket. It seems need another query, but we have
2670 # rights cache in Principal::HasRight.
2671 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
2672 || $Ticket->CurrentUserHasRight('ShowTicket') )
2677 if ( $Ticket->__Value('Status') eq 'deleted' ) {
2678 return ( $self->Next() );
2681 # Since Ticket could be granted with more rights instead
2682 # of being revoked, it's ok if queue rights allow
2683 # ShowTicket. It seems need another query, but we have
2684 # rights cache in Principal::HasRight.
2685 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
2686 || $Ticket->CurrentUserHasRight('ShowTicket') )
2691 #If the user doesn't have the right to show this ticket
2693 return ( $self->Next() );
2697 #if there never was any ticket
2708 # {{{ Deal with storing and restoring restrictions
2710 # {{{ sub LoadRestrictions
2712 =head2 LoadRestrictions
2714 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
2715 TODO It is not yet implemented
2721 # {{{ sub DescribeRestrictions
2723 =head2 DescribeRestrictions
2726 Returns a hash keyed by restriction id.
2727 Each element of the hash is currently a one element hash that contains DESCRIPTION which
2728 is a description of the purpose of that TicketRestriction
2732 sub DescribeRestrictions {
2735 my ( $row, %listing );
2737 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2738 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
2745 # {{{ sub RestrictionValues
2747 =head2 RestrictionValues FIELD
2749 Takes a restriction field and returns a list of values this field is restricted
2754 sub RestrictionValues {
2757 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
2758 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
2759 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
2761 keys %{ $self->{'TicketRestrictions'} };
2766 # {{{ sub ClearRestrictions
2768 =head2 ClearRestrictions
2770 Removes all restrictions irretrievably
2774 sub ClearRestrictions {
2776 delete $self->{'TicketRestrictions'};
2777 $self->{'looking_at_effective_id'} = 0;
2778 $self->{'looking_at_type'} = 0;
2779 $self->{'RecalcTicketLimits'} = 1;
2784 # {{{ sub DeleteRestriction
2786 =head2 DeleteRestriction
2788 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
2789 Removes that restriction from the session's limits.
2793 sub DeleteRestriction {
2796 delete $self->{'TicketRestrictions'}{$row};
2798 $self->{'RecalcTicketLimits'} = 1;
2800 #make the underlying easysearch object forget all its preconceptions
2805 # {{{ sub _RestrictionsToClauses
2807 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
2809 sub _RestrictionsToClauses {
2814 foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2815 my $restriction = $self->{'TicketRestrictions'}{$row};
2818 #print Dumper($restriction),"\n";
2820 # We need to reimplement the subclause aggregation that SearchBuilder does.
2821 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
2822 # Then SB AND's the different Subclauses together.
2824 # So, we want to group things into Subclauses, convert them to
2825 # SQL, and then join them with the appropriate DefaultEA.
2826 # Then join each subclause group with AND.
2828 my $field = $restriction->{'FIELD'};
2829 my $realfield = $field; # CustomFields fake up a fieldname, so
2830 # we need to figure that out
2833 # Rewrite LinkedTo meta field to the real field
2834 if ( $field =~ /LinkedTo/ ) {
2835 $realfield = $field = $restriction->{'TYPE'};
2839 # Handle subkey fields with a different real field
2840 if ( $field =~ /^(\w+)\./ ) {
2844 die "I don't know about $field yet"
2845 unless ( exists $FIELD_METADATA{$realfield}
2846 or $restriction->{CUSTOMFIELD} );
2848 my $type = $FIELD_METADATA{$realfield}->[0];
2849 my $op = $restriction->{'OPERATOR'};
2853 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
2856 # this performs the moral equivalent of defined or/dor/C<//>,
2857 # without the short circuiting.You need to use a 'defined or'
2858 # type thing instead of just checking for truth values, because
2859 # VALUE could be 0.(i.e. "false")
2861 # You could also use this, but I find it less aesthetic:
2862 # (although it does short circuit)
2863 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2864 # defined $restriction->{'TICKET'} ?
2865 # $restriction->{TICKET} :
2866 # defined $restriction->{'BASE'} ?
2867 # $restriction->{BASE} :
2868 # defined $restriction->{'TARGET'} ?
2869 # $restriction->{TARGET} )
2871 my $ea = $restriction->{ENTRYAGGREGATOR}
2872 || $DefaultEA{$type}
2875 die "Invalid operator $op for $field ($type)"
2876 unless exists $ea->{$op};
2880 # Each CustomField should be put into a different Clause so they
2881 # are ANDed together.
2882 if ( $restriction->{CUSTOMFIELD} ) {
2883 $realfield = $field;
2886 exists $clause{$realfield} or $clause{$realfield} = [];
2889 $field =~ s!(['"])!\\$1!g;
2890 $value =~ s!(['"])!\\$1!g;
2891 my $data = [ $ea, $type, $field, $op, $value ];
2893 # here is where we store extra data, say if it's a keyword or
2894 # something. (I.e. "TYPE SPECIFIC STUFF")
2896 #print Dumper($data);
2897 push @{ $clause{$realfield} }, $data;
2904 # {{{ sub _ProcessRestrictions
2906 =head2 _ProcessRestrictions PARAMHASH
2908 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
2909 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
2913 sub _ProcessRestrictions {
2916 #Blow away ticket aliases since we'll need to regenerate them for
2918 delete $self->{'TicketAliases'};
2919 delete $self->{'items_array'};
2920 delete $self->{'item_map'};
2921 delete $self->{'raw_rows'};
2922 delete $self->{'rows'};
2923 delete $self->{'count_all'};
2925 my $sql = $self->Query; # Violating the _SQL namespace
2926 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
2928 # "Restrictions to Clauses Branch\n";
2929 my $clauseRef = eval { $self->_RestrictionsToClauses; };
2931 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2935 $sql = $self->ClausesToSQL($clauseRef);
2936 $self->FromSQL($sql) if $sql;
2940 $self->{'RecalcTicketLimits'} = 0;
2944 =head2 _BuildItemMap
2946 # Build up a map of first/last/next/prev items, so that we can display search nav quickly
2953 my $items = $self->ItemsArrayRef;
2956 delete $self->{'item_map'};
2957 if ( $items->[0] ) {
2958 $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
2959 while ( my $item = shift @$items ) {
2960 my $id = $item->EffectiveId;
2961 $self->{'item_map'}->{$id}->{'defined'} = 1;
2962 $self->{'item_map'}->{$id}->{prev} = $prev;
2963 $self->{'item_map'}->{$id}->{next} = $items->[0]->EffectiveId
2967 $self->{'item_map'}->{'last'} = $prev;
2973 Returns an a map of all items found by this search. The map is of the form
2975 $ItemMap->{'first'} = first ticketid found
2976 $ItemMap->{'last'} = last ticketid found
2977 $ItemMap->{$id}->{prev} = the ticket id found before $id
2978 $ItemMap->{$id}->{next} = the ticket id found after $id
2984 $self->_BuildItemMap()
2985 unless ( $self->{'items_array'} and $self->{'item_map'} );
2986 return ( $self->{'item_map'} );
3001 =head2 PrepForSerialization
3003 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
3007 sub PrepForSerialization {
3009 delete $self->{'items'};
3010 $self->RedoSearch();
3015 RT::Tickets supports several flags which alter search behavior:
3018 allow_deleted_search (Otherwise never show deleted tickets in search results)
3019 looking_at_type (otherwise limit to type=ticket)
3021 These flags are set by calling
3023 $tickets->{'flagname'} = 1;
3025 BUG: There should be an API for this
3031 # We assume that we've got some tickets hanging around from before.
3032 ok( my $unlimittickets = RT::Tickets->new( $RT::SystemUser ) );
3033 ok( $unlimittickets->UnLimit );
3034 ok( $unlimittickets->Count > 0, "UnLimited tickets object should return tickets" );