From ab8aef9ec21df4b149f39cd24c9c5f3542dd2e3e Mon Sep 17 00:00:00 2001 From: mark Date: Tue, 23 Aug 2011 21:46:34 +0000 Subject: [PATCH] RT future ticket resolve, #13853 --- FS/FS/Cron/rt_tasks.pm | 86 +++++++++++++++++++++------------ FS/FS/TicketSystem.pm | 49 +++++++++++++++++++ FS/FS/Upgrade.pm | 2 + FS/bin/freeside-daily | 4 +- FS/bin/freeside-upgrade | 15 ------ rt/etc/initialdata | 14 ++++++ rt/etc/schema.Pg | 1 + rt/etc/schema.mysql-4.1 | 1 + rt/lib/RT/Action/ScheduledResolve.pm | 37 ++++++++++++++ rt/lib/RT/Action/SetWillResolve.pm | 27 +++++++++++ rt/lib/RT/Interface/Web_Vendor.pm | 56 ++++++++++++++++++++- rt/lib/RT/Ticket_Vendor.pm | 29 +++++++++++ rt/lib/RT/Tickets_Overlay.pm | 2 + rt/lib/RT/Transaction_Vendor.pm | 35 ++++++++++++++ rt/share/html/Elements/SelectStatus | 4 +- rt/share/html/Ticket/Elements/EditDates | 6 +++ rt/share/html/Ticket/Elements/ShowDates | 7 +++ rt/share/html/Ticket/Update.html | 36 +++++++++++++- 18 files changed, 359 insertions(+), 52 deletions(-) create mode 100644 rt/lib/RT/Action/ScheduledResolve.pm create mode 100644 rt/lib/RT/Action/SetWillResolve.pm create mode 100644 rt/lib/RT/Transaction_Vendor.pm diff --git a/FS/FS/Cron/rt_tasks.pm b/FS/FS/Cron/rt_tasks.pm index 26e305d59..6658b4781 100644 --- a/FS/FS/Cron/rt_tasks.pm +++ b/FS/FS/Cron/rt_tasks.pm @@ -1,7 +1,7 @@ package FS::Cron::rt_tasks; use strict; -use vars qw( @ISA @EXPORT_OK $DEBUG ); +use vars qw( @ISA @EXPORT_OK $DEBUG $conf ); use Exporter; use FS::UID qw( dbh driver_name ); use FS::Record qw(qsearch qsearchs); @@ -11,83 +11,91 @@ use FS::Conf; use Date::Parse qw(str2time); @ISA = qw( Exporter ); -@EXPORT_OK = qw ( rt_escalate ); +@EXPORT_OK = qw ( rt_daily ); $DEBUG = 0; +FS::UID->install_callback( sub { + eval "use FS::Conf;"; + die $@ if $@; + $conf = FS::Conf->new; +}); + + my %void = (); -sub rt_escalate { +sub rt_daily { my %opt = @_; + my @custnums = @ARGV; # ick + # RT_External installations should have their own cron scripts for this my $system = $FS::TicketSystem::system; return if $system ne 'RT_Internal'; - my $conf = new FS::Conf; - return if !$conf->exists('ticket_system-escalation'); - FS::TicketSystem->init; $DEBUG = 1 if $opt{'v'}; RT::Config->Set( LogToScreen => 'debug' ) if $DEBUG; - - #we're at now now (and later). - my $time = $opt{'d'} ? str2time($opt{'d'}) : $^T; - $time += $opt{'y'} * 86400 if $opt{'y'}; - my $error = ''; + + # if -d or -y is in use, bail out. There's no reliable way to tell RT + # to use an alternate system time. + if ( $opt{'d'} or $opt{'y'} ) { + warn "Forced date options in use - RT daily tasks skipped.\n"; + return; + } my $session = FS::TicketSystem->session(); my $CurrentUser = $session->{'CurrentUser'} or die "Failed to create RT session"; - + # load some modules that aren't handled in FS::TicketSystem foreach (qw( - Search::ActiveTicketsInQueue + Search::ActiveTicketsInQueue Action::EscalatePriority Action::EscalateQueue + Action::ScheduledResolve )) { eval "use RT::$_"; die $@ if $@; } # adapted from rt-crontool - # Mechanics: - # We're using EscalatePriority, so search in all queues that have a - # priority range defined. Select all active tickets in those queues and - # EscalatePriority, then EscalateQueue them. # to make some actions work without complaining %void = map { $_ => "RT::$_"->new($CurrentUser) } (qw(Scrip ScripAction)); - # Most of this stuff is common to any condition -> action processing - # we might want to do, but escalation is the only one we do now. + # compile actions to be run + my (@actions, @active_tickets); 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; - next; - } warn "Queue '".$queue->Name."'\n" if $DEBUG; + my $CurrentUser = $queue->CurrentUser; + my %opt = @_; my $tickets = RT::Tickets->new($CurrentUser); my $search = RT::Search::ActiveTicketsInQueue->new( TicketsObj => $tickets, - Argument => $queue->Name, + Argument => $queue->Id, CurrentUser => $CurrentUser, ); $search->Prepare; + foreach my $custnum ( @custnums ) { + die "invalid custnum passed to rt_daily: $custnum" + if !$custnum =~ /^\d+$/; + $tickets->LimitMemberOf( + "freeside://freeside/cust_main/$custnum", + ENTRYAGGREGATOR => 'OR', + SUBCLAUSE => 'custnum' + ); + } while (my $ticket = $tickets->Next) { warn 'Ticket #'.$ticket->Id()."\n" if $DEBUG; - my @a = ( - action($ticket, 'EscalatePriority', "CurrentTime:$time"), - action($ticket, 'EscalateQueue') - ); - next if !@a; + my @a = task_actions($ticket); push @actions, @a; - push @active_tickets, $ticket; # avoid RT's overzealous garbage collector + push @active_tickets, $ticket if @a; # avoid garbage collection } } + + # and then commit them all foreach (grep {$_} @actions) { my ($val, $msg) = $_->Commit; if ( $DEBUG ) { @@ -102,6 +110,20 @@ sub rt_escalate { return; } +sub task_actions { + my $ticket = shift; + ( + ### escalation ### + $conf->exists('ticket_system-escalation') ? ( + action($ticket, 'EscalatePriority', "CurrentTime: $^T"), + action($ticket, 'EscalateQueue') + ) : (), + + ### scheduled resolve ### + action($ticket, 'ScheduledResolve'), + ); +} + sub action { my $ticket = shift; my $CurrentUser = $ticket->CurrentUser; diff --git a/FS/FS/TicketSystem.pm b/FS/FS/TicketSystem.pm index 63ab865c4..169f0dc4d 100644 --- a/FS/FS/TicketSystem.pm +++ b/FS/FS/TicketSystem.pm @@ -4,6 +4,7 @@ use strict; use vars qw( $conf $system $AUTOLOAD ); use FS::Conf; use FS::UID qw( dbh driver_name ); +use FS::Record qw( dbdef ); FS::UID->install_callback( sub { $conf = new FS::Conf; @@ -27,6 +28,54 @@ sub AUTOLOAD { $self->$sub(@_); } +# Our schema changes +my %columns = ( + Tickets => { + WillResolve => { type => 'timestamp', null => 1, default => '', }, + }, + CustomFields => { + Required => { type => 'integer', default => 0, null => 0 }, + }, +); + +sub _upgrade_schema { + my $system = FS::Conf->new->config('ticket_system'); + return if !defined($system) || $system ne 'RT_Internal'; + my ($class, %opts) = @_; + + my $dbh = dbh; + my @sql; + my $case = driver_name eq 'mysql' ? sub {@_} : sub {map lc, @_}; + foreach my $tablename (keys %columns) { + my $table = dbdef->table(&$case($tablename)); + if ( !$table ) { + warn + "$tablename table does not exist. Your RT installation is incomplete.\n"; + next; + } + foreach my $colname (keys %{ $columns{$tablename} }) { + if ( !$table->column(&$case($colname)) ) { + my $col = new DBIx::DBSchema::Column { + table_obj => $table, + name => &$case($colname), + %{ $columns{$tablename}->{$colname} } + }; + $col->table_obj($table); + push @sql, $col->sql_add_column($dbh); + } + } #foreach $colname + } #foreach $tablename + + return if !@sql; + warn "Upgrading RT schema:\n"; + foreach my $statement (@sql) { + warn "$statement\n"; + $dbh->do( $statement ) + or die "Error: ". $dbh->errstr. "\n executing: $statement"; + } + return; +} + sub _upgrade_data { return if !defined($system) || $system ne 'RT_Internal'; my ($class, %opts) = @_; diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index 40d347327..03d24f7f6 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -303,6 +303,8 @@ sub upgrade_schema_data { #fix classnum character(1) 'cust_bill_pkg_detail' => [], + #add necessary columns to RT schema + 'TicketSystem' => [], ; diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily index a7c38d557..2beb096ab 100755 --- a/FS/bin/freeside-daily +++ b/FS/bin/freeside-daily @@ -62,8 +62,8 @@ use FS::Cron::backup qw(backup); backup(); #same -use FS::Cron::rt_tasks qw(rt_escalate); -rt_escalate(%opt); +use FS::Cron::rt_tasks qw(rt_daily); +rt_daily(%opt); my $deldir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/"; unlink <${deldir}.invoice*>; diff --git a/FS/bin/freeside-upgrade b/FS/bin/freeside-upgrade index 16c30d9cf..48304d319 100755 --- a/FS/bin/freeside-upgrade +++ b/FS/bin/freeside-upgrade @@ -84,21 +84,6 @@ if ( dbdef->table('areacode') and } } -# RT required field flag -# for consistency with RT schema: mysql is in CamelCase, -# pg is in lowercase, and they use different data types. -my ($t, $creq, $cdis) = - map { driver_name =~ /^mysql/i ? $_ : lc($_) } - ('CustomFields','Required','Disabled'); - -if ( dbdef->table($t) && - ! dbdef->table($t)->column($creq) ) { - push @bugfix, - "ALTER TABLE $t ADD COLUMN $creq ". - dbdef->table($t)->column($cdis)->type . - ' NOT NULL DEFAULT 0'; -} - if ( $DRY_RUN ) { print join(";\n", @bugfix ). ";\n"; diff --git a/rt/etc/initialdata b/rt/etc/initialdata index fc2ed2ffb..7b3f6a6bf 100644 --- a/rt/etc/initialdata +++ b/rt/etc/initialdata @@ -104,6 +104,16 @@ ExecModule => 'SetPriority', Argument => '', }, + { Name => 'Cancel Scheduled Resolve', + Description => 'Set ticket not to resolve in the future', + ExecModule => 'SetWillResolve', + Argument => '', + }, + { Name => 'Scheduled Resolve', + Description => 'Resolve ticket if its WillResolve date has passed', + ExecModule => 'ScheduledResolve', + Argument => '', + }, ); @ScripConditions = ( @@ -564,6 +574,10 @@ Hour: { $SubscriptionObj->SubValue('Hour') } ScripCondition => 'On Transaction', ScripAction => 'Extract Subject Tag', Template => 'Blank' }, + { Description => 'On Correspond, cancel future resolve', + ScripCondition => 'On Correspond', + ScripAction => 'Cancel Scheduled Resolve', + Template => 'Blank' }, ); @ACL = ( diff --git a/rt/etc/schema.Pg b/rt/etc/schema.Pg index e3006d073..32c5e872d 100755 --- a/rt/etc/schema.Pg +++ b/rt/etc/schema.Pg @@ -404,6 +404,7 @@ CREATE TABLE Tickets ( Due TIMESTAMP NULL , Resolved TIMESTAMP NULL , + WillResolve TIMESTAMP NULL , LastUpdatedBy integer NOT NULL DEFAULT 0 , LastUpdated TIMESTAMP NULL , diff --git a/rt/etc/schema.mysql-4.1 b/rt/etc/schema.mysql-4.1 index 173570219..edd3deda7 100755 --- a/rt/etc/schema.mysql-4.1 +++ b/rt/etc/schema.mysql-4.1 @@ -289,6 +289,7 @@ CREATE TABLE Tickets ( Due DATETIME NULL , Resolved DATETIME NULL , + WillResolve DATETIME NULL , LastUpdatedBy integer NOT NULL DEFAULT 0 , LastUpdated DATETIME NULL , diff --git a/rt/lib/RT/Action/ScheduledResolve.pm b/rt/lib/RT/Action/ScheduledResolve.pm new file mode 100644 index 000000000..6b323cb6f --- /dev/null +++ b/rt/lib/RT/Action/ScheduledResolve.pm @@ -0,0 +1,37 @@ +package RT::Action::ScheduledResolve; + +use strict; +use warnings; + +use base qw(RT::Action); + +=head1 DESCRIPTION + +If the ticket's WillResolve date is in the past, set its status to resolved. + +=cut + +sub Prepare { + my $self = shift; + + return undef if grep { $self->TicketObj->Status eq $_ } ( + 'resolved', + 'rejected', + 'deleted' + ); # don't resolve from any of these states. + my $time = $self->TicketObj->WillResolveObj->Unix; + return ( $time > 0 and $time < time() ); +} + +sub Commit { + my $self = shift; + + my $never = RT::Date->new($self->CurrentUser); + $never->Unix(-1); + $self->TicketObj->SetWillResolve($never->ISO); + $self->TicketObj->SetStatus('resolved'); +} + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/lib/RT/Action/SetWillResolve.pm b/rt/lib/RT/Action/SetWillResolve.pm new file mode 100644 index 000000000..807b3c64c --- /dev/null +++ b/rt/lib/RT/Action/SetWillResolve.pm @@ -0,0 +1,27 @@ +package RT::Action::SetWillResolve; +use base 'RT::Action'; + +use strict; + +sub Describe { + my $self = shift; + return (ref $self ." will set a ticket's future resolve date to the argument."); +} + +sub Prepare { + return 1; +} + +sub Commit { + my $self = shift; + my $DateObj = RT::Date->new( $self->CurrentUser ); + $DateObj->Set( + Format => 'unknown', + Value => $self->Argument, + ); + $self->TicketObj->SetWillResolve( $DateObj->ISO ); +} + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/lib/RT/Interface/Web_Vendor.pm b/rt/lib/RT/Interface/Web_Vendor.pm index c79222be5..27c647f18 100644 --- a/rt/lib/RT/Interface/Web_Vendor.pm +++ b/rt/lib/RT/Interface/Web_Vendor.pm @@ -255,8 +255,62 @@ sub ProcessTicketBasics { push( @results, $msg ); } - # }}} + return (@results); +} + +=head2 ProcessTicketDates (TicketObj => RT::Ticket, ARGSRef => {}) + +Process updates to the Starts, Started, Told, Resolved, and WillResolve +fields. + +=cut +sub ProcessTicketDates { + my %args = ( + TicketObj => undef, + ARGSRef => undef, + @_ + ); + + my $Ticket = $args{'TicketObj'}; + my $ARGSRef = $args{'ARGSRef'}; + + my (@results); + + # {{{ Set date fields + my @date_fields = qw( + Told + Resolved + Starts + Started + Due + WillResolve + ); + + #Run through each field in this list. update the value if apropriate + foreach my $field (@date_fields) { + next unless exists $ARGSRef->{ $field . '_Date' }; + next if $ARGSRef->{ $field . '_Date' } eq ''; + + my ( $code, $msg ); + + my $DateObj = RT::Date->new( $session{'CurrentUser'} ); + $DateObj->Set( + Format => 'unknown', + Value => $ARGSRef->{ $field . '_Date' } + ); + + my $obj = $field . "Obj"; + if ( ( defined $DateObj->Unix ) + and ( $DateObj->Unix != $Ticket->$obj()->Unix() ) ) + { + my $method = "Set$field"; + my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO ); + push @results, "$msg"; + } + } + + # }}} return (@results); } diff --git a/rt/lib/RT/Ticket_Vendor.pm b/rt/lib/RT/Ticket_Vendor.pm index 2039f3e2d..9fa24a2a8 100644 --- a/rt/lib/RT/Ticket_Vendor.pm +++ b/rt/lib/RT/Ticket_Vendor.pm @@ -33,4 +33,33 @@ sub MissingRequiredFields { return @results; } +# Declare the 'WillResolve' field +sub _VendorAccessible { + { + WillResolve => + {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, + }, +}; + +sub WillResolveObj { + my $self = shift; + + my $time = new RT::Date( $self->CurrentUser ); + + if ( my $willresolve = $self->WillResolve ) { + $time->Set( Format => 'sql', Value => $willresolve ); + } + else { + $time->Set( Format => 'unix', Value => -1 ); + } + + return $time; +} + +sub WillResolveAsString { + my $self = shift; + return $self->WillResolveObj->AsString(); +} + + 1; diff --git a/rt/lib/RT/Tickets_Overlay.pm b/rt/lib/RT/Tickets_Overlay.pm index 876f1084e..f6df5530d 100644 --- a/rt/lib/RT/Tickets_Overlay.pm +++ b/rt/lib/RT/Tickets_Overlay.pm @@ -145,9 +145,11 @@ our %FIELD_METADATA = ( WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair HasAttribute => [ 'HASATTRIBUTE', 1 ], HasNoAttribute => [ 'HASATTRIBUTE', 0 ], + #freeside Agentnum => [ 'FREESIDEFIELD', ], Classnum => [ 'FREESIDEFIELD', ], Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ], + WillResolve => [ 'DATE' => 'WillResolve', ], #loc_left_pair ); our %SEARCHABLE_SUBFIELDS = ( diff --git a/rt/lib/RT/Transaction_Vendor.pm b/rt/lib/RT/Transaction_Vendor.pm new file mode 100644 index 000000000..caeb3f72c --- /dev/null +++ b/rt/lib/RT/Transaction_Vendor.pm @@ -0,0 +1,35 @@ +package RT::Transaction; +use strict; +use vars qw(%_BriefDescriptions); + +$_BriefDescriptions{'Set'} = sub { + my $self = shift; + if ( $self->Field eq 'Password' ) { + return $self->loc('Password changed'); + } + elsif ( $self->Field eq 'Queue' ) { + my $q1 = new RT::Queue( $self->CurrentUser ); + $q1->Load( $self->OldValue ); + my $q2 = new RT::Queue( $self->CurrentUser ); + $q2->Load( $self->NewValue ); + return $self->loc("[_1] changed from [_2] to [_3]", + $self->loc($self->Field) , $q1->Name , $q2->Name); + } + + # Write the date/time change at local time: + elsif ($self->Field =~ /Due|Starts|Started|Told|WillResolve/) { + my $t1 = new RT::Date($self->CurrentUser); + $t1->Set(Format => 'ISO', Value => $self->NewValue); + my $t2 = new RT::Date($self->CurrentUser); + $t2->Set(Format => 'ISO', Value => $self->OldValue); + return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); + } + else { + return $self->loc( "[_1] changed from [_2] to [_3]", + $self->loc($self->Field), + ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" ); + } +}; + +1; + diff --git a/rt/share/html/Elements/SelectStatus b/rt/share/html/Elements/SelectStatus index 7aa7aa528..5718a2a9d 100755 --- a/rt/share/html/Elements/SelectStatus +++ b/rt/share/html/Elements/SelectStatus @@ -45,7 +45,8 @@ %# those contributions and any derivatives thereof. %# %# END BPS TAGGED BLOCK }}} -> %if ($DefaultValue) { %} @@ -64,4 +65,5 @@ $Default => '' $SkipDeleted => 0 $DefaultValue => 1 $DefaultLabel => "-" +$onchange => '' diff --git a/rt/share/html/Ticket/Elements/EditDates b/rt/share/html/Ticket/Elements/EditDates index bfa3a3049..371f6e31e 100755 --- a/rt/share/html/Ticket/Elements/EditDates +++ b/rt/share/html/Ticket/Elements/EditDates @@ -70,6 +70,12 @@ <& /Elements/SelectDate, menu_prefix => 'Due', current => 0 &> (<% $TicketObj->DueObj->AsString %>) + + <&|/l&>Close After: + + <& /Elements/SelectDate, menu_prefix => 'WillResolve', current => 0 &> (<% $TicketObj->WillResolveObj->AsString %>) + + % $m->callback( %ARGS, CallbackName => 'EndOfList', Ticket => $TicketObj ); <%ARGS> diff --git a/rt/share/html/Ticket/Elements/ShowDates b/rt/share/html/Ticket/Elements/ShowDates index 1a79628a9..fc0146194 100755 --- a/rt/share/html/Ticket/Elements/ShowDates +++ b/rt/share/html/Ticket/Elements/ShowDates @@ -75,6 +75,13 @@ <&|/l&>Closed: <% $Ticket->ResolvedObj->AsString %> +% my $willresolve = $Ticket->WillResolveObj; +% if ( $willresolve && $willresolve->Unix > 0 ) { + + <&|/l&>Will Resolve: + <% $willresolve->AsString %> + +% } # else don't display either of them <&|/l&>Updated: % my $UpdatedString = $Ticket->LastUpdated ? loc("[_1] by [_2]", $Ticket->LastUpdatedAsString, $m->scomp('/Elements/ShowUser', User => $Ticket->LastUpdatedByObj)) : loc("Never"); diff --git a/rt/share/html/Ticket/Update.html b/rt/share/html/Ticket/Update.html index 62db0d1c3..7c28cc30d 100755 --- a/rt/share/html/Ticket/Update.html +++ b/rt/share/html/Ticket/Update.html @@ -67,7 +67,29 @@ <&|/l&>Status: -<& /Elements/SelectStatus, Name=>"Status", DefaultLabel => loc("[_1] (Unchanged)", loc($TicketObj->Status)), Default => $ARGS{'Status'} || ($TicketObj->Status eq $DefaultStatus ? undef : $DefaultStatus)&> + +<& /Elements/SelectStatus, + Name=>"Status", + DefaultLabel => loc("[_1] (Unchanged)", loc($TicketObj->Status)), + Default => $ARGS{'Status'} + || ($TicketObj->Status eq $DefaultStatus ? undef : $DefaultStatus, + onchange => 'changeStatus()' +)&> <&|/l&>Owner: <& /Elements/SelectOwner, Name => "Owner", @@ -76,6 +98,18 @@ DefaultLabel => loc("[_1] (Unchanged)", $m->scomp('/Elements/ShowUser', User => $TicketObj->OwnerObj)), Default => $ARGS{'Owner'} &> +<&|/l&>Close this Ticket on: +<& /Elements/SelectDate, + menu_prefix => 'WillResolve', + current => 0, + ShowTime => 0, +&> +% if ( $TicketObj->WillResolve ) { + (<% $TicketObj->WillResolveObj->AsString %>) +% } + -- 2.11.0