#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Tickets.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $ =head1 NAME RT::Tickets - A collection of Ticket objects =head1 SYNOPSIS use RT::Tickets; my $tickets = new RT::Tickets($CurrentUser); =head1 DESCRIPTION A collection of RT::Tickets. =head1 METHODS =begin testing ok (require RT::TestHarness); ok (require RT::Tickets); =end testing =cut package RT::Tickets; use RT::EasySearch; use RT::Ticket; @ISA= qw(RT::EasySearch); use vars qw(%TYPES @SORTFIELDS); # {{{ TYPES %TYPES = ( Status => 'ENUM', Queue => 'ENUM', Type => 'ENUM', Creator => 'ENUM', LastUpdatedBy => 'ENUM', Owner => 'ENUM', EffectiveId => 'INT', id => 'INT', InitialPriority => 'INT', FinalPriority => 'INT', Priority => 'INT', TimeLeft => 'INT', TimeWorked => 'INT', MemberOf => 'LINK', DependsOn => 'LINK', HasMember => 'LINK', HasDepender => 'LINK', RelatedTo => 'LINK', Told => 'DATE', StartsBy => 'DATE', Started => 'DATE', Due => 'DATE', Resolved => 'DATE', LastUpdated => 'DATE', Created => 'DATE', Subject => 'STRING', Type => 'STRING', Content => 'TRANSFIELD', ContentType => 'TRANSFIELD', TransactionDate => 'TRANSDATE', Watcher => 'WATCHERFIELD', LinkedTo => 'LINKFIELD', Keyword => 'KEYWORDFIELD' ); # }}} # {{{ sub SortFields @SORTFIELDS = qw(id Status Owner Created Due Starts Started Queue Subject Told Started Resolved LastUpdated Priority TimeWorked TimeLeft); =head2 SortFields Returns the list of fields that lists of tickets can easily be sorted by =cut sub SortFields { my $self = shift; return(@SORTFIELDS); } # }}} # {{{ Limit the result set based on content # {{{ sub Limit =head2 Limit Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION Generally best called from LimitFoo methods =cut sub Limit { my $self = shift; my %args = ( FIELD => undef, OPERATOR => '=', VALUE => undef, DESCRIPTION => undef, @_ ); $args{'DESCRIPTION'} = "Autodescribed: ".$args{'FIELD'} . $args{'OPERATOR'} . $args{'VALUE'}, if (!defined $args{'DESCRIPTION'}) ; my $index = $self->_NextIndex; #make the TicketRestrictions hash the equivalent of whatever we just passed in; %{$self->{'TicketRestrictions'}{$index}} = %args; $self->{'RecalcTicketLimits'} = 1; # If we're looking at the effective id, we don't want to append the other clause # which limits us to tickets where id = effective id if ($args{'FIELD'} eq 'EffectiveId') { $self->{'looking_at_effective_id'} = 1; } return ($index); } # }}} =head2 FreezeLimits Returns a frozen string suitable for handing back to ThawLimits. =cut # {{{ sub FreezeLimits sub FreezeLimits { my $self = shift; require FreezeThaw; return (FreezeThaw::freeze($self->{'TicketRestrictions'}, $self->{'restriction_index'} )); } # }}} =head2 ThawLimits Take a frozen Limits string generated by FreezeLimits and make this tickets object have that set of limits. =cut # {{{ sub ThawLimits sub ThawLimits { my $self = shift; my $in = shift; #if we don't have $in, get outta here. return undef unless ($in); $self->{'RecalcTicketLimits'} = 1; require FreezeThaw; #We don't need to die if the thaw fails. eval { ($self->{'TicketRestrictions'}, $self->{'restriction_index'} ) = FreezeThaw::thaw($in); } } # }}} # {{{ Limit by enum or foreign key # {{{ sub LimitQueue =head2 LimitQueue LimitQueue takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of = or !=. (It defaults to =). VALUE is a queue id. =cut sub LimitQueue { my $self = shift; my %args = (VALUE => undef, OPERATOR => '=', @_); #TODO VALUE should also take queue names and queue objects my $queue = new RT::Queue($self->CurrentUser); $queue->Load($args{'VALUE'}); #TODO check for a valid queue here $self->Limit (FIELD => 'Queue', VALUE => $queue->id(), OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Queue ' . $args{'OPERATOR'}. " ". $queue->Name ); } # }}} # {{{ sub LimitStatus =head2 LimitStatus Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of = or !=. VALUE is a status. =cut sub LimitStatus { my $self = shift; my %args = ( OPERATOR => '=', @_); $self->Limit (FIELD => 'Status', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Status ' . $args{'OPERATOR'}. " ". $args{'VALUE'}, ); } # }}} # {{{ sub LimitType =head2 LimitType Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of = or !=, it defaults to "=". VALUE is a string to search for in the type of the ticket. =cut sub LimitType { my $self = shift; my %args = (OPERATOR => '=', VALUE => undef, @_); $self->Limit (FIELD => 'Type', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Type ' . $args{'OPERATOR'}. " ". $args{'Limit'}, ); } # }}} # }}} # {{{ Limit by string field # {{{ sub LimitSubject =head2 LimitSubject Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of = or !=. VALUE is a string to search for in the subject of the ticket. =cut sub LimitSubject { my $self = shift; my %args = (@_); $self->Limit (FIELD => 'Subject', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Subject ' . $args{'OPERATOR'}. " ". $args{'VALUE'}, ); } # }}} # }}} # {{{ Limit based on ticket numerical attributes # Things that can be > < = != # {{{ sub LimitId =head2 LimitId Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of =, >, < or !=. VALUE is a ticket Id to search for =cut sub LimitId { my $self = shift; my %args = (OPERATOR => '=', @_); $self->Limit (FIELD => 'id', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Id ' . $args{'OPERATOR'}. " ". $args{'VALUE'}, ); } # }}} # {{{ sub LimitPriority =head2 LimitPriority Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of =, >, < or !=. VALUE is a value to match the ticket\'s priority against =cut sub LimitPriority { my $self = shift; my %args = (@_); $self->Limit (FIELD => 'Priority', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Priority ' . $args{'OPERATOR'}. " ". $args{'VALUE'}, ); } # }}} # {{{ sub LimitInitialPriority =head2 LimitInitialPriority Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of =, >, < or !=. VALUE is a value to match the ticket\'s initial priority against =cut sub LimitInitialPriority { my $self = shift; my %args = (@_); $self->Limit (FIELD => 'InitialPriority', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Initial Priority ' . $args{'OPERATOR'}. " ". $args{'VALUE'}, ); } # }}} # {{{ sub LimitFinalPriority =head2 LimitFinalPriority Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of =, >, < or !=. VALUE is a value to match the ticket\'s final priority against =cut sub LimitFinalPriority { my $self = shift; my %args = (@_); $self->Limit (FIELD => 'FinalPriority', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Final Priority ' . $args{'OPERATOR'}. " ". $args{'VALUE'}, ); } # }}} # {{{ sub LimitTimeWorked =head2 LimitTimeWorked Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of =, >, < or !=. VALUE is a value to match the ticket's TimeWorked attribute =cut sub LimitTimeWorked { my $self = shift; my %args = (@_); $self->Limit (FIELD => 'TimeWorked', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Time worked ' . $args{'OPERATOR'}. " ". $args{'VALUE'}, ); } # }}} # {{{ sub LimitTimeLeft =head2 LimitTimeLeft Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of =, >, < or !=. VALUE is a value to match the ticket's TimeLeft attribute =cut sub LimitTimeLeft { my $self = shift; my %args = (@_); $self->Limit (FIELD => 'TimeLeft', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Time left ' . $args{'OPERATOR'}. " ". $args{'VALUE'}, ); } # }}} # }}} # {{{ Limiting based on attachment attributes # {{{ sub LimitContent =head2 LimitContent Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of =, LIKE, NOT LIKE or !=. VALUE is a string to search for in the body of the ticket =cut sub LimitContent { my $self = shift; my %args = (@_); $self->Limit (FIELD => 'Content', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Ticket content ' . $args{'OPERATOR'}. " ". $args{'VALUE'}, ); } # }}} # {{{ sub LimitContentType =head2 LimitContentType Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of =, LIKE, NOT LIKE or !=. VALUE is a content type to search ticket attachments for =cut sub LimitContentType { my $self = shift; my %args = (@_); $self->Limit (FIELD => 'ContentType', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Ticket content type ' . $args{'OPERATOR'}. " ". $args{'VALUE'}, ); } # }}} # }}} # {{{ Limiting based on people # {{{ sub LimitOwner =head2 LimitOwner Takes a paramhash with the fields OPERATOR and VALUE. OPERATOR is one of = or !=. VALUE is a user id. =cut sub LimitOwner { my $self = shift; my %args = ( OPERATOR => '=', @_); my $owner = new RT::User($self->CurrentUser); $owner->Load($args{'VALUE'}); $self->Limit (FIELD => 'Owner', VALUE => $owner->Id, OPERATOR => $args{'OPERATOR'}, DESCRIPTION => 'Owner ' . $args{'OPERATOR'}. " ". $owner->Name() ); } # }}} # {{{ Limiting watchers # {{{ sub LimitWatcher =head2 LimitWatcher Takes a paramhash with the fields OPERATOR, TYPE and VALUE. OPERATOR is one of =, LIKE, NOT LIKE or !=. VALUE is a value to match the ticket\'s watcher email addresses against TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them =cut sub LimitWatcher { my $self = shift; my %args = ( OPERATOR => '=', VALUE => undef, TYPE => undef, @_); #build us up a description my ($watcher_type, $desc); if ($args{'TYPE'}) { $watcher_type = $args{'TYPE'}; } else { $watcher_type = "Watcher"; } $desc = "$watcher_type ".$args{'OPERATOR'}." ".$args{'VALUE'}; $self->Limit (FIELD => 'Watcher', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, TYPE => $args{'TYPE'}, DESCRIPTION => "$desc" ); } # }}} # {{{ sub LimitRequestor =head2 LimitRequestor It\'s like LimitWatcher, but it presets TYPE to Requestor =cut sub LimitRequestor { my $self = shift; $self->LimitWatcher(TYPE=> 'Requestor', @_); } # }}} # {{{ sub LimitCc =head2 LimitCC It\'s like LimitWatcher, but it presets TYPE to Cc =cut sub LimitCc { my $self = shift; $self->LimitWatcher(TYPE=> 'Cc', @_); } # }}} # {{{ sub LimitAdminCc =head2 LimitAdminCc It\'s like LimitWatcher, but it presets TYPE to AdminCc =cut sub LimitAdminCc { my $self = shift; $self->LimitWatcher(TYPE=> 'AdminCc', @_); } # }}} # }}} # }}} # {{{ Limiting based on links # {{{ LimitLinkedTo =head2 LimitLinkedTo LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET TYPE limits the sort of relationship we want to search on TARGET is the id or URI of the TARGET of the link (TARGET used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as TARGET =cut sub LimitLinkedTo { my $self = shift; my %args = ( TICKET => undef, TARGET => undef, TYPE => undef, @_); $self->Limit( FIELD => 'LinkedTo', BASE => undef, TARGET => ($args{'TARGET'} || $args{'TICKET'}), TYPE => $args{'TYPE'}, DESCRIPTION => "Tickets ".$args{'TYPE'}." by ".($args{'TARGET'} || $args{'TICKET'}) ); } # }}} # {{{ LimitLinkedFrom =head2 LimitLinkedFrom LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE TYPE limits the sort of relationship we want to search on BASE is the id or URI of the BASE of the link (BASE used to be 'TICKET'. 'TICKET' is deprecated, but will be treated as BASE =cut sub LimitLinkedFrom { my $self = shift; my %args = ( BASE => undef, TICKET => undef, TYPE => undef, @_); $self->Limit( FIELD => 'LinkedTo', TARGET => undef, BASE => ($args{'BASE'} || $args{'TICKET'}), TYPE => $args{'TYPE'}, DESCRIPTION => "Tickets " .($args{'BASE'} || $args{'TICKET'}) ." ".$args{'TYPE'} ); } # }}} # {{{ LimitMemberOf sub LimitMemberOf { my $self = shift; my $ticket_id = shift; $self->LimitLinkedTo ( TARGET=> "$ticket_id", TYPE => 'MemberOf', ); } # }}} # {{{ LimitHasMember sub LimitHasMember { my $self = shift; my $ticket_id =shift; $self->LimitLinkedFrom ( BASE => "$ticket_id", TYPE => 'MemberOf', ); } # }}} # {{{ LimitDependsOn sub LimitDependsOn { my $self = shift; my $ticket_id = shift; $self->LimitLinkedTo ( TARGET => "$ticket_id", TYPE => 'DependsOn', ); } # }}} # {{{ LimitDependedOnBy sub LimitDependedOnBy { my $self = shift; my $ticket_id = shift; $self->LimitLinkedFrom ( BASE => "$ticket_id", TYPE => 'DependsOn', ); } # }}} # {{{ LimitRefersTo sub LimitRefersTo { my $self = shift; my $ticket_id = shift; $self->LimitLinkedTo ( TARGET => "$ticket_id", TYPE => 'RefersTo', ); } # }}} # {{{ LimitReferredToBy sub LimitReferredToBy { my $self = shift; my $ticket_id = shift; $self->LimitLinkedFrom ( BASE=> "$ticket_id", TYPE => 'RefersTo', ); } # }}} # }}} # {{{ limit based on ticket date attribtes # {{{ sub LimitDate =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate) Takes a paramhash with the fields FIELD OPERATOR and VALUE. OPERATOR is one of > or < VALUE is a date and time in ISO format in GMT FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated There are also helper functions of the form LimitFIELD that eliminate the need to pass in a FIELD argument. =cut sub LimitDate { my $self = shift; my %args = ( FIELD => undef, VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, @_); #Set the description if we didn't get handed it above unless ($args{'DESCRIPTION'} ) { $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT" } $self->Limit (%args); } # }}} sub LimitCreated { my $self = shift; $self->LimitDate( FIELD => 'Created', @_); } sub LimitDue { my $self = shift; $self->LimitDate( FIELD => 'Due', @_); } sub LimitStarts { my $self = shift; $self->LimitDate( FIELD => 'Starts', @_); } sub LimitStarted { my $self = shift; $self->LimitDate( FIELD => 'Started', @_); } sub LimitResolved { my $self = shift; $self->LimitDate( FIELD => 'Resolved', @_); } sub LimitTold { my $self = shift; $self->LimitDate( FIELD => 'Told', @_); } sub LimitLastUpdated { my $self = shift; $self->LimitDate( FIELD => 'LastUpdated', @_); } # # {{{ sub LimitTransactionDate =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate) Takes a paramhash with the fields FIELD OPERATOR and VALUE. OPERATOR is one of > or < VALUE is a date and time in ISO format in GMT =cut sub LimitTransactionDate { my $self = shift; my %args = ( FIELD => 'TransactionDate', VALUE => $args{'VALUE'}, OPERATOR => $args{'OPERATOR'}, @_); #Set the description if we didn't get handed it above unless ($args{'DESCRIPTION'} ) { $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT" } $self->Limit (%args); } # }}} # }}} # {{{ sub LimitKeyword =head2 LimitKeyword Takes a paramhash of key/value pairs with the following keys: =over 4 =item KEYWORDSELECT - KeywordSelect id =item OPERATOR - (for KEYWORD only - KEYWORDSELECT operator is always `=') =item KEYWORD - Keyword id =back =cut sub LimitKeyword { my $self = shift; my %args = ( KEYWORD => undef, KEYWORDSELECT => undef, OPERATOR => '=', DESCRIPTION => undef, FIELD => 'Keyword', QUOTEVALUE => 1, @_ ); use RT::KeywordSelect; my $KeywordSelect = RT::KeywordSelect->new($self->CurrentUser); $KeywordSelect->Load($args{KEYWORDSELECT}); # Below, We're checking to see whether the keyword we're searching for # is null or not. # This could probably be rewritten to be easier to read and understand #If we are looking to compare with a null value. if ($args{'OPERATOR'} =~ /is/i) { if ($args{'OPERATOR'} =~ /^is$/i) { $args{'DESCRIPTION'} ||= "Keyword Selection ". $KeywordSelect->Name . " has no value"; } elsif ($args{'OPERATOR'} =~ /^is not$/i) { $args{'DESCRIPTION'} ||= "Keyword Selection ". $KeywordSelect->Name . " has a value"; } } # if we're not looking to compare with a null value else { use RT::Keyword; my $Keyword = RT::Keyword->new($self->CurrentUser); $Keyword->Load($args{KEYWORD}); $args{'DESCRIPTION'} ||= "Keyword Selection " . $KeywordSelect->Name. " $args{OPERATOR} ". $Keyword->Name; } $args{SingleValued} = $KeywordSelect->Single(); my $index = $self->_NextIndex; %{$self->{'TicketRestrictions'}{$index}} = %args; $self->{'RecalcTicketLimits'} = 1; return ($index); } # }}} # {{{ sub _NextIndex =head2 _NextIndex Keep track of the counter for the array of restrictions =cut sub _NextIndex { my $self = shift; return ($self->{'restriction_index'}++); } # }}} # }}} # {{{ Core bits to make this a DBIx::SearchBuilder object # {{{ sub _Init sub _Init { my $self = shift; $self->{'table'} = "Tickets"; $self->{'RecalcTicketLimits'} = 1; $self->{'looking_at_effective_id'} = 0; $self->{'restriction_index'} =1; $self->{'primary_key'} = "id"; $self->SUPER::_Init(@_); } # }}} # {{{ sub NewItem sub NewItem { my $self = shift; return(RT::Ticket->new($self->CurrentUser)); } # }}} # {{{ sub Count sub Count { my $self = shift; $self->_ProcessRestrictions if ($self->{'RecalcTicketLimits'} == 1 ); return($self->SUPER::Count()); } # }}} # {{{ sub ItemsArrayRef =head2 ItemsArrayRef Returns a reference to the set of all items found in this search =cut sub ItemsArrayRef { my $self = shift; my @items; my $placeholder = $self->_ItemsCounter; $self->GotoFirstItem(); while (my $item = $self->Next) { push (@items, $item); } $self->GotoItem($placeholder); return(\@items); } # }}} # {{{ sub Next sub Next { my $self = shift; $self->_ProcessRestrictions if ($self->{'RecalcTicketLimits'} == 1 ); my $Ticket = $self->SUPER::Next(); if ((defined($Ticket)) and (ref($Ticket))) { #Make sure we _never_ show dead tickets #TODO we should be doing this in the where clause. #but you can't do multiple clauses on the same field just yet :/ if ($Ticket->Status eq 'dead') { return($self->Next()); } elsif ($Ticket->CurrentUserHasRight('ShowTicket')) { return($Ticket); } #If the user doesn't have the right to show this ticket else { return($self->Next()); } } #if there never was any ticket else { return(undef); } } # }}} # }}} # {{{ Deal with storing and restoring restrictions # {{{ sub LoadRestrictions =head2 LoadRestrictions LoadRestrictions takes a string which can fully populate the TicketRestrictons hash. TODO It is not yet implemented =cut # }}} # {{{ sub DescribeRestrictions =head2 DescribeRestrictions takes nothing. Returns a hash keyed by restriction id. Each element of the hash is currently a one element hash that contains DESCRIPTION which is a description of the purpose of that TicketRestriction =cut sub DescribeRestrictions { my $self = shift; my ($row, %listing); foreach $row (keys %{$self->{'TicketRestrictions'}}) { $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'}; } return (%listing); } # }}} # {{{ sub RestrictionValues =head2 RestrictionValues FIELD Takes a restriction field and returns a list of values this field is restricted to. =cut sub RestrictionValues { my $self = shift; my $field = shift; map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep { $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "=" } keys %{$self->{'TicketRestrictions'}}; } # }}} # {{{ sub ClearRestrictions =head2 ClearRestrictions Removes all restrictions irretrievably =cut sub ClearRestrictions { my $self = shift; delete $self->{'TicketRestrictions'}; $self->{'looking_at_effective_id'} = 0; $self->{'RecalcTicketLimits'} =1; } # }}} # {{{ sub DeleteRestriction =head2 DeleteRestriction Takes the row Id of a restriction (From DescribeRestrictions' output, for example. Removes that restriction from the session's limits. =cut sub DeleteRestriction { my $self = shift; my $row = shift; delete $self->{'TicketRestrictions'}{$row}; $self->{'RecalcTicketLimits'} = 1; #make the underlying easysearch object forget all its preconceptions } # }}} # {{{ sub _ProcessRestrictions sub _ProcessRestrictions { my $self = shift; #Need to clean the EasySearch slate because it makes things too sticky $self->CleanSlate(); #Blow away ticket aliases since we'll need to regenerate them for a new search delete $self->{'TicketAliases'}; delete $self->{KeywordsAliases}; my $row; foreach $row (keys %{$self->{'TicketRestrictions'}}) { my $restriction = $self->{'TicketRestrictions'}{$row}; # {{{ if it's an int if ($TYPES{$restriction->{'FIELD'}} eq 'INT' ) { if ($restriction->{'OPERATOR'} =~ /^(=|!=|>|<|>=|<=)$/) { $self->SUPER::Limit( FIELD => $restriction->{'FIELD'}, ENTRYAGGREGATOR => 'AND', OPERATOR => $restriction->{'OPERATOR'}, VALUE => $restriction->{'VALUE'}, ); } } # }}} # {{{ if it's an enum elsif ($TYPES{$restriction->{'FIELD'}} eq 'ENUM') { if ($restriction->{'OPERATOR'} eq '=') { $self->SUPER::Limit( FIELD => $restriction->{'FIELD'}, ENTRYAGGREGATOR => 'OR', OPERATOR => '=', VALUE => $restriction->{'VALUE'}, ); } elsif ($restriction->{'OPERATOR'} eq '!=') { $self->SUPER::Limit( FIELD => $restriction->{'FIELD'}, ENTRYAGGREGATOR => 'AND', OPERATOR => '!=', VALUE => $restriction->{'VALUE'}, ); } } # }}} # {{{ if it's a date elsif ($TYPES{$restriction->{'FIELD'}} eq 'DATE') { $self->SUPER::Limit( FIELD => $restriction->{'FIELD'}, ENTRYAGGREGATOR => 'AND', OPERATOR => $restriction->{'OPERATOR'}, VALUE => $restriction->{'VALUE'}, ); } # }}} # {{{ if it's a string elsif ($TYPES{$restriction->{'FIELD'}} eq 'STRING') { if ($restriction->{'OPERATOR'} eq '=') { $self->SUPER::Limit( FIELD => $restriction->{'FIELD'}, ENTRYAGGREGATOR => 'OR', OPERATOR => '=', VALUE => $restriction->{'VALUE'}, CASESENSITIVE => 0 ); } elsif ($restriction->{'OPERATOR'} eq '!=') { $self->SUPER::Limit( FIELD => $restriction->{'FIELD'}, ENTRYAGGREGATOR => 'AND', OPERATOR => '!=', VALUE => $restriction->{'VALUE'}, CASESENSITIVE => 0 ); } elsif ($restriction->{'OPERATOR'} eq 'LIKE') { $self->SUPER::Limit( FIELD => $restriction->{'FIELD'}, ENTRYAGGREGATOR => 'AND', OPERATOR => 'LIKE', VALUE => $restriction->{'VALUE'}, CASESENSITIVE => 0 ); } elsif ($restriction->{'OPERATOR'} eq 'NOT LIKE') { $self->SUPER::Limit( FIELD => $restriction->{'FIELD'}, ENTRYAGGREGATOR => 'AND', OPERATOR => 'NOT LIKE', VALUE => $restriction->{'VALUE'}, CASESENSITIVE => 0 ); } } # }}} # {{{ if it's Transaction content that we're hunting for elsif ($TYPES{$restriction->{'FIELD'}} eq 'TRANSFIELD') { #Basically, we want to make sure that the limits apply to the same attachment, #rather than just another attachment for the same ticket, no matter how many #clauses we lump on. #We put them in TicketAliases so that they get nuked when we redo the join. unless (defined $self->{'TicketAliases'}{'TransFieldAlias'}) { $self->{'TicketAliases'}{'TransFieldAlias'} = $self->NewAlias ('Transactions'); } unless (defined $self->{'TicketAliases'}{'TransFieldAttachAlias'}){ $self->{'TicketAliases'}{'TransFieldAttachAlias'} = $self->NewAlias('Attachments'); } #Join transactions to attachments $self->Join( ALIAS1 => $self->{'TicketAliases'}{'TransFieldAttachAlias'}, FIELD1 => 'TransactionId', ALIAS2 => $self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2=> 'id'); #Join transactions to tickets $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'}, ALIAS2 =>$self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2 => 'Ticket'); #Search for the right field $self->SUPER::Limit(ALIAS => $self->{'TicketAliases'}{'TransFieldAttachAlias'}, ENTRYAGGREGATOR => 'AND', FIELD => $restriction->{'FIELD'}, OPERATOR => $restriction->{'OPERATOR'} , VALUE => $restriction->{'VALUE'}, CASESENSITIVE => 0 ); } # }}} # {{{ if it's a Transaction date that we're hunting for elsif ($TYPES{$restriction->{'FIELD'}} eq 'TRANSDATE') { #Basically, we want to make sure that the limits apply to the same attachment, #rather than just another attachment for the same ticket, no matter how many #clauses we lump on. #We put them in TicketAliases so that they get nuked when we redo the join. unless (defined $self->{'TicketAliases'}{'TransFieldAlias'}) { $self->{'TicketAliases'}{'TransFieldAlias'} = $self->NewAlias ('Transactions'); } #Join transactions to tickets $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'}, ALIAS2 =>$self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2 => 'Ticket'); #Search for the right field $self->SUPER::Limit(ALIAS => $self->{'TicketAliases'}{'TransFieldAlias'}, ENTRYAGGREGATOR => 'AND', FIELD => 'Created', OPERATOR => $restriction->{'OPERATOR'} , VALUE => $restriction->{'VALUE'} ); } # }}} # {{{ if it's a relationship that we're hunting for # Takes FIELD: which is something like "LinkedTo" # takes TARGET or BASE which is the TARGET or BASE id that we're searching for # takes TYPE which is the type of link we're looking for. elsif ($TYPES{$restriction->{'FIELD'}} eq 'LINKFIELD') { my $LinkAlias = $self->NewAlias ('Links'); #Make sure we get the right type of link, if we're restricting it if ($restriction->{'TYPE'}) { $self->SUPER::Limit(ALIAS => $LinkAlias, ENTRYAGGREGATOR => 'AND', FIELD => 'Type', OPERATOR => '=', VALUE => $restriction->{'TYPE'} ); } #If we're trying to limit it to things that are target of if ($restriction->{'TARGET'}) { # If the TARGET is an integer that means that we want to look at the LocalTarget # field. otherwise, we want to look at the "Target" field my ($matchfield); if ($restriction->{'TARGET'} =~/^(\d+)$/) { $matchfield = "LocalTarget"; } else { $matchfield = "Target"; } $self->SUPER::Limit(ALIAS => $LinkAlias, ENTRYAGGREGATOR => 'AND', FIELD => $matchfield, OPERATOR => '=', VALUE => $restriction->{'TARGET'} ); #If we're searching on target, join the base to ticket.id $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'}, ALIAS2 => $LinkAlias, FIELD2 => 'LocalBase'); } #If we're trying to limit it to things that are base of elsif ($restriction->{'BASE'}) { # If we're trying to match a numeric link, we want to look at LocalBase, # otherwise we want to look at "Base" my ($matchfield); if ($restriction->{'BASE'} =~/^(\d+)$/) { $matchfield = "LocalBase"; } else { $matchfield = "Base"; } $self->SUPER::Limit(ALIAS => $LinkAlias, ENTRYAGGREGATOR => 'AND', FIELD => $matchfield, OPERATOR => '=', VALUE => $restriction->{'BASE'} ); #If we're searching on base, join the target to ticket.id $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'}, ALIAS2 => $LinkAlias, FIELD2 => 'LocalTarget'); } } # }}} # {{{ if it's a watcher that we're hunting for elsif ($TYPES{$restriction->{'FIELD'}} eq 'WATCHERFIELD') { my $Watch = $self->NewAlias('Watchers'); #Join watchers to users my $User = $self->Join( TYPE => 'left', ALIAS1 => $Watch, FIELD1 => 'Owner', TABLE2 => 'Users', FIELD2 => 'id', ); #Join Ticket to watchers $self->Join( ALIAS1 => 'main', FIELD1 => 'id', ALIAS2 => $Watch, FIELD2 => 'Value'); #Make sure we're only talking about ticket watchers $self->SUPER::Limit( ALIAS => $Watch, FIELD => 'Scope', VALUE => 'Ticket', OPERATOR => '='); # Find email address watchers $self->SUPER::Limit( SUBCLAUSE => 'WatcherEmailAddress', ALIAS => $Watch, FIELD => 'Email', ENTRYAGGREGATOR => 'OR', VALUE => $restriction->{'VALUE'}, OPERATOR => $restriction->{'OPERATOR'}, CASESENSITIVE => 0 ); #Find user watchers $self->SUPER::Limit( SUBCLAUSE => 'WatcherEmailAddress', ALIAS => $User, FIELD => 'EmailAddress', ENTRYAGGREGATOR => 'OR', VALUE => $restriction->{'VALUE'}, OPERATOR => $restriction->{'OPERATOR'}, CASESENSITIVE => 0 ); #If we only want a specific type of watchers, then limit it to that if ($restriction->{'TYPE'}) { $self->SUPER::Limit( ALIAS => $Watch, FIELD => 'Type', ENTRYAGGREGATOR => 'OR', VALUE => $restriction->{'TYPE'}, OPERATOR => '='); } } # }}} # {{{ if it's a keyword elsif ($TYPES{$restriction->{'FIELD'}} eq 'KEYWORDFIELD') { my $null_columns_ok; my $ObjKeywordsAlias; $ObjKeywordsAlias = $self->{KeywordsAliases}{$restriction->{'KEYWORDSELECT'}} if $restriction->{SingleValued}; unless (defined $ObjKeywordsAlias) { $ObjKeywordsAlias = $self->Join( TYPE => 'left', ALIAS1 => 'main', FIELD1 => 'id', TABLE2 => 'ObjectKeywords', FIELD2 => 'ObjectId' ); if ($restriction->{'SingleValued'}) { $self->{KeywordsAliases}{$restriction->{'KEYWORDSELECT'}} = $ObjKeywordsAlias; } } $self->SUPER::Limit( ALIAS => $ObjKeywordsAlias, FIELD => 'Keyword', OPERATOR => $restriction->{'OPERATOR'}, VALUE => $restriction->{'KEYWORD'}, QUOTEVALUE => $restriction->{'QUOTEVALUE'}, ENTRYAGGREGATOR => 'OR', ); if ( ($restriction->{'OPERATOR'} =~ /^IS$/i) or ($restriction->{'OPERATOR'} eq '!=') ) { $null_columns_ok=1; } #If we're trying to find tickets where the keyword isn't somethng, also check ones where it _IS_ null if ( $restriction->{'OPERATOR'} eq '!=') { $self->SUPER::Limit( ALIAS => $ObjKeywordsAlias, FIELD => 'Keyword', OPERATOR => 'IS', VALUE => 'NULL', QUOTEVALUE => 0, ENTRYAGGREGATOR => 'OR', ); } $self->SUPER::Limit(LEFTJOIN => $ObjKeywordsAlias, FIELD => 'KeywordSelect', VALUE => $restriction->{'KEYWORDSELECT'}, ENTRYAGGREGATOR => 'OR'); $self->SUPER::Limit( ALIAS => $ObjKeywordsAlias, FIELD => 'ObjectType', VALUE => 'Ticket', ENTRYAGGREGATOR => 'AND'); if ($null_columns_ok) { $self->SUPER::Limit(ALIAS => $ObjKeywordsAlias, FIELD => 'ObjectType', OPERATOR => 'IS', VALUE => 'NULL', QUOTEVALUE => 0, ENTRYAGGREGATOR => 'OR'); } } # }}} } # here, we make sure we don't get any tickets that have been merged into other tickets # (Ticket Id == Ticket EffectiveId # note that we _really_ don't want to do this if we're already looking at the effectiveid if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) { $self->SUPER::Limit( FIELD => 'EffectiveId', OPERATOR => '=', QUOTEVALUE => 0, VALUE => 'main.id'); #TODO, we shouldn't be hard coding the tablename to main. } $self->{'RecalcTicketLimits'} = 0; } # }}} # }}} # {{{ Deal with displaying rows of the listing # # Everything in this section is stub code for 2.2 # It's not part of the API. It's not for your use # It's not for our use. # # {{{ sub SetListingFormat =head2 SetListingFormat Takes a single Format string as specified below. parses that format string and makes the various listing output things DTRT. =item Format strings Format strings are made up of a chain of Elements delimited with vertical pipes (|). Elements of a Format string FormatString: Element[::FormatString] Element: AttributeName[;HREF=][;TITLE=] AttributeName Id | Subject | Status | Owner | Priority | InitialPriority | TimeWorked | TimeLeft | Keywords[;SELECT=<KeywordSelect>] | <Created|Starts|Started|Contacted|Due|Resolved>Date<AsString|AsISO|AsAge> =cut #accept a format string sub SetListingFormat { my $self = shift; my $listing_format = shift; my ($element, $attribs); my $i = 0; foreach $element (split (/::/,$listing_format)) { if ($element =~ /^(.*?);(.*)$/) { $element = $1; $attribs = $2; } $self->{'format_string'}->[$i]->{'Element'} = $element; foreach $attrib (split (/;/, $attribs)) { my $value = ""; if ($attrib =~ /^(.*?)=(.*)$/) { $attrib = $1; $value = $2; } $self->{'format_string'}->[$i]->{"$attrib"} = $val; } } return(1); } # }}} # {{{ sub HeaderAsHTML sub HeaderAsHTML { my $self = shift; my $header = ""; my $col; foreach $col ( @{[ $self->{'format_string'} ]}) { $header .= "<TH>" . $self->_ColumnTitle($self->{'format_string'}->[$col]) . "</TH>"; } return ($header); } # }}} # {{{ sub HeaderAsText #Print text header sub HeaderAsText { my $self = shift; my ($header); return ($header); } # }}} # {{{ sub TicketAsHTMLRow #Print HTML row sub TicketAsHTMLRow { my $self = shift; my $Ticket = shift; my ($row, $col); foreach $col (@{[$self->{'format_string'}]}) { $row .= "<TD>" . $self->_TicketColumnValue($ticket,$self->{'format_string'}->[$col]) . "</TD>"; } return ($row); } # }}} # {{{ sub TicketAsTextRow #Print text row sub TicketAsTextRow { my $self = shift; my ($row); #TODO implement return ($row); } # }}} # {{{ _ColumnTitle { sub _ColumnTitle { my $self = shift; # Attrib is a hash my $attrib = shift; # return either attrib->{'TITLE'} or.. if ($attrib->{'TITLE'}) { return($attrib->{'TITLE'}); } # failing that, Look up the title in a hash else { #TODO create $self->{'ColumnTitles'}; return ($self->{'ColumnTitles'}->{$attrib->{'Element'}}); } } # }}} # {{{ _TicketColumnValue sub _TicketColumnValue { my $self = shift; my $Ticket = shift; my $attrib = shift; my $out; SWITCH: { /^id/i && do { $out = $Ticket->id; last SWITCH; }; /^subj/i && do { last SWITCH; $Ticket->Subject; }; /^status/i && do { last SWITCH; $Ticket->Status; }; /^prio/i && do { last SWITCH; $Ticket->Priority; }; /^finalprio/i && do { last SWITCH; $Ticket->FinalPriority }; /^initialprio/i && do { last SWITCH; $Ticket->InitialPriority; }; /^timel/i && do { last SWITCH; $Ticket->TimeWorked; }; /^timew/i && do { last SWITCH; $Ticket->TimeLeft; }; /^(.*?)date(.*)$/i && do { my $o = $1; my $m = $2; my ($obj); #TODO: optimize $obj = $Ticket->DueObj if $o =~ /due/i; $obj = $Ticket->CreatedObj if $o =~ /created/i; $obj = $Ticket->StartsObj if $o =~ /starts/i; $obj = $Ticket->StartedObj if $o =~ /started/i; $obj = $Ticket->ToldObj if $o =~ /told/i; $obj = $Ticket->LastUpdatedObj if $o =~ /lastu/i; $method = 'ISO' if $m =~ /iso/i; $method = 'AsString' if $m =~ /asstring/i; $method = 'AgeAsString' if $m =~ /age/i; last SWITCH; $obj->$method(); }; /^watcher/i && do { last SWITCH; $Ticket->WatchersAsString(); }; /^requestor/i && do { last SWITCH; $Ticket->RequestorsAsString(); }; /^cc/i && do { last SWITCH; $Ticket->CCAsString(); }; /^admincc/i && do { last SWITCH; $Ticket->AdminCcAsString(); }; /^keywords/i && do { last SWITCH; #Limit it to the keyword select we're talking about, if we've got one. my $objkeys =$Ticket->KeywordsObj($attrib->{'SELECT'}); $objkeys->KeywordRelativePathsAsString(); }; } } # }}} # }}} # {{{ POD =head2 notes "Enum" Things that get Is, IsNot "Int" Things that get Is LessThan and GreaterThan id InitialPriority FinalPriority Priority TimeLeft TimeWorked "Text" Things that get Is, Like Subject TransactionContent "Link" OPERATORs "Date" OPERATORs Is, Before, After =cut # }}} 1;