RT future ticket resolve, #13853
authormark <mark>
Tue, 23 Aug 2011 21:45:51 +0000 (21:45 +0000)
committermark <mark>
Tue, 23 Aug 2011 21:45:51 +0000 (21:45 +0000)
18 files changed:
FS/FS/Cron/rt_tasks.pm
FS/FS/TicketSystem.pm
FS/FS/Upgrade.pm
FS/bin/freeside-daily
FS/bin/freeside-upgrade
rt/etc/initialdata
rt/etc/schema.Pg
rt/etc/schema.mysql-4.1
rt/lib/RT/Action/ScheduledResolve.pm [new file with mode: 0644]
rt/lib/RT/Action/SetWillResolve.pm [new file with mode: 0644]
rt/lib/RT/Interface/Web_Vendor.pm
rt/lib/RT/Ticket_Vendor.pm
rt/lib/RT/Tickets_Overlay.pm
rt/lib/RT/Transaction_Vendor.pm [new file with mode: 0644]
rt/share/html/Elements/SelectStatus
rt/share/html/Ticket/Elements/EditDates
rt/share/html/Ticket/Elements/ShowDates
rt/share/html/Ticket/Update.html

index 26e305d..6658b47 100644 (file)
@@ -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;
index 63ab865..169f0dc 100644 (file)
@@ -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) = @_;
index 40d3473..03d24f7 100644 (file)
@@ -303,6 +303,8 @@ sub upgrade_schema_data {
 
     #fix classnum character(1)
     'cust_bill_pkg_detail' => [],
+    #add necessary columns to RT schema
+    'TicketSystem' => [],
 
   ;
 
index a7c38d5..2beb096 100755 (executable)
@@ -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*>;
index 6f4e439..b08a840 100755 (executable)
@@ -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";
index fc2ed2f..7b3f6a6 100644 (file)
       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 = (
index e3006d0..32c5e87 100755 (executable)
@@ -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  ,
index 1735702..edd3ded 100755 (executable)
@@ -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 (file)
index 0000000..6b323cb
--- /dev/null
@@ -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 (file)
index 0000000..807b3c6
--- /dev/null
@@ -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;
index c79222b..27c647f 100644 (file)
@@ -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);
 }
 
index 2039f3e..9fa24a2 100644 (file)
@@ -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;
index 876f108..f6df553 100644 (file)
@@ -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 (file)
index 0000000..caeb3f7
--- /dev/null
@@ -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;
+
index 7aa7aa5..5718a2a 100755 (executable)
@@ -45,7 +45,8 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-<select name="<%$Name%>">
+% my $onchange_attr = $onchange ? " onchange=\"$onchange\"" : '';
+<select id="<%$Name%>" name="<%$Name%>"<% $onchange_attr |n%>>
 %if ($DefaultValue) {
 <option value=""<% !$Default && qq[ selected="selected"] |n %>><%$DefaultLabel%></option>
 %}
@@ -64,4 +65,5 @@ $Default => ''
 $SkipDeleted => 0
 $DefaultValue => 1
 $DefaultLabel => "-"
+$onchange => ''
 </%ARGS>
index bfa3a30..371f6e3 100755 (executable)
       <& /Elements/SelectDate, menu_prefix => 'Due', current => 0 &> (<% $TicketObj->DueObj->AsString %>)
     </td>
   </tr>
+  <tr>
+    <td class="label"><&|/l&>Close After</&>:</td>
+    <td class="entry">
+      <& /Elements/SelectDate, menu_prefix => 'WillResolve', current => 0 &> (<% $TicketObj->WillResolveObj->AsString %>)
+    </td>
+  </tr>
 % $m->callback( %ARGS, CallbackName => 'EndOfList', Ticket => $TicketObj );
 </table>
 <%ARGS>
index 1a79628..fc01461 100755 (executable)
     <td class="label date resolved"><&|/l&>Closed</&>:</td>
     <td class="value date resolved"><% $Ticket->ResolvedObj->AsString  %></td>
   </tr>
+% my $willresolve = $Ticket->WillResolveObj;
+% if ( $willresolve && $willresolve->Unix > 0 ) {
+  <tr>
+    <td class="label date willresolve"><&|/l&>Will Resolve</&>:</td>
+    <td class="value date willresolve"><% $willresolve->AsString %></td>
+  </tr>
+% } # else don't display either of them
   <tr>
     <td class="label date updated"><&|/l&>Updated</&>:</td>
 % my $UpdatedString = $Ticket->LastUpdated ? loc("[_1] by [_2]", $Ticket->LastUpdatedAsString, $m->scomp('/Elements/ShowUser', User => $Ticket->LastUpdatedByObj)) : loc("Never");
index 62db0d1..7c28cc3 100755 (executable)
 
 <tr><td valign="baseline" class="label"><&|/l&>Status</&>:</td>
 <td valign="baseline">
-<& /Elements/SelectStatus, Name=>"Status", DefaultLabel => loc("[_1] (Unchanged)", loc($TicketObj->Status)), Default => $ARGS{'Status'} || ($TicketObj->Status eq $DefaultStatus ? undef : $DefaultStatus)&>
+<script type="text/javascript">
+function changeStatus() {
+  var Status_select = document.getElementById('Status');
+  var x = Status_select.options[Status_select.selectedIndex].value;
+  var text = document.getElementById('WillResolve_Date');
+  var button = document.getElementById('WillResolve_Date_date_button');
+  if (x == 'resolved' || x == 'rejected' || x == 'deleted') {
+    text.disabled = true;
+    button.style.display = 'none';
+  }
+  else {
+    text.disabled = false;
+    button.style.display = 'inline';
+  }
+}
+</script>
+<& /Elements/SelectStatus, 
+    Name=>"Status",
+    DefaultLabel => loc("[_1] (Unchanged)", loc($TicketObj->Status)),
+    Default => $ARGS{'Status'}
+              || ($TicketObj->Status eq $DefaultStatus ? undef : $DefaultStatus,
+    onchange => 'changeStatus()'
+)&>
 <span class="label"><&|/l&>Owner</&>:</span>
 <& /Elements/SelectOwner,
     Name         => "Owner",
     DefaultLabel => loc("[_1] (Unchanged)", $m->scomp('/Elements/ShowUser', User => $TicketObj->OwnerObj)),
     Default      => $ARGS{'Owner'}
 &>
+<span class="label"><&|/l&>Close this Ticket on</&>:</span>
+<& /Elements/SelectDate, 
+    menu_prefix => 'WillResolve',
+    current => 0,
+    ShowTime => 0,
+&>
+% if ( $TicketObj->WillResolve ) {
+<span class="label"> (<% $TicketObj->WillResolveObj->AsString %>)</span>
+% }
+<script type="text/javascript">
+changeStatus();
+</script>
 </td>
 <td rowspan=4 valign="top">
 <table style="float:right;">