UntouchedInHours

From Request Tracker Wiki
Jump to: navigation, 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