Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a30d6fa099 | |||
| b49ce8a895 | |||
| 5094b31ce6 | |||
| 768318b43a | |||
| e4db24ee8d | |||
| 2bed04cd18 |
34
README.md
34
README.md
@@ -1,3 +1,35 @@
|
||||
# sprinkle-scheduler
|
||||
|
||||
A UserFrosting sprinkle to add scheduled events
|
||||
A UserFrosting sprinkle to add scheduled events.
|
||||
|
||||
Either a crontab set to run once a minute is required or a systemd timer:
|
||||
|
||||
Systemd service: /etc/systemd/system/uf-scheduler.service
|
||||
```systemd
|
||||
[Unit]
|
||||
Description=UserFrosting Scheduler
|
||||
|
||||
[Service]
|
||||
Restart=no
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/php bakery schedule -n
|
||||
WorkingDirectory=/srv/UserFrosting
|
||||
User=www-data
|
||||
Group=www-data
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
Systemd timer: /etc/systemd/system/uf-scheduler.timer
|
||||
```systemd
|
||||
[Unit]
|
||||
Description=UserFrosting Scheduler trigger
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* *:*:00
|
||||
Persistent=true
|
||||
Unit=uf-scheduler.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
@@ -3,7 +3,9 @@
|
||||
"type": "userfrosting-sprinkle",
|
||||
"description": "A sprinkle to add scheduled events.",
|
||||
"require": {
|
||||
"dragonmantank/cron-expression": "*"
|
||||
"dragonmantank/cron-expression": "*",
|
||||
"lorisleiva/cron-translator": "^0.3.2",
|
||||
"twig/intl-extra": "^3.6"
|
||||
},
|
||||
"autoload": {
|
||||
"files" : [
|
||||
|
||||
27
locale/en_US/messages.php
Normal file
27
locale/en_US/messages.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* AVSDev UF Scheduler (https://avsdev.uk)
|
||||
*
|
||||
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
|
||||
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
|
||||
*/
|
||||
|
||||
/**
|
||||
* US English message token translations for the 'scheduler' sprinkle.
|
||||
*
|
||||
* @author Craig Williams
|
||||
*/
|
||||
return [
|
||||
'TASK' => [
|
||||
1 => 'Task',
|
||||
2 => 'Tasks',
|
||||
|
||||
'NAME' => 'Task name',
|
||||
'PAGE_DESCRIPTION' => 'This page shows a list of programmed tasks that the UserFrosting Scheduler sprinkle knows about, their schedules and when they are next expected to run.',
|
||||
],
|
||||
|
||||
'SPRINKLE' => 'Sprinkle',
|
||||
'SCHEDULE' => 'Schedule',
|
||||
'NEXT_RUN_TIME' => 'Next run time',
|
||||
];
|
||||
22
routes/tasks.php
Normal file
22
routes/tasks.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* AVSDev UF Scheduler (https://avsdev.uk)
|
||||
*
|
||||
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
|
||||
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
|
||||
*/
|
||||
|
||||
use UserFrosting\Sprinkle\Core\Util\NoCache;
|
||||
|
||||
/*
|
||||
* Routes for schdeuled task listing.
|
||||
*/
|
||||
$app->group('/tasks', function () {
|
||||
$this->get('', 'UserFrosting\Sprinkle\Scheduler\Controller\TaskController:pageList')
|
||||
->setName('uri_permissions');
|
||||
})->add('authGuard')->add(new NoCache());
|
||||
|
||||
$app->group('/api/tasks', function () {
|
||||
$this->get('', 'UserFrosting\Sprinkle\Scheduler\Controller\TaskController:getList');
|
||||
})->add('authGuard')->add(new NoCache());
|
||||
@@ -110,6 +110,5 @@ class ScheduleCommand extends BaseCommand
|
||||
$this->io->error('Schedule failed !');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
110
src/Controller/TaskController.php
Normal file
110
src/Controller/TaskController.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* AVSDev UF Scheduler (https://avsdev.uk)
|
||||
*
|
||||
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
|
||||
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
|
||||
*/
|
||||
|
||||
namespace UserFrosting\Sprinkle\Scheduler\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use UserFrosting\Sprinkle\Core\Controller\SimpleController;
|
||||
use UserFrosting\Support\Exception\ForbiddenException;
|
||||
use UserFrosting\Support\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Controller class for task-related requests, (listing tasks).
|
||||
*
|
||||
* @author Craig Williams (https://avsdev.uk)
|
||||
*/
|
||||
class TaskController extends SimpleController
|
||||
{
|
||||
/**
|
||||
* Returns a list of Tasks.
|
||||
*
|
||||
* Generates a list of tasks from the scehduler. No sorting or pagination functionality.
|
||||
* This page requires authentication.
|
||||
*
|
||||
* Request type: GET
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param array $args
|
||||
*
|
||||
* @throws ForbiddenException If user is not authorized to access page
|
||||
*/
|
||||
public function getList(Request $request, Response $response, $args)
|
||||
{
|
||||
// GET parameters
|
||||
$params = $request->getQueryParams();
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
|
||||
$authorizer = $this->ci->authorizer;
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */
|
||||
$currentUser = $this->ci->currentUser;
|
||||
|
||||
// Access-controlled page
|
||||
if (!$authorizer->checkAccess($currentUser, 'uri_tasks')) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Scheduler\Scheduler $scheduler */
|
||||
$scheduler = $this->ci->scheduler;
|
||||
|
||||
$tasks = $scheduler->getTasks();
|
||||
|
||||
$tasks = array_map(
|
||||
function($val) {
|
||||
$val['schedule_nice'] = CronTranslator::translate($val['schedule']);
|
||||
return $val;
|
||||
},
|
||||
$tasks
|
||||
);
|
||||
|
||||
// Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content.
|
||||
// For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating).
|
||||
return $response->withJson($tasks, 200, JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the task listing page.
|
||||
*
|
||||
* This page renders a table of tasks, with dropdown menus for admin actions for each task.
|
||||
* Actions typically include: run task
|
||||
* This page requires authentication.
|
||||
*
|
||||
* Request type: GET
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param array $args
|
||||
*
|
||||
* @throws ForbiddenException If user is not authorized to access page
|
||||
*/
|
||||
public function pageList(Request $request, Response $response, $args)
|
||||
{
|
||||
/** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
|
||||
$authorizer = $this->ci->authorizer;
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */
|
||||
$currentUser = $this->ci->currentUser;
|
||||
|
||||
// Access-controlled page
|
||||
if (!$authorizer->checkAccess($currentUser, 'uri_tasks')) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Scheduler\Scheduler $scheduler */
|
||||
$scheduler = $this->ci->scheduler;
|
||||
|
||||
$tasks = $scheduler->getTasks();
|
||||
|
||||
return $this->ci->view->render($response, 'pages/tasks.html.twig', [
|
||||
'tasks' => $tasks
|
||||
]);
|
||||
}
|
||||
}
|
||||
95
src/Database/Seeds/SchedulerPermissions.php
Normal file
95
src/Database/Seeds/SchedulerPermissions.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* AVSDev UF Scheduler (https://avsdev.uk)
|
||||
*
|
||||
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
|
||||
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
|
||||
*/
|
||||
|
||||
namespace UserFrosting\Sprinkle\Scheduler\Database\Seeds;
|
||||
|
||||
use UserFrosting\Sprinkle\Account\Database\Models\Permission;
|
||||
use UserFrosting\Sprinkle\Account\Database\Models\Role;
|
||||
use UserFrosting\Sprinkle\Core\Database\Seeder\BaseSeed;
|
||||
use UserFrosting\Sprinkle\Core\Facades\Seeder;
|
||||
|
||||
/**
|
||||
* Seeder for the dashboard permissions.
|
||||
*/
|
||||
class SchedulerPermissions extends BaseSeed
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
// We require the default roles
|
||||
Seeder::execute('DefaultRoles');
|
||||
|
||||
// Get and save permissions
|
||||
$permissions = $this->getPermissions();
|
||||
$this->savePermissions($permissions);
|
||||
|
||||
// Add default mappings to permissions
|
||||
$this->syncPermissionsRole($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array Permissions to seed
|
||||
*/
|
||||
protected function getPermissions()
|
||||
{
|
||||
return [
|
||||
'uri_task' => new Permission([
|
||||
'slug' => 'uri_task',
|
||||
'name' => 'View task',
|
||||
'conditions' => 'always()',
|
||||
'description' => 'View the task page of any task.',
|
||||
]),
|
||||
'uri_tasks' => new Permission([
|
||||
'slug' => 'uri_tasks',
|
||||
'name' => 'Tasks management page',
|
||||
'conditions' => 'always()',
|
||||
'description' => 'View a page containing a table of tasks.',
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save permissions.
|
||||
*
|
||||
* @param array $permissions
|
||||
*/
|
||||
protected function savePermissions(array &$permissions)
|
||||
{
|
||||
foreach ($permissions as $slug => $permission) {
|
||||
// Trying to find if the permission already exist
|
||||
$existingPermission = Permission::where(['slug' => $permission->slug, 'conditions' => $permission->conditions])->first();
|
||||
|
||||
// Don't save if already exist, use existing permission reference
|
||||
// otherwise to re-sync permissions and roles
|
||||
if ($existingPermission == null) {
|
||||
$permission->save();
|
||||
} else {
|
||||
$permissions[$slug] = $existingPermission;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync permissions with default roles.
|
||||
*
|
||||
* @param array $permissions
|
||||
*/
|
||||
protected function syncPermissionsRole(array $permissions)
|
||||
{
|
||||
$roleSiteAdmin = Role::where('slug', 'site-admin')->first();
|
||||
if ($roleSiteAdmin) {
|
||||
$roleSiteAdmin->permissions()->syncWithoutDetaching([
|
||||
$permissions['uri_tasks']->id,
|
||||
$permissions['uri_task']->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,15 +179,19 @@ class Scheduler
|
||||
*/
|
||||
protected function getTaskDetails(ResourceInstance $file)
|
||||
{
|
||||
/** @var \UserFrosting\System\Sprinkle\SprinkleManager */
|
||||
$sprinkleManager = $this->ci->sprinkleManager;
|
||||
|
||||
// Format the sprinkle name for the namespace
|
||||
$sprinkleName = $file->getLocation()->getName();
|
||||
$sprinkleName = Str::studly($sprinkleName);
|
||||
$sprinkleNS = $sprinkleManager->getSprinkleClassNamespace($sprinkleName);
|
||||
|
||||
|
||||
// Getting base path, name and class name
|
||||
$basePath = str_replace($file->getBasename(), '', $file->getBasePath());
|
||||
$name = $basePath . $file->getFilename();
|
||||
$className = str_replace('/', '\\', $basePath) . $file->getFilename();
|
||||
$classPath = "\\UserFrosting\\Sprinkle\\$sprinkleName\\Scheduler\\Tasks\\$className";
|
||||
$classPath = "\\$sprinkleNS\\Scheduler\\Tasks\\$className";
|
||||
|
||||
if (!class_exists($classPath)) {
|
||||
throw new \Exception("Task class `$className` not found. Make sure the class has the correct namespace.");
|
||||
|
||||
@@ -11,8 +11,11 @@ namespace UserFrosting\Sprinkle\Scheduler\ServicesProvider;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use UserFrosting\Sprinkle\Scheduler\Scheduler\Scheduler;
|
||||
use Twig\Extra\Intl\IntlExtension;
|
||||
use UserFrosting\Sprinkle\Core\ServicesProvider\BaseServicesProvider;
|
||||
use UserFrosting\Sprinkle\Scheduler\Scheduler\Scheduler;
|
||||
use UserFrosting\Sprinkle\Scheduler\Twig\FormatCronExtension;
|
||||
|
||||
|
||||
/**
|
||||
* Sheduler services provider.
|
||||
@@ -38,5 +41,21 @@ class ServicesProvider
|
||||
$container['scheduler'] = function ($c) {
|
||||
return new Scheduler($c);
|
||||
};
|
||||
|
||||
/*
|
||||
* Extends the 'view' service with the HasRole extension for Twig.
|
||||
*
|
||||
* @return \Slim\Views\Twig
|
||||
*/
|
||||
$container->extend('view', function ($view, $c) {
|
||||
$twig = $view->getEnvironment();
|
||||
|
||||
if (!$twig->hasExtension('Twig\Extra\Intl\IntlExtension')) {
|
||||
$twig->addExtension(new IntlExtension());
|
||||
}
|
||||
$twig->addExtension(new FormatCronExtension($c));
|
||||
|
||||
return $view;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
64
src/Twig/FormatCronExtension.php
Normal file
64
src/Twig/FormatCronExtension.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* AVSDev UF Scheduler (https://avsdev.uk)
|
||||
*
|
||||
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
|
||||
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
|
||||
*/
|
||||
|
||||
namespace UserFrosting\Sprinkle\Scheduler\Twig;
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Lorisleiva\CronTranslator\CronTranslator;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\Extension\GlobalsInterface;
|
||||
use Twig\TwigFilter;
|
||||
use UserFrosting\Support\Repository\Repository as Config;
|
||||
|
||||
/**
|
||||
* Extends Twig functionality to add formatCron.
|
||||
*
|
||||
* @author Craig Williams (https://avsdev.uk)
|
||||
*/
|
||||
class FormatCronExtension extends AbstractExtension implements GlobalsInterface
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
protected $services;
|
||||
|
||||
/**
|
||||
* @var Config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $services
|
||||
*/
|
||||
public function __construct(ContainerInterface $services)
|
||||
{
|
||||
$this->services = $services;
|
||||
$this->config = $services->config;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'avsdev/schedule-formatCron';
|
||||
}
|
||||
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new TwigFilter('formatCron', function ($cron) {
|
||||
return CronTranslator::translate($cron);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getGlobals(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
5
templates/navigation/partials/sidebar-tasks.html.twig
Normal file
5
templates/navigation/partials/sidebar-tasks.html.twig
Normal file
@@ -0,0 +1,5 @@
|
||||
{% if checkAccess('uri_tasks') %}
|
||||
<li>
|
||||
<a href="{{site.uri.public}}/tasks"><i class="fas fa-tasks fa-fw"></i> <span>{{ translate("TASK", 2) }}</span></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
6
templates/navigation/sidebar-menu.html.twig
Normal file
6
templates/navigation/sidebar-menu.html.twig
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends "@admin/navigation/sidebar-menu.html.twig" %}
|
||||
|
||||
{% block navigation %}
|
||||
{{ parent() }}
|
||||
{% include "navigation/partials/sidebar-tasks.html.twig" %}
|
||||
{% endblock %}
|
||||
41
templates/pages/tasks.html.twig
Normal file
41
templates/pages/tasks.html.twig
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "pages/abstract/dashboard.html.twig" %}
|
||||
|
||||
{% block stylesheets_page %}
|
||||
<!-- Page-specific CSS asset bundle -->
|
||||
{{ assets.css('css/form-widgets') | raw }}
|
||||
{% endblock %}
|
||||
|
||||
{# Overrides blocks in head of base template #}
|
||||
{% block page_title %}{{ translate("TASK", 2)}}{% endblock %}
|
||||
|
||||
{% block page_description %}{{ translate("TASK.PAGE_DESCRIPTION")}}{% endblock %}
|
||||
|
||||
{% block body_matter %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="widget-tasks" class="box box-primary">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title pull-left"><i class="fas fa-tasks fa-fw"></i> {{translate('TASK', 2)}}</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
{% include "tables/tasks.html.twig" with {
|
||||
"table" : {
|
||||
"id" : "table-tasks"
|
||||
}
|
||||
}
|
||||
%}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts_page %}
|
||||
<!-- Include validation rules -->
|
||||
<script>
|
||||
{% include "pages/partials/page.js.twig" %}
|
||||
</script>
|
||||
|
||||
<!-- Include form widgets JS -->
|
||||
{{ assets.js('js/form-widgets') | raw }}
|
||||
|
||||
{% endblock %}
|
||||
49
templates/tables/tasks.html.twig
Normal file
49
templates/tables/tasks.html.twig
Normal file
@@ -0,0 +1,49 @@
|
||||
{# This partial template renders a table of permissions, to be populated with rows via an AJAX request.
|
||||
# This extends a generic template for paginated tables.
|
||||
#
|
||||
# Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used
|
||||
# to render the table cells with the data from the AJAX request.
|
||||
#}
|
||||
|
||||
{% extends "tables/table-paginated.html.twig" %}
|
||||
|
||||
{% block table %}
|
||||
<table id="{{table.id}}" class="tablesorter table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-column-name="name" data-priority="1">{{translate('NAME')}}</th>
|
||||
<th data-column-name="sprinkle" data-priority="1">{{translate('SPRINKLE')}}</th>
|
||||
<th data-column-name="schedule" data-priority="1">{{translate('SCHEDULE')}}</th>
|
||||
<th data-column-name="next_run" data-priority="1">{{translate('NEXT_RUN_TIME')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for task in tasks %}
|
||||
<tr>
|
||||
<td data-text="{{task.sprinkle}}/{{task.name}}">
|
||||
<div>
|
||||
<strong>
|
||||
<!--<a href="{{site.uri.public}}/tasks/t/{{task.sprinkle}}/{{task.name}}">{{task.name}}</a>-->
|
||||
{{task.name}}
|
||||
</strong>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td data-text="{{task.sprinkle}}">
|
||||
{{task.sprinkle}}
|
||||
</td>
|
||||
|
||||
<td data-text="{{task.schedule}}">
|
||||
<i class="text-muted" style="min-width: 70px; display: inline-block">({{task.schedule}})</i> {{task.schedule | formatCron()}}
|
||||
</td>
|
||||
|
||||
<td data-text="{{task.next_run}}">
|
||||
{{ task.next_run | format_datetime(pattern='HH:mm:ss dd/MM/yyyy') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_pager_controls %}{% endblock %}
|
||||
Reference in New Issue
Block a user