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 ReferredTo => ['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 # Default EntryAggregator per type
129 ENUM => { '=' => 'OR',
133 STRING => { '=' => 'OR',
143 WATCHERFIELD => { '=' => 'OR',
153 # Helper functions for passing the above lexically scoped tables above
154 # into Tickets_Overlay_SQL.
155 sub FIELDS { return \%FIELDS }
156 sub dispatch { return \%dispatch }
158 # Bring in the clowns.
159 require RT::Tickets_Overlay_SQL;
163 @SORTFIELDS = qw(id Status
165 Owner Created Due Starts Started
167 Resolved LastUpdated Priority TimeWorked TimeLeft);
171 Returns the list of fields that lists of tickets can easily be sorted by
184 # BEGIN SQL STUFF *********************************
186 =head1 Limit Helper Routines
188 These routines are the targets of a dispatch table depending on the
189 type of field. They all share the same signature:
191 my ($self,$field,$op,$value,@rest) = @_;
193 The values in @rest should be suitable for passing directly to
194 DBIx::SearchBuilder::Limit.
196 Essentially they are an expanded/broken out (and much simplified)
197 version of what ProcessRestrictions used to do. They're also much
198 more clearly delineated by the TYPE of field being processed.
202 Handle Fields which are limited to certain values, and potentially
203 need to be looked up from another class.
205 This subroutine actually handles two different kinds of fields. For
206 some the user is responsible for limiting the values. (i.e. Status,
209 For others, the value specified by the user will be looked by via
213 name of class to lookup in (Optional)
218 my ($sb,$field,$op,$value,@rest) = @_;
220 # SQL::Statement changes != to <>. (Can we remove this now?)
221 $op = "!=" if $op eq "<>";
223 die "Invalid Operation: $op for $field"
224 unless $op eq "=" or $op eq "!=";
226 my $meta = $FIELDS{$field};
227 if (defined $meta->[1]) {
228 my $class = "RT::" . $meta->[1];
229 my $o = $class->new($sb->CurrentUser);
233 $sb->_SQLLimit( FIELD => $field,,
242 Handle fields where the values are limited to integers. (For example,
243 Priority, TimeWorked.)
251 my ($sb,$field,$op,$value,@rest) = @_;
253 die "Invalid Operator $op for $field"
254 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
267 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
270 1: Direction (From,To)
271 2: Relationship Type (MemberOf, DependsOn,RefersTo)
276 my ($sb,$field,$op,$value,@rest) = @_;
281 my $meta = $FIELDS{$field};
282 die "Incorrect Meta Data for $field"
283 unless (defined $meta->[1] and defined $meta->[2]);
285 my $LinkAlias = $sb->NewAlias ('Links');
297 if ($meta->[1] eq "To") {
298 my $matchfield = ( $value =~ /^(\d+)$/ ? "LocalTarget" : "Target" );
302 ENTRYAGGREGATOR => 'AND',
303 FIELD => $matchfield,
308 #If we're searching on target, join the base to ticket.id
309 $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
310 ALIAS2 => $LinkAlias, FIELD2 => 'LocalBase');
312 } elsif ( $meta->[1] eq "From" ) {
313 my $matchfield = ( $value =~ /^(\d+)$/ ? "LocalBase" : "Base" );
317 ENTRYAGGREGATOR => 'AND',
318 FIELD => $matchfield,
323 #If we're searching on base, join the target to ticket.id
324 $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
325 ALIAS2 => $LinkAlias, FIELD2 => 'LocalTarget');
328 die "Invalid link direction '$meta->[1]' for $field\n";
337 Handle date fields. (Created, LastTold..)
340 1: type of relationship. (Probably not necessary.)
345 my ($sb,$field,$op,$value,@rest) = @_;
347 die "Invalid Date Op: $op"
348 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
350 my $meta = $FIELDS{$field};
351 die "Incorrect Meta Data for $field"
352 unless (defined $meta->[1]);
354 require Time::ParseDate;
355 use POSIX 'strftime';
357 my $time = Time::ParseDate::parsedate( $value,
358 UK => $RT::DateDayBeforeMonth,
359 PREFER_PAST => $RT::AmbiguousDayInPast,
360 PREFER_FUTURE => !($RT::AmbiguousDayInPast));
361 $value = strftime("%Y-%m-%d %H:%M",localtime($time));
373 Handle simple fields which are just strings. (Subject,Type)
381 my ($sb,$field,$op,$value,@rest) = @_;
385 # =, !=, LIKE, NOT LIKE
396 =head2 _TransDateLimit
398 Handle fields limiting based on Transaction Date.
400 The inpupt value must be in a format parseable by Time::ParseDate
407 sub _TransDateLimit {
408 my ($sb,$field,$op,$value,@rest) = @_;
410 # See the comments for TransLimit, they apply here too
412 $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
413 unless defined $sb->{_sql_transalias};
414 $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
415 unless defined $sb->{_sql_trattachalias};
419 # Join Transactions To Attachments
420 $sb->Join( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
421 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
423 # Join Transactions to Tickets
424 $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
425 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
427 my $d = new RT::Date( $sb->CurrentUser );
431 #Search for the right field
432 $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
445 Limit based on the Content of a transaction or the ContentType.
453 # Content, ContentType, Filename
455 # If only this was this simple. We've got to do something
458 #Basically, we want to make sure that the limits apply to
459 #the same attachment, rather than just another attachment
460 #for the same ticket, no matter how many clauses we lump
461 #on. We put them in TicketAliases so that they get nuked
462 #when we redo the join.
464 # In the SQL, we might have
465 # (( Content = foo ) or ( Content = bar AND Content = baz ))
466 # The AND group should share the same Alias.
468 # Actually, maybe it doesn't matter. We use the same alias and it
469 # works itself out? (er.. different.)
471 # Steal more from _ProcessRestrictions
473 # FIXME: Maybe look at the previous FooLimit call, and if it was a
474 # TransLimit and EntryAggregator == AND, reuse the Aliases?
476 # Or better - store the aliases on a per subclause basis - since
477 # those are going to be the things we want to relate to each other,
480 # maybe we should not allow certain kinds of aggregation of these
481 # clauses and do a psuedo regex instead? - the problem is getting
482 # them all into the same subclause when you have (A op B op C) - the
483 # way they get parsed in the tree they're in different subclauses.
485 my ($sb,$field,$op,$value,@rest) = @_;
487 $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
488 unless defined $sb->{_sql_transalias};
489 $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
490 unless defined $sb->{_sql_trattachalias};
494 # Join Transactions To Attachments
495 $sb->Join( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
496 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
498 # Join Transactions to Tickets
499 $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
500 ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
502 #Search for the right field
503 $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
517 Handle watcher limits. (Requestor, CC, etc..)
525 my ($self,$field,$op,$value,@rest) = @_;
530 my $groups = $self->NewAlias('Groups');
531 my $group_princs = $self->NewAlias('Principals');
532 my $groupmembers = $self->NewAlias('CachedGroupMembers');
533 my $member_princs = $self->NewAlias('Principals');
534 my $users = $self->NewAlias('Users');
538 # my $subclause = undef;
539 # my $aggregator = 'OR';
540 # if ($restriction->{'OPERATOR'} =~ /!|NOT/i ){
541 # $subclause = 'AndEmailIsNot';
542 # $aggregator = 'AND';
546 $self->_SQLLimit(ALIAS => $users,
547 FIELD => $rest{SUBKEY} || 'EmailAddress',
554 # {{{ Tie to groups for tickets we care about
555 $self->_SQLLimit(ALIAS => $groups,
557 VALUE => 'RT::Ticket-Role',
558 ENTRYAGGREGATOR => 'AND');
560 $self->Join(ALIAS1 => $groups, FIELD1 => 'Instance',
561 ALIAS2 => 'main', FIELD2 => 'id');
564 # If we care about which sort of watcher
565 my $meta = $FIELDS{$field};
566 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
569 $self->_SQLLimit(ALIAS => $groups,
572 ENTRYAGGREGATOR => 'AND');
575 $self->Join (ALIAS1 => $groups, FIELD1 => 'id',
576 ALIAS2 => $group_princs, FIELD2 => 'ObjectId');
577 $self->_SQLLimit(ALIAS => $group_princs,
578 FIELD => 'PrincipalType',
580 ENTRYAGGREGATOR => 'AND');
581 $self->Join( ALIAS1 => $group_princs, FIELD1 => 'id',
582 ALIAS2 => $groupmembers, FIELD2 => 'GroupId');
584 $self->Join( ALIAS1 => $groupmembers, FIELD1 => 'MemberId',
585 ALIAS2 => $member_princs, FIELD2 => 'id');
586 $self->Join (ALIAS1 => $member_princs, FIELD1 => 'ObjectId',
587 ALIAS2 => $users, FIELD2 => 'id');
593 sub _LinkFieldLimit {
598 if ($restriction->{'TYPE'}) {
599 $self->SUPER::Limit(ALIAS => $LinkAlias,
600 ENTRYAGGREGATOR => 'AND',
603 VALUE => $restriction->{'TYPE'} );
606 #If we're trying to limit it to things that are target of
607 if ($restriction->{'TARGET'}) {
608 # If the TARGET is an integer that means that we want to look at
609 # the LocalTarget field. otherwise, we want to look at the
612 if ($restriction->{'TARGET'} =~/^(\d+)$/) {
613 $matchfield = "LocalTarget";
615 $matchfield = "Target";
617 $self->SUPER::Limit(ALIAS => $LinkAlias,
618 ENTRYAGGREGATOR => 'AND',
619 FIELD => $matchfield,
621 VALUE => $restriction->{'TARGET'} );
622 #If we're searching on target, join the base to ticket.id
623 $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
624 ALIAS2 => $LinkAlias,
625 FIELD2 => 'LocalBase');
627 #If we're trying to limit it to things that are base of
628 elsif ($restriction->{'BASE'}) {
629 # If we're trying to match a numeric link, we want to look at
630 # LocalBase, otherwise we want to look at "Base"
632 if ($restriction->{'BASE'} =~/^(\d+)$/) {
633 $matchfield = "LocalBase";
635 $matchfield = "Base";
638 $self->SUPER::Limit(ALIAS => $LinkAlias,
639 ENTRYAGGREGATOR => 'AND',
640 FIELD => $matchfield,
642 VALUE => $restriction->{'BASE'} );
643 #If we're searching on base, join the target to ticket.id
644 $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
645 ALIAS2 => $LinkAlias,
646 FIELD2 => 'LocalTarget')
653 Limit based on Keywords
660 sub _CustomFieldLimit {
661 my ($self,$_field,$op,$value,@rest) = @_;
664 my $field = $rest{SUBKEY} || die "No field specified";
666 # For our sanity, we can only limit on one queue at a time
668 # Ugh. This will not do well for things with underscores in them
670 use RT::CustomFields;
671 my $CF = RT::CustomFields->new( $self->CurrentUser );
672 #$CF->Load( $cfid} );
675 if ($field =~ /^(.+?)\.{(.+)}$/) {
676 my $q = RT::Queue->new($self->CurrentUser);
679 $CF->LimitToQueue( $q->Id );
688 while ( my $CustomField = $CF->Next ) {
689 if ($CustomField->Name eq $field) {
690 $cfid = $CustomField->Id;
694 die "No custom field named $field found\n"
697 # use RT::CustomFields;
698 # my $CF = RT::CustomField->new( $self->CurrentUser );
699 # $CF->Load( $cfid );
703 my $TicketCFs = $self->Join( TYPE => 'left',
706 TABLE2 => 'TicketCustomFieldValues',
707 FIELD2 => 'Ticket' );
711 $self->_SQLLimit( ALIAS => $TicketCFs,
719 or ( $op eq '!=' ) ) {
720 $null_columns_ok = 1;
723 #If we're trying to find tickets where the keyword isn't somethng,
724 #also check ones where it _IS_ null
727 $self->_SQLLimit( ALIAS => $TicketCFs,
732 ENTRYAGGREGATOR => 'OR', );
735 $self->_SQLLimit( LEFTJOIN => $TicketCFs,
736 FIELD => 'CustomField',
738 ENTRYAGGREGATOR => 'OR' );
747 # End Helper Functions
749 # End of SQL Stuff -------------------------------------------------
751 # {{{ Limit the result set based on content
757 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
758 Generally best called from LimitFoo methods
763 my %args = ( FIELD => undef,
766 DESCRIPTION => undef,
769 $args{'DESCRIPTION'} = $self->loc(
770 "[_1] [_2] [_3]", $args{'FIELD'}, $args{'OPERATOR'}, $args{'VALUE'}
771 ) if (!defined $args{'DESCRIPTION'}) ;
773 my $index = $self->_NextIndex;
775 #make the TicketRestrictions hash the equivalent of whatever we just passed in;
777 %{$self->{'TicketRestrictions'}{$index}} = %args;
779 $self->{'RecalcTicketLimits'} = 1;
781 # If we're looking at the effective id, we don't want to append the other clause
782 # which limits us to tickets where id = effective id
783 if ($args{'FIELD'} eq 'EffectiveId') {
784 $self->{'looking_at_effective_id'} = 1;
787 if ($args{'FIELD'} eq 'Type') {
788 $self->{'looking_at_type'} = 1;
801 Returns a frozen string suitable for handing back to ThawLimits.
804 # {{{ sub FreezeLimits
809 return (FreezeThaw::freeze($self->{'TicketRestrictions'},
810 $self->{'restriction_index'}
818 Take a frozen Limits string generated by FreezeLimits and make this tickets
819 object have that set of limits.
828 #if we don't have $in, get outta here.
829 return undef unless ($in);
831 $self->{'RecalcTicketLimits'} = 1;
835 #We don't need to die if the thaw fails.
838 ($self->{'TicketRestrictions'},
839 $self->{'restriction_index'}
840 ) = FreezeThaw::thaw($in);
847 # {{{ Limit by enum or foreign key
853 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
854 OPERATOR is one of = or !=. (It defaults to =).
855 VALUE is a queue id or Name.
862 my %args = (VALUE => undef,
866 #TODO VALUE should also take queue names and queue objects
867 #TODO FIXME why are we canonicalizing to name, not id, robrt?
868 if ($args{VALUE} =~ /^\d+$/) {
869 my $queue = new RT::Queue($self->CurrentUser);
870 $queue->Load($args{'VALUE'});
871 $args{VALUE} = $queue->Name;
874 # What if they pass in an Id? Check for isNum() and convert to
877 #TODO check for a valid queue here
879 $self->Limit (FIELD => 'Queue',
880 VALUE => $args{VALUE},
881 OPERATOR => $args{'OPERATOR'},
883 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE},
890 # {{{ sub LimitStatus
894 Takes a paramhash with the fields OPERATOR and VALUE.
895 OPERATOR is one of = or !=.
902 my %args = ( OPERATOR => '=',
904 $self->Limit (FIELD => 'Status',
905 VALUE => $args{'VALUE'},
906 OPERATOR => $args{'OPERATOR'},
908 ' ', $self->loc('Status'), $args{'OPERATOR'}, $self->loc($args{'VALUE'})
919 If called, this search will not automatically limit the set of results found
920 to tickets of type "Ticket". Tickets of other types, such as "project" and
921 "approval" will be found.
928 # Instead of faking a Limit that later gets ignored, fake up the
929 # fact that we're already looking at type, so that the check in
930 # Tickets_Overlay_SQL/FromSQL goes down the right branch
932 # $self->LimitType(VALUE => '__any');
933 $self->{looking_at_type} = 1;
942 Takes a paramhash with the fields OPERATOR and VALUE.
943 OPERATOR is one of = or !=, it defaults to "=".
944 VALUE is a string to search for in the type of the ticket.
952 my %args = (OPERATOR => '=',
955 $self->Limit (FIELD => 'Type',
956 VALUE => $args{'VALUE'},
957 OPERATOR => $args{'OPERATOR'},
959 ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'},
968 # {{{ Limit by string field
970 # {{{ sub LimitSubject
974 Takes a paramhash with the fields OPERATOR and VALUE.
975 OPERATOR is one of = or !=.
976 VALUE is a string to search for in the subject of the ticket.
983 $self->Limit (FIELD => 'Subject',
984 VALUE => $args{'VALUE'},
985 OPERATOR => $args{'OPERATOR'},
987 ' ', $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'},
996 # {{{ Limit based on ticket numerical attributes
997 # Things that can be > < = !=
1003 Takes a paramhash with the fields OPERATOR and VALUE.
1004 OPERATOR is one of =, >, < or !=.
1005 VALUE is a ticket Id to search for
1011 my %args = (OPERATOR => '=',
1014 $self->Limit (FIELD => 'id',
1015 VALUE => $args{'VALUE'},
1016 OPERATOR => $args{'OPERATOR'},
1017 DESCRIPTION => join(
1018 ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'},
1025 # {{{ sub LimitPriority
1027 =head2 LimitPriority
1029 Takes a paramhash with the fields OPERATOR and VALUE.
1030 OPERATOR is one of =, >, < or !=.
1031 VALUE is a value to match the ticket\'s priority against
1038 $self->Limit (FIELD => 'Priority',
1039 VALUE => $args{'VALUE'},
1040 OPERATOR => $args{'OPERATOR'},
1041 DESCRIPTION => join(
1042 ' ', $self->loc('Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1049 # {{{ sub LimitInitialPriority
1051 =head2 LimitInitialPriority
1053 Takes a paramhash with the fields OPERATOR and VALUE.
1054 OPERATOR is one of =, >, < or !=.
1055 VALUE is a value to match the ticket\'s initial priority against
1060 sub LimitInitialPriority {
1063 $self->Limit (FIELD => 'InitialPriority',
1064 VALUE => $args{'VALUE'},
1065 OPERATOR => $args{'OPERATOR'},
1066 DESCRIPTION => join(
1067 ' ', $self->loc('Initial Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1074 # {{{ sub LimitFinalPriority
1076 =head2 LimitFinalPriority
1078 Takes a paramhash with the fields OPERATOR and VALUE.
1079 OPERATOR is one of =, >, < or !=.
1080 VALUE is a value to match the ticket\'s final priority against
1084 sub LimitFinalPriority {
1087 $self->Limit (FIELD => 'FinalPriority',
1088 VALUE => $args{'VALUE'},
1089 OPERATOR => $args{'OPERATOR'},
1090 DESCRIPTION => join(
1091 ' ', $self->loc('Final Priority'), $args{'OPERATOR'}, $args{'VALUE'},
1098 # {{{ sub LimitTimeWorked
1100 =head2 LimitTimeWorked
1102 Takes a paramhash with the fields OPERATOR and VALUE.
1103 OPERATOR is one of =, >, < or !=.
1104 VALUE is a value to match the ticket's TimeWorked attribute
1108 sub LimitTimeWorked {
1111 $self->Limit (FIELD => 'TimeWorked',
1112 VALUE => $args{'VALUE'},
1113 OPERATOR => $args{'OPERATOR'},
1114 DESCRIPTION => join(
1115 ' ', $self->loc('Time worked'), $args{'OPERATOR'}, $args{'VALUE'},
1122 # {{{ sub LimitTimeLeft
1124 =head2 LimitTimeLeft
1126 Takes a paramhash with the fields OPERATOR and VALUE.
1127 OPERATOR is one of =, >, < or !=.
1128 VALUE is a value to match the ticket's TimeLeft attribute
1135 $self->Limit (FIELD => 'TimeLeft',
1136 VALUE => $args{'VALUE'},
1137 OPERATOR => $args{'OPERATOR'},
1138 DESCRIPTION => join(
1139 ' ', $self->loc('Time left'), $args{'OPERATOR'}, $args{'VALUE'},
1148 # {{{ Limiting based on attachment attributes
1150 # {{{ sub LimitContent
1154 Takes a paramhash with the fields OPERATOR and VALUE.
1155 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1156 VALUE is a string to search for in the body of the ticket
1162 $self->Limit (FIELD => 'Content',
1163 VALUE => $args{'VALUE'},
1164 OPERATOR => $args{'OPERATOR'},
1165 DESCRIPTION => join(
1166 ' ', $self->loc('Ticket content'), $args{'OPERATOR'}, $args{'VALUE'},
1173 # {{{ sub LimitFilename
1175 =head2 LimitFilename
1177 Takes a paramhash with the fields OPERATOR and VALUE.
1178 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1179 VALUE is a string to search for in the body of the ticket
1185 $self->Limit (FIELD => 'Filename',
1186 VALUE => $args{'VALUE'},
1187 OPERATOR => $args{'OPERATOR'},
1188 DESCRIPTION => join(
1189 ' ', $self->loc('Attachment filename'), $args{'OPERATOR'}, $args{'VALUE'},
1195 # {{{ sub LimitContentType
1197 =head2 LimitContentType
1199 Takes a paramhash with the fields OPERATOR and VALUE.
1200 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1201 VALUE is a content type to search ticket attachments for
1205 sub LimitContentType {
1208 $self->Limit (FIELD => 'ContentType',
1209 VALUE => $args{'VALUE'},
1210 OPERATOR => $args{'OPERATOR'},
1211 DESCRIPTION => join(
1212 ' ', $self->loc('Ticket content type'), $args{'OPERATOR'}, $args{'VALUE'},
1220 # {{{ Limiting based on people
1222 # {{{ sub LimitOwner
1226 Takes a paramhash with the fields OPERATOR and VALUE.
1227 OPERATOR is one of = or !=.
1234 my %args = ( OPERATOR => '=',
1237 my $owner = new RT::User($self->CurrentUser);
1238 $owner->Load($args{'VALUE'});
1239 # FIXME: check for a valid $owner
1240 $self->Limit (FIELD => 'Owner',
1241 VALUE => $args{'VALUE'},
1242 OPERATOR => $args{'OPERATOR'},
1243 DESCRIPTION => join(
1244 ' ', $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(),
1252 # {{{ Limiting watchers
1254 # {{{ sub LimitWatcher
1259 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1260 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1261 VALUE is a value to match the ticket\'s watcher email addresses against
1262 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1266 my $t1 = RT::Ticket->new($RT::SystemUser);
1267 $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
1275 my %args = ( OPERATOR => '=',
1281 #build us up a description
1282 my ($watcher_type, $desc);
1283 if ($args{'TYPE'}) {
1284 $watcher_type = $args{'TYPE'};
1287 $watcher_type = "Watcher";
1290 $self->Limit (FIELD => $watcher_type,
1291 VALUE => $args{'VALUE'},
1292 OPERATOR => $args{'OPERATOR'},
1293 TYPE => $args{'TYPE'},
1294 DESCRIPTION => join(
1295 ' ', $self->loc($watcher_type), $args{'OPERATOR'}, $args{'VALUE'},
1301 sub LimitRequestor {
1304 my ($package, $filename, $line) = caller;
1305 $RT::Logger->error("Tickets->LimitRequestor is deprecated. please rewrite call at $package - $filename: $line");
1306 $self->LimitWatcher(TYPE => 'Requestor', @_);
1317 # {{{ Limiting based on links
1321 =head2 LimitLinkedTo
1323 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1324 TYPE limits the sort of relationship we want to search on
1326 TYPE = { RefersTo, MemberOf, DependsOn }
1328 TARGET is the id or URI of the TARGET of the link
1329 (TARGET used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as TARGET
1342 FIELD => 'LinkedTo',
1344 TARGET => ($args{'TARGET'} || $args{'TICKET'}),
1345 TYPE => $args{'TYPE'},
1346 DESCRIPTION => $self->loc(
1347 "Tickets [_1] by [_2]", $self->loc($args{'TYPE'}), ($args{'TARGET'} || $args{'TICKET'})
1355 # {{{ LimitLinkedFrom
1357 =head2 LimitLinkedFrom
1359 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1360 TYPE limits the sort of relationship we want to search on
1363 BASE is the id or URI of the BASE of the link
1364 (BASE used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as BASE
1369 sub LimitLinkedFrom {
1371 my %args = ( BASE => undef,
1377 $self->Limit( FIELD => 'LinkedTo',
1379 BASE => ($args{'BASE'} || $args{'TICKET'}),
1380 TYPE => $args{'TYPE'},
1381 DESCRIPTION => $self->loc(
1382 "Tickets [_1] [_2]", $self->loc($args{'TYPE'}), ($args{'BASE'} || $args{'TICKET'})
1393 my $ticket_id = shift;
1394 $self->LimitLinkedTo ( TARGET=> "$ticket_id",
1401 # {{{ LimitHasMember
1402 sub LimitHasMember {
1404 my $ticket_id =shift;
1405 $self->LimitLinkedFrom ( BASE => "$ticket_id",
1406 TYPE => 'HasMember',
1412 # {{{ LimitDependsOn
1414 sub LimitDependsOn {
1416 my $ticket_id = shift;
1417 $self->LimitLinkedTo ( TARGET => "$ticket_id",
1418 TYPE => 'DependsOn',
1425 # {{{ LimitDependedOnBy
1427 sub LimitDependedOnBy {
1429 my $ticket_id = shift;
1430 $self->LimitLinkedFrom ( BASE => "$ticket_id",
1431 TYPE => 'DependentOn',
1443 my $ticket_id = shift;
1444 $self->LimitLinkedTo ( TARGET => "$ticket_id",
1452 # {{{ LimitReferredToBy
1454 sub LimitReferredToBy {
1456 my $ticket_id = shift;
1457 $self->LimitLinkedFrom ( BASE=> "$ticket_id",
1458 TYPE => 'ReferredTo',
1467 # {{{ limit based on ticket date attribtes
1471 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
1473 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
1475 OPERATOR is one of > or <
1476 VALUE is a date and time in ISO format in GMT
1477 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
1479 There are also helper functions of the form LimitFIELD that eliminate
1480 the need to pass in a FIELD argument.
1493 #Set the description if we didn't get handed it above
1494 unless ($args{'DESCRIPTION'} ) {
1495 $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
1498 $self->Limit (%args);
1509 $self->LimitDate( FIELD => 'Created', @_);
1513 $self->LimitDate( FIELD => 'Due', @_);
1518 $self->LimitDate( FIELD => 'Starts', @_);
1523 $self->LimitDate( FIELD => 'Started', @_);
1527 $self->LimitDate( FIELD => 'Resolved', @_);
1531 $self->LimitDate( FIELD => 'Told', @_);
1533 sub LimitLastUpdated {
1535 $self->LimitDate( FIELD => 'LastUpdated', @_);
1538 # {{{ sub LimitTransactionDate
1540 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
1542 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
1544 OPERATOR is one of > or <
1545 VALUE is a date and time in ISO format in GMT
1550 sub LimitTransactionDate {
1553 FIELD => 'TransactionDate',
1559 # <20021217042756.GK28744@pallas.fsck.com>
1560 # "Kill It" - Jesse.
1562 #Set the description if we didn't get handed it above
1563 unless ($args{'DESCRIPTION'} ) {
1564 $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
1567 $self->Limit (%args);
1575 # {{{ Limit based on custom fields
1576 # {{{ sub LimitCustomField
1578 =head2 LimitCustomField
1580 Takes a paramhash of key/value pairs with the following keys:
1584 =item KEYWORDSELECT - KeywordSelect id
1586 =item OPERATOR - (for KEYWORD only - KEYWORDSELECT operator is always `=')
1588 =item KEYWORD - Keyword id
1594 sub LimitCustomField {
1596 my %args = ( VALUE => undef,
1597 CUSTOMFIELD => undef,
1599 DESCRIPTION => undef,
1600 FIELD => 'CustomFieldValue',
1604 use RT::CustomFields;
1605 my $CF = RT::CustomField->new( $self->CurrentUser );
1606 $CF->Load( $args{CUSTOMFIELD} );
1608 #If we are looking to compare with a null value.
1609 if ( $args{'OPERATOR'} =~ /^is$/i ) {
1610 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has no value.", $CF->Name);
1612 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
1613 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has a value.", $CF->Name);
1616 # if we're not looking to compare with a null value
1618 $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] [_2] [_3]", $CF->Name , $args{OPERATOR} , $args{VALUE});
1621 # my $index = $self->_NextIndex;
1622 # %{ $self->{'TicketRestrictions'}{$index} } = %args;
1627 my $qo = new RT::Queue( $self->CurrentUser );
1628 $qo->load( $CF->Queue );
1632 $self->Limit( VALUE => $args{VALUE},
1634 ? $q . ".{" . $CF->Name . "}"
1637 OPERATOR => $args{OPERATOR},
1642 $self->{'RecalcTicketLimits'} = 1;
1650 # {{{ sub _NextIndex
1654 Keep track of the counter for the array of restrictions
1660 return ($self->{'restriction_index'}++);
1666 # {{{ Core bits to make this a DBIx::SearchBuilder object
1671 $self->{'table'} = "Tickets";
1672 $self->{'RecalcTicketLimits'} = 1;
1673 $self->{'looking_at_effective_id'} = 0;
1674 $self->{'looking_at_type'} = 0;
1675 $self->{'restriction_index'} =1;
1676 $self->{'primary_key'} = "id";
1677 delete $self->{'items_array'};
1678 delete $self->{'item_map'};
1679 $self->SUPER::_Init(@_);
1689 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1690 return($self->SUPER::Count());
1697 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1698 return($self->SUPER::CountAll());
1703 # {{{ sub ItemsArrayRef
1705 =head2 ItemsArrayRef
1707 Returns a reference to the set of all items found in this search
1715 unless ( $self->{'items_array'} ) {
1717 my $placeholder = $self->_ItemsCounter;
1718 $self->GotoFirstItem();
1719 while ( my $item = $self->Next ) {
1720 push ( @{ $self->{'items_array'} }, $item );
1722 $self->GotoItem($placeholder);
1724 return ( $self->{'items_array'} );
1732 $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
1734 my $Ticket = $self->SUPER::Next();
1735 if ((defined($Ticket)) and (ref($Ticket))) {
1737 #Make sure we _never_ show deleted tickets
1738 #TODO we should be doing this in the where clause.
1739 #but you can't do multiple clauses on the same field just yet :/
1741 if ($Ticket->__Value('Status') eq 'deleted') {
1742 return($self->Next());
1744 # Since Ticket could be granted with more rights instead
1745 # of being revoked, it's ok if queue rights allow
1746 # ShowTicket. It seems need another query, but we have
1747 # rights cache in Principal::HasRight.
1748 elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket') ||
1749 $Ticket->CurrentUserHasRight('ShowTicket')) {
1753 #If the user doesn't have the right to show this ticket
1755 return($self->Next());
1758 #if there never was any ticket
1768 # {{{ Deal with storing and restoring restrictions
1770 # {{{ sub LoadRestrictions
1772 =head2 LoadRestrictions
1774 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
1775 TODO It is not yet implemented
1781 # {{{ sub DescribeRestrictions
1783 =head2 DescribeRestrictions
1786 Returns a hash keyed by restriction id.
1787 Each element of the hash is currently a one element hash that contains DESCRIPTION which
1788 is a description of the purpose of that TicketRestriction
1792 sub DescribeRestrictions {
1795 my ($row, %listing);
1797 foreach $row (keys %{$self->{'TicketRestrictions'}}) {
1798 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
1804 # {{{ sub RestrictionValues
1806 =head2 RestrictionValues FIELD
1808 Takes a restriction field and returns a list of values this field is restricted
1813 sub RestrictionValues {
1816 map $self->{'TicketRestrictions'}{$_}{'VALUE'},
1818 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
1819 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
1821 keys %{$self->{'TicketRestrictions'}};
1826 # {{{ sub ClearRestrictions
1828 =head2 ClearRestrictions
1830 Removes all restrictions irretrievably
1834 sub ClearRestrictions {
1836 delete $self->{'TicketRestrictions'};
1837 $self->{'looking_at_effective_id'} = 0;
1838 $self->{'looking_at_type'} = 0;
1839 $self->{'RecalcTicketLimits'} =1;
1844 # {{{ sub DeleteRestriction
1846 =head2 DeleteRestriction
1848 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
1849 Removes that restriction from the session's limits.
1854 sub DeleteRestriction {
1857 delete $self->{'TicketRestrictions'}{$row};
1859 $self->{'RecalcTicketLimits'} = 1;
1860 #make the underlying easysearch object forget all its preconceptions
1865 # {{{ sub _RestrictionsToClauses
1867 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
1869 sub _RestrictionsToClauses {
1874 foreach $row (keys %{$self->{'TicketRestrictions'}}) {
1875 my $restriction = $self->{'TicketRestrictions'}{$row};
1877 #print Dumper($restriction),"\n";
1879 # We need to reimplement the subclause aggregation that SearchBuilder does.
1880 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
1881 # Then SB AND's the different Subclauses together.
1883 # So, we want to group things into Subclauses, convert them to
1884 # SQL, and then join them with the appropriate DefaultEA.
1885 # Then join each subclause group with AND.
1887 my $field = $restriction->{'FIELD'};
1888 my $realfield = $field; # CustomFields fake up a fieldname, so
1889 # we need to figure that out
1892 # Rewrite LinkedTo meta field to the real field
1893 if ($field =~ /LinkedTo/) {
1894 $realfield = $field = $restriction->{'TYPE'};
1898 # CustomFields have a different real field
1899 if ($field =~ /^CF\./) {
1903 die "I don't know about $field yet"
1904 unless (exists $FIELDS{$realfield} or $restriction->{CUSTOMFIELD});
1906 my $type = $FIELDS{$realfield}->[0];
1907 my $op = $restriction->{'OPERATOR'};
1909 my $value = ( grep { defined }
1910 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET))[0];
1912 # this performs the moral equivalent of defined or/dor/C<//>,
1913 # without the short circuiting.You need to use a 'defined or'
1914 # type thing instead of just checking for truth values, because
1915 # VALUE could be 0.(i.e. "false")
1917 # You could also use this, but I find it less aesthetic:
1918 # (although it does short circuit)
1919 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
1920 # defined $restriction->{'TICKET'} ?
1921 # $restriction->{TICKET} :
1922 # defined $restriction->{'BASE'} ?
1923 # $restriction->{BASE} :
1924 # defined $restriction->{'TARGET'} ?
1925 # $restriction->{TARGET} )
1927 my $ea = $DefaultEA{$type};
1929 die "Invalid operator $op for $field ($type)"
1930 unless exists $ea->{$op};
1933 exists $clause{$realfield} or $clause{$realfield} = [];
1935 $field =~ s!(['"])!\\$1!g;
1936 $value =~ s!(['"])!\\$1!g;
1937 my $data = [ $ea, $type, $field, $op, $value ];
1939 # here is where we store extra data, say if it's a keyword or
1940 # something. (I.e. "TYPE SPECIFIC STUFF")
1942 #print Dumper($data);
1943 push @{$clause{$realfield}}, $data;
1950 # {{{ sub _ProcessRestrictions
1952 =head2 _ProcessRestrictions PARAMHASH
1954 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
1955 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
1959 sub _ProcessRestrictions {
1962 #Blow away ticket aliases since we'll need to regenerate them for
1964 delete $self->{'TicketAliases'};
1965 delete $self->{'items_array'};
1966 my $sql = $self->{_sql_query}; # Violating the _SQL namespace
1967 if (!$sql||$self->{'RecalcTicketLimits'}) {
1968 # "Restrictions to Clauses Branch\n";
1969 my $clauseRef = eval { $self->_RestrictionsToClauses; };
1971 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
1974 $sql = $self->ClausesToSQL($clauseRef);
1975 $self->FromSQL($sql);
1980 $self->{'RecalcTicketLimits'} = 0;
1984 =head2 _BuildItemMap
1986 # Build up a map of first/last/next/prev items, so that we can display search nav quickly
1993 my $items = $self->ItemsArrayRef;
1996 delete $self->{'item_map'};
1998 $self->{'item_map'}->{'first'} = $items->[0]->Id;
1999 while (my $item = shift @$items ) {
2001 $self->{'item_map'}->{$id}->{'defined'} = 1;
2002 $self->{'item_map'}->{$id}->{prev} = $prev;
2003 $self->{'item_map'}->{$id}->{next} = $items->[0]->Id if ($items->[0]);
2006 $self->{'item_map'}->{'last'} = $prev;
2013 Returns an a map of all items found by this search. The map is of the form
2015 $ItemMap->{'first'} = first ticketid found
2016 $ItemMap->{'last'} = last ticketid found
2017 $ItemMap->{$id}->{prev} = the tikcet id found before $id
2018 $ItemMap->{$id}->{next} = the tikcet id found after $id
2024 $self->_BuildItemMap() unless ($self->{'item_map'});
2025 return ($self->{'item_map'});
2041 =head2 PrepForSerialization
2043 You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
2048 sub PrepForSerialization {
2050 delete $self->{'items'};
2051 $self->RedoSearch();