From 6cc42813cd4e39154b2589c315e1271fa6b43ff1 Mon Sep 17 00:00:00 2001 From: mark Date: Tue, 25 Jan 2011 10:13:16 +0000 Subject: [PATCH] ticket escalation, part 2, RT#8254 --- FS/FS/Cron/rt_tasks.pm | 37 +++++++-- FS/FS/TicketSystem.pm | 70 +++++++++++++---- rt/FREESIDE_MODIFIED | 1 + rt/etc/RT_SiteConfig.pm | 4 + rt/lib/RT/Action/EscalateQueue.pm | 141 ++++++++++++++++++++++++++++++++++ rt/lib/RT/Action/SetPriority_Local.pm | 47 ++++++++++++ rt/lib/RT/CustomFieldValues/Queues.pm | 30 ++++++++ 7 files changed, 306 insertions(+), 24 deletions(-) create mode 100755 rt/lib/RT/Action/EscalateQueue.pm create mode 100644 rt/lib/RT/Action/SetPriority_Local.pm create mode 100644 rt/lib/RT/CustomFieldValues/Queues.pm diff --git a/FS/FS/Cron/rt_tasks.pm b/FS/FS/Cron/rt_tasks.pm index 066aeebde..26e305d59 100644 --- a/FS/FS/Cron/rt_tasks.pm +++ b/FS/FS/Cron/rt_tasks.pm @@ -42,6 +42,7 @@ sub rt_escalate { foreach (qw( Search::ActiveTicketsInQueue Action::EscalatePriority + Action::EscalateQueue )) { eval "use RT::$_"; die $@ if $@; @@ -51,7 +52,7 @@ sub rt_escalate { # Mechanics: # We're using EscalatePriority, so search in all queues that have a # priority range defined. Select all active tickets in those queues and - # LinearEscalate them. + # EscalatePriority, then EscalateQueue them. # to make some actions work without complaining %void = map { $_ => "RT::$_"->new($CurrentUser) } @@ -61,6 +62,8 @@ sub rt_escalate { # we might want to do, but escalation is the only one we do now. my $queues = RT::Queues->new($CurrentUser); $queues->UnLimit; + my @actions = (); + my @active_tickets = (); while (my $queue = $queues->Next) { if ( $queue->InitialPriority == $queue->FinalPriority ) { warn "Queue '".$queue->Name."' (skipped)\n" if $DEBUG; @@ -76,8 +79,24 @@ sub rt_escalate { $search->Prepare; while (my $ticket = $tickets->Next) { warn 'Ticket #'.$ticket->Id()."\n" if $DEBUG; - # We don't need transaction stuff from rt-crontool here - action($ticket, 'EscalatePriority', "CurrentTime:$time"); + my @a = ( + action($ticket, 'EscalatePriority', "CurrentTime:$time"), + action($ticket, 'EscalateQueue') + ); + next if !@a; + push @actions, @a; + push @active_tickets, $ticket; # avoid RT's overzealous garbage collector + } + } + foreach (grep {$_} @actions) { + my ($val, $msg) = $_->Commit; + if ( $DEBUG ) { + if ($val) { + warn "Action committed: ".ref($_)." #".$_->TicketObj->Id."\n"; + } + else { + warn "Action returned $msg: #".$_->TicketObj->Id."\n"; + } } } return; @@ -98,11 +117,13 @@ sub action { ScripAction => $void{'ScripAction'}, CurrentUser => $CurrentUser, ); - return unless $action_obj->Prepare; - warn "Action prepared: $action\n" if $DEBUG; - $action_obj->Commit; - warn "Action committed: $action\n" if $DEBUG; - return; + if ( $action_obj->Prepare ) { + warn "Action prepared: $action\n" if $DEBUG; + return $action_obj; + } + else { + return; + } } 1; diff --git a/FS/FS/TicketSystem.pm b/FS/FS/TicketSystem.pm index d53d2f679..f5c8e7dad 100644 --- a/FS/FS/TicketSystem.pm +++ b/FS/FS/TicketSystem.pm @@ -29,23 +29,61 @@ sub AUTOLOAD { sub _upgrade_data { return if $system ne 'RT_Internal'; - my ($class, %opts) = @_; - my ($t, $exec, @fields) = map { driver_name =~ /^mysql/i ? $_ : lc($_) } - (qw( ScripConditions ExecModule - Name Description ExecModule ApplicableTransTypes - Creator Created LastUpdatedBy LastUpdated)); - my $count_sql = "SELECT COUNT(*) FROM $t WHERE $exec = 'CustomFieldChange'"; - my $sth = dbh->prepare($count_sql) or die dbh->errstr; - $sth->execute or die $sth->errstr; - my $total = $sth->fetchrow_arrayref->[0]; - return if $total > 0; - - my $insert_sql = "INSERT INTO $t (".join(',',@fields).") VALUES (". - "'On Custom Field Change', 'When a custom field is changed to some value', - 'CustomFieldChange', 'Any', 1, CURRENT_DATE, 1, CURRENT_DATE )"; - $sth = dbh->prepare($insert_sql) or die dbh->errstr; - $sth->execute or die $sth->errstr; + + # go ahead and use the RT API for this + + FS::TicketSystem->init; + my $session = FS::TicketSystem->session(); + my $CurrentUser = $session->{'CurrentUser'} + or die 'freeside-upgrade must run as a valid RT user'; + + # CustomFieldChange scrip condition + my $ScripCondition = RT::ScripCondition->new($CurrentUser); + $ScripCondition->LoadByCols('ExecModule' => 'CustomFieldChange'); + if (!defined($ScripCondition->Id)) { + my ($val, $msg) = $ScripCondition->Create( + 'Name' => 'On Custom Field Change', + 'Description' => 'When a custom field is changed to some value', + 'ExecModule' => 'CustomFieldChange', + 'ApplicableTransTypes' => 'Any', + ); + die $msg if !$val; + } + + # SetPriority scrip action + my $ScripAction = RT::ScripAction->new($CurrentUser); + $ScripAction->LoadByCols('ExecModule' => 'SetPriority'); + if (!defined($ScripAction->Id)) { + my ($val, $msg) = $ScripAction->Create( + 'Name' => 'Set Priority', + 'Description' => 'Set ticket priority', + 'ExecModule' => 'SetPriority', + 'Argument' => '', + ); + die $msg if !$val; + } + + # EscalateQueue custom field and friends + my $CF = RT::CustomField->new($CurrentUser); + $CF->Load('EscalateQueue'); + if (!defined($CF->Id)) { + my ($val, $msg) = $CF->Create( + 'Name' => 'EscalateQueue', + 'Type' => 'Select', + 'MaxValues' => 1, + 'LookupType' => 'RT::Queue', + 'Description' => 'Escalate to Queue', + 'ValuesClass' => 'RT::CustomFieldValues::Queues', #magic! + ); + die $msg if !$val; + my $OCF = RT::ObjectCustomField->new($CurrentUser); + ($val, $msg) = $OCF->Create( + 'CustomField' => $CF->Id, + 'ObjectId' => 0, + ); + die $msg if !$val; + } return; } diff --git a/rt/FREESIDE_MODIFIED b/rt/FREESIDE_MODIFIED index 70801d50b..5bef0bf49 100644 --- a/rt/FREESIDE_MODIFIED +++ b/rt/FREESIDE_MODIFIED @@ -17,6 +17,7 @@ lib/RT/Scrip_Overlay.pm #create ticket on custom field change lib/RT/Action/CreateTickets.pm #create ticket on custom field change lib/RT/Action/EscalatePriority.pm #ticket escalation lib/RT/Action/EscalateQueue.pm #ticket escalation +lib/RT/Action/SetPriority_Local.pm #ticket escalation lib/RT/CustomFieldValues/Queues.pm #ticket escalation lib/RT/Condition/CustomFieldChange.pm #create ticket on custom field change lib/RT/Interface/Web_Vendor.pm diff --git a/rt/etc/RT_SiteConfig.pm b/rt/etc/RT_SiteConfig.pm index f833f0d52..872b8c3a5 100644 --- a/rt/etc/RT_SiteConfig.pm +++ b/rt/etc/RT_SiteConfig.pm @@ -53,4 +53,8 @@ Set($MessageBoxRichTextHeight, 368); #Set($QuickCreateRedirect, 1); #Set(@Plugins,(qw(Extension::QuickDelete RT::FM))); + +#used in ticket escalation +Set(@CustomFieldValuesSources, ('RT::CustomFieldValues::Queues')); + 1; diff --git a/rt/lib/RT/Action/EscalateQueue.pm b/rt/lib/RT/Action/EscalateQueue.pm new file mode 100755 index 000000000..adafbdfb7 --- /dev/null +++ b/rt/lib/RT/Action/EscalateQueue.pm @@ -0,0 +1,141 @@ +=head1 NAME + +RT::Action::EscalateQueue - move a ticket to a different queue when it reaches its final priority + +=head1 DESCRIPTION + +EscalateQueue is a ScripAction that will move a ticket to a new +queue when its priority equals its final priority. It is designed +to be used with LinearEscalate or another action that increments +ticket priority on some schedule. Like those actions, it is intended +to be called from an escalation tool. + +=head1 CONFIGURATION + +FinalPriority is a ticket property, defaulting to the queue property. + +EscalateQueue is a queue custom field using RT::CustomFieldValues::Queue +as its data source (that is, it refers to another queue). Tickets at +FinalPriority will be moved to that queue. + +From a shell you can use the following command: + + rt-crontool --search RT::Search::FromSQL --search-arg \ + "(Status='new' OR Status='open' OR Status = 'stalled')" \ + --action RT::Action::EscalateQueue + +No action argument is needed. Each ticket will be escalated based on the +EscalateQueue property of its current queue. + +=cut + +package RT::Action::EscalateQueue; + +use strict; +use warnings; +use base qw(RT::Action); + +our $VERSION = '0.01'; + +#What does this type of Action does + +sub Describe { + my $self = shift; + my $class = ref($self) || $self; + return "$class will move a ticket to its escalation queue when it reaches its final priority." +} + +#This Prepare only returns 1 if the ticket will be escalated. + +sub Prepare { + my $self = shift; + + my $ticket = $self->TicketObj; + my $queue = $ticket->QueueObj; + my $new_queue = $queue->FirstCustomFieldValue('EscalateQueue'); + + my $ticketid = 'Ticket #'.$ticket->Id; #for debug messages + if ( $ticket->InitialPriority == $ticket->FinalPriority ) { + $RT::Logger->debug("$ticketid has no priority range. Not escalating."); + return 0; + } + + if ( $ticket->Priority == $ticket->FinalPriority ) { + if (!$new_queue) { + $RT::Logger->debug("$ticketid has no escalation queue. Not escalating."); + return 0; + } + if ($new_queue eq $queue->Name) { + $RT::Logger->debug("$ticketid would be escalated to its current queue."); + return 0; + } + $self->{'new_queue'} = $new_queue; + return 1; + } + return 0; +} + +# whereas Commit returns 1 if it succeeds at whatever it's doing +sub Commit { + my $self = shift; + + return 1 if !exists($self->{'new_queue'}); + + my $ticket = $self->TicketObj; + my $ticketid = 'Ticket #'.$ticket->Id; + my $new_queue = RT::Queue->new($ticket->CurrentUser); + $new_queue->Load($self->{'new_queue'}); + if ( ! $new_queue ) { + $RT::Logger->debug("Escalation queue ".$self->{'new_queue'}." not found."); + return 0; + } + + $RT::Logger->debug("Escalating $ticket from ".$ticket->QueueObj->Name . + ' to '. $new_queue->Name . ', FinalPriority '.$new_queue->FinalPriority); + + my ( $val, $msg ) = $ticket->SetQueue($self->{'new_queue'}); + if (! $val) { + $RT::Logger->error( "Couldn't set queue: $msg" ); + return (0, $msg); + } + + # Set properties of the ticket according to its new queue, so that + # escalation Does What You Expect. Don't record transactions for this; + # the queue change should be enough. + + ( $val, $msg ) = $ticket->_Set( + Field => 'FinalPriority', + Value => $new_queue->FinalPriority, + RecordTransaction => 0, + ); + if (! $val) { + $RT::Logger->error( "Couldn't set new final priority: $msg" ); + return (0, $msg); + } + my $Due = new RT::Date( $ticket->CurrentUser ); + if ( my $due_in = $new_queue->DefaultDueIn ) { + $Due->SetToNow; + $Due->AddDays( $due_in ); + } + ( $val, $msg ) = $ticket->_Set( + Field => 'Due', + Value => $Due->ISO, + RecordTransaction => 0, + ); + if (! $val) { + $RT::Logger->error( "Couldn't set new due date: $msg" ); + return (0, $msg); + } + return 1; +} + +1; + +=head1 AUTHOR + +Mark Wells Emark@freeside.bizE + +Based on in part LinearEscalate by Kevin Riggle Ekevinr@bestpractical.comE +and Ruslan Zakirov Eruz@bestpractical.comE . + +=cut diff --git a/rt/lib/RT/Action/SetPriority_Local.pm b/rt/lib/RT/Action/SetPriority_Local.pm new file mode 100644 index 000000000..efaadc961 --- /dev/null +++ b/rt/lib/RT/Action/SetPriority_Local.pm @@ -0,0 +1,47 @@ +package RT::Action::SetPriority; +use strict; +no warnings 'redefine'; + +# Extension to allow relative priority changes: +# if Argument is "R" followed by a value, it's +# relative to current priority. +sub Commit { + my $self = shift; + my ($rel, $val); + my $arg = $self->Argument; + if ( $arg ) { + ($rel, $val) = ( $arg =~ /^(r?)(-?\d+)$/i ); + if (!length($val)) { + warn "Bad argument to SetPriority: '$arg'\n"; + return 0; + } + } + else { + my %Rules = $self->Rules; + $rel = length($Rules{'inc'}) ? 1 : 0; + $val = $Rules{'inc'} || $Rules{'set'}; + if ($val !~ /^[+-]?\d+$/) { + warn "Bad argument to SetPriority: '$val'\n"; + return 0; + } + } + $val += $self->TicketObj->Priority if $rel; + $self->TicketObj->SetPriority($val); +} + +sub Options { + ( + { + 'name' => 'set', + 'label' => 'Set to value', + 'type' => 'text', + }, + { + 'name' => 'inc', + 'label' => 'Increment by', + 'type' => 'text', + }, + ) +} + +1; diff --git a/rt/lib/RT/CustomFieldValues/Queues.pm b/rt/lib/RT/CustomFieldValues/Queues.pm new file mode 100644 index 000000000..59529b6ac --- /dev/null +++ b/rt/lib/RT/CustomFieldValues/Queues.pm @@ -0,0 +1,30 @@ +package RT::CustomFieldValues::Queues; + +use strict; +use warnings; + +use base qw(RT::CustomFieldValues::External); + +sub SourceDescription { + return 'RT ticket queues'; +} + +sub ExternalValues { + my $self = shift; + + my @res; + my $i = 0; + my $queues = RT::Queues->new( $self->CurrentUser ); + $queues->UnLimit; + $queues->OrderByCols( { FIELD => 'Name' } ); + while( my $queue = $queues->Next ) { + push @res, { + name => $queue->Name, + description => $queue->Description, + sortorder => $i++, + }; + } + return \@res; +} + +1; -- 2.11.0