4 Commits
v0.1 ... v1.0

13 changed files with 449 additions and 6 deletions

View File

@@ -3,7 +3,9 @@
"type": "userfrosting-sprinkle", "type": "userfrosting-sprinkle",
"description": "A sprinkle to add scheduled events.", "description": "A sprinkle to add scheduled events.",
"require": { "require": {
"dragonmantank/cron-expression": "*" "dragonmantank/cron-expression": "*",
"lorisleiva/cron-translator": "^0.3.2",
"twig/intl-extra": "^3.6"
}, },
"autoload": { "autoload": {
"files" : [ "files" : [

27
locale/en_US/messages.php Normal file
View 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
View 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());

View File

@@ -110,6 +110,5 @@ class ScheduleCommand extends BaseCommand
$this->io->error('Schedule failed !'); $this->io->error('Schedule failed !');
return self::FAILURE; return self::FAILURE;
} }
} }
} }

View 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
]);
}
}

View 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,
]);
}
}
}

View File

@@ -179,15 +179,19 @@ class Scheduler
*/ */
protected function getTaskDetails(ResourceInstance $file) protected function getTaskDetails(ResourceInstance $file)
{ {
/** @var \UserFrosting\System\Sprinkle\SprinkleManager */
$sprinkleManager = $this->ci->sprinkleManager;
// Format the sprinkle name for the namespace // Format the sprinkle name for the namespace
$sprinkleName = $file->getLocation()->getName(); $sprinkleName = $file->getLocation()->getName();
$sprinkleName = Str::studly($sprinkleName); $sprinkleNS = $sprinkleManager->getSprinkleClassNamespace($sprinkleName);
// Getting base path, name and class name // Getting base path, name and class name
$basePath = str_replace($file->getBasename(), '', $file->getBasePath()); $basePath = str_replace($file->getBasename(), '', $file->getBasePath());
$name = $basePath . $file->getFilename(); $name = $basePath . $file->getFilename();
$className = str_replace('/', '\\', $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)) { if (!class_exists($classPath)) {
throw new \Exception("Task class `$className` not found. Make sure the class has the correct namespace."); throw new \Exception("Task class `$className` not found. Make sure the class has the correct namespace.");

View File

@@ -11,8 +11,11 @@ namespace UserFrosting\Sprinkle\Scheduler\ServicesProvider;
use Illuminate\Container\Container; use Illuminate\Container\Container;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use UserFrosting\Sprinkle\Scheduler\Scheduler\Scheduler; use Twig\Extra\Intl\IntlExtension;
use UserFrosting\Sprinkle\Core\ServicesProvider\BaseServicesProvider; use UserFrosting\Sprinkle\Core\ServicesProvider\BaseServicesProvider;
use UserFrosting\Sprinkle\Scheduler\Scheduler\Scheduler;
use UserFrosting\Sprinkle\Scheduler\Twig\FormatCronExtension;
/** /**
* Sheduler services provider. * Sheduler services provider.
@@ -38,5 +41,21 @@ class ServicesProvider
$container['scheduler'] = function ($c) { $container['scheduler'] = function ($c) {
return new Scheduler($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;
});
} }
} }

View 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()
{
return [];
}
}

View 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 %}

View File

@@ -0,0 +1,6 @@
{% extends "@admin/navigation/sidebar-menu.html.twig" %}
{% block navigation %}
{{ parent() }}
{% include "navigation/partials/sidebar-tasks.html.twig" %}
{% endblock %}

View 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 %}

View 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 %}