#$Header: /raid/cvsroot/rt/lib/RT/Tickets.pm,v 1.6 2001/12/26 19:51:46 jesse 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;
    }
    
 
    
    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'};

    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 = $self->Join(
					       TYPE => 'left',
					       ALIAS1 => 'main',
					       FIELD1 => 'id',
					       TABLE2 => 'ObjectKeywords',
					       FIELD2 => 'ObjectId'
					      );
            $self->SUPER::Limit(
				ALIAS => $ObjKeywordsAlias,
				FIELD => 'Keyword',
				OPERATOR => $restriction->{'OPERATOR'},
				VALUE => $restriction->{'KEYWORD'},
				QUOTEVALUE => $restriction->{'QUOTEVALUE'},
				ENTRYAGGREGATOR => 'AND',
                               );
	    
            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=<URL>][;TITLE=<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;
