[FEATURE] Grouping for scheduler-jobs 08/24608/9
authorStefan Neufeind <typo3.neufeind@speedpartner.de>
Sat, 12 Oct 2013 11:11:42 +0000 (13:11 +0200)
committerMarkus Klein <klein.t3@mfc-linz.at>
Tue, 15 Oct 2013 10:19:12 +0000 (12:19 +0200)
Allows defining of task-groups and groups tasks in
scheduler-list by those groups.

Tasks inside a hidden group are neither automatically executed
nor displayed in the scheduler module.
Task-groups feature a description field that can hold additional
information and is displayed below the group name in the scheduler
list.

Change-Id: I445cf9296ab03e9e0e9e3a35ba5a6aa66ad23f13
Resolves: #52695
Releases: 6.2
Reviewed-on: https://review.typo3.org/24608
Reviewed-by: Xavier Perseguers
Tested-by: Xavier Perseguers
Reviewed-by: Georg Ringer
Tested-by: Markus Klein
Reviewed-by: Markus Klein
typo3/sysext/scheduler/Classes/Controller/SchedulerModuleController.php
typo3/sysext/scheduler/Classes/Scheduler.php
typo3/sysext/scheduler/Classes/Task/AbstractTask.php
typo3/sysext/scheduler/Configuration/TCA/tx_scheduler_task_group.php [new file with mode: 0644]
typo3/sysext/scheduler/Documentation/Administration/GroupTask/Index.rst [new file with mode: 0644]
typo3/sysext/scheduler/Documentation/Administration/Index.rst
typo3/sysext/scheduler/Documentation/Images/GroupRecords.png [new file with mode: 0644]
typo3/sysext/scheduler/Documentation/Images/GroupedTasks.png [new file with mode: 0644]
typo3/sysext/scheduler/ext_tables.sql
typo3/sysext/scheduler/locallang_tca.xlf [new file with mode: 0644]
typo3/sysext/scheduler/mod1/locallang.xlf

index bc318c4..7209db0 100644 (file)
@@ -547,6 +547,7 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
         */
        protected function editTask() {
                $registeredClasses = self::getRegisteredClasses();
+               $registeredTaskGroups = self::getRegisteredTaskGroups();
                $content = '';
                $taskInfo = array();
                $task = NULL;
@@ -566,6 +567,7 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                                // Set some task information
                                $taskInfo['disable'] = $taskRecord['disable'];
                                $taskInfo['description'] = $taskRecord['description'];
+                               $taskInfo['task_group'] = $taskRecord['task_group'];
                                // Check that the task object is valid
                                if ($this->scheduler->isValidTaskObject($task)) {
                                        // The task object is valid, process with fetching current data
@@ -726,6 +728,26 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                        '0' => array('<td class="td-label">', '</td>')
                );
                $tr++;
+               // Task group selector
+               $label = '<label for="task_group">' . $GLOBALS['LANG']->getLL('label.group') . '</label>';
+               $table[$tr][] = BackendUtility::wrapInHelp($this->cshKey, 'task_group', $label);
+               $cell = '<select name="tx_scheduler[task_group]" id="task_class" class="wide">';
+               // Loop on all groups to display a selector
+               $cell .= '<option value="0" title=""></option>';
+               foreach ($registeredTaskGroups as $taskGroup) {
+                       $selected = $taskGroup['uid'] == $taskInfo['task_group'] ? ' selected="selected"' : '';
+                       $cell .= '<option value="' . $taskGroup['uid'] . '"' . 'title="';
+                       $cell .= htmlspecialchars($taskGroup['groupName']) . '"' . $selected . '>';
+                       $cell .= htmlspecialchars($taskGroup['groupName']) . '</option>';
+               }
+               $cell .= '</select>';
+               $table[$tr][] = $cell;
+               $tableLayout[$tr] = array(
+                       'tr' => array('<tr id="task_group_row">', '</tr>'),
+                       'defCol' => $defaultCell,
+                       '0' => array('<td class="td-label">', '</td>')
+               );
+               $tr++;
                // Start date/time field
                // NOTE: datetime fields need a special id naming scheme
                $label = '<label for="tceforms-datetimefield-task_start">' . $GLOBALS['LANG']->getLL('label.start') . '</label>';
@@ -876,15 +898,25 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                $content = '';
                // Get list of registered classes
                $registeredClasses = self::getRegisteredClasses();
+               // Get list of registered task groups
+               $registeredTaskGroups = self::getRegisteredTaskGroups();
+
+               // add an empty entry for non-grouped tasks
+               // add in front of list
+               array_unshift($registeredTaskGroups, array('uid' => 0, 'groupName' => ''));
+
                // Get all registered tasks
+               // Just to get the number of entries
                $query = array(
                        'SELECT' => '*',
                        'FROM' => 'tx_scheduler_task',
                        'WHERE' => '1=1',
-                       'ORDERBY' => 'nextexecution'
+                       'ORDERBY' => ''
                );
                $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($query);
                $numRows = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
+               $GLOBALS['TYPO3_DB']->sql_free_result($res);
+
                // No tasks defined, display information message
                if ($numRows == 0) {
                        /** @var $flashMessage \TYPO3\CMS\Core\Messaging\FlashMessage */
@@ -928,6 +960,11 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                                '2' => array('<td class="right">', '</td>'),
                                '3' => array('<td colspan="6">', '</td>')
                        );
+                       $taskGroupRow = array(
+                               'tr' => array('<tr class="db_list_normal">', '</tr>'),
+                               'defCol' => array('<td>', '</td>'),
+                               '0' => array('<td colspan="10">', '</td>')
+                       );
                        $table = array();
                        $tr = 0;
                        // Header row
@@ -941,158 +978,188 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                        $table[$tr][] = $GLOBALS['LANG']->getLL('label.lastExecution');
                        $table[$tr][] = $GLOBALS['LANG']->getLL('label.nextExecution');
                        $tr++;
-                       // Loop on all tasks
-                       while ($schedulerRecord = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
-                               // Define action icons
-                               $editAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&CMD=edit&tx_scheduler[uid]=' . $schedulerRecord['uid'] . '" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:edit', TRUE) . '" class="icon">' . IconUtility::getSpriteIcon('actions-document-open') . '</a>';
-                               $deleteAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&CMD=delete&tx_scheduler[uid]=' . $schedulerRecord['uid'] . '" onclick="return confirm(\'' . $GLOBALS['LANG']->getLL('msg.delete') . '\');" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:delete', TRUE) . '" class="icon">' . IconUtility::getSpriteIcon('actions-edit-delete') . '</a>';
-                               $stopAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&CMD=stop&tx_scheduler[uid]=' . $schedulerRecord['uid'] . '" onclick="return confirm(\'' . $GLOBALS['LANG']->getLL('msg.stop') . '\');" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:stop', TRUE) . '" class="icon"><img ' . IconUtility::skinImg($this->backPath, (ExtensionManagementUtility::extRelPath('scheduler') . '/res/gfx/stop.png')) . ' alt="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:stop') . '" /></a>';
-                               $runAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&tx_scheduler[execute][]=' . $schedulerRecord['uid'] . '" title="' . $GLOBALS['LANG']->getLL('action.run_task') . '" class="icon">' . IconUtility::getSpriteIcon('extensions-scheduler-run-task') . '</a>';
-                               // Define some default values
-                               $lastExecution = '-';
-                               $isRunning = FALSE;
-                               $executionStatus = 'scheduled';
-                               $executionStatusOutput = '';
-                               $name = '';
-                               $nextDate = '-';
-                               $execType = '-';
-                               $frequency = '-';
-                               $multiple = '-';
-                               $startExecutionElement = '&nbsp;';
-                               // Restore the serialized task and pass it a reference to the scheduler object
-                               /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask|\TYPO3\CMS\Scheduler\ProgressProviderInterface */
-                               $task = unserialize($schedulerRecord['serialized_task_object']);
-                               $class = get_class($task);
-                               if ($class === '__PHP_Incomplete_Class' && preg_match('/^O:[0-9]+:"(?P<classname>.+?)"/', $schedulerRecord['serialized_task_object'], $matches) === 1) {
-                                       $class = $matches['classname'];
+
+                       foreach ($registeredTaskGroups as $taskGroup) {
+                               $query = array(
+                                       'SELECT' => '*',
+                                       'FROM' => 'tx_scheduler_task',
+                                       'WHERE' => 'task_group=' . $taskGroup['uid'],
+                                       'ORDERBY' => 'nextexecution'
+                               );
+
+                               $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($query);
+                               $numRows = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
+
+                               if ($numRows === 0) {
+                                       continue;
                                }
-                               // Assemble information about last execution
-                               $context = '';
-                               if (!empty($schedulerRecord['lastexecution_time'])) {
-                                       $lastExecution = date($dateFormat, $schedulerRecord['lastexecution_time']);
-                                       if ($schedulerRecord['lastexecution_context'] == 'CLI') {
-                                               $context = $GLOBALS['LANG']->getLL('label.cron');
-                                       } else {
-                                               $context = $GLOBALS['LANG']->getLL('label.manual');
+
+                               if ($taskGroup['groupName'] !== '') {
+                                       $tableLayout[$tr] = $taskGroupRow;
+                                       $groupText = '<strong>' . htmlspecialchars($taskGroup['groupName']) . '</strong>';
+                                       if (!empty($taskGroup['description'])) {
+                                               $groupText .= '<br />' . nl2br(htmlspecialchars($taskGroup['description']));
                                        }
-                                       $lastExecution .= ' (' . $context . ')';
+                                       $table[$tr][] = $groupText;
+                                       $tr++;
                                }
-                               if ($this->scheduler->isValidTaskObject($task)) {
-                                       // The task object is valid
-                                       $name = htmlspecialchars($registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')');
-                                       $additionalInformation = $task->getAdditionalInformation();
-                                       if ($task instanceof \TYPO3\CMS\Scheduler\ProgressProviderInterface) {
-                                               $progress = round(floatval($task->getProgress()), 2);
-                                               $name .= '<br />' . $this->renderTaskProgressBar($progress);
-                                       }
-                                       if (!empty($additionalInformation)) {
-                                               $name .= '<br />[' . htmlspecialchars($additionalInformation) . ']';
-                                       }
-                                       // Check if task currently has a running execution
-                                       if (!empty($schedulerRecord['serialized_executions'])) {
-                                               $isRunning = TRUE;
-                                               $executionStatus = 'running';
-                                       }
-                                       // Prepare display of next execution date
-                                       // If task is currently running, date is not displayed (as next hasn't been calculated yet)
-                                       // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway)
-                                       if ($isRunning || $schedulerRecord['disable'] == 1) {
-                                               $nextDate = '-';
-                                       } else {
-                                               $nextDate = date($dateFormat, $schedulerRecord['nextexecution']);
-                                               if (empty($schedulerRecord['nextexecution'])) {
-                                                       $nextDate = $GLOBALS['LANG']->getLL('none');
-                                               } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) {
-                                                       // Next execution is overdue, highlight date
-                                                       $nextDate = '<span class="late" title="' . $GLOBALS['LANG']->getLL('status.legend.scheduled') . '">' . $nextDate . '</span>';
-                                                       $executionStatus = 'late';
-                                               }
+
+                               // Loop on all tasks
+                               while ($schedulerRecord = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
+                                       // Define action icons
+                                       $editAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&CMD=edit&tx_scheduler[uid]=' . $schedulerRecord['uid'] . '" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:edit', TRUE) . '" class="icon">' . IconUtility::getSpriteIcon('actions-document-open') . '</a>';
+                                       $deleteAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&CMD=delete&tx_scheduler[uid]=' . $schedulerRecord['uid'] . '" onclick="return confirm(\'' . $GLOBALS['LANG']->getLL('msg.delete') . '\');" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:delete', TRUE) . '" class="icon">' . IconUtility::getSpriteIcon('actions-edit-delete') . '</a>';
+                                       $stopAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&CMD=stop&tx_scheduler[uid]=' . $schedulerRecord['uid'] . '" onclick="return confirm(\'' . $GLOBALS['LANG']->getLL('msg.stop') . '\');" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:stop', TRUE) . '" class="icon"><img ' . IconUtility::skinImg($this->backPath, (ExtensionManagementUtility::extRelPath('scheduler') . '/res/gfx/stop.png')) . ' alt="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:stop') . '" /></a>';
+                                       $runAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&tx_scheduler[execute][]=' . $schedulerRecord['uid'] . '" title="' . $GLOBALS['LANG']->getLL('action.run_task') . '" class="icon">' . IconUtility::getSpriteIcon('extensions-scheduler-run-task') . '</a>';
+                                       // Define some default values
+                                       $lastExecution = '-';
+                                       $isRunning = FALSE;
+                                       $executionStatus = 'scheduled';
+                                       $executionStatusOutput = '';
+                                       $name = '';
+                                       $nextDate = '-';
+                                       $execType = '-';
+                                       $frequency = '-';
+                                       $multiple = '-';
+                                       $startExecutionElement = '&nbsp;';
+                                       // Restore the serialized task and pass it a reference to the scheduler object
+                                       /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask|\TYPO3\CMS\Scheduler\ProgressProviderInterface */
+                                       $task = unserialize($schedulerRecord['serialized_task_object']);
+                                       $class = get_class($task);
+                                       if ($class === '__PHP_Incomplete_Class' && preg_match('/^O:[0-9]+:"(?P<classname>.+?)"/', $schedulerRecord['serialized_task_object'], $matches) === 1) {
+                                               $class = $matches['classname'];
                                        }
-                                       // Get execution type
-                                       if ($task->getExecution()->getInterval() == 0 && $task->getExecution()->getCronCmd() == '') {
-                                               $execType = $GLOBALS['LANG']->getLL('label.type.single');
-                                               $frequency = '-';
-                                       } else {
-                                               $execType = $GLOBALS['LANG']->getLL('label.type.recurring');
-                                               if ($task->getExecution()->getCronCmd() == '') {
-                                                       $frequency = $task->getExecution()->getInterval();
+                                       // Assemble information about last execution
+                                       $context = '';
+                                       if (!empty($schedulerRecord['lastexecution_time'])) {
+                                               $lastExecution = date($dateFormat, $schedulerRecord['lastexecution_time']);
+                                               if ($schedulerRecord['lastexecution_context'] == 'CLI') {
+                                                       $context = $GLOBALS['LANG']->getLL('label.cron');
                                                } else {
-                                                       $frequency = $task->getExecution()->getCronCmd();
+                                                       $context = $GLOBALS['LANG']->getLL('label.manual');
                                                }
+                                               $lastExecution .= ' (' . $context . ')';
                                        }
-                                       // Get multiple executions setting
-                                       if ($task->getExecution()->getMultiple()) {
-                                               $multiple = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:yes');
-                                       } else {
-                                               $multiple = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:no');
-                                       }
-                                       // Define checkbox
-                                       $startExecutionElement = '<input type="checkbox" name="tx_scheduler[execute][]" value="' . $schedulerRecord['uid'] . '" id="task_' . $schedulerRecord['uid'] . '" class="checkboxes" />';
 
-                                       $actions = $editAction . $deleteAction;
+                                       if ($this->scheduler->isValidTaskObject($task)) {
+                                               // The task object is valid
+                                               $name = htmlspecialchars($registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')');
+                                               $additionalInformation = $task->getAdditionalInformation();
+                                               if ($task instanceof \TYPO3\CMS\Scheduler\ProgressProviderInterface) {
+                                                       $progress = round(floatval($task->getProgress()), 2);
+                                                       $name .= '<br />' . $this->renderTaskProgressBar($progress);
+                                               }
+                                               if (!empty($additionalInformation)) {
+                                                       $name .= '<br />[' . htmlspecialchars($additionalInformation) . ']';
+                                               }
+                                               // Check if task currently has a running execution
+                                               if (!empty($schedulerRecord['serialized_executions'])) {
+                                                       $isRunning = TRUE;
+                                                       $executionStatus = 'running';
+                                               }
 
-                                       // Check the disable status
-                                       // Row is shown dimmed if task is disabled, unless it is still running
-                                       if ($schedulerRecord['disable'] == 1 && !$isRunning) {
-                                               $tableLayout[$tr] = $disabledTaskRow;
-                                               $executionStatus = 'disabled';
-                                       }
+                                               // Prepare display of next execution date
+                                               // If task is currently running, date is not displayed (as next hasn't been calculated yet)
+                                               // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway)
+                                               if ($isRunning || $schedulerRecord['disable'] == 1) {
+                                                       $nextDate = '-';
+                                               } else {
+                                                       $nextDate = date($dateFormat, $schedulerRecord['nextexecution']);
+                                                       if (empty($schedulerRecord['nextexecution'])) {
+                                                               $nextDate = $GLOBALS['LANG']->getLL('none');
+                                                       } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) {
+                                                               // Next execution is overdue, highlight date
+                                                               $nextDate = '<span class="late" title="' . $GLOBALS['LANG']->getLL('status.legend.scheduled') . '">' . $nextDate . '</span>';
+                                                               $executionStatus = 'late';
+                                                       }
+                                               }
+                                               // Get execution type
+                                               if ($task->getExecution()->getInterval() == 0 && $task->getExecution()->getCronCmd() == '') {
+                                                       $execType = $GLOBALS['LANG']->getLL('label.type.single');
+                                                       $frequency = '-';
+                                               } else {
+                                                       $execType = $GLOBALS['LANG']->getLL('label.type.recurring');
+                                                       if ($task->getExecution()->getCronCmd() == '') {
+                                                               $frequency = $task->getExecution()->getInterval();
+                                                       } else {
+                                                               $frequency = $task->getExecution()->getCronCmd();
+                                                       }
+                                               }
+                                               // Get multiple executions setting
+                                               if ($task->getExecution()->getMultiple()) {
+                                                       $multiple = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:yes');
+                                               } else {
+                                                       $multiple = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:no');
+                                               }
+                                               // Define checkbox
+                                               $startExecutionElement = '<input type="checkbox" name="tx_scheduler[execute][]" value="' . $schedulerRecord['uid'] . '" id="task_' . $schedulerRecord['uid'] . '" class="checkboxes" />';
 
-                                       // Show no action links (edit, delete) if task is running
-                                       if ($isRunning) {
-                                               $actions = $stopAction;
-                                       } else {
-                                               $actions .= $runAction;
-                                       }
+                                               $actions = $editAction . $deleteAction;
+
+                                               // Check the disable status
+                                               // Row is shown dimmed if task is disabled, unless it is still running
+                                               if ($schedulerRecord['disable'] == 1 && !$isRunning) {
+                                                       $tableLayout[$tr] = $disabledTaskRow;
+                                                       $executionStatus = 'disabled';
+                                               }
 
-                                       // Check if the last run failed
-                                       $failureOutput = '';
-                                       if (!empty($schedulerRecord['lastexecution_failure'])) {
-                                               // Try to get the stored exception object
-                                               /** @var $exception \Exception */
-                                               $exception = unserialize($schedulerRecord['lastexecution_failure']);
-                                               // If the exception could not be unserialized, issue a default error message
-                                               if ($exception === FALSE || $exception instanceof \__PHP_Incomplete_Class) {
-                                                       $failureDetail = $GLOBALS['LANG']->getLL('msg.executionFailureDefault');
+                                               // Show no action links (edit, delete) if task is running
+                                               if ($isRunning) {
+                                                       $actions = $stopAction;
                                                } else {
-                                                       $failureDetail = sprintf($GLOBALS['LANG']->getLL('msg.executionFailureReport'), $exception->getCode(), $exception->getMessage());
+                                                       $actions .= $runAction;
                                                }
-                                               $failureOutput = ' <img ' . IconUtility::skinImg(ExtensionManagementUtility::extRelPath('scheduler'), 'res/gfx/status_failure.png') . ' alt="' . htmlspecialchars($GLOBALS['LANG']->getLL('status.failure')) . '" title="' . htmlspecialchars($failureDetail) . '" />';
-                                       }
-                                       // Format the execution status,
-                                       // including failure feedback, if any
-                                       $executionStatusOutput = '<img ' . IconUtility::skinImg(ExtensionManagementUtility::extRelPath('scheduler'), ('res/gfx/status_' . $executionStatus . '.png')) . ' id="executionstatus_' . $schedulerRecord['uid'] . '" alt="' . htmlspecialchars($GLOBALS['LANG']->getLL(('status.' . $executionStatus))) . '" title="' . htmlspecialchars($GLOBALS['LANG']->getLL(('status.legend.' . $executionStatus))) . '" />' . $failureOutput;
-                                       $table[$tr][] = $startExecutionElement;
-                                       $table[$tr][] = $actions;
-                                       $table[$tr][] = $schedulerRecord['uid'];
-                                       $table[$tr][] = $executionStatusOutput;
-                                       if ($schedulerRecord['description'] !== '') {
-                                               if (!empty($this->scheduler->extConf['listShowTaskDescriptionAsHover'])) {
-                                                       $table[$tr][] = '<span title="' . htmlspecialchars($schedulerRecord['description']) . '">' . $name . '</span>';
+
+                                               // Check if the last run failed
+                                               $failureOutput = '';
+                                               if (!empty($schedulerRecord['lastexecution_failure'])) {
+                                                       // Try to get the stored exception object
+                                                       /** @var $exception \Exception */
+                                                       $exception = unserialize($schedulerRecord['lastexecution_failure']);
+                                                       // If the exception could not be unserialized, issue a default error message
+                                                       if ($exception === FALSE || $exception instanceof \__PHP_Incomplete_Class) {
+                                                               $failureDetail = $GLOBALS['LANG']->getLL('msg.executionFailureDefault');
+                                                       } else {
+                                                               $failureDetail = sprintf($GLOBALS['LANG']->getLL('msg.executionFailureReport'), $exception->getCode(), $exception->getMessage());
+                                                       }
+                                                       $failureOutput = ' <img ' . IconUtility::skinImg(ExtensionManagementUtility::extRelPath('scheduler'), 'res/gfx/status_failure.png') . ' alt="' . htmlspecialchars($GLOBALS['LANG']->getLL('status.failure')) . '" title="' . htmlspecialchars($failureDetail) . '" />';
+                                               }
+                                               // Format the execution status,
+                                               // including failure feedback, if any
+                                               $executionStatusOutput = '<img ' . IconUtility::skinImg(ExtensionManagementUtility::extRelPath('scheduler'), ('res/gfx/status_' . $executionStatus . '.png')) . ' id="executionstatus_' . $schedulerRecord['uid'] . '" alt="' . htmlspecialchars($GLOBALS['LANG']->getLL(('status.' . $executionStatus))) . '" title="' . htmlspecialchars($GLOBALS['LANG']->getLL(('status.legend.' . $executionStatus))) . '" />' . $failureOutput;
+                                               $table[$tr][] = $startExecutionElement;
+                                               $table[$tr][] = $actions;
+                                               $table[$tr][] = $schedulerRecord['uid'];
+                                               $table[$tr][] = $executionStatusOutput;
+                                               if ($schedulerRecord['description'] !== '') {
+                                                       if (!empty($this->scheduler->extConf['listShowTaskDescriptionAsHover'])) {
+                                                               $table[$tr][] = '<span title="' . htmlspecialchars($schedulerRecord['description']) . '">' . $name . '</span>';
+                                                       } else {
+                                                               $table[$tr][] = $name . '<br />' . nl2br(htmlspecialchars($schedulerRecord['description'])) . '<br />';
+                                                       }
                                                } else {
-                                                       $table[$tr][] = $name . '<br />' . nl2br(htmlspecialchars($schedulerRecord['description'])) . '<br />';
+                                                       $table[$tr][] = $name;
                                                }
+                                               $table[$tr][] = $execType;
+                                               $table[$tr][] = $frequency;
+                                               $table[$tr][] = $multiple;
+                                               $table[$tr][] = $lastExecution;
+                                               $table[$tr][] = $nextDate;
                                        } else {
-                                               $table[$tr][] = $name;
+                                               // The task object is not valid
+                                               // Prepare to issue an error
+                                               /** @var $flashMessage \TYPO3\CMS\Core\Messaging\FlashMessage */
+                                               $flashMessage = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Messaging\\FlashMessage', sprintf($GLOBALS['LANG']->getLL('msg.invalidTaskClass'), $class), '', \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR);
+                                               $executionStatusOutput = $flashMessage->render();
+                                               $tableLayout[$tr] = $rowWithSpan;
+                                               $table[$tr][] = $startExecutionElement;
+                                               $table[$tr][] = $deleteAction;
+                                               $table[$tr][] = $schedulerRecord['uid'];
+                                               $table[$tr][] = $executionStatusOutput;
                                        }
-                                       $table[$tr][] = $execType;
-                                       $table[$tr][] = $frequency;
-                                       $table[$tr][] = $multiple;
-                                       $table[$tr][] = $lastExecution;
-                                       $table[$tr][] = $nextDate;
-                               } else {
-                                       // The task object is not valid
-                                       // Prepare to issue an error
-                                       /** @var $flashMessage \TYPO3\CMS\Core\Messaging\FlashMessage */
-                                       $flashMessage = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Messaging\\FlashMessage', sprintf($GLOBALS['LANG']->getLL('msg.invalidTaskClass'), $class), '', \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR);
-                                       $executionStatusOutput = $flashMessage->render();
-                                       $tableLayout[$tr] = $rowWithSpan;
-                                       $table[$tr][] = $startExecutionElement;
-                                       $table[$tr][] = $deleteAction;
-                                       $table[$tr][] = $schedulerRecord['uid'];
-                                       $table[$tr][] = $executionStatusOutput;
+                                       $tr++;
                                }
-                               $tr++;
+                               $GLOBALS['TYPO3_DB']->sql_free_result($res);
                        }
                        // Render table
                        $content .= $this->doc->table($table, $tableLayout);
@@ -1117,7 +1184,6 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                        </ul>';
                        $content .= $this->displayServerTime();
                }
-               $GLOBALS['TYPO3_DB']->sql_free_result($res);
                return $content;
        }
 
@@ -1159,6 +1225,8 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                        $task->setDisabled($this->submittedData['disable']);
                        // Set description
                        $task->setDescription($this->submittedData['description']);
+                       // Set task group
+                       $task->setTaskGroup($this->submittedData['task_group']);
                        // Save additional input values
                        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
                                /** @var $providerObject \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface */
@@ -1198,6 +1266,8 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                        $task->setDisabled($this->submittedData['disable']);
                        // Set description
                        $task->setDescription($this->submittedData['description']);
+                       // Set description
+                       $task->setTaskGroup($this->submittedData['task_group']);
                        // Add to database
                        $result = $this->scheduler->addTask($task);
                        if ($result) {
@@ -1363,7 +1433,7 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
        }
 
        /**
-        * This method a list of all classes that have been registered with the Scheduler
+        * This method fetches a list of all classes that have been registered with the Scheduler
         * For each item the following information is provided, as an associative array:
         *
         * ['extension']        =>      Key of the extension which provides the class
@@ -1392,6 +1462,31 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                return $list;
        }
 
+       /**
+        * This method fetches list of all group that have been registered with the Scheduler
+        *
+        * @return array List of registered groups
+        */
+       static protected function getRegisteredTaskGroups() {
+               $list = array();
+
+               // Get all registered task groups
+               $query = array(
+                       'SELECT' => '*',
+                       'FROM' => 'tx_scheduler_task_group',
+                       'WHERE' => '1=1' . \TYPO3\CMS\Backend\Utility\BackendUtility::BEenableFields('tx_scheduler_task_group'),
+                       'ORDERBY' => 'sorting'
+               );
+               $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($query);
+
+               while (($groupRecord = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) !== FALSE) {
+                       $list[] = $groupRecord;
+               }
+               $GLOBALS['TYPO3_DB']->sql_free_result($res);
+
+               return $list;
+       }
+
        /*************************
         *
         * RENDERING UTILITIES
index 839bdf2..f8c81c5 100644 (file)
@@ -70,6 +70,7 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                                'crdate' => $GLOBALS['EXEC_TIME'],
                                'disable' => $task->isDisabled(),
                                'description' => $task->getDescription(),
+                               'task_group' => $task->getTaskGroup(),
                                'serialized_task_object' => 'RESERVED'
                        );
                        $result = $GLOBALS['TYPO3_DB']->exec_INSERTquery('tx_scheduler_task', $fields);
@@ -235,6 +236,7 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                                'nextexecution' => $executionTime,
                                'disable' => $task->isDisabled(),
                                'description' => $task->getDescription(),
+                               'task_group' => $task->getTaskGroup(),
                                'serialized_task_object' => serialize($task)
                        );
                        $result = $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . $taskUid, $fields);
@@ -259,16 +261,21 @@ class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
                // Define where clause
                // If no uid is given, take any non-disabled task which has a next execution time in the past
                if (empty($uid)) {
-                       $whereClause = 'disable = 0 AND nextexecution != 0 AND nextexecution <= ' . $GLOBALS['EXEC_TIME'];
+                       $queryArray = array(
+                               'SELECT' => 'tx_scheduler_task.uid AS uid, serialized_task_object',
+                               'FROM' => 'tx_scheduler_task LEFT JOIN tx_scheduler_task_group ON (tx_scheduler_task.task_group = tx_scheduler_task_group.uid)',
+                               'WHERE' => 'disable = 0 AND nextexecution != 0 AND nextexecution <= ' . $GLOBALS['EXEC_TIME'] . ' AND (tx_scheduler_task_group.hidden = 0 OR tx_scheduler_task_group.hidden IS NULL)',
+                               'LIMIT' => 1
+                       );
                } else {
-                       $whereClause = 'uid = ' . intval($uid);
+                       $queryArray = array(
+                               'SELECT' => 'uid, serialized_task_object',
+                               'FROM' => 'tx_scheduler_task',
+                               'WHERE' => 'uid = ' . intval($uid),
+                               'LIMIT' => 1
+                       );
                }
-               $queryArray = array(
-                       'SELECT' => 'uid, serialized_task_object',
-                       'FROM' => 'tx_scheduler_task',
-                       'WHERE' => $whereClause,
-                       'LIMIT' => 1
-               );
+
                $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($queryArray);
                // If there are no available tasks, thrown an exception
                if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
index fbc3310..bb2abc7 100644 (file)
@@ -43,14 +43,14 @@ abstract class AbstractTask {
        /**
         * The unique id of the task used to identify it in the database
         *
-        * @var int
+        * @var integer
         */
        protected $taskUid;
 
        /**
         * Disable flag, TRUE if task is disabled, FALSE otherwise
         *
-        * @var         boolean
+        * @var boolean
         */
        protected $disabled = FALSE;
 
@@ -76,6 +76,13 @@ abstract class AbstractTask {
        protected $description = '';
 
        /**
+        * Task group for this task
+        *
+        * @var integer
+        */
+       protected $taskGroup;
+
+       /**
         * Constructor
         */
        public function __construct() {
@@ -186,6 +193,25 @@ abstract class AbstractTask {
        }
 
        /**
+        * This method returns the task group (uid) of the task
+        *
+        * @return integer Uid of task group
+        */
+       public function getTaskGroup() {
+               return $this->taskGroup;
+       }
+
+       /**
+        * This method is used to set the task group (uid) of the task
+        *
+        * @param integer $timestamp Uid of task group
+        * @return void
+        */
+       public function setTaskGroup($taskGroup) {
+               $this->taskGroup = intval($taskGroup);
+       }
+
+       /**
         * This method returns the timestamp corresponding to the next execution time of the task
         *
         * @return integer Timestamp of next execution
diff --git a/typo3/sysext/scheduler/Configuration/TCA/tx_scheduler_task_group.php b/typo3/sysext/scheduler/Configuration/TCA/tx_scheduler_task_group.php
new file mode 100644 (file)
index 0000000..79de433
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+return array(
+       'ctrl' => array(
+               'label' => 'groupName',
+               'tstamp' => 'tstamp',
+               'title' => 'LLL:EXT:scheduler/locallang_tca.xlf:tx_scheduler_task_group',
+               'crdate' => 'crdate',
+               'cruser_id' => 'cruser_id',
+               'delete' => 'deleted',
+               'sortby' => 'sorting',
+               'iconfile' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extRelPath('scheduler') . 'ext_icon.gif',
+               'adminOnly' => 1, // Only admin users can edit
+               'rootLevel' => 1,
+               'enablecolumns' => array(
+                       'disabled' => 'hidden'
+               ),
+               'searchFields' => 'groupName'
+       ),
+       'interface' => array(
+               'showRecordFieldList' => 'hidden,groupName'
+       ),
+       'columns' => array(
+               'groupName' => array(
+                       'label' => 'LLL:EXT:scheduler/locallang_tca.xlf:tx_scheduler_task_group.groupName',
+                       'config' => array(
+                               'type' => 'input',
+                               'size' => '35',
+                               'max' => '80',
+                               'eval' => 'required,unique,trim',
+                               'softref' => 'substitute'
+                       )
+               ),
+               'description' => array(
+                       'label' => 'LLL:EXT:scheduler/locallang_tca.xlf:tx_scheduler_task_group.description',
+                       'config' => array(
+                               'type' => 'text'
+                       ),
+               ),
+               'hidden' => array(
+                       'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.disable',
+                       'exclude' => 1,
+                       'config' => array(
+                               'type' => 'check',
+                               'default' => '0'
+                       )
+               )
+       ),
+       'types' => array(
+               '1' => array('showitem' => 'hidden;;;;1-1-1,groupName;;1;;3-3-3,description;;1;;3-3-3')
+       )
+);
\ No newline at end of file
diff --git a/typo3/sysext/scheduler/Documentation/Administration/GroupTask/Index.rst b/typo3/sysext/scheduler/Documentation/Administration/GroupTask/Index.rst
new file mode 100644 (file)
index 0000000..907cd53
--- /dev/null
@@ -0,0 +1,32 @@
+.. ==================================================
+.. FOR YOUR INFORMATION
+.. --------------------------------------------------
+.. -*- coding: utf-8 -*- with BOM.
+
+.. include:: ../../Includes.txt
+
+
+
+.. _groupting-tasks:
+
+Grouping tasks together
+^^^^^^^^^^^^^^^^^^^^^^^
+
+In case of a high number of different tasks, it may be useful to visually group similar tasks together:
+
+.. figure:: ../../Images/GroupedTasks.png
+   :alt: Overview of tasks with grouped
+
+   Grouping related tasks together
+
+Scheduler task groups are records stored on the root page (pid=0). They may be created, edited and sorted with Web > List:
+
+.. figure:: ../../Images/GroupRecords.png
+   :alt: Task group records
+
+   Management of scheduler task groups
+
+Individual tasks may then be edited as usual and associated to a given scheduler task group.
+
+.. note::
+   Tasks inside a hidden group are neither automatically executed nor displayed in the scheduler module.
index 62df850..b3517ea 100644 (file)
@@ -25,6 +25,7 @@ one for actually managing the tasks.
    BackendModule/Index
    EditTask/Index
    DeleteTask/Index
+   GroupTask/Index
    StopTask/Index
    ManualExecution/Index
 
diff --git a/typo3/sysext/scheduler/Documentation/Images/GroupRecords.png b/typo3/sysext/scheduler/Documentation/Images/GroupRecords.png
new file mode 100644 (file)
index 0000000..ccbdd55
Binary files /dev/null and b/typo3/sysext/scheduler/Documentation/Images/GroupRecords.png differ
diff --git a/typo3/sysext/scheduler/Documentation/Images/GroupedTasks.png b/typo3/sysext/scheduler/Documentation/Images/GroupedTasks.png
new file mode 100644 (file)
index 0000000..858a6c4
Binary files /dev/null and b/typo3/sysext/scheduler/Documentation/Images/GroupedTasks.png differ
index 4fe7e64..a12e283 100755 (executable)
@@ -12,6 +12,26 @@ CREATE TABLE tx_scheduler_task (
        lastexecution_context char(3) DEFAULT '' NOT NULL,
        serialized_task_object blob,
        serialized_executions blob,
+       task_group int(11) unsigned DEFAULT '0' NOT NULL,
        PRIMARY KEY (uid),
        KEY index_nextexecution (nextexecution)
 );
+
+#
+# Table structure for table 'tx_scheduler_task_group'
+#
+CREATE TABLE tx_scheduler_task_group (
+       uid int(11) unsigned DEFAULT '0' NOT NULL auto_increment,
+       pid int(11) unsigned DEFAULT '0' NOT NULL,
+       tstamp int(11) unsigned DEFAULT '0' NOT NULL,
+       crdate int(11) unsigned DEFAULT '0' NOT NULL,
+       cruser_id int(11) unsigned DEFAULT '0' NOT NULL,
+       deleted tinyint(4) unsigned DEFAULT '0' NOT NULL,
+       sorting int(11) unsigned DEFAULT '0' NOT NULL,
+       hidden tinyint(4) unsigned DEFAULT '0' NOT NULL,
+       groupName varchar(80) DEFAULT '' NOT NULL,
+       description text NOT NULL,
+
+       PRIMARY KEY (uid),
+       KEY parent (pid)
+);
\ No newline at end of file
diff --git a/typo3/sysext/scheduler/locallang_tca.xlf b/typo3/sysext/scheduler/locallang_tca.xlf
new file mode 100644 (file)
index 0000000..86be6c8
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.0">
+       <file source-language="en" datatype="plaintext" original="messages" date="2011-10-17T20:22:37Z" product-name="scheduler">
+               <header/>
+               <body>
+                       <trans-unit id="tx_scheduler_task_group" xml:space="preserve">
+                               <source>Scheduler task group</source>
+                       </trans-unit>
+                       <trans-unit id="tx_scheduler_task_group.groupName" xml:space="preserve">
+                               <source>Group name</source>
+                       </trans-unit>
+               </body>
+       </file>
+</xliff>
index 836a34d..5f4fa35 100644 (file)
                        <trans-unit id="label.type.single" xml:space="preserve">
                                <source>Single</source>
                        </trans-unit>
+                       <trans-unit id="label.group" xml:space="preserve">
+                               <source>Task group</source>
+                       </trans-unit>
                        <trans-unit id="msg.addError" xml:space="preserve">
                                <source>The task could not be added.</source>
                        </trans-unit>