3 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
5 # (Except where explictly superceded by other copyright notices)
7 # This work is made available to you under the terms of Version 2 of
8 # the GNU General Public License. A copy of that license should have
9 # been provided with this software, but in any event can be snarfed
12 # This work is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # Unless otherwise specified, all modifications, corrections or
18 # extensions to this work which alter its source code become the
19 # property of Best Practical Solutions, LLC when submitted for
20 # inclusion in the work.
26 # - Decimated ProcessRestrictions and broke it into multiple
27 # functions joined by a LUT
28 # - Semi-Generic SQL stuff moved to another file
30 # Known Issues: FIXME!
32 # - ClearRestrictions and Reinitialization is messy and unclear. The
33 # only good way to do it is to create a new RT::Tickets object.
37 RT::Tickets - A collection of Ticket objects
43 my $tickets = new RT::Tickets($CurrentUser);
47 A collection of RT::Tickets.
53 ok (require RT::Tickets);
59 no warnings qw(redefine);
60 use vars qw(@SORTFIELDS);
63 # Configuration Tables:
65 # FIELDS is a mapping of searchable Field name, to Type, and other
70 Queue => ['ENUM' => 'Queue',],
72 Creator => ['ENUM' => 'User',],
73 LastUpdatedBy => ['ENUM' => 'User',],
74 Owner => ['ENUM' => 'User',],
75 EffectiveId => ['INT',],
77 InitialPriority => ['INT',],
78 FinalPriority => ['INT',],
81 TimeWorked => ['INT',],
82 MemberOf => ['LINK' => To => 'MemberOf', ],
83 DependsOn => ['LINK' => To => 'DependsOn',],
84 RefersTo => ['LINK' => To => 'RefersTo',],
85 HasMember => ['LINK' => From => 'MemberOf',],
86 DependentOn => ['LINK' => From => 'DependsOn',],
87 ReferredToBy => ['LINK' => From => 'RefersTo',],
88 # HasDepender => ['LINK',],
89 # RelatedTo => ['LINK',],
90 Told => ['DATE' => 'Told',],
91 Starts => ['DATE' => 'Starts',],
92 Started => ['DATE' => 'Started',],
93 Due => ['DATE' => 'Due',],
94 Resolved => ['DATE' => 'Resolved',],
95 LastUpdated => ['DATE' => 'LastUpdated',],
96 Created => ['DATE' => 'Created',],
97 Subject => ['STRING',],
99 Content => ['TRANSFIELD',],
100 ContentType => ['TRANSFIELD',],
101 Filename => ['TRANSFIELD',],
102 TransactionDate => ['TRANSDATE',],
103 Requestor => ['WATCHERFIELD' => 'Requestor',],
104 CC => ['WATCHERFIELD' => 'Cc',],
105 AdminCC => ['WATCHERFIELD' => 'AdminCC',],
106 Watcher => ['WATCHERFIELD'],
107 LinkedTo => ['LINKFIELD',],
108 CustomFieldValue =>['CUSTOMFIELD',],
109 CF => ['CUSTOMFIELD',],
112 # Mapping of Field Type to Function
114 ( ENUM => \&_EnumLimit,
116 LINK => \&_LinkLimit,
117 DATE => \&_DateLimit,
118 STRING => \&_StringLimit,
119 TRANSFIELD => \&_TransLimit,
120 TRANSDATE => \&_TransDateLimit,
121 WATCHERFIELD => \&_WatcherLimit,
122 LINKFIELD => \&_LinkFieldLimit,
123 CUSTOMFIELD => \&_CustomFieldLimit,
126 ( WATCHERFIELD => "yeps",
129 # Default EntryAggregator per type
130 # if you specify OP, you must specify all valid OPs
133 ENUM => { '=' => 'OR',
136 DATE => { '=' => 'OR',
142 STRING => { '=' => 'OR',
153 WATCHERFIELD => { '=' => 'OR',
163 # Helper functions for passing the above lexically scoped tables above
164 # into Tickets_Overlay_SQL.
165 sub FIELDS { return \%FIELDS }
166 sub dispatch { return \%dispatch }
167 sub can_bundle { return \%can_bundle }
169 # Bring in the clowns.
170 require RT::Tickets_Overlay_SQL;
174 @SORTFIELDS = qw(id Status
176 Owner Created Due Starts Started
178 Resolved LastUpdated Priority TimeWorked TimeLeft);
182 Returns the list of fields that lists of tickets can easily be sorted by
195 # BEGIN SQL STUFF *********************************
197 =head1 Limit Helper Routines
199 These routines are the targets of a dispatch table depending on the
200 type of field. They all share the same signature:
202 my ($self,$field,$op,$value,@rest) = @_;
204 The values in @rest should be suitable for passing directly to
205 DBIx::SearchBuilder::Limit.
207 Essentially they are an expanded/broken out (and much simplified)
208 version of what ProcessRestrictions used to do. They're also much
209 more clearly delineated by the TYPE of field being processed.
213 Handle Fields which are limited to certain values, and potentially
214 need to be looked up from another class.
216 This subroutine actually handles two different kinds of fields. For
217 some the user is responsible for limiting the values. (i.e. Status,
220 For others, the value specified by the user will be looked by via
224 name of class to lookup in (Optional)
229 my ($sb,$field,$op,$value,@rest) = @_;
231 # SQL::Statement changes != to <>. (Can we remove this now?)
232 $op = "!=" if $op eq "<>";
234 die "Invalid Operation: $op for $field"
235 unless $op eq "=" or $op eq "!=";
237 my $meta = $FIELDS{$field};
238 if (defined $meta->[1]) {
239 my $class = "RT::" . $meta->[1];
240 my $o = $class->new($sb->CurrentUser);
244 $sb->_SQLLimit( FIELD => $field,,
253 Handle fields where the values are limited to integers. (For example,
254 Priority, TimeWorked.)
262 my ($sb,$field,$op,$value,@rest) = @_;
264 die "Invalid Operator $op for $field"
265 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
278 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
281 1: Direction (From,To)
282 2: Relationship Type (MemberOf, DependsOn,RefersTo)
287 my ($sb,$field,$op,$value,@rest) = @_;
292 my $meta = $FIELDS{$field};
293 die "Incorrect Meta Data for $field"
294 unless (defined $meta->[1] and defined $meta->[2]);
296 my $LinkAlias = $sb->NewAlias ('Links');
308 if ($meta->[1] eq "To") {
309 my $matchfield = ( $value =~ /^(\d+)$/ ? "LocalTarget" : "Target" );
313 ENTRYAGGREGATOR => 'AND',
314 FIELD => $matchfield,
319 #If we're searching on target, join the base to ticket.id
320 $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
321 ALIAS2 => $LinkAlias, FIELD2 => 'LocalBase');
323 } elsif ( $meta->[1] eq "From" ) {
324 my $matchfield = ( $value =~ /^(\d+)$/ ? "LocalBase" : "Base" );
328 ENTRYAGGREGATOR => 'AND',
329 FIELD => $matchfield,
334 #If we're searching on base, join the target to ticket.id
335 $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
336 ALIAS2 => $LinkAlias, FIELD2 => 'LocalTarget');
339 die "Invalid link direction '$meta->[1]' for $field\n";
348 Handle date fields. (Created, LastTold..)
351 1: type of relationship. (Probably not necessary.)
356 my ($sb,$field,$op,$value,@rest) = @_;
358 die "Invalid Date Op: $op"
359 unless $op =~ /^(=|>|<|>=|<=)$/;
361 my $meta = $FIELDS{$field};
362 die "Incorrect Meta Data for $field"
363 unless (defined $meta->[1]);
365 require Time::ParseDate;
366 use POSIX 'strftime';
368 # FIXME: Replace me with RT::Date( Type => 'unknown' ...)
369 my $time = Time::ParseDate::parsedate( $value,
370 UK => $RT::DateDayBeforeMonth,
371 PREFER_PAST => $RT::AmbiguousDayInPast,
372 PREFER_FUTURE => !($RT::AmbiguousDayInPast),
377 # if we're specifying =, that means we want everything on a
378 # particular single day. in the database, we need to check for >
379 # and < the edges of that day.
381 my $daystart = strftime("%Y-%m-%d %H:%M",
382 gmtime($time - ( $time % 86400 )));
383 my $dayend = strftime("%Y-%m-%d %H:%M",
384 gmtime($time + ( 86399 - $time % 86400 )));
400 ENTRYAGGREGATOR => 'AND',
406 $value = strftime("%Y-%m-%d %H:%M", gmtime($time));
418 Handle simple fields which are just strings. (Subject,Type)
426 my ($sb,$field,$op,$value,@rest) = @_;
430 # =, !=, LIKE, NOT LIKE
441 =head2 _TransDateLimit
443 Handle fields limiting based on Transaction Date.
445 The inpupt value must be in a format parseable by Time::ParseDate
452 sub _TransDateLimit {
453 my ($sb,$field,$op,$value,@rest) = @_;
455 # See the comments for TransLimit, they apply here too
457 $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
458 unless defined $sb->{_sql_transalias};
459 $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
460 unless defined $sb->{_sql_trattachalias};
464 # Join Transactions To Attachments
465 $sb->Join( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
466 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
468 # Join Transactions to Tickets
469 $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
470 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
472 my $d = new RT::Date( $sb->CurrentUser );
473 $d->Set( Format => 'ISO', Value => $value);
476 #Search for the right field
477 $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
490 Limit based on the Content of a transaction or the ContentType.
498 # Content, ContentType, Filename
500 # If only this was this simple. We've got to do something
503 #Basically, we want to make sure that the limits apply to
504 #the same attachment, rather than just another attachment
505 #for the same ticket, no matter how many clauses we lump
506 #on. We put them in TicketAliases so that they get nuked
507 #when we redo the join.
509 # In the SQL, we might have
510 # (( Content = foo ) or ( Content = bar AND Content = baz ))
511 # The AND group should share the same Alias.
513 # Actually, maybe it doesn't matter. We use the same alias and it
514 # works itself out? (er.. different.)
516 # Steal more from _ProcessRestrictions
518 # FIXME: Maybe look at the previous FooLimit call, and if it was a
519 # TransLimit and EntryAggregator == AND, reuse the Aliases?
521 # Or better - store the aliases on a per subclause basis - since
522 # those are going to be the things we want to relate to each other,
525 # maybe we should not allow certain kinds of aggregation of these
526 # clauses and do a psuedo regex instead? - the problem is getting
527 # them all into the same subclause when you have (A op B op C) - the
528 # way they get parsed in the tree they're in different subclauses.
530 my ($sb,$field,$op,$value,@rest) = @_;
532 $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
533 unless defined $sb->{_sql_transalias};
534 $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
535 unless defined $sb->{_sql_trattachalias};
539 # Join Transactions To Attachments
540 $sb->Join( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
541 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
543 # Join Transactions to Tickets
544 $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
545 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
547 #Search for the right field
548 $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
562 Handle watcher limits. (Requestor, CC, etc..)
570 my ($self,$field,$op,$value,@rest) = @_;
575 my $groups = $self->NewAlias('Groups');
576 my $groupmembers = $self->NewAlias('CachedGroupMembers');
577 my $users = $self->NewAlias('Users');
581 # my $subclause = undef;
582 # my $aggregator = 'OR';
583 # if ($restriction->{'OPERATOR'} =~ /!|NOT/i ){
584 # $subclause = 'AndEmailIsNot';
585 # $aggregator = 'AND';
588 if (ref $field) { # gross hack
589 my @bundle = @$field;
591 for my $chunk (@bundle) {
592 ($field,$op,$value,@rest) = @$chunk;
593 $self->_SQLLimit(ALIAS => $users,
594 FIELD => $rest{SUBKEY} || 'EmailAddress',
603 $self->_SQLLimit(ALIAS => $users,
604 FIELD => $rest{SUBKEY} || 'EmailAddress',
612 # {{{ Tie to groups for tickets we care about
613 $self->_SQLLimit(ALIAS => $groups,
615 VALUE => 'RT::Ticket-Role',
616 ENTRYAGGREGATOR => 'AND');
618 $self->Join(ALIAS1 => $groups, FIELD1 => 'Instance',
619 ALIAS2 => 'main', FIELD2 => 'id');
622 # If we care about which sort of watcher
623 my $meta = $FIELDS{$field};
624 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
627 $self->_SQLLimit(ALIAS => $groups,
630 ENTRYAGGREGATOR => 'AND');
633 $self->Join (ALIAS1 => $groups, FIELD1 => 'id',
634 ALIAS2 => $groupmembers, FIELD2 => 'GroupId');
636 $self->Join( ALIAS1 => $groupmembers, FIELD1 => 'MemberId',
637 ALIAS2 => $users, FIELD2 => 'id');
643 sub _LinkFieldLimit {
648 if ($restriction->{'TYPE'}) {
649 $self->SUPER::Limit(ALIAS => $LinkAlias,
650 ENTRYAGGREGATOR => 'AND',
653 VALUE => $restriction->{'TYPE'} );
656 #If we're trying to limit it to things that are target of
657 if ($restriction->{'TARGET'}) {
658 # If the TARGET is an integer that means that we want to look at
659 # the LocalTarget field. otherwise, we want to look at the
662 if ($restriction->{'TARGET'} =~/^(\d+)$/) {
663 $matchfield = "LocalTarget";
665 $matchfield = "Target";
667 $self->SUPER::Limit(ALIAS => $LinkAlias,
668 ENTRYAGGREGATOR => 'AND',
669 FIELD => $matchfield,
671 VALUE => $restriction->{'TARGET'} );
672 #If we're searching on target, join the base to ticket.id
673 $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
674 ALIAS2 => $LinkAlias,
675 FIELD2 => 'LocalBase');
677 #If we're trying to limit it to things that are base of
678 elsif ($restriction->{'BASE'}) {
679 # If we're trying to match a numeric link, we want to look at
680 # LocalBase, otherwise we want to look at "Base"
682 if ($restriction->{'BASE'} =~/^(\d+)$/) {
683 $matchfield = "LocalBase";
685 $matchfield = "Base";
688 $self->SUPER::Limit(ALIAS => $LinkAlias,
689 ENTRYAGGREGATOR => 'AND',
690 FIELD => $matchfield,
692 VALUE => $restriction->{'BASE'} );
693 #If we're searching on base, join the target to ticket.id
694 $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
695 ALIAS2 => $LinkAlias,
696 FIELD2 => 'LocalTarget')
703 Limit based on Keywords
710 sub _CustomFieldLimit {
711 my ($self,$_field,$op,$value,@rest) = @_;
714 my $field = $rest{SUBKEY} || die "No field specified";
716 # For our sanity, we can only limit on one queue at a time
718 # Ugh. This will not do well for things with underscores in them
720 use RT::CustomFields;
721 my $CF = RT::CustomFields->new( $self->CurrentUser );
722 #$CF->Load( $cfid} );
725 if ($field =~ /^(.+?)\.{(.+)}$/) {
726 my $q = RT::Queue->new($self->CurrentUser);
729 $CF->LimitToQueue( $q->Id );
732 $field = $1 if $field =~ /^{(.+)}$/; # trim { }
739 while ( my $CustomField = $CF->Next ) {
740 if ($CustomField->Name eq $field) {
741 $cfid = $CustomField->Id;
745 die "No custom field named $field found\n"
748 # use RT::CustomFields;
749 # my $CF = RT::CustomField->new( $self->CurrentUser );
750 # $CF->Load( $cfid );
756 # Perform one Join per CustomField
757 if ($self->{_sql_keywordalias}{$cfid}) {
758 $TicketCFs = $self->{_sql_keywordalias}{$cfid};
760 $TicketCFs = $self->{_sql_keywordalias}{$cfid} =
761 $self->Join( TYPE => 'left',
764 TABLE2 => 'TicketCustomFieldValues',
765 FIELD2 => 'Ticket' );
770 $self->_SQLLimit( ALIAS => $TicketCFs,
778 or ( $op eq '!=' ) ) {
779 $null_columns_ok = 1;
782 #If we're trying to find tickets where the keyword isn't somethng,
783 #also check ones where it _IS_ null
786 $self->_SQLLimit( ALIAS => $TicketCFs,
791 ENTRYAGGREGATOR => 'OR', );
794 $self->_SQLLimit( LEFTJOIN => $TicketCFs,
795 FIELD => 'CustomField',
797 ENTRYAGGREGATOR => 'OR' );
806 # End Helper Functions
808 # End of SQL Stuff -------------------------------------------------
810 # {{{ Limit the result set based on content
816 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
817 Generally best called from LimitFoo methods
822 my %args = ( FIELD => undef,
825 DESCRIPTION => undef,
828 $args{'DESCRIPTION'} = $self->loc(
829 "[_1] [_2] [_3]", $args{'FIELD'}, $args{'OPERATOR'}, $args{'VALUE'}
830 ) if (!defined $args{'DESCRIPTION'}) ;
832 my $index = $self->_NextIndex;
834 #make the TicketRestrictions hash the equivalent of whatever we just passed in;
836 %{$self->{'TicketRestrictions'}{$index}} = %args;
838 $self->{'RecalcTicketLimits'} = 1;
840 # If we're looking at the effective id, we don't want to append the other clause
841 # which limits us to tickets where id = effective id
842 if ($args{'FIELD'} eq 'EffectiveId') {
843 $self->{'looking_at_effective_id'} = 1;
846 if ($args{'FIELD'} eq 'Type') {
847 $self->{'looking_at_type'} = 1;
860 Returns a frozen string suitable for handing back to ThawLimits.
863 # {{{ sub FreezeLimits
868 return (FreezeThaw::freeze($self->{'TicketRestrictions'},
869 $self->{'restriction_index'}
877 Take a frozen Limits string generated by FreezeLimits and make this tickets
878 object have that set of limits.
887 #if we don't have $in, get outta here.
888 return undef unless ($in);
890 $self->{'RecalcTicketLimits'} = 1;
894 #We don't need to die if the thaw fails.
897 ($self->{'TicketRestrictions'},
898 $self->{'restriction_index'}
899 ) = FreezeThaw::thaw($in);
906 # {{{ Limit by enum or foreign key
912 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
913 OPERATOR is one of = or !=. (It defaults to =).
914 VALUE is a queue id or Name.
921 my %args = (VALUE => undef,
925 #TODO VALUE should also take queue names and queue objects
926 #TODO FIXME why are we canonicalizing to name, not id, robrt?
927 if ($args{VALUE} =~ /^\d+$/) {
928 my $queue = new RT::Queue($self->CurrentUser);
929 $queue->Load($args{'VALUE'});
930 $args{VALUE} = $queue->Name;
933 # What if they pass in an Id? Check for isNum() and convert to
936 #TODO check for a valid queue here
938 $self->Limit (FIELD => 'Queue',
939 VALUE => $args{VALUE},
940 OPERATOR => $args{'OPERATOR'},
942 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE},
949 # {{{ sub LimitStatus
953 Takes a paramhash with the fields OPERATOR and VALUE.
954 OPERATOR is one of = or !=.
961 my %args = ( OPERATOR => '=',
963 $self->Limit (FIELD => 'Status',
964 VALUE => $args{'VALUE'},
965 OPERATOR => $args{'OPERATOR'},
967 ' ', $self->loc('Status'), $args{'OPERATOR'}, $self->loc($args{'VALUE'})
978 If called, this search will not automatically limit the set of results found
979 to tickets of type "Ticket". Tickets of other types, such as "project" and
980 "approval" will be found.
987 # Instead of faking a Limit that later gets ignored, fake up the
988 # fact that we're already looking at type, so that the check in
989 # Tickets_Overlay_SQL/FromSQL goes down the right branch
991 # $self->LimitType(VALUE => '__any');
992 $self->{looking_at_type} = 1;
1001 Takes a paramhash with the fields OPERATOR and VALUE.
1002 OPERATOR is one of = or !=, it defaults to "=".
1003 VALUE is a string to search for in the type of the ticket.
1011 my %args = (OPERATOR => '=',
1014 $self->Limit (FIELD => 'Type',
1015 VALUE => $args{'VALUE'},
1016 OPERATOR => $args{'OPERATOR'},
1017 DESCRIPTION => join(
1018 ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'},
1027 # {{{ Limit by string field
1029 # {{{ sub LimitSubject
1033 Takes a paramhash with the fields OPERATOR and VALUE.
1034 OPERATOR is one of = or !=.
1035 VALUE is a string to search for in the subject of the ticket.
1042 $self->Limit (FIELD => 'Subject',
1043 VALUE => $args{'VALUE'},
1044 OPERATOR => $args{'OPERATOR'},
1045 DESCRIPTION => join(
1046 ' ', $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'},
1055 # {{{ Limit based on ticket numerical attributes
1056 # Things that can be > < = !=
1062 Takes a paramhash with the fields OPERATOR and VALUE.
1063 OPERATOR is one of =, >, < or !=.
1064 VALUE is a ticket Id to search for
1070 my %args = (OPERATOR => '=',
1073 $self->Limit (FIELD => 'id',
1074 VALUE => $args{'VALUE'},
1075 OPERATOR => $args{'OPERATOR'},
1076 DESCRIPTION => join(
1077 ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'},
1084 # {{{ sub LimitPriority
1086 =head2 LimitPriority
1088 Takes a paramhash with the fields OPERATOR and VALUE.
1089 OPERATOR is one of =, >, < or !=.
1090 VALUE is a value to match the ticket\'s priority against
1097 $self->Limit (FIELD => 'Priority',
1098 VALUE => $args{'VALUE'},
1099 OPERATOR => $args{'OPERATOR'},
1100 DESCRIPTION => join(
1101 ' ', $self->loc('Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1108 # {{{ sub LimitInitialPriority
1110 =head2 LimitInitialPriority
1112 Takes a paramhash with the fields OPERATOR and VALUE.
1113 OPERATOR is one of =, >, < or !=.
1114 VALUE is a value to match the ticket\'s initial priority against
1119 sub LimitInitialPriority {
1122 $self->Limit (FIELD => 'InitialPriority',
1123 VALUE => $args{'VALUE'},
1124 OPERATOR => $args{'OPERATOR'},
1125 DESCRIPTION => join(
1126 ' ', $self->loc('Initial Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1133 # {{{ sub LimitFinalPriority
1135 =head2 LimitFinalPriority
1137 Takes a paramhash with the fields OPERATOR and VALUE.
1138 OPERATOR is one of =, >, < or !=.
1139 VALUE is a value to match the ticket\'s final priority against
1143 sub LimitFinalPriority {
1146 $self->Limit (FIELD => 'FinalPriority',
1147 VALUE => $args{'VALUE'},
1148 OPERATOR => $args{'OPERATOR'},
1149 DESCRIPTION => join(
1150 ' ', $self->loc('Final Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1157 # {{{ sub LimitTimeWorked
1159 =head2 LimitTimeWorked
1161 Takes a paramhash with the fields OPERATOR and VALUE.
1162 OPERATOR is one of =, >, < or !=.
1163 VALUE is a value to match the ticket's TimeWorked attribute
1167 sub LimitTimeWorked {
1170 $self->Limit (FIELD => 'TimeWorked',
1171 VALUE => $args{'VALUE'},
1172 OPERATOR => $args{'OPERATOR'},
1173 DESCRIPTION => join(
1174 ' ', $self->loc('Time worked'), $args{'OPERATOR'}, $args{'VALUE'},
1181 # {{{ sub LimitTimeLeft
1183 =head2 LimitTimeLeft
1185 Takes a paramhash with the fields OPERATOR and VALUE.
1186 OPERATOR is one of =, >, < or !=.
1187 VALUE is a value to match the ticket's TimeLeft attribute
1194 $self->Limit (FIELD => 'TimeLeft',
1195 VALUE => $args{'VALUE'},
1196 OPERATOR => $args{'OPERATOR'},
1197 DESCRIPTION => join(
1198 ' ', $self->loc('Time left'), $args{'OPERATOR'}, $args{'VALUE'},
1207 # {{{ Limiting based on attachment attributes
1209 # {{{ sub LimitContent
1213 Takes a paramhash with the fields OPERATOR and VALUE.
1214 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1215 VALUE is a string to search for in the body of the ticket
1221 $self->Limit (FIELD => 'Content',
1222 VALUE => $args{'VALUE'},
1223 OPERATOR => $args{'OPERATOR'},
1224 DESCRIPTION => join(
1225 ' ', $self->loc('Ticket content'), $args{'OPERATOR'}, $args{'VALUE'},
1232 # {{{ sub LimitFilename
1234 =head2 LimitFilename
1236 Takes a paramhash with the fields OPERATOR and VALUE.
1237 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1238 VALUE is a string to search for in the body of the ticket
1244 $self->Limit (FIELD => 'Filename',
1245 VALUE => $args{'VALUE'},
1246 OPERATOR => $args{'OPERATOR'},
1247 DESCRIPTION => join(
1248 ' ', $self->loc('Attachment filename'), $args{'OPERATOR'}, $args{'VALUE'},
1254 # {{{ sub LimitContentType
1256 =head2 LimitContentType
1258 Takes a paramhash with the fields OPERATOR and VALUE.
1259 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1260 VALUE is a content type to search ticket attachments for
1264 sub LimitContentType {
1267 $self->Limit (FIELD => 'ContentType',
1268 VALUE => $args{'VALUE'},
1269 OPERATOR => $args{'OPERATOR'},
1270 DESCRIPTION => join(
1271 ' ', $self->loc('Ticket content type'), $args{'OPERATOR'}, $args{'VALUE'},
1279 # {{{ Limiting based on people
1281 # {{{ sub LimitOwner
1285 Takes a paramhash with the fields OPERATOR and VALUE.
1286 OPERATOR is one of = or !=.
1293 my %args = ( OPERATOR => '=',
1296 my $owner = new RT::User($self->CurrentUser);
1297 $owner->Load($args{'VALUE'});
1298 # FIXME: check for a valid $owner
1299 $self->Limit (FIELD => 'Owner',
1300 VALUE => $args{'VALUE'},
1301 OPERATOR => $args{'OPERATOR'},
1302 DESCRIPTION => join(
1303 ' ', $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(),
1311 # {{{ Limiting watchers
1313 # {{{ sub LimitWatcher
1318 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1319 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1320 VALUE is a value to match the ticket\'s watcher email addresses against
1321 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1325 my $t1 = RT::Ticket->new($RT::SystemUser);
1326 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
1334 my %args = ( OPERATOR => '=',
1340 #build us up a description
1341 my ($watcher_type, $desc);
1342 if ($args{'TYPE'}) {
1343 $watcher_type = $args{'TYPE'};
1346 $watcher_type = "Watcher";
1349 $self->Limit (FIELD => $watcher_type,
1350 VALUE => $args{'VALUE'},
1351 OPERATOR => $args{'OPERATOR'},
1352 TYPE => $args{'TYPE'},
1353 DESCRIPTION => join(
1354 ' ', $self->loc($watcher_type), $args{'OPERATOR'}, $args{'VALUE'},
1360 sub LimitRequestor {
1363 my ($package, $filename, $line) = caller;
1364 $RT::Logger->error("Tickets->LimitRequestor is deprecated. please rewrite call at $package - $filename: $line");
1365 $self->LimitWatcher(TYPE => 'Requestor', @_);
1376 # {{{ Limiting based on links
1380 =head2 LimitLinkedTo
1382 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1383 TYPE limits the sort of relationship we want to search on
1385 TYPE = { RefersTo, MemberOf, DependsOn }
1387 TARGET is the id or URI of the TARGET of the link
1388 (TARGET used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as TARGET
1401 FIELD => 'LinkedTo',
1403 TARGET => ($args{'TARGET'} || $args{'TICKET'}),
1404 TYPE => $args{'TYPE'},
1405 DESCRIPTION => $self->loc(
1406 "Tickets [_1] by [_2]", $self->loc($args{'TYPE'}), ($args{'TARGET'} || $args{'TICKET'})
1414 # {{{ LimitLinkedFrom
1416 =head2 LimitLinkedFrom
1418 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1419 TYPE limits the sort of relationship we want to search on
1422 BASE is the id or URI of the BASE of the link
1423 (BASE used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as BASE
1428 sub LimitLinkedFrom {
1430 my %args = ( BASE => undef,
1435 # translate RT2 From/To naming to RT3 TicketSQL naming
1436 my %fromToMap = qw(DependsOn DependentOn
1438 RefersTo ReferredToBy);
1440 my $type = $args{'TYPE'};
1441 $type = $fromToMap{$type} if exists($fromToMap{$type});
1443 $self->Limit( FIELD => 'LinkedTo',
1445 BASE => ($args{'BASE'} || $args{'TICKET'}),
1447 DESCRIPTION => $self->loc(
1448 "Tickets [_1] [_2]", $self->loc($args{'TYPE'}), ($args{'BASE'} || $args{'TICKET'})
1459 my $ticket_id = shift;
1460 $self->LimitLinkedTo ( TARGET=> "$ticket_id",
1467 # {{{ LimitHasMember
1468 sub LimitHasMember {
1470 my $ticket_id =shift;
1471 $self->LimitLinkedFrom ( BASE => "$ticket_id",
1472 TYPE => 'HasMember',
1478 # {{{ LimitDependsOn
1480 sub LimitDependsOn {
1482 my $ticket_id = shift;
1483 $self->LimitLinkedTo ( TARGET => "$ticket_id",
1484 TYPE => 'DependsOn',
1491 # {{{ LimitDependedOnBy
1493 sub LimitDependedOnBy {
1495 my $ticket_id = shift;
1496 $self->LimitLinkedFrom ( BASE => "$ticket_id",
1497 TYPE => 'DependentOn',
1509 my $ticket_id = shift;
1510 $self->LimitLinkedTo ( TARGET => "$ticket_id",
1518 # {{{ LimitReferredToBy
1520 sub LimitReferredToBy {
1522 my $ticket_id = shift;
1523 $self->LimitLinkedFrom ( BASE=> "$ticket_id",
1524 TYPE => 'ReferredTo',
1533 # {{{ limit based on ticket date attribtes
1537 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
1539 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
1541 OPERATOR is one of > or <
1542 VALUE is a date and time in ISO format in GMT
1543 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
1545 There are also helper functions of the form LimitFIELD that eliminate
1546 the need to pass in a FIELD argument.
1559 #Set the description if we didn't get handed it above
1560 unless ($args{'DESCRIPTION'} ) {
1561 $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
1564 $self->Limit (%args);
1575 $self->LimitDate( FIELD => 'Created', @_);
1579 $self->LimitDate( FIELD => 'Due', @_);
1584 $self->LimitDate( FIELD => 'Starts', @_);
1589 $self->LimitDate( FIELD => 'Started', @_);
1593 $self->LimitDate( FIELD => 'Resolved', @_);
1597 $self->LimitDate( FIELD => 'Told', @_);
1599 sub LimitLastUpdated {
1601 $self->LimitDate( FIELD => 'LastUpdated', @_);
1604 # {{{ sub LimitTransactionDate
1606 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
1608 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
1610 OPERATOR is one of > or <
1611 VALUE is a date and time in ISO format in GMT
1616 sub LimitTransactionDate {
1619 FIELD => 'TransactionDate',
1625 # <20021217042756.GK28744@pallas.fsck.com>
1626 # "Kill It" - Jesse.
1628 #Set the description if we didn't get handed it above
1629 unless ($args{'DESCRIPTION'} ) {
1630 $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
1633 $self->Limit (%args);
1641 # {{{ Limit based on custom fields
1642 # {{{ sub LimitCustomField
1644 =head2 LimitCustomField
1646 Takes a paramhash of key/value pairs with the following keys:
1650 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional
1651 parameter QUEUE may also be passed to distinguish the custom field.
1653 =item OPERATOR - The usual Limit operators
1655 =item VALUE - The value to compare against
1661 sub LimitCustomField {
1663 my %args = ( VALUE => undef,
1664 CUSTOMFIELD => undef,
1666 DESCRIPTION => undef,
1667 FIELD => 'CustomFieldValue',
1671 use RT::CustomFields;
1672 my $CF = RT::CustomField->new( $self->CurrentUser );
1673 if ( $args{CUSTOMFIELD} =~ /^\d+$/) {
1674 $CF->Load( $args{CUSTOMFIELD} );
1677 $CF->LoadByNameAndQueue( Name => $args{CUSTOMFIELD}, Queue => $args{QUEUE} );
1678 $args{CUSTOMFIELD} = $CF->Id;
1681 #If we are looking to compare with a null value.
1682 if ( $args{'OPERATOR'} =~ /^is$/i ) {
1683 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has no value.", $CF->Name);
1685 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
1686 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has a value.", $CF->Name);
1689 # if we're not looking to compare with a null value
1691 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] [_2] [_3]", $CF->Name , $args{OPERATOR} , $args{VALUE});
1696 my $qo = new RT::Queue( $self->CurrentUser );
1697 $qo->load( $CF->Queue );
1702 @rest = ( ENTRYAGGREGATOR => 'AND' )
1703 if ($CF->Type eq 'SelectMultiple');
1705 $self->Limit( VALUE => $args{VALUE},
1707 ? $q . ".{" . $CF->Name . "}"
1710 OPERATOR => $args{OPERATOR},
1716 $self->{'RecalcTicketLimits'} = 1;
1723 # {{{ sub _NextIndex
1727 Keep track of the counter for the array of restrictions
1733 return ($self->{'restriction_index'}++);
1739 # {{{ Core bits to make this a DBIx::SearchBuilder object
1744 $self->{'table'} = "Tickets";
1745 $self->{'RecalcTicketLimits'} = 1;
1746 $self->{'looking_at_effective_id'} = 0;
1747 $self->{'looking_at_type'} = 0;
1748 $self->{'restriction_index'} =1;
1749 $self->{'primary_key'} = "id";
1750 delete $self->{'items_array'};
1751 delete $self->{'item_map'};
1752 $self->SUPER::_Init(@_);
1762 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1763 return($self->SUPER::Count());
1770 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1771 return($self->SUPER::CountAll());
1776 # {{{ sub ItemsArrayRef
1778 =head2 ItemsArrayRef
1780 Returns a reference to the set of all items found in this search
1788 unless ( $self->{'items_array'} ) {
1790 my $placeholder = $self->_ItemsCounter;
1791 $self->GotoFirstItem();
1792 while ( my $item = $self->Next ) {
1793 push ( @{ $self->{'items_array'} }, $item );
1795 $self->GotoItem($placeholder);
1797 return ( $self->{'items_array'} );
1805 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1807 my $Ticket = $self->SUPER::Next();
1808 if ((defined($Ticket)) and (ref($Ticket))) {
1810 #Make sure we _never_ show deleted tickets
1811 #TODO we should be doing this in the where clause.
1812 #but you can't do multiple clauses on the same field just yet :/
1814 if ($Ticket->__Value('Status') eq 'deleted') {
1815 return($self->Next());
1817 # Since Ticket could be granted with more rights instead
1818 # of being revoked, it's ok if queue rights allow
1819 # ShowTicket. It seems need another query, but we have
1820 # rights cache in Principal::HasRight.
1821 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket') ||
1822 $Ticket->CurrentUserHasRight('ShowTicket')) {
1826 #If the user doesn't have the right to show this ticket
1828 return($self->Next());
1831 #if there never was any ticket
1841 # {{{ Deal with storing and restoring restrictions
1843 # {{{ sub LoadRestrictions
1845 =head2 LoadRestrictions
1847 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
1848 TODO It is not yet implemented
1854 # {{{ sub DescribeRestrictions
1856 =head2 DescribeRestrictions
1859 Returns a hash keyed by restriction id.
1860 Each element of the hash is currently a one element hash that contains DESCRIPTION which
1861 is a description of the purpose of that TicketRestriction
1865 sub DescribeRestrictions {
1868 my ($row, %listing);
1870 foreach $row (keys %{$self->{'TicketRestrictions'}}) {
1871 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
1877 # {{{ sub RestrictionValues
1879 =head2 RestrictionValues FIELD
1881 Takes a restriction field and returns a list of values this field is restricted
1886 sub RestrictionValues {
1889 map $self->{'TicketRestrictions'}{$_}{'VALUE'},
1891 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
1892 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
1894 keys %{$self->{'TicketRestrictions'}};
1899 # {{{ sub ClearRestrictions
1901 =head2 ClearRestrictions
1903 Removes all restrictions irretrievably
1907 sub ClearRestrictions {
1909 delete $self->{'TicketRestrictions'};
1910 $self->{'looking_at_effective_id'} = 0;
1911 $self->{'looking_at_type'} = 0;
1912 $self->{'RecalcTicketLimits'} =1;
1917 # {{{ sub DeleteRestriction
1919 =head2 DeleteRestriction
1921 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
1922 Removes that restriction from the session's limits.
1927 sub DeleteRestriction {
1930 delete $self->{'TicketRestrictions'}{$row};
1932 $self->{'RecalcTicketLimits'} = 1;
1933 #make the underlying easysearch object forget all its preconceptions
1938 # {{{ sub _RestrictionsToClauses
1940 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
1942 sub _RestrictionsToClauses {
1947 foreach $row (keys %{$self->{'TicketRestrictions'}}) {
1948 my $restriction = $self->{'TicketRestrictions'}{$row};
1950 #print Dumper($restriction),"\n";
1952 # We need to reimplement the subclause aggregation that SearchBuilder does.
1953 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
1954 # Then SB AND's the different Subclauses together.
1956 # So, we want to group things into Subclauses, convert them to
1957 # SQL, and then join them with the appropriate DefaultEA.
1958 # Then join each subclause group with AND.
1960 my $field = $restriction->{'FIELD'};
1961 my $realfield = $field; # CustomFields fake up a fieldname, so
1962 # we need to figure that out
1965 # Rewrite LinkedTo meta field to the real field
1966 if ($field =~ /LinkedTo/) {
1967 $realfield = $field = $restriction->{'TYPE'};
1971 # CustomFields have a different real field
1972 if ($field =~ /^CF\./) {
1976 die "I don't know about $field yet"
1977 unless (exists $FIELDS{$realfield} or $restriction->{CUSTOMFIELD});
1979 my $type = $FIELDS{$realfield}->[0];
1980 my $op = $restriction->{'OPERATOR'};
1982 my $value = ( grep { defined }
1983 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET))[0];
1985 # this performs the moral equivalent of defined or/dor/C<//>,
1986 # without the short circuiting.You need to use a 'defined or'
1987 # type thing instead of just checking for truth values, because
1988 # VALUE could be 0.(i.e. "false")
1990 # You could also use this, but I find it less aesthetic:
1991 # (although it does short circuit)
1992 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
1993 # defined $restriction->{'TICKET'} ?
1994 # $restriction->{TICKET} :
1995 # defined $restriction->{'BASE'} ?
1996 # $restriction->{BASE} :
1997 # defined $restriction->{'TARGET'} ?
1998 # $restriction->{TARGET} )
2000 my $ea = $restriction->{ENTRYAGGREGATOR} || $DefaultEA{$type} || "AND";
2002 die "Invalid operator $op for $field ($type)"
2003 unless exists $ea->{$op};
2007 # Each CustomField should be put into a different Clause so they
2008 # are ANDed together.
2009 if ($restriction->{CUSTOMFIELD}) {
2010 $realfield = $field;
2013 exists $clause{$realfield} or $clause{$realfield} = [];
2015 $field =~ s!(['"])!\\$1!g;
2016 $value =~ s!(['"])!\\$1!g;
2017 my $data = [ $ea, $type, $field, $op, $value ];
2019 # here is where we store extra data, say if it's a keyword or
2020 # something. (I.e. "TYPE SPECIFIC STUFF")
2022 #print Dumper($data);
2023 push @{$clause{$realfield}}, $data;
2030 # {{{ sub _ProcessRestrictions
2032 =head2 _ProcessRestrictions PARAMHASH
2034 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
2035 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
2039 sub _ProcessRestrictions {
2042 #Blow away ticket aliases since we'll need to regenerate them for
2044 delete $self->{'TicketAliases'};
2045 delete $self->{'items_array'};
2046 delete $self->{'item_map'};
2047 delete $self->{'raw_rows'};
2048 delete $self->{'rows'};
2049 delete $self->{'count_all'};
2051 my $sql = $self->{_sql_query}; # Violating the _SQL namespace
2052 if (!$sql||$self->{'RecalcTicketLimits'}) {
2053 # "Restrictions to Clauses Branch\n";
2054 my $clauseRef = eval { $self->_RestrictionsToClauses; };
2056 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2059 $sql = $self->ClausesToSQL($clauseRef);
2060 $self->FromSQL($sql);
2065 $self->{'RecalcTicketLimits'} = 0;
2069 =head2 _BuildItemMap
2071 # Build up a map of first/last/next/prev items, so that we can display search nav quickly
2078 my $items = $self->ItemsArrayRef;
2081 delete $self->{'item_map'};
2083 $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
2084 while (my $item = shift @$items ) {
2085 my $id = $item->EffectiveId;
2086 $self->{'item_map'}->{$id}->{'defined'} = 1;
2087 $self->{'item_map'}->{$id}->{prev} = $prev;
2088 $self->{'item_map'}->{$id}->{next} = $items->[0]->EffectiveId if ($items->[0]);
2091 $self->{'item_map'}->{'last'} = $prev;
2098 Returns an a map of all items found by this search. The map is of the form
2100 $ItemMap->{'first'} = first ticketid found
2101 $ItemMap->{'last'} = last ticketid found
2102 $ItemMap->{$id}->{prev} = the tikcet id found before $id
2103 $ItemMap->{$id}->{next} = the tikcet id found after $id
2109 $self->_BuildItemMap() unless ($self->{'item_map'});
2110 return ($self->{'item_map'});
2126 =head2 PrepForSerialization
2128 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
2133 sub PrepForSerialization {
2135 delete $self->{'items'};
2136 $self->RedoSearch();