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 $sb->{_sql_linkalias} = $sb->NewAlias ('Links')
297 unless defined $sb->{_sql_linkalias};
302 ALIAS => $sb->{_sql_linkalias},
309 if ($meta->[1] eq "To") {
310 my $matchfield = ( $value =~ /^(\d+)$/ ? "LocalTarget" : "Target" );
313 ALIAS => $sb->{_sql_linkalias},
314 ENTRYAGGREGATOR => 'AND',
315 FIELD => $matchfield,
320 #If we're searching on target, join the base to ticket.id
321 $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
322 ALIAS2 => $sb->{_sql_linkalias}, FIELD2 => 'LocalBase');
324 } elsif ( $meta->[1] eq "From" ) {
325 my $matchfield = ( $value =~ /^(\d+)$/ ? "LocalBase" : "Base" );
328 ALIAS => $sb->{_sql_linkalias},
329 ENTRYAGGREGATOR => 'AND',
330 FIELD => $matchfield,
335 #If we're searching on base, join the target to ticket.id
336 $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
337 ALIAS2 => $sb->{_sql_linkalias}, FIELD2 => 'LocalTarget');
340 die "Invalid link direction '$meta->[1]' for $field\n";
349 Handle date fields. (Created, LastTold..)
352 1: type of relationship. (Probably not necessary.)
357 my ($sb,$field,$op,$value,@rest) = @_;
359 die "Invalid Date Op: $op"
360 unless $op =~ /^(=|>|<|>=|<=)$/;
362 my $meta = $FIELDS{$field};
363 die "Incorrect Meta Data for $field"
364 unless (defined $meta->[1]);
366 require Time::ParseDate;
367 use POSIX 'strftime';
369 # FIXME: Replace me with RT::Date( Type => 'unknown' ...)
370 my $time = Time::ParseDate::parsedate( $value,
371 UK => $RT::DateDayBeforeMonth,
372 PREFER_PAST => $RT::AmbiguousDayInPast,
373 PREFER_FUTURE => !($RT::AmbiguousDayInPast),
378 # if we're specifying =, that means we want everything on a
379 # particular single day. in the database, we need to check for >
380 # and < the edges of that day.
382 my $daystart = strftime("%Y-%m-%d %H:%M",
383 gmtime($time - ( $time % 86400 )));
384 my $dayend = strftime("%Y-%m-%d %H:%M",
385 gmtime($time + ( 86399 - $time % 86400 )));
401 ENTRYAGGREGATOR => 'AND',
407 $value = strftime("%Y-%m-%d %H:%M", gmtime($time));
419 Handle simple fields which are just strings. (Subject,Type)
427 my ($sb,$field,$op,$value,@rest) = @_;
431 # =, !=, LIKE, NOT LIKE
442 =head2 _TransDateLimit
444 Handle fields limiting based on Transaction Date.
446 The inpupt value must be in a format parseable by Time::ParseDate
453 sub _TransDateLimit {
454 my ($sb,$field,$op,$value,@rest) = @_;
456 # See the comments for TransLimit, they apply here too
458 $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
459 unless defined $sb->{_sql_transalias};
460 $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
461 unless defined $sb->{_sql_trattachalias};
465 # Join Transactions To Attachments
466 $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
467 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
469 # Join Transactions to Tickets
470 $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
471 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
473 my $d = new RT::Date( $sb->CurrentUser );
474 $d->Set( Format => 'ISO', Value => $value);
477 #Search for the right field
478 $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
491 Limit based on the Content of a transaction or the ContentType.
499 # Content, ContentType, Filename
501 # If only this was this simple. We've got to do something
504 #Basically, we want to make sure that the limits apply to
505 #the same attachment, rather than just another attachment
506 #for the same ticket, no matter how many clauses we lump
507 #on. We put them in TicketAliases so that they get nuked
508 #when we redo the join.
510 # In the SQL, we might have
511 # (( Content = foo ) or ( Content = bar AND Content = baz ))
512 # The AND group should share the same Alias.
514 # Actually, maybe it doesn't matter. We use the same alias and it
515 # works itself out? (er.. different.)
517 # Steal more from _ProcessRestrictions
519 # FIXME: Maybe look at the previous FooLimit call, and if it was a
520 # TransLimit and EntryAggregator == AND, reuse the Aliases?
522 # Or better - store the aliases on a per subclause basis - since
523 # those are going to be the things we want to relate to each other,
526 # maybe we should not allow certain kinds of aggregation of these
527 # clauses and do a psuedo regex instead? - the problem is getting
528 # them all into the same subclause when you have (A op B op C) - the
529 # way they get parsed in the tree they're in different subclauses.
531 my ($sb,$field,$op,$value,@rest) = @_;
533 $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
534 unless defined $sb->{_sql_transalias};
535 $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
536 unless defined $sb->{_sql_trattachalias};
540 #Search for the right field
541 $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
549 # Join Transactions To Attachments
550 $sb->_SQLJoin( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
551 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
553 # Join Transactions to Tickets
554 $sb->_SQLJoin( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
555 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
563 Handle watcher limits. (Requestor, CC, etc..)
571 my ($self,$field,$op,$value,@rest) = @_;
576 my $groups = $self->NewAlias('Groups');
577 my $groupmembers = $self->NewAlias('CachedGroupMembers');
578 my $users = $self->NewAlias('Users');
582 # my $subclause = undef;
583 # my $aggregator = 'OR';
584 # if ($restriction->{'OPERATOR'} =~ /!|NOT/i ){
585 # $subclause = 'AndEmailIsNot';
586 # $aggregator = 'AND';
589 if (ref $field) { # gross hack
590 my @bundle = @$field;
592 for my $chunk (@bundle) {
593 ($field,$op,$value,@rest) = @$chunk;
594 $self->_SQLLimit(ALIAS => $users,
595 FIELD => $rest{SUBKEY} || 'EmailAddress',
604 $self->_SQLLimit(ALIAS => $users,
605 FIELD => $rest{SUBKEY} || 'EmailAddress',
613 # {{{ Tie to groups for tickets we care about
614 $self->_SQLLimit(ALIAS => $groups,
616 VALUE => 'RT::Ticket-Role',
617 ENTRYAGGREGATOR => 'AND');
619 $self->_SQLJoin(ALIAS1 => $groups, FIELD1 => 'Instance',
620 ALIAS2 => 'main', FIELD2 => 'id');
623 # If we care about which sort of watcher
624 my $meta = $FIELDS{$field};
625 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
628 $self->_SQLLimit(ALIAS => $groups,
631 ENTRYAGGREGATOR => 'AND');
634 $self->_SQLJoin (ALIAS1 => $groups, FIELD1 => 'id',
635 ALIAS2 => $groupmembers, FIELD2 => 'GroupId');
637 $self->_SQLJoin( ALIAS1 => $groupmembers, FIELD1 => 'MemberId',
638 ALIAS2 => $users, FIELD2 => 'id');
644 sub _LinkFieldLimit {
649 if ($restriction->{'TYPE'}) {
650 $self->SUPER::Limit(ALIAS => $LinkAlias,
651 ENTRYAGGREGATOR => 'AND',
654 VALUE => $restriction->{'TYPE'} );
657 #If we're trying to limit it to things that are target of
658 if ($restriction->{'TARGET'}) {
659 # If the TARGET is an integer that means that we want to look at
660 # the LocalTarget field. otherwise, we want to look at the
663 if ($restriction->{'TARGET'} =~/^(\d+)$/) {
664 $matchfield = "LocalTarget";
666 $matchfield = "Target";
668 $self->SUPER::Limit(ALIAS => $LinkAlias,
669 ENTRYAGGREGATOR => 'AND',
670 FIELD => $matchfield,
672 VALUE => $restriction->{'TARGET'} );
673 #If we're searching on target, join the base to ticket.id
674 $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
675 ALIAS2 => $LinkAlias,
676 FIELD2 => 'LocalBase');
678 #If we're trying to limit it to things that are base of
679 elsif ($restriction->{'BASE'}) {
680 # If we're trying to match a numeric link, we want to look at
681 # LocalBase, otherwise we want to look at "Base"
683 if ($restriction->{'BASE'} =~/^(\d+)$/) {
684 $matchfield = "LocalBase";
686 $matchfield = "Base";
689 $self->SUPER::Limit(ALIAS => $LinkAlias,
690 ENTRYAGGREGATOR => 'AND',
691 FIELD => $matchfield,
693 VALUE => $restriction->{'BASE'} );
694 #If we're searching on base, join the target to ticket.id
695 $self->_SQLJoin( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
696 ALIAS2 => $LinkAlias,
697 FIELD2 => 'LocalTarget')
704 Limit based on Keywords
711 sub _CustomFieldLimit {
712 my ($self,$_field,$op,$value,@rest) = @_;
715 my $field = $rest{SUBKEY} || die "No field specified";
717 # For our sanity, we can only limit on one queue at a time
719 # Ugh. This will not do well for things with underscores in them
721 use RT::CustomFields;
722 my $CF = RT::CustomFields->new( $self->CurrentUser );
723 #$CF->Load( $cfid} );
726 if ($field =~ /^(.+?)\.{(.+)}$/) {
727 my $q = RT::Queue->new($self->CurrentUser);
730 $CF->LimitToQueue( $q->Id );
733 $field = $1 if $field =~ /^{(.+)}$/; # trim { }
740 # this is pretty inefficient for huge numbers of CFs...
741 while ( my $CustomField = $CF->Next ) {
742 if (lc $CustomField->Name eq lc $field) {
743 $cfid = $CustomField->Id;
747 die "No custom field named $field found\n"
750 # use RT::CustomFields;
751 # my $CF = RT::CustomField->new( $self->CurrentUser );
752 # $CF->Load( $cfid );
758 # Perform one Join per CustomField
759 if ($self->{_sql_keywordalias}{$cfid}) {
760 $TicketCFs = $self->{_sql_keywordalias}{$cfid};
762 $TicketCFs = $self->{_sql_keywordalias}{$cfid} =
763 $self->_SQLJoin( TYPE => 'left',
766 TABLE2 => 'TicketCustomFieldValues',
767 FIELD2 => 'Ticket' );
772 $self->_SQLLimit( ALIAS => $TicketCFs,
780 or ( $op eq '!=' ) ) {
781 $null_columns_ok = 1;
784 #If we're trying to find tickets where the keyword isn't somethng,
785 #also check ones where it _IS_ null
788 $self->_SQLLimit( ALIAS => $TicketCFs,
793 ENTRYAGGREGATOR => 'OR', );
796 $self->_SQLLimit( LEFTJOIN => $TicketCFs,
797 FIELD => 'CustomField',
799 ENTRYAGGREGATOR => 'OR' );
808 # End Helper Functions
810 # End of SQL Stuff -------------------------------------------------
812 # {{{ Limit the result set based on content
818 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
819 Generally best called from LimitFoo methods
824 my %args = ( FIELD => undef,
827 DESCRIPTION => undef,
830 $args{'DESCRIPTION'} = $self->loc(
831 "[_1] [_2] [_3]", $args{'FIELD'}, $args{'OPERATOR'}, $args{'VALUE'}
832 ) if (!defined $args{'DESCRIPTION'}) ;
834 my $index = $self->_NextIndex;
836 #make the TicketRestrictions hash the equivalent of whatever we just passed in;
838 %{$self->{'TicketRestrictions'}{$index}} = %args;
840 $self->{'RecalcTicketLimits'} = 1;
842 # If we're looking at the effective id, we don't want to append the other clause
843 # which limits us to tickets where id = effective id
844 if ($args{'FIELD'} eq 'EffectiveId') {
845 $self->{'looking_at_effective_id'} = 1;
848 if ($args{'FIELD'} eq 'Type') {
849 $self->{'looking_at_type'} = 1;
862 Returns a frozen string suitable for handing back to ThawLimits.
866 sub _FreezeThawKeys {
867 'TicketRestrictions',
869 'looking_at_effective_id',
873 # {{{ sub FreezeLimits
878 return (FreezeThaw::freeze(@{$self}{$self->_FreezeThawKeys}));
885 Take a frozen Limits string generated by FreezeLimits and make this tickets
886 object have that set of limits.
895 #if we don't have $in, get outta here.
896 return undef unless ($in);
898 $self->{'RecalcTicketLimits'} = 1;
902 #We don't need to die if the thaw fails.
905 @{$self}{$self->_FreezeThawKeys} = FreezeThaw::thaw($in);
907 $RT::Logger->error( $@ ) if $@;
913 # {{{ Limit by enum or foreign key
919 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
920 OPERATOR is one of = or !=. (It defaults to =).
921 VALUE is a queue id or Name.
928 my %args = (VALUE => undef,
932 #TODO VALUE should also take queue names and queue objects
933 #TODO FIXME why are we canonicalizing to name, not id, robrt?
934 if ($args{VALUE} =~ /^\d+$/) {
935 my $queue = new RT::Queue($self->CurrentUser);
936 $queue->Load($args{'VALUE'});
937 $args{VALUE} = $queue->Name;
940 # What if they pass in an Id? Check for isNum() and convert to
943 #TODO check for a valid queue here
945 $self->Limit (FIELD => 'Queue',
946 VALUE => $args{VALUE},
947 OPERATOR => $args{'OPERATOR'},
949 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE},
956 # {{{ sub LimitStatus
960 Takes a paramhash with the fields OPERATOR and VALUE.
961 OPERATOR is one of = or !=.
968 my %args = ( OPERATOR => '=',
970 $self->Limit (FIELD => 'Status',
971 VALUE => $args{'VALUE'},
972 OPERATOR => $args{'OPERATOR'},
974 ' ', $self->loc('Status'), $args{'OPERATOR'}, $self->loc($args{'VALUE'})
985 If called, this search will not automatically limit the set of results found
986 to tickets of type "Ticket". Tickets of other types, such as "project" and
987 "approval" will be found.
994 # Instead of faking a Limit that later gets ignored, fake up the
995 # fact that we're already looking at type, so that the check in
996 # Tickets_Overlay_SQL/FromSQL goes down the right branch
998 # $self->LimitType(VALUE => '__any');
999 $self->{looking_at_type} = 1;
1008 Takes a paramhash with the fields OPERATOR and VALUE.
1009 OPERATOR is one of = or !=, it defaults to "=".
1010 VALUE is a string to search for in the type of the ticket.
1018 my %args = (OPERATOR => '=',
1021 $self->Limit (FIELD => 'Type',
1022 VALUE => $args{'VALUE'},
1023 OPERATOR => $args{'OPERATOR'},
1024 DESCRIPTION => join(
1025 ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'},
1034 # {{{ Limit by string field
1036 # {{{ sub LimitSubject
1040 Takes a paramhash with the fields OPERATOR and VALUE.
1041 OPERATOR is one of = or !=.
1042 VALUE is a string to search for in the subject of the ticket.
1049 $self->Limit (FIELD => 'Subject',
1050 VALUE => $args{'VALUE'},
1051 OPERATOR => $args{'OPERATOR'},
1052 DESCRIPTION => join(
1053 ' ', $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'},
1062 # {{{ Limit based on ticket numerical attributes
1063 # Things that can be > < = !=
1069 Takes a paramhash with the fields OPERATOR and VALUE.
1070 OPERATOR is one of =, >, < or !=.
1071 VALUE is a ticket Id to search for
1077 my %args = (OPERATOR => '=',
1080 $self->Limit (FIELD => 'id',
1081 VALUE => $args{'VALUE'},
1082 OPERATOR => $args{'OPERATOR'},
1083 DESCRIPTION => join(
1084 ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'},
1091 # {{{ sub LimitPriority
1093 =head2 LimitPriority
1095 Takes a paramhash with the fields OPERATOR and VALUE.
1096 OPERATOR is one of =, >, < or !=.
1097 VALUE is a value to match the ticket\'s priority against
1104 $self->Limit (FIELD => 'Priority',
1105 VALUE => $args{'VALUE'},
1106 OPERATOR => $args{'OPERATOR'},
1107 DESCRIPTION => join(
1108 ' ', $self->loc('Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1115 # {{{ sub LimitInitialPriority
1117 =head2 LimitInitialPriority
1119 Takes a paramhash with the fields OPERATOR and VALUE.
1120 OPERATOR is one of =, >, < or !=.
1121 VALUE is a value to match the ticket\'s initial priority against
1126 sub LimitInitialPriority {
1129 $self->Limit (FIELD => 'InitialPriority',
1130 VALUE => $args{'VALUE'},
1131 OPERATOR => $args{'OPERATOR'},
1132 DESCRIPTION => join(
1133 ' ', $self->loc('Initial Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1140 # {{{ sub LimitFinalPriority
1142 =head2 LimitFinalPriority
1144 Takes a paramhash with the fields OPERATOR and VALUE.
1145 OPERATOR is one of =, >, < or !=.
1146 VALUE is a value to match the ticket\'s final priority against
1150 sub LimitFinalPriority {
1153 $self->Limit (FIELD => 'FinalPriority',
1154 VALUE => $args{'VALUE'},
1155 OPERATOR => $args{'OPERATOR'},
1156 DESCRIPTION => join(
1157 ' ', $self->loc('Final Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1164 # {{{ sub LimitTimeWorked
1166 =head2 LimitTimeWorked
1168 Takes a paramhash with the fields OPERATOR and VALUE.
1169 OPERATOR is one of =, >, < or !=.
1170 VALUE is a value to match the ticket's TimeWorked attribute
1174 sub LimitTimeWorked {
1177 $self->Limit (FIELD => 'TimeWorked',
1178 VALUE => $args{'VALUE'},
1179 OPERATOR => $args{'OPERATOR'},
1180 DESCRIPTION => join(
1181 ' ', $self->loc('Time worked'), $args{'OPERATOR'}, $args{'VALUE'},
1188 # {{{ sub LimitTimeLeft
1190 =head2 LimitTimeLeft
1192 Takes a paramhash with the fields OPERATOR and VALUE.
1193 OPERATOR is one of =, >, < or !=.
1194 VALUE is a value to match the ticket's TimeLeft attribute
1201 $self->Limit (FIELD => 'TimeLeft',
1202 VALUE => $args{'VALUE'},
1203 OPERATOR => $args{'OPERATOR'},
1204 DESCRIPTION => join(
1205 ' ', $self->loc('Time left'), $args{'OPERATOR'}, $args{'VALUE'},
1214 # {{{ Limiting based on attachment attributes
1216 # {{{ sub LimitContent
1220 Takes a paramhash with the fields OPERATOR and VALUE.
1221 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1222 VALUE is a string to search for in the body of the ticket
1228 $self->Limit (FIELD => 'Content',
1229 VALUE => $args{'VALUE'},
1230 OPERATOR => $args{'OPERATOR'},
1231 DESCRIPTION => join(
1232 ' ', $self->loc('Ticket content'), $args{'OPERATOR'}, $args{'VALUE'},
1239 # {{{ sub LimitFilename
1241 =head2 LimitFilename
1243 Takes a paramhash with the fields OPERATOR and VALUE.
1244 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1245 VALUE is a string to search for in the body of the ticket
1251 $self->Limit (FIELD => 'Filename',
1252 VALUE => $args{'VALUE'},
1253 OPERATOR => $args{'OPERATOR'},
1254 DESCRIPTION => join(
1255 ' ', $self->loc('Attachment filename'), $args{'OPERATOR'}, $args{'VALUE'},
1261 # {{{ sub LimitContentType
1263 =head2 LimitContentType
1265 Takes a paramhash with the fields OPERATOR and VALUE.
1266 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1267 VALUE is a content type to search ticket attachments for
1271 sub LimitContentType {
1274 $self->Limit (FIELD => 'ContentType',
1275 VALUE => $args{'VALUE'},
1276 OPERATOR => $args{'OPERATOR'},
1277 DESCRIPTION => join(
1278 ' ', $self->loc('Ticket content type'), $args{'OPERATOR'}, $args{'VALUE'},
1286 # {{{ Limiting based on people
1288 # {{{ sub LimitOwner
1292 Takes a paramhash with the fields OPERATOR and VALUE.
1293 OPERATOR is one of = or !=.
1300 my %args = ( OPERATOR => '=',
1303 my $owner = new RT::User($self->CurrentUser);
1304 $owner->Load($args{'VALUE'});
1305 # FIXME: check for a valid $owner
1306 $self->Limit (FIELD => 'Owner',
1307 VALUE => $args{'VALUE'},
1308 OPERATOR => $args{'OPERATOR'},
1309 DESCRIPTION => join(
1310 ' ', $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(),
1318 # {{{ Limiting watchers
1320 # {{{ sub LimitWatcher
1325 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1326 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1327 VALUE is a value to match the ticket\'s watcher email addresses against
1328 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1332 my $t1 = RT::Ticket->new($RT::SystemUser);
1333 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
1341 my %args = ( OPERATOR => '=',
1347 #build us up a description
1348 my ($watcher_type, $desc);
1349 if ($args{'TYPE'}) {
1350 $watcher_type = $args{'TYPE'};
1353 $watcher_type = "Watcher";
1356 $self->Limit (FIELD => $watcher_type,
1357 VALUE => $args{'VALUE'},
1358 OPERATOR => $args{'OPERATOR'},
1359 TYPE => $args{'TYPE'},
1360 DESCRIPTION => join(
1361 ' ', $self->loc($watcher_type), $args{'OPERATOR'}, $args{'VALUE'},
1367 sub LimitRequestor {
1370 my ($package, $filename, $line) = caller;
1371 $RT::Logger->error("Tickets->LimitRequestor is deprecated. please rewrite call at $package - $filename: $line");
1372 $self->LimitWatcher(TYPE => 'Requestor', @_);
1383 # {{{ Limiting based on links
1387 =head2 LimitLinkedTo
1389 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1390 TYPE limits the sort of relationship we want to search on
1392 TYPE = { RefersTo, MemberOf, DependsOn }
1394 TARGET is the id or URI of the TARGET of the link
1395 (TARGET used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as TARGET
1408 FIELD => 'LinkedTo',
1410 TARGET => ($args{'TARGET'} || $args{'TICKET'}),
1411 TYPE => $args{'TYPE'},
1412 DESCRIPTION => $self->loc(
1413 "Tickets [_1] by [_2]", $self->loc($args{'TYPE'}), ($args{'TARGET'} || $args{'TICKET'})
1421 # {{{ LimitLinkedFrom
1423 =head2 LimitLinkedFrom
1425 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1426 TYPE limits the sort of relationship we want to search on
1429 BASE is the id or URI of the BASE of the link
1430 (BASE used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as BASE
1435 sub LimitLinkedFrom {
1437 my %args = ( BASE => undef,
1442 # translate RT2 From/To naming to RT3 TicketSQL naming
1443 my %fromToMap = qw(DependsOn DependentOn
1445 RefersTo ReferredToBy);
1447 my $type = $args{'TYPE'};
1448 $type = $fromToMap{$type} if exists($fromToMap{$type});
1450 $self->Limit( FIELD => 'LinkedTo',
1452 BASE => ($args{'BASE'} || $args{'TICKET'}),
1454 DESCRIPTION => $self->loc(
1455 "Tickets [_1] [_2]", $self->loc($args{'TYPE'}), ($args{'BASE'} || $args{'TICKET'})
1466 my $ticket_id = shift;
1467 $self->LimitLinkedTo ( TARGET=> "$ticket_id",
1474 # {{{ LimitHasMember
1475 sub LimitHasMember {
1477 my $ticket_id =shift;
1478 $self->LimitLinkedFrom ( BASE => "$ticket_id",
1479 TYPE => 'HasMember',
1485 # {{{ LimitDependsOn
1487 sub LimitDependsOn {
1489 my $ticket_id = shift;
1490 $self->LimitLinkedTo ( TARGET => "$ticket_id",
1491 TYPE => 'DependsOn',
1498 # {{{ LimitDependedOnBy
1500 sub LimitDependedOnBy {
1502 my $ticket_id = shift;
1503 $self->LimitLinkedFrom ( BASE => "$ticket_id",
1504 TYPE => 'DependentOn',
1516 my $ticket_id = shift;
1517 $self->LimitLinkedTo ( TARGET => "$ticket_id",
1525 # {{{ LimitReferredToBy
1527 sub LimitReferredToBy {
1529 my $ticket_id = shift;
1530 $self->LimitLinkedFrom ( BASE=> "$ticket_id",
1531 TYPE => 'ReferredTo',
1540 # {{{ limit based on ticket date attribtes
1544 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
1546 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
1548 OPERATOR is one of > or <
1549 VALUE is a date and time in ISO format in GMT
1550 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
1552 There are also helper functions of the form LimitFIELD that eliminate
1553 the need to pass in a FIELD argument.
1566 #Set the description if we didn't get handed it above
1567 unless ($args{'DESCRIPTION'} ) {
1568 $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
1571 $self->Limit (%args);
1582 $self->LimitDate( FIELD => 'Created', @_);
1586 $self->LimitDate( FIELD => 'Due', @_);
1591 $self->LimitDate( FIELD => 'Starts', @_);
1596 $self->LimitDate( FIELD => 'Started', @_);
1600 $self->LimitDate( FIELD => 'Resolved', @_);
1604 $self->LimitDate( FIELD => 'Told', @_);
1606 sub LimitLastUpdated {
1608 $self->LimitDate( FIELD => 'LastUpdated', @_);
1611 # {{{ sub LimitTransactionDate
1613 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
1615 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
1617 OPERATOR is one of > or <
1618 VALUE is a date and time in ISO format in GMT
1623 sub LimitTransactionDate {
1626 FIELD => 'TransactionDate',
1632 # <20021217042756.GK28744@pallas.fsck.com>
1633 # "Kill It" - Jesse.
1635 #Set the description if we didn't get handed it above
1636 unless ($args{'DESCRIPTION'} ) {
1637 $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
1640 $self->Limit (%args);
1648 # {{{ Limit based on custom fields
1649 # {{{ sub LimitCustomField
1651 =head2 LimitCustomField
1653 Takes a paramhash of key/value pairs with the following keys:
1657 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional
1658 parameter QUEUE may also be passed to distinguish the custom field.
1660 =item OPERATOR - The usual Limit operators
1662 =item VALUE - The value to compare against
1668 sub LimitCustomField {
1670 my %args = ( VALUE => undef,
1671 CUSTOMFIELD => undef,
1673 DESCRIPTION => undef,
1674 FIELD => 'CustomFieldValue',
1678 use RT::CustomFields;
1679 my $CF = RT::CustomField->new( $self->CurrentUser );
1680 if ( $args{CUSTOMFIELD} =~ /^\d+$/) {
1681 $CF->Load( $args{CUSTOMFIELD} );
1684 $CF->LoadByNameAndQueue( Name => $args{CUSTOMFIELD}, Queue => $args{QUEUE} );
1685 $args{CUSTOMFIELD} = $CF->Id;
1688 #If we are looking to compare with a null value.
1689 if ( $args{'OPERATOR'} =~ /^is$/i ) {
1690 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has no value.", $CF->Name);
1692 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
1693 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has a value.", $CF->Name);
1696 # if we're not looking to compare with a null value
1698 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] [_2] [_3]", $CF->Name , $args{OPERATOR} , $args{VALUE});
1703 my $qo = new RT::Queue( $self->CurrentUser );
1704 $qo->load( $CF->Queue );
1709 @rest = ( ENTRYAGGREGATOR => 'AND' )
1710 if ($CF->Type eq 'SelectMultiple');
1712 $self->Limit( VALUE => $args{VALUE},
1714 ? $q . ".{" . $CF->Name . "}"
1717 OPERATOR => $args{OPERATOR},
1723 $self->{'RecalcTicketLimits'} = 1;
1730 # {{{ sub _NextIndex
1734 Keep track of the counter for the array of restrictions
1740 return ($self->{'restriction_index'}++);
1746 # {{{ Core bits to make this a DBIx::SearchBuilder object
1751 $self->{'table'} = "Tickets";
1752 $self->{'RecalcTicketLimits'} = 1;
1753 $self->{'looking_at_effective_id'} = 0;
1754 $self->{'looking_at_type'} = 0;
1755 $self->{'restriction_index'} =1;
1756 $self->{'primary_key'} = "id";
1757 delete $self->{'items_array'};
1758 delete $self->{'item_map'};
1759 delete $self->{'columns_to_display'};
1760 $self->SUPER::_Init(@_);
1770 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1771 return($self->SUPER::Count());
1778 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1779 return($self->SUPER::CountAll());
1784 # {{{ sub ItemsArrayRef
1786 =head2 ItemsArrayRef
1788 Returns a reference to the set of all items found in this search
1796 unless ( $self->{'items_array'} ) {
1798 my $placeholder = $self->_ItemsCounter;
1799 $self->GotoFirstItem();
1800 while ( my $item = $self->Next ) {
1801 push ( @{ $self->{'items_array'} }, $item );
1803 $self->GotoItem($placeholder);
1805 return ( $self->{'items_array'} );
1813 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1815 my $Ticket = $self->SUPER::Next();
1816 if ((defined($Ticket)) and (ref($Ticket))) {
1818 #Make sure we _never_ show deleted tickets
1819 #TODO we should be doing this in the where clause.
1820 #but you can't do multiple clauses on the same field just yet :/
1822 if ($Ticket->__Value('Status') eq 'deleted') {
1823 return($self->Next());
1825 # Since Ticket could be granted with more rights instead
1826 # of being revoked, it's ok if queue rights allow
1827 # ShowTicket. It seems need another query, but we have
1828 # rights cache in Principal::HasRight.
1829 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket') ||
1830 $Ticket->CurrentUserHasRight('ShowTicket')) {
1834 #If the user doesn't have the right to show this ticket
1836 return($self->Next());
1839 #if there never was any ticket
1849 # {{{ Deal with storing and restoring restrictions
1851 # {{{ sub LoadRestrictions
1853 =head2 LoadRestrictions
1855 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
1856 TODO It is not yet implemented
1862 # {{{ sub DescribeRestrictions
1864 =head2 DescribeRestrictions
1867 Returns a hash keyed by restriction id.
1868 Each element of the hash is currently a one element hash that contains DESCRIPTION which
1869 is a description of the purpose of that TicketRestriction
1873 sub DescribeRestrictions {
1876 my ($row, %listing);
1878 foreach $row (keys %{$self->{'TicketRestrictions'}}) {
1879 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
1885 # {{{ sub RestrictionValues
1887 =head2 RestrictionValues FIELD
1889 Takes a restriction field and returns a list of values this field is restricted
1894 sub RestrictionValues {
1897 map $self->{'TicketRestrictions'}{$_}{'VALUE'},
1899 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
1900 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
1902 keys %{$self->{'TicketRestrictions'}};
1907 # {{{ sub ClearRestrictions
1909 =head2 ClearRestrictions
1911 Removes all restrictions irretrievably
1915 sub ClearRestrictions {
1917 delete $self->{'TicketRestrictions'};
1918 $self->{'looking_at_effective_id'} = 0;
1919 $self->{'looking_at_type'} = 0;
1920 $self->{'RecalcTicketLimits'} =1;
1925 # {{{ sub DeleteRestriction
1927 =head2 DeleteRestriction
1929 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
1930 Removes that restriction from the session's limits.
1935 sub DeleteRestriction {
1938 delete $self->{'TicketRestrictions'}{$row};
1940 $self->{'RecalcTicketLimits'} = 1;
1941 #make the underlying easysearch object forget all its preconceptions
1946 # {{{ sub _RestrictionsToClauses
1948 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
1950 sub _RestrictionsToClauses {
1955 foreach $row (keys %{$self->{'TicketRestrictions'}}) {
1956 my $restriction = $self->{'TicketRestrictions'}{$row};
1958 #print Dumper($restriction),"\n";
1960 # We need to reimplement the subclause aggregation that SearchBuilder does.
1961 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
1962 # Then SB AND's the different Subclauses together.
1964 # So, we want to group things into Subclauses, convert them to
1965 # SQL, and then join them with the appropriate DefaultEA.
1966 # Then join each subclause group with AND.
1968 my $field = $restriction->{'FIELD'};
1969 my $realfield = $field; # CustomFields fake up a fieldname, so
1970 # we need to figure that out
1973 # Rewrite LinkedTo meta field to the real field
1974 if ($field =~ /LinkedTo/) {
1975 $realfield = $field = $restriction->{'TYPE'};
1979 # CustomFields have a different real field
1980 if ($field =~ /^CF\./) {
1984 die "I don't know about $field yet"
1985 unless (exists $FIELDS{$realfield} or $restriction->{CUSTOMFIELD});
1987 my $type = $FIELDS{$realfield}->[0];
1988 my $op = $restriction->{'OPERATOR'};
1990 my $value = ( grep { defined }
1991 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET))[0];
1993 # this performs the moral equivalent of defined or/dor/C<//>,
1994 # without the short circuiting.You need to use a 'defined or'
1995 # type thing instead of just checking for truth values, because
1996 # VALUE could be 0.(i.e. "false")
1998 # You could also use this, but I find it less aesthetic:
1999 # (although it does short circuit)
2000 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2001 # defined $restriction->{'TICKET'} ?
2002 # $restriction->{TICKET} :
2003 # defined $restriction->{'BASE'} ?
2004 # $restriction->{BASE} :
2005 # defined $restriction->{'TARGET'} ?
2006 # $restriction->{TARGET} )
2008 my $ea = $restriction->{ENTRYAGGREGATOR} || $DefaultEA{$type} || "AND";
2010 die "Invalid operator $op for $field ($type)"
2011 unless exists $ea->{$op};
2015 # Each CustomField should be put into a different Clause so they
2016 # are ANDed together.
2017 if ($restriction->{CUSTOMFIELD}) {
2018 $realfield = $field;
2021 exists $clause{$realfield} or $clause{$realfield} = [];
2023 $field =~ s!(['"])!\\$1!g;
2024 $value =~ s!(['"])!\\$1!g;
2025 my $data = [ $ea, $type, $field, $op, $value ];
2027 # here is where we store extra data, say if it's a keyword or
2028 # something. (I.e. "TYPE SPECIFIC STUFF")
2030 #print Dumper($data);
2031 push @{$clause{$realfield}}, $data;
2038 # {{{ sub _ProcessRestrictions
2040 =head2 _ProcessRestrictions PARAMHASH
2042 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
2043 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
2047 sub _ProcessRestrictions {
2050 #Blow away ticket aliases since we'll need to regenerate them for
2052 delete $self->{'TicketAliases'};
2053 delete $self->{'items_array'};
2054 delete $self->{'item_map'};
2055 delete $self->{'raw_rows'};
2056 delete $self->{'rows'};
2057 delete $self->{'count_all'};
2059 my $sql = $self->{_sql_query}; # Violating the _SQL namespace
2060 if (!$sql||$self->{'RecalcTicketLimits'}) {
2061 # "Restrictions to Clauses Branch\n";
2062 my $clauseRef = eval { $self->_RestrictionsToClauses; };
2064 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2067 $sql = $self->ClausesToSQL($clauseRef);
2068 $self->FromSQL($sql);
2073 $self->{'RecalcTicketLimits'} = 0;
2077 =head2 _BuildItemMap
2079 # Build up a map of first/last/next/prev items, so that we can display search nav quickly
2086 my $items = $self->ItemsArrayRef;
2089 delete $self->{'item_map'};
2091 $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
2092 while (my $item = shift @$items ) {
2093 my $id = $item->EffectiveId;
2094 $self->{'item_map'}->{$id}->{'defined'} = 1;
2095 $self->{'item_map'}->{$id}->{prev} = $prev;
2096 $self->{'item_map'}->{$id}->{next} = $items->[0]->EffectiveId if ($items->[0]);
2099 $self->{'item_map'}->{'last'} = $prev;
2106 Returns an a map of all items found by this search. The map is of the form
2108 $ItemMap->{'first'} = first ticketid found
2109 $ItemMap->{'last'} = last ticketid found
2110 $ItemMap->{$id}->{prev} = the tikcet id found before $id
2111 $ItemMap->{$id}->{next} = the tikcet id found after $id
2117 $self->_BuildItemMap() unless ($self->{'item_map'});
2118 return ($self->{'item_map'});
2134 =head2 PrepForSerialization
2136 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
2141 sub PrepForSerialization {
2143 delete $self->{'items'};
2144 $self->RedoSearch();