[Rt-commit] rt branch 5.0/create-rt-crontool-admin-interface created. rt-5.0.4-230-g121d09075d

BPS Git Server git at git.bestpractical.com
Sun Sep 24 22:07:27 UTC 2023


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rt".

The branch, 5.0/create-rt-crontool-admin-interface has been created
        at  121d09075d286293b2bd6163ceef0fc7e2c30d9d (commit)

- Log -----------------------------------------------------------------
commit 121d09075d286293b2bd6163ceef0fc7e2c30d9d
Author: Brad Embree <brad at bestpractical.com>
Date:   Sun Sep 24 14:18:35 2023 -0700

    Add menu option for new Crontool list page

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 19d0e68920..c8e1eec4ae 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -1372,6 +1372,15 @@ sub _BuildAdminMenu {
         path        => '/Admin/Tools/Shortener.html',
     );
 
+    if ( $current_user->HasRight( Right => 'SuperUser', Object => RT->System ) ) {
+        $admin_tools->child( 'crontool' => title => loc('Crontool Jobs'), path => "/Admin/Tools/Crontool.html" );
+
+        if ( $request_path =~ m{^/Admin/Tools/Crontool} ) {
+            $page->child( 'list' => title => loc('Select'), path => "/Admin/Tools/Crontool.html" );
+            $page->child( 'job' => title => loc('Create'), path => "/Admin/Tools/CrontoolJob.html" );
+        }
+    }
+
     if ( $request_path =~ m{^/Admin/(Queues|Users|Groups|CustomFields|CustomRoles)} ) {
         my $type = $1;
 

commit 3cac811729e87d6f20aa3a11f1b9b338a5cd6bde
Author: Brad Embree <brad at bestpractical.com>
Date:   Sun Sep 24 14:17:19 2023 -0700

    Add script to run scheduled crontool jobs

diff --git a/.gitignore b/.gitignore
index f07ed8350a..fbbe16bf98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 /bin/rt-crontool
 /bin/rt-mailgate
 /bin/rt
+/bin/rt-run-scheduled-crontool
 /etc/RT_Config.pm
 /etc/upgrade/3.8-ical-extension
 /etc/upgrade/4.0-customfield-checkbox-extension
diff --git a/bin/rt-run-scheduled-crontool.in b/bin/rt-run-scheduled-crontool.in
new file mode 100644
index 0000000000..18e86b7741
--- /dev/null
+++ b/bin/rt-run-scheduled-crontool.in
@@ -0,0 +1,349 @@
+#!@PERL@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+use POSIX 'tzset';
+
+# fix lib paths, some may be relative
+my $bin_path;
+BEGIN { # BEGIN RT CMD BOILERPLATE
+    require File::Spec;
+    require Cwd;
+    my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
+
+    for my $lib (@libs) {
+        unless ( File::Spec->file_name_is_absolute($lib) ) {
+            $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1];
+            $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
+        }
+        unshift @INC, $lib;
+    }
+
+}
+
+# Read in the options
+my %opts;
+use Getopt::Long;
+GetOptions(
+    \%opts,
+    "help|h", "dryrun", "time=i", "all", "log=s"
+);
+
+if ( $opts{'help'} ) {
+    require Pod::Usage;
+    print Pod::Usage::pod2usage( -verbose => 2 );
+    exit;
+}
+
+$opts{dryrun} = 1
+    if $opts{all};
+
+require RT;
+require RT::Interface::CLI;
+
+# Load the config file
+RT::LoadConfig();
+
+# adjust logging to the screen according to options
+RT->Config->Set( LogToSTDERR => $opts{log} ) if $opts{log};
+
+RT::Init();
+
+$opts{time} ||= time;
+
+my $CrontoolJobs = RT::Attributes->new( RT->SystemUser );
+$CrontoolJobs->LimitToObject( RT->SystemUser );
+$CrontoolJobs->Limit(
+    FIELD           => 'Name',
+    VALUE           => 'Crontool',
+    OPERATOR        => '=',
+    ENTRYAGGREGATOR => 'AND',
+);
+
+my ( $minute, $hour, $dow, $dom ) = MinuteHourDowDomIn( $opts{time}, RT->Config->Get('Timezone') );
+$hour .= ':00';
+$RT::Logger->debug( "Checking crontool jobs: minute $minute, hour $hour, dow $dow, dom $dom" );
+
+while ( my $job = $CrontoolJobs->Next ) {
+    next unless IsCrontoolReady(
+        %opts,
+        Crontool  => $job,
+        LocalTime => [ $minute, $hour, $dow, $dom ],
+    );
+
+    my $crontool_success = RunCrontoolJob(
+        %opts,
+        Crontool => $job,
+    );
+
+    if ( $crontool_success ) {
+        my $counter = $job->SubValue('Counter') || 0;
+        $job->SetSubValues( Counter => $counter + 1 )
+            unless $opts{dryrun};
+    }
+}
+
+sub IsCrontoolReady {
+    my %args = (
+        all       => 0,
+        Crontool  => undef,
+        LocalTime => [0, 0, 0, 0],
+        @_,
+    );
+
+    my $crontool = $args{Crontool};
+
+    return 1 if $args{all};
+
+    my $counter       = $crontool->SubValue('Counter') || 0;
+    my $sub_frequency = $crontool->SubValue('Frequency');
+    my $sub_minute    = $crontool->SubValue('Minute');
+    my $sub_hour      = $crontool->SubValue('Hour');
+    my $sub_dow       = $crontool->SubValue('Dow');
+    my $sub_dom       = $crontool->SubValue('Dom');
+    my $sub_fow       = $crontool->SubValue('Fow') || 1;
+
+    my $log_frequency = $sub_frequency;
+    if ( $log_frequency eq 'daily' ) {
+        my $days
+            = join ' ',
+                grep { $crontool->SubValue($_) }
+                    qw/Monday Tuesday Wednesday Thursday Friday Saturday Sunday/;
+        $log_frequency = "$log_frequency ($days)";
+    }
+
+    my ( $minute, $hour, $dow, $dom ) = @{ $args{LocalTime} };
+
+    $RT::Logger->debug( "Checking crontool job " . $crontool->Id . " with frequency $log_frequency, minute $sub_minute, hour $sub_hour, dow $sub_dow, dom $sub_dom, fow $sub_fow, counter $counter" );
+
+    return 0 if $sub_frequency eq 'never';
+
+    # correct minute?
+    return 0 if $sub_minute ne $minute;
+
+    # correct hour?
+    return 0 if $sub_hour ne $hour;
+
+    if ( $sub_frequency eq 'daily' ) {
+        return $crontool->SubValue($dow) ? 1 : 0;
+    }
+
+    if ( $sub_frequency eq 'weekly' ) {
+        # correct day of week?
+        return 0 if $sub_dow ne $dow;
+
+        # does it match the "every N weeks" clause?
+        return 1 if $counter % $sub_fow == 0;
+
+        $crontool->SetSubValues( Counter => $counter + 1 )
+            unless $args{dryrun};
+
+        return 0;
+    }
+
+    # if monthly, correct day of month?
+    if ( $sub_frequency eq 'monthly' ) {
+        return $sub_dom == $dom;
+    }
+
+    $RT::Logger->debug( "Invalid frequency $sub_frequency for crontool job: " . $crontool->Id . ' - ' . $crontool->SubValue('Description') );
+
+    # unknown frequency type, bail out
+    return 0;
+}
+
+sub RunCrontoolJob {
+    my %args = (
+        Crontool => undef,
+        dryrun   => 0,
+        @_,
+    );
+
+    my $crontool = $args{Crontool};
+    my $content  = $crontool->Content;
+
+    $RT::Logger->debug( "running crontool job: " . $crontool->Id );
+
+    # build command line
+    my $cmd = "${bin_path}rt-crontool ";
+
+    if ( my $search_module = $content->{SearchModule} ) {
+        $cmd .= "--search RT::Search::$search_module ";
+        $cmd .= '--search-arg "' . $content->{SearchModuleArg} . '" '
+            if $content->{SearchModuleArg};
+    }
+    if ( my $condition_module = $content->{ConditionModule} ) {
+        $cmd .= "--condition RT::Condition::$condition_module ";
+        $cmd .= '--condition-arg "' . $content->{ConditionModuleArg} . '" '
+            if $content->{ConditionModuleArg};
+    }
+    if ( my $action_module = $content->{ActionModule} ) {
+        $cmd .= "--action RT::Action::$action_module ";
+        $cmd .= '--action-arg "' . $content->{ActionModuleArg} . '" '
+            if $content->{ActionModuleArg};
+    }
+    if ( my $template = $content->{Template} ) {
+        $cmd .= "--template '$template' ";
+    }
+    $cmd .= "--transaction '" . $content->{Transaction} . "' ";
+
+    if ( $content->{TransactionTypes} ne 'all' ) {
+        $cmd .= "--transaction-type '" . $content->{TransactionTypes} . "' ";
+    }
+
+    $cmd .= "--reload-ticket "
+        if $content->{ReloadTicket};
+
+    if ( $args{dryrun} ) {
+        print "dryrun: $cmd\n";
+
+        return;
+    }
+
+    # run command line and capture return
+    my $return = `$cmd`;
+
+    $RT::Logger->debug( "crontool job output: $return\n" );
+
+    return $return ? 0 : 1;
+}
+
+{
+    my %cache;
+
+    sub MinuteHourDowDomIn {
+        my $now = shift;
+        my $tz  = shift;
+
+        my $key = "$now $tz";
+        return @{ $cache{$key} } if exists $cache{$key};
+
+        my ( $minute, $hour, $dow, $dom );
+
+        {
+            local $ENV{'TZ'} = $tz;
+            ## Using POSIX::tzset fixes a bug where the TZ environment variable
+            ## is cached.
+            tzset();
+            ( undef, $minute, $hour, $dom, undef, undef, $dow ) = localtime($now);
+        }
+        tzset(); # return back previous value
+
+        $minute = "0$minute"
+            if length($minute) == 1;
+        $hour = "0$hour"
+            if length($hour) == 1;
+        $dow = (qw/Sunday Monday Tuesday Wednesday Thursday Friday Saturday/)[$dow];
+
+        return @{ $cache{$key} } = ( $minute, $hour, $dow, $dom) ;
+    }
+}
+
+=head1 NAME
+
+rt-run-scheduled-crontool - Check for scheduled crontool jobs and run
+them
+
+=head1 SYNOPSIS
+
+    rt-run-scheduled-crontool [options]
+
+=head1 DESCRIPTION
+
+This tool will find any crontool jobs that are scheduled and run them.
+
+Each crontool job has a minute and hour, and possibly day of week or day
+of month. These are taken to be in the RT server timezone.
+
+=head1 SETUP
+
+You'll need to have cron run this script every 15 minutes. Here's an
+example crontab entry to do this.
+
+    */15 * * * * @RT_SBIN_PATH_R@/rt-run-scheduled-crontool
+
+This will run the script every 15 minutes, every hour. This may need
+some further tweaking to be run as the correct user.
+
+=head1 OPTIONS
+
+This tool supports a few options. Most are for debugging.
+
+=over 8
+
+=item -h
+
+=item --help
+
+Display this documentation
+
+=item --dryrun
+
+Figure out which crontool jobs would be run, but don't actually run them
+
+=item --time SECONDS
+
+Instead of using the current time to figure out which crontool jobs
+should be run, use SECONDS (usually since midnight Jan 1st, 1970, so
+C<1192216018> would be Oct 12 19:06:58 GMT 2007).
+
+=item --all
+
+Ignore crontool job frequency when considering each crontool job
+(--dryrun is implied when using --all)
+
+=item --log LEVEL
+
+Adjust LogToSTDERR config option
+
+=back
+
+=cut
+
diff --git a/configure.ac b/configure.ac
index dd34674364..2b7f46f154 100755
--- a/configure.ac
+++ b/configure.ac
@@ -493,6 +493,7 @@ AC_CONFIG_FILES([
                  sbin/rt-passwd
                  sbin/rt-munge-attachments
                  bin/rt-crontool
+                 bin/rt-run-scheduled-crontool
                  bin/rt-mailgate
                  bin/rt],
                 [chmod ug+x $ac_file]

commit fbf4a2eab06db0c0a829a8dfc97308ef46f9d9b5
Author: Brad Embree <brad at bestpractical.com>
Date:   Sun Sep 24 14:16:39 2023 -0700

    Add Crontool list page

diff --git a/share/html/Admin/Tools/Crontool.html b/share/html/Admin/Tools/Crontool.html
new file mode 100644
index 0000000000..6f0196fcc3
--- /dev/null
+++ b/share/html/Admin/Tools/Crontool.html
@@ -0,0 +1,129 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => 'Crontool Jobs' &>
+<& /Elements/Tabs &>
+
+<&| /Widgets/TitleBox, title => loc('Crontool Jobs') &>
+<div class="table-responsive">
+  <table cellspacing="0" class="table collection collection-as-table">
+    <colgroup>
+      <col>
+      <col>
+      <col>
+    </colgroup>
+    <tr class="collection-as-table">
+      <th class="collection-as-table">
+        <span class="title">Description</span>
+      </th>
+      <th class="collection-as-table">
+        <span class="title">Frequency</span>
+      </th>
+      <th class="collection-as-table" style="text-align: right">
+        <span class="title"></span>
+      </th>
+    </tr>
+% my $rowcount = 1;
+% foreach my $cronjob ( @cronjobs ) {
+    <tbody class="list-item">
+      <tr class="<% $rowcount % 2 ? 'oddline' : 'evenline' %>" >
+        <td class="collection-as-table" ><% $cronjob->{description} %></td>
+        <td class="collection-as-table" ><% $cronjob->{frequency} %></td>
+        <td class="collection-as-table" align="right"><a href="/Admin/Tools/CrontoolJob.html?id=<% $cronjob->{id} %>">Edit</a></td>
+      </tr>
+    </tbody>
+%   $rowcount++;
+% }
+  </table>
+</div>
+</&>
+
+<%INIT>
+my $CrontoolJobs = RT::Attributes->new( RT->SystemUser );
+$CrontoolJobs->LimitToObject( RT->SystemUser );
+$CrontoolJobs->Limit(
+  FIELD           => 'Name',
+  VALUE           => 'Crontool',
+  OPERATOR        => '=',
+  ENTRYAGGREGATOR => 'AND',
+);
+
+my @cronjobs;
+while ( my $cronjob = $CrontoolJobs->Next ) {
+  my $frequency = $cronjob->SubValue('Frequency');
+  my $hour      = substr( $cronjob->SubValue('Hour'), 0, 3 );
+  my $minute    = $cronjob->SubValue('Minute');
+  my $timezone  = RT->Config->Get('Timezone');
+
+  my $frequency_details = '';
+  if ( $frequency eq 'daily' ) {
+      my @days;
+      foreach my $day ( qw( Monday Tuesday Wednesday Thursday Friday Saturday Sunday ) ) {
+          if ( $cronjob->SubValue($day) ) {
+              push @days, $day;
+          }
+      }
+      $frequency_details = ' on ' . join ', ', map { substr( $_, 0, 2 ) } @days;
+  }
+  elsif ( $frequency eq 'weekly' ) {
+      $frequency_details = 'on ' . $cronjob->SubValue('Dow') . ' every ' . $cronjob->SubValue('Fow') . ' ' . ( $cronjob->SubValue('Fow') == 1 ? 'week' : 'weeks' );
+  }
+  elsif ( $frequency eq 'monthly' ) {
+      $frequency_details = 'on day ' . $cronjob->SubValue('Dom');
+  }
+  $frequency_details .= " at $hour$minute $timezone"
+    if $frequency_details;
+
+  push @cronjobs, {
+    description => $cronjob->SubValue('Description'),
+    frequency   => "$frequency $frequency_details",
+    id          => $cronjob->id,
+  };
+}
+</%INIT>
+<%ARGS>
+</%ARGS>

commit ae363619a753908e22f2026d449ca26389005b58
Author: Brad Embree <brad at bestpractical.com>
Date:   Sun Sep 24 14:15:52 2023 -0700

    Add Crontool Job Create/Edit page

diff --git a/share/html/Admin/Tools/CrontoolJob.html b/share/html/Admin/Tools/CrontoolJob.html
new file mode 100644
index 0000000000..f77cddb59d
--- /dev/null
+++ b/share/html/Admin/Tools/CrontoolJob.html
@@ -0,0 +1,565 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => 'Crontool Jobs' &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<&| /Widgets/TitleBox, title => $CrontoolObj ? loc( 'Edit Crontool Job - [_1]', $fields{Description} ) : loc('Create New Crontool Job') &>
+
+<form action="<% RT->Config->Get('WebPath') %>/Admin/Tools/CrontoolJob.html?id=<% $fields{'CrontoolId'} %>" method="post" enctype="multipart/form-data" name="AddCrontool">
+
+<&| /Widgets/TitleBox, title => loc('Details') &>
+
+<&| /Elements/LabeledValue, Label => loc('Description'), Class => 'edit-custom-field' &>
+<div class="row">
+  <div class="col-6">
+    <input name="Description" type="text" value="<% $fields{'Description'} || '' %>" size="50" class="form-control" />
+  </div>
+</div>
+</&>
+
+<&| /Elements/LabeledValue, Label => loc('Search'), Class => 'edit-custom-field' &>
+<div class="row">
+    <div class="col-3">
+      <select name="SearchModule" class="form-control selectpicker">
+        <option value="" >-</option>
+% for my $module ( sort keys %{ $META{Search} } ) {
+%     my $selected = $module eq $fields{'SearchModule'}
+%                  ? 'selected="selected"'
+%                  : '';
+        <option value="<% $module %>" <%$selected|n %>><% $module %></option>
+% }
+      </select>
+    </div>
+    <div class="col-3">
+      <input name="SearchModuleArg" type="text" value="<% $fields{'SearchModuleArg'} || '' %>" size="20" class="form-control" />
+    </div>
+    <div class="col-3 <% $fields{'SearchModule'} ? '' : 'hidden' %>">
+      <a id="search-module-docs" target="_blank" href="<% $fields{'SearchModule'} ? 'https://docs.bestpractical.com/rt/latest/RT/Search/' . $fields{'SearchModule'} . '.html' : '' %>"><% loc('Documentation') %></a>
+    </div>
+  </div>
+</&>
+
+<&| /Elements/LabeledValue, Label => loc('Condition'), Class => 'edit-custom-field' &>
+<div class="row">
+    <div class="col-3">
+      <select name="ConditionModule" class="form-control selectpicker">
+        <option value="" >-</option>
+% for my $module ( sort keys %{ $META{Condition} } ) {
+%     my $selected = $module eq $fields{'ConditionModule'}
+%                  ? 'selected="selected"'
+%                  : '';
+        <option value="<% $module %>" <%$selected|n %>><% $module %></option>
+% }
+      </select>
+    </div>
+    <div class="col-3">
+      <input name="ConditionModuleArg" type="text" value="<% $fields{'ConditionModuleArg'} || '' %>" size="20" class="form-control" />
+    </div>
+    <div class="col-3 <% $fields{'ConditionModule'} ? '' : 'hidden' %>">
+      <a id="condition-module-docs" target="_blank" href="<% $fields{'ConditionModule'} ? 'https://docs.bestpractical.com/rt/latest/RT/Condition/' . $fields{'ConditionModule'} . '.html' : '' %>"><% loc('Documentation') %></a>
+    </div>
+  </div>
+</&>
+
+<&| /Elements/LabeledValue, Label => loc('Action Module'), Class => 'edit-custom-field' &>
+<div class="row">
+    <div class="col-3">
+      <select name="ActionModule" class="form-control selectpicker">
+        <option value="" >-</option>
+% for my $module ( sort keys %{ $META{Action} } ) {
+%     my $selected = $module eq $fields{'ActionModule'}
+%                  ? 'selected="selected"'
+%                  : '';
+        <option value="<% $module %>" <%$selected|n %>><% $module %></option>
+% }
+      </select>
+    </div>
+    <div class="col-3">
+      <input name="ActionModuleArg" type="text" value="<% $fields{'ActionModuleArg'} || '' %>" size="20" class="form-control" />
+    </div>
+    <div class="col-3 <% $fields{'ActionModule'} ? '' : 'hidden' %>">
+      <a id="action-module-docs" target="_blank" href="<% $fields{'ActionModule'} ? 'https://docs.bestpractical.com/rt/latest/RT/Action/' . $fields{'ActionModule'} . '.html' : '' %>"><% loc('Documentation') %></a>
+    </div>
+  </div>
+</&>
+
+<&| /Elements/LabeledValue, Label => loc('Template'), Class => 'edit-custom-field' &>
+<& /Admin/Scrips/Elements/SelectTemplate, Default => $fields{'Template'}, Class => 'col-6' &>
+</&>
+
+<&| /Elements/LabeledValue, Label => loc('Transaction'), Class => 'edit-custom-field cfrendertype-Checkbox' &>
+<div class="form-row">
+    <div class="col-6">
+      <div class="custom-control custom-radio">
+        <input type="radio" id="Transaction-first" name="Transaction" value="first" <% $fields{'Transaction'} eq 'first' ? 'checked="checked"' : "" |n %> class="custom-control-input">
+        <label class="custom-control-label" for="Transaction-first"><&|/l&>First Transaction</&></label>
+      </div>
+      <div class="custom-control custom-radio">
+        <input type="radio" id="Transaction-last" name="Transaction" value="last" <% $fields{'Transaction'} eq 'last' ? 'checked="checked"' : "" |n %> class="custom-control-input">
+        <label class="custom-control-label" for="Transaction-last"><&|/l&>Last Transaction</&></label>
+      </div>
+      <div class="custom-control custom-radio">
+        <input type="radio" id="Transaction-all" name="Transaction" value="all" <% $fields{'Transaction'} eq 'all' ? 'checked="checked"' : "" |n %> class="custom-control-input">
+        <label class="custom-control-label" for="Transaction-all"><&|/l&>All Transactions</&></label>
+      </div>
+    </div>
+</div>
+</&>
+
+<&| /Elements/LabeledValue, Label => loc('Transaction Types'), Class => 'edit-custom-field cfrendertype-Checkbox' &>
+<div class="form-row" id="transaction-types-all">
+  <div class="col-3">
+    <div class="custom-control custom-checkbox">
+      <input type="checkbox" id="Transaction-Type-ALL" name="Transaction-Type-ALL" class="custom-control-input" value="ALL" <% $fields{TransactionTypes} eq 'all' ? 'checked="checked"' : '' %>>
+      <label class="custom-control-label" for="Transaction-Type-ALL"><% loc('All Types') %></label>
+    </div>
+  </div>
+</div>
+<div class="form-row <% $fields{TransactionTypes} eq 'all' ? 'hidden' : '' %>" id="transaction-types-list">
+% for my $type ( sort keys %RT::Transaction::_BriefDescriptions ) {
+  <div class="col-3">
+    <div class="custom-control custom-checkbox">
+      <input type="checkbox" id="Transaction-Type-<% $type %>" name="Transaction-Type-<% $type %>" class="custom-control-input" value="<% $type %>" <% $fields{TransactionTypes} eq 'all' || $transaction_types{$type} ? 'checked="checked"' : '' %>>
+      <label class="custom-control-label" for="Transaction-Type-<% $type %>"><% loc($type) %></label>
+    </div>
+  </div>
+% }
+</div>
+</&>
+
+<&| /Elements/LabeledValue, Label => loc('Reload Ticket'), Class => 'edit-custom-field cfrendertype-Checkbox' &>
+  <div class="custom-control custom-checkbox edit-custom-field">
+    <input type="checkbox" id="ReloadTicket" name="ReloadTicket" class="custom-control-input" value="1" <% $fields{ReloadTicket} ? 'checked="checked"' : '' %>>
+    <label class="custom-control-label" for="ReloadTicket"><% loc('Reload ticket before processing in tickets iteration') %></label>
+  </div>
+</&>
+
+</&>
+
+<&| /Widgets/TitleBox, title => loc('Schedule') &>
+<&| /Elements/LabeledValue, Label => loc('Frequency'), Class => 'edit-custom-field cfrendertype-Checkbox' &>
+      <div class="form-row">
+        <div class="col-auto">
+          <div class="custom-control custom-radio">
+            <input type="radio" id="Frequency-daily" name="Frequency" value="daily" <% $fields{'Frequency'} eq 'daily' ? 'checked="checked"' : "" |n %> class="custom-control-input">
+            <label class="custom-control-label" for="Frequency-daily"><&|/l&>daily, on</&></label>
+          </div>
+        </div>
+% for my $day ( qw/Monday Tuesday Wednesday Thursday Friday Saturday Sunday/ ) {
+        <div class="col-auto">
+          <input type="hidden" class="hidden" name="<% $day %>-Magic" value="1" />
+          <div class="custom-control custom-checkbox">
+            <input type="checkbox" id="Frequency-daily-<% $day %>" name="<% $day %>" class="custom-control-input" value="1" <% $fields{$day} ? 'checked="checked"' : '' %>>
+            <label class="custom-control-label" for="Frequency-daily-<% $day %>"><% loc($day) %></label>
+          </div>
+        </div>
+% }
+      </div>
+      <div class="form-row">
+        <div class="col-auto">
+          <span class="current-value form-control">
+            <div class="custom-control custom-radio">
+              <input type="radio" id="Frequency-weekly" name="Frequency" value="weekly" <% $fields{'Frequency'} eq 'weekly' ? 'checked="checked"' : "" |n %> class="custom-control-input">
+              <label class="custom-control-label" for="Frequency-weekly"><&|/l&>weekly</&>, <&|/l&>on</&></label>
+            </div>
+          </span>
+        </div>
+        <div class="col-auto">
+          <select name="Dow" class="form-control selectpicker">
+              <option value="Monday" <% $fields{'Dow'} eq 'Monday' ? 'selected="selected"' : '' |n %>><&|/l&>Monday</&></option>
+              <option value="Tuesday" <% $fields{'Dow'} eq 'Tuesday' ? 'selected="selected"' : '' |n %>><&|/l&>Tuesday</&></option>
+              <option value="Wednesday" <% $fields{'Dow'} eq 'Wednesday' ? 'selected="selected"' : '' |n %>><&|/l&>Wednesday</&></option>
+              <option value="Thursday" <% $fields{'Dow'} eq 'Thursday' ? 'selected="selected"' : '' |n %>><&|/l&>Thursday</&></option>
+              <option value="Friday" <% $fields{'Dow'} eq 'Friday' ? 'selected="selected"' : '' |n %>><&|/l&>Friday</&></option>
+              <option value="Saturday" <% $fields{'Dow'} eq 'Saturday' ? 'selected="selected"' : '' |n %>><&|/l&>Saturday</&></option>
+              <option value="Sunday" <% $fields{'Dow'} eq 'Sunday' ? 'selected="selected"' : '' |n %>><&|/l&>Sunday</&></option>
+          </select>
+        </div>
+        <div class="col-auto">
+          <span class="current-value form-control"><&|/l&>every</&></span>
+        </div>
+        <div class="col-auto">
+          <select name="Fow" class="form-control selectpicker">
+% for my $f ( qw/1 2 3 4/ ) {
+            <option value="<%$f%>" <% $fields{'Fow'} == $f ? 'selected="selected"' : '' |n %>><% $f %></option>
+% }
+          </select>
+        </div>
+        <div class="col-auto">
+          <span class="current-value form-control"><&|/l&>weeks</&></span>
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="col-auto">
+          <span class="current-value form-control">
+            <div class="custom-control custom-radio">
+              <input type="radio" id="Frequency-monthly" name="Frequency" value="monthly" <% $fields{'Frequency'} eq 'monthly' ? 'checked="checked"' : "" |n %> class="custom-control-input">
+              <label class="custom-control-label" for="Frequency-monthly"><&|/l&>monthly</&>, <&|/l&>on day</&></label>
+            </div>
+          </span>
+        </div>
+        <div class="col-auto">
+          <select name="Dom" class="form-control selectpicker">
+% for my $dom (1..31) {
+            <option value="<% $dom %>" <% $fields{'Dom'} == $dom ? 'selected="selected"' : '' |n %>><% loc($dom) %></option>
+% }
+          </select>
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="col-auto">
+          <span class="current-value form-control">
+            <div class="custom-control custom-radio">
+              <input type="radio" id="Frequency-never" name="Frequency" value="never" <% $fields{'Frequency'} eq 'never' ? 'checked="checked"' : "" |n %> class="custom-control-input">
+              <label class="custom-control-label" for="Frequency-never"><&|/l&>never</&></label>
+            </div>
+          </span>
+        </div>
+      </div>
+  </&>
+
+  <&| /Elements/LabeledValue, Label => loc('Hour'), Class => 'edit-custom-field' &>
+      <div class="row">
+        <div class="col-auto">
+          <select name="Hour" class="form-control selectpicker">
+% my $formatter = RT::Date->new(RT->SystemUser)->LocaleObj;
+% my $dt = DateTime->now;
+% $dt->set_minute(0);
+% $dt->set_second(0);
+
+% for my $hour (0..23) {
+%     $dt->set_hour($hour);
+%     my $formatted = $dt->format_cldr($formatter->time_format_short);
+
+%     my $value = sprintf '%02d:00', $hour;
+%     my $selected = $value eq $fields{'Hour'}
+%                  ? 'selected="selected"'
+%                  : '';
+
+            <option value="<% $value %>" <%$selected|n %>><% $formatted %></option>
+% }
+          </select>
+        </div>
+        <div class="col-auto">
+          <span class="current-value form-control">(<%$timezone%>)</span>
+        </div>
+      </div>
+  </&>
+
+  <&| /Elements/LabeledValue, Label => loc('Minute'), Class => 'edit-custom-field' &>
+      <div class="row">
+        <div class="col-auto">
+          <select name="Minute" class="form-control selectpicker">
+% for my $minutes ( qw( 00 15 30 45 ) ) {
+%     my $selected = $minutes eq $fields{'Minute'}
+%                  ? 'selected="selected"'
+%                  : '';
+            <option value="<% $minutes %>" <%$selected|n %>><% $minutes %></option>
+% }
+          </select>
+        </div>
+      </div>
+  </&>
+</&>
+
+<div class="form-row">
+    <div class="col-12">
+% if ($CrontoolObj) {
+      <& /Elements/Submit, Name => "Save", Label => loc('Save Changes') &>
+% } else {
+      <& /Elements/Submit, Name => "Save", Label => loc('Create') &>
+% }
+    </div>
+  </div>
+
+</form>
+</&>
+<script type="text/javascript">
+    jQuery( function () {
+        jQuery('div#transaction-types-all input[name=Transaction-Type-ALL]').change( function () {
+            console.log('foobar');
+            if ( jQuery(this).is(':checked') ) {
+                jQuery('div#transaction-types-list').addClass('hidden');
+            }
+            else {
+                jQuery('div#transaction-types-list').removeClass('hidden');
+            }
+        } );
+
+        jQuery('select[name=SearchModule]').change( function () {
+            var val = jQuery(this).val();
+            jQuery('a#search-module-docs').attr( 'href', 'https://docs.bestpractical.com/rt/latest/RT/Search/' + val + '.html' );
+            if ( val == "" ) {
+                jQuery('a#search-module-docs').parent().addClass('hidden');
+            }
+            else {
+                jQuery('a#search-module-docs').parent().removeClass('hidden');
+            }
+        } );
+        jQuery('select[name=ConditionModule]').change( function () {
+            var val = jQuery(this).val();
+            jQuery('a#condition-module-docs').attr( 'href', 'https://docs.bestpractical.com/rt/latest/RT/Condition/' + val + '.html' );
+            if ( val == "" ) {
+                jQuery('a#condition-module-docs').parent().addClass('hidden');
+            }
+            else {
+                jQuery('a#condition-module-docs').parent().removeClass('hidden');
+            }
+        } );
+        jQuery('select[name=ActionModule]').change( function () {
+            var val = jQuery(this).val();
+            jQuery('a#action-module-docs').attr( 'href', 'https://docs.bestpractical.com/rt/latest/RT/Action/' + val + '.html' );
+            if ( val == "" ) {
+                jQuery('a#action-module-docs').parent().addClass('hidden');
+            }
+            else {
+                jQuery('a#action-module-docs').parent().removeClass('hidden');
+            }
+        } );
+    });
+</script>
+<%INIT>
+my %META = (
+    Search => {
+        ActiveTicketsInQueue => { argument => 1 },
+        FromSQL              => { argument => 1 },
+        Simple               => { argument => 1 },
+    },
+    Condition => {
+        BeforeDue            => { argument => 1 },
+        CloseTicket          => { argument => 0 },
+        Overdue              => { argument => 0 },
+        OwnerChange          => { argument => 0 },
+        PriorityChange       => { argument => 0 },
+        PriorityExceeds      => { argument => 1 },
+        QueueChange          => { argument => 0 },
+        ReopenTicket         => { argument => 0 },
+        SLA                  => { argument => 0 },
+        SLA_RequireDueSet    => { argument => 0 },
+        SLA_RequireStartsSet => { argument => 0 },
+        StatusChange         => { argument => 1 },
+        TimeWorkedChange     => { argument => 0 },
+        ViaInterface         => { argument => 1 },
+    },
+    Action => {
+        AddPriority            => { argument => 1 },
+        AutoOpen               => { argument => 0 },
+        AutoOpenInactive       => { argument => 0 },
+        Autoreply              => { argument => 0 },
+        ClearCustomFieldValues => { argument => 1 },
+        CreateTickets          => { argument => 0 },
+        EscalatePriority       => { argument => 1 },
+        ExtractSubjectTag      => { argument => 0 },
+        LinearEscalate         => { argument => 1 },
+        Notify                 => { argument => 1 },
+        NotifyAsComment        => { argument => 1 },
+        NotifyGroup            => { argument => 1 },
+        NotifyGroupAsComment   => { argument => 1 },
+        NotifyOwnerOrAdminCc   => { argument => 1 },
+        OpenOnStarted          => { argument => 0 },
+        RecordComment          => { argument => 0 },
+        RecordCorrespondence   => { argument => 0 },
+        SLA_SetDue             => { argument => 0 },
+        SLA_SetStarts          => { argument => 0 },
+        SendForward            => { argument => 1 },
+        SetSetCustomFieldToNow => { argument => 1 },
+        SetPriority            => { argument => 1 },
+        SetStatus              => { argument => 1 },
+        UpdateParentTimeWorked => { argument => 0 },
+    },
+);
+
+my @results = ();
+my %fields = (
+    CrontoolId         => $id,
+    Description        => '',
+    SearchModule       => '',
+    SearchModuleArg    => '',
+    ConditionModule    => '',
+    ConditionModuleArg => '',
+    ActionModule       => '',
+    ActionModuleArg    => '',
+    Frequency          => 'daily',
+    Monday             => 1,
+    Tuesday            => 1,
+    Wednesday          => 1,
+    Thursday           => 1,
+    Friday             => 1,
+    Saturday           => 0,
+    Sunday             => 0,
+    Hour               => '06:00',
+    Minute             => '0',
+    Dow                => 'Monday',
+    Dom                => 1,
+    Fow                => 1,
+    Counter            => 0,
+    Transaction        => 'first',
+    TransactionTypes   => 'all',
+    Template           => '',
+    ReloadTicket       => 0,
+);
+my $timezone = RT->Config->Get('Timezone');
+
+my $CrontoolJobs = RT::Attributes->new( RT->SystemUser );
+$CrontoolJobs->LimitToObject( RT->SystemUser );
+$CrontoolJobs->Limit(
+  FIELD           => 'Name',
+  VALUE           => 'Crontool',
+  OPERATOR        => '=',
+  ENTRYAGGREGATOR => 'AND',
+);
+
+my @cronjobs;
+while ( my $cronjob = $CrontoolJobs->Next ) {
+  my $frequency = $cronjob->SubValue('Frequency');
+  my $hour      = substr( $cronjob->SubValue('Hour'), 0, 3 );
+  my $minute    = $cronjob->SubValue('Minute');
+  my $timezone  = RT->Config->Get('Timezone');
+
+  my $frequency_details = '';
+  if ( $frequency eq 'daily' ) {
+      my @days;
+      foreach my $day ( qw( Monday Tuesday Wednesday Thursday Friday Saturday Sunday ) ) {
+          if ( $cronjob->SubValue($day) ) {
+              push @days, $day;
+          }
+      }
+      $frequency_details = join ', ', map { substr( $_, 0, 2 ) } @days;
+  }
+  elsif ( $frequency eq 'weekly' ) {
+      $frequency_details = 'on ' . $cronjob->SubValue('Dow') . ' every ' . $cronjob->SubValue('Fow') . ' ' . ( $cronjob->SubValue('Fow') == 1 ? 'week' : 'weeks' );
+  }
+  elsif ( $frequency eq 'monthly' ) {
+      $frequency_details = 'on day ' . $cronjob->SubValue('Dom');
+  }
+  push @cronjobs, {
+    description => $cronjob->Description,
+    frequency   => $frequency_details,
+    id          => $cronjob->id,
+  };
+}
+
+my $CrontoolObj;
+if ( $id ) {
+  $CrontoolObj = RT::Attribute->new( RT->SystemUser );
+  my ( $ok, $msg ) = $CrontoolObj->LoadById($id);
+
+  if ( $ok ) {
+    $fields{CrontoolId} = $id;
+    for my $field ( keys %fields ) {
+      $fields{$field} = $CrontoolObj->SubValue($field)
+        if defined $CrontoolObj->SubValue($field);
+    }
+  }
+  else {
+    RT->Logger->warning("Could not load Crontool Attribute $id: $msg");
+  }
+}
+
+# this'll be defined on submit
+if ( defined $ARGS{Save} ) {
+  # update fields with arguments passed in by the user
+  if ( $ARGS{'Transaction-Type-ALL'} ) {
+    $fields{TransactionTypes} = 'all';
+  }
+  else {
+    $fields{TransactionTypes} = join ',', map { $ARGS{$_} } grep { $_ =~ /^Transaction-Type-/ } keys %ARGS;
+  }
+
+  for my $field ( keys %fields ) {
+      next if $field eq 'CrontoolId';       # immutable
+      next if $field eq 'TransactionTypes'; # handled above
+      $fields{$field} = $ARGS{$field}
+          if defined($ARGS{$field}) || $ARGS{$field.'-Magic'};
+  }
+  
+  # update
+  $id = delete $fields{'CrontoolId'}; # immutable
+  if ( $CrontoolObj ) {
+      my ( $ok, $msg ) = $CrontoolObj->SetSubValues(%fields);
+      ( $ok, $msg ) = $CrontoolObj->SetDescription( $fields{Description} )
+        if $ok && $CrontoolObj->SubValue('Description') ne $fields{Description};
+
+      $msg = loc("Crontool updated") if $ok;
+      push @results, $msg;
+  }
+  # create
+  else {
+      $CrontoolObj = RT::Attribute->new( RT->SystemUser );
+      my ( $ok, $msg ) = $CrontoolObj->Create(
+          Name        => 'Crontool',
+          Description => $fields{Description},
+          ContentType => 'storable',
+          Object      => RT->SystemUser,
+          Content     => \%fields,
+      );
+      if ( $ok ) {
+          push @results, loc("Crontool created");
+      }
+      else {
+          push @results, loc('Crontool could not be created: [_1]', $msg);
+      }
+  }
+  $fields{'CrontoolId'} = $id;
+}
+
+my %transaction_types;
+foreach my $type ( split ',', $fields{TransactionTypes} ) {
+    $transaction_types{$type} = 1;
+}
+</%INIT>
+<%ARGS>
+$id        => undef
+$Frequency => undef
+$Hour      => undef
+$Dow       => undef
+$Dom       => undef
+</%ARGS>
+

-----------------------------------------------------------------------


hooks/post-receive
-- 
rt


More information about the rt-commit mailing list