UntouchedInHours

From Request Tracker Wiki
Jump to navigation Jump to search

UntouchedInHours

A condition for rt-crontool

RT's Crontool (bin/rt-crontool) offers several recipes for cron jobs. Here's one:

   bin/rt-crontool \
    --search RT::Search::ActiveTicketsInQueue  --search-arg general \
    --condition RT::Condition::UntouchedInHours --condition-arg 4 \
    --action RT::Action::SetPriority --action-arg 99 \
    --verbose

Some of the examples don't work out-of-the-box because they're just examples. Creating your own conditions is not hard, but to save you the couple of seconds, and if you would like to use the UntouchedInHours condition, save the following code to [=local/lib/RT/Condition/UntouchedInHours.pm]

   package RT::Condition::UntouchedInHours;
   require RT::Condition::Generic;
   use strict;
   use vars qw/@ISA/;
   # We inherit from RT::Condition::Generic so we don't have to write (and maintain)
   # all the other stuff that goes into a condition
   @ISA = qw(RT::Condition::Generic);


   =head2 IsApplicable
   If the ticket's LastUpdated is more than n hours ago
   =cut
   # We just need to include this one function. I could have put everything in here but in
   # order to demonstrate the use of external functions, I've created the local_ageinhours
   # function. The function subtracts the LastUpdated time (as an Epoch timestamp) from the
   # current time stamp, then turns these seconds into hours.
   # The main function then just checks if this number is greater or equal to the argument
   # the crontool passed in.
   #
   # Note that the main function MUST be called 'IsApplicable'. For complex conditions it
   # may be best to create other functions as I've done here - so long as they're called
   # from IsApplicable.
   # I'd suggest naming your own functions with a 'local_' prefix so as to be certain not
   # to overwrite any current or future core function.
   sub IsApplicable {
      my $self = shift;
      if ( local_ageInHours($self->TicketObj) >= $self->Argument ) {
          # Returning true (1) indicates that this condition is true
          return 1;
      } else {
          # Returning undef indicates that this condition is false
          return undef;
      }
   }
   sub local_ageInHours {
      my $ticketObj = shift;
      return (time - $ticketObj->LastUpdatedObj->Unix) / (60*60);
   }


   # The following could be omitted. They're there to allow overrides from Vendor and Local
   # but as this isn't a core module, they're just there for completeness :)
   eval "require RT::Condition::UntouchedInHours_Vendor";
   die $@ if ($@ && $@ !~ qr{^Can't locate RT/Condition/UntouchedInHours_Vendor.pm});
   eval "require RT::Condition::UntouchedInHours_Local";
   die $@ if ($@ && $@ !~ qr{^Can't locate RT/Condition/UntouchedInHours_Local.pm});
   # If you weren't already aware, all perl modules need to evaluate to true. So we
   # force it to evaluate to true by finishing with a '1'.
   1;

Alternative Version

Could not get the above example to work successfully so I have created an alternative version (as simple as I could make it):

   package RT::Condition::UntouchedInHours;
   require RT::Condition::Generic;
   use RT::Date;


   @ISA = qw(RT::Condition::Generic);


   use strict;
   use vars qw/@ISA/;
   sub IsApplicable {
           my $self = shift;
           if ((time()-$self->TicketObj->LastUpdatedObj->Unix)/3600 >= $self->Argument) {
                   return 1
           }
           else {
                   return 0;
           }
   }
   # The following could be omitted. They're there to allow overrides from Vendor and Local
   # but as this isn't a core module, they're just there for completeness :)
   eval "require RT::Condition::UntouchedInHours_Vendor";
   die $@ if ($@ && $@ !~ qr{^Can't locate RT/Condition/UntouchedInHours_Vendor.pm});
   eval "require RT::Condition::UntouchedInHours_Local";
   die $@ if ($@ && $@ !~ qr{^Can't locate RT/Condition/UntouchedInHours_Local.pm});
   1;

Just a footnote on that, I have fixed 2 mistakes spotted in the origional example. It may work now. I am using the Alternative version simply because I prefer its brevity.

Alternative Version: UntouchedInBusinessHours

Based on this module, i created a additional Modul [[[UntouchedInBusinessHours]].pm] based on Business Hour calculation.

Yet Another Alternative Version

At our site, we have a number of automatic operations that update a ticket's *LastUpdated* value, which means that it's not so useful for determining when the ticket owner last touched the ticket. This version works for us:

   package RT::Condition::UntouchedInHours;
   require RT::Condition::Generic;
   # RT::Date::Set kept misbehaving when fed an ISO-formatted string
   require DateTime::Format::MySQL;
   @ISA = qw(RT::Condition::Generic);
   use strict;
   use vars qw/@ISA/;
   # default age threshold
   use constant HOURS => 4;
   sub IsApplicable {
       my $self = shift;
       my $hours = $self->Argument || HOURS;
       # validate input
       unless ( $hours =~ /^\d+$/ ) {
           $hours = HOURS;
       }
       my $threshold = $hours * 60 * 60;
       my $now = time();
       my $last = local_lastCorrespondence( $self->TicketObj );
       if ( ( $last > 0 ) && ( $last + $threshold < $now ) ) {
           # this ticket has not been touched recently enough
           return $last;
       }
       else {
           return 0;
       }
   }
   sub local_lastCorrespondence {
       my $ticketObj = shift;
       # list the transactions
       my $transactions = $ticketObj->Transactions;
       # only the Correspondence, please
       $transactions->Limit( FIELD => 'Type', VALUE => 'Correspond' );
       # sort in descending order, by creation time then id
       $transactions->OrderByCols (
           { FIELD => 'Created', ORDER => 'DESC' },
           { FIELD => 'id', ORDER => 'DESC' },
       );
       # get the latest reply
       my $timestamp;
       my $transactionObj = $transactions->First;
       if ( defined( $transactionObj) && $transactionObj->id ) {
           # if the last Correspondence was from a requestor, check the time
           if ( $transactionObj->IsInbound ) {
               my $last = DateTime::Format::MySQL->parse_datetime( $transactionObj->Created );
               $timestamp = $last->epoch;
           }
           else {
               # otherwise we don't care
               $timestamp = -1;
           }
       }
       else {
           # try the start time, then the created time
           my $started = DateTime::Format::MySQL->parse_datetime( $ticketObj->Started );
           my $created = DateTime::Format::MySQL->parse_datetime( $ticketObj->Created );
           if ( $started->epoch > 0 ) {
               $timestamp = $started->epoch;
           }
           else {
               $timestamp = $created->epoch;
           }
       }
       # if we've gotten this far, and there's still no timestamp,
       # then something is terribly wrong
       defined( $timestamp ) or $timestamp = -1;
       return( $timestamp );
   }
   # The following could be omitted. They're there to allow overrides from Vendor and Local
   # but as this isn't a core module, they're just there for completeness :)
   eval "require RT::Condition::UntouchedInHours_Vendor";
   die $@ if ($@ && $@ !~ qr{^Can't locate RT/Condition/UntouchedInHours_Vendor.pm});
   eval "require RT::Condition::UntouchedInHours_Local";
   die $@ if ($@ && $@ !~ qr{^Can't locate RT/Condition/UntouchedInHours_Local.pm});
   1;

Note that *IsApplicable* returns the epoch time of the most recent ticket update or 0; this means that you can call this condition from inside a template and then use *RT::Date::AgeAsString* to generate a human-readable representation of how long the ticket has gone without response.

Note that this version works with RT 4.0.1 by editing the two instances of RT::Condition::Generic to RT::Condition