import rt 2.0.14
[freeside.git] / rt / lib / RT / Tickets.pm
diff --git a/rt/lib/RT/Tickets.pm b/rt/lib/RT/Tickets.pm
new file mode 100755 (executable)
index 0000000..dd91126
--- /dev/null
@@ -0,0 +1,1789 @@
+#$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=<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;