diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 81ba4368..cabc576d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -> **Note:** Please fill out all required sections and remove irrelevant ones. +> **Note:** Please fill out all relevant sections and remove irrelevant ones. ### 🔀 Purpose of this PR: - [ ] Fixes a bug diff --git a/.github/workflows/config.json b/.github/workflows/config.json index cc1bd5a9..71a177e2 100644 --- a/.github/workflows/config.json +++ b/.github/workflows/config.json @@ -1,16 +1,33 @@ { + "moodle-plugin-ci": "4.5.6", "main-moodle": "MOODLE_405_STABLE", "main-php": "8.3", "main-db": "pgsql", "moodle-testmatrix": { "MOODLE_401_STABLE": { - "php": ["8.0", "8.1"], - "db": ["pgsql", "mariadb", "mysqli"] + "php": [ + "8.0", + "8.1" + ] + }, + "MOODLE_404_STABLE": { + "php": [ + "8.1", + "8.2", + "8.3" + ] }, "MOODLE_405_STABLE": { - "php": ["8.1", "8.2", "8.3"], - "db": ["pgsql", "mariadb", "mysqli"] + "php": [ + "8.1", + "8.2", + "8.3" + ], + "db": [ + "pgsql", + "mariadb", + "mysqli" + ] } - }, - "moodle-plugin-ci": "4.4.5" + } } diff --git a/.github/workflows/config_dev.json b/.github/workflows/config_dev.json new file mode 100644 index 00000000..e48a1e63 --- /dev/null +++ b/.github/workflows/config_dev.json @@ -0,0 +1,18 @@ +{ + "moodle-plugin-ci": "4.5.6", + "main-moodle": "MOODLE_405_STABLE", + "main-php": "8.3", + "main-db": "pgsql", + "moodle-testmatrix": { + "MOODLE_405_STABLE": { + "php": [ + "8.3" + ], + "db": [ + "pgsql", + "mariadb", + "mysqli" + ] + } + } +} diff --git a/.github/workflows/config_publish.json b/.github/workflows/config_publish.json new file mode 100644 index 00000000..68bf2ffc --- /dev/null +++ b/.github/workflows/config_publish.json @@ -0,0 +1,33 @@ +{ + "moodle-plugin-ci": "4.5.6", + "main-moodle": "MOODLE_405_STABLE", + "main-php": "8.3", + "main-db": "pgsql", + "moodle-testmatrix": { + "MOODLE_401_STABLE": { + "php": [ + "8.0", + "8.1" + ] + }, + "MOODLE_404_STABLE": { + "php": [ + "8.1", + "8.2", + "8.3" + ] + }, + "MOODLE_405_STABLE": { + "php": [ + "8.1", + "8.2", + "8.3" + ], + "db": [ + "pgsql", + "mariadb", + "mysqli" + ] + } + } +} diff --git a/CHANGES.md b/CHANGES.md index 0c42ad85..d8e86195 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,67 @@ CHANGELOG ========= +Further information to the latest changes can be found here: https://github.com/learnweb/moodle-tool_lifecycle/wiki/Changes-of-version-4.5.4-and-4.5.5 +and here: https://github.com/learnweb/moodle-tool_lifecycle/wiki/Changes-of-version-4.5-in-detail + +4.5.5 (2025-02-10) +------------------ +* [FIXED] Problems with check_course_code +* [FIXED] Display time as usertime, not UTC (Issue #188) +* [FEATURE] A lack of capability is now the only reason not to render the manage courses sec nav link issue #198 +* [FIXED] Fix unit test PR #230 +* [FIXED] Workflowoverview Course selection: show run-link only when workflow is active +* [FEATURE] Display error time in errors table +* [FEATURE] Provide possibility to delete invalid/outdated process error entries +* [FEATURE] Lifecycle Task: trace course processing as well, Trigger categories: improve cat listing in frozen edit form +* [FIXED] Fix missing course error when aborting processes after course deletion +* [FEATURE] New lib function multiple_use() makes a trigger type choosable for a single workflow n-times +* [FEATURE] Introduce a step option to define an individual target step in case of rollback (issue #213) +* [FEATURE] EXPERIMENTAL: Trigger selection sql - conjunction(AND) and disjunction(OR) now possible (workflow option) +* [FEATURE] Refactor last access trigger: no single course ids in sql (issue #243) +* [FEATURE] Subplugins both ways of describing following MDL-83705 (issue #249) +* [FEATURE] Introduce new lib function check_course_code to force using function check_course for every course candidate (issue #243) +* [FIXED] Workflows with triggers which have more than 65.535 paramaters throw an error (issue #243) +* [FEATURE] Display trigger counting partial as tooltip; show courses already part of the workflow process or the process errors +* [FEATURE] Workflowoverview: show also '0' courses in exclude trigger. Make instancenames in supplugin form of active workflows static +* [FIXED] proceed, rollback event: take course context when context is missing +* [FIXED] Fix trigger customfielddelay's missing field error message +* [FEATURE] Delete all delays: show amount of delays that would be deleted next to button +* [FEATURE] Workflowoverview: exclude trigger: show excluded 0 as well +* [FIXED] prozessor.php: restore version 4.5 of function process_courses +* [FIXED] prozessor.php: restore version 4.5 of function call_trigger +* [FEATURE] Workflowoverview: place new link to run lifecycle task in timetrigger row +* [FIXED] Fix step email context course id error +* [FIXED] call_trigger: mtrace only when called by cron +* [FIXED] Fix behat tests interaction.feature and manual_trigger.feature +* [FIXED] Step libs' function process_course error: make sure course is of type stdClass +* [FIXED] Fix unit test process_error_test +* [FEATURE] Add additional jobs to run in ci-file +* [FIXED] Fixed error occurring when renaming field manual to manually during the upgrade + +4.5.4 (2025-06-23) +------------------ +* [FEATURE] No php notice if string plugindescription is missing +* [FIXED] Fix adminapprove sql error single approve +* [FIXED] catch/prevent missing workflow error +* [FIXED] Fix uploadworkflow wrong redirect +* [FEATURE] Workflowoverview: show also delayed trigger courses but only when still in delay +* [FEATURE] Shift showdetails icon to block trigger +* [FEATURE] activestep.php: make approval tab an active link +* [FEATURE] Exclude trigger customfieldsemester from git +* [FIXED] mtrace processes without workflow and delete processes of removed courses +* [FEATURE] Remove customfield_semester dependency +* [FIXED] Improve display of next run time +* [FEATURE] Trigger byrole: introduce invert function +* [FEATURE] Prevent deleting course 1 +* [FEATURE] Add step movecategory +* [FEATURE] Add trigger lastaccess +* [FIXED] Fix lastaccess error when no courses found +* [FEATURE] Add filter form to procerror page +* [FEATURE] Categories trigger: instance settings should not be editable +* [FEATURE] Shift subplugins list to own page +* [FIXED] DB field 'manual' now reserved word in mysql version 8.4, change to 'manually' + 4.5.0 (2025-05-04) ------------------ * [FEATURE] Workflowoverview: Added possibility to select single courses for process diff --git a/README.md b/README.md index 7c8bf9a2..811aa7c4 100644 --- a/README.md +++ b/README.md @@ -20,20 +20,19 @@ To be adaptable to the needs of different institutions the plugin provides two s ## Subplugins Requirements that are specific to your institution can be added through additional subplugins. -A list of all subplugins and more information can be found in the [Wiki](https://github.com/learnweb/moodle-tool_lifecycle/wiki/List-of-Additional-Subplugins) ([subpluginslist](https://github.com/learnweb/moodle-tool_lifecycle/wiki/List-of-Additional-Subplugins)). +A list of all subplugins and more information can be found in the [Wiki](https://github.com/learnweb/moodle-tool_lifecycle/wiki) ([subpluginslist](https://github.com/learnweb/moodle-tool_lifecycle/wiki/List-of-Installed-Subplugins)). It provides instructions for administrators as well as for developers to implement their own requirements into subplugins. Installation ============ This is an admin plugin and should go into ``admin/tool/lifecycle``. + +In the current Lifecycle version 4.5.5 (v4.5-r6), it may be necessary to delete the old directory admin/tool/lifecycle and +clone the new version with Git. This should be possible without any issues, as no user data is stored in that directory. ==> But be aware: If you are using the customfieldsemester trigger or other special triggers and steps, or if you have made local +changes to the included triggers or steps you would need to save them before installation and copy them into the new directory afterwards. + Obtain this plugin from https://moodle.org/plugins/view/tool_lifecycle. Moodle version ============== -The plugin is continously tested with all moodle versions, which are security supported by the moodle headquarter. -Therefore, Travis uses the most current release to build a test instance and run the behat and unit tests on them. -In addition to all stable branches the version is tested against the master branch to support early adopters. - -Changelog -========= -The changes for every release are listed here: https://github.com/learnweb/moodle-tool_lifecycle/wiki/Change-log. +The plugin is continuously tested with all moodle versions, which are security supported by the moodle headquarter. diff --git a/activeprocesses.php b/activeprocesses.php index 0b801e87..16dec68c 100644 --- a/activeprocesses.php +++ b/activeprocesses.php @@ -31,8 +31,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::ACTIVE_PROCESSES)); @@ -71,7 +70,9 @@ $heading = get_string('pluginname', 'tool_lifecycle')." / ".get_string('find_course_list_header', 'tool_lifecycle'); echo $renderer->header($heading); -$tabrow = tabs::get_tabrow(true); +$tabparams = new stdClass(); +$tabparams->activelink = true; +$tabrow = tabs::get_tabrow($tabparams); $id = 'activeworkflows'; $renderer->tabs($tabrow, $id); diff --git a/activeworkflows.php b/activeworkflows.php index 702c7705..1784c74d 100644 --- a/activeworkflows.php +++ b/activeworkflows.php @@ -32,8 +32,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::ACTIVE_WORKFLOWS)); diff --git a/classes/event/process_proceeded.php b/classes/event/process_proceeded.php index 8a6e56a0..d82a8623 100644 --- a/classes/event/process_proceeded.php +++ b/classes/event/process_proceeded.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\event; +use context_course; use moodle_url; use tool_lifecycle\local\entity\process; @@ -54,7 +55,7 @@ class process_proceeded extends \core\event\base { public static function event_from_process($process) { $data = [ 'courseid' => $process->courseid, - 'context' => $process->context, + 'context' => $process->context ?? context_course::instance($process->courseid), 'other' => [ 'processid' => $process->id, 'workflowid' => $process->workflowid, diff --git a/classes/event/process_rollback.php b/classes/event/process_rollback.php index 9a76c675..bffc8271 100644 --- a/classes/event/process_rollback.php +++ b/classes/event/process_rollback.php @@ -24,6 +24,7 @@ namespace tool_lifecycle\event; +use context_course; use moodle_url; use tool_lifecycle\local\entity\process; @@ -45,7 +46,7 @@ class process_rollback extends \core\event\base { /** - * Creates an process_rollabck event from a process. + * Creates an process_rollback event from a process. * * @param process $process * @return process_rollback @@ -55,7 +56,7 @@ class process_rollback extends \core\event\base { public static function event_from_process($process) { $data = [ 'courseid' => $process->courseid, - 'context' => $process->context, + 'context' => $process->context ?? context_course::instance($process->courseid), 'other' => [ 'processid' => $process->id, 'workflowid' => $process->workflowid, @@ -142,4 +143,5 @@ public static function get_other_mapping() { // No backup and restore. return false; } + } diff --git a/classes/local/backup/restore_lifecycle_workflow.php b/classes/local/backup/restore_lifecycle_workflow.php index cd05dba4..1a216e1e 100644 --- a/classes/local/backup/restore_lifecycle_workflow.php +++ b/classes/local/backup/restore_lifecycle_workflow.php @@ -203,7 +203,7 @@ private function check_subplugin_validity() { } foreach ($this->trigger as $trigger) { - $steplib = lib_manager::get_trigger_lib($trigger->subpluginname); + $triggerlib = lib_manager::get_trigger_lib($trigger->subpluginname); $filteredsettings = []; foreach ($this->settings as $setting) { if ($setting->pluginid === $trigger->id) { @@ -212,7 +212,7 @@ private function check_subplugin_validity() { } $errors = array_map( fn($x) => get_string('restore_error_in_trigger', 'tool_lifecycle', $trigger->instancename) . $x, - $steplib->ensure_validity($filteredsettings) + $triggerlib->ensure_validity($filteredsettings) ); $this->errors = array_merge($this->errors, $errors); } diff --git a/classes/local/entity/process.php b/classes/local/entity/process.php index c72cf1f0..839a2615 100644 --- a/classes/local/entity/process.php +++ b/classes/local/entity/process.php @@ -80,16 +80,17 @@ private function __construct($id, $workflowid, $courseid, $context, $stepindex, /** * Creates a Life Cycle Process from a db record. * @param object $record Data object. + * @param bool $coursedeleted If course is deleted no context can be fetched * @return process */ - public static function from_record($record) { + public static function from_record($record, $coursedeleted = false) { if (!object_property_exists($record, 'id')) { return null; } if (!object_property_exists($record, 'workflowid')) { return null; } - if (!object_property_exists($record, 'courseid')) { + if (!object_property_exists($record, 'courseid') || !is_numeric($record->courseid)) { return null; } if (object_property_exists($record, 'waiting') && $record->waiting) { @@ -104,7 +105,11 @@ public static function from_record($record) { $stepindex = 0; } - $context = \context_course::instance($record->courseid); + if (!$coursedeleted) { + $context = \context_course::instance($record->courseid); + } else { + $context = ""; + } $instance = new self($record->id, $record->workflowid, $record->courseid, diff --git a/classes/local/entity/step_subplugin.php b/classes/local/entity/step_subplugin.php index 711a37e1..f418b8f2 100644 --- a/classes/local/entity/step_subplugin.php +++ b/classes/local/entity/step_subplugin.php @@ -32,6 +32,9 @@ */ class step_subplugin extends subplugin { + /** @var int $rollbacktosortindex the stepindex of the step to which the current step has to be rolled back */ + public $rollbacktosortindex; + /** * Creates a subplugin from a db record. * @param object $record Data object. @@ -55,6 +58,9 @@ public static function from_record($record) { if (object_property_exists($record, 'sortindex') ) { $instance->sortindex = $record->sortindex; } + if (object_property_exists($record, 'rollbacktosortindex') ) { + $instance->rollbacktosortindex = $record->rollbacktosortindex; + } return $instance; } diff --git a/classes/local/entity/workflow.php b/classes/local/entity/workflow.php index 777decee..700b77c4 100644 --- a/classes/local/entity/workflow.php +++ b/classes/local/entity/workflow.php @@ -47,8 +47,8 @@ class workflow { /** @var int $sortindex Sort index of all active workflows. */ public $sortindex; - /** @var bool|null $manual True if workflow is manually triggered. */ - public $manual; + /** @var bool|null $manually True if workflow is manually triggered. */ + public $manually; /** @var string $displaytitle Title that is displayed to users. */ public $displaytitle; @@ -68,6 +68,9 @@ class workflow { /** @var int $includesitecourse Is course 1 supposed to be processed in this workflow. */ public $includesitecourse; + /** @var int $andor conjunction or disjunction when combining triggers. */ + public $andor; + /** * Workflow constructor. * @param int $id Id of the workflow. @@ -82,21 +85,24 @@ class workflow { * @param bool $delayforallworkflows True if a delay counts for all workflows. * @param int $includedelayedcourses Are delayed courses supposed to be processed in this workflow. * @param int $includesitecourse Is course 1 supposed to be processed in this workflow. + * @param int $andor conjunction or disjunction when combining triggers. */ private function __construct($id, $title, $timeactive, $timedeactive, $sortindex, $manual, $displaytitle, - $rollbackdelay, $finishdelay, $delayforallworkflows, $includedelayedcourses, $includesitecourse) { + $rollbackdelay, $finishdelay, $delayforallworkflows, $includedelayedcourses, + $includesitecourse, $andor) { $this->id = $id; $this->title = $title; $this->timeactive = $timeactive; $this->timedeactive = $timedeactive; $this->sortindex = $sortindex; - $this->manual = $manual; + $this->manually = $manual; $this->displaytitle = $displaytitle; $this->rollbackdelay = $rollbackdelay; $this->finishdelay = $finishdelay; $this->delayforallworkflows = $delayforallworkflows; $this->includedelayedcourses = $includedelayedcourses; $this->includesitecourse = $includesitecourse; + $this->andor = $andor; } /** @@ -129,10 +135,10 @@ public static function from_record($record) { } $manual = null; - if (object_property_exists($record, 'manual')) { - if ($record->manual == 1) { + if (object_property_exists($record, 'manually')) { + if ($record->manually == 1) { $manual = true; - } else if ($record->manual == 0) { + } else if ($record->manually == 0) { $manual = false; } } @@ -168,8 +174,13 @@ public static function from_record($record) { $includesitecourse = $record->includesitecourse; } + $andor = false; + if (object_property_exists($record, 'andor')) { + $andor = $record->andor; + } + $instance = new self($id, $record->title, $timeactive, $timedeactive, $sortindex, $manual, $displaytitle, - $rollbackdelay, $finishdelay, $delayforallworkflows, $includedelayedcourses, $includesitecourse); + $rollbackdelay, $finishdelay, $delayforallworkflows, $includedelayedcourses, $includesitecourse, $andor); return $instance; } diff --git a/classes/local/form/form_errors_filter.php b/classes/local/form/form_errors_filter.php new file mode 100644 index 00000000..5108fcf6 --- /dev/null +++ b/classes/local/form/form_errors_filter.php @@ -0,0 +1,112 @@ +. + +/** + * A moodle form for filtering the process errors table + * + * @package tool_lifecycle + * @copyright 2025 Thomas Niedermaier University Münster + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lifecycle\local\form; + +use tool_lifecycle\local\manager\workflow_manager; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + +/** + * A moodle form for filtering the process errors table + * + * @package tool_lifecycle + * @copyright 2025 Thomas Niedermaier University Münster + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class form_errors_filter extends \moodleform { + + /** + * Defines forms elements + */ + public function definition() { + global $DB; + + $mform = $this->_form; + + $workflow = $this->_customdata['workflow']; + $addbuttons = false; + + // Get distinct workflows with process errors and populate workflow filter with them. + $sql = "select DISTINCT(wf.id), wf.title from {tool_lifecycle_workflow} wf where wf.id in + (select w.id from {tool_lifecycle_proc_error} pe + JOIN {tool_lifecycle_workflow} w ON pe.workflowid = w.id + JOIN {tool_lifecycle_step} s ON pe.workflowid = s.workflowid AND pe.stepindex = s.sortindex + LEFT JOIN {course} c ON pe.courseid = c.id)"; + if ($workflows = $DB->get_records_sql($sql)) { + $workflowoptions[''] = get_string('choose').'...'; + foreach ($workflows as $wf) { + $workflowoptions[$wf->id] = $wf->title; + } + $mform->addElement('select', 'workflow', get_string('workflow', 'tool_lifecycle'), $workflowoptions); + $addbuttons = true; + } + + // Get distinct workflow steps with process errors and populate step filter with them. + // If workflow filter is active use it here as well. + $sql = "SELECT DISTINCT s.id, s.instancename from {tool_lifecycle_proc_error} pe + JOIN {tool_lifecycle_workflow} w ON pe.workflowid = w.id + JOIN {tool_lifecycle_step} s ON pe.workflowid = s.workflowid AND pe.stepindex = s.sortindex + JOIN {course} c ON pe.courseid = c.id"; + $params = []; + if ($workflow) { + $sql .= " and pe.workflowid = :workflow"; + $params["workflow"] = $workflow; + } + if ($steps = $DB->get_records_sql($sql, $params)) { + $stepsoptions[''] = get_string('choose').'...'; + foreach ($steps as $step) { + $stepsoptions[$step->id] = $step->instancename; + } + $mform->addElement('select', 'step', get_string('step', 'tool_lifecycle'), $stepsoptions); + $addbuttons = true; + } + + // Get distinct courses with process errors and populate courses filter with them. + // If workflow filter is active use it here as well. + $sql = "SELECT DISTINCT c.id, c.fullname from {tool_lifecycle_proc_error} pe + JOIN {tool_lifecycle_workflow} w ON pe.workflowid = w.id + JOIN {tool_lifecycle_step} s ON pe.workflowid = s.workflowid AND pe.stepindex = s.sortindex + JOIN {course} c ON pe.courseid = c.id"; + $params = []; + if ($workflow) { + $sql .= " and pe.workflowid = :workflow"; + $params["workflow"] = $workflow; + } + if ($courses = $DB->get_records_sql($sql, $params)) { + $coursesoptions[''] = get_string('choose').'...'; + foreach ($courses as $course) { + $coursesoptions[$course->id] = $course->fullname; + } + $mform->addElement('select', 'course', get_string('course'), $coursesoptions); + $addbuttons = true; + } + + if ($addbuttons) { + $this->add_action_buttons(true, get_string('apply', 'tool_lifecycle')); + } + } + +} diff --git a/classes/local/form/form_step_instance.php b/classes/local/form/form_step_instance.php index 564b6526..85d36689 100644 --- a/classes/local/form/form_step_instance.php +++ b/classes/local/form/form_step_instance.php @@ -26,6 +26,7 @@ use tool_lifecycle\action; use tool_lifecycle\local\entity\step_subplugin; use tool_lifecycle\local\manager\lib_manager; +use tool_lifecycle\local\manager\step_manager; use tool_lifecycle\local\manager\workflow_manager; use tool_lifecycle\step\libbase; @@ -111,20 +112,47 @@ public function definition() { $mform->addElement('header', 'step_settings_header', get_string('step_settings_header', 'tool_lifecycle')); $elementname = 'instancename'; - $mform->addElement('text', $elementname, get_string('step_instancename', 'tool_lifecycle')); - $mform->addHelpButton($elementname, 'step_instancename', 'tool_lifecycle'); - $mform->setType($elementname, PARAM_TEXT); - $mform->addRule($elementname, get_string('maximumchars', '', 100), 'maxlength', 100, 'client'); - $mform->addRule($elementname, null, 'required'); + if ($this->workflowid && !workflow_manager::is_editable($this->workflowid)) { + $mform->addElement('static', $elementname, get_string('step_instancename', 'tool_lifecycle')); + $mform->setType($elementname, PARAM_TEXT); + } else { + $mform->addElement('text', $elementname, get_string('step_instancename', 'tool_lifecycle'), ['size' => 60]); + $mform->addHelpButton($elementname, 'step_instancename', 'tool_lifecycle'); + $mform->setType($elementname, PARAM_TEXT); + $mform->addRule($elementname, get_string('maximumchars', '', 100), 'maxlength', 100, 'client'); + $mform->addRule($elementname, null, 'required'); + } $elementname = 'subpluginnamestatic'; $mform->addElement('static', $elementname, get_string('step_subpluginname', 'tool_lifecycle')); $mform->addHelpButton($elementname, 'step_subpluginname', 'tool_lifecycle'); $mform->setType($elementname, PARAM_TEXT); + $elementname = 'subpluginname'; $mform->addElement('hidden', $elementname); $mform->setType($elementname, PARAM_TEXT); + // If editing a step and if it's not the first workflow step provide the option to define target rollback index. + $options = []; + if ($this->step) { + if ($steps = step_manager::get_step_instances($this->workflowid)) { + foreach ($steps as $step) { + if ($step->id != $this->step && $step->sortindex < $this->step->sortindex) { + $options[$step->id] = get_string('step', 'tool_lifecycle'). + " #".$step->sortindex." ('".$step->instancename."')"; + } + } + } + } + if ($options) { + array_unshift($options, get_string('none')); + $elementname = 'rollbacktosortindex'; + $mform->addElement('select', $elementname, get_string($elementname, 'tool_lifecycle'), $options); + $mform->addHelpButton($elementname, $elementname, 'tool_lifecycle'); + $mform->setDefault($elementname, $this->step->rollbacktosortindex); + $mform->setType($elementname, PARAM_INT); + } + // Insert the subplugin specific settings. if (!empty($this->lib->instance_settings())) { $mform->addElement('header', 'steptype_settings_header', get_string('steptype_settings_header', 'tool_lifecycle')); @@ -163,8 +191,11 @@ public function definition_after_data() { $mform->setDefault('id', ''); $subpluginname = $this->subpluginname; } - $mform->setDefault('subpluginnamestatic', - get_string('pluginname', 'lifecyclestep_' . $subpluginname)); + + if (isset($this->lib)) { + $mform->setDefault('subpluginnamestatic', $this->lib->get_plugin_description()); + } + $mform->setDefault('subpluginname', $subpluginname); // Setting the default values for the local step settings. diff --git a/classes/local/form/form_trigger_instance.php b/classes/local/form/form_trigger_instance.php index 64aa2bc0..d7c9834f 100644 --- a/classes/local/form/form_trigger_instance.php +++ b/classes/local/form/form_trigger_instance.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Offers the possibility to add or modify a step instance. + * Offers the possibility to add or modify a trigger instance. * * @package tool_lifecycle * @copyright 2017 Tobias Reischmann WWU @@ -35,7 +35,7 @@ require_once($CFG->libdir . '/formslib.php'); /** - * Provides a form to modify a step instance + * Provides a form to modify a trigger instance * @package tool_lifecycle * @copyright 2017 Tobias Reischmann WWU * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -118,11 +118,17 @@ public function definition() { $mform->addElement('header', 'trigger_settings_header', get_string('trigger_settings_header', 'tool_lifecycle')); $elementname = 'instancename'; - $mform->addElement('text', $elementname, get_string('trigger_instancename', 'tool_lifecycle')); - $mform->addHelpButton($elementname, 'trigger_instancename', 'tool_lifecycle'); - $mform->setType($elementname, PARAM_TEXT); - $mform->addRule($elementname, get_string('maximumchars', '', 100), 'maxlength', 100, 'client'); - $mform->addRule($elementname, null, 'required'); + if ($this->workflowid && !workflow_manager::is_editable($this->workflowid)) { + $mform->addElement('static', $elementname, + get_string('trigger_instancename', 'tool_lifecycle')); + $mform->setType($elementname, PARAM_TEXT); + } else { + $mform->addElement('text', $elementname, get_string('trigger_instancename', 'tool_lifecycle')); + $mform->addHelpButton($elementname, 'trigger_instancename', 'tool_lifecycle'); + $mform->setType($elementname, PARAM_TEXT); + $mform->addRule($elementname, get_string('maximumchars', '', 100), 'maxlength', 100, 'client'); + $mform->addRule($elementname, null, 'required'); + } $elementname = 'subpluginnamestatic'; $mform->addElement('static', $elementname, @@ -167,8 +173,11 @@ public function definition_after_data() { $mform->setDefault('id', $this->trigger->id); $mform->setDefault('instancename', $this->trigger->instancename); } - $mform->setDefault('subpluginnamestatic', - get_string('pluginname', 'lifecycletrigger_' . $this->subpluginname)); + + if (isset($this->lib)) { + $mform->setDefault('subpluginnamestatic', $this->lib->get_plugin_description()); + } + $mform->setDefault('subpluginname', $this->subpluginname); // Setting the default values for the local trigger settings. diff --git a/classes/local/form/form_workflow_instance.php b/classes/local/form/form_workflow_instance.php index a4c0db1d..d49b764a 100644 --- a/classes/local/form/form_workflow_instance.php +++ b/classes/local/form/form_workflow_instance.php @@ -130,6 +130,20 @@ public function definition() { $mform->setDefault($elementname, $this->workflow->includesitecourse); } + $elementname = 'andor'; + $groupelements = [ + $mform->createElement('radio', $elementname, '', 'AND', '0'), + $mform->createElement('radio', $elementname, '', 'OR', '1'), + ]; + $mform->addElement('group', 'andorgroup', get_string('andor', 'tool_lifecycle'), $groupelements, null, true); + $mform->addHelpButton('andorgroup', 'andor', 'tool_lifecycle'); + $mform->setType($elementname, PARAM_INT); + if (isset($this->workflow)) { + $mform->setDefault("andorgroup[$elementname]", $this->workflow->andor); + } else { + $mform->setDefault("andorgroup[$elementname]", '0'); + } + $this->add_action_buttons(); } diff --git a/classes/local/manager/delayed_courses_manager.php b/classes/local/manager/delayed_courses_manager.php index b298aad7..74713c5d 100644 --- a/classes/local/manager/delayed_courses_manager.php +++ b/classes/local/manager/delayed_courses_manager.php @@ -53,6 +53,11 @@ public static function set_course_delayed_for_workflow($courseid, $becauserollba $workflow = $workfloworid; } else { $workflow = workflow_manager::get_workflow($workfloworid); + if (!$workflow) { + mtrace("Set course delayed: no workflow found. Course-ID:{$courseid}, + var_dump workfloworid:". var_dump($workfloworid)); + return; + } } if ($becauserollback) { $duration = $workflow->rollbackdelay; diff --git a/classes/local/manager/interaction_manager.php b/classes/local/manager/interaction_manager.php index f263fb8a..edb1e635 100644 --- a/classes/local/manager/interaction_manager.php +++ b/classes/local/manager/interaction_manager.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\manager; +use tool_lifecycle\event\process_rollback; use tool_lifecycle\local\entity\process; use tool_lifecycle\processor; use tool_lifecycle\local\response\step_interactive_response; @@ -135,12 +136,12 @@ public static function show_relevant_courses_instance_dependent($subpluginname) } /** - * Returns an array of interaction tools to be displayed to be displayed on the view.php - * Every entry is itself an array which consist of three elements: + * Returns an array of interaction tools to be displayed on view.php + * Every entry is an array that consists of three elements: * 'action' => an action string, which is later passed to handle_action - * 'alt' => a string text of the button + * 'alt' => text of the button * @param string $subpluginname name of the step - * @param int $processid if of the process the action tools are requested for + * @param int $processid id of the process the action tools are requested for * @return array of action tools * @throws \invalid_parameter_exception * @throws \coding_exception diff --git a/classes/local/manager/lib_manager.php b/classes/local/manager/lib_manager.php index afd381ee..22861672 100644 --- a/classes/local/manager/lib_manager.php +++ b/classes/local/manager/lib_manager.php @@ -102,18 +102,27 @@ public static function get_step_interactionlib($subpluginname) { * @return null|base|libbase */ private static function get_lib($subpluginname, $subplugintype, $libsubtype = '') { + // Plugins defined in subplugins.json file. $triggerlist = \core_component::get_plugin_list('lifecycle' . $subplugintype); - if (!array_key_exists($subpluginname, $triggerlist)) { - return null; - } - $filename = $triggerlist[$subpluginname].'/'.$libsubtype.'lib.php'; - if (file_exists($filename)) { - require_once($filename); - $extendedclass = "tool_lifecycle\\$subplugintype\\$libsubtype$subpluginname"; - if (class_exists($extendedclass)) { - return new $extendedclass(); + if (array_key_exists($subpluginname, $triggerlist)) { + $filename = $triggerlist[$subpluginname].'/'.$libsubtype.'lib.php'; + if (file_exists($filename)) { + require_once($filename); + $extendedclass = "tool_lifecycle\\$subplugintype\\$libsubtype$subpluginname"; + if (class_exists($extendedclass)) { + return new $extendedclass(); + } } } + + // Plugins defined under "lifecycle" name space. + // The base class has already been checked by get_trigger_types or get_steps_types. + $classname = !$libsubtype ? $subplugintype : $libsubtype; + $classname = "$subpluginname\\lifecycle\\$classname"; + if (class_exists($classname)) { + return new $classname(); + } + return null; } } diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index 49b4c1a1..8d61e60a 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -65,7 +65,7 @@ public static function create_process($courseid, $workflowid) { /** * Creates a process based on a manual trigger. - * @param int $courseid Id of the course to be triggerd. + * @param int $courseid Id of the course to be triggered. * @param int $triggerid Id of the triggering trigger. * @return process the triggered process instance. * @throws \moodle_exception for invalid workflow definition or missing trigger. @@ -77,7 +77,7 @@ public static function manually_trigger_process($courseid, $triggerid) { } $workflow = workflow_manager::get_workflow($trigger->workflowid); if (!$workflow || !workflow_manager::is_active($workflow->id) || !workflow_manager::is_valid($workflow->id) || - $workflow->manual !== true) { + $workflow->manually !== true) { throw new \moodle_exception('cannot_trigger_workflow_manually', 'tool_lifecycle'); } return self::create_process($courseid, $workflow->id); @@ -90,6 +90,12 @@ public static function manually_trigger_process($courseid, $triggerid) { */ public static function get_processes() { global $DB; + // Delete processes of already removed courses. + $DB->delete_records_select( + 'tool_lifecycle_process', + "courseid not in (SELECT id FROM {course}) ", + [] + ); $records = $DB->get_records('tool_lifecycle_process'); $processes = []; foreach ($records as $record) { @@ -125,6 +131,17 @@ public static function count_processes_by_workflow($workflowid) { return $DB->count_records('tool_lifecycle_process', ['workflowid' => $workflowid]); } + /** + * Counts all process errors for the given workflow id. + * @param int $workflowid id of the workflow + * @return int number of process errors. + * @throws \dml_exception + */ + public static function count_process_errors_by_workflow($workflowid) { + global $DB; + return $DB->count_records('tool_lifecycle_proc_error', ['workflowid' => $workflowid]); + } + /** * Returns all processes for given workflow id * @param int $workflowid id of the workflow @@ -141,6 +158,22 @@ public static function get_processes_by_workflow($workflowid) { return $processes; } + /** + * Returns all processes of a deleted course + * @param int $courseid id of the course + * @return array of proccesses + * @throws \dml_exception + */ + public static function get_processes_of_deleted_course($courseid) { + global $DB; + $records = $DB->get_records('tool_lifecycle_process', ['courseid' => $courseid]); + $processes = []; + foreach ($records as $record) { + $processes[] = process::from_record($record, true); + } + return $processes; + } + /** * Proceeds the process to the next step. * @param process $process @@ -183,9 +216,14 @@ public static function set_process_waiting(&$process) { * @throws \dml_exception */ public static function rollback_process($process) { + global $DB; + process_rollback::event_from_process($process)->trigger(); - unset($process->context); - for ($i = $process->stepindex; $i >= 1; $i--) { + if (!$tosortindex = $DB->get_field('tool_lifecycle_step', + 'rollbacktosortindex', ['workflowid' => $process->workflowid, 'sortindex' => $process->stepindex])) { + $tosortindex = 0; + } + for ($i = $process->stepindex; $i > $tosortindex; $i--) { $step = step_manager::get_step_instance_by_workflow_index($process->workflowid, $i); $lib = lib_manager::get_step_lib($step->subpluginname); try { @@ -196,7 +234,9 @@ public static function rollback_process($process) { } $lib->rollback_course($process->id, $step->id, $course); } - self::remove_process($process); + if ($tosortindex == 0) { + self::remove_process($process); + } } /** @@ -207,7 +247,7 @@ public static function rollback_process($process) { private static function remove_process($process) { global $DB; $DB->delete_records('tool_lifecycle_procdata', ['processid' => $process->id]); - $DB->delete_records('tool_lifecycle_process', (array) $process); + $DB->delete_records('tool_lifecycle_process', ['id' => $process->id]); } /** @@ -218,8 +258,7 @@ private static function remove_process($process) { */ public static function get_process_by_course_id($courseid) { global $DB; - $record = $DB->get_record('tool_lifecycle_process', ['courseid' => $courseid]); - if ($record) { + if ($record = $DB->get_record('tool_lifecycle_process', ['courseid' => $courseid])) { return process::from_record($record); } else { return null; @@ -250,9 +289,11 @@ public static function has_other_process($courseid, $workflowid) { * @throws \dml_exception */ public static function course_deletion_observed($event) { - $process = self::get_process_by_course_id($event->get_data()['courseid']); - if ($process) { - self::abort_process($process); + if (is_numeric($courseid = $event->get_data()['courseid'])) { + $processes = self::get_processes_of_deleted_course($courseid); + foreach ($processes as $process) { + self::abort_process($process); + } } } @@ -269,17 +310,17 @@ public static function abort_process($process) { } /** - * Moves a process into the procerror table. + * Moves the process into the procerror table. * * @param process $process The process * @param Exception $e The exception * @return void + * @throws \dml_exception */ public static function insert_process_error(process $process, Exception $e) { global $DB; $procerror = new stdClass(); - $procerror->id = $process->id; $procerror->courseid = $process->courseid; $procerror->workflowid = $process->workflowid; $procerror->stepindex = $process->stepindex; @@ -294,44 +335,50 @@ public static function insert_process_error(process $process, Exception $e) { $procerror->errorhash = md5($m); $procerror->waiting = intval($process->waiting); - $DB->insert_record_raw('tool_lifecycle_proc_error', $procerror, false, false, true); + $DB->insert_record('tool_lifecycle_proc_error', $procerror, false, false); $DB->delete_records('tool_lifecycle_process', ['id' => $process->id]); } /** - * Proceed process from procerror back into the process board. + * Return process from process-error-table back into the process board. * @param int $processid the processid * @return void + * @throws \dml_exception */ public static function proceed_process_after_error(int $processid) { global $DB; + $process = $DB->get_record('tool_lifecycle_proc_error', ['id' => $processid]); - // Unset process error entries. + // Unset process-error-only entries and id. + unset($process->id); unset($process->errormessage); unset($process->errortrace); unset($process->errorhash); unset($process->errortimecreated); - $DB->insert_record_raw('tool_lifecycle_process', $process, false, false, true); - $DB->delete_records('tool_lifecycle_proc_error', ['id' => $process->id]); + $DB->insert_record('tool_lifecycle_process', $process, false); + $DB->delete_records('tool_lifecycle_proc_error', ['id' => $processid]); } /** - * Rolls back a process from procerror table + * Roll back a process from procoss-error-table to process-table while setting the rollback delay * @param int $processid the processid * @return void + * @throws \coding_exception + * @throws \dml_exception */ public static function rollback_process_after_error(int $processid) { global $DB; $process = $DB->get_record('tool_lifecycle_proc_error', ['id' => $processid]); - // Unset process error entries. + // Unset process-error-only entries. + unset($process->id); unset($process->errormessage); unset($process->errortrace); unset($process->errorhash); unset($process->errortimecreated); - $DB->insert_record_raw('tool_lifecycle_process', $process, false, false, true); + $DB->insert_record('tool_lifecycle_process', $process, false); $DB->delete_records('tool_lifecycle_proc_error', ['id' => $process->id]); delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, $process->workflowid); diff --git a/classes/local/manager/step_manager.php b/classes/local/manager/step_manager.php index 3158cd40..fa26279c 100644 --- a/classes/local/manager/step_manager.php +++ b/classes/local/manager/step_manager.php @@ -24,6 +24,7 @@ namespace tool_lifecycle\local\manager; use tool_lifecycle\action; +use tool_lifecycle\event\process_rollback; use tool_lifecycle\local\entity\step_subplugin; use tool_lifecycle\settings_type; @@ -156,7 +157,9 @@ private static function remove_from_sortindex(&$toberemoved) { */ public static function change_sortindex($stepid, $up) { global $DB; + $step = self::get_step_instance($stepid); + // Prevent first entry to be put up even more. if ($step->sortindex == 1 && $up) { return; @@ -165,6 +168,14 @@ public static function change_sortindex($stepid, $up) { if ($step->sortindex == self::count_steps_of_workflow($step->workflowid) && !$up) { return; } + // Prevent reducing step sortindex under the step's target rollback sortindex, if any. + if ($step->rollbacktosortindex) { + if (!$up && $step->sortindex == $step->rollbacktosortindex + 1) { + return; + } + + } + $index = $step->sortindex; if ($up) { $otherindex = $index - 1; @@ -228,11 +239,27 @@ public static function get_step_instances_by_subpluginname($subpluginname) { * @throws \coding_exception */ public static function get_step_types() { + // Sub plugins in 'step' folder. $subplugins = \core_component::get_plugin_list('lifecyclestep'); $result = []; foreach (array_keys($subplugins) as $plugin) { $result[$plugin] = get_string('pluginname', 'lifecyclestep_' . $plugin); } + + // Additional sub plugins defined under "lifecycle" name space, ie "local_newstep\lifecycle". + // The class name must be step (step.php) and placed under "classes/lifecycle" folder. + // The name space must be "local_newstep\lifecycle" + // The "local_newstep\lifecycle\step" class must extend the step base classes. + foreach (array_keys(\core_component::get_plugin_types()) as $plugintype) { + $potentialsteps = \core_component::get_plugin_list_with_class($plugintype, 'lifecycle\\step'); + foreach ($potentialsteps as $plugin => $potentialstep) { + // Check if it implements the step base class. + if (is_a($potentialstep, \tool_lifecycle\step\libbase::class, true)) { + $result[$plugin] = get_string('pluginname', $plugin); + } + } + } + return $result; } @@ -323,7 +350,6 @@ public static function duplicate_steps($oldworkflowid, $newworkflowid) { $steps = self::get_step_instances($oldworkflowid); foreach ($steps as $step) { $settings = settings_manager::get_settings($step->id, settings_type::STEP); - $step->id = null; $step->workflowid = $newworkflowid; self::insert_or_update($step); diff --git a/classes/local/manager/trigger_manager.php b/classes/local/manager/trigger_manager.php index 030c1e98..b24ad41e 100644 --- a/classes/local/manager/trigger_manager.php +++ b/classes/local/manager/trigger_manager.php @@ -238,8 +238,33 @@ public static function get_trigger_types() { $subplugins = \core_component::get_plugin_list('lifecycletrigger'); $result = []; foreach (array_keys($subplugins) as $plugin) { - $result[$plugin] = get_string('pluginname', 'lifecycletrigger_' . $plugin); + if ($plugin == 'customfieldsemester') { // List only if plugin customfield_semester is installed. + $customfields = \core_component::get_plugin_list('customfield'); + foreach (array_keys($customfields) as $field) { + if ($field == 'semester') { + $result[$plugin] = get_string('pluginname', 'lifecycletrigger_' . $plugin); + break; + } + } + } else { + $result[$plugin] = get_string('pluginname', 'lifecycletrigger_' . $plugin); + } } + + // Additional sub plugins defined under "lifecycle" name space, ie "local_newtrigger\lifecycle". + // The class name must be trigger (trigger.php) and placed under "classes/lifecycle" folder. + // The name space must be "local_newtrigger\lifecycle" + // The "local_newtrigger\lifecycle\trigger" class must extend the trigger base classes (base_automatic or base_manual). + foreach (array_keys(\core_component::get_plugin_types()) as $plugintype) { + $potentialtriggers = \core_component::get_plugin_list_with_class($plugintype, 'lifecycle\\trigger'); + foreach ($potentialtriggers as $plugin => $potentialtrigger) { + // Check if it implements the trigger base class. + if (is_a($potentialtrigger, \tool_lifecycle\trigger\base::class, true)) { + $result[$plugin] = get_string('pluginname', $plugin); + } + } + } + return $result; } @@ -262,7 +287,7 @@ public static function get_chooseable_trigger_types() { /** - * Handles an action for a workflow step. + * Handles an action for a workflow trigger. * @param string $action action to be executed * @param int $subpluginid id of the trigger instance * @param int $workflowid id of the workflow @@ -331,27 +356,43 @@ public static function duplicate_triggers($oldworkflowid, $newworkflowid) { $triggers = self::get_triggers_for_workflow($oldworkflowid); foreach ($triggers as $trigger) { $settings = settings_manager::get_settings($trigger->id, settings_type::TRIGGER); - $trigger->id = null; $trigger->workflowid = $newworkflowid; self::insert_or_update($trigger); - settings_manager::save_settings($trigger->id, settings_type::TRIGGER, $trigger->subpluginname, $settings); } } /** - * Returns true if the trigger fires, i.e. returns a list of one or more courses to process upon. + * Returns the sql where clause of the trigger. * @param trigger_subplugin $trigger - * @return mixed $sql returns false if the trigger does not fire + * @return string where clause of this trigger * @throws \coding_exception - * @throws \dml_exception */ public static function get_trigger_sqlresult($trigger) { - $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); - [$sql, $params] = $lib->get_course_recordset_where($trigger->id); + [$sql, ] = $lib->get_course_recordset_where($trigger->id); return $sql; } + + /** + * Returns whether this triggertype is allowed to create more than one instance for a workflow. + * @param string $subpluginname of the trigger + * @return bool + * @throws \coding_exception + */ + public static function trigger_multipleuse($subpluginname) { + $lib = lib_manager::get_trigger_lib($subpluginname); + if (!$lib->is_manual_trigger()) { + $lib = lib_manager::get_automatic_trigger_lib($subpluginname); + if ($lib->multiple_use()) { + return true; + } else { + return false; + } + } else { + return false; + } + } } diff --git a/classes/local/manager/workflow_manager.php b/classes/local/manager/workflow_manager.php index 26c444bf..e1867f51 100644 --- a/classes/local/manager/workflow_manager.php +++ b/classes/local/manager/workflow_manager.php @@ -73,6 +73,7 @@ public static function remove($workflowid, $hard = false) { trigger_manager::remove_instances_of_workflow($workflowid); step_manager::remove_instances_of_workflow($workflowid); $DB->delete_records('tool_lifecycle_workflow', ['id' => $workflowid]); + $DB->delete_records('tool_lifecycle_proc_error', ['workflowid' => $workflowid]); } } @@ -132,7 +133,7 @@ public static function abortprocesses($workflowid) { } /** - * Returns a workflow instance if one with the is is available. + * Returns a workflow instance if one with the id is available. * * @param int $workflowid id of the workflow * @return workflow|null @@ -175,7 +176,7 @@ public static function get_active_workflows() { global $DB; $records = $DB->get_records_sql( 'SELECT * FROM {tool_lifecycle_workflow} - WHERE timeactive IS NOT NULL ORDER BY sortindex'); + WHERE timeactive IS NOT NULL ORDER BY sortindex'); $result = []; foreach ($records as $record) { $result[] = workflow::from_record($record); @@ -194,7 +195,7 @@ public static function get_active_automatic_workflows() { $records = $DB->get_records_sql( 'SELECT * FROM {tool_lifecycle_workflow} WHERE timeactive IS NOT NULL AND - (manual IS NULL OR manual = 0) ORDER BY sortindex', []); + (manually IS NULL OR manually = 0) ORDER BY sortindex', []); $result = []; foreach ($records as $record) { $result[] = workflow::from_record($record); @@ -211,7 +212,7 @@ public static function get_active_automatic_workflows() { public static function get_active_manual_workflow_triggers() { global $DB; $sql = 'SELECT t.* FROM {tool_lifecycle_workflow} w JOIN {tool_lifecycle_trigger} t ON t.workflowid = w.id' . - ' WHERE w.timeactive IS NOT NULL AND w.manual = ?'; + ' WHERE w.timeactive IS NOT NULL AND w.manually = ?'; $records = $DB->get_records_sql($sql, [true]); $result = []; foreach ($records as $record) { @@ -264,10 +265,11 @@ public static function activate_workflow($workflowid) { $triggers = trigger_manager::get_triggers_for_workflow($workflowid); foreach ($triggers as $trigger) { $lib = lib_manager::get_trigger_lib($trigger->subpluginname); - $workflow->manual |= $lib->is_manual_trigger(); + $workflow->manually |= $lib->is_manual_trigger(); } - $workflow->timeactive = time(); - if (!$workflow->manual) { + $workflow->timeactive = (new \DateTime())->getTimestamp(); + $workflow->timedeactive = null; + if (!$workflow->manually) { $workflow->sortindex = count(self::get_active_automatic_workflows()) + 1; } self::insert_or_update($workflow); @@ -491,6 +493,7 @@ public static function backup_workflow($workflowid) { * * @param int $workflowid Id of the workflow. * @return bool + * @throws \dml_exception */ public static function is_disableable($workflowid) { $trigger = trigger_manager::get_triggers_for_workflow($workflowid); diff --git a/classes/local/table/active_automatic_workflows_table.php b/classes/local/table/active_automatic_workflows_table.php index e35f0ef9..c20bf94b 100644 --- a/classes/local/table/active_automatic_workflows_table.php +++ b/classes/local/table/active_automatic_workflows_table.php @@ -51,7 +51,7 @@ class active_automatic_workflows_table extends active_workflows_table { public function __construct($uniqueid) { parent::__construct($uniqueid); global $PAGE; - $sqlwhere = 'timeactive IS NOT NULL AND (manual IS NULL OR manual = 0)'; + $sqlwhere = 'timeactive IS NOT NULL AND (manually IS NULL OR manually = 0)'; $this->set_sql("id, title, displaytitle, timeactive, sortindex", '{tool_lifecycle_workflow}', $sqlwhere, []); $this->define_baseurl($PAGE->url); diff --git a/classes/local/table/active_manual_workflows_table.php b/classes/local/table/active_manual_workflows_table.php index 32faf8a3..bde835e7 100644 --- a/classes/local/table/active_manual_workflows_table.php +++ b/classes/local/table/active_manual_workflows_table.php @@ -52,7 +52,7 @@ public function __construct($uniqueid) { parent::__construct($uniqueid); global $PAGE, $DB; list($sqlwheremanual, $paramsmanual) = $DB->get_in_or_equal(true); - $sqlwhere = 'timeactive IS NOT NULL AND manual ' . $sqlwheremanual; + $sqlwhere = 'timeactive IS NOT NULL AND manually ' . $sqlwheremanual; $params[1] = $paramsmanual[0]; $this->set_sql("id, title, displaytitle, timeactive", '{tool_lifecycle_workflow}', $sqlwhere, $params); diff --git a/classes/local/table/active_workflows_table.php b/classes/local/table/active_workflows_table.php index 62d6eae7..ca134292 100644 --- a/classes/local/table/active_workflows_table.php +++ b/classes/local/table/active_workflows_table.php @@ -62,7 +62,7 @@ public function col_tools($row) { if (workflow_manager::is_disableable($row->id)) { $action = action::WORKFLOW_BACKUP; - $alt = get_string('backupworkflow', 'tool_lifecycle'); + $alt = get_string('downloadworkflow', 'tool_lifecycle'); $icon = 't/backup'; $output .= $this->format_icon_link($action, $row->id, $icon, $alt); diff --git a/classes/local/table/course_backups_table.php b/classes/local/table/course_backups_table.php index 0739ab3c..285b3fbd 100644 --- a/classes/local/table/course_backups_table.php +++ b/classes/local/table/course_backups_table.php @@ -23,6 +23,8 @@ */ namespace tool_lifecycle\local\table; +use core_date; + defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir . '/tablelib.php'); @@ -135,7 +137,9 @@ public function col_coursefullname($row) { * @return string date of the backupcreated */ public function col_backupcreated($row) { - return userdate($row->backupcreated); + global $USER; + return userdate($row->backupcreated, '', + core_date::get_user_timezone($USER)); } /** diff --git a/classes/local/table/courses_in_step_table.php b/classes/local/table/courses_in_step_table.php index 89461ba4..fadda725 100644 --- a/classes/local/table/courses_in_step_table.php +++ b/classes/local/table/courses_in_step_table.php @@ -24,6 +24,7 @@ */ namespace tool_lifecycle\local\table; +use core_date; use tool_lifecycle\local\entity\step_subplugin; defined('MOODLE_INTERNAL') || die; @@ -157,9 +158,12 @@ public function col_coursefullname($row) { * @throws \coding_exception */ public function col_startdate($row) { + global $USER; + if ($row->startdate) { $dateformat = get_string('strftimedate', 'langconfig'); - return userdate($row->startdate, $dateformat); + return userdate($row->startdate, $dateformat, + core_date::get_user_timezone($USER)); } else { return "-"; } diff --git a/classes/local/table/delayed_courses_table.php b/classes/local/table/delayed_courses_table.php index f2237698..3ca4dd09 100644 --- a/classes/local/table/delayed_courses_table.php +++ b/classes/local/table/delayed_courses_table.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\table; +use core_date; use moodle_url; use tool_lifecycle\local\manager\delayed_courses_manager; use tool_lifecycle\urls; @@ -150,12 +151,13 @@ public function __construct($filterdata) { $this->set_sql($fields, $from, $where, $params); $this->column_nosort = ['workflow', 'tools']; - $this->define_columns(['coursefullname', 'category', 'workflow', 'tools']); + $this->define_columns(['courseid', 'coursefullname', 'category', 'workflow', 'tools']); $this->define_headers([ - get_string('coursename', 'tool_lifecycle'), - get_string('category'), - get_string('delays', 'tool_lifecycle'), - get_string('tools', 'tool_lifecycle'), + get_string('courseid', 'tool_lifecycle'), + get_string('coursename', 'tool_lifecycle'), + get_string('category'), + get_string('delays', 'tool_lifecycle'), + get_string('tools', 'tool_lifecycle'), ]); } @@ -168,7 +170,7 @@ public function __construct($filterdata) { * @throws \dml_exception */ public function col_workflow($row) { - global $OUTPUT; + global $OUTPUT, $USER; $url = new moodle_url(urls::WORKFLOW_DETAILS, ['wf' => $row->workflowid]); if ($row->globaldelay >= time()) { @@ -181,7 +183,7 @@ public function col_workflow($row) { $html = $text; } else { $dateformat = get_string('strftimedatetimeshort', 'core_langconfig'); - $date = userdate($row->globaldelay, $dateformat); + $date = userdate($row->globaldelay, $dateformat, core_date::get_user_timezone($USER)); $typehtml = delayed_courses_manager::delaytype_html($row->delaytype); $text = get_string('delayed_globally', 'tool_lifecycle', $date); $html = \html_writer::link($url, $text).$typehtml; @@ -191,7 +193,8 @@ public function col_workflow($row) { $html = ''; } else if ($row->workflowcount == 1) { $dateformat = get_string('strftimedatetimeshort', 'core_langconfig'); - $date = userdate($row->workflowdelay, $dateformat); + $date = userdate($row->workflowdelay, $dateformat, + core_date::get_user_timezone($USER)); $typehtml = delayed_courses_manager::delaytype_html($row->delaytype); $text = get_string('delayed_for_workflow_until', 'tool_lifecycle', ['name' => $row->workflow, 'date' => $date]); @@ -215,17 +218,19 @@ public function col_workflow($row) { * @throws \dml_exception */ private function get_worklows_multiple($row, $url) { - global $DB; + global $DB, $USER; $html = ''; $dateformat = get_string('strftimedatetimeshort', 'core_langconfig'); if ($row->globaldelay >= time()) { - $date = userdate($row->globaldelay, $dateformat); + $date = userdate($row->globaldelay, $dateformat, + core_date::get_user_timezone($USER)); $typehtml = delayed_courses_manager::delaytype_html($row->delaytype); $text = get_string('globally_until_date', 'tool_lifecycle', $date); $html .= \html_writer::link($url, $text).$typehtml."
"; } if ($row->workflowcount == 1) { - $date = userdate($row->workflowdelay, $dateformat); + $date = userdate($row->workflowdelay, $dateformat, + core_date::get_user_timezone($USER)); $typehtml = delayed_courses_manager::delaytype_html($row->delaytype); $text = get_string('name_until_date', 'tool_lifecycle', ['name' => $row->workflow, 'date' => $date]); @@ -238,7 +243,8 @@ private function get_worklows_multiple($row, $url) { AND w.timeactive IS NOT NULL'; $records = $DB->get_records_sql($sql, ['courseid' => $row->courseid]); foreach ($records as $record) { - $date = userdate($record->delayeduntil, $dateformat); + $date = userdate($record->delayeduntil, $dateformat, + core_date::get_user_timezone($USER)); $typehtml = delayed_courses_manager::delaytype_html($row->delaytype); $text = get_string('name_until_date', 'tool_lifecycle', ['name' => $record->title, 'date' => $date]); diff --git a/classes/local/table/interaction_remaining_table.php b/classes/local/table/interaction_remaining_table.php index d2ba5423..37453372 100644 --- a/classes/local/table/interaction_remaining_table.php +++ b/classes/local/table/interaction_remaining_table.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\table; +use core_date; use tool_lifecycle\local\manager\lib_manager; use tool_lifecycle\local\manager\workflow_manager; use tool_lifecycle\local\data\manual_trigger_tool; @@ -124,10 +125,10 @@ public function col_tools($row) { global $PAGE, $OUTPUT; if ($row->processid !== null) { - return ''; + return '--'; } if (empty($this->availabletools)) { - return get_string('noactiontools', 'tool_lifecycle'); + return "--"; } $actions = []; foreach ($this->availabletools as $tool) { @@ -188,12 +189,15 @@ public function col_status($row) { * @throws \coding_exception */ public function col_lastmodified($row) { + global $USER; + if (!$row->lastmodified) { return ''; } $dateformat = get_string('strftimedatetime', 'core_langconfig'); - return userdate($row->lastmodified, $dateformat); + + return userdate($row->lastmodified, $dateformat, core_date::get_user_timezone($USER)); } /** diff --git a/classes/local/table/interaction_table.php b/classes/local/table/interaction_table.php index a387deba..d601148f 100644 --- a/classes/local/table/interaction_table.php +++ b/classes/local/table/interaction_table.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\table; +use core_date; use tool_lifecycle\local\entity\step_subplugin; use tool_lifecycle\local\manager\interaction_manager; use tool_lifecycle\local\manager\process_manager; @@ -87,9 +88,11 @@ public function col_coursefullname($row) { * @throws \coding_exception */ public function col_startdate($row) { + global $USER; + if ($row->startdate) { $dateformat = get_string('strftimedate', 'langconfig'); - return userdate($row->startdate, $dateformat); + return userdate($row->startdate, $dateformat, core_date::get_user_timezone($USER)); } else { return "-"; } @@ -131,19 +134,20 @@ public function col_status($row) { public function col_category($row): String { $categorydepth = get_config('tool_lifecycle', 'enablecategoryhierachy'); if ($categorydepth == false) { - return $row->category; + $categoryname = $row->category; } else { $categorydepth = (int) get_config('tool_lifecycle', 'coursecategorydepth'); $categoryhierachy = explode('/', substr($row->categorypath, 1)); $categoryhierachy = array_map('intval', $categoryhierachy); if (isset($categoryhierachy[$categorydepth])) { $category = $this->coursecategories[$categoryhierachy[$categorydepth]]; - return $category->name; + $categoryname = $category->name; } else { $category = $this->coursecategories[end($categoryhierachy)]; - return $category->name; + $categoryname = $category->name; } } + return \html_writer::div($categoryname, "badge text-bg-secondary"); } /** diff --git a/classes/local/table/process_errors_table.php b/classes/local/table/process_errors_table.php index 5b99cbe8..3156b649 100644 --- a/classes/local/table/process_errors_table.php +++ b/classes/local/table/process_errors_table.php @@ -23,6 +23,9 @@ */ namespace tool_lifecycle\local\table; +use core\exception\coding_exception; +use core_date; + defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir . '/tablelib.php'); @@ -42,11 +45,12 @@ class process_errors_table extends \table_sql { private $strings; /** - * Constructor for delayed_courses_table. + * Constructor for process_errors_table. * + * @param object $filterdata the previously submitted filter data * @throws \coding_exception */ - public function __construct() { + public function __construct($filterdata) { global $OUTPUT; parent::__construct('tool_lifecycle-process_errors'); @@ -54,18 +58,38 @@ public function __construct() { $this->strings = [ 'proceed' => get_string('proceed', 'tool_lifecycle'), 'rollback' => get_string('rollback', 'tool_lifecycle'), + 'delete' => get_string('deleteprocesserror', 'tool_lifecycle'), ]; - $fields = 'c.id, c.fullname as course, w.title as workflow, s.instancename as step, pe.*'; + $fields = 'c.id, c.fullname as course, + w.id as workflowid, w.title as workflow, + s.id as stepid, s.instancename as step, + pe.id as errorid, pe.courseid, pe.errormessage, pe.errortrace, pe.errortimecreated'; $from = '{tool_lifecycle_proc_error} pe ' . - 'JOIN {tool_lifecycle_workflow} w ON pe.workflowid = w.id ' . - 'JOIN {tool_lifecycle_step} s ON pe.workflowid = s.workflowid AND pe.stepindex = s.sortindex ' . + 'LEFT JOIN {tool_lifecycle_workflow} w ON pe.workflowid = w.id ' . + 'LEFT JOIN {tool_lifecycle_step} s ON pe.workflowid = s.workflowid AND pe.stepindex = s.sortindex ' . 'LEFT JOIN {course} c ON pe.courseid = c.id '; - - $this->set_sql($fields, $from, 'TRUE'); + $where = 'TRUE'; + $params = []; + $workflow = $filterdata->workflow ?? null; + if ($workflow) { + $where .= ' AND w.id = :workflow'; + $params['workflow'] = $workflow; + } + $step = $filterdata->step ?? null; + if ($step) { + $where .= ' AND s.id = :step'; + $params['step'] = $step; + } + $course = $filterdata->course ?? null; + if ($course) { + $where .= ' AND c.id = :course'; + $params['course'] = $course; + } + $this->set_sql($fields, $from, $where, $params); $this->column_nosort = ['select', 'tools']; - $this->define_columns(['select', 'workflow', 'step', 'courseid', 'course', 'error', 'tools']); + $this->define_columns(['select', 'workflow', 'step', 'courseid', 'course', 'errortime', 'error', 'tools']); $this->define_headers([ $OUTPUT->render(new \core\output\checkbox_toggleall('procerrors-table', true, [ 'id' => 'select-all-procerrors', @@ -79,6 +103,7 @@ public function __construct() { get_string('step', 'tool_lifecycle'), get_string('courseid', 'tool_lifecycle'), get_string('course'), + get_string('errortime', 'tool_lifecycle'), get_string('error'), get_string('tools', 'tool_lifecycle'), ]); @@ -100,6 +125,22 @@ public function col_error($row) { ""; } + /** + * Render time of error. + * + * @param object $row Row data. + * @return string errortime cell + * @throws \coding_exception + * @throws \moodle_exception + */ + public function col_errortime($row) { + global $USER; + + return userdate($row->errortimecreated, + get_string('strftimedatetimeshortaccurate', 'core_langconfig'), + core_date::get_user_timezone($USER)); + } + /** * Render tools column. * @@ -112,20 +153,30 @@ public function col_tools($row) { global $OUTPUT; $actionmenu = new \action_menu(); - $actionmenu->add_primary_action( + if ($row->workflowid && $row->stepid ?? false) { + $actionmenu->add_primary_action( new \action_menu_link_primary( - new \moodle_url('', ['action' => 'proceed', 'id[]' => $row->id, 'sesskey' => sesskey()]), - new \pix_icon('e/tick', $this->strings['proceed']), - $this->strings['proceed'] + new \moodle_url('', ['action' => 'proceed', 'id[]' => $row->errorid, 'sesskey' => sesskey()]), + new \pix_icon('e/tick', $this->strings['proceed']), + $this->strings['proceed'] ) - ); - $actionmenu->add_primary_action( + ); + $actionmenu->add_primary_action( new \action_menu_link_primary( - new \moodle_url('', ['action' => 'rollback', 'id[]' => $row->id, 'sesskey' => sesskey()]), - new \pix_icon('e/undo', $this->strings['rollback']), - $this->strings['rollback'] + new \moodle_url('', ['action' => 'rollback', 'id[]' => $row->errorid, 'sesskey' => sesskey()]), + new \pix_icon('e/undo', $this->strings['rollback']), + $this->strings['rollback'] ) - ); + ); + } else { + $actionmenu->add_primary_action( + new \action_menu_link_primary( + new \moodle_url('', ['action' => 'delete', 'id[]' => $row->errorid, 'sesskey' => sesskey()]), + new \pix_icon('t/delete', $this->strings['delete']), + $this->strings['delete'] + ) + ); + } return $OUTPUT->render($actionmenu); } @@ -134,11 +185,14 @@ public function col_tools($row) { * * @param \stdClass $data * @return string + * @throws \coding_exception + * @throws coding_exception */ public function col_select($data) { global $OUTPUT; - $checkbox = new \core\output\checkbox_toggleall('procerrors-table', false, [ + if ($data->workflowid && $data->stepid) { + $checkbox = new \core\output\checkbox_toggleall('procerrors-table', false, [ 'classes' => 'usercheckbox m-1', 'id' => 'procerror' . $data->id, 'name' => 'procerror-select', @@ -146,9 +200,12 @@ public function col_select($data) { 'checked' => false, 'label' => get_string('selectitem', 'moodle', $data->id), 'labelclasses' => 'accesshide', - ]); + ]); - return $OUTPUT->render($checkbox); + return $OUTPUT->render($checkbox); + } else { + return ''; + } } /** @@ -170,9 +227,9 @@ protected function show_hide_link($column, $index) { * for example. Called only when there is data to display and not * downloading. */ - public function wrap_html_finish() { + public function wrap_html_start() { global $OUTPUT; - parent::wrap_html_finish(); + parent::wrap_html_start(); echo "
"; $actionmenu = new \action_menu(); diff --git a/classes/local/table/select_workflow_table.php b/classes/local/table/select_workflow_table.php index 4d0e3a43..a9c36d0b 100644 --- a/classes/local/table/select_workflow_table.php +++ b/classes/local/table/select_workflow_table.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\table; +use core_date; use tool_lifecycle\action; use tool_lifecycle\local\manager\process_manager; use tool_lifecycle\local\manager\trigger_manager; @@ -139,10 +140,14 @@ public function col_status($row) { * @return string since column */ public function col_since($row) { + global $USER; + if ($row->timedeactive) { - return userdate($row->timedeactive, get_string('strftimedatetime')); + return userdate($row->timedeactive, get_string('strftimedatetime'), + core_date::get_user_timezone($USER)); } else if ($row->timeactive) { - return userdate($row->timeactive, get_string('strftimedatetime')); + return userdate($row->timeactive, get_string('strftimedatetime'), + core_date::get_user_timezone($USER)); } else { return ''; } diff --git a/classes/local/table/triggered_courses_table.php b/classes/local/table/triggered_courses_table.php deleted file mode 100644 index 23129130..00000000 --- a/classes/local/table/triggered_courses_table.php +++ /dev/null @@ -1,209 +0,0 @@ -. - -/** - * Table listing all courses triggered by a trigger. - * - * @package tool_lifecycle - * @copyright 2025 Thomas Niedermaier Universität Münster - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -namespace tool_lifecycle\local\table; - -use tool_lifecycle\local\manager\delayed_courses_manager; -use tool_lifecycle\local\manager\workflow_manager; -use tool_lifecycle\urls;; - -defined('MOODLE_INTERNAL') || die; - -require_once($CFG->libdir . '/tablelib.php'); - -/** - * Table listing all courses triggered by a trigger. - * - * @package tool_lifecycle - * @copyright 2025 Thomas Niedermaier Universität Münster - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class triggered_courses_table extends \table_sql { - - /** @var string $type of the courses list */ - private $type; - - /** @var int $workflowid Id of the workflow */ - private $workflowid; - - /** @var bool $selectable Is workflow a draft */ - private $selectable = false; - - /** - * Builds a table of courses. - * @param array $courseids of the courses to list - * @param string $type of list: triggered, triggeredworkflow, delayed, excluded - * @param string $triggername optional, if type triggered - * @param string $workflowname optional, if type delayed - * @param null $workflowid optional, if type delayed - * @param string $filterdata optional, term to filter the table by course id or -name - * @throws \coding_exception - * @throws \dml_exception - */ - public function __construct($courseids, $type, $triggername = '', $workflowname = '', $workflowid = null, $filterdata = '') { - parent::__construct('tool_lifecycle-courses-in-trigger'); - global $DB, $PAGE; - - if (!$courseids) { - return; - } - - $this->define_baseurl($PAGE->url); - $this->type = $type; - if ($type == 'triggered') { - $this->caption = get_string('coursestriggered', 'tool_lifecycle', $triggername)." (".count($courseids).")"; - $this->workflowid = $workflowid; - } else if ($type == 'triggeredworkflow') { - $this->caption = get_string('coursestriggeredworkflow', 'tool_lifecycle', $workflowname)." (".count($courseids).")"; - $this->selectable = workflow_manager::is_active($workflowid); - $this->workflowid = $workflowid; - } else if ($type == 'delayed') { - $this->caption = get_string('coursesdelayed', 'tool_lifecycle', $workflowname)." (".count($courseids).")"; - $this->workflowid = $workflowid; - } else if ($type == 'used') { - $this->caption = get_string('coursesused', 'tool_lifecycle', $workflowname)." (".count($courseids).")"; - } else { - $this->caption = get_string('coursesexcluded', 'tool_lifecycle', $triggername)." (".count($courseids).")"; - } - $this->captionattributes = ['class' => 'ml-3']; - $columns = ['courseid', 'coursefullname', 'coursecategory']; - if ($type == 'triggeredworkflow' && $this->selectable) { - $columns[] = 'tools'; - } else if ($type == 'delayed') { - $columns[] = 'delayeduntil'; - $columns[] = 'tools'; - } else if ($type == 'used') { - $columns[] = 'otherworkflow'; - } - $this->define_columns($columns); - $headers = [ - get_string('courseid', 'tool_lifecycle'), - get_string('coursename', 'tool_lifecycle'), - get_string('coursecategory', 'moodle'), - ]; - if ($type == 'triggeredworkflow' && $this->selectable) { - $headers[] = get_string('tools', 'tool_lifecycle'); - } else if ($type == 'delayed') { - $headers[] = get_string('delayeduntil', 'tool_lifecycle'); - $headers[] = get_string('tools', 'tool_lifecycle'); - } else if ($type == 'used') { - $headers[] = get_string('workflow', 'tool_lifecycle'); - } - $this->define_headers($headers); - - $fields = "c.id as courseid, c.fullname as coursefullname, c.shortname as courseshortname, cc.name as coursecategory"; - if ($type == 'used') { - $fields .= ", COALESCE(wfp.title, wfpe.title) as otherworkflow"; - } - $from = "{course} c LEFT JOIN {course_categories} cc ON c.category = cc.id "; - if ($type == 'used') { - $from .= " LEFT JOIN {tool_lifecycle_process} p ON c.id = p.courseid - LEFT JOIN {tool_lifecycle_proc_error} pe ON c.id = pe.courseid - LEFT JOIN {tool_lifecycle_workflow} wfp ON p.workflowid = wfp.id - LEFT JOIN {tool_lifecycle_workflow} wfpe ON pe.workflowid = wfpe.id"; - } - [$insql, $inparams] = $DB->get_in_or_equal($courseids); - $where = "c.id ".$insql; - - if ($filterdata) { - if (is_numeric($filterdata)) { - $where = " c.id = $filterdata "; - } else { - $where = $where . " AND ( c.fullname LIKE '%$filterdata%' OR c.shortname LIKE '%$filterdata%')"; - } - } - - $this->set_sql($fields, $from, $where, $inparams); - $this->set_sortdata([['sortby' => 'fullname', 'sortorder' => '1']]); - } - - /** - * Render coursefullname column. - * @param object $row Row data. - * @return string course link - */ - public function col_coursefullname($row) { - $courselink = \html_writer::link(course_get_url($row->courseid), - format_string($row->coursefullname), ['target' => '_blank']); - return $courselink . '
' . $row->courseshortname . ''; - } - - /** - * Render delayeduntil column. - * @param object $row Row data. - * @return string date - * @throws \coding_exception - */ - public function col_delayeduntil($row) { - if ($delay = delayed_courses_manager::get_course_delayed($row->courseid)) { - return userdate($delay, get_string('strftimedatetime', 'core_langconfig')); - } - return "-"; - } - - /** - * Render tools column. - * - * @param object $row Row data. - * @return string html of the delete button - * @throws \coding_exception - * @throws \moodle_exception - */ - public function col_tools($row) { - global $OUTPUT, $PAGE; - - $button = ""; - if ($this->type == 'delayed') { - $params = [ - 'action' => 'deletedelay', - 'cid' => $row->courseid, - 'sesskey' => sesskey(), - 'wf' => $this->workflowid, - ]; - $button = new \single_button(new \moodle_url(urls::WORKFLOW_DETAILS, $params), - get_string('delete_delay', 'tool_lifecycle')); - } else if ($this->type == 'triggeredworkflow' && $this->selectable) { - $params = [ - 'action' => 'select', - 'cid' => $row->courseid, - 'sesskey' => sesskey(), - 'wf' => $this->workflowid, - ]; - $button = new \single_button(new \moodle_url($PAGE->url, $params), get_string('select')); - } - if ($button) { - return $OUTPUT->render($button); - } else { - return ''; - } - } - - /** - * Prints a customized "nothing to display" message. - */ - public function print_nothing_to_display() { - global $OUTPUT; - echo \html_writer::div($OUTPUT->notification(get_string('nothingtodisplay', 'moodle'), 'info'), - 'm-3'); - } -} diff --git a/classes/local/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php new file mode 100644 index 00000000..7e88cd02 --- /dev/null +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -0,0 +1,256 @@ +. + +/** + * Table listing all courses triggered by a trigger. + * + * @package tool_lifecycle + * @copyright 2025 Thomas Niedermaier Universität Münster + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lifecycle\local\table; + +use core\exception\moodle_exception; +use stdClass; +use tool_lifecycle\local\entity\trigger_subplugin; +use tool_lifecycle\local\manager\lib_manager; +use tool_lifecycle\local\manager\settings_manager; +use tool_lifecycle\local\manager\trigger_manager; +use tool_lifecycle\local\manager\workflow_manager; +use tool_lifecycle\local\response\trigger_response; +use tool_lifecycle\settings_type; +use tool_lifecycle\urls; + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->libdir . '/tablelib.php'); + +/** + * Table listing all courses triggered by a trigger. + * + * @package tool_lifecycle + * @copyright 2025 Thomas Niedermaier Universität Münster + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class triggered_courses_table_trigger extends \table_sql { + + /** @var string $type of the courses list: triggerid or delayed */ + private $type; + + /** @var int $triggerid Id of the trigger */ + private $triggerid; + + /** @var int $workflowid Id of the trigger's workflow */ + private $workflowid; + + /** @var int $triggerexclude if a trigger has setting exclude activated */ + private $triggerexclude; + + /** @var int $otherwf to count the number of courses in another workflow */ + public $otherwf = 0; + + /** @var int $delayed to count the number of courses that are delayed */ + public $delayed = 0; + + /** @var int $tablerows number of table rows effectively written */ + public $tablerows = 0; + + /** + * Builds a table of courses. + * @param trigger_subplugin $trigger of which the courses are listed + * @param string $type of list: triggered or excluded + * @param string $filterdata optional, term to filter the table by course id or course name + * @throws \coding_exception + * @throws \dml_exception + */ + public function __construct($trigger, $type, $filterdata = '') { + parent::__construct('tool_lifecycle-courses-in-trigger'); + global $PAGE, $SESSION; + + $workflow = workflow_manager::get_workflow($trigger->workflowid); + + $this->triggerid = $trigger->id; + $this->workflowid = $workflow->id; + $this->type = $type; + + $settings = settings_manager::get_settings($trigger->id, settings_type::TRIGGER); + $this->triggerexclude = $settings['exclude'] ?? false; + + $this->define_baseurl($PAGE->url); + $a = new \stdClass(); + $a->title = $trigger->instancename; + if ($type == 'triggerid') { + $this->caption = get_string('coursestriggered', 'tool_lifecycle', $a); + } else if ($type == 'excluded') { + $this->caption = get_string('coursesexcluded', 'tool_lifecycle', $a); + } + $this->captionattributes = ['class' => 'ml-3']; + + $columns = ['courseid', 'coursefullname', 'coursecategory', 'status']; + $this->define_columns($columns); + $headers = [ + get_string('courseid', 'tool_lifecycle'), + get_string('coursename', 'tool_lifecycle'), + get_string('coursecategory', 'moodle'), + get_string('status', 'moodle'), + ]; + $this->define_headers($headers); + + $lib = lib_manager::get_trigger_lib($trigger->subpluginname); + [$where, $whereparams] = $lib->get_course_recordset_where($trigger->id); + $where = str_replace("{course}", "c", $where); + // If exclude-trigger show selected courses to exclude. + $where = str_replace("<>", "=", str_replace(" NOT ", " ", $where)); + + $fields = " c.id as courseid, + c.fullname as coursefullname, + c.shortname as courseshortname, + cc.name as coursecategory, + COALESCE(p.courseid, pe.courseid, 0) as hasprocess, + COALESCE(po.workflowid, peo.workflowid, 0) as hasotherwfprocess, + CASE + WHEN COALESCE(d.delayeduntil, 0) > COALESCE(dw.delayeduntil, 0) THEN d.delayeduntil + WHEN COALESCE(d.delayeduntil, 0) < COALESCE(dw.delayeduntil, 0) THEN dw.delayeduntil + ELSE 0 + END as delaycourse "; + $from = " {course} c LEFT JOIN {course_categories} cc ON c.category = cc.id + LEFT JOIN {tool_lifecycle_process} p ON c.id = p.courseid AND p.workflowid = $workflow->id + LEFT JOIN {tool_lifecycle_proc_error} pe ON c.id = pe.courseid AND pe.workflowid = $workflow->id + LEFT JOIN {tool_lifecycle_process} po ON c.id = po.courseid AND po.workflowid <> $workflow->id + LEFT JOIN {tool_lifecycle_proc_error} peo ON c.id = peo.courseid AND peo.workflowid <> $workflow->id + LEFT JOIN {tool_lifecycle_delayed} d ON c.id = d.courseid + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid + AND dw.workflowid = $workflow->id "; + + $where .= " AND p.courseid IS NULL AND pe.courseid IS NULL "; + if (!$workflow->includesitecourse) { + $where .= " AND c.id <> 1 "; + } + + if ($filterdata) { + if (is_numeric($filterdata)) { + $where .= " AND c.id = $filterdata "; + } else { + $where .= " AND ( c.fullname LIKE '%$filterdata%' OR c.shortname LIKE '%$filterdata%')"; + } + } + + $debugsql = "SELECT ".$fields." FROM ".$from." WHERE ".$where; + foreach ($whereparams as $key => $value) { + $debugsql = str_replace(":".$key, $value, $debugsql); + } + $SESSION->debugtablesql = $debugsql; + + $this->set_sql($fields, $from, $where, $whereparams); + } + + /** + * Build the table from the fetched data. + * + * Take the data returned from the db_query and go through all the rows + * processing each col using either col_{columnname} method or other_cols + * method or if other_cols returns NULL then put the data straight into the + * table. + * + * After calling this function, don't forget to call close_recordset. + * @throws \dml_exception + */ + public function build_table() { + + if (!$this->rawdata) { + return; + } + + $trigger = trigger_manager::get_instance($this->triggerid); + $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); + $response = $lib->default_response(); + $course = new stdClass(); + foreach ($this->rawdata as $row) { + if ($row->hasotherwfprocess) { + $this->otherwf++; + } + if ($row->delaycourse && $row->delaycourse > time() && !$this->triggerexclude) { + $this->delayed++; + } + if ($lib->check_course_code()) { + $course->id = $row->courseid; + $response = $lib->check_course($course, $this->triggerid); + } + if (!($response == trigger_response::exclude() || $response == trigger_response::trigger())) { + continue; + } + if ($this->type == 'triggerid' && !$response == trigger_response::trigger()) { + continue; + } else if ($this->type == 'excluded' && + (!$response == trigger_response::exclude() || + !($response == trigger_response::trigger() && $this->triggerexclude))) { + continue; + } + $row->status = $response; + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + $this->tablerows++; + } + } + + /** + * Render coursefullname column. + * @param object $row Row data. + * @return string course link + */ + public function col_coursefullname($row) { + $courselink = \html_writer::link(course_get_url($row->courseid), + format_string($row->coursefullname), ['target' => '_blank']); + return $courselink . '
' . $row->courseshortname . ''; + } + + /** + * Render trigger status of the course (triggered, already in process, another process, delayed). + * @param object $row Row data. + * @return string status + * @throws \coding_exception + */ + public function col_status($row) { + $out = ""; + if ($row->hasotherwfprocess) { + $out .= \html_writer::div(get_string('alreadyinprocessotherworkflow', 'tool_lifecycle'), + 'text-warning'); + } + if ($row->delaycourse && $row->delaycourse > time() && !$this->triggerexclude) { + $out .= \html_writer::div(get_string('delayed', 'tool_lifecycle'), 'text-info'); + } + if ($row->status && !($row->status == trigger_response::trigger())) { + $out .= \html_writer::div(get_string('excludedbycoursecode', 'tool_lifecycle'), + 'text-warning'); + } + if ($out == "") { + $out .= \html_writer::div(get_string('ok'), 'text-success'); + } + return $out; + } + + /** + * Prints a customized "nothing to display" message. + */ + public function print_nothing_to_display() { + global $OUTPUT; + echo \html_writer::div($OUTPUT->notification(get_string('nothingtodisplay', 'moodle'), 'info'), + 'm-3'); + echo \html_writer::div("   ".\html_writer::link(new \moodle_url(urls::WORKFLOW_DETAILS, + ["wf" => $this->workflowid, "showsql" => "1", "showtablesql" => "1", "showdetails" => "1"]), + "   ", ["class" => "text-muted fs-6 text-decoration-none"])); + } +} diff --git a/classes/local/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php new file mode 100644 index 00000000..297a9ec9 --- /dev/null +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -0,0 +1,361 @@ +. + +/** + * Table listing all courses triggered by a workflow's triggers. + * + * @package tool_lifecycle + * @copyright 2025 Thomas Niedermaier Universität Münster + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lifecycle\local\table; + +use core\exception\moodle_exception; +use core_date; +use stdClass; +use tool_lifecycle\local\entity\trigger_subplugin; +use tool_lifecycle\local\entity\workflow; +use tool_lifecycle\local\manager\delayed_courses_manager; +use tool_lifecycle\local\manager\lib_manager; +use tool_lifecycle\local\manager\trigger_manager; +use tool_lifecycle\local\manager\workflow_manager; +use tool_lifecycle\local\response\trigger_response; +use tool_lifecycle\processor; +use tool_lifecycle\urls; + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->libdir . '/tablelib.php'); + +/** + * Table listing all courses triggered by a workflow's triggers. + * + * @package tool_lifecycle + * @copyright 2025 Thomas Niedermaier Universität Münster + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class triggered_courses_table_workflow extends \table_sql { + + /** @var string $type of the courses list: triggeredworkflow, delayed, used, processes */ + private $type; + + /** @var int $workflowid Id of the workflow */ + private $workflowid; + + /** @var bool $selectable Is workflow a draft */ + private $selectable = false; + + /** @var bool $coursecheckcode use course_check function to trigger courses */ + private $checkcoursecode = false; + + /** @var int $tablerows number of table rows effectively written */ + public $tablerows = 0; + + /** + * Builds a table of courses. + * @param int $courses number of courses to list + * @param workflow $workflow of which the courses are listed + * @param string $type of list: triggeredworkflow, delayed, used, processes + * @param string $filterdata optional, term to filter the table by course id or -name + * @throws \coding_exception + * @throws \dml_exception + */ + public function __construct($courses, $workflow, $type, $filterdata = '') { + parent::__construct('tool_lifecycle-courses-in-trigger'); + global $PAGE, $SESSION; + + $this->define_baseurl($PAGE->url); + $this->type = $type; + $this->workflowid = $workflow->id; + + $a = new \stdClass(); + $a->title = $workflow->title; + if ($type == 'triggeredworkflow') { + $this->caption = get_string('coursestriggeredworkflow', 'tool_lifecycle', $a); + $this->selectable = workflow_manager::is_active($workflow->id); + } else if ($type == 'delayed') { + $this->caption = get_string('coursesdelayed', 'tool_lifecycle', $a); + } else if ($type == 'used') { + $this->caption = get_string('coursesused', 'tool_lifecycle', $a); + } else if ($type == 'processes') { + $this->caption = get_string('coursesinprocess', 'tool_lifecycle', $a); + } + $this->captionattributes = ['class' => 'ml-3']; + + $columns = ['courseid', 'coursefullname', 'coursecategory']; + if ( $type == 'triggeredworkflow' && $this->selectable) { + $columns[] = 'tools'; + } else if ($type == 'delayed') { + $columns[] = 'delayeduntil'; + } else if ($type == 'used') { + $columns[] = 'otherworkflow'; + } else if ($type == 'processes') { + $columns[] = 'processtype'; + } + $this->define_columns($columns); + $headers = [ + get_string('courseid', 'tool_lifecycle'), + get_string('coursename', 'tool_lifecycle'), + get_string('coursecategory', 'moodle'), + ]; + if ( $type == 'triggeredworkflow' && $this->selectable) { + $headers[] = get_string('tools', 'tool_lifecycle'); + } else if ($type == 'delayed') { + $headers[] = get_string('delayeduntil', 'tool_lifecycle'); + } else if ($type == 'used') { + $headers[] = get_string('workflow', 'tool_lifecycle'); + } else if ($type == 'processes') { + $headers[] = get_string('type', 'tool_lifecycle'); + } + $this->define_headers($headers); + + $fields = " c.id as courseid, + c.fullname as coursefullname, + c.shortname as courseshortname, + cc.name as coursecategory, + pe.id as errorid, + COALESCE(p.courseid, pe.courseid, 0) as hasprocess, + COALESCE(po.workflowid, peo.workflowid, 0) as hasotherwfprocess, + CASE + WHEN COALESCE(d.delayeduntil, 0) > COALESCE(dw.delayeduntil, 0) THEN d.delayeduntil + WHEN COALESCE(d.delayeduntil, 0) < COALESCE(dw.delayeduntil, 0) THEN dw.delayeduntil + ELSE 0 + END as delaycourse "; + $from = " {course} c LEFT JOIN {course_categories} cc ON c.category = cc.id + LEFT JOIN {tool_lifecycle_process} p ON c.id = p.courseid AND p.workflowid = $workflow->id + LEFT JOIN {tool_lifecycle_proc_error} pe ON c.id = pe.courseid AND pe.workflowid = $workflow->id + LEFT JOIN {tool_lifecycle_process} po ON c.id = po.courseid AND po.workflowid <> $workflow->id + LEFT JOIN {tool_lifecycle_proc_error} peo ON c.id = peo.courseid AND peo.workflowid <> $workflow->id + LEFT JOIN {tool_lifecycle_delayed} d ON c.id = d.courseid + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid"; + + $where = 'true'; + $inparams = []; + if ($type == 'processes') { + $where .= " AND (p.workflowid = $workflow->id OR pe.workflowid = $workflow->id)"; + } else { + if ($type == 'delayed') { + $where .= " AND (d.delayeduntil > 0 OR dw.delayeduntil > 0) "; + } else if ($type == 'used') { + $where .= " AND (po.workflowid IS NOT NULL OR peo.workflowid IS NOT NULL)"; + } else if ($type == 'triggeredworkflow') { + $where .= " AND p.courseid IS NULL AND pe.courseid IS NULL + AND po.workflowid IS NULL AND peo.workflowid IS NULL + AND COALESCE(d.delayeduntil, 0) < :time1 AND COALESCE(dw.delayeduntil, 0) < :time2 "; + $inparams = array_merge($inparams, ['time1' => time(), 'time2' => time()]); + } + $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); + $andor = ($workflow->andor ?? 0) == 0 ? 'AND' : 'OR'; + foreach ($triggers as $trigger) { + $lib = lib_manager::get_trigger_lib($trigger->subpluginname); + if ($lib->is_manual_trigger()) { + continue; + } else { + if (!$this->checkcoursecode) { + $this->checkcoursecode = $lib->check_course_code(); + } + [$sql, $params] = $lib->get_course_recordset_where($trigger->id); + $sql = preg_replace("/{course}/", "c", $sql, 1); + if (!empty($sql)) { + $where .= " $andor " . $sql; + $inparams = array_merge($inparams, $params); + } + } + } + } + + if (!$workflow->includesitecourse) { + $where = "($where) AND c.id <> 1 "; + } + + if ($filterdata) { + if (is_numeric($filterdata)) { + $where = "($where) AND c.id = $filterdata "; + } else { + $where = "($where) AND (c.fullname LIKE '%$filterdata%' OR c.shortname LIKE '%$filterdata%')"; + } + } + + $debugsql = "SELECT ".$fields." FROM ".$from." WHERE ".$where; + foreach ($inparams as $key => $value) { + $debugsql = str_replace(":".$key, $value, $debugsql); + } + $SESSION->debugtablesql = $debugsql; + + $this->set_sql($fields, $from, $where, $inparams); + } + + /** + * Build the table from the fetched data. + * + * Take the data returned from the db_query and go through all the rows + * processing each col using either col_{columnname} method or other_cols + * method or if other_cols return NULL, then put the data straight into the + * table. + * + * After calling this function, remember to call close_recordset. + */ + public function build_table() { + if (!$this->rawdata) { + return; + } + + if ($this->type == 'triggeredworkflow' && $this->checkcoursecode) { + $autotriggers = []; + $triggers = trigger_manager::get_triggers_for_workflow($this->workflowid); + foreach ($triggers as $trigger) { + $lib = lib_manager::get_trigger_lib($trigger->subpluginname); + if ($lib->is_manual_trigger()) { + continue; + } else { + $autotriggers[] = $trigger; + } + } + } + $course = new stdClass(); + foreach ($this->rawdata as $row) { + if ($this->type == 'triggeredworkflow') { + if ($this->checkcoursecode) { + foreach ($autotriggers as $trigger) { + $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); + $course->id = $row->courseid; + $response = $lib->check_course($course, $trigger->id); + if ($response == trigger_response::next()) { + continue 2; + } + if ($response == trigger_response::exclude()) { + continue 2; + } + } + } else { + if ($row->hasprocess || $row->hasotherwfprocess || + ($row->delaycourse && $row->delaycourse > time())) { + continue; + } + } + } + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + $this->tablerows++; + } + } + + /** + * Render coursefullname column. + * @param object $row Row data. + * @return string course link + */ + public function col_coursefullname($row) { + $courselink = \html_writer::link(course_get_url($row->courseid), + format_string($row->coursefullname), ['target' => '_blank']); + return $courselink . '
' . $row->courseshortname . ''; + } + + /** + * Render delayeduntil column. + * @param object $row Row data. + * @return string date + * @throws \coding_exception + */ + public function col_delayeduntil($row) { + global $USER; + $delay = delayed_courses_manager::get_course_delayed($row->courseid); + $delaywf = delayed_courses_manager::get_course_delayed_workflow($row->courseid, $this->workflowid); + if ($delay || $delaywf) { + return userdate(max($delay, $delaywf), get_string('strftimedatetime', 'core_langconfig'), + core_date::get_user_timezone($USER)); + } + return "-"; + } + + /** + * Render tools column. + * + * @param object $row Row data. + * @return string HTML of the select button or nothing + * @throws \coding_exception + * @throws \moodle_exception + */ + public function col_tools($row) { + global $OUTPUT, $PAGE; + + if ($this->type == 'triggeredworkflow' && $this->selectable) { + $params = [ + 'action' => 'select', + 'cid' => $row->courseid, + 'sesskey' => sesskey(), + 'wf' => $this->workflowid, + ]; + $button = new \single_button(new \moodle_url($PAGE->url, $params), get_string('select')); + return $OUTPUT->render($button); + } + + return ''; + } + + /** + * Render processtype column. + * + * @param object $row Row data. + * @return string of link to processerror-page or string 'step' + * @throws \coding_exception + * @throws \moodle_exception + */ + public function col_processtype($row) { + if ($row->errorid) { + $params = [ + 'workflow' => $this->workflowid, + 'course' => $row->courseid, + ]; + return \html_writer::link( + new \moodle_url(urls::PROCESS_ERRORS, $params), + get_string('process_error', 'tool_lifecycle'), + ['class' => 'error']); + } else { + return get_string('step', 'tool_lifecycle'); + } + } + + /** + * Render otherworkflow column. + * @param object $row Row data. + * @return string date + * @throws \coding_exception + */ + public function col_otherworkflow($row) { + global $DB; + if ($row->hasotherwfprocess) { + if ($workflowname = $DB->get_field('tool_lifecycle_workflow', 'title', ['id' => $row->hasotherwfprocess])) { + return $workflowname; + } else { + return "-"; + } + } + return "-"; + } + + /** + * Prints a customized "nothing to display" message. + */ + public function print_nothing_to_display() { + global $OUTPUT; + echo \html_writer::div($OUTPUT->notification(get_string('nothingtodisplay', 'moodle'), 'info'), + 'm-3'); + echo \html_writer::div("   ".\html_writer::link(new \moodle_url(urls::WORKFLOW_DETAILS, + ["wf" => $this->workflowid, "showsql" => "1", "showtablesql" => "1", "showdetails" => "1"]), + "   ", ["class" => "text-muted fs-6 text-decoration-none"])); + } +} diff --git a/classes/local/table/workflow_definition_table.php b/classes/local/table/workflow_definition_table.php index 56862afe..e5d70d29 100644 --- a/classes/local/table/workflow_definition_table.php +++ b/classes/local/table/workflow_definition_table.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\table; +use core_date; use tool_lifecycle\action; use tool_lifecycle\local\manager\lib_manager; use tool_lifecycle\local\manager\step_manager; @@ -81,9 +82,10 @@ public function init() { * @throws \moodle_exception */ public function col_timeactive($row) { - global $OUTPUT; + global $OUTPUT, $USER; if ($row->timeactive) { - return userdate($row->timeactive, get_string('strftimedatetime'), 0); + return userdate($row->timeactive, get_string('strftimedatetime'), + core_date::get_user_timezone($USER)); } if (workflow_manager::is_valid($row->id)) { return $OUTPUT->single_button(new \moodle_url(urls::ACTIVE_WORKFLOWS, @@ -122,8 +124,8 @@ public function col_tools($row) { if (!isset($lib) || $lib->has_multiple_instances()) { $action = action::WORKFLOW_BACKUP; - $alt = get_string('backupworkflow', 'tool_lifecycle'); - $icon = 't/backup'; + $alt = get_string('downloadworkflow', 'tool_lifecycle'); + $icon = 't/download'; $output .= $this->format_icon_link($action, $row->id, $icon, $alt); if (!workflow_manager::is_active($row->id)) { diff --git a/classes/local/table/workflow_table.php b/classes/local/table/workflow_table.php index 1763c0a5..881bab82 100644 --- a/classes/local/table/workflow_table.php +++ b/classes/local/table/workflow_table.php @@ -23,6 +23,8 @@ */ namespace tool_lifecycle\local\table; +use core_date; +use html_writer; use tool_lifecycle\action; use tool_lifecycle\local\manager\process_manager; use tool_lifecycle\local\manager\trigger_manager; @@ -68,9 +70,10 @@ public function col_title($row) { * @throws \moodle_exception */ public function col_timeactive($row) { - global $OUTPUT, $PAGE; + global $OUTPUT, $PAGE, $USER; if ($row->timeactive) { - return userdate($row->timeactive, get_string('strftimedatetime'), 0); + return userdate($row->timeactive, get_string('strftimedatetime'), + core_date::get_user_timezone($USER)); } return $OUTPUT->single_button(new \moodle_url($PAGE->url, ['action' => action::WORKFLOW_ACTIVATE, @@ -86,8 +89,10 @@ public function col_timeactive($row) { * @throws \coding_exception */ public function col_timedeactive($row) { + global $USER; if ($row->timedeactive) { - return userdate($row->timedeactive, get_string('strftimedatetime'), 0); + return userdate($row->timedeactive, get_string('strftimedatetime'), + core_date::get_user_timezone($USER)); } return get_string('workflow_active', 'tool_lifecycle'); } @@ -98,6 +103,7 @@ public function col_timedeactive($row) { * @return string instancename of the trigger */ public function col_trigger($row) { + $triggerstring = "--"; $triggers = trigger_manager::get_triggers_for_workflow($row->id); if ($triggers) { $triggerstring = $triggers[0]->instancename; @@ -161,4 +167,56 @@ protected function format_icon_link($action, $workflowid, $icon, $alt) { null , ['title' => $alt]) . ' '; } + /** + * This function is not part of the public api. + * @param array $row Row date + * @param string $classname classes to add + * @return string HTML code for the row passed. + */ + public function print_row($row, $classname = '') { + echo $this->get_row_html($row, $classname); + } + + /** + * Generate html code for the passed row. + * + * @param array $row Row data. + * @param string $classname classes to add. + * + * @return string $html html code for the row passed. + */ + public function get_row_html($row, $classname = '') { + static $suppresslastrow = null; + $rowclasses = []; + + if ($classname) { + $rowclasses[] = $classname; + } + + $rowid = $this->uniqueid . '_r' . $this->currentrow; + $html = ''; + + $html .= html_writer::start_tag('tr', ['class' => implode(' ', $rowclasses), 'id' => $rowid]); + + // If we have a separator, print it. + if ($row === null) { + $colcount = count($this->columns); + $html .= html_writer::tag('td', html_writer::tag( + 'div', + '', + ['class' => 'tabledivider'] + ), ['colspan' => $colcount]); + } else { + $html .= $this->get_row_cells_html($rowid, $row, $suppresslastrow); + } + + $html .= html_writer::end_tag('tr'); + + $suppressenabled = array_sum($this->column_suppress); + if ($suppressenabled) { + $suppresslastrow = $row; + } + $this->currentrow++; + return $html; + } } diff --git a/classes/processor.php b/classes/processor.php index 37be60a5..3652561f 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -22,11 +22,16 @@ * @copyright 2017 Tobias Reischmann WWU * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + +// phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameLowerCase + namespace tool_lifecycle; -use core\task\manager; +use core_date; +use tool_lifecycle\event\process_rollback; use tool_lifecycle\local\entity\trigger_subplugin; use tool_lifecycle\event\process_triggered; +use tool_lifecycle\local\entity\workflow; use tool_lifecycle\local\manager\process_manager; use tool_lifecycle\local\manager\settings_manager; use tool_lifecycle\local\manager\step_manager; @@ -38,8 +43,6 @@ use tool_lifecycle\local\response\step_response; use tool_lifecycle\local\response\trigger_response; -define("OTHERWORKFLOW", 2); - /** * Offers functionality to trigger, process and finish lifecycle processes. * @@ -53,33 +56,106 @@ class processor { * Processes the trigger plugins for all relevant courses. */ public function call_trigger() { + global $FULLSCRIPT, $CFG, $USER; + + $run = str_contains($FULLSCRIPT, 'run.php'); // Called by run-command of workflowoverview? + // Debug mode if admin setting debug is active and function is not called within a behat test. + $debug = $run && $CFG->debugdeveloper && !defined('BEHAT_SITE_RUNNING'); + + // Only active workflows that are not manual workflows. $activeworkflows = workflow_manager::get_active_automatic_workflows(); - $globallydelayedcourses = delayed_courses_manager::get_globally_delayed_courses(); + // Print debug message if this is not a behat test. + if (!defined('BEHAT_SITE_RUNNING')) { + if ($run) { + echo \html_writer::div(get_string ('active_workflows_header_title', 'tool_lifecycle'). + ": ".count($activeworkflows)); + } else { + mtrace(get_string ('active_workflows_header_title', 'tool_lifecycle'). + ": ".count($activeworkflows)); + } + } + + // Walk through the active workflows. foreach ($activeworkflows as $workflow) { $countcourses = 0; - mtrace('Calling triggers for workflow "' . $workflow->title . '"'); + $counttriggered = 0; + $countexcluded = 0; + $exclude = []; + + // Print debug message if this is not a behat test. + if (!defined('BEHAT_SITE_RUNNING')) { + if ($run) { + echo \html_writer::div('Calling triggers for workflow "' . $workflow->title . '" '. + userdate(time(), get_string('strftimedatetimeaccurate'), + core_date::get_user_timezone($USER))); + } else { + mtrace('Calling triggers for workflow "' . $workflow->title . '" '. + userdate(time(), get_string('strftimedatetimeaccurate'), + core_date::get_user_timezone($USER))); + } + } + + // Get workflow triggers and settings. $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); - if ($workflow->includesitecourse) { - $sitecourse = []; - } else { - $sitecourse = [1]; + if (!$workflow->includesitecourse) { + $exclude[] = 1; } - if ($workflow->includedelayedcourses) { - $delayedcourses = []; - } else { - $delayedcourses = array_merge(delayed_courses_manager::get_delayed_courses_for_workflow($workflow->id), - $globallydelayedcourses); + if (!$workflow->includedelayedcourses) { + $exclude = array_merge(delayed_courses_manager::get_delayed_courses_for_workflow($workflow->id), + delayed_courses_manager::get_globally_delayed_courses(), $exclude); } - $recordset = $this->get_course_recordset($triggers, array_merge($delayedcourses, $sitecourse)); + // Get recordset of triggered courses. + $recordset = $this->get_course_recordset($triggers, !$workflow->includesitecourse); + // Walk through the course list. while ($recordset->valid()) { $course = $recordset->current(); $countcourses++; + // Check trigger by trigger if the course is to be triggered or not. + foreach ($triggers as $trigger) { + $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); + $response = $lib->check_course($course, $trigger->id); + if ($response == trigger_response::next()) { + if ($debug) { + echo \html_writer::div("Course next: $course->id"); + } + $recordset->next(); + continue 2; + } + if ($response == trigger_response::exclude()) { + array_push($exclude, $course->id); + $countexcluded++; + if ($debug) { + echo \html_writer::div("Course exclude: $course->id"); + } + $recordset->next(); + continue 2; + } + if ($response == trigger_response::trigger()) { + continue; + } + } + // If all trigger instances agree that they want to trigger a process we do so. $process = process_manager::create_process($course->id, $workflow->id); process_triggered::event_from_process($process)->trigger(); + $counttriggered++; + if ($debug) { + echo \html_writer::div("Course triggered: $course->id"); + } $recordset->next(); } - mtrace(" $countcourses courses processed."); + // Final debug messages if this is not a behat test. + if (!defined('BEHAT_SITE_RUNNING')) { + if ($run) { + echo \html_writer::div(" $countcourses courses processed."); + echo \html_writer::div(" $counttriggered courses triggered."); + echo \html_writer::div(" $countexcluded courses excluded."); + } else { + mtrace(" $countcourses courses processed."); + mtrace(" $counttriggered courses triggered."); + mtrace(" $countexcluded courses excluded."); + } + } } } @@ -87,22 +163,36 @@ public function call_trigger() { * Calls the process_course() method of each step submodule currently responsible for a given course. */ public function process_courses() { + global $FULLSCRIPT, $CFG; + + $run = str_contains($FULLSCRIPT, 'run.php'); + $debug = $run && $CFG->debugdeveloper && !defined('BEHAT_SITE_RUNNING'); + + if (!defined('BEHAT_SITE_RUNNING')) { + if ($run) { + echo \html_writer::div(get_string ('lifecycle_task', 'tool_lifecycle')); + } else { + mtrace(get_string ('lifecycle_task', 'tool_lifecycle')); + } + } + $coursesprocessed = 0; + $coursesprocesserrors = 0; + foreach (process_manager::get_processes() as $process) { - $workflow = workflow_manager::get_workflow($process->workflowid); while (true) { try { $course = get_course($process->courseid); } catch (\dml_missing_record_exception $e) { - mtrace("The course with id $process->courseid no longer exists. New stdClass with id property is created."); - $course = new \stdClass(); - $course->id = $process->courseid; + // Course no longer exists! + break; } if ($process->stepindex == 0) { if (!process_manager::proceed_process($process)) { // Happens for a workflow with no step. - delayed_courses_manager::set_course_delayed_for_workflow($course->id, false, $workflow); + delayed_courses_manager::set_course_delayed_for_workflow($course->id, + false, $process->workflowid); break; } } @@ -115,28 +205,49 @@ public function process_courses() { } else { $result = $lib->process_course($process->id, $step->id, $course); } + $coursesprocessed++; } catch (\Exception $e) { - unset($process->context); process_manager::insert_process_error($process, $e); + $coursesprocesserrors++; break; } if ($result == step_response::waiting()) { process_manager::set_process_waiting($process); + if ($debug) { + echo \html_writer::div("Course processed: $course->id - Result: Waiting"); + } break; } else if ($result == step_response::proceed()) { if (!process_manager::proceed_process($process)) { - delayed_courses_manager::set_course_delayed_for_workflow($course->id, false, $workflow); - break; + delayed_courses_manager::set_course_delayed_for_workflow($course->id, + false, $process->workflowid); } + if ($debug) { + echo \html_writer::div("Course processed: $course->id - Result: Proceed"); + } + break; } else if ($result == step_response::rollback()) { - delayed_courses_manager::set_course_delayed_for_workflow($course->id, true, $workflow); + delayed_courses_manager::set_course_delayed_for_workflow($course->id, + true, $process->workflowid); process_manager::rollback_process($process); + if ($debug) { + echo \html_writer::div("Course processed: $course->id - Result: Rollback"); + } break; } else { throw new \moodle_exception('Return code \''. var_dump($result) . '\' is not allowed!'); } } } + if (!defined('BEHAT_SITE_RUNNING')) { + if ($run) { + echo \html_writer::div(" $coursesprocessed courses processed."); + echo \html_writer::div(" $coursesprocesserrors ".get_string('errors', 'search')."."); + } else { + mtrace(" $coursesprocessed courses processed."); + mtrace(" $coursesprocesserrors ".get_string('errors', 'search')."."); + } + } } @@ -186,146 +297,205 @@ public function process_course_interactive($processid) { * Returns a record set with all relevant courses for a list of automatic triggers. * Relevant means that there is currently no lifecycle process running for this course. * @param trigger_subplugin[] $triggers List of triggers, which will be asked for additional where requirements. - * @param int[] $exclude List of course id, which should be excluded from execution. + * @param bool $nositecourse is the site course to be excluded or not. * @param bool $forcounting * @return \moodle_recordset with relevant courses. * @throws \coding_exception * @throws \dml_exception */ - public function get_course_recordset($triggers, $exclude, $forcounting = false) { - global $DB; + public function get_course_recordset($triggers, $nositecourse = true, $forcounting = false) { + global $DB, $SESSION; - $where = 'true'; + $where = " TRUE "; $whereparams = []; + $workflow = false; foreach ($triggers as $trigger) { + if (!$workflow) { + $workflow = workflow_manager::get_workflow($trigger->workflowid); + $andor = ($workflow->andor ?? 0) == 0 ? 'AND' : 'OR'; + $where = $andor == 'AND' ? 'true ' : 'false '; + } $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); [$sql, $params] = $lib->get_course_recordset_where($trigger->id); + $sql = preg_replace("/{course}/", "c", $sql, 1); if (!empty($sql)) { - $where .= ' AND ' . $sql; + $where .= " $andor " . $sql; $whereparams = array_merge($whereparams, $params); } } - if (!empty($exclude)) { - [$insql, $inparams] = $DB->get_in_or_equal($exclude, SQL_PARAMS_NAMED); - $where .= " AND NOT {course}.id {$insql}"; - $whereparams = array_merge($whereparams, $inparams); - } - if ($forcounting) { - // Get course hasotherprocess and delay with the sql. - $sql = "SELECT {course}.id, + // We include delayed courses here anyway, so we only take the site course into account. + if ($nositecourse) { + $where = "($where) AND c.id <> 1 "; + } + // Get course hasprocess and delay with the sql. + $sql = "SELECT c.id, COALESCE(p.courseid, pe.courseid, 0) as hasprocess, - CASE - WHEN COALESCE(p.workflowid, 0) > COALESCE(pe.workflowid, 0) THEN p.workflowid - WHEN COALESCE(p.workflowid, 0) < COALESCE(pe.workflowid, 0) THEN pe.workflowid - ELSE 0 - END as workflowid, + COALESCE(po.workflowid, peo.workflowid, 0) as hasotherwfprocess, CASE WHEN COALESCE(d.delayeduntil, 0) > COALESCE(dw.delayeduntil, 0) THEN d.delayeduntil WHEN COALESCE(d.delayeduntil, 0) < COALESCE(dw.delayeduntil, 0) THEN dw.delayeduntil ELSE 0 - END as delay - FROM {course} - LEFT JOIN {tool_lifecycle_process} p - ON {course}.id = p.courseid - LEFT JOIN {tool_lifecycle_proc_error} pe ON {course}.id = pe.courseid - LEFT JOIN {tool_lifecycle_delayed} d ON {course}.id = d.courseid - LEFT JOIN {tool_lifecycle_delayed_workf} dw ON {course}.id = dw.courseid - WHERE " . $where; + END as delaycourse + FROM {course} c + LEFT JOIN {tool_lifecycle_process} p ON c.id = p.courseid AND p.workflowid = $workflow->id + LEFT JOIN {tool_lifecycle_proc_error} pe ON c.id = pe.courseid AND pe.workflowid = $workflow->id + LEFT JOIN {tool_lifecycle_process} po ON c.id = po.courseid AND po.workflowid <> $workflow->id + LEFT JOIN {tool_lifecycle_proc_error} peo ON c.id = peo.courseid AND peo.workflowid <> $workflow->id + LEFT JOIN {tool_lifecycle_delayed} d ON c.id = d.courseid + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON + c.id = dw.courseid AND dw.workflowid = $workflow->id"; + $sql .= " WHERE $where "; } else { + if ($workflow) { + if (!$workflow->includesitecourse) { + $where = "($where) AND c.id <> 1 "; + } + if (!$workflow->includedelayedcourses) { + $where = "($where) AND NOT c.id in (select courseid FROM {tool_lifecycle_delayed_workf} + WHERE delayeduntil > :time1 AND workflowid = :workflowid) + AND NOT c.id in (select courseid FROM {tool_lifecycle_delayed} WHERE delayeduntil > :time2) "; + $inparams = ['time1' => time(), 'time2' => time(), 'workflowid' => $workflow->id]; + $whereparams = array_merge($whereparams, $inparams); + } + } // Get only courses which are not part of an existing process. - $sql = 'SELECT {course}.id from {course} '. - 'LEFT JOIN {tool_lifecycle_process} '. - 'ON {course}.id = {tool_lifecycle_process}.courseid '. - 'LEFT JOIN {tool_lifecycle_proc_error} pe ON {course}.id = pe.courseid ' . - 'WHERE {tool_lifecycle_process}.courseid is null AND ' . - 'pe.courseid IS NULL AND '. $where; + $sql = "SELECT c.id from {course} c + LEFT JOIN {tool_lifecycle_process} p ON c.id = p.courseid + LEFT JOIN {tool_lifecycle_proc_error} pe ON c.id = pe.courseid + WHERE p.courseid is null AND pe.courseid IS NULL AND " . $where; } + $debugsql = $sql; + foreach ($whereparams as $key => $value) { + $debugsql = str_replace(":".$key, $value, $debugsql); + } + $SESSION->debugtriggersql = $debugsql; return $DB->get_recordset_sql($sql, $whereparams); } /** - * Returns the amount of courses for a trigger for counting. + * Returns the number of courses for a trigger for counting. * Relevant means that there is currently no lifecycle process running for this course. - * @param trigger_subplugin $trigger trigger, which will be asked for additional where requirements. - * @param int[] $exclude List of course id, which should be excluded from execution. - * @return int $amount of triggered courses. + * Triggered courses: Courses triggered by the SQL regarding the delayed courses configuration of workflow + * New courses: The number of triggered courses that are not already part of the workflow process + * Delayed courses: Triggered courses which are in a delay (for workflow or at system level) + * @param object $trigger trigger, which will be asked for additional where requirements. + * @return array[$triggered, $new, $delayed] number of triggered, new, delayed courses * @throws \coding_exception * @throws \dml_exception */ - public function get_triggercourses_forcounting($trigger, $exclude) { + public function get_triggercourses_forcounting($trigger) { global $DB; $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); // Get SQL for this trigger. - [$sql, $whereparams] = $lib->get_course_recordset_where($trigger->id); + [$where, $whereparams] = $lib->get_course_recordset_where($trigger->id); + $where = str_replace("{course}", "c", $where); + // We just want the triggered courses here, no matter of including or excluding. - $where = str_replace(" NOT ", " ", $sql); - // Exclude courses in steps of this wf, delayed courses and sitecourse according to the workflow settings. - if (!empty($exclude)) { - [$insql, $inparams] = $DB->get_in_or_equal($exclude, SQL_PARAMS_NAMED); - $where .= " AND NOT {course}.id {$insql}"; + $where = str_replace("<>", "=", str_replace(" NOT ", " ", $where)); + + // Now get all the courses triggered by this trigger. + $sql = 'SELECT c.id from {course} c WHERE '. $where; + $triggercoursesall = $DB->get_fieldset_sql($sql, $whereparams); + + // Get number of delayed courses which would be triggered by this trigger. + $workflow = workflow_manager::get_workflow($trigger->workflowid); + if ($workflow->includedelayedcourses) { + $delayed = []; + } else { + $delayed = array_merge(delayed_courses_manager::get_delayed_courses_for_workflow($workflow->id), + delayed_courses_manager::get_globally_delayed_courses()); + } + $delayedcourses = count(array_intersect($triggercoursesall, $delayed)); + + // Exclude delayed courses and site-course according to the workflow settings. + if (!$workflow->includesitecourse) { + $where .= " AND c.id <> 1 "; + } + if (!$workflow->includedelayedcourses) { + $where .= " AND NOT c.id in (select courseid FROM {tool_lifecycle_delayed_workf} + WHERE delayeduntil > :time1 AND workflowid = :workflowid) + AND NOT c.id in (select courseid FROM {tool_lifecycle_delayed} WHERE delayeduntil > :time2) "; + $inparams = ['time1' => time(), 'time2' => time(), 'workflowid' => $workflow->id]; $whereparams = array_merge($whereparams, $inparams); } - // Now get the amount of courses triggered by this trigger. - $sql = 'SELECT {course}.id from {course} WHERE '. $where; - $triggercourses = $DB->get_records_sql($sql, $whereparams); - $sql .= " AND {course}.id NOT IN (". - "SELECT {course}.id from {course} - LEFT JOIN {tool_lifecycle_process} - ON {course}.id = {tool_lifecycle_process}.courseid - LEFT JOIN {tool_lifecycle_proc_error} pe ON {course}.id = pe.courseid - WHERE ({tool_lifecycle_process}.courseid IS NOT NULL AND {tool_lifecycle_process}.workflowid = $trigger->workflowid) - OR (pe.courseid IS NOT NULL AND pe.workflowid = $trigger->workflowid))"; - $newcourses = $DB->get_records_sql($sql, $whereparams); - - return [count($triggercourses), count($newcourses)]; + + $sql = 'SELECT count(c.id) from {course} c WHERE '. $where; + + $triggercourses = $DB->count_records_sql($sql, $whereparams); + + // Only get courses which are not part of this workflow yet. Exclude processes and proc_errors of this wf. + $sql .= " AND c.id NOT IN ( + SELECT {course}.id from {course} + LEFT JOIN {tool_lifecycle_process} p ON {course}.id = p.courseid + LEFT JOIN {tool_lifecycle_proc_error} pe ON {course}.id = pe.courseid + WHERE (p.courseid IS NOT NULL AND p.workflowid = :workflowid1) OR + (pe.courseid IS NOT NULL AND pe.workflowid = :workflowid2) + )"; + $inparams = ['workflowid1' => $workflow->id, 'workflowid2' => $workflow->id]; + $whereparams = array_merge($whereparams, $inparams); + $newcourses = $DB->count_records_sql($sql, $whereparams); + + return [$triggercourses, $newcourses, $delayedcourses]; } + /** - * Returns a record set with all relevant courses for a trigger for counting. - * Relevant means that there is currently no lifecycle process running for this course. - * @param trigger_subplugin $trigger trigger, which will be asked for additional where requirements. - * @param object $workflow workflow instance. - * @return \moodle_recordset with relevant courses. + * Returns the number of courses triggered by a trigger for counting. BUT the trigger lib function check_courses + * is used to select a course for triggering/excluding. + * Triggered courses: Which courses are triggered by the SQL regarding the delayed courses configuration of workflow + * New courses: The number of triggered courses that are not already part of the workflow process + * Delayed courses: Triggered courses which are in a delay (for workflow or at system level) + * @param object $trigger trigger, which will be asked for additional where requirements. + * @return array[$triggered, $new, $delayed] number of triggered, new, delayed courses * @throws \coding_exception * @throws \dml_exception */ - public function get_triggercourses($trigger, $workflow) { + public function get_triggercourses_forcounting_check_course($trigger) { global $DB; + $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); + $delayedcourses = []; + $workflow = workflow_manager::get_workflow($trigger->workflowid); // Exclude delayed courses and sitecourse according to the workflow settings. - $sitecourse = $workflow->includesitecourse ? [] : [1]; - if ($workflow->includedelayedcourses) { - $delayedcourses = []; - } else { + $excludesitecourse = $workflow->includesitecourse ? [] : [1]; + if (!$workflow->includedelayedcourses) { $delayedcourses = array_merge(delayed_courses_manager::get_delayed_courses_for_workflow($workflow->id), delayed_courses_manager::get_globally_delayed_courses()); } - // Exclude courses in steps of this workflow. - $sqlstepcourses = "SELECT {course}.id from {course} - LEFT JOIN {tool_lifecycle_process} - ON {course}.id = {tool_lifecycle_process}.courseid - LEFT JOIN {tool_lifecycle_proc_error} pe ON {course}.id = pe.courseid - WHERE ({tool_lifecycle_process}.courseid IS NOT NULL AND {tool_lifecycle_process}.workflowid = $workflow->id) - OR (pe.courseid IS NOT NULL AND pe.workflowid = $workflow->id)"; - $stepcourses = $DB->get_fieldset_sql($sqlstepcourses); - $excludedcourses = array_merge($sitecourse, $delayedcourses, $stepcourses); + $excludedcourses = array_merge($excludesitecourse, $delayedcourses); - $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); - // Get SQL for this trigger. - [$sql, $whereparams] = $lib->get_course_recordset_where($trigger->id); - // We just want the triggered courses here, no matter of including or excluding. - $where = str_replace(" NOT ", " ", $sql); - if (!empty($excludedcourses)) { - [$insql, $inparams] = $DB->get_in_or_equal($excludedcourses, SQL_PARAMS_NAMED); - $where .= " AND NOT {course}.id {$insql}"; - $whereparams = array_merge($whereparams, $inparams); + $triggercoursesall = []; + $recordset = $this->get_course_recordset([$trigger], !$workflow->includesitecourse, true); + $response = $lib->default_response(); + while ($recordset->valid()) { + $course = $recordset->current(); + if ($lib->check_course_code()) { + $response = $lib->check_course($course, $trigger->id); + } + if ($response !== trigger_response::next()) { + $triggercoursesall[] = $course->id; + } + $recordset->next(); } - // Now get the list of course IDs triggered by this trigger. - $sql = "SELECT {course}.id FROM {course} WHERE " . $where; - return $DB->get_fieldset_sql($sql, $whereparams); + + // Get delayed courses which would be triggered by this trigger. + $delayedcourses = array_intersect($triggercoursesall, $delayedcourses); + + $triggercourses = array_diff($triggercoursesall, $excludedcourses); + + // Only get courses which are not part of this workflow yet. Exclude processes and proc_errors of this wf. + $sql = "SELECT {course}.id from {course} + LEFT JOIN {tool_lifecycle_process} p ON {course}.id = p.courseid + LEFT JOIN {tool_lifecycle_proc_error} pe ON {course}.id = pe.courseid + WHERE (p.courseid IS NOT NULL AND p.workflowid = $trigger->workflowid) + OR (pe.courseid IS NOT NULL AND pe.workflowid = $trigger->workflowid)"; + $stepcourses = $DB->get_fieldset_sql($sql, []); + $newcourses = array_diff($triggercourses, $stepcourses); + + return [count($triggercourses), count($newcourses), count($delayedcourses)]; } /** @@ -336,28 +506,16 @@ public function get_triggercourses($trigger, $workflow) { * @throws \dml_exception */ public function get_count_of_courses_to_trigger_for_workflow($workflow) { - global $DB; - - $counttriggered = 0; - $coursestriggered = []; - $usedcourses = []; - - // Exclude delayed courses and sitecourse according to the workflow settings. - $sitecourse = $workflow->includesitecourse ? [] : [1]; - if ($workflow->includedelayedcourses) { - $delayedcourses = []; - } else { - $delayedcourses = array_merge(delayed_courses_manager::get_delayed_courses_for_workflow($workflow->id), - delayed_courses_manager::get_globally_delayed_courses()); - } - $excludedcourses = array_merge($sitecourse, $delayedcourses); + global $USER; $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); $amounts = []; $autotriggers = []; $nextrun = 0; + // If at least one trigger demands the function check_course() it will be applied for every trigger. + $checkcoursecode = false; foreach ($triggers as $trigger) { - $trigger = (object)(array) $trigger; // Cast to normal object to be able to set dynamic properties. + $trigger = (object)(array) $trigger; // Cast to a std object to be able to set dynamic properties. $settings = settings_manager::get_settings($trigger->id, settings_type::TRIGGER); $trigger->exclude = $settings['exclude'] ?? false; $obj = new \stdClass(); @@ -365,36 +523,54 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { if ($lib->is_manual_trigger()) { $obj->automatic = false; } else { + if (!$checkcoursecode) { + $checkcoursecode = $lib->check_course_code(); + } $obj->automatic = true; $obj->triggered = 0; - $obj->excluded = 0; + $obj->excluded = false; // Only use triggers with true sql to display the real amounts for the others (instead of always 0). $obj->sql = trigger_manager::get_trigger_sqlresult($trigger); - $obj->response = $lib->check_course(null, null); + // We only need the trigger response here. + $obj->response = $lib->default_response(); if ($obj->sql != "false") { + // Get courses amounts. + // Triggercourses: Courses in current selection without defined excluded courses. + // Newcourses: Triggercourses which are not already in workflow (process or process error). + // Delayed: Courses in current selection, which are delayed. + if ($lib->check_course_code()) { + [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting_check_course( + $trigger); + } else { + [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting($trigger); + } if ($obj->response == trigger_response::exclude()) { - // Get courses excluded amount. - [$triggercourses, $newcourses] = $this->get_triggercourses_forcounting($trigger, $excludedcourses); - $obj->excluded = $triggercourses; - $obj->alreadyin = $triggercourses - $newcourses; + $obj->excluded = $newcourses; + $obj->delayed = $delayed; + $obj->alreadyin = 0; } else if ($obj->response == trigger_response::trigger()) { - // Get courses triggered amount. - [$triggercourses, $newcourses] = $this->get_triggercourses_forcounting($trigger, $excludedcourses); if ($trigger->exclude) { - $obj->excluded = $triggercourses; + $obj->excluded = $newcourses; + $obj->delayed = $delayed; + $obj->alreadyin = 0; } else { - $obj->triggered = $triggercourses; + $obj->triggered = $newcourses; + $obj->delayed = $delayed; + $obj->alreadyin = $triggercourses - $newcourses; } - $obj->alreadyin = $triggercourses - $newcourses; } $autotriggers[] = $trigger; } else if ($obj->response == trigger_response::triggertime()) { if ($nextrun = $lib->get_next_run_time($trigger->id)) { - $obj->lastrun = $settings['timelastrun']; - $obj->additionalinfo = get_string('lastrun', 'tool_lifecycle', - userdate($settings['timelastrun'], get_string('strftimedatetimeshort', 'langconfig'))); + if ($obj->lastrun = $settings['timelastrun'] ?? 0) { + $obj->additionalinfo = get_string('lastrun', 'tool_lifecycle', + userdate($settings['timelastrun'], get_string('strftimedatetimeshort', 'langconfig'), + core_date::get_user_timezone($USER))); + } else { + $obj->additionalinfo = "--"; + } } else { - $obj->additionalinfo = '-'; + $obj->additionalinfo = "--"; } $obj->sql = "---"; $autotriggers[] = $trigger; @@ -403,37 +579,82 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $amounts[$trigger->sortindex] = $obj; } - // Exclude courses in steps of this workflow. - $sqlstepcourses = "SELECT {course}.id from {course} - LEFT JOIN {tool_lifecycle_process} - ON {course}.id = {tool_lifecycle_process}.courseid - LEFT JOIN {tool_lifecycle_proc_error} pe ON {course}.id = pe.courseid - WHERE ({tool_lifecycle_process}.courseid IS NOT NULL AND {tool_lifecycle_process}.workflowid = $workflow->id) - OR (pe.courseid IS NOT NULL AND pe.workflowid = $workflow->id)"; - $excludedcourses = array_merge($DB->get_fieldset_sql($sqlstepcourses), $sitecourse); - - $delayedcourses = []; // Only delayed courses of selected courses are of interest here. - $recordset = $this->get_course_recordset($autotriggers, $excludedcourses, true); - while ($recordset->valid()) { - $course = $recordset->current(); - if ($course->hasprocess) { - if ($course->workflowid && ($course->workflowid != $workflow->id)) { - $usedcourses[] = $course->id; + $recordset = false; + if ($autotriggers) { + $recordset = $this->get_course_recordset($autotriggers, !$workflow->includesitecourse, true); + } + + $coursestriggered = 0; + $coursesdelayed = 0; // Only delayed courses of selected courses are of interest here. + $hasprocess = 0; // Number of courses that already have a process in this workflow. + $hasotherwfprocess = 0; // Number of courses that have a process in another workflow. + if ($recordset) { + while ($recordset->valid()) { + $course = $recordset->current(); + if ($checkcoursecode) { + $action = false; + foreach ($autotriggers as $trigger) { + $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); + $response = $lib->check_course($course, $trigger->id); + if ($response == trigger_response::next()) { + if (!$action) { + $action = true; + } + continue; + } + if ($response == trigger_response::exclude()) { + if (!$action) { + $action = true; + } + continue; + } + if ($response == trigger_response::trigger()) { + continue; + } + } + if (!$action) { + if ($course->hasprocess) { + $hasprocess++; + if ($course->delaycourse && $course->delaycourse > time()) { + $coursesdelayed++; + } + } else if ($course->hasotherwfprocess) { + $hasotherwfprocess++; + if ($course->delaycourse && $course->delaycourse > time()) { + $coursesdelayed++; + } + } else if ($course->delaycourse && $course->delaycourse > time()) { + $coursesdelayed++; + } else { + $coursestriggered++; + } + } + } else { + if ($course->hasprocess) { + $hasprocess++; + if ($course->delaycourse && $course->delaycourse > time()) { + $coursesdelayed++; + } + } else if ($course->hasotherwfprocess) { + $hasotherwfprocess++; + if ($course->delaycourse && $course->delaycourse > time()) { + $coursesdelayed++; + } + } else if ($course->delaycourse && $course->delaycourse > time()) { + $coursesdelayed++; + } else { + $coursestriggered++; + } } - } else if ($course->delay) { - $delayedcourses[] = $course->id; - } else { - $counttriggered++; - $coursestriggered[] = $course->id; + $recordset->next(); } - $recordset->next(); } $all = new \stdClass(); - $all->triggered = $counttriggered; $all->coursestriggered = $coursestriggered; - $all->delayedcourses = $delayedcourses; // Delayed courses for workflow and globally. Excluded per default. - $all->used = $usedcourses; + $all->delayedcourses = $coursesdelayed; // Delayed courses for workflow and globally. Excluded per default. + $all->used = $hasprocess; + $all->hasotherwf = $hasotherwfprocess; $all->nextrun = $nextrun; $amounts['all'] = $all; return $amounts; diff --git a/classes/tabs.php b/classes/tabs.php index c5a54875..ed6444f2 100644 --- a/classes/tabs.php +++ b/classes/tabs.php @@ -23,7 +23,9 @@ namespace tool_lifecycle; +use tool_lifecycle\local\manager\delayed_courses_manager; use core\exception\moodle_exception; +use stdClass; /** * Class to generate a tab row for navigation within this plugin @@ -36,17 +38,34 @@ class tabs { /** * Generates a Moodle tabrow i.e. an array of tabs * - * @param bool $activelink display active workflows tab as link - * @param bool $deactivatelink display deactivated workflows tab as link - * @param bool $draftlink display draft workflows tab as link + * @param object $params * @return array of tabobjects * @throws \coding_exception * @throws \dml_exception * @throws moodle_exception */ - public static function get_tabrow($activelink = false, $deactivatelink = false, $draftlink = false) { + public static function get_tabrow($params = null) { global $DB; + $activelink = false; + $deactivatedlink = false; + $draftlink = false; + $approvelink = false; + if ($params !== null) { + if (isset($params->activelink)) { + $activelink = true; + } + if (isset($params->deactivatedlink)) { + $deactivatedlink = true; + } + if (isset($params->draftlink)) { + $draftlink = true; + } + if (isset($params->approvelink)) { + $approvelink = true; + } + } + $classnotnull = 'badge badge-primary badge-pill ml-1'; $classnull = 'badge badge-secondary badge-pill ml-1'; @@ -69,20 +88,21 @@ public static function get_tabrow($activelink = false, $deactivatelink = false, from {tool_lifecycle_workflow} where timeactive IS NULL AND timedeactive IS NOT NULL"; $i = $DB->count_records_sql($sql); - $deactivatedewf = \html_writer::span($i, $i > 0 ? $classnotnull : $classnull); + $deactivatedwf = \html_writer::span($i, $i > 0 ? $classnotnull : $classnull); $time = time(); // Get number of delayed courses. $sql = "select count(c.id) from {course} c LEFT JOIN - (SELECT dw.courseid, dw.workflowid, w.title as workflow, dw.delayeduntil as workflowdelay,maxtable.wfcount as workflowcount - FROM ( SELECT courseid, MAX(dw.id) AS maxid, COUNT(*) AS wfcount FROM {tool_lifecycle_delayed_workf} dw - JOIN {tool_lifecycle_workflow} w ON dw.workflowid = w.id - WHERE dw.delayeduntil >= $time AND w.timeactive IS NOT NULL GROUP BY courseid ) maxtable JOIN - {tool_lifecycle_delayed_workf} dw ON maxtable.maxid = dw.id JOIN - {tool_lifecycle_workflow} w ON dw.workflowid = w.id ) wfdelay ON wfdelay.courseid = c.id LEFT JOIN - (SELECT * FROM {tool_lifecycle_delayed} d WHERE d.delayeduntil > $time ) d ON c.id = d.courseid JOIN - {course_categories} cat ON c.category = cat.id - where COALESCE(wfdelay.courseid, d.courseid) IS NOT NULL"; + (SELECT dw.courseid, dw.workflowid, w.title as workflow, + dw.delayeduntil as workflowdelay,maxtable.wfcount as workflowcount + FROM (SELECT courseid, MAX(dw.id) AS maxid, COUNT(*) AS wfcount FROM {tool_lifecycle_delayed_workf} dw + JOIN {tool_lifecycle_workflow} w ON dw.workflowid = w.id + WHERE dw.delayeduntil >= $time AND w.timeactive IS NOT NULL GROUP BY courseid) maxtable JOIN + {tool_lifecycle_delayed_workf} dw ON maxtable.maxid = dw.id JOIN + {tool_lifecycle_workflow} w ON dw.workflowid = w.id ) wfdelay ON wfdelay.courseid = c.id LEFT JOIN + (SELECT * FROM {tool_lifecycle_delayed} d WHERE d.delayeduntil > $time ) d ON c.id = d.courseid JOIN + {course_categories} cat ON c.category = cat.id + where COALESCE(wfdelay.courseid, d.courseid) IS NOT NULL"; $i = $DB->count_records_sql($sql); $delayedcourses = \html_writer::span($i, $i > 0 ? $classnotnull : $classnull); @@ -109,12 +129,18 @@ public static function get_tabrow($activelink = false, $deactivatelink = false, $i = $DB->count_records_sql($sql); $lcerrors = \html_writer::span($i, $i > 0 ? $classnotnull : $classnull); - // General Settings and Subplugins. + // General Settings. $targeturl = new \moodle_url('/admin/settings.php', ['section' => 'lifecycle']); $tabrow[] = new \tabobject('settings', $targeturl, get_string('general_config_header', 'tool_lifecycle'), get_string('general_config_header_title', 'tool_lifecycle')); + // Subplugins. + $targeturl = new \moodle_url('/admin/tool/lifecycle/subplugins.php', ['id' => 'subplugins']); + $tabrow[] = new \tabobject('subplugins', $targeturl, + get_string('subplugins', 'tool_lifecycle'), + get_string('subpluginsdesc', 'tool_lifecycle')); + // Tab to the draft workflows page. $targeturl = new \moodle_url('/admin/tool/lifecycle/workflowdrafts.php', ['id' => 'workflowdrafts']); $tabrow[] = new \tabobject('workflowdrafts', $targeturl, @@ -130,8 +156,8 @@ public static function get_tabrow($activelink = false, $deactivatelink = false, // Tab to the deactivated workflows page. $targeturl = new \moodle_url('/admin/tool/lifecycle/deactivatedworkflows.php', ['id' => 'deactivatedworkflows']); $tabrow[] = new \tabobject('deactivatedworkflows', $targeturl, - get_string('deactivated_workflows_header', 'tool_lifecycle').$deactivatedewf, - get_string('deactivated_workflows_header_title', 'tool_lifecycle'), $deactivatelink); + get_string('deactivated_workflows_header', 'tool_lifecycle').$deactivatedwf, + get_string('deactivated_workflows_header_title', 'tool_lifecycle'), $deactivatedlink); // Tab to the delayed courses list page. $targeturl = new \moodle_url('/admin/tool/lifecycle/delayedcourses.php', ['id' => 'delayedcourses']); @@ -143,7 +169,7 @@ public static function get_tabrow($activelink = false, $deactivatelink = false, $targeturl = new \moodle_url('/admin/tool/lifecycle/step/adminapprove/index.php', ['id' => 'adminapprove']); $tabrow[] = new \tabobject('adminapprove', $targeturl, get_string('adminapprovals_header', 'tool_lifecycle').$adminapprovals, - get_string('adminapprovals_header_title', 'tool_lifecycle')); + get_string('adminapprovals_header_title', 'tool_lifecycle'), $approvelink); // Tab to the course backups list page. $targeturl = new \moodle_url('/admin/tool/lifecycle/coursebackups.php', ['id' => 'coursebackups']); diff --git a/classes/urls.php b/classes/urls.php index dec4a786..28f9fcef 100644 --- a/classes/urls.php +++ b/classes/urls.php @@ -59,5 +59,9 @@ class urls { const PROCESS_ERRORS = '/admin/tool/lifecycle/errors.php?id=errors'; /** @var string Confirmation page for bulk operations */ const CONFIRMATION = '/admin/tool/lifecycle/confirmation.php'; + /** @var string subplugins page for a list of installed subplugins */ + const SUBPLUGINS = '/admin/tool/lifecycle/subplugins.php?id=subplugins'; + /** @var string run page for execute the scheduled lifecycle task ad hoc */ + const RUN = '/admin/tool/lifecycle/run.php?id=run'; } diff --git a/classes/view_controller.php b/classes/view_controller.php index 1f93a580..189c6b6e 100644 --- a/classes/view_controller.php +++ b/classes/view_controller.php @@ -84,9 +84,9 @@ public function handle_view($renderer, $filterdata) { if ($capability && has_capability($capability, \context_course::instance($process->courseid), null, false) && !empty(interaction_manager::get_action_tools($step->subpluginname, $process->processid))) { $requiresinteraction[] = $process->courseid; - unset($remainingcourses[$process->courseid]); } } + $remainingcourses = array_diff($remainingcourses, $requiresinteraction); echo $renderer->heading(get_string('tablecoursesrequiringattention', 'tool_lifecycle'), 3); $table1 = new interaction_attention_table('tool_lifecycle_interaction', $requiresinteraction, $filterdata); diff --git a/confirmation.php b/confirmation.php index 633cea1a..67f55d84 100644 --- a/confirmation.php +++ b/confirmation.php @@ -28,8 +28,7 @@ require_once(__DIR__ . '/../../../config.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::CONFIRMATION)); diff --git a/coursebackups.php b/coursebackups.php index a275da3b..98f8d92b 100644 --- a/coursebackups.php +++ b/coursebackups.php @@ -30,8 +30,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::COURSE_BACKUPS)); diff --git a/createworkflowfromexisting.php b/createworkflowfromexisting.php index 82c226c8..c87937d0 100644 --- a/createworkflowfromexisting.php +++ b/createworkflowfromexisting.php @@ -32,8 +32,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $workflowid = optional_param('wf', null, PARAM_INT); diff --git a/db/install.xml b/db/install.xml index a2262cba..6c27f072 100644 --- a/db/install.xml +++ b/db/install.xml @@ -51,6 +51,7 @@ + @@ -105,13 +106,14 @@ - + + diff --git a/db/subplugins.json b/db/subplugins.json index e21b91d6..f5b37182 100644 --- a/db/subplugins.json +++ b/db/subplugins.json @@ -1,6 +1,10 @@ { + "subplugintypes": { + "lifecycletrigger": "trigger", + "lifecyclestep": "step" + }, "plugintypes" : { - "lifecycletrigger" : "admin\/tool\/lifecycle\/trigger", - "lifecyclestep" : "admin\/tool\/lifecycle\/step" + "lifecycletrigger" : "admin/tool/lifecycle/trigger", + "lifecyclestep" : "admin/tool/lifecycle/step" } -} \ No newline at end of file +} diff --git a/db/upgrade.php b/db/upgrade.php index 2a0e4ad6..14475a69 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -22,7 +22,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use tool_lifecycle\local\entity\process; use tool_lifecycle\local\manager\lib_manager; +use tool_lifecycle\local\manager\process_manager; use tool_lifecycle\local\manager\trigger_manager; use tool_lifecycle\local\manager\workflow_manager; @@ -455,8 +457,8 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { "WHERE c.id IS NULL"; $processes = $DB->get_records_sql($sql); foreach ($processes as $procrecord) { - $process = \tool_lifecycle\local\entity\process::from_record($procrecord); - \tool_lifecycle\local\manager\process_manager::abort_process($process); + $process = process::from_record($procrecord); + process_manager::abort_process($process); } upgrade_plugin_savepoint(true, 2020091800, 'tool', 'lifecycle'); @@ -502,7 +504,16 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { } - if ($oldversion < 2025050400) { + if ($oldversion < 2025050405) { + + // Define field manual to be renamed to manually in table tool_lifecycle_workflow. + $table = new xmldb_table('tool_lifecycle_workflow'); + $field = new xmldb_field('manual', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'sortindex'); + + // Rename field manual to manually. + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'manually'); + } // Changing precision of field instancename on table tool_lifecycle_trigger to (100). $table = new xmldb_table('tool_lifecycle_trigger'); @@ -560,8 +571,8 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { // Define field "includesitecourse" to be added to tool_lifecycle_workflow. $table = new xmldb_table('tool_lifecycle_workflow'); - $field = new xmldb_field('includesitecourse', XMLDB_TYPE_INTEGER, '5', null, null, null, '0', 'delayforallworkflows'); + $field = new xmldb_field('includesitecourse', XMLDB_TYPE_INTEGER, '5', null, null, null, '0', 'delayforallworkflows'); // Conditionally launch add field "includesitecourse". if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); @@ -569,12 +580,18 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { // Define field "includedelayedcourses" to be added to tool_lifecycle_workflow. $field = new xmldb_field('includedelayedcourses', XMLDB_TYPE_INTEGER, '5', null, null, null, '0', 'includesitecourse'); - // Conditionally add field "includedelayedcourses". if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } + // Define field "andor" to be added to tool_lifecycle_workflow. + $field = new xmldb_field('andor', XMLDB_TYPE_INTEGER, '1', null, null, null, '0', 'includedelayedcourses'); + // Conditionally add field "andor". + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $triggers = trigger_manager::get_instances('delayedcourses'); foreach ($triggers as $trigger) { workflow_manager::remove($trigger->workflowid); @@ -584,7 +601,15 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { workflow_manager::remove($trigger->workflowid); } - upgrade_plugin_savepoint(true, 2025050400, 'tool', 'lifecycle'); + $table = new xmldb_table('tool_lifecycle_step'); + $field = new xmldb_field('rollbacktosortindex', XMLDB_TYPE_INTEGER, '5', null, null, null, null, 'delaytype'); + + // Conditionally create the field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_plugin_savepoint(true, 2025050405, 'tool', 'lifecycle'); } diff --git a/deactivatedworkflows.php b/deactivatedworkflows.php index e45f6d4a..71ef6bd0 100644 --- a/deactivatedworkflows.php +++ b/deactivatedworkflows.php @@ -31,8 +31,7 @@ use tool_lifecycle\tabs; use tool_lifecycle\urls; -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::DEACTIVATED_WORKFLOWS)); diff --git a/delayedcourses.php b/delayedcourses.php index 8a589950..60b16472 100644 --- a/delayedcourses.php +++ b/delayedcourses.php @@ -31,11 +31,12 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); + +$wfid = optional_param('wfid', 0, PARAM_INT); $syscontext = context_system::instance(); -$PAGE->set_url(new \moodle_url(urls::DELAYED_COURSES)); +$PAGE->set_url(new \moodle_url(urls::DELAYED_COURSES, ['wfid' => $wfid])); $PAGE->set_context($syscontext); // Action handling (delete, bulk-delete). @@ -133,7 +134,9 @@ $heading = get_string('pluginname', 'tool_lifecycle')." / ".get_string('delayed_courses_header', 'tool_lifecycle'); echo $renderer->header($heading); -$tabrow = tabs::get_tabrow(); +$tabparams = new stdClass(); +$tabparams->wfid = $wfid; +$tabrow = tabs::get_tabrow($tabparams); $id = optional_param('id', 'settings', PARAM_TEXT); $renderer->tabs($tabrow, $id); @@ -176,12 +179,19 @@ if ($data) { $params = array_merge($params, (array) $data); } - $button = new single_button(new moodle_url('confirmation.php'), + $button = new single_button(new moodle_url('confirmation.php', $params), get_string('delete_all_delays', 'tool_lifecycle')); echo $OUTPUT->render($button); + $classnotnull = 'badge badge-primary badge-pill ml-1'; + $classnull = 'badge badge-secondary badge-pill ml-1'; + echo \html_writer::span($delayedcourses, $delayedcourses > 0 ? $classnotnull : $classnull); echo html_writer::div('', 'mb-2'); } $table->out(100, false); +if ($table->totalrows) { + echo \html_writer::div($table->totalrows." ".get_string('courses'), 'mt-3'); +} + echo $OUTPUT->footer(); diff --git a/editelement.php b/editelement.php index ad63d8ff..1fa4c887 100644 --- a/editelement.php +++ b/editelement.php @@ -38,8 +38,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $type = required_param('type', PARAM_ALPHA); $elementid = optional_param('elementid', null, PARAM_INT); @@ -121,6 +120,9 @@ if (isset($data->instancename)) { $element->instancename = $data->instancename; } + if (isset($data->rollbacktosortindex)) { + $element->rollbacktosortindex = $data->rollbacktosortindex; + } } else { $element = step_subplugin::from_record($data); } @@ -135,7 +137,9 @@ $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); foreach ($triggers as $trigger) { if ($trigger->subpluginname == $data->subpluginname) { - throw new coding_exception('Only one instance of each trigger type allowed!'); + if (!trigger_manager::trigger_multipleuse($trigger->subpluginname)) { + throw new coding_exception('Only one instance of each trigger type allowed!'); + } } } $element = trigger_subplugin::from_record($data); diff --git a/editworkflow.php b/editworkflow.php index da361db0..ad79970d 100644 --- a/editworkflow.php +++ b/editworkflow.php @@ -32,8 +32,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_context($syscontext); @@ -76,6 +75,7 @@ $workflow->delayforallworkflows = property_exists($data, 'delayforallworkflows') ? $data->delayforallworkflows : 0; $workflow->includedelayedcourses = $data->includedelayedcourses; $workflow->includesitecourse = $data->includesitecourse; + $workflow->andor = $data->andorgroup['andor']; $newworkflow = false; } else { $workflow = workflow::from_record($data); diff --git a/errors.php b/errors.php index 93857e4d..04cc5609 100644 --- a/errors.php +++ b/errors.php @@ -23,6 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use tool_lifecycle\local\form\form_errors_filter; use tool_lifecycle\local\manager\process_manager; use tool_lifecycle\local\table\process_errors_table; use tool_lifecycle\tabs; @@ -31,8 +32,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::PROCESS_ERRORS)); @@ -52,15 +52,38 @@ foreach ($ids as $id) { process_manager::rollback_process_after_error($id); } - } else { - throw new coding_exception("action must be either 'proceed' or 'rollback'"); + } else if ($action == 'delete') { + foreach ($ids as $id) { + $DB->delete_records('tool_lifecycle_proc_error', ['id' => $id]); + } } - redirect($PAGE->url); + redirect($PAGE->url, get_string('deleteprocesserrormsg', 'tool_lifecycle'), 3); } $PAGE->set_pagetype('admin-setting-' . 'tool_lifecycle'); $PAGE->set_pagelayout('admin'); +// Get selected filter form options if there are any. +$workflow = optional_param('workflow', 0, PARAM_INT); +$course = optional_param('course', 0, PARAM_INT); +$step = optional_param('step', 0, PARAM_INT); +// Load filter form. +$mform = new form_errors_filter($PAGE->url, ['workflow' => $workflow, 'course' => $course, 'step' => $step]); + +// Cache handling. +$cache = cache::make('tool_lifecycle', 'mformdata'); +if ($mform->is_cancelled()) { + $cache->delete('errors_filter'); + redirect($PAGE->url); +} else if ($data = $mform->get_data()) { + $cache->set('errors_filter', $data); +} else { + $data = $cache->get('errors_filter'); + if ($data) { + $mform->set_data($data); + } +} + $renderer = $PAGE->get_renderer('tool_lifecycle'); $heading = get_string('pluginname', 'tool_lifecycle')." / ".get_string('process_errors_header', 'tool_lifecycle'); @@ -69,11 +92,22 @@ $id = optional_param('id', 'settings', PARAM_TEXT); $renderer->tabs($tabrow, $id); -$table = new process_errors_table(); +// Get number of process errors. +$sql = "select count(c.id) from {tool_lifecycle_proc_error} pe + LEFT JOIN {tool_lifecycle_workflow} w ON pe.workflowid = w.id + LEFT JOIN {tool_lifecycle_step} s ON pe.workflowid = s.workflowid AND pe.stepindex = s.sortindex + LEFT JOIN {course} c ON pe.courseid = c.id"; +$errors = $DB->count_records_sql($sql); + +$table = new process_errors_table($data); $table->define_baseurl($PAGE->url); $PAGE->requires->js_call_amd('tool_lifecycle/tablebulkactions', 'init'); +if ($errors > 0) { + $mform->display(); +} + $table->out(100, false); echo $OUTPUT->footer(); diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index ca346201..090ab369 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -49,10 +49,13 @@ $string['adminsettings_notriggers'] = 'Keine Trigger-Subplugins installiert'; $string['adminsettings_workflow_definition_steps_heading'] = 'Workflowschritte'; $string['all_delays'] = 'Alle Verzögerungen'; +$string['alreadyinprocess'] = 'Bereits in Verarbeitung'; +$string['alreadyinprocessotherworkflow'] = 'Bereits in einem anderen Workflow'; +$string['andor'] = 'Trigger-Verknüpfungstyp (experimentell)'; +$string['andor_help'] = 'Kombiniere die Trigger mit einer UND- oder ODER-Verknüpfung.'; $string['anonymous_user'] = 'Anonyme:r Nutzer:in'; $string['apply'] = 'Anwenden'; $string['backupcreated'] = 'Erstellt am'; -$string['backupworkflow'] = 'Workflow sichern'; $string['cachedef_application'] = 'Cache für das Kurs-Menü wenn es aktivierte Workflows gibt.'; $string['cachedef_mformdata'] = 'Cached die mform-Daten.'; $string['cannot_trigger_workflow_manually'] = 'Der Workflow konnte nicht manuell ausgelöst werden.'; @@ -79,9 +82,11 @@ $string['coursename'] = 'Kursname'; $string['courseproceeded'] = 'Ein Kurs wurde dem nächsten Verarbeitungsschritt zugeführt.'; $string['courserolledback'] = 'Ein Kurs wurde zurückgesetzt.'; -$string['courses_are_alreadyin'] = '{$a} Kurse befinden sich schon in diesem Prozess'; +$string['courses_are_alreadyin'] = '{$a} Kurse befinden sich schon in einem Prozess oder in der Prozessfehlerliste.'; $string['courses_are_delayed'] = '{$a} Kurse sind verzögert'; -$string['courses_are_used_total'] = '{$a} Kurse bereits in anderem Prozess'; +$string['courses_are_used_total'] = '{$a} Kurse werden bereits in einem anderen Workflow verarbeitet'; +$string['courses_candidates_alreadyin'] = ', weitere {$a} Kandidaten sind bereits in einem Prozess oder in der Prozessfehlerliste.'; +$string['courses_candidates_delayed'] = ', weitere {$a} Kandidaten sind verzögert'; $string['courses_excluded'] = 'Kurse insgesamt ausgeschlossen: {$a}'; $string['courses_size'] = 'Kurse insgesamt genauer betrachtet: {$a}'; $string['courses_triggered'] = 'Kurse insgesamt getriggered: {$a}'; @@ -90,20 +95,20 @@ $string['courses_will_be_triggered'] = '{$a} Kurse sind in der Auswahl'; $string['courses_will_be_triggered_total'] = '{$a} Kurse werden insgesamt getriggert'; $string['courses_will_be_triggered_total_without_amount'] = ' Kurse werden insgesamt getriggert'; -$string['coursesdelayed'] = 'Verzögerte Kurse des Workflows \'{$a}\''; +$string['coursesdelayed'] = 'Verzögerte Kurse des Workflows \'{$a->title}\''; $string['courseselected'] = 'Ein Kurs wurde dem Verarbeitungsprozess hinzugefügt.'; $string['courseselection_title'] = 'Kurs-Selektion'; $string['courseselectionrun_title'] = 'Nächster Selektionslauf'; -$string['coursesexcluded'] = 'Ausgeschlossene Kurse für Trigger \'{$a}\''; +$string['coursesexcluded'] = 'Ausgeschlossene Kurse für Trigger \'{$a->title}\''; +$string['coursesinprocess'] = 'Kurse schon in einem Prozess des Workflows \'{$a->title}\''; $string['coursesinstep'] = 'Kurse im Schritt \'{$a}\''; -$string['coursestriggered'] = 'Getriggerte Kurse für Trigger \'{$a}\''; -$string['coursestriggeredworkflow'] = 'Getriggerte Kurse für diesen Workflow \'{$a}\''; -$string['coursesused'] = 'Kurse schon in einem anderen Prozess für Workflow \'{$a}\''; +$string['coursestriggered'] = 'Kursauswahl durch Trigger \'{$a->title}\''; +$string['coursestriggeredworkflow'] = 'Getriggerte Kurse für diesen Workflow \'{$a->title}\''; +$string['coursesused'] = 'Kurse bereits in der Verarbeitung eines anderen Workflows'; $string['create_copy'] = 'Kopie erstellen'; $string['create_step'] = 'Step erstellen'; $string['create_trigger'] = 'Trigger erstellen'; $string['create_workflow_from_existing'] = 'Kopie von bestehendem Workflow erstellen'; -$string['customfieldsemesterdescription'] = 'Löst Kurse gemäß dem benutzerdefinierten Kursfeld \'semester\' aus.'; $string['date'] = 'Fällligkeitsdatum'; $string['deactivated'] = 'Deaktiviert'; $string['deactivated_workflows_header'] = 'Inaktive'; @@ -111,6 +116,7 @@ $string['deactivated_workflows_list'] = 'Zeige deaktivierte Workflows'; $string['deactivated_workflows_list_header'] = 'Deaktivierte Workflows'; $string['delaydeleted'] = 'Eine Kurs-Verzögerung wurde gelöscht.'; +$string['delayed'] = 'Verzögert'; $string['delayed_courses_header'] = 'Verzögerungen'; $string['delayed_courses_header_title'] = 'Liste von allen verzögerten Kursen'; $string['delayed_for_workflow_until'] = 'Verzögert für "{$a->name}" bis {$a->date}'; @@ -124,6 +130,8 @@ $string['delete_all_delays'] = 'Alle Verzögerungen löschen'; $string['delete_all_delays_confirmation'] = 'Sie sind dabei, alle Verzögerungen zu löschen. Sind sie sicher?'; $string['delete_delay'] = 'Verzögerung löschen'; +$string['deleteprocesserror'] = 'Prozessfehlereintrag löschen'; +$string['deleteprocesserrormsg'] = 'Prozessfehlereintrag gelöscht.'; $string['deleteworkflow'] = 'Workflow löschen'; $string['deleteworkflow_confirm'] = 'Sie sind dabei, den Workflow zu löschen. Das kann nicht rückgängig gemacht werden. Sind Sie sicher?'; $string['details:finishdelay'] = 'Verzögerung nach Abschluß'; @@ -134,8 +142,10 @@ $string['details:rollbackdelay_help'] = 'Nachdem ein Kurs zurückgesetzt wird, wird er für den angegebenen Zeitraum verzögert.'; $string['disableworkflow'] = 'Workflow deaktivieren (Prozesse laufen weiter)'; $string['disableworkflow_confirm'] = 'Sie sind dabei, den Workflow zu deaktivieren. Sind Sie sicher?'; +$string['documentationlink'] = 'Dokumentation auf github'; $string['dontshowextendeddetails'] = 'Zeige detaillierte Workflow-Informationen nicht an'; $string['download'] = 'Herunterladen'; +$string['downloadworkflow'] = 'Workflow-Definition herunterladen'; $string['draft'] = 'Entwurf'; $string['duplicateworkflow'] = 'Workflow duplizieren'; $string['edit_step'] = 'Step bearbeiten'; @@ -146,8 +156,10 @@ Ihnen fehlen wahrscheinlich die Berechtigung dazu. Bitte überprüfen Sie den Pfad unter Seitenadministration/Plugins/Dienstprogramme/Kurs-Lebenszyklus/Allgemein & Subplugins."; $string['errornobackup'] = "Es wurde kein Backup in dem angegebenen Pfad erstellt."; +$string['errortime'] = 'Fehler-Zeitpunkt'; +$string['excludedbycoursecode'] = 'Durch den check-course Code ausgeschlossen'; $string['find_course_list_header'] = 'Kurse finden'; -$string['finished'] = 'Beendet'; +$string['finished'] = 'Fortgeführt/Beendet'; $string['followedby_none'] = 'Keine'; $string['force_import'] = 'Die Fehler ignorieren und den Workflow trotzdem importieren. Das kann zu unerwünschten Effekten führen.'; $string['forselected'] = 'Für alle ausgewählten Prozesse'; @@ -158,12 +170,12 @@ $string['globally_until_date'] = 'Global bis {$a}'; $string['includedelayedcourses'] = 'Verzögerte Kurse einschliessen'; $string['includedelayedcourses_help'] = 'Mit dieser Option werden verzögerte Kurse (ob global oder für den Workflow) in die Verarbeitung dieses Workflows eingeschlossen.'; -$string['includesitecourse'] = 'Den Site-Kurs einschliessen'; +$string['includesitecourse'] = 'Site-Kurs einschliessen'; $string['includesitecourse_help'] = 'Mit dieser Option wird der Startkurs (Kurs mit der ID=1, "Home") in die Verarbeitung dieses Workflows eingeschlossen.'; $string['interaction_success'] = 'Aktion erfolgreich gespeichert.'; $string['invalid_workflow'] = 'Workflowkonfiguration noch nicht aktivierbar'; $string['invalid_workflow_cannot_be_activated'] = 'Der Workflow kann noch nicht aktiviert werden, da die Workflowdefinition ungültig ist.'; -$string['invalid_workflow_details'] = 'Erstelle mindestens einen Trigger und einen Schritt für diesen Workflow.'; +$string['invalid_workflow_details'] = 'Füge mindestens einen Trigger und einen Schritt in diesen Workflow ein.'; $string['lastaction'] = 'Letzte Aktion am'; $string['lastrun'] = 'Letzte Durchführung: {$a}'; $string['lifecycle:managecourses'] = 'Darf Kurse in tool_lifecycle verwalten.'; @@ -171,7 +183,7 @@ $string['lifecycle_task'] = 'Führt den Lifecycle-Prozess aus.'; $string['lifecyclestep'] = 'Schritt'; $string['lifecycletrigger'] = 'Trigger'; -$string['managecourses_link'] = 'Kurse verwalten'; +$string['managecourses_link'] = 'Lifecycle - Kurse verwalten'; $string['manual_trigger_process_existed'] = 'Es existiert bereits ein Workflow für diesen Kurs.'; $string['manual_trigger_success'] = 'Workflow erfolgreich gestartet.'; $string['manualtriggerenvolved'] = 'Ein manueller Trigger ist vorhanden.'; @@ -185,21 +197,23 @@ $string['nointeractioninterface'] = 'Keine Interaktionsschnittstelle verfügbar!'; $string['noprocesserrors'] = 'Es gibt keine fehlerhaften Prozesse, die behandelt werden müssen!'; $string['noprocessfound'] = 'Es konnte kein Prozess mit der gegebenen Prozessid gefunden werden!'; -$string['noremainingcoursestodisplay'] = 'Es gibt derzeit keine verbleibenden Kurse!'; +$string['noremainingcoursestodisplay'] = 'Derzeit befindet sich kein Kurs in einem Lifecycle Prozess!'; $string['nostepfound'] = 'Es konnte kein Schritt mit der gegebenen Schritt-ID gefunden werden!'; $string['notifyerrorsemailcontent'] = '{$a->amount} neue fehlerhafte tool_lifecycle Prozesse warten darauf, behandelt zu werden! Bitte besuchen Sie {$a->url}.'; $string['notifyerrorsemailcontenthtml'] = '{$a->amount} neue fehlerhafte tool_lifecycle Prozesse warten darauf, behandelt zu werden!
Bitte besuchen Sie die Übersichtsseite.'; $string['notifyerrorsemailsubject'] = '{$a->amount} neue fehlerhafte tool_lifecycle Prozesse warten darauf, behandelt zu werden!'; +$string['numbersotherwfordelayed'] = ' / {$a->otherwf} in anderen Worfklows / {$a->delayed} verzögert'; $string['overview:add_trigger'] = 'Trigger hinzufügen'; $string['overview:add_trigger_help'] = 'Es kann nur eine Instanz jedes Triggertyps hinzugefügt werden.'; $string['overview:timetrigger_help'] = 'Wann sollte der Kursselektionsjob für diesen Workflow das nächste Mal laufen?'; $string['overview:trigger'] = 'Trigger'; -$string['overview:trigger_help'] = 'Die getriggerten Kurse würden der Verarbeitung dieses Worklows hinzugefügt werden wenn der Kursselektions-Lauf jetzt zur Durchführung käme. -Ein Kurs fängt nur dann an, einen Workflow zu durchlaufen, wenn alle Trigger des Workflows dies übereinstimmend (UND-Verknüpfung) aussagen. -In den hier genannten Zahlen werden Kurse, die verzögert werden oder sich bereits in anderen Workflows befinden, nicht mitgezählt. -Trotzdem sind die Zahlen nur approximiert, da es sein könnte, dass die Kurse vor diesem einen anderen Workflow auslösen.'; +$string['overview:trigger_help'] = 'Die getriggerten Kurse würden der Verarbeitung dieses Workflows hinzugefügt werden wenn der Kursselektions-Lauf jetzt durchgeführt würde. +Ein Kurs wird je nach Workflow-Konfiguration der Verarbeitung zugeführt wenn alle Trigger feuern (AND) oder wenn er zumindest von einem Trigger inkludiert wird (OR). +Trotzdem sind die Zahlen nur Näherungswerte, da der Kurs z.Bsp. schon von einem höher gereihten Workflow exkludiert oder verarbeitet werden könnte.'; +$string['overview:trigger_info'] = 'Hier wird die Anzahl der Kurse ausgewiesen, die durch den Trigger ausgelöst würden, abzüglich der Kurse, die vom Workflow schon verarbeitet werden oder - je nach Workflow-Einstellung - verzögert sind. +Beim Mouseover sehen Sie nähere Informationen zum aktuellen Stand des Triggers.'; $string['pluginname'] = 'Kurs-Lebenszyklus'; $string['plugintitle'] = 'Kurs-Lebenszyklus'; $string['privacy:metadata:tool_lifecycle_action_log'] = 'Ein Log von Aktionen des Kursmanagers.'; @@ -211,6 +225,7 @@ $string['privacy:metadata:tool_lifecycle_action_log:userid'] = 'ID der/des Benutzer:in, der/die die Aktion angestoßen hat.'; $string['privacy:metadata:tool_lifecycle_action_log:workflowid'] = 'ID des Worflows innerhalb dessen die Aktion durchgeführt wurde.'; $string['proceed'] = 'Fortfahren'; +$string['process_error'] = 'Prozess-Fehler'; $string['process_errors_header'] = 'Fehler'; $string['process_errors_header_title'] = 'Liste der Bearbeitungsfehler'; $string['process_proceeded_event'] = 'Ein Prozess wurde fortgeführt'; @@ -224,7 +239,12 @@ $string['restore_trigger_does_not_exist'] = 'Der Trigger {$a} ist nicht installiert, aber in der Sicherungsdatei enthalten. Bitte installieren Sie ihn zuerst und versuchen es dann erneut.'; $string['restore_workflow_not_found'] = 'Falsches Format der Sicherungsdatei. Der Workflow konnte nicht gefunden werden.'; $string['rollback'] = 'Zurücksetzung'; +$string['rollbacktosortindex'] = 'Zu diesem Schritt zurücksetzen (optional)'; +$string['rollbacktosortindex_help'] = 'Wenn es zu einem Zurücksetzen dieses Schritts kommen sollte wird zu dem hier angegebenen Schritt züruckgesetzt und nicht bis zum ersten Schritt des Workflows. Der Prozess wird nicht entfernt.'; +$string['rollbacktotitle'] = 'Zurücksetzung zu Schritt \'{$a}\''; $string['rolledback'] = 'Zurückgesetzt'; +$string['run'] = 'Ausführen'; +$string['runtask'] = 'Führe Lifecycle System-Task aus'; $string['searchcourses'] = 'Kurs-Suche'; $string['see_in_workflow'] = 'In Workflow ansehen'; $string['show_delays'] = 'Wähle die Ansicht'; @@ -263,15 +283,15 @@ $string['trigger_subpluginname'] = 'Trigger Typ'; $string['trigger_subpluginname_help'] = 'Name des Schritt/Trigger-Subplugins (nur für Admins sichtbar).'; $string['trigger_workflow'] = 'Workflow'; -$string['triggers_installed'] = 'Installierte Trigger'; +$string['triggers_installed'] = 'Installierte Trigger-Subplugins'; $string['triggertype_settings_header'] = 'Spezifische Einstellungen des Triggertypen'; $string['type'] = 'Typ'; $string['upload_workflow'] = 'Workflow hochladen'; -$string['viewheading'] = 'Kurse verwalten'; -$string['viewsteps'] = 'Zeige Workflowschritte'; +$string['viewheading'] = 'Lifecycle - Kurse verwalten'; +$string['viewsteps'] = 'Zeige Workflow-Schritte'; $string['workflow'] = 'Workflow'; $string['workflow_active'] = 'Aktiv'; -$string['workflow_definition_heading'] = 'Workflowdefinitionen'; +$string['workflow_definition_heading'] = 'Workflow-Definitionen'; $string['workflow_delayforallworkflows'] = 'Ausschluss für alle Workflows?'; $string['workflow_delayforallworkflows_help'] = 'Falls ja, wird ein Kurs für die oben genannte Zeit nicht nur von diesem, sondern von allen Workflows ausgeschlossen. Das heißt, bis die Zeit abgelaufen ist, kann kein Prozess für den Kurs gestartet werden.'; @@ -287,6 +307,7 @@ $string['workflow_is_running'] = 'Workflow läuft.'; $string['workflow_not_removeable'] = 'Es ist nicht möglich, diese Workflow-Instanz zu entfernen. Vielleicht hat sie noch laufende Prozesse?'; $string['workflow_processes'] = 'Aktive Prozesse'; +$string['workflow_processesanderrors'] = 'Aktive Prozesse und Prozessfehler des Workflows'; $string['workflow_rollbackdelay'] = 'Kursauschluss beim Zurücksetzen'; $string['workflow_rollbackdelay_help'] = 'Dieser Wert beschreibt die Zeit, bis wieder ein Prozess für diesen Workflow und einen Kurs gestartet werden kann, nachdem der Kurs innerhalb eines Prozesses dieses Workflows zurückgesetzt wurde.'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 0db8b4d7..5ede3984 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -49,10 +49,13 @@ $string['adminsettings_notriggers'] = 'No trigger subplugins installed'; $string['adminsettings_workflow_definition_steps_heading'] = 'Workflow steps'; $string['all_delays'] = 'All delays'; +$string['alreadyinprocess'] = 'Already in process'; +$string['alreadyinprocessotherworkflow'] = 'Already in process of another workflow'; +$string['andor'] = 'Combine triggers (experimental)'; +$string['andor_help'] = 'Combine triggers by conjunction(AND) or disjunction(OR).'; $string['anonymous_user'] = 'Anonymous User'; $string['apply'] = 'Apply'; $string['backupcreated'] = 'Created at'; -$string['backupworkflow'] = 'Backup workflow'; $string['cachedef_application'] = 'Cache for course menu if there are workflows enabled.'; $string['cachedef_mformdata'] = 'Caches the mform data.'; $string['cannot_trigger_workflow_manually'] = 'The requested workflow could not be triggered manually.'; @@ -79,10 +82,12 @@ $string['coursename'] = 'Course name'; $string['courseproceeded'] = 'One course has been proceeded.'; $string['courserolledback'] = 'One course has been rolled back.'; -$string['courses_are_alreadyin'] = '{$a} courses are already part of this process'; +$string['courses_are_alreadyin'] = '{$a} courses are already part of a process or of the process errors list'; $string['courses_are_delayed'] = '{$a} delayed courses'; $string['courses_are_delayed_total'] = '{$a} delayed courses in total'; -$string['courses_are_used_total'] = '{$a} courses in another process'; +$string['courses_are_used_total'] = '{$a} courses already processed by another workflow'; +$string['courses_candidates_alreadyin'] = ', another {$a} candidates are already part of a process or of the process errors list'; +$string['courses_candidates_delayed'] = ', another {$a} candidates are delayed'; $string['courses_excluded'] = 'Courses excluded total: {$a}'; $string['courses_size'] = 'Courses checked total: {$a}'; $string['courses_triggered'] = 'Courses triggered total: {$a}'; @@ -91,20 +96,20 @@ $string['courses_will_be_triggered'] = '{$a} courses are in selection'; $string['courses_will_be_triggered_total'] = '{$a} courses will be triggered in total'; $string['courses_will_be_triggered_total_without_amount'] = ' courses will be triggered in total'; -$string['coursesdelayed'] = 'Courses delayed for workflow \'{$a}\''; +$string['coursesdelayed'] = 'Courses delayed for workflow \'{$a->title}\''; $string['courseselected'] = 'One course has been added to the process.'; $string['courseselection_title'] = 'Course Selection'; $string['courseselectionrun_title'] = 'Course Selection Run'; -$string['coursesexcluded'] = 'Courses excluded by trigger \'{$a}\''; +$string['coursesexcluded'] = 'Courses excluded by trigger \'{$a->title}\''; +$string['coursesinprocess'] = 'Courses already in a process of the workflow \'{$a->title}\''; $string['coursesinstep'] = 'Courses in step \'{$a}\''; -$string['coursestriggered'] = 'Courses triggered by trigger \'{$a}\''; -$string['coursestriggeredworkflow'] = 'Courses triggered for this workflow \'{$a}\''; -$string['coursesused'] = 'Courses already used in another process for workflow \'{$a}\''; +$string['coursestriggered'] = 'Courses selected by trigger \'{$a->title}\''; +$string['coursestriggeredworkflow'] = 'Courses triggered for this workflow \'{$a->title}\''; +$string['coursesused'] = 'Courses already processed by another workflow'; $string['create_copy'] = 'Create copy'; $string['create_step'] = 'Create step'; $string['create_trigger'] = 'Create trigger'; $string['create_workflow_from_existing'] = 'Copy new workflow from existing'; -$string['customfieldsemesterdescription'] = 'Trigger courses by the course custom field \'semester\''; $string['date'] = 'Due date'; $string['deactivated'] = 'Deactivated'; $string['deactivated_workflows_header'] = 'Deactivated'; @@ -112,6 +117,7 @@ $string['deactivated_workflows_list'] = 'List deactivated workflows'; $string['deactivated_workflows_list_header'] = 'Deactivated workflows'; $string['delaydeleted'] = 'One course delay deleted.'; +$string['delayed'] = 'Delayed'; $string['delayed_courses_header'] = 'Delayed'; $string['delayed_courses_header_title'] = 'List of all delayed courses'; $string['delayed_for_workflow_until'] = 'Delayed for "{$a->name}" until {$a->date}'; @@ -125,6 +131,8 @@ $string['delete_all_delays'] = 'Delete all delays'; $string['delete_all_delays_confirmation'] = 'This will delete all delays. Are you sure?'; $string['delete_delay'] = 'Delete delay'; +$string['deleteprocesserror'] = 'Delete process error entry'; +$string['deleteprocesserrormsg'] = 'Process error entry deleted.'; $string['deleteworkflow'] = 'Delete workflow'; $string['deleteworkflow_confirm'] = 'The workflow is going to be deleted. This can\'t be undone. Are you sure?'; $string['details:finishdelay'] = 'Finish delay'; @@ -135,8 +143,10 @@ $string['details:rollbackdelay_help'] = 'When a course is rolled back, the time it will be delayed for.'; $string['disableworkflow'] = 'Disable workflow (processes keep running)'; $string['disableworkflow_confirm'] = 'The workflow is going to be disabled. Are you sure?'; +$string['documentationlink'] = 'Documentation at github'; $string['dontshowextendeddetails'] = 'Do not show extended workflow details'; $string['download'] = 'Download'; +$string['downloadworkflow'] = 'Download workflow definition'; $string['draft'] = 'Draft'; $string['duplicateworkflow'] = 'Duplicate workflow'; $string['edit_step'] = 'Edit step'; @@ -146,8 +156,10 @@ $string['errorbackuppath'] = 'Error while trying to create the backup directory. You might be missing the permission to do so. Please check your path at Site administration/Plugins/Admin tools/Life Cycle/General & subplugins/backup_path.'; $string['errornobackup'] = 'No backup was created at the specified directory, reasons unknown.'; +$string['errortime'] = 'Time of Error'; +$string['excludedbycoursecode'] = 'Not triggered because of the check-course code'; $string['find_course_list_header'] = 'Find courses'; -$string['finished'] = 'Finished'; +$string['finished'] = 'Proceeded/Finished'; $string['followedby_none'] = 'None'; $string['force_import'] = 'Try ignoring errors and import the workflow anyway. Use this at your own risk!'; $string['forselected'] = 'For all selected processes'; @@ -163,7 +175,7 @@ $string['interaction_success'] = 'Action successfully saved.'; $string['invalid_workflow'] = 'Workflow configuration not valid yet.'; $string['invalid_workflow_cannot_be_activated'] = 'The workflow definition is not valid yet, thus it cannot be activated.'; -$string['invalid_workflow_details'] = 'Create at least one trigger and one step for this workflow'; +$string['invalid_workflow_details'] = 'Add at least one trigger and one step for this workflow'; $string['lastaction'] = 'Last action on'; $string['lastrun'] = 'Last run: {$a}'; $string['lifecycle:managecourses'] = 'May manage courses in tool_lifecycle'; @@ -172,7 +184,7 @@ $string['lifecycle_task'] = 'Run the life cycle processes'; $string['lifecyclestep'] = 'Process step'; $string['lifecycletrigger'] = 'Trigger'; -$string['managecourses_link'] = 'Manage courses'; +$string['managecourses_link'] = 'Lifecycle - Manage courses'; $string['manual_trigger_process_existed'] = 'A workflow for this course already exists.'; $string['manual_trigger_success'] = 'Workflow started successfully.'; $string['manualtriggerenvolved'] = 'Manual trigger envolved.'; @@ -186,20 +198,23 @@ $string['nointeractioninterface'] = 'No interaction interface available!'; $string['noprocesserrors'] = 'There are no process errors to handle!'; $string['noprocessfound'] = 'A process with the given processid could not be found!'; -$string['noremainingcoursestodisplay'] = 'There are currently no remaining courses!'; +$string['noremainingcoursestodisplay'] = 'There are currently no courses in a lifecycle process!'; $string['nostepfound'] = 'A step with the given stepid could not be found!'; $string['notifyerrorsemailcontent'] = 'There are {$a->amount} new tool_lifecycle process errors waiting to be fixed! Please review them at {$a->url}.'; $string['notifyerrorsemailcontenthtml'] = 'There are {$a->amount} new tool_lifecycle process errors waiting to be fixed!
Please review them at the error handling overview.'; $string['notifyerrorsemailsubject'] = 'There are {$a->amount} new tool_lifecycle process errors waiting to be fixed!'; +$string['numbersotherwfordelayed'] = ' / {$a->otherwf} in other workflows / {$a->delayed} delayed'; $string['overview:add_trigger'] = 'Add trigger'; $string['overview:add_trigger_help'] = 'You can only add one instance of each trigger type.'; $string['overview:timetrigger_help'] = 'When should the next course selection for this workflow takes place?'; $string['overview:trigger'] = 'Trigger'; $string['overview:trigger_help'] = 'The triggered courses would be added to the workflow process if the course selection would run now. -A course will only trigger a workflow, if all triggers agree on it (AND operation). Courses which are delayed, or already in another workflow are not included in the displayed counts. +According to your workflow configuration courses will be triggered if all triggered agree on it (AND) or if at least one trigger agrees (OR). Still, these numbers are only approximates, since it could be that a course is excluded by another workflow, or will trigger another workflow before this one.'; +$string['overview:trigger_info'] = 'The number of courses which would be triggered by this trigger and are not already processed by this workflow and - regarding to the workflow configuration - are not delayed. +See the tooltip for more details.'; $string['pluginname'] = 'Life Cycle'; $string['plugintitle'] = 'Course Life Cycle'; $string['privacy:metadata:tool_lifecycle_action_log'] = 'A log of actions done by course managers.'; @@ -211,6 +226,7 @@ $string['privacy:metadata:tool_lifecycle_action_log:userid'] = 'ID of the user that did the action.'; $string['privacy:metadata:tool_lifecycle_action_log:workflowid'] = 'ID of the Workflow the action was done in.'; $string['proceed'] = 'Proceed'; +$string['process_error'] = 'Process Error'; $string['process_errors_header'] = 'Errors'; $string['process_errors_header_title'] = 'List of process errors'; $string['process_proceeded_event'] = 'A process has been proceeded'; @@ -224,7 +240,12 @@ $string['restore_trigger_does_not_exist'] = 'The trigger {$a} is not installed, but is included in the backup file. Please installed it first and try again.'; $string['restore_workflow_not_found'] = 'Wrong format of the backup file. The workflow could not be found.'; $string['rollback'] = 'Rollback'; +$string['rollbacktosortindex'] = 'Roll back to step (optional)'; +$string['rollbacktosortindex_help'] = 'In case of a rollback of this step the rollback will stop at the step you define here instead of returning to step one and removing the process.'; +$string['rollbacktotitle'] = 'Roll back to step \'{$a}\''; $string['rolledback'] = 'Rolled back'; +$string['run'] = 'Run'; +$string['runtask'] = 'Run scheduled lifecycle task'; $string['searchcourses'] = 'Search for courses'; $string['see_in_workflow'] = 'See in workflow'; $string['show_delays'] = 'Kind of view'; @@ -244,7 +265,7 @@ $string['steps_installed'] = 'Installed Steps'; $string['steptype_settings_header'] = 'Specific settings of the step type'; $string['subplugins'] = 'Subplugins'; -$string['subpluginsdesc'] = 'Subplugins'; +$string['subpluginsdesc'] = 'List of installed subplugins'; $string['subplugintype_lifecyclestep'] = 'Step within a lifecycle process'; $string['subplugintype_lifecyclestep_plural'] = 'Steps within a lifecycle process'; $string['subplugintype_lifecycletrigger'] = 'Trigger for starting a lifecycle process'; @@ -288,6 +309,7 @@ $string['workflow_is_running'] = 'Workflow is running.'; $string['workflow_not_removeable'] = 'It is not possible to remove this workflow instance. Maybe it still has running processes?'; $string['workflow_processes'] = 'Active processes'; +$string['workflow_processesanderrors'] = 'Active workflow processes and process errors'; $string['workflow_rollbackdelay'] = 'Delay in case of rollback'; $string['workflow_rollbackdelay_help'] = 'If a course was rolled back within a process instance of this workflow, this value describes the time until a process for this combination of course and workflow can be started again.'; diff --git a/lib.php b/lib.php index b742b32a..1cc6c697 100644 --- a/lib.php +++ b/lib.php @@ -48,16 +48,11 @@ function tool_lifecycle_extend_navigation_course($navigation, $course, $context) if ($cache->has('workflowactive')) { $wfexists = $cache->get('workflowactive'); } else { - $wfexists = $DB->record_exists_sql("SELECT 'yes' FROM {tool_lifecycle_workflow} wf " . - "JOIN {tool_lifecycle_trigger} t ON wf.id = t.workflowid " . - "WHERE wf.timeactive IS NOT NULL AND t.subpluginname NOT IN ('sitecourse', 'delayedcourses')"); + $wfexists = $DB->record_exists_sql("SELECT 'yes' FROM {tool_lifecycle_workflow} " . + " WHERE timeactive IS NOT NULL"); $cache->set('workflowactive', $wfexists); } - if (!$wfexists) { - return null; - } - $url = new moodle_url('/admin/tool/lifecycle/view.php', [ 'contextid' => $context->id, ]); diff --git a/renderer.php b/renderer.php index dbafe5c7..e49c9ed0 100644 --- a/renderer.php +++ b/renderer.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use core\output\single_button; + /** * Renderer for life cycle * @@ -57,9 +59,10 @@ public function header($title = null) { * Write the tab row in page * * @param array $tabs the tabs - * @param string $id ID of current page (can be empty) + * @param string $id ID of current page (can be empty) */ public function tabs($tabs, $id) { echo $this->output->tabtree($tabs, $id); } + } diff --git a/run.php b/run.php new file mode 100644 index 00000000..87b2150f --- /dev/null +++ b/run.php @@ -0,0 +1,57 @@ +. + +/** + * Displays the installed subplugins (steps and trigger). + * + * @package tool_lifecycle + * @copyright 2025 Thomas Niedermaier University Münster + * @copyright 2022 Justus Dieckmann WWU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use tool_lifecycle\tabs; +use tool_lifecycle\task\lifecycle_task; +use tool_lifecycle\urls; + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/adminlib.php'); + +require_admin(); + +$syscontext = context_system::instance(); +$PAGE->set_url(new \moodle_url(urls::RUN)); +$PAGE->set_context($syscontext); + +$PAGE->set_pagetype('admin-setting-' . 'tool_lifecycle'); +$PAGE->set_pagelayout('admin'); + +$renderer = $PAGE->get_renderer('tool_lifecycle'); + +$heading = get_string('pluginname', 'tool_lifecycle')." / ".get_string('runtask', 'tool_lifecycle'); +echo $renderer->header($heading); +$tabrow = tabs::get_tabrow(); +$id = 'settings'; +$renderer->tabs($tabrow, $id); + +echo \html_writer::start_div(''); + +$task = new lifecycle_task(); +$task->execute(); + +echo \html_writer::end_div(); + +echo $renderer->footer(); diff --git a/settings.php b/settings.php index c97c413f..b0fdaab0 100644 --- a/settings.php +++ b/settings.php @@ -23,10 +23,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - +use tool_lifecycle\local\manager\lib_manager; +use tool_lifecycle\local\manager\step_manager; +use tool_lifecycle\local\manager\trigger_manager; use tool_lifecycle\tabs; +defined('MOODLE_INTERNAL') || die; + // Check for the moodle/site:config permission. if ($hassiteconfig) { @@ -89,6 +92,8 @@ $settings->add(new admin_setting_description('lifecycletriggersetting_'.$trigger, $triggername, $plugindescription.$uninstall)); + $lib = lib_manager::get_trigger_lib($trigger); + $lib->get_plugin_settings(); } } } else { @@ -111,6 +116,8 @@ $settings->add(new admin_setting_description('lifecyclestepsetting_'.$step, $stepname, $plugindescription)); + $lib = lib_manager::get_step_lib($step); + $lib->get_plugin_settings(); } } else { $settings->add(new admin_setting_heading('adminsettings_nosteps', diff --git a/step/adminapprove/amd/build/init.min.js b/step/adminapprove/amd/build/init.min.js index cdce9896..0d9cf039 100644 --- a/step/adminapprove/amd/build/init.min.js +++ b/step/adminapprove/amd/build/init.min.js @@ -5,6 +5,6 @@ * @copyright 2019 Justus Dieckmann WWU * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("lifecyclestep_adminapprove/init",["jquery"],(function($){return{init:function(sesskey,url){$('input[name="checkall"]').click((function(){$('input[name="c[]"]').prop("checked",$('input[name="checkall"]').prop("checked"))})),$(".adminapprove-action").each((function(){$(this).click((function(){var post={act:$(this).attr("data-action"),"c[]":$(this).attr("data-content"),sesskey:sesskey},form=document.createElement("form");for(var k in form.hidden=!0,form.method="post",form.action=url,post){var input=document.createElement("input");input.type="hidden",input.name=k,input.value=post[k],form.append(input)}document.body.append(form),form.submit()}))}))}}})); +define("lifecyclestep_adminapprove/init",["jquery"],(function($){return{init:function(sesskey,url,totalrows){$("#adminapprove_totalrows").html(totalrows),$('input[name="checkall"]').click((function(){$('input[name="c[]"]').prop("checked",$('input[name="checkall"]').prop("checked"))})),$(".adminapprove-action").each((function(){$(this).click((function(){var post={act:$(this).attr("data-action"),"c[]":$(this).attr("data-content"),sesskey:sesskey},form=document.createElement("form");for(var k in form.hidden=!0,form.method="post",form.action=url,post){var input=document.createElement("input");input.type="hidden",input.name=k,input.value=post[k],form.append(input)}document.body.append(form),form.submit()}))}))}}})); //# sourceMappingURL=init.min.js.map \ No newline at end of file diff --git a/step/adminapprove/amd/build/init.min.js.map b/step/adminapprove/amd/build/init.min.js.map index e3d84231..3625354d 100644 --- a/step/adminapprove/amd/build/init.min.js.map +++ b/step/adminapprove/amd/build/init.min.js.map @@ -1 +1 @@ -{"version":3,"file":"init.min.js","sources":["../src/init.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Life Cycle Admin Approve Step AMD Module\n *\n * @module lifecyclestep_adminapprove/init\n * @copyright 2019 Justus Dieckmann WWU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery'], function($) {\n return {\n init: function(sesskey, url) {\n $('input[name=\"checkall\"]').click(function() {\n $('input[name=\"c[]\"]').prop('checked', $('input[name=\"checkall\"]').prop('checked'));\n });\n\n $('.adminapprove-action').each(function() {\n $(this).click(function() {\n var post = {\n 'act': $(this).attr('data-action'),\n 'c[]': $(this).attr('data-content'),\n 'sesskey': sesskey\n };\n var form = document.createElement('form');\n form.hidden = true;\n form.method = 'post';\n form.action = url;\n for (var k in post) {\n var input = document.createElement('input');\n input.type = 'hidden';\n input.name = k;\n input.value = post[k];\n form.append(input);\n }\n document.body.append(form);\n form.submit();\n });\n });\n }\n };\n});"],"names":["define","$","init","sesskey","url","click","prop","each","this","post","attr","form","document","createElement","k","hidden","method","action","input","type","name","value","append","body","submit"],"mappings":";;;;;;;AAsBAA,yCAAO,CAAC,WAAW,SAASC,SACjB,CACHC,KAAM,SAASC,QAASC,KACpBH,EAAE,0BAA0BI,OAAM,WAC9BJ,EAAE,qBAAqBK,KAAK,UAAWL,EAAE,0BAA0BK,KAAK,eAG5EL,EAAE,wBAAwBM,MAAK,WAC3BN,EAAEO,MAAMH,OAAM,eACNI,KAAO,KACAR,EAAEO,MAAME,KAAK,qBACbT,EAAEO,MAAME,KAAK,wBACTP,SAEXQ,KAAOC,SAASC,cAAc,YAI7B,IAAIC,KAHTH,KAAKI,QAAS,EACdJ,KAAKK,OAAS,OACdL,KAAKM,OAASb,IACAK,KAAM,KACZS,MAAQN,SAASC,cAAc,SACnCK,MAAMC,KAAO,SACbD,MAAME,KAAON,EACbI,MAAMG,MAAQZ,KAAKK,GACnBH,KAAKW,OAAOJ,OAEhBN,SAASW,KAAKD,OAAOX,MACrBA,KAAKa"} \ No newline at end of file +{"version":3,"file":"init.min.js","sources":["../src/init.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Life Cycle Admin Approve Step AMD Module\n *\n * @module lifecyclestep_adminapprove/init\n * @copyright 2019 Justus Dieckmann WWU\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery'], function($) {\n return {\n init: function(sesskey, url, totalrows) {\n\n $('#adminapprove_totalrows').html(totalrows);\n\n $('input[name=\"checkall\"]').click(function() {\n $('input[name=\"c[]\"]').prop('checked', $('input[name=\"checkall\"]').prop('checked'));\n });\n\n $('.adminapprove-action').each(function() {\n $(this).click(function() {\n var post = {\n 'act': $(this).attr('data-action'),\n 'c[]': $(this).attr('data-content'),\n 'sesskey': sesskey\n };\n var form = document.createElement('form');\n form.hidden = true;\n form.method = 'post';\n form.action = url;\n for (var k in post) {\n var input = document.createElement('input');\n input.type = 'hidden';\n input.name = k;\n input.value = post[k];\n form.append(input);\n }\n document.body.append(form);\n form.submit();\n });\n });\n }\n };\n});"],"names":["define","$","init","sesskey","url","totalrows","html","click","prop","each","this","post","attr","form","document","createElement","k","hidden","method","action","input","type","name","value","append","body","submit"],"mappings":";;;;;;;AAsBAA,yCAAO,CAAC,WAAW,SAASC,SACjB,CACHC,KAAM,SAASC,QAASC,IAAKC,WAEzBJ,EAAE,2BAA2BK,KAAKD,WAElCJ,EAAE,0BAA0BM,OAAM,WAC9BN,EAAE,qBAAqBO,KAAK,UAAWP,EAAE,0BAA0BO,KAAK,eAG5EP,EAAE,wBAAwBQ,MAAK,WAC3BR,EAAES,MAAMH,OAAM,eACNI,KAAO,KACAV,EAAES,MAAME,KAAK,qBACbX,EAAES,MAAME,KAAK,wBACTT,SAEXU,KAAOC,SAASC,cAAc,YAI7B,IAAIC,KAHTH,KAAKI,QAAS,EACdJ,KAAKK,OAAS,OACdL,KAAKM,OAASf,IACAO,KAAM,KACZS,MAAQN,SAASC,cAAc,SACnCK,MAAMC,KAAO,SACbD,MAAME,KAAON,EACbI,MAAMG,MAAQZ,KAAKK,GACnBH,KAAKW,OAAOJ,OAEhBN,SAASW,KAAKD,OAAOX,MACrBA,KAAKa"} \ No newline at end of file diff --git a/step/adminapprove/amd/src/init.js b/step/adminapprove/amd/src/init.js index c4267039..4a10fcf9 100644 --- a/step/adminapprove/amd/src/init.js +++ b/step/adminapprove/amd/src/init.js @@ -22,7 +22,10 @@ */ define(['jquery'], function($) { return { - init: function(sesskey, url) { + init: function(sesskey, url, totalrows) { + + $('#adminapprove_totalrows').html(totalrows); + $('input[name="checkall"]').click(function() { $('input[name="c[]"]').prop('checked', $('input[name="checkall"]').prop('checked')); }); diff --git a/step/adminapprove/approvestep.php b/step/adminapprove/approvestep.php index c2711cae..2f40bee1 100644 --- a/step/adminapprove/approvestep.php +++ b/step/adminapprove/approvestep.php @@ -23,6 +23,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use lifecyclestep_adminapprove\course_filter_form; +use tool_lifecycle\local\manager\step_manager; +use tool_lifecycle\local\manager\workflow_manager; use tool_lifecycle\tabs; use tool_lifecycle\urls; @@ -44,7 +47,7 @@ $PAGE->set_pagelayout('admin'); -$step = \tool_lifecycle\local\manager\step_manager::get_step_instance($stepid); +$step = step_manager::get_step_instance($stepid); if (!$step) { throw new moodle_exception('Stepid does not correspond to any step.'); } @@ -69,11 +72,11 @@ */ const PROCEED_ALL = 'proceedall'; -$workflow = \tool_lifecycle\local\manager\workflow_manager::get_workflow($step->workflowid); +$workflow = workflow_manager::get_workflow($step->workflowid); $mformdata = cache::make('lifecyclestep_adminapprove', 'mformdata'); -$mform = new \lifecyclestep_adminapprove\course_filter_form($PAGE->url->out()); +$mform = new course_filter_form($PAGE->url->out()); if ($mform->is_cancelled()) { $mformdata->delete('data'); redirect($PAGE->url); @@ -106,7 +109,7 @@ [$insql, $inparams] = $DB->get_in_or_equal($ids); $sql = 'UPDATE {lifecyclestep_adminapprove} ' . 'SET status = ' . ($action == PROCEED ? 1 : 2) . ' ' . - 'WHERE id ' . $insql . + 'WHERE id ' . $insql . ' ' . 'AND status = 0'; $DB->execute($sql, $inparams); } else if ($action == PROCEED_ALL || $action == ROLLBACK_ALL) { @@ -144,7 +147,9 @@ $heading = get_string('pluginname', 'tool_lifecycle')." / ".get_string('adminapprovals', 'lifecyclestep_adminapprove') . ': ' . get_string('step', 'tool_lifecycle') . ' ' . $step->instancename; echo $renderer->header($heading); -$tabrow = tabs::get_tabrow(true); +$tabparams = new stdClass(); +$tabparams->approvelink = true; +$tabrow = tabs::get_tabrow($tabparams); $id = optional_param('id', 'adminapprove', PARAM_TEXT); $renderer->tabs($tabrow, $id); @@ -158,33 +163,31 @@ echo get_string('courses_waiting', 'lifecyclestep_adminapprove', ['step' => $step->instancename, 'workflow' => $workflow->title]); - echo "

"; - echo '
'; - $table = new lifecyclestep_adminapprove\decision_table($stepid, $courseid, $category, $coursename); - $table->out(100, false); - if ($table->totalrows) { - echo get_string('bulkactions') . ':
'; - echo html_writer::start_div('singlebutton'); - echo html_writer::tag('button', get_string('proceedselected', 'lifecyclestep_adminapprove'), - ['type' => 'submit', 'name' => 'act', 'value' => PROCEED, 'class' => 'btn btn-secondary']); - echo html_writer::end_div() . html_writer::start_div('singlebutton'); - echo html_writer::tag('button', get_string('rollbackselected', 'lifecyclestep_adminapprove'), - ['type' => 'submit', 'name' => 'act', 'value' => ROLLBACK, 'class' => 'btn btn-secondary']); - echo html_writer::end_div(); - } - echo '
'; - - echo '
'; + echo '
'; + echo \html_writer::div('0', 'totalrows badge badge-primary badge-pill mr-2', + ['id' => 'adminapprove_totalrows']); + $button = new \single_button(new moodle_url($PAGE->url, ['act' => ROLLBACK_ALL]), + get_string(ROLLBACK_ALL, 'lifecyclestep_adminapprove')); + echo $OUTPUT->render($button); $button = new \single_button(new moodle_url($PAGE->url, ['act' => PROCEED_ALL]), - get_string(PROCEED_ALL, 'lifecyclestep_adminapprove')); + get_string(PROCEED_ALL, 'lifecyclestep_adminapprove'), 'post', 'primary'); echo $OUTPUT->render($button); - - $button = new \single_button(new moodle_url($PAGE->url, ['act' => ROLLBACK_ALL]), - get_string(ROLLBACK_ALL, 'lifecyclestep_adminapprove')); + $button = new \single_button(new moodle_url($PAGE->url, ['act' => ROLLBACK]), + get_string('rollbackselected', 'lifecyclestep_adminapprove'), 'post', 'secondary'); + echo $OUTPUT->render($button); + $button = new \single_button(new moodle_url($PAGE->url, ['act' => PROCEED]), + get_string('proceedselected', 'lifecyclestep_adminapprove'), 'post', 'primary'); echo $OUTPUT->render($button); echo '
'; - $PAGE->requires->js_call_amd('lifecyclestep_adminapprove/init', 'init', [sesskey(), $PAGE->url->out()]); + + echo '
'; + $table = new lifecyclestep_adminapprove\decision_table($stepid, $courseid, $category, $coursename); + $table->out(100, false); + echo '
'; + + $PAGE->requires->js_call_amd('lifecyclestep_adminapprove/init', 'init', + [sesskey(), $PAGE->url->out(), $table->totalrows]); } else { echo get_string('no_courses_waiting', 'lifecyclestep_adminapprove', ['step' => $step->instancename, 'workflow' => $workflow->title]); diff --git a/step/adminapprove/classes/decision_table.php b/step/adminapprove/classes/decision_table.php index 2b811068..9c8eae3c 100644 --- a/step/adminapprove/classes/decision_table.php +++ b/step/adminapprove/classes/decision_table.php @@ -24,6 +24,8 @@ namespace lifecyclestep_adminapprove; +use core_date; + defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -34,17 +36,6 @@ */ class decision_table extends \table_sql { - /** - * @var coursecategory - */ - private $category; - - /** - * @var string pattern for the coursename. - */ - private $coursename; - - /** * Constructs the table. * @param int $stepid @@ -55,8 +46,6 @@ class decision_table extends \table_sql { */ public function __construct($stepid, $courseid, $category, $coursename) { parent::__construct('lifecyclestep_adminapprove-decisiontable'); - $this->category = $category; - $this->coursename = $coursename; $this->define_baseurl("/admin/tool/lifecycle/step/adminapprove/approvestep.php?stepid=$stepid"); $this->define_columns(['checkbox', 'courseid', 'course', 'category', 'startdate', 'tools']); $this->define_headers( @@ -90,10 +79,10 @@ public function __construct($stepid, $courseid, $category, $coursename) { $where .= "AND c.fullname LIKE :cname "; $params['cname'] = '%' . $DB->sql_like_escape($coursename) . '%'; } + $this->set_sql($fields, $from, $where, $params); } - /** * Column of checkboxes. * @param object $row @@ -137,9 +126,12 @@ public function col_category($row) { * @return string human readable date */ public function col_startdate($row) { + global $USER; + if ($row->startdate) { $dateformat = get_string('strftimedate', 'langconfig'); - return userdate($row->startdate, $dateformat); + return userdate($row->startdate, $dateformat, + core_date::get_user_timezone($USER)); } else { return "-"; } @@ -153,14 +145,14 @@ public function col_startdate($row) { */ public function col_tools($row) { $output = \html_writer::start_div('singlebutton mr-1'); - $output .= \html_writer::tag('button', get_string('proceed', 'lifecyclestep_adminapprove'), - ['class' => 'btn btn-secondary adminapprove-action', 'data-action' => 'proceed', 'data-content' => $row->id, - 'type' => 'button']); + $output .= \html_writer::tag('button', get_string('rollback', 'lifecyclestep_adminapprove'), + ['class' => 'btn btn-secondary adminapprove-action', 'data-action' => 'rollback', 'data-content' => $row->id, + 'type' => 'button']); $output .= \html_writer::end_div(); $output .= \html_writer::start_div('singlebutton mr-1 ml-0 mt-1'); - $output .= \html_writer::tag('button', get_string('rollback', 'lifecyclestep_adminapprove'), - ['class' => 'btn btn-secondary adminapprove-action', 'data-action' => 'rollback', 'data-content' => $row->id, - 'type' => 'button']); + $output .= \html_writer::tag('button', get_string('proceed', 'lifecyclestep_adminapprove'), + ['class' => 'btn btn-primary adminapprove-action', 'data-action' => 'proceed', 'data-content' => $row->id, + 'type' => 'button']); $output .= \html_writer::end_div(); return $output; } diff --git a/step/adminapprove/index.php b/step/adminapprove/index.php index 5383b8e1..9211fa8a 100644 --- a/step/adminapprove/index.php +++ b/step/adminapprove/index.php @@ -23,15 +23,15 @@ */ use tool_lifecycle\tabs; +use tool_lifecycle\urls; require_once(__DIR__ . '/../../../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $PAGE->set_context(context_system::instance()); -$PAGE->set_url(new \moodle_url("/admin/tool/lifecycle/step/adminapprove/index.php")); +$PAGE->set_url(new \moodle_url(urls::SUBPLUGINS)); $PAGE->set_pagetype('admin-setting-' . 'tool_lifecycle'); $PAGE->set_pagelayout('admin'); diff --git a/step/adminapprove/lib.php b/step/adminapprove/lib.php index 8862fe57..c53fb3b7 100644 --- a/step/adminapprove/lib.php +++ b/step/adminapprove/lib.php @@ -44,11 +44,12 @@ class adminapprove extends libbase { private static $newcourses = 0; /** - * Provess a single course. + * Process a single course. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param stdClass $course to be processed. * @return step_response + * @throws \dml_exception */ public function process_course($processid, $instanceid, $course) { global $DB; @@ -85,7 +86,7 @@ public function get_subpluginname() { * Process a course which is waiting. * @param int $processid * @param int $instanceid - * @param stdClass $course + * @param int $course * @return step_response * @throws \dml_exception */ @@ -137,7 +138,7 @@ public function post_processing_bulk_operation() { */ public function instance_settings() { return [ - new instance_setting('statusmessage', PARAM_TEXT), + new instance_setting('statusmessage', PARAM_TEXT, true), ]; } diff --git a/step/adminapprove/tests/admin_approve_test.php b/step/adminapprove/tests/admin_approve_test.php index 3510f56e..2a831575 100644 --- a/step/adminapprove/tests/admin_approve_test.php +++ b/step/adminapprove/tests/admin_approve_test.php @@ -16,9 +16,8 @@ namespace lifecyclestep_adminapprove; -namespace lifecyclestep_adminapprove; - defined('MOODLE_INTERNAL') || die(); + require_once(__DIR__ . '/../../../tests/generator/lib.php'); use tool_lifecycle\local\manager\process_manager; diff --git a/step/adminapprove/version.php b/step/adminapprove/version.php index 8b87a7f5..3cfcf385 100644 --- a/step/adminapprove/version.php +++ b/step/adminapprove/version.php @@ -26,9 +26,6 @@ $plugin->version = 2025050400; $plugin->component = 'lifecyclestep_adminapprove'; -$plugin->dependencies = [ - 'tool_lifecycle' => 2025050400, -]; $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; $plugin->release = 'v4.5-r1'; diff --git a/step/createbackup/lang/de/lifecyclestep_createbackup.php b/step/createbackup/lang/de/lifecyclestep_createbackup.php index 9b9aa793..108e7f9d 100644 --- a/step/createbackup/lang/de/lifecyclestep_createbackup.php +++ b/step/createbackup/lang/de/lifecyclestep_createbackup.php @@ -23,7 +23,7 @@ */ $string['maximumbackupspercron'] = 'Maximale Anzahl an Sicherungen per cron'; -$string['plugindescription'] = 'Stößt ein Backup der getriggerten Kursen an.'; +$string['plugindescription'] = 'Stößt ein Backup der getriggerten Kurse an.'; $string['pluginname'] = 'Kurssicherungs-Schritt'; $string['privacy:metadata'] = 'Dieses Subplugin speichert keine persönlichen Daten.'; diff --git a/step/createbackup/lib.php b/step/createbackup/lib.php index 18724439..f95e9d82 100644 --- a/step/createbackup/lib.php +++ b/step/createbackup/lib.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\step; +use stdClass; use tool_lifecycle\local\manager\settings_manager; use tool_lifecycle\local\response\step_response; use tool_lifecycle\local\manager\backup_manager; @@ -50,14 +51,14 @@ class createbackup extends libbase { private static $numberofbackups = 0; /** - * Processes the course and returns a repsonse. + * Processes the course and returns a response. * The response tells either * - that the subplugin is finished processing. * - that the subplugin is not yet finished processing. * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param stdClass $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception @@ -78,7 +79,7 @@ public function process_course($processid, $instanceid, $course) { * Simply call the process_course since it handles everything necessary for this plugin. * @param int $processid * @param int $instanceid - * @param mixed $course + * @param stdClass $course * @return step_response * @throws \coding_exception * @throws \dml_exception diff --git a/step/deletecourse/lib.php b/step/deletecourse/lib.php index 1509cdaf..3253efda 100644 --- a/step/deletecourse/lib.php +++ b/step/deletecourse/lib.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\step; +use stdClass; use tool_lifecycle\local\manager\settings_manager; use tool_lifecycle\local\response\step_response; use tool_lifecycle\settings_type; @@ -44,14 +45,14 @@ class deletecourse extends libbase { private static $numberofdeletions = 0; /** - * Processes the course and returns a repsonse. + * Processes the course and returns a response. * The response tells either * - that the subplugin is finished processing. * - that the subplugin is not yet finished processing. * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param stdClass $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception @@ -59,11 +60,16 @@ class deletecourse extends libbase { public function process_course($processid, $instanceid, $course) { global $CFG; + if ($course->id == 1) { + return step_response::rollback(); + } + if (self::$numberofdeletions >= settings_manager::get_settings( $instanceid, settings_type::STEP)['maximumdeletionspercron']) { return step_response::waiting(); // Wait with further deletions til the next cron run. } - delete_course($course->id, true); + + delete_course($course); /* Fix 'delete & backup (other) course aftwerwards' error, which is created by moodle core issue MDL-65228 (https://tracker.moodle.org/browse/MDL-65228) */ @@ -77,15 +83,17 @@ public function process_course($processid, $instanceid, $course) { } /** - * Processes the course in status waiting and returns a repsonse. + * Processes the course in status waiting and returns a response. * The response tells either * - that the subplugin is finished processing. * - that the subplugin is not yet finished processing. * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param stdClass $course to be processed. * @return step_response + * @throws \coding_exception + * @throws \dml_exception */ public function process_waiting_course($processid, $instanceid, $course) { return $this->process_course($processid, $instanceid, $course); diff --git a/step/duplicate/lib.php b/step/duplicate/lib.php index d730b8df..d9abd38c 100644 --- a/step/duplicate/lib.php +++ b/step/duplicate/lib.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\step; +use stdClass; use tool_lifecycle\local\manager\process_manager; use tool_lifecycle\local\manager\settings_manager; use tool_lifecycle\local\response\step_response; @@ -49,18 +50,19 @@ class duplicate extends libbase { const PROC_DATA_COURSESHORTNAME = 'shortname'; /** - * Processes the course and returns a repsonse. + * Processes the course and returns a response. * The response tells either * - that the subplugin is finished processing. * - that the subplugin is not yet finished processing. * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param stdClass $course to be processed. * @return step_response * @throws \dml_exception */ public function process_course($processid, $instanceid, $course) { + $course = get_course($course->id); $fullname = process_data_manager::get_process_data($processid, $instanceid, self::PROC_DATA_COURSEFULLNAME); $shortname = process_data_manager::get_process_data($processid, $instanceid, self::PROC_DATA_COURSESHORTNAME); if (!empty($fullname) && !empty($shortname)) { @@ -84,14 +86,14 @@ public function process_course($processid, $instanceid, $course) { } /** - * Processes the course in status waiting and returns a repsonse. + * Processes the course in status waiting and returns a response. * The response tells either * - that the subplugin is finished processing. * - that the subplugin is not yet finished processing. * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param stdClass $course to be processed. * @return step_response * @throws \dml_exception */ diff --git a/step/email/interactionlib.php b/step/email/interactionlib.php index 65054d19..c67cfd53 100644 --- a/step/email/interactionlib.php +++ b/step/email/interactionlib.php @@ -58,8 +58,8 @@ public function get_relevant_capability() { } /** - * Returns an array of interaction tools to be displayed to be displayed on the view.php - * Every entry is itself an array which consist of three elements: + * Returns an array of interaction tools to be displayed on view.php + * Every entry is itself an array that consists of two elements: * 'action' => an action string, which is later passed to handle_action * 'alt' => a string text of the button * @param process $process process the action tools are requested for @@ -90,10 +90,10 @@ public function get_status_message($process) { * @param step_subplugin $step instance of the step the process is currently in. * @param string $action action string * @return step_interactive_response defines if the step still wants to process this course - * - proceed: the step has finished and respective controller class can take over. + * - proceed: the step has finished and the respective controller class can take over. * - stillprocessing: the step still wants to process the course and is responsible for rendering the site. * - noaction: the action is not defined for the step. - * - rollback: the step has finished and respective controller class should rollback the process. + * - rollback: the step has finished and the respective controller class should rollback the process. */ public function handle_interaction($process, $step, $action = 'default') { if ($action == self::ACTION_KEEP) { diff --git a/step/email/lib.php b/step/email/lib.php index e4f5bd6b..d412f56b 100644 --- a/step/email/lib.php +++ b/step/email/lib.php @@ -44,14 +44,14 @@ class email extends libbase { /** - * Processes the course and returns a repsonse. + * Processes the course and returns a response. * The response tells either * - that the subplugin is finished processing. * - that the subplugin is not yet finished processing. * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param object $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception @@ -72,14 +72,14 @@ public function process_course($processid, $instanceid, $course) { } /** - * Processes the course in status waiting and returns a repsonse. + * Processes the course in status waiting and returns a response. * The response tells either * - that the subplugin is finished processing. * - that the subplugin is not yet finished processing. * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param object $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception @@ -155,7 +155,7 @@ public function post_processing_bulk_operation() { * @throws \dml_exception * @throws \moodle_exception */ - private function replace_placeholders($strings, $user, $stepid, $mailentries) { + public function replace_placeholders($strings, $user, $stepid, $mailentries) { global $CFG; $patterns = []; diff --git a/step/email/tests/lib_test.php b/step/email/tests/lib_test.php index fba24188..0293e301 100644 --- a/step/email/tests/lib_test.php +++ b/step/email/tests/lib_test.php @@ -50,14 +50,10 @@ public function test_replace_placeholders(): void { $course1 = $this->getDataGenerator()->create_course(['fullname' => 'Course 1', 'shortname' => 'C1']); $course2 = $this->getDataGenerator()->create_course(['fullname' => 'Course 2', 'shortname' => 'C2']); $lib = new email(); - $callreplaceplaceholders = function($strings, $user, $stepid, $mailentries) { - return $this->replace_placeholders($strings, $user, $stepid, $mailentries); - }; - $response = $callreplaceplaceholders->call($lib, [ + $response = $lib->replace_placeholders([ "##firstname##\n##lastname##\n##courses##\n##shortcourses##", "##firstname##
##lastname##
##courses-html##
##shortcourses-html##", ], $user1, 0, [(object) ['courseid' => $course1->id], (object) ['courseid' => $course2->id]]); - $this->assertCount(2, $response); $this->assertEquals("Jane\nDoe\nCourse 1\nCourse 2\nC1\nC2", $response[0]); $this->assertEquals("Jane
Doe
Course 1
Course 2
C1
C2", $response[1]); diff --git a/step/lib.php b/step/lib.php index 6c439f9d..5918c26d 100644 --- a/step/lib.php +++ b/step/lib.php @@ -24,6 +24,7 @@ */ namespace tool_lifecycle\step; +use stdClass; use tool_lifecycle\local\entity\process; use tool_lifecycle\local\manager\step_manager; use tool_lifecycle\local\response\step_response; @@ -41,27 +42,27 @@ abstract class libbase { /** - * Processes the course and returns a repsonse. + * Processes the course and returns a response. * The response tells either * - that the subplugin is finished processing. * - that the subplugin is not yet finished processing. * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param stdClass $course to be processed. * @return step_response */ abstract public function process_course($processid, $instanceid, $course); /** - * Processes the course in status waiting and returns a repsonse. + * Processes the course in status waiting and returns a response. * The response tells either * - that the subplugin is finished processing. * - that the subplugin is not yet finished processing. * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param stdClass $course to be processed. * @return step_response */ public function process_waiting_course($processid, $instanceid, $course) { @@ -150,7 +151,6 @@ public function extend_add_instance_form_validation(&$error, $data) { public function abort_course($process) { } - /** * Ensure validity of settings upon backup restoration. * @param array $settings @@ -160,6 +160,30 @@ public function ensure_validity(array $settings): array { return []; } + /** + * Define description of the step. + * Allow subplugins to have custom description. + * + * @return string description of the trigger. + */ + public function get_plugin_description() { + return get_string("pluginname", "lifecyclestep_" . $this->get_subpluginname()); + } + + /** + * Returns the settings of the step. + * + * @return void + */ + public function get_plugin_settings() { + $step = $this->get_subpluginname(); + $file = __DIR__ . "/$step/settings.php"; + + if (file_exists($file)) { + include($file); + } + } + } /** diff --git a/step/makeinvisible/lib.php b/step/makeinvisible/lib.php index 46b59f7b..83737594 100644 --- a/step/makeinvisible/lib.php +++ b/step/makeinvisible/lib.php @@ -24,8 +24,10 @@ namespace tool_lifecycle\step; +use stdClass; use tool_lifecycle\local\manager\process_data_manager; use tool_lifecycle\local\response\step_response; +use function update_course; defined('MOODLE_INTERNAL') || die(); @@ -45,7 +47,7 @@ class makeinvisible extends libbase { * * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be processed. + * @param stdClass $course to be processed. * @return step_response */ public function process_course($processid, $instanceid, $course) { @@ -59,7 +61,8 @@ public function process_course($processid, $instanceid, $course) { * Roll back the changes. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param mixed $course to be rolled back. + * @param stdClass $course to be rolled back. + * @throws \moodle_exception */ public function rollback_course($processid, $instanceid, $course) { global $CFG; @@ -70,11 +73,11 @@ public function rollback_course($processid, $instanceid, $course) { require_once($CFG->dirroot . '/course/lib.php'); $cat = \core_course_category::get($course->category, MUST_EXIST, true); - $record = new \stdClass(); + $record = new stdClass(); $record->id = $course->id; $record->visibleold = (bool) process_data_manager::get_process_data($processid, $instanceid, 'visibleold'); $record->visible = $record->visibleold && (bool)$cat->visible; - \update_course($record); + update_course($record); } /** diff --git a/step/makeinvisible/version.php b/step/makeinvisible/version.php index 94f34f4b..a16808a8 100644 --- a/step/makeinvisible/version.php +++ b/step/makeinvisible/version.php @@ -28,8 +28,5 @@ $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; $plugin->component = 'lifecyclestep_makeinvisible'; -$plugin->dependencies = [ - 'tool_lifecycle' => 2025050400, -]; $plugin->release = 'v4.5-r1'; $plugin->maturity = MATURITY_STABLE; diff --git a/step/movecategory/classes/privacy/provider.php b/step/movecategory/classes/privacy/provider.php new file mode 100644 index 00000000..f4b981fd --- /dev/null +++ b/step/movecategory/classes/privacy/provider.php @@ -0,0 +1,39 @@ +. + +namespace lifecyclestep_movecategory\privacy; + +use core_privacy\local\metadata\null_provider; + +/** + * Privacy subsystem implementation for lifecyclestep_movecategory. + * + * @package lifecyclestep_movecategory + * @copyright 2023 Justus Dieckmann WWU Münster + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/step/movecategory/interactionlib.php b/step/movecategory/interactionlib.php new file mode 100644 index 00000000..e3bc78d0 --- /dev/null +++ b/step/movecategory/interactionlib.php @@ -0,0 +1,106 @@ +. + +/** + * Interface for the interactions of the subplugintype step + * + * @package lifecyclestep_movecategory + * @copyright 2023 Norman Stulier, JMU Universität Würzburg Rechenzentrum. + * @copyright based on code from moodle-lifecyclestep_adminapprove by 2017 Tobias Reischmann WWU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lifecycle\step; + +use tool_lifecycle\local\entity\process; +use tool_lifecycle\local\entity\step_subplugin; +use tool_lifecycle\local\manager\settings_manager; +use tool_lifecycle\local\manager\step_manager; +use tool_lifecycle\local\response\step_interactive_response; +use tool_lifecycle\settings_type; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../interactionlib.php'); +require_once(__DIR__ . '/lib.php'); + +/** + * movecategory interactionlib + * + * @package lifecyclestep_movecategory + * @copyright 2023 Norman Stulier, JMU Universität Würzburg Rechenzentrum. + * @copyright based on code from moodle-lifecyclestep_adminapprove by 2017 Tobias Reischmann WWU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class interactionmovecategory extends interactionlibbase { + + + /** + * Returns the capability a user has to have to make decisions for a specific course. + * @return string capability string. + */ + public function get_relevant_capability() { + return null; + } + + /** + * Returns an array of interaction tools to be displayed to be displayed on the view.php for the given process + * Every entry is itself an array which consist of three elements: + * 'action' => an action string, which is later passed to handle_action + * 'alt' => a string text of the button + * @param process $process process the action tools are requested for + * @return array of action tools + */ + public function get_action_tools($process) { + return []; + } + + /** + * Returns the status message for the given process. + * @param process $process process the status message is requested for + * @return string status message + * @throws \coding_exception + */ + public function get_status_message($process) { + $step = step_manager::get_step_instance_by_workflow_index($process->workflowid, $process->stepindex); + return settings_manager::get_settings($step->id, settings_type::STEP)['statusmessage']; + } + + /** + * Returns the display name for the given action. + * Used for the past actions table in view.php. + * @param string $action Identifier of action + * @param string $userlink html-link with username as text that refers to the user profile. + * @return string action display name + */ + public function get_action_string($action, $userlink) { + return ""; + } + + /** + * Called when a user triggered an action for a process instance. + * @param process $process instance of the process the action was triggered upon. + * @param step_subplugin $step instance of the step the process is currently in. + * @param string $action action string. The function is called with 'default', during interactive processing. + * @return step_interactive_response defines if the step still wants to process this course + * - proceed: the step has finished and respective controller class can take over. + * - stillprocessing: the step still wants to process the course and is responsible for rendering the site. + * - noaction: the action is not defined for the step. + * - rollback: the step has finished and respective controller class should rollback the process. + */ + public function handle_interaction($process, $step, $action = 'default') { + return step_interactive_response::no_action(); + } +} diff --git a/step/movecategory/lang/de/lifecyclestep_movecategory.php b/step/movecategory/lang/de/lifecyclestep_movecategory.php new file mode 100644 index 00000000..db75a915 --- /dev/null +++ b/step/movecategory/lang/de/lifecyclestep_movecategory.php @@ -0,0 +1,29 @@ +. + +/** + * Lang strings for movecategory step + * + * @package lifecyclestep_movecategory + * @copyright 2019 Yorick Reum JMU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['categorytomoveto'] = 'Zielkursbereich'; +$string['categorytomoveto_help'] = 'Bestimme den Kursbereich, in den die Kurse verschoben werden sollen.'; +$string['plugindescription'] = 'Verschiebt die ausgewählten Kurse in einen (anderen) Kursbereich.'; +$string['pluginname'] = 'Kurs verschieben - Schritt'; +$string['privacy:metadata'] = 'Dieses Subplugin speichert keine persönlichen Daten.'; diff --git a/step/movecategory/lang/en/lifecyclestep_movecategory.php b/step/movecategory/lang/en/lifecyclestep_movecategory.php new file mode 100644 index 00000000..d1e95ff8 --- /dev/null +++ b/step/movecategory/lang/en/lifecyclestep_movecategory.php @@ -0,0 +1,29 @@ +. + +/** + * Lang strings for movecategory step + * + * @package lifecyclestep_movecategory + * @copyright 2019 Yorick Reum JMU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['categorytomoveto'] = 'Target Category'; +$string['categorytomoveto_help'] = 'Set the category which you want to move the courses to.'; +$string['plugindescription'] = 'Moves all triggered courses to a defined course category. '; +$string['pluginname'] = 'Move Category Step'; +$string['privacy:metadata'] = 'The lifecyclestep_movecategory plugin does not store any personal data.'; diff --git a/step/movecategory/lib.php b/step/movecategory/lib.php new file mode 100644 index 00000000..a2ac984f --- /dev/null +++ b/step/movecategory/lib.php @@ -0,0 +1,114 @@ +. + +/** + * Interface for the subplugintype step + * It has to be implemented by all subplugins. + * + * @package lifecyclestep_movecategory + * @copyright 2019 Yorick Reum JMU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lifecycle\step; + +use stdClass; +use tool_lifecycle\local\manager\settings_manager; +use tool_lifecycle\local\response\step_response; +use tool_lifecycle\settings_type; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../lib.php'); + +/** + * Class for processing courses. + */ +class movecategory extends libbase { + + /** + * Processes the course and returns a response. + * The response tells either + * - that the subplugin is finished processing. + * - that the subplugin is not yet finished processing. + * - that a rollback for this course is necessary. + * + * @param int $processid of the respective process. + * @param int $instanceid of the step instance. + * @param stdClass $course to be processed. + * @return step_response + * @throws \coding_exception + * @throws \dml_exception + */ + public function process_course($processid, $instanceid, $course) { + $categoryid = settings_manager::get_settings( + $instanceid, + settings_type::STEP + )['categorytomoveto']; + + $success = move_courses( + [$course->id], $categoryid + ); + + if ($success) { + return step_response::proceed(); + } else { + return step_response::rollback(); + } + + } + + /** + * Settings for the movecategory step + * + * @return instance_setting[] categorymoveto + */ + public function instance_settings() { + return [ + new instance_setting('categorytomoveto', PARAM_INT), + ]; + } + + /** + * Form elements for the instance. + * + * @param \MoodleQuickForm $mform + * @return void + * @throws \coding_exception + * @throws \dml_exception + */ + public function extend_add_instance_form_definition($mform) { + + $elementname = 'categorytomoveto'; + + // Fetch a complete list of courses and let it be shown in a flat hierarchical view with all parent branches. + $displaylist = \core_course_category::make_categories_list('moodle/course:changecategory'); + + $mform->addElement('autocomplete', $elementname, + get_string('categorytomoveto', 'lifecyclestep_movecategory'), $displaylist); + $mform->addHelpButton($elementname, 'categorytomoveto', 'lifecyclestep_movecategory'); + $mform->setType($elementname, PARAM_INT); + } + + /** + * Function to return subpluginname + * + * @return string subpluginname + */ + public function get_subpluginname() { + return 'movecategory'; + } +} diff --git a/step/movecategory/version.php b/step/movecategory/version.php new file mode 100644 index 00000000..941b534a --- /dev/null +++ b/step/movecategory/version.php @@ -0,0 +1,32 @@ +. + +/** + * Life Cycle Move Category Step + * + * @package lifecyclestep_movecategory + * @copyright 2019 Yorick Reum JMU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$plugin->version = 2025050400; +$plugin->requires = 2022112800; // Requires Moodle 4.1+. +$plugin->supported = [401, 405]; +$plugin->component = 'lifecyclestep_movecategory'; +$plugin->release = 'v4.5-r1'; +$plugin->maturity = MATURITY_STABLE; diff --git a/step/pushbackuptask/README.md b/step/pushbackuptask/README.md new file mode 100644 index 00000000..a3b3c70b --- /dev/null +++ b/step/pushbackuptask/README.md @@ -0,0 +1,5 @@ +# Pushbackuptask (moodle-lifecyclestep_pushbackuptask) + +This is a step for [Life Cycle](https://github.com/learnweb/moodle-tool_lifecycle) which does basically the same as the createbackup step but asynchronously by utilizing an adhoc task. The step just pushes one adhoc task for each triggered course. The adhoc task does on execution the same as the createbackup step. That allows the backup processes to run in parallel when cron configured accordingly (see https://docs.moodle.org/401/en/Cron#Scaling_up_cron_with_multiple_processes) + +Creation of backups during execution of the adhoc task can be disable by setting `$CFG->custom_no_tool_lifecycle_adhoc_backups = true;` in `config.php`. Useful for staging environments. diff --git a/step/pushbackuptask/classes/task/course_backup_task.php b/step/pushbackuptask/classes/task/course_backup_task.php new file mode 100644 index 00000000..93bec1a1 --- /dev/null +++ b/step/pushbackuptask/classes/task/course_backup_task.php @@ -0,0 +1,74 @@ +. + +/** + * Life Cycle Admin push backup task step + * + * @package lifecyclestep_pushbackuptask + * @copyright 2024 Johannes Burk (HTW Berlin) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace lifecyclestep_pushbackuptask\task; + +use tool_lifecycle\local\manager\backup_manager; + +/** + * Task for adhoc backups of courses. + */ +class course_backup_task extends \core\task\adhoc_task { + + /** + * Run the adhoc task and preform the backup. + */ + public function execute() { + global $CFG, $DB; + + if (!empty($CFG->custom_no_tool_lifecycle_adhoc_backups)) { + mtrace('Tool lifecycle adhoc backups are disabled via config.php ($CFG->custom_no_tool_lifecycle_adhoc_backups)'); + return; + } + + $lockfactory = \core\lock\lock_config::get_lock_factory('course_backup_adhoc'); + $courseid = $this->get_custom_data()->courseid; + + try { + $course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST); + } catch (\moodle_exception $e) { + mtrace('Invalid course id: ' . $courseid . ', task aborted.'); + return; + } + + if (!$lock = $lockfactory->get_lock('lifecyclestep_pushbackuptask_' . $courseid, 10)) { + mtrace('Backup adhoc task for: ' . $course->fullname . 'is already running.'); + return; + } else { + mtrace('Processing backup for course: ' . $course->fullname); + } + + try { + backup_manager::create_course_backup($courseid); + } catch (Exception $e) { + mtrace('Backup for course: ' . $course->fullname . ' encounters an error.'); + mtrace('Exception: ' . $e->getMessage()); + mtrace('Debug: ' . $e->debuginfo); + } finally { + // Everything is finished release lock. + $lock->release(); + mtrace('Backup for course: ' . $course->fullname . ' completed.'); + } + } +} diff --git a/step/pushbackuptask/lang/de/lifecyclestep_pushbackuptask.php b/step/pushbackuptask/lang/de/lifecyclestep_pushbackuptask.php new file mode 100644 index 00000000..0a426a57 --- /dev/null +++ b/step/pushbackuptask/lang/de/lifecyclestep_pushbackuptask.php @@ -0,0 +1,27 @@ +. + +/** + * Lang strings for push backup task step + * + * @package lifecyclestep_pushbackuptask + * @copyright 2024 Johannes Burk (HTW Berlin) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['plugindescription'] = 'Ermöglicht augenblickliche Sicherungen von Kursen durch die Verwendung von ad hoc-Tasks.'; +$string['pluginname'] = 'Kurssicherungs-Adhoc-Schritt'; +$string['privacy:metadata'] = 'Dieses Subplugin speichert keine persönlichen Daten..'; diff --git a/step/pushbackuptask/lang/en/lifecyclestep_pushbackuptask.php b/step/pushbackuptask/lang/en/lifecyclestep_pushbackuptask.php new file mode 100644 index 00000000..b48700ba --- /dev/null +++ b/step/pushbackuptask/lang/en/lifecyclestep_pushbackuptask.php @@ -0,0 +1,28 @@ +. + +/** + * Lang strings for push backup task step + * + * @package lifecyclestep_pushbackuptask + * @copyright 2024 Johannes Burk (HTW Berlin) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['plugindescription'] = 'Enforces the backup of scheduled courses immediately by using adhoc tasks.'; +$string['pluginname'] = 'Create Adhoc Backup Step'; +$string['privacy:metadata'] = 'This subplugin does not store any personal data.'; + diff --git a/step/pushbackuptask/lib.php b/step/pushbackuptask/lib.php new file mode 100644 index 00000000..577f4a03 --- /dev/null +++ b/step/pushbackuptask/lib.php @@ -0,0 +1,77 @@ +. + +/** + * Life Cycle Admin push backup task step + * + * @package lifecyclestep_pushbackuptask + * @copyright 2024 Johannes Burk (HTW Berlin) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lifecycle\step; + +use lifecyclestep_pushbackuptask\task\course_backup_task; +use stdClass; +use tool_lifecycle\local\response\step_response; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../lib.php'); + +/** + * Class for processing courses. + */ +class pushbackuptask extends libbase { + /** + * Processes the course and returns a response. + * The response tells either + * - that the subplugin is finished processing. + * - that the subplugin is not yet finished processing. + * - that a rollback for this course is necessary. + * @param int $processid of the respective process. + * @param int $instanceid of the step instance. + * @param stdClass $course to be processed. + * @return step_response + */ + public function process_course($processid, $instanceid, $course) { + $asynctask = new course_backup_task(); + $asynctask->set_custom_data(['courseid' => $course->id]); + \core\task\manager::queue_adhoc_task($asynctask); + return step_response::proceed(); + } + + /** + * Simply call the process_course since it handles everything necessary for this plugin. + * @param int $processid + * @param int $instanceid + * @param stdClass $course + * @return step_response + * @throws \coding_exception + * @throws \dml_exception + */ + public function process_waiting_course($processid, $instanceid, $course) { + return $this->process_course($processid, $instanceid, $course); + } + + /** + * The return value should be equivalent with the name of the subplugin folder. + * @return string technical name of the subplugin + */ + public function get_subpluginname() { + return 'pushbackuptask'; + } +} diff --git a/step/pushbackuptask/version.php b/step/pushbackuptask/version.php new file mode 100644 index 00000000..b4695f3d --- /dev/null +++ b/step/pushbackuptask/version.php @@ -0,0 +1,33 @@ +. + +/** + * Life Cycle Admin push backup task step + * + * @package lifecyclestep_pushbackuptask + * @copyright 2025 Thomas Niedermaier University Münster + * @copyright 2024 Johannes Burk (HTW Berlin) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$plugin->version = 2025050400; +$plugin->requires = 2022112800; // Requires Moodle 4.1+. +$plugin->supported = [401, 405]; +$plugin->component = 'lifecyclestep_pushbackuptask'; +$plugin->release = 'v4.5-r1'; +$plugin->maturity = MATURITY_STABLE; diff --git a/styles.css b/styles.css index 61b7dd2e..303962d8 100644 --- a/styles.css +++ b/styles.css @@ -32,7 +32,7 @@ span.tool_lifecycle-hint { text-align: center; border: 1px solid #aaa; border-radius: 8px; - min-width: 220px; + min-width: 260px; background-color: white; } @@ -47,6 +47,7 @@ span.tool_lifecycle-hint { #lifecycle-workflow-details .workflow-trigger { max-width: 300px; + min-width: 260px; margin: 8px; } @@ -110,12 +111,14 @@ span.tool_lifecycle-hint { #lifecycle-workflow-details .courses-table { flex: 45% 1 1; border: 1px solid #bbb; - margin-right: 2em; + border-radius: 8px; + margin-left: 0.5em; + margin-right: 0.5em; } #lifecycle-workflow-details .bar { - border: 1px solid #dee2e6; - border-right: none; + border: 1px none #dee2e6; + border-bottom-style: solid; } #lifecycle-workflow-details .close-button { @@ -149,3 +152,7 @@ span.tool_lifecycle-hint { #lifecycle-workflow-details .dropdown-toggle::after { content: none; } + +#lifecycle-workflow-details .wf-wrapper a:hover { + text-decoration: none; +} diff --git a/subplugins.php b/subplugins.php new file mode 100644 index 00000000..30f293aa --- /dev/null +++ b/subplugins.php @@ -0,0 +1,94 @@ +. + +/** + * Displays the installed subplugins (steps and trigger). + * + * @package tool_lifecycle + * @copyright 2025 Thomas Niedermaier University Münster + * @copyright 2022 Justus Dieckmann WWU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use tool_lifecycle\tabs; +use tool_lifecycle\urls; + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/adminlib.php'); + +require_admin(); + +$syscontext = context_system::instance(); +$PAGE->set_url(new \moodle_url(urls::SUBPLUGINS)); +$PAGE->set_context($syscontext); + +$PAGE->set_pagetype('admin-setting-' . 'tool_lifecycle'); +$PAGE->set_pagelayout('admin'); + +$renderer = $PAGE->get_renderer('tool_lifecycle'); + +$heading = get_string('pluginname', 'tool_lifecycle')." / ".get_string('subplugins', 'tool_lifecycle'); +echo $renderer->header($heading); +$tabrow = tabs::get_tabrow(); +$id = optional_param('id', 'subplugins', PARAM_TEXT); +$renderer->tabs($tabrow, $id); + +echo html_writer::link('https://github.com/learnweb/moodle-tool_lifecycle/wiki/List-of-Installed-Subplugins', + get_string('documentationlink', 'tool_lifecycle'), ['target' => '_blank']); + +$triggers = core_component::get_plugin_list('lifecycletrigger'); +if ($triggers) { + echo html_writer::div(get_string('triggers_installed', 'tool_lifecycle'), 'h2 mt-2'); + foreach ($triggers as $trigger => $path) { + echo html_writer::div(get_string('pluginname', 'lifecycletrigger_' . $trigger), + "font-weight-bold"); + try { + $plugindescription = get_string('plugindescription', 'lifecycletrigger_' . $trigger); + } catch (Exception $e) { + $plugindescription = ""; + } + if ($plugindescription) { + echo html_writer::start_div().$plugindescription; + if ($trigger == 'sitecourse' || $trigger == 'delayedcourses') { + echo html_writer::span(' Depracated. Will be removed with version 5.0.', 'text-danger'); + } + echo html_writer::end_div(); + } + } +} else { + echo html_writer::div(get_string('adminsettings_notriggers', 'tool_lifecycle')); +} + +$steps = core_component::get_plugin_list('lifecyclestep'); +if ($steps) { + echo html_writer::div(get_string('steps_installed', 'tool_lifecycle'), 'h2 mt-2'); + foreach ($steps as $step => $path) { + echo html_writer::div(get_string('pluginname', 'lifecyclestep_' . $step), + "font-weight-bold"); + try { + $plugindescription = get_string('plugindescription', 'lifecyclestep_' . $step); + } catch (Exception $e) { + $plugindescription = ""; + } + if ($plugindescription) { + echo html_writer::div($plugindescription); + } + } +} else { + echo html_writer::div(get_string('adminsettings_nosteps', 'tool_lifecycle')); +} + +echo $renderer->footer(); diff --git a/templates/overview_processeslink.mustache b/templates/overview_processeslink.mustache new file mode 100644 index 00000000..aa698a2a --- /dev/null +++ b/templates/overview_processeslink.mustache @@ -0,0 +1,33 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle 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 Moodle. If not, see . +}} +{{! + @template tool_lifecycle/overview_processeslink + + Add trigger and add step select fields + + Example context (json): + { + "url": "admin/tool/lifecycle/workflowoverview.php?wf=1&processes=4&showdetails=1", + "alt": "Active workflow processes and process errors", + "processes": "4" + } +}} + + + {{processes}} + + diff --git a/templates/overview_step.mustache b/templates/overview_step.mustache index d2faf758..12fae71b 100644 --- a/templates/overview_step.mustache +++ b/templates/overview_step.mustache @@ -39,7 +39,13 @@
- {{#shortentext}} 25, {{instancename}} {{/shortentext}}
+ {{#shortentext}} 50, {{instancename}} {{/shortentext}} + {{#rollbacktosortindex}} + + {{rollbacktosortindex}} + + {{/rollbacktosortindex}} +
{{subpluginname}}
@@ -49,7 +55,7 @@
{{#numberofcourses}} - {{#str}} courses {{/str}}: {{numberofcourses}} + {{#str}} courses {{/str}}: {{numberofcourses}} {{/numberofcourses}} {{^numberofcourses}} diff --git a/templates/overview_timetrigger.mustache b/templates/overview_timetrigger.mustache index 82add35c..b09fcc19 100644 --- a/templates/overview_timetrigger.mustache +++ b/templates/overview_timetrigger.mustache @@ -34,7 +34,7 @@
- {{#shortentext}} 25, {{instancename}} {{/shortentext}}
+ {{#shortentext}} 50, {{instancename}} {{/shortentext}}
{{subpluginname}}
diff --git a/templates/overview_trigger.mustache b/templates/overview_trigger.mustache index 0067c3f4..5de7f760 100644 --- a/templates/overview_trigger.mustache +++ b/templates/overview_trigger.mustache @@ -30,6 +30,7 @@ "showdetails": 1, "id": 1, "triggeredcourses": 5, + "tooltip": " 5 courses will be triggered", "actionmenu": "actionmenu", "popuplink": "admin/tool/lifecycle/workflowoverview.php?wf=1" } @@ -38,7 +39,7 @@
- {{#shortentext}} 25, {{instancename}} {{/shortentext}}
+ {{#shortentext}} 50, {{instancename}} {{/shortentext}}
{{subpluginname}}
@@ -54,26 +55,29 @@ {{^additionalinfo}} {{#triggeredcourses}} - - {{triggeredcourses}} - + + {{triggeredcourses}} + {{/triggeredcourses}} {{^triggeredcourses}} - {{#excludedcourses}} - - - {{excludedcourses}} - - - {{/excludedcourses}} {{^excludedcourses}} - 0 + {{^exclude}} + 0 + {{/exclude}} {{/excludedcourses}} {{/triggeredcourses}} - {{#alreadyin}} - {{alreadyin}} - {{/alreadyin}} + {{#excludedcourses}} + + - {{excludedcourses}} + + {{/excludedcourses}} + {{^excludedcourses}} + {{#exclude}} + 0 + {{/exclude}} + {{/excludedcourses}} + {{/additionalinfo}} {{/automatic}} {{^automatic}} diff --git a/templates/search_input.mustache b/templates/search_input.mustache index 3c0131ce..8bcbc9c6 100644 --- a/templates/search_input.mustache +++ b/templates/search_input.mustache @@ -70,7 +70,7 @@
{{#otherfields}} -
{{{ otherfields }}}
+
{{{ otherfields }}}
{{/otherfields}} {{^inform}} diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 98b595b3..3acb48b7 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -31,7 +31,7 @@ Example context (json): { "triggerhelp": "", - "editsettingslink": "http://localhost/moodle400/admin/tool/lifecycle/editworkflow.php?wf=1", + "editsettingslink": "http://localhost:8000/admin/tool/lifecycle/editworkflow.php?wf=1", "title": "My Workflow Title", "rollbackdelay": "183 days", "finishdelay": "183 days", @@ -74,55 +74,74 @@
-
+
- {{#pix}} i/edit, core, {{/pix}}
- {{#showdetailsicon}} - - {{/showdetailsicon}} - {{^showdetailsicon}} - - {{/showdetailsicon}} + {{#pix}} i/edit, core, {{/pix}} + {{{disableworkflowlink}}} + {{{abortdisableworkflowlink}}} + {{{workflowprocesseslink}}}

{{title}} - {{#includedelayedcourses}}{{/includedelayedcourses}} - {{#includesitecourse}}{{/includesitecourse}}

- {{#str}} workflow_rollbackdelay, tool_lifecycle{{/str}}: {{rollbackdelay}}
- {{#str}} workflow_finishdelay, tool_lifecycle{{/str}}: {{finishdelay}}
+ {{#includedelayedcourses}}{{/includedelayedcourses}} + {{#includesitecourse}}{{/includesitecourse}} + {{#str}} workflow_rollbackdelay, tool_lifecycle{{/str}}: {{rollbackdelay}}
+ {{#str}} workflow_finishdelay, tool_lifecycle{{/str}}: {{finishdelay}}
{{# delayglobally }}{{#str}}details:globaldelay_yes, tool_lifecycle{{/str}}{{/ delayglobally }} {{^delayglobally}}{{#str}}details:globaldelay_no, tool_lifecycle{{/str}}{{/delayglobally}} {{! Add trigger and add step selection fields. }} {{>tool_lifecycle/overview_addinstance}} + {{! If deactivated workflow display activate button. }} + {{{activatebutton}}}
{{#counttriggers}}
-
{{#str}} courseselection_title, tool_lifecycle{{/str}}
+
{{#str}} courseselection_title, tool_lifecycle{{/str}} + + {{#showdetailsicon}} + + {{/showdetailsicon}} + {{^showdetailsicon}} + +     + {{/showdetailsicon}} +
{{#showcoursecounts}} {{#displaytotaltriggered}}
{{#coursestriggeredcount}} - - - {{coursestriggeredcount}} - - - {{#str}} courses_will_be_triggered_total_without_amount, tool_lifecycle {{/str}}
+
+ + + {{coursestriggeredcount}} + + + {{#str}} courses_will_be_triggered_total_without_amount, tool_lifecycle {{/str}} +
{{/coursestriggeredcount}} {{^coursestriggeredcount}} - {{#str}} courses_will_be_triggered_total, tool_lifecycle, {{{coursestriggered}}} {{/str}}
+
+ + {{coursestriggeredcount}} + + {{#str}} courses_will_be_triggered_total_without_amount, tool_lifecycle {{/str}} +
{{/coursestriggeredcount}} {{#coursesused}} - {{#str}} courses_are_used_total, tool_lifecycle, {{{coursesused}}} {{/str}}
+
+ {{#str}} courses_are_used_total, tool_lifecycle, {{{coursesused}}} {{/str}}
+
{{/coursesused}} {{#coursesdelayed}} - {{#str}} courses_are_delayed, tool_lifecycle, {{{coursesdelayed}}} {{/str}} +
+ {{#str}} courses_are_delayed, tool_lifecycle, {{{coursesdelayed}}} {{/str}} +
{{/coursesdelayed}}
{{/displaytotaltriggered}} {{^displaytotaltriggered}}
- {{#str}} manualtriggerenvolved, tool_lifecycle {{/str}} + {{#str}} manualtriggerenvolved, tool_lifecycle {{/str}}
{{/displaytotaltriggered}} {{/showcoursecounts}} @@ -137,18 +156,29 @@
-
{{#str}} courseselectionrun_title, tool_lifecycle{{/str}}
+
{{#str}} courseselectionrun_title, tool_lifecycle{{/str}}
{{#nomanualtriggerinvolved}} -
- {{#str}} nextrun, tool_lifecycle, {{{nextrun}}} {{/str}}
+
+ {{#str}} nextrun, tool_lifecycle, {{{nextrun}}} {{/str}} +
{{#str}} lastrun, tool_lifecycle, {{lastrun}} {{/str}}
{{/nomanualtriggerinvolved}} {{^nomanualtriggerinvolved}} -
- {{#str}} manualtriggerenvolved, tool_lifecycle {{/str}} +
+ {{#str}} manualtriggerenvolved, tool_lifecycle {{/str}}
{{/nomanualtriggerinvolved}} + {{#runnable}} + + {{/runnable}} + {{#counttimetriggers}}
diff --git a/tests/active_workflow_is_manual_test.php b/tests/active_workflow_is_manual_test.php index 86aac4b8..55155e05 100644 --- a/tests/active_workflow_is_manual_test.php +++ b/tests/active_workflow_is_manual_test.php @@ -75,8 +75,8 @@ public function setUp(): void { $this->automaticworkflow = $generator->create_workflow(); $generator->create_step("instance1", "createbackup", $this->automaticworkflow->id); - $this->assertNull($this->manualworkflow->manual); - $this->assertNull($this->automaticworkflow->manual); + $this->assertNull($this->manualworkflow->manually); + $this->assertNull($this->automaticworkflow->manually); // We do not need a sesskey check in these tests. $USER->ignoresesskey = true; @@ -94,7 +94,7 @@ public function test_activate_manual(): void { workflow_manager::handle_action(action::WORKFLOW_ACTIVATE, $this->manualworkflow->id); $reloadworkflow = workflow_manager::get_workflow($this->manualworkflow->id); $this->assertTrue(workflow_manager::is_active($this->manualworkflow->id)); - $this->assertTrue($reloadworkflow->manual); + $this->assertTrue($reloadworkflow->manually); } /** @@ -109,6 +109,6 @@ public function test_activate_automatic(): void { workflow_manager::handle_action(action::WORKFLOW_ACTIVATE, $this->automaticworkflow->id); $reloadworkflow = workflow_manager::get_workflow($this->automaticworkflow->id); $this->assertTrue(workflow_manager::is_active($this->automaticworkflow->id)); - $this->assertEquals(false, $reloadworkflow->manual); + $this->assertEquals(false, $reloadworkflow->manually); } } diff --git a/tests/backup_and_restore_workflow_test.php b/tests/backup_and_restore_workflow_test.php index e7e6bae8..242e9efc 100644 --- a/tests/backup_and_restore_workflow_test.php +++ b/tests/backup_and_restore_workflow_test.php @@ -101,7 +101,7 @@ public function test_backup_workflow(): void { } $this->assertNotNull($newworkflow); - foreach (['title', 'displaytitle', 'manual'] as $property) { + foreach (['title', 'displaytitle', 'manually'] as $property) { $this->assertEquals($this->workflow->$property, $newworkflow->$property); } foreach (['timeactive', 'timedeactive', 'sortindex'] as $property) { diff --git a/tests/behat/activate_workflow.feature b/tests/behat/activate_workflow.feature index 40582f26..ef66d90b 100644 --- a/tests/behat/activate_workflow.feature +++ b/tests/behat/activate_workflow.feature @@ -38,7 +38,7 @@ Feature: Add a workflow definition activate it And I press "Save changes" And I am on workflowdrafts page Then I should see the tool "View workflow steps" in the "My Workflow" row of the "tool_lifecycle_workflow_definitions" table - And I should see the tool "Backup workflow" in the "My Workflow" row of the "tool_lifecycle_workflow_definitions" table + And I should see the tool "Download workflow definition" in the "My Workflow" row of the "tool_lifecycle_workflow_definitions" table And I should see the tool "Delete workflow" in the "My Workflow" row of the "tool_lifecycle_workflow_definitions" table And I press "Activate" When I click on the tool "View workflow steps" in the "My Workflow" row of the "tool_lifecycle_active_automatic_workflows" table diff --git a/tests/behat/behat_tool_lifecycle.php b/tests/behat/behat_tool_lifecycle.php index 992e44a4..e057d825 100644 --- a/tests/behat/behat_tool_lifecycle.php +++ b/tests/behat/behat_tool_lifecycle.php @@ -364,7 +364,7 @@ public function i_should_not_see_the_tool_in_any_row_of_the_table($tool, $tablen public function the_step_should_be_at_the_position($stepname, $position) { $xpathelement = "(//*[@id='lifecycle-workflow-details']" . "//*[contains(concat(' ', normalize-space(@class), ' '), ' workflow-step ')])[$position]" . - "//span[contains(text(),'$stepname')]"; + "//a[contains(text(),'$stepname')]"; try { $this->find('xpath', $xpathelement); @@ -387,7 +387,7 @@ public function the_step_should_be_at_the_position($stepname, $position) { public function i_click_on_in_the_step($linktext, $stepname) { $xpathelement = "//*[@id='lifecycle-workflow-details']" . "//*[contains(concat(' ', normalize-space(@class), ' '), ' workflow-step ') and " . - "descendant::span[contains(text(),'$stepname')]]"; + "descendant::a[contains(text(),'$stepname')]]"; try { $this->find('xpath', $xpathelement); } catch (ElementNotFoundException $e) { @@ -411,7 +411,7 @@ public function i_click_on_in_the_step($linktext, $stepname) { public function i_click_on_in_the_trigger($linktext, $triggername) { $xpathelement = "//*[@id='lifecycle-workflow-details']" . "//*[contains(concat(' ', normalize-space(@class), ' '), ' workflow-trigger ') and " . - "descendant::span[contains(text(),'$triggername')]]"; + "descendant::a[contains(text(),'$triggername')]]"; try { $this->find('xpath', $xpathelement); } catch (ElementNotFoundException $e) { diff --git a/tests/behat/interaction.feature b/tests/behat/interaction.feature index 5477260d..0fb15324 100644 --- a/tests/behat/interaction.feature +++ b/tests/behat/interaction.feature @@ -38,15 +38,15 @@ Feature: Add a workflow with an email step and test the interaction as a teacher And I press "Save changes" And I select "Email step" from the "tool_lifecycle-choose-step" singleselect And I set the following fields to these values: - | Instance name | Email step | - | responsetimeout[number] | 8 | - | responsetimeout[timeunit] | seconds | - | Subject template | Subject | - | Content plain text template | Content | - | Content HTML Template | Content HTML | + | Instance name | Email step | + | responsetimeout[number] | 8 | + | responsetimeout[timeunit] | seconds | + | Subject template | Subject | + | Content plain text template | Content | + | Content HTML Template | Content HTML | And I press "Save changes" And I select "Delete course step" from the "tool_lifecycle-choose-step" singleselect - And I set the field "Instance name" to "Delete Course 2" + And I set the field "Instance name" to "Delete Course 3" And I press "Save changes" And I am on workflowdrafts page And I press "Activate" @@ -62,7 +62,11 @@ Feature: Add a workflow with an email step and test the interaction as a teacher Then I should see "Course 1" in the "tool_lifecycle_remaining" "table" And I should see "Course 2" in the "tool_lifecycle_remaining" "table" And I should see "Course 3" in the "tool_lifecycle_remaining" "table" + And I log out + And I log in as "admin" When I run the scheduled task "tool_lifecycle\task\lifecycle_task" + And I log out + And I log in as "teacher1" And I am on lifecycle view Then I should see "Course 1" in the "tool_lifecycle_remaining" "table" And I should see "Course 2" in the "tool_lifecycle_interaction" "table" @@ -76,7 +80,14 @@ Feature: Add a workflow with an email step and test the interaction as a teacher And I should see "Course 2" in the "tool_lifecycle_remaining" "table" And I should see "Course 3" in the "tool_lifecycle_interaction" "table" When I wait "10" seconds + And I log out + And I log in as "admin" + When I run the scheduled task "tool_lifecycle\task\lifecycle_task" + And I wait "10" seconds And I run the scheduled task "tool_lifecycle\task\lifecycle_task" + And I log out + And I wait "10" seconds + And I log in as "teacher1" And I am on lifecycle view Then I should see "Course 1" in the "tool_lifecycle_remaining" "table" And I should see "Course 2" in the "tool_lifecycle_remaining" "table" diff --git a/tests/behat/manual_trigger.feature b/tests/behat/manual_trigger.feature index ab7f5938..783681fe 100644 --- a/tests/behat/manual_trigger.feature +++ b/tests/behat/manual_trigger.feature @@ -85,7 +85,14 @@ Feature: Add a manual trigger and test view and actions as a teacher And I should see "Course 2" And I should not see the tool "Delete course" in the "Course 1" row of the "tool_lifecycle_remaining" table And I should see the tool "Delete course" in the "Course 2" row of the "tool_lifecycle_remaining" table - When I run the scheduled task "tool_lifecycle\task\lifecycle_task" + When I log out + And I log in as "admin" + And I run the scheduled task "tool_lifecycle\task\lifecycle_task" + And I wait "20" seconds + And I run the scheduled task "tool_lifecycle\task\lifecycle_task" + And I wait "20" seconds + And I log out + And I log in as "teacher1" And I am on lifecycle view Then I should not see "Course 1" And I should see "Course 2" diff --git a/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php b/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php new file mode 100644 index 00000000..4fa861f4 --- /dev/null +++ b/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php @@ -0,0 +1,75 @@ +. + +/** + * Fake component for testing + * @package tool_lifecycle + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_samplestep\lifecycle; +defined('MOODLE_INTERNAL') || die(); + +use tool_lifecycle\local\entity\process; +use tool_lifecycle\local\entity\step_subplugin; +use tool_lifecycle\step\interactionlibbase; + +global $CFG; +require_once($CFG->dirroot . '/admin/tool/lifecycle/step/interactionlib.php'); + +/** + * Fake class + */ +class interaction extends interactionlibbase { + + /** + * Fake function + */ + public function get_relevant_capability() { + } + + /** + * Fake function + * @param process $process process + */ + public function get_action_tools($process) { + } + + /** + * Fake function + * @param process $process process + */ + public function get_status_message($process) { + } + + /** + * Fake function + * @param string $action Identifier of action + * @param string $user html-link with username as text that refers to the user profile + */ + public function get_action_string($action, $user) { + } + + /** + * Fake function + * @param process $process instance of the process the action was triggered upon. + * @param step_subplugin $step instance of the step the process is currently in. + * @param string $action action string. The function is called with 'default', during interactive processing. + */ + public function handle_interaction($process, $step, $action = 'default') { + } +} diff --git a/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/step.php b/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/step.php new file mode 100644 index 00000000..6f74bb6f --- /dev/null +++ b/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/step.php @@ -0,0 +1,60 @@ +. + +/** + * Fake component for testing + * @package tool_lifecycle + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_samplestep\lifecycle; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/admin/tool/lifecycle/step/lib.php'); + +use tool_lifecycle\step\libbase; + +/** + * Fake class + */ +class step extends libbase { + /** + * Fake function + */ + public function get_subpluginname() { + return 'sample step'; + } + + /** + * Fake function + */ + public function get_plugin_description() { + return "Sample step plugin"; + } + + /** + * Fake function + * @param int $processid of the respective process. + * @param int $instanceid of the step instance. + * @param mixed $course to be processed. + */ + public function process_course($processid, $instanceid, $course) { + return null; + } + +} diff --git a/tests/fixtures/fakeplugins/samplestep/classes/privacy/provider.php b/tests/fixtures/fakeplugins/samplestep/classes/privacy/provider.php new file mode 100644 index 00000000..a28bfc96 --- /dev/null +++ b/tests/fixtures/fakeplugins/samplestep/classes/privacy/provider.php @@ -0,0 +1,39 @@ +. + +namespace tool_samplestep\privacy; + +use core_privacy\local\metadata\null_provider; + +/** + * Privacy subsystem implementation for tool_samplestep. + * + * @package tool_lifecycle + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string the reason + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/tests/fixtures/fakeplugins/samplestep/lang/en/tool_samplestep.php b/tests/fixtures/fakeplugins/samplestep/lang/en/tool_samplestep.php new file mode 100644 index 00000000..dcf24499 --- /dev/null +++ b/tests/fixtures/fakeplugins/samplestep/lang/en/tool_samplestep.php @@ -0,0 +1,24 @@ +. + +/** + * Fake component for testing + * @package tool_lifecycle + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +$string['pluginname'] = 'Sample step'; +$string['privacy:metadata'] = 'The plugin does not store any personal data.'; diff --git a/tests/fixtures/fakeplugins/samplestep/version.php b/tests/fixtures/fakeplugins/samplestep/version.php new file mode 100644 index 00000000..7029ad2e --- /dev/null +++ b/tests/fixtures/fakeplugins/samplestep/version.php @@ -0,0 +1,28 @@ +. + +/** + * Fake component for testing + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @package tool_lifecycle + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2023100400; +$plugin->requires = 2022041200; +$plugin->component = 'tool_samplestep'; diff --git a/tests/fixtures/fakeplugins/sampletrigger/classes/lifecycle/trigger.php b/tests/fixtures/fakeplugins/sampletrigger/classes/lifecycle/trigger.php new file mode 100644 index 00000000..08f756bf --- /dev/null +++ b/tests/fixtures/fakeplugins/sampletrigger/classes/lifecycle/trigger.php @@ -0,0 +1,62 @@ +. + +/** + * Fake component for testing + * @package tool_lifecycle + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_sampletrigger\lifecycle; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/admin/tool/lifecycle/trigger/lib.php'); + +use tool_lifecycle\trigger\base_automatic; + + +/** + * Fake class + */ +class trigger extends base_automatic { + + /** + * Fake function + */ + public function get_subpluginname() { + return 'sample trigger'; + } + + /** + * Fake function + */ + public function get_plugin_description() { + return "Sample trigger"; + } + + /** + * Fake function + * @param int $course DEPRECATED + * @param int $triggerid DEPRECATED + */ + public function check_course($course, $triggerid) { + return null; + } + +} diff --git a/tests/fixtures/fakeplugins/sampletrigger/classes/privacy/provider.php b/tests/fixtures/fakeplugins/sampletrigger/classes/privacy/provider.php new file mode 100644 index 00000000..9d37e207 --- /dev/null +++ b/tests/fixtures/fakeplugins/sampletrigger/classes/privacy/provider.php @@ -0,0 +1,38 @@ +. + +namespace tool_sampletrigger\privacy; + +use core_privacy\local\metadata\null_provider; + +/** + * Privacy subsystem implementation for tool_sampletrigger. + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @package tool_lifecycle + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string the reason + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/tests/fixtures/fakeplugins/sampletrigger/lang/en/tool_sampletrigger.php b/tests/fixtures/fakeplugins/sampletrigger/lang/en/tool_sampletrigger.php new file mode 100644 index 00000000..bc8a5fa6 --- /dev/null +++ b/tests/fixtures/fakeplugins/sampletrigger/lang/en/tool_sampletrigger.php @@ -0,0 +1,24 @@ +. + +/** + * Fake component for testing + * @package tool_lifecycle + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +$string['pluginname'] = 'Sample trigger'; +$string['privacy:metadata'] = 'The plugin does not store any personal data.'; diff --git a/tests/fixtures/fakeplugins/sampletrigger/version.php b/tests/fixtures/fakeplugins/sampletrigger/version.php new file mode 100644 index 00000000..8bad1333 --- /dev/null +++ b/tests/fixtures/fakeplugins/sampletrigger/version.php @@ -0,0 +1,28 @@ +. + +/** + * Fake component for testing + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @package tool_lifecycle + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2023100400; +$plugin->requires = 2022041200; +$plugin->component = 'tool_sampletrigger'; diff --git a/tests/process_error_test.php b/tests/process_error_test.php index f9052c07..8c9d7387 100644 --- a/tests/process_error_test.php +++ b/tests/process_error_test.php @@ -56,7 +56,7 @@ final class process_error_test extends \advanced_testcase { private $course; /** - * Setup the testcase. + * Set up the testcase. * @throws \coding_exception * @throws \moodle_exception */ @@ -65,7 +65,7 @@ public function setUp(): void { parent::setUp(); - // We do not need a sesskey check in theses tests. + // We do not need a sesskey check in these tests. $USER->ignoresesskey = true; $this->resetAfterTest(true); @@ -112,14 +112,9 @@ public function test_process_error_in_table(): void { $record = reset($records); $this->assertEquals($this->course->id, $record->courseid); - if (version_compare(PHP_VERSION, '8.0', '<')) { - $this->assertStringContainsString("Trying to get property 'id' of non-object", $record->errormessage); - } else if (version_compare(PHP_VERSION, '8.3', '<')) { - $this->assertStringContainsString("Attempt to read property \"id\" on bool", $record->errormessage); - } else { - $this->assertStringContainsString("Attempt to read property \"id\" on false", $record->errormessage); - } - $this->assertEquals($process->id, $record->id); + $this->assertEquals($process->workflowid, $record->workflowid); + $this->assertEquals(1, $record->stepindex); + $this->assertNotEmpty($record->errormessage); } } diff --git a/tests/subplugin_test.php b/tests/subplugin_test.php new file mode 100644 index 00000000..fa71e846 --- /dev/null +++ b/tests/subplugin_test.php @@ -0,0 +1,140 @@ +. + +namespace tool_lifecycle; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir.'/upgradelib.php'); + +use tool_lifecycle\local\manager\lib_manager; +use tool_lifecycle\local\manager\step_manager; +use tool_lifecycle\local\manager\trigger_manager; + +/** + * Tests the subplugins. + * @package tool_lifecycle + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class subplugin_test extends \advanced_testcase { + + + /** + * Test built in triggers. + * @covers \tool_lifecycle\local\manager\subplugin_manager + */ + public function test_builtin_triggers(): void { + $this->resetAfterTest(); + $builtintriggers = \core_component::get_plugin_list('lifecycletrigger'); + $triggers = trigger_manager::get_trigger_types(); + + // All builtin triggers are also in the list of triggers. + $this->assertEmpty(array_diff_key($builtintriggers, $triggers)); + + // Trigger classes are loadable. + foreach ($builtintriggers as $triggername => $triggerpath) { + $this->assertNotEmpty(lib_manager::get_trigger_lib($triggername)); + } + } + + /** + * Test built in steps. + * @covers \tool_lifecycle\local\manager\subplugin_manager + */ + public function test_builtin_steps(): void { + $this->resetAfterTest(); + $builtinsteps = \core_component::get_plugin_list('lifecyclestep'); + $steps = step_manager::get_step_types(); + + // All builtin steps are also in the list of steps. + $this->assertEmpty(array_diff_key($builtinsteps, $steps)); + + // Step classes are loadable. + foreach ($builtinsteps as $stepname => $steppath) { + $this->assertNotEmpty(lib_manager::get_step_lib($stepname)); + } + } + + /** + * Test additional triggers + * @covers \tool_lifecycle\local\manager\subplugin_manager + */ + public function test_additional_triggers(): void { + $this->resetAfterTest(); + + // Add a fake tool plugin, which define a trigger. + $mockedcomponent = new \ReflectionClass(\core_component::class); + $mockedplugins = $mockedcomponent->getProperty('plugins'); + $mockedplugins->setAccessible(true); + $plugins = $mockedplugins->getValue(); + $plugins['tool'] += ['sampletrigger' => __DIR__ . '/fixtures/fakeplugins/sampletrigger']; + $mockedplugins->setValue($plugins); + + // The 'fixture' is not autoloaded, so we need to require it. + require_once(__DIR__ . '/fixtures/fakeplugins/sampletrigger/classes//lifecycle/trigger.php'); + + // Check if the trigger is available. + $triggers = trigger_manager::get_trigger_types(); + $this->assertArrayHasKey('tool_sampletrigger', $triggers); + + // Lib is loadable. + $this->assertInstanceOf('tool_sampletrigger\lifecycle\trigger', + lib_manager::get_trigger_lib('tool_sampletrigger')); + + // Unset the fake plugin. + unset($plugins['tool']['sampletrigger']); + $mockedplugins->setValue($plugins); + } + + /** + * Test additional steps + * @covers \tool_lifecycle\local\manager\subplugin_manager + */ + public function test_additional_steps(): void { + $this->resetAfterTest(); + + // Add a fake tool plugin, which define a step. + $mockedcomponent = new \ReflectionClass(\core_component::class); + $mockedplugins = $mockedcomponent->getProperty('plugins'); + $mockedplugins->setAccessible(true); + $plugins = $mockedplugins->getValue(); + $plugins['tool'] += ['samplestep' => __DIR__ . '/fixtures/fakeplugins/samplestep']; + $mockedplugins->setValue($plugins); + + // The 'fixture' is not autoloaded, so we need to require it. + require_once(__DIR__ . '/fixtures/fakeplugins/samplestep/classes//lifecycle/step.php'); + require_once(__DIR__ . '/fixtures/fakeplugins/samplestep/classes//lifecycle/interaction.php'); + + // Check if the step is available. + $steps = step_manager::get_step_types(); + $this->assertArrayHasKey('tool_samplestep', $steps); + + // Lib is loadable. + $this->assertInstanceOf('tool_samplestep\lifecycle\step', + lib_manager::get_step_lib('tool_samplestep')); + + // Lib subtype is loadable. + $this->assertInstanceOf('tool_samplestep\lifecycle\interaction', + lib_manager::get_step_interactionlib('tool_samplestep')); + + // Unset the fake plugin. + unset($plugins['tool']['samplestep']); + $mockedplugins->setValue($plugins); + } + +} diff --git a/trigger/byrole/lang/de/lifecycletrigger_byrole.php b/trigger/byrole/lang/de/lifecycletrigger_byrole.php index 09c55416..c31db4a3 100644 --- a/trigger/byrole/lang/de/lifecycletrigger_byrole.php +++ b/trigger/byrole/lang/de/lifecycletrigger_byrole.php @@ -24,6 +24,8 @@ $string['delay'] = 'Wieviele Tage ohne Rolle'; $string['delay_help'] = 'Wieviele Tage ein Kurs ohne diese Rolle sein muß, damit der Trigger ausgelöst wird.'; +$string['invert'] = 'Selektion der Kurse MIT dieser Rolle'; +$string['invert_help'] = 'Die Anwesenheit von mindestens einer der gewählten Rollen führt zur Selektion des Kurses.'; $string['plugindescription'] = 'Löst aus wenn in einem Kurs für eine bestimmte Anzahl an Tagen eine bestimmte Rolle nicht vergeben wurde.'; $string['pluginname'] = 'Kurse mit fehlender Rolle'; $string['privacy:metadata'] = 'Speichert keine Userdaten'; diff --git a/trigger/byrole/lang/en/lifecycletrigger_byrole.php b/trigger/byrole/lang/en/lifecycletrigger_byrole.php index 3fe6e8d6..9bc01920 100644 --- a/trigger/byrole/lang/en/lifecycletrigger_byrole.php +++ b/trigger/byrole/lang/en/lifecycletrigger_byrole.php @@ -24,6 +24,8 @@ $string['delay'] = 'Days of delay for triggering'; $string['delay_help'] = 'Days a course has to remain without the mandatory role until the course is finally triggered'; +$string['invert'] = 'Invert role selection for triggering'; +$string['invert_help'] = 'If ticked, any of the selected roles have to be present for a course to be triggered.'; $string['plugindescription'] = 'Triggers if a specified role is misssing in a course for a certain timespan.'; $string['pluginname'] = 'Trigger courses by roles missing'; $string['privacy:metadata'] = 'Does not store user specific data'; diff --git a/trigger/byrole/lib.php b/trigger/byrole/lib.php index 459f0fe0..7d5d2dd3 100644 --- a/trigger/byrole/lib.php +++ b/trigger/byrole/lib.php @@ -49,15 +49,15 @@ public function get_course_recordset_where($triggerid) { $delay = settings_manager::get_settings($triggerid, settings_type::TRIGGER)['delay']; $maxtime = time() - $delay; - $sql = "{course}.id in (SELECT DISTINCT courseid + $sql = "c.id in (SELECT DISTINCT courseid FROM {lifecycletrigger_byrole} WHERE triggerid = $triggerid AND timecreated < $maxtime)"; return [$sql, []]; } /** - * Returns triggertype of trigger: trigger, triggertime or exclude. - * @param \stdClass $course DEPRECATED - * @param int $triggerid DEPRECATED + * If check_course_code() returns true, code to check the given course is placed here + * @param \stdClass $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { @@ -100,9 +100,15 @@ private function update_courses($triggerid) { list($insql, $inparams) = $DB->get_in_or_equal($this->get_roles($triggerid), SQL_PARAMS_NAMED); + if (isset(settings_manager::get_settings($triggerid, settings_type::TRIGGER)['invert'])) { + $invert = settings_manager::get_settings($triggerid, settings_type::TRIGGER)['invert'] && true; + } else { + $invert = false; + } + $sql = "SELECT c.id FROM {course} c - WHERE c.id NOT IN ( + WHERE c.id " . ($invert ? "" : "NOT") . " IN ( SELECT e.courseid FROM {context} coursectx JOIN {enrol} e ON coursectx.contextlevel = 50 AND e.courseid = coursectx.instanceid AND e.status = 0 JOIN {user_enrolments} ue ON e.id = ue.enrolid AND ue.status = 0 @@ -159,6 +165,7 @@ public function get_subpluginname() { public function instance_settings() { return [ new instance_setting('roles', PARAM_SEQUENCE), + new instance_setting('invert', PARAM_BOOL), new instance_setting('delay', PARAM_INT), ]; } @@ -188,6 +195,10 @@ public function extend_add_instance_form_definition($mform) { $mform->setType('roles', PARAM_SEQUENCE); $mform->addRule('roles', 'Test', 'required'); + $mform->addElement('advcheckbox', 'invert', get_string('invert', 'lifecycletrigger_byrole'), + get_string('invert_help', 'lifecycletrigger_byrole')); + $mform->setType('invert', PARAM_BOOL); + $elementname = 'delay'; $mform->addElement('duration', $elementname, get_string('delay', 'lifecycletrigger_byrole')); $mform->addHelpButton('delay', 'delay', 'lifecycletrigger_byrole'); diff --git a/trigger/byrole/tests/lifecycletrigger_byrole_test.php b/trigger/byrole/tests/lifecycletrigger_byrole_test.php index 37e40695..15d856cb 100644 --- a/trigger/byrole/tests/lifecycletrigger_byrole_test.php +++ b/trigger/byrole/tests/lifecycletrigger_byrole_test.php @@ -67,7 +67,7 @@ public function test_lib_validcourse(): void { global $DB; $generator = $this->getDataGenerator()->get_plugin_generator('lifecycletrigger_byrole'); $data = $generator->test_create_preparation(); - $recordset = $this->processor->get_course_recordset([$data['trigger']], []); + $recordset = $this->processor->get_course_recordset([$data['trigger']]); foreach ($recordset as $element) { $this->assertNotEquals($data['teachercourse']->id, $element->id, 'The course should not have been triggered'); @@ -87,7 +87,7 @@ public function test_lib_norolecourse(): void { global $DB; $generator = $this->getDataGenerator()->get_plugin_generator('lifecycletrigger_byrole'); $data = $generator->test_create_preparation(); - $recordset = $this->processor->get_course_recordset([$data['trigger']], []); + $recordset = $this->processor->get_course_recordset([$data['trigger']]); foreach ($recordset as $element) { $this->assertNotEquals($data['norolecourse']->id, $element->id, 'The course should not have been triggered'); @@ -108,7 +108,7 @@ public function test_lib_norolefoundcourse(): void { global $DB; $generator = $this->getDataGenerator()->get_plugin_generator('lifecycletrigger_byrole'); $data = $generator->test_create_preparation(); - $recordset = $this->processor->get_course_recordset([$data['trigger']], []); + $recordset = $this->processor->get_course_recordset([$data['trigger']]); $found = false; foreach ($recordset as $element) { @@ -137,7 +137,7 @@ public function test_lib_rolefoundagain(): void { $exist = $DB->record_exists('lifecycletrigger_byrole', ['courseid' => $data['rolefoundagain']->id]); $this->assertEquals(true, $exist); - $recordset = $this->processor->get_course_recordset([$data['trigger']], []); + $recordset = $this->processor->get_course_recordset([$data['trigger']]); foreach ($recordset as $element) { $this->assertNotEquals($data['rolefoundagain']->id, $element->id, 'The course should not have been triggered'); diff --git a/trigger/byrole/version.php b/trigger/byrole/version.php index 47797279..e90b1dbf 100644 --- a/trigger/byrole/version.php +++ b/trigger/byrole/version.php @@ -28,8 +28,5 @@ $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; $plugin->component = 'lifecycletrigger_byrole'; -$plugin->dependencies = [ - 'tool_lifecycle' => 2025050400, -]; $plugin->release = 'v4.5-r1'; $plugin->maturity = MATURITY_STABLE; diff --git a/trigger/categories/lib.php b/trigger/categories/lib.php index 73cf85b9..fc9c84be 100644 --- a/trigger/categories/lib.php +++ b/trigger/categories/lib.php @@ -42,9 +42,9 @@ class categories extends base_automatic { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. - * @param object $course DEPRECATED - * @param int $triggerid DEPRECATED + * If check_course_code() returns true, code to check the given course is placed here + * @param object $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { @@ -87,7 +87,7 @@ public function get_course_recordset_where($triggerid) { [$insql, $inparams] = $DB->get_in_or_equal($allcategories, SQL_PARAMS_NAMED, 'param', !$exclude); - $where = "{course}.category {$insql}"; + $where = "c.category {$insql}"; return [$where, $inparams]; } @@ -106,8 +106,8 @@ public function get_subpluginname() { */ public function instance_settings() { return [ - new instance_setting('categories', PARAM_SEQUENCE, true), - new instance_setting('exclude', PARAM_BOOL, true), + new instance_setting('categories', PARAM_SEQUENCE), + new instance_setting('exclude', PARAM_BOOL), ]; } @@ -119,24 +119,63 @@ public function instance_settings() { * @throws \dml_exception */ public function extend_add_instance_form_definition($mform) { - $displaylist = core_course_category::make_categories_list(); - $options = [ - 'multiple' => true, - 'noselectionstring' => get_string('categories_noselection', 'lifecycletrigger_categories'), - ]; - $mform->addElement('autocomplete', 'categories', - get_string('categories', 'lifecycletrigger_categories'), - $displaylist, $options); - $mform->setType('categories', PARAM_SEQUENCE); + $categories = core_course_category::make_categories_list(); + $type = $mform->getElementType('instancename'); + if ($type == "text") { + $options = [ + 'multiple' => true, + 'noselectionstring' => get_string('categories_noselection', 'lifecycletrigger_categories'), + ]; + $mform->addElement('autocomplete', 'categories', + get_string('categories', 'lifecycletrigger_categories'), + $categories, $options); + $mform->setType('categories', PARAM_SEQUENCE); + + $mform->addElement('advcheckbox', 'exclude', get_string('exclude', 'lifecycletrigger_categories')); + $mform->setType('exclude', PARAM_BOOL); + } + } - $mform->addElement('advcheckbox', 'exclude', get_string('exclude', 'lifecycletrigger_categories')); - $mform->setType('exclude', PARAM_BOOL); + /** + * Since the rendering of frozen autocomplete elements is awful we overide it here. + * @param \MoodleQuickForm $mform + * @param array $settings array containing the settings from the db. + * @throws \coding_exception + */ + public function extend_add_instance_form_definition_after_data($mform, $settings) { + $type = $mform->getElementType('instancename'); + if ($type ?? "" == "text") { + if (is_array($settings) && array_key_exists('categories', $settings)) { + $triggercategories = explode(",", $settings['categories']); + } else { + $triggercategories = []; + } + $categories = core_course_category::make_categories_list(); + $categorieshtml = ""; + foreach ($categories as $key => $value) { + if (in_array($key, $triggercategories)) { + $categorieshtml .= \html_writer::div($value, "badge text-bg-secondary mr-1"); + } + } + $mform->insertElementBefore($mform->createElement( + 'static', + 'categoriesstatic', + get_string('categories', 'lifecycletrigger_categories'), + $categorieshtml), 'buttonar'); + $mform->insertElementBefore($mform->createElement( + 'advcheckbox', + 'exclude', + get_string('exclude', 'lifecycletrigger_categories')), + 'buttonar'); + $mform->setType('exclude', PARAM_BOOL); + } } /** * Ensure validity of settings upon backup restoration. * @param array $settings * @return array List of errors with settings. If empty, the given settings are valid. + * @throws \coding_exception */ public function ensure_validity(array $settings): array { $missingcategories = []; @@ -154,4 +193,12 @@ public function ensure_validity(array $settings): array { } } + /** + * Specifies if this trigger can be used more than once in a single workflow. + * @return bool + */ + public function multiple_use() { + return true; + } + } diff --git a/trigger/categories/tests/trigger_test.php b/trigger/categories/tests/trigger_test.php index df8948b3..b7d63b12 100644 --- a/trigger/categories/tests/trigger_test.php +++ b/trigger/categories/tests/trigger_test.php @@ -91,12 +91,12 @@ public function test_course_has_cat(): void { $course = $this->getDataGenerator()->create_course(['category' => $this->category->id]); - $recordset = $this->processor->get_course_recordset([$this->excludetrigger], []); + $recordset = $this->processor->get_course_recordset([$this->excludetrigger]); foreach ($recordset as $element) { $this->assertNotEquals($course->id, $element->id, 'The course should have been excluded by the trigger'); } $recordset->close(); - $recordset = $this->processor->get_course_recordset([$this->includetrigger], []); + $recordset = $this->processor->get_course_recordset([$this->includetrigger]); $found = false; foreach ($recordset as $element) { if ($course->id === $element->id) { @@ -116,12 +116,12 @@ public function test_course_within_cat(): void { $course = $this->getDataGenerator()->create_course(['category' => $this->childcategory->id]); - $recordset = $this->processor->get_course_recordset([$this->excludetrigger], []); + $recordset = $this->processor->get_course_recordset([$this->excludetrigger]); foreach ($recordset as $element) { $this->assertNotEquals($course->id, $element->id, 'The course should have been excluded by the trigger'); } $recordset->close(); - $recordset = $this->processor->get_course_recordset([$this->includetrigger], []); + $recordset = $this->processor->get_course_recordset([$this->includetrigger]); $found = false; foreach ($recordset as $element) { if ($course->id === $element->id) { @@ -140,12 +140,12 @@ public function test_course_within_cat(): void { public function test_course_not_within_cat(): void { $course = $this->getDataGenerator()->create_course(); - $recordset = $this->processor->get_course_recordset([$this->includetrigger], []); + $recordset = $this->processor->get_course_recordset([$this->includetrigger]); foreach ($recordset as $element) { $this->assertNotEquals($course->id, $element->id, 'The course should have been excluded by the trigger'); } $recordset->close(); - $recordset = $this->processor->get_course_recordset([$this->excludetrigger], []); + $recordset = $this->processor->get_course_recordset([$this->excludetrigger]); $found = false; foreach ($recordset as $element) { if ($course->id === $element->id) { diff --git a/trigger/customfielddelay/README.md b/trigger/customfielddelay/README.md deleted file mode 100644 index 70089b50..00000000 --- a/trigger/customfielddelay/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# moodle-lifecycletrigger_customfielddelay -This is a trigger subplugin for the admin tool [moodle-tool_lifecycle](https://github.com/learnweb/moodle-tool_lifecycle). -A course will be triggered if the value of a specifiable date customfield is further in the past than a specified period. - - diff --git a/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php b/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php index 6f6fcdff..b3871590 100644 --- a/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php +++ b/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php @@ -23,10 +23,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['customfield'] = 'Ein Customfield des Kurses'; -$string['customfield_help'] = 'Der Trigger prüft den Wert dieses Customfields des Kurses.'; -$string['delay'] = 'Zeit seit dem Datum des Customfield, bis ein Prozess gestartet wird'; -$string['delay_help'] = 'Der Trigger wird ausgeführt, falls die Zeit, die seit dem Customfield-Datum des Kurses vergangen ist, größer ist, als der angegebene Zeitraum.'; -$string['plugindescription'] = 'Löst aus wenn das Datum eines zu spezifizierenden Customfield vom Typ Datum nach einem Zeitpunkt in der Zukunft ist.'; -$string['pluginname'] = 'Customfield Datum - Trigger'; +$string['customfield'] = 'Ein nutzerdefiniertes Kursfeld vom Typ Datum'; +$string['customfield_help'] = 'Der Trigger prüft den Wert dieses nutzerdefinierten Kursfeldes vom Typ Datum.'; +$string['delay'] = 'Zeitraum nach dem Datum des nutzerdefinierten Kursfeldes'; +$string['delay_help'] = 'Der Prozess startet sobald der angegebene Zeitraum nach dem nutzerdefinierten Kursfeld-Datum abgelaufen ist.'; +$string['missingfield'] = 'Nutzerdefiniertes Kursfeld "{$a}" vom Typ Datum ist in diesem Moodle nicht vorhanden und muss zuerst angelegt werden.'; +$string['nocustomfields_link'] = 'Link zum Anlegen von Nutzerdefinierten Kursfeldern'; +$string['nocustomfields_warning'] = 'Keine passenden Felder gefunden!'; +$string['plugindescription'] = 'Der Trigger löst aus sobald der angegebene Zeitraum nach dem nutzerdefinierten Kursfeld-Datum abgelaufen ist.'; +$string['pluginname'] = 'Nutzerdefiniertes Kursfeld Typ Datum - Trigger'; $string['privacy:metadata'] = 'Dieses Subplugin speichert keine persönlichen Daten.'; diff --git a/trigger/customfielddelay/lang/en/lifecycletrigger_customfielddelay.php b/trigger/customfielddelay/lang/en/lifecycletrigger_customfielddelay.php index 1f2d3d87..df80e4c1 100644 --- a/trigger/customfielddelay/lang/en/lifecycletrigger_customfielddelay.php +++ b/trigger/customfielddelay/lang/en/lifecycletrigger_customfielddelay.php @@ -23,10 +23,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['customfield'] = 'Customfield of the course'; -$string['customfield_help'] = 'The trigger checks the value saved in the course for this customfield.'; +$string['customfield'] = 'Customfield of type date of the course'; +$string['customfield_help'] = 'The trigger checks the value saved in the course for this customfield of type date.'; $string['delay'] = 'Delay from customfield date of course until starting a process'; $string['delay_help'] = 'The trigger will be invoked if the time passed since the customfield date of the course is longer than this delay.'; +$string['missingfield'] = 'Customfield "{$a}" of type date is missing in this Moodle. Please create it first.'; +$string['nocustomfields_link'] = 'Link to add costumfields to this Moodle site'; +$string['nocustomfields_warning'] = 'No appropriate fields have been defined!'; $string['plugindescription'] = 'Triggers if the value of a specifiable datetype customfield is after a point in the future.'; $string['pluginname'] = 'Customfield date delay trigger'; $string['privacy:metadata'] = 'The lifecycletrigger_customfielddelay plugin does not store any personal data.'; diff --git a/trigger/customfielddelay/lib.php b/trigger/customfielddelay/lib.php index 1aabec35..35adcb73 100644 --- a/trigger/customfielddelay/lib.php +++ b/trigger/customfielddelay/lib.php @@ -16,6 +16,7 @@ namespace tool_lifecycle\trigger; +use moodle_url; use tool_lifecycle\local\manager\settings_manager; use tool_lifecycle\local\response\trigger_response; use tool_lifecycle\settings_type; @@ -34,16 +35,23 @@ class customfielddelay extends base_automatic { /** - * Checks the course and returns a response, which tells if the course should be further processed. - * @param object $course DEPRECATED. - * @param int $triggerid DEPRECATED. + * If check_course_code() returns true, code to check the given course is placed here + * @param object $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { - // Everything is already in the sql statement. return trigger_response::trigger(); } + /** + * Returns whether the lib function check_course contains particular selection code per course or not. + * @return bool + */ + public function check_course_code() { + return false; + } + /** * Add sql comparing the current date to the start date of a course in combination with the specified delay. * @param int $triggerid Id of the trigger. @@ -57,12 +65,13 @@ public function get_course_recordset_where($triggerid) { $delay = settings_manager::get_settings($triggerid, settings_type::TRIGGER)['delay']; $fieldname = settings_manager::get_settings($triggerid, settings_type::TRIGGER)['customfield']; if (!($field = $DB->get_record('customfield_field', ['shortname' => $fieldname, 'type' => 'date']))) { - throw new \moodle_exception("missingfield"); + throw new \moodle_exception('missingfield', + 'lifecycletrigger_customfielddelay', '', $fieldname); } - $where = "{course}.id in (select cxt.instanceid from {context} cxt join {customfield_data} d " . + $where = "c.id in (select cxt.instanceid from {context} cxt join {customfield_data} d " . "ON d.contextid = cxt.id AND cxt.contextlevel=" . CONTEXT_COURSE . " " . - "WHERE d.fieldid = :customfieldid AND d.intvalue > 0 AND d.intvalue < :customfielddelay)"; - $params = ["customfielddelay" => time() - $delay, "customfieldid" => $field->id]; + "WHERE d.fieldid = :customfielddateid AND d.intvalue > 0 AND d.intvalue < :customfielddelay)"; + $params = ["customfielddelay" => time() - $delay, "customfielddateid" => $field->id]; return [$where, $params]; } @@ -79,7 +88,10 @@ public function get_subpluginname() { * @return instance_setting[] containing settings keys and PARAM_TYPES */ public function instance_settings() { - return [new instance_setting('delay', PARAM_INT), new instance_setting('customfield', PARAM_TEXT)]; + return [ + new instance_setting('delay', PARAM_INT), + new instance_setting('customfield', PARAM_TEXT), + ]; } /** @@ -90,15 +102,25 @@ public function instance_settings() { */ public function extend_add_instance_form_definition($mform) { global $DB; - $mform->addElement('duration', 'delay', get_string('delay', 'lifecycletrigger_customfielddelay')); + $mform->addElement('duration', 'delay', + get_string('delay', 'lifecycletrigger_customfielddelay')); $mform->addHelpButton('delay', 'delay', 'lifecycletrigger_customfielddelay'); $fields = $DB->get_records('customfield_field', ['type' => 'date']); $choices = []; foreach ($fields as $field) { $choices[$field->shortname] = $field->name; } - $mform->addElement('select', 'customfield', get_string('customfield', 'lifecycletrigger_customfielddelay'), $choices); - $mform->addHelpButton('customfield', 'customfield', 'lifecycletrigger_customfielddelay'); + if ($choices) { + $mform->addElement('select', 'customfield', + get_string('customfield', 'lifecycletrigger_customfielddelay'), $choices); + $mform->addHelpButton('customfield', 'customfield', + 'lifecycletrigger_customfielddelay'); + } else { + $mform->addElement('static', 'nocustomfields', + get_string('nocustomfields_warning', 'lifecycletrigger_customfielddelay'), + \html_writer::link(new moodle_url('/course/customfield.php'), + get_string('nocustomfields_link', 'lifecycletrigger_customfielddelay'))); + } } /** diff --git a/trigger/customfielddelay/tests/trigger_test.php b/trigger/customfielddelay/tests/trigger_test.php index 0a4c0073..dff2abcf 100644 --- a/trigger/customfielddelay/tests/trigger_test.php +++ b/trigger/customfielddelay/tests/trigger_test.php @@ -87,7 +87,7 @@ public function test_young_course(): void { $customfieldvalue = ['shortname' => 'test', 'value' => time() + 1000000]; $course = $this->getDataGenerator()->create_course(['customfields' => [$customfieldvalue]]); - $recordset = $this->processor->get_course_recordset([$this->triggerinstance], []); + $recordset = $this->processor->get_course_recordset([$this->triggerinstance]); $found = false; foreach ($recordset as $element) { if ($course->id === $element->id) { @@ -118,7 +118,7 @@ public function test_young_course_with_second_customcourse_field(): void { $customfieldvalue2 = ['shortname' => 'test2', 'value' => 100]; $course = $this->getDataGenerator()->create_course(['customfields' => [$customfieldvalue, $customfieldvalue2]]); - $recordset = $this->processor->get_course_recordset([$this->triggerinstance], []); + $recordset = $this->processor->get_course_recordset([$this->triggerinstance]); $found = false; foreach ($recordset as $element) { if ($course->id === $element->id) { @@ -142,7 +142,7 @@ public function test_old_course(): void { $customfieldvalue = ['shortname' => 'test', 'value' => time() - 1000000]; $course = $this->getDataGenerator()->create_course(['customfields' => [$customfieldvalue]]); - $recordset = $this->processor->get_course_recordset([$this->triggerinstance], []); + $recordset = $this->processor->get_course_recordset([$this->triggerinstance]); $found = false; foreach ($recordset as $element) { if ($course->id === $element->id) { diff --git a/trigger/customfielddelay/version.php b/trigger/customfielddelay/version.php index dffdae73..edb4b10d 100644 --- a/trigger/customfielddelay/version.php +++ b/trigger/customfielddelay/version.php @@ -31,4 +31,3 @@ $plugin->component = 'lifecycletrigger_customfielddelay'; $plugin->release = 'v4.5-r1'; $plugin->maturity = MATURITY_STABLE; -$plugin->dependencies = ['customfield_semester' => 2020041304]; diff --git a/trigger/delayedcourses/lib.php b/trigger/delayedcourses/lib.php index d4e8cd55..6859f533 100644 --- a/trigger/delayedcourses/lib.php +++ b/trigger/delayedcourses/lib.php @@ -39,15 +39,23 @@ class delayedcourses extends base_automatic { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. - * @param object $course DEPRECATED. - * @param int $triggerid DEPRECATED + * If check_course_code() returns true, code to check the given course is placed here + * @param object $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { return trigger_response::exclude(); } + /** + * Returns the default response of this trigger. + * @return trigger_response + */ + public function default_response() { + return trigger_response::exclude(); + } + /** * Return sql which excludes delayed courses. * @param int $triggerid Id of the trigger. diff --git a/trigger/lastaccess/classes/privacy/provider.php b/trigger/lastaccess/classes/privacy/provider.php new file mode 100644 index 00000000..5c90fd63 --- /dev/null +++ b/trigger/lastaccess/classes/privacy/provider.php @@ -0,0 +1,39 @@ +. + +namespace lifecycletrigger_lastaccess\privacy; + +use core_privacy\local\metadata\null_provider; + +/** + * Privacy subsystem implementation for lifecycletrigger_lastaccess. + * + * @package lifecycletrigger_lastaccess + * @copyright 2023 Justus Dieckmann WWU Münster + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/trigger/lastaccess/lang/de/lifecycletrigger_lastaccess.php b/trigger/lastaccess/lang/de/lifecycletrigger_lastaccess.php new file mode 100644 index 00000000..74495f90 --- /dev/null +++ b/trigger/lastaccess/lang/de/lifecycletrigger_lastaccess.php @@ -0,0 +1,29 @@ +. + +/** + * Lang strings for last access trigger + * + * @package lifecycletrigger_lastaccess + * @copyright 2019 Yorick Reum JMU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['delay'] = 'Zeitraum nach dem letzten Zugriff auf den Kurs'; +$string['delay_help'] = 'Bezeichnet den Zeitraum nach dem letzten Zugriff auf den Kurs durch eingeschriebene Benutzer:innen, nachdem der Prozess starten soll.'; +$string['plugindescription'] = 'Löst aus, wenn ein zu spezifizierender Zeitraum nach dem letzten Zugriff auf den Kurs abgelaufen ist.'; +$string['pluginname'] = 'Letzter Zugriff - Trigger'; +$string['privacy:metadata'] = 'Dieses Subplugin speichert keine persönlichen Daten.'; diff --git a/trigger/lastaccess/lang/en/lifecycletrigger_lastaccess.php b/trigger/lastaccess/lang/en/lifecycletrigger_lastaccess.php new file mode 100644 index 00000000..c4f2a337 --- /dev/null +++ b/trigger/lastaccess/lang/en/lifecycletrigger_lastaccess.php @@ -0,0 +1,29 @@ +. + +/** + * Lang strings for last access trigger + * + * @package lifecycletrigger_lastaccess + * @copyright 2019 Yorick Reum JMU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['delay'] = 'Delay from last access of course until starting a process'; +$string['delay_help'] = 'Delay from last access of course (only regarding enrolled users) until starting a process.'; +$string['plugindescription'] = 'Triggers when a specifiable period after the last access of course is over.'; +$string['pluginname'] = 'Last access trigger'; +$string['privacy:metadata'] = 'The lifecycletrigger_lastaccess plugin does not store any personal data.'; diff --git a/trigger/lastaccess/lib.php b/trigger/lastaccess/lib.php new file mode 100644 index 00000000..9450f3d6 --- /dev/null +++ b/trigger/lastaccess/lib.php @@ -0,0 +1,130 @@ +. + +/** + * Interface for the subplugintype trigger + * It has to be implemented by all subplugins. + * + * @package lifecycletrigger_lastaccess + * @copyright 2019 Yorick Reum JMU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lifecycle\trigger; + +use tool_lifecycle\local\manager\settings_manager; +use tool_lifecycle\local\response\trigger_response; +use tool_lifecycle\settings_type; + +defined('MOODLE_INTERNAL') || die(); +require_once(__DIR__ . '/../lib.php'); + +/** + * Class which implements the basic methods necessary for a cleanyp courses trigger subplugin + * + * @package lifecycletrigger_lastaccess + */ +class lastaccess extends base_automatic { + /** + * If check_course_code() returns true, code to check the given course is placed here + * @param \stdClass $course + * @param int $triggerid + * @return trigger_response + */ + public function check_course($course, $triggerid) { + // Every decision is already in the where statement. + return trigger_response::trigger(); + } + + /** + * Instance setting delay. + * + * @return instance_setting[] + */ + public function instance_settings() { + return [ + new instance_setting('delay', PARAM_INT), + ]; + } + + /** + * Returns the where statement for all courses that should be triggered, + * meaning timestamp of the last access / interaction with this course is older than delay + * (only counting interactions of users who are enrolled in the course) + * + * @param int $triggerid id of the trigger instance. + * @return string[] + * @throws \coding_exception + * @throws \dml_exception + */ + public function get_course_recordset_where($triggerid) { + + $where = 'c.id IN + (SELECT la.courseid + FROM {user_enrolments} ue + JOIN {enrol} e ON ue.enrolid = e.id + JOIN {user_lastaccess} la ON ue.userid = la.userid + WHERE e.courseid = la.courseid + GROUP BY la.courseid + HAVING MAX(la.timeaccess) < :lastaccessthreshold + )'; + + $delay = settings_manager::get_settings($triggerid, settings_type::TRIGGER)['delay']; + $now = time(); + $params = ["lastaccessthreshold" => $now - $delay]; + + return [$where, $params]; + } + + /** + * Add elements to add instance form + * + * @param \MoodleQuickForm $mform + * @return void + * @throws \coding_exception + */ + public function extend_add_instance_form_definition($mform) { + $elementname = 'delay'; + $mform->addElement('duration', $elementname, get_string($elementname, 'lifecycletrigger_lastaccess')); + $mform->addHelpButton($elementname, $elementname, 'lifecycletrigger_lastaccess'); + } + + /** + * Extend add instance form + * + * @param \MoodleQuickForm $mform + * @param array $settings + * @return void + */ + public function extend_add_instance_form_definition_after_data($mform, $settings) { + if (is_array($settings) && array_key_exists('delay', $settings)) { + $default = $settings['delay']; + } else { + $default = 16416000; + } + $mform->setDefault('delay', $default); + } + + /** + * Return subplugin name + * + * @return string + */ + public function get_subpluginname() { + return 'lastaccess'; + } + +} diff --git a/trigger/lastaccess/version.php b/trigger/lastaccess/version.php new file mode 100644 index 00000000..09e8d0e8 --- /dev/null +++ b/trigger/lastaccess/version.php @@ -0,0 +1,32 @@ +. + +/** + * Life Cycle last access Trigger + * + * @package lifecycletrigger_lastaccess + * @copyright 2019 Yorick Reum JMU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$plugin->version = 2025050400; +$plugin->requires = 2022112800; // Requires Moodle 4.1+. +$plugin->supported = [401, 405]; +$plugin->component = 'lifecycletrigger_lastaccess'; +$plugin->release = 'v4.5-r1'; +$plugin->maturity = MATURITY_STABLE; diff --git a/trigger/lib.php b/trigger/lib.php index d9a63cff..e842525d 100644 --- a/trigger/lib.php +++ b/trigger/lib.php @@ -89,7 +89,6 @@ public function extend_add_instance_form_definition_after_data($mform, $settings public function extend_add_instance_form_validation(&$error, $data) { } - /** * If true, the trigger can be used to manually define workflows, based on an instance of this trigger. * This has to be combined with installing the workflow in db/install.php of the trigger plugin. @@ -125,6 +124,30 @@ public function ensure_validity(array $settings): array { return []; } + /** + * Define description of the trigger. + * Allow subplugins to have custom description. + * + * @return string description of the trigger. + */ + public function get_plugin_description() { + return get_string("pluginname", "lifecycletrigger_" . $this->get_subpluginname()); + } + + /** + * Returns the settings of the trigger. + * + * @return void + */ + public function get_plugin_settings() { + $trigger = $this->get_subpluginname(); + $file = __DIR__ . "/$trigger/settings.php"; + + if (file_exists($file)) { + include($file); + } + } + } /** @@ -138,13 +161,29 @@ public function ensure_validity(array $settings): array { abstract class base_automatic extends base { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. - * @param int $course DEPRECATED - * @param int $triggerid DEPRECATED + * If check_course_code() returns true, code to check the given course is placed here + * @param \stdClass $course + * @param int $triggerid * @return trigger_response */ abstract public function check_course($course, $triggerid); + /** + * Returns whether the lib function check_course contains particular selection code per course or not. + * @return bool + */ + public function check_course_code() { + return false; + } + + /** + * Returns the default response of this trigger. + * @return trigger_response + */ + public function default_response() { + return trigger_response::trigger(); + } + /** * Defines if the trigger subplugin is started manually or automatically. * @return bool @@ -153,6 +192,14 @@ public function is_manual_trigger() { return false; } + /** + * Specifies if this trigger can be used more than once in a single workflow. + * @return bool + */ + public function multiple_use() { + return false; + } + /** * if trigger is of trigger type triggertime it returns the date of the next run. * @param int $triggerid id of the trigger diff --git a/trigger/manual/lib.php b/trigger/manual/lib.php index 37bc2ce9..fc34b64e 100644 --- a/trigger/manual/lib.php +++ b/trigger/manual/lib.php @@ -47,10 +47,11 @@ public function get_subpluginname() { * @return instance_setting[] containing settings keys and PARAM_TYPES */ public function instance_settings() { - return [new instance_setting('icon', PARAM_SAFEPATH), + return [ + new instance_setting('icon', PARAM_SAFEPATH), new instance_setting('displayname', PARAM_TEXT), new instance_setting('capability', PARAM_CAPABILITY), - ]; + ]; } /** diff --git a/trigger/semindependent/lib.php b/trigger/semindependent/lib.php index 9949b3b9..3d0aeb8b 100644 --- a/trigger/semindependent/lib.php +++ b/trigger/semindependent/lib.php @@ -41,8 +41,8 @@ class semindependent extends base_automatic { /** * Returns triggertype of trigger: trigger, triggertime or exclude. - * @param object $course DEPRECATED - * @param int $triggerid DEPRECATED + * @param object $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { @@ -59,9 +59,9 @@ public function check_course($course, $triggerid) { public function get_course_recordset_where($triggerid) { $exclude = settings_manager::get_settings($triggerid, settings_type::TRIGGER)['exclude']; if ($exclude) { - $where = "{course}.startdate > :semindepdate"; + $where = " NOT c.startdate > :semindepdate"; } else { - $where = "{course}.startdate <= :semindepdate"; + $where = "c.startdate > :semindepdate"; } // Date before which a course counts as semester independent. In this case the 1.1.2000. $params = ["semindepdate" => 946688400]; diff --git a/trigger/semindependent/tests/trigger_test.php b/trigger/semindependent/tests/trigger_test.php index 5087defa..1a94799e 100644 --- a/trigger/semindependent/tests/trigger_test.php +++ b/trigger/semindependent/tests/trigger_test.php @@ -16,6 +16,7 @@ namespace lifecycletrigger_semindependent; +use tool_lifecycle\local\entity\trigger_subplugin; use tool_lifecycle\processor; use tool_lifecycle_trigger_semindependent_generator as generator; @@ -43,6 +44,9 @@ final class trigger_test extends \advanced_testcase { /**@var \stdClass course with startdate now */ private $semcourse; + /**@var trigger_subplugin instance of trigger */ + private $triggerinstance; + /** * Setup function for the trigger test. * @return void @@ -64,7 +68,7 @@ public function test_include_semester_independent(): void { $this->triggerinstance = generator::create_workflow_with_semindependent(false); - $recordset = $this->processor->get_course_recordset([$this->triggerinstance], []); + $recordset = $this->processor->get_course_recordset([$this->triggerinstance]); $foundsem = false; $foundsemindep = false; foreach ($recordset as $element) { @@ -89,7 +93,7 @@ public function test_exclude_semester_independent(): void { $this->triggerinstance = generator::create_workflow_with_semindependent(true); - $recordset = $this->processor->get_course_recordset([$this->triggerinstance], []); + $recordset = $this->processor->get_course_recordset([$this->triggerinstance]); $foundsem = false; $foundsemindep = false; foreach ($recordset as $element) { diff --git a/trigger/semindependent/version.php b/trigger/semindependent/version.php index 5e27fd3f..3cb9da42 100644 --- a/trigger/semindependent/version.php +++ b/trigger/semindependent/version.php @@ -29,8 +29,5 @@ $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; $plugin->component = 'lifecycletrigger_semindependent'; -$plugin->dependencies = [ - 'tool_lifecycle' => 2025050400, -]; $plugin->release = 'v4.5-r1'; $plugin->maturity = MATURITY_STABLE; diff --git a/trigger/sitecourse/lib.php b/trigger/sitecourse/lib.php index dfaba56c..7581703e 100644 --- a/trigger/sitecourse/lib.php +++ b/trigger/sitecourse/lib.php @@ -37,9 +37,9 @@ class sitecourse extends base_automatic { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. - * @param object $course DEPRECATED - * @param int $triggerid DEPRECATED + * If check_course_code() returns true, code to check the given course is placed here + * @param object $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { diff --git a/trigger/specificdate/lib.php b/trigger/specificdate/lib.php index eb0a1ddb..d5a67d6d 100644 --- a/trigger/specificdate/lib.php +++ b/trigger/specificdate/lib.php @@ -42,15 +42,23 @@ class specificdate extends base_automatic { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. - * @param object $course DEPRECATED - * @param int $triggerid DEPRECATED + * If check_course_code() returns true, code to check the given course is placed here + * @param object $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { return trigger_response::triggertime(); } + /** + * Returns the default response of this trigger. + * @return trigger_response + */ + public function default_response() { + return trigger_response::triggertime(); + } + /** * Returns true or false, depending on if the current date is one of the specified days, * at which the trigger should run. @@ -172,7 +180,6 @@ public function get_subpluginname() { public function instance_settings() { return [ new instance_setting('dates', PARAM_TEXT), - // Add activate timelastrun. new instance_setting('timelastrunactive', PARAM_INT), new instance_setting('timelastrun', PARAM_INT), ]; diff --git a/trigger/startdatedelay/lib.php b/trigger/startdatedelay/lib.php index 286271e9..d1c934e3 100644 --- a/trigger/startdatedelay/lib.php +++ b/trigger/startdatedelay/lib.php @@ -40,9 +40,9 @@ class startdatedelay extends base_automatic { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. - * @param object $course DEPRECATED - * @param int $triggerid DEPRECATED + * If check_course_code() returns true, code to check the given course is placed here + * @param object $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { diff --git a/trigger/startdatedelay/tests/trigger_test.php b/trigger/startdatedelay/tests/trigger_test.php index d7b71ae0..7ca5197d 100644 --- a/trigger/startdatedelay/tests/trigger_test.php +++ b/trigger/startdatedelay/tests/trigger_test.php @@ -65,7 +65,7 @@ public function test_young_course(): void { $course = $this->getDataGenerator()->create_course(['startdate' => time() - 50 * 24 * 60 * 60]); - $recordset = $this->processor->get_course_recordset([$this->triggerinstance], []); + $recordset = $this->processor->get_course_recordset([$this->triggerinstance]); $found = false; foreach ($recordset as $element) { if ($course->id === $element->id) { @@ -84,7 +84,7 @@ public function test_old_course(): void { $course = $this->getDataGenerator()->create_course(['startdate' => time() - 200 * 24 * 60 * 60]); - $recordset = $this->processor->get_course_recordset([$this->triggerinstance], []); + $recordset = $this->processor->get_course_recordset([$this->triggerinstance]); $found = false; foreach ($recordset as $element) { if ($course->id === $element->id) { diff --git a/uploadworkflow.php b/uploadworkflow.php index df375a70..c4b91f04 100644 --- a/uploadworkflow.php +++ b/uploadworkflow.php @@ -32,8 +32,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::UPLOAD_WORKFLOW)); @@ -45,9 +44,6 @@ $renderer = $PAGE->get_renderer('tool_lifecycle'); $heading = get_string('pluginname', 'tool_lifecycle')." / ".get_string('upload_workflow', 'tool_lifecycle'); -echo $renderer->header($heading); -$tabrow = tabs::get_tabrow(); -$renderer->tabs($tabrow, ''); $form = new form_upload_workflow(); if ($form->is_cancelled()) { @@ -69,6 +65,9 @@ redirect(new moodle_url(urls::WORKFLOW_DETAILS, ['wf' => $restore->get_workflow()->id])); } } +echo $renderer->header($heading); +$tabrow = tabs::get_tabrow(); +$renderer->tabs($tabrow, ''); $form->display(); echo $renderer->footer(); diff --git a/version.php b/version.php index 4b6e2dd4..ca9b2fa0 100644 --- a/version.php +++ b/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die; $plugin->maturity = MATURITY_STABLE; -$plugin->version = 2025050404; +$plugin->version = 2025092300; $plugin->component = 'tool_lifecycle'; $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; -$plugin->release = 'v4.5-r5'; +$plugin->release = 'v4.5-r6'; diff --git a/view.php b/view.php index 695c798d..23293830 100644 --- a/view.php +++ b/view.php @@ -18,7 +18,6 @@ * Display the list of courses relevant for a specific user in a specific step instance. * * @package tool_lifecycle -use tool_lifecycle\tabs; * @copyright 2017 Tobias Reischmann WWU * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/workflowdrafts.php b/workflowdrafts.php index d419885b..e35cd79a 100644 --- a/workflowdrafts.php +++ b/workflowdrafts.php @@ -30,8 +30,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); -require_capability('moodle/site:config', context_system::instance()); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::WORKFLOW_DRAFTS)); diff --git a/workflowoverview.php b/workflowoverview.php index 8680cb1f..fb8498c5 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -26,23 +26,27 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); -define('PAGESIZE', 20); +define('PAGESIZE', 10); use core\output\notification; +use core\output\single_button; use core\task\manager; use tool_lifecycle\action; +use tool_lifecycle\event\process_rollback; use tool_lifecycle\event\process_triggered; use tool_lifecycle\local\manager\delayed_courses_manager; use tool_lifecycle\local\manager\lib_manager; use tool_lifecycle\local\manager\process_manager; +use tool_lifecycle\local\manager\settings_manager; use tool_lifecycle\local\manager\step_manager; use tool_lifecycle\local\manager\trigger_manager; use tool_lifecycle\local\manager\workflow_manager; use tool_lifecycle\local\response\trigger_response; use tool_lifecycle\local\table\courses_in_step_table; -use tool_lifecycle\local\table\triggered_courses_table; +use tool_lifecycle\local\table\triggered_courses_table_trigger; +use tool_lifecycle\local\table\triggered_courses_table_workflow; use tool_lifecycle\processor; use tool_lifecycle\settings_type; use tool_lifecycle\tabs; @@ -57,9 +61,14 @@ $triggered = optional_param('triggered', null, PARAM_INT); $excluded = optional_param('excluded', null, PARAM_INT); $delayed = optional_param('delayed', null, PARAM_INT); +$processes = optional_param('processes', null, PARAM_INT); $used = optional_param('used', null, PARAM_INT); $search = optional_param('search', null, PARAM_RAW); $showdetails = optional_param('showdetails', 0, PARAM_INT); +$showsql = optional_param('showsql', 0, PARAM_INT); +$showtablesql = optional_param('showtablesql', 0, PARAM_INT); +$debugtriggersql = ""; +$debugtablesql = ""; if ($showdetails == 0) { if (isset($SESSION->showdetails)) { if ($SESSION->showdetails == $workflowid) { @@ -68,8 +77,15 @@ } } else if ($showdetails == -1) { $SESSION->showdetails = $showdetails = 0; + $SESSION->debugtriggersql = $debugtriggersql = ''; + $SESSION->debugtablesql = $debugtablesql = ''; } else { $SESSION->showdetails = $workflowid; + if ($showtablesql && isset($SESSION->debugtablesql)) { + $debugtablesql = $SESSION->debugtablesql; + } else if ($showsql && isset($SESSION->debugtriggersql)) { + $debugtriggersql = $SESSION->debugtriggersql; + } } $workflow = workflow_manager::get_workflow($workflowid); @@ -88,6 +104,8 @@ $params['delayed'] = $delayed; } else if ($excluded) { $params['excluded'] = $excluded; +} else if ($processes) { + $params['processes'] = $processes; } if ($search) { $params['search'] = $search; @@ -117,12 +135,16 @@ $msg = get_string('courseselected', 'tool_lifecycle'); } else if ($action == 'deletedelay') { $cid = required_param('cid', PARAM_INT); - $DB->delete_records('tool_lifecycle_delayed_workf', ['courseid' => $cid]); + $DB->delete_records('tool_lifecycle_delayed_workf', ['courseid' => $cid, 'workflowid' => $workflow->id]); delayed_courses_manager::remove_delay_entry($cid); $msg = get_string('delaydeleted', 'tool_lifecycle'); } else { - step_manager::handle_action($action, optional_param('actionstep', null, PARAM_INT), $workflow->id); - trigger_manager::handle_action($action, optional_param('actiontrigger', null, PARAM_INT), $workflow->id); + if ($actionstep = optional_param('actionstep', null, PARAM_INT)) { + step_manager::handle_action($action, $actionstep, $workflow->id); + } + if ($actiontrigger = optional_param('actiontrigger', null, PARAM_INT)) { + trigger_manager::handle_action($action, $actiontrigger, $workflow->id); + } $processid = optional_param('processid', null, PARAM_INT); if ($processid) { $process = process_manager::get_process_by_id($processid); @@ -150,25 +172,24 @@ $heading = get_string('pluginname', 'tool_lifecycle')." / ". get_string('workflowoverview', 'tool_lifecycle').": ". $workflow->title; echo $renderer->header($heading); -$activelink = false; -$deactivatedlink = false; -$draftlink = false; + +$tabparams = new stdClass(); if ($isactive) { // Active workflow. $id = 'activeworkflows'; - $activelink = true; + $tabparams->activelink = true; $classdetails = "bg-primary text-white"; } else { if ($isdeactivated) { // Deactivated workflow. $id = 'deactivatedworkflows'; - $deactivatedlink = true; + $tabparams->deactivatedlink = true; $classdetails = "bg-dark text-white"; } else { // Draft. $id = 'workflowdrafts'; - $draftlink = true; + $tabparams->draftlink = true; $classdetails = "bg-light"; } } -$tabrow = tabs::get_tabrow($activelink, $deactivatedlink, $draftlink); +$tabrow = tabs::get_tabrow($tabparams); $renderer->tabs($tabrow, $id); $steps = step_manager::get_step_instances($workflow->id); @@ -201,35 +222,45 @@ $task = manager::get_scheduled_task('tool_lifecycle\task\lifecycle_task'); $lastrun = $task->get_last_run_time(); $nextrunt = $task->get_next_run_time(); +$nextrunout = ""; if (!$task->is_component_enabled() && !$task->get_run_if_component_disabled()) { $nextrunt = get_string('plugindisabled', 'tool_task'); } else if ($task->get_disabled()) { $nextrunt = get_string('taskdisabled', 'tool_task'); -} else if ($nextrunt < time()) { +} else if (is_numeric($nextrunt) && $nextrunt < time()) { $nextrunt = get_string('asap', 'tool_task'); } -if (is_int($nextrunt) && is_int($nextrun)) { // Task nextrun and trigger nextrun are valid times: take the minimum. - $nextrun = userdate(min($nextrunt, $nextrun), get_string('strftimedatetimeshort', 'langconfig')); -} else if (!is_int($nextrunt) && is_int($nextrun)) { // Only trigger nextrun is valid time. - $nextrun = userdate($nextrun, get_string('strftimedatetimeshort', 'langconfig')); -} else if (is_int($nextrunt)) { // Only task next run is valid time. - $nextrun = userdate($nextrunt, get_string('strftimedatetimeshort', 'langconfig')); +if (is_numeric($nextrunt) && is_numeric($nextrun)) { // Task nextrun and trigger nextrun are valid times: take the minimum. + $nextrunout = min($nextrunt, $nextrun); +} else if (!is_numeric($nextrunt) && is_numeric($nextrun)) { // Only trigger nextrun is valid time. + $nextrun = $nextrun; +} else if (is_numeric($nextrunt)) { // Only task next run is valid time. + $nextrunout = $nextrunt; } else { // There is no valid next run time. Print the task message. - $nextrun = $nextrunt; + $nextrunout = $nextrunt; +} +if (is_numeric($nextrunout)) { + if ($nextrunout) { + $nextrunout = userdate($nextrunout, get_string('strftimedatetimeshort', 'langconfig'), + core_date::get_user_timezone($USER)); + } else { + $nextrunout = get_string('statusunknown'); + } } +$nomanualtriggerinvolved = true; $displaytriggers = []; $displaytimetriggers = []; -$displaysteps = []; -$nomanualtriggerinvolved = true; foreach ($triggers as $trigger) { // The array from the DB Function uses ids as keys. // Mustache cannot handle arrays which have other keys therefore a new array is build. // FUTURE: Nice to have Icon for each subplugin. $trigger = (object)(array) $trigger; // Cast to normal object to be able to set dynamic properties. + $editlink = new moodle_url(urls::EDIT_ELEMENT, ['type' => settings_type::TRIGGER, 'elementid' => $trigger->id]); + $trigger->editlink = $editlink->out(); $actionmenu = new action_menu([ new action_menu_link_secondary( - new moodle_url(urls::EDIT_ELEMENT, ['type' => settings_type::TRIGGER, 'elementid' => $trigger->id]), + $editlink, new pix_icon('i/edit', $str['edit']), $str['edit']), ]); if ($iseditable) { @@ -243,7 +274,8 @@ $response = null; $lib = lib_manager::get_trigger_lib($trigger->subpluginname); if ($trigger->automatic = !$lib->is_manual_trigger()) { - $response = $lib->check_course(null, null); + // We need the response type only. + $response = $lib->default_response(); } $nomanualtriggerinvolved &= $trigger->automatic; if ($showdetails) { @@ -253,15 +285,32 @@ $trigger->classfires = "border-danger"; $trigger->additionalinfo = $amounts[$trigger->sortindex]->additionalinfo ?? "-"; } else { + $settings = settings_manager::get_settings($trigger->id, settings_type::TRIGGER); + $trigger->exclude = $settings['exclude'] ?? false; if ($response != trigger_response::triggertime()) { if ($amounts[$trigger->sortindex]->triggered) { $trigger->classfires = "border-success"; } else if ($amounts[$trigger->sortindex]->excluded) { $trigger->classfires = "border-danger"; } - $trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded; - $trigger->triggeredcourses = $amounts[$trigger->sortindex]->triggered; - $trigger->alreadyin = $amounts[$trigger->sortindex]->alreadyin; + $trigger->tooltip = ""; + if ($amounts[$trigger->sortindex]->excluded !== false) { + $trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded ?? 0; + $trigger->tooltip = get_string('courses_will_be_excluded', + 'tool_lifecycle', $trigger->excludedcourses); + } else { + $trigger->triggeredcourses = $amounts[$trigger->sortindex]->triggered; + $trigger->tooltip = get_string('courses_will_be_triggered', + 'tool_lifecycle', $trigger->triggeredcourses); + if ($trigger->delayedcourses = $amounts[$trigger->sortindex]->delayed) { + $trigger->tooltip .= get_string('courses_candidates_delayed', + 'tool_lifecycle', $trigger->delayedcourses); + } + if ($trigger->alreadyin = $amounts[$trigger->sortindex]->alreadyin) { + $trigger->tooltip .= get_string('courses_candidates_alreadyin', + 'tool_lifecycle', $trigger->alreadyin); + } + } } } } @@ -269,14 +318,17 @@ } if ($response == trigger_response::triggertime()) { $displaytimetriggers[] = $trigger; - if (isset($amounts[$trigger->sortindex]->lastrun)) { + if (isset($amounts[$trigger->sortindex]->lastrun) && $amounts[$trigger->sortindex]->lastrun) { $lastrun = $amounts[$trigger->sortindex]->lastrun; + } else { + $lastrun = 0; } } else { $displaytriggers[] = $trigger; } } +$displaysteps = []; foreach ($steps as $step) { $step = (object)(array) $step; // Cast to normal object to be able to set dynamic properties. $ncourses = $DB->count_records('tool_lifecycle_process', @@ -285,9 +337,11 @@ if ($step->id == $stepid) { $step->selected = true; } + $editlink = new moodle_url(urls::EDIT_ELEMENT, ['type' => settings_type::STEP, 'elementid' => $step->id]); + $step->editlink = $editlink->out(); $actionmenu = new action_menu([ new action_menu_link_secondary( - new moodle_url(urls::EDIT_ELEMENT, ['type' => settings_type::STEP, 'elementid' => $step->id]), + $editlink, new pix_icon('i/edit', $str['edit']), $str['edit']), ]); if ($iseditable) { @@ -312,6 +366,10 @@ } } $step->actionmenu = $OUTPUT->render($actionmenu); + if ($step->rollbacktosortindex) { + $steptorollback = step_manager::get_step_instance_by_workflow_index($workflowid, $step->rollbacktosortindex); + $step->rollbacktosortindexname = $steptorollback->instancename; + } $displaysteps[] = $step; } @@ -335,58 +393,108 @@ $tablecoursesamount = $courseids; } else if ($triggerid) { // Display courses table with triggered courses of this trigger. $trigger = trigger_manager::get_instance($triggerid); - if ($courseids = (new processor())->get_triggercourses($trigger, $workflow)) { - $table = new triggered_courses_table($courseids, 'triggered', $trigger->instancename, - null, $workflow->id, $search); + if ($amounts[$trigger->sortindex]->triggered ?? false) { + $table = new triggered_courses_table_trigger($trigger, 'triggerid', $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); ob_end_clean(); + if ($table->otherwf > 0 || $table->delayed > 0) { + $a = new \stdClass(); + $a->otherwf = $table->otherwf; + $a->delayed = $table->delayed; + $out .= \html_writer::div(get_string('total').": ".$table->tablerows." ". + get_string('courses')." ".get_string('numbersotherwfordelayed', 'tool_lifecycle', $a), 'm-3'); + } else { + $out .= \html_writer::div(get_string('total').": ".$table->tablerows." ". + get_string('courses'), 'm-3'); + } $hiddenfieldssearch[] = ['name' => 'trigger', 'value' => $triggerid]; - $tablecoursesamount = count($courseids); + $tablecoursesamount = $amounts[$trigger->sortindex]->triggered; } -} else if ($triggered) { // Display courses table with triggered courses of this workflow. - $table = new triggered_courses_table($coursestriggered, 'triggeredworkflow', null, - $workflow->title, $workflow->id, $search); - ob_start(); - $table->out(PAGESIZE, false); - $out = ob_get_contents(); - ob_end_clean(); - $hiddenfieldssearch[] = ['name' => 'triggered', 'value' => $triggered]; - $tablecoursesamount = count($coursestriggered); } else if ($excluded) { // Display courses table with excluded courses of this trigger. $trigger = trigger_manager::get_instance($excluded); - if ($courseids = (new processor())->get_triggercourses($trigger, $workflow)) { - $table = new triggered_courses_table($courseids, 'exclude', $trigger->instancename, null, null, $search); + if ($amounts[$trigger->sortindex]->excluded ?? false) { + $table = new triggered_courses_table_trigger($trigger, 'excluded', $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); ob_end_clean(); + if ($table->otherwf > 0 || $table->delayed > 0) { + $a = new \stdClass(); + $a->otherwf = $table->otherwf; + $a->delayed = $table->delayed; + $out .= \html_writer::div(get_string('total').": ".$table->tablerows." ". + get_string('courses')." ".get_string('numbersotherwfordelayed', 'tool_lifecycle', $a), 'm-3'); + } else { + $out .= \html_writer::div(get_string('total').": ".$table->tablerows." ". + get_string('courses'), 'm-3'); + } $hiddenfieldssearch[] = ['name' => 'excluded', 'value' => $excluded]; - $tablecoursesamount = count($courseids); + $tablecoursesamount = $amounts[$trigger->sortindex]->excluded; + } +} else if ($triggered) { // Display courses table with triggered courses of this workflow. + if ($coursestriggered ?? false) { + $table = new triggered_courses_table_workflow($coursestriggered, $workflow, 'triggeredworkflow', $search); + ob_start(); + $table->out(PAGESIZE, false); + $out = ob_get_contents(); + ob_end_clean(); + $out .= \html_writer::div(get_string('total').": ".$table->tablerows." ". + get_string('courses'), 'm-3'); + $hiddenfieldssearch[] = ['name' => 'triggered', 'value' => $triggered]; + $tablecoursesamount = $coursestriggered; } } else if ($delayed) { // Display courses table with courses delayed for this workflow. - $table = new triggered_courses_table( $coursesdelayed, 'delayed', - null, $workflow->title, $workflowid, $search); - ob_start(); - $table->out(PAGESIZE, false); - $out = ob_get_contents(); - ob_end_clean(); - $hiddenfieldssearch[] = ['name' => 'delayed', 'value' => $delayed]; - $tablecoursesamount = count($coursesdelayed); + if ($coursesdelayed ?? false) { + $table = new triggered_courses_table_workflow($coursesdelayed, $workflow, 'delayed', $search); + ob_start(); + $table->out(PAGESIZE, false); + $out = ob_get_contents(); + ob_end_clean(); + $out .= \html_writer::div(get_string('total').": ".$table->tablerows." ". + get_string('courses'), 'm-3'); + $hiddenfieldssearch[] = ['name' => 'delayed', 'value' => $delayed]; + $tablecoursesamount = $coursesdelayed; + } } else if ($used) { // Display courses triggered by this workflow but involved in other processes already. - if ($courseids = $amounts['all']->used ?? null) { - $table = new triggered_courses_table( $courseids, 'used', - null, $workflow->title, $workflowid, $search); + if ($amounts['all']->hasotherwf ?? null) { + $table = new triggered_courses_table_workflow($amounts['all']->hasotherwf, $workflow, 'used', $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); ob_end_clean(); + $out .= \html_writer::div(get_string('total').": ".$table->tablerows." ". + get_string('courses'), 'm-3'); $hiddenfieldssearch[] = ['name' => 'used', 'value' => $used]; - $tablecoursesamount = count($courseids); + $tablecoursesamount = $amounts['all']->used; + } +} else if ($processes) { // Display courses table with courses in a process or in state process error for this workflow. + $coursesinprocess = process_manager::count_processes_by_workflow($workflow->id) + + process_manager::count_process_errors_by_workflow($workflow->id); + if ($coursesinprocess) { + $table = new triggered_courses_table_workflow($coursesinprocess, $workflow, 'processes', $search); + ob_start(); + $table->out(PAGESIZE, false); + $out = ob_get_contents(); + ob_end_clean(); + $out .= \html_writer::div(get_string('total').": ".$table->totalrows." ". + get_string('courses'), 'm-3'); + $hiddenfieldssearch[] = ['name' => 'processes', 'value' => $processes]; + $tablecoursesamount = $coursesinprocess; } +} else if ($showsql && $debugtriggersql) { // Display the trigger sql statement stored in session. + $out = \html_writer::div( + str_replace("}", "", str_replace("{", $CFG->prefix, $debugtriggersql)), + "p-3" + ); +} else if ($showsql && $debugtablesql) { // Display the table sql statement stored in session. + $out .= \html_writer::div( + str_replace("}", "", str_replace("{", $CFG->prefix, $debugtablesql)), "p-3" + ); } -// Search box for courses list. + +// Search box for the course list. $searchhtml = ''; if ($tablecoursesamount > PAGESIZE ) { $searchhtml = $renderer->render_from_template('tool_lifecycle/search_input', [ @@ -400,7 +508,50 @@ 'hiddenfields' => $hiddenfieldssearch, ]); } +$disableworkflowlink = ""; +$abortdisableworkflowlink = ""; +$workflowprocesseslink = ""; +if ($isactive) { + // Disable workflow link. + $alt = get_string('disableworkflow', 'tool_lifecycle'); + $icon = 't/disable'; + $url = new \moodle_url(urls::DEACTIVATED_WORKFLOWS, + ['workflowid' => $workflow->id, 'action' => action::WORKFLOW_DISABLE, 'sesskey' => sesskey()]); + $confirmaction = new \confirm_action(get_string('disableworkflow_confirm', 'tool_lifecycle')); + $disableworkflowlink = $OUTPUT->action_icon($url, + new \pix_icon($icon, $alt, 'tool_lifecycle', ['title' => $alt, 'class' => 'text-white']), + $confirmaction, + ['title' => $alt] + ); + $disableworkflowlink = "
".$disableworkflowlink; + // Abort workflow link. + $alt = get_string('abortdisableworkflow', 'tool_lifecycle'); + $icon = 't/stop'; + $url = new \moodle_url(urls::DEACTIVATED_WORKFLOWS, + ['workflowid' => $workflow->id, 'action' => action::WORKFLOW_ABORTDISABLE, 'sesskey' => sesskey()]); + $confirmaction = new \confirm_action(get_string('abortdisableworkflow_confirm', 'tool_lifecycle')); + $abortdisableworkflowlink = $OUTPUT->action_icon($url, + new \pix_icon($icon, $alt, 'moodle', ['title' => $alt, 'class' => 'text-white']), + $confirmaction, + ['title' => $alt] + ); + $abortdisableworkflowlink = "
".$abortdisableworkflowlink; + // Workflow processes and process errors link. + if ($coursesinprocess = process_manager::count_processes_by_workflow($workflow->id) + + process_manager::count_process_errors_by_workflow($workflow->id)) { + $ldata = new \stdClass(); + $ldata->alt = get_string('workflow_processesanderrors', 'tool_lifecycle'); + $ldata->url = new moodle_url($popuplink, ['processes' => $workflowid, 'showdetails' => $showdetails]); + $ldata->processes = process_manager::count_processes_by_workflow($workflow->id) + + process_manager::count_process_errors_by_workflow($workflow->id); + $workflowprocesseslink = $OUTPUT->render_from_template('tool_lifecycle/overview_processeslink', $ldata);; + $workflowprocesseslink = "
".$workflowprocesseslink; + } +} +if (!($isactive || $isdeactivated)) { + $lastrun = 0; +} $data = [ 'editsettingslink' => (new moodle_url(urls::EDIT_WORKFLOW, ['wf' => $workflow->id]))->out(false), 'title' => $workflow->title, @@ -425,31 +576,36 @@ 'showdetailslink' => $showdetailslink, 'showdetailsicon' => $showdetails == 0, 'isactive' => $isactive || $isdeactivated, - 'nextrun' => $nextrun, - 'lastrun' => userdate($lastrun, get_string('strftimedatetimeshort', 'langconfig')), + 'nextrun' => $nextrunout, + 'lastrun' => $lastrun != 0 ? + userdate($lastrun, get_string('strftimedatetimeshort', 'langconfig'), + core_date::get_user_timezone($USER)) : '--', 'nomanualtriggerinvolved' => $nomanualtriggerinvolved, + 'disableworkflowlink' => $disableworkflowlink, + 'abortdisableworkflowlink' => $abortdisableworkflowlink, + 'workflowprocesseslink' => $workflowprocesseslink, + 'runnable' => $isactive, + 'runlink' => new \moodle_url(urls::RUN), ]; if ($showdetails) { // The triggers total box. $data['displaytotaltriggered'] = $displaytotaltriggered; - $triggered = $amounts['all']->triggered ?? 0; + $triggered = $amounts['all']->coursestriggered ?? 0; $triggeredhtml = $triggered > 0 ? html_writer::span($triggered, 'text-success font-weight-bold') : 0; $data['coursestriggered'] = $triggeredhtml; $data['coursestriggeredcount'] = $triggered; - if ($triggered) { - // Count delayed total, displayed in mustache only if there are any. - $delayed = count($amounts['all']->delayedcourses); // Matters only if delayed courses are not included in workflow. - $delayedlink = new moodle_url($popuplink, ['delayed' => $workflowid]); - $delayedhtml = $delayed > 0 ? html_writer::link($delayedlink, $delayed, - ['class' => 'btn btn-outline-secondary mt-1']) : 0; - $data['coursesdelayed'] = $delayedhtml; - // Count in other processes used courses total, displayed in mustache only if there are any. - $used = count($amounts['all']->used) ?? 0; - $usedlink = new moodle_url($popuplink, ['used' => "1"]); - $usedhtml = $used > 0 ? html_writer::link($usedlink, $used, - ['class' => 'btn btn-outline-secondary mt-1']) : 0; - $data['coursesused'] = $usedhtml; - } + // Count delayed total, displayed in mustache only if there are any. + $delayed = $amounts['all']->delayedcourses ?? 0; // Matters only if delayed courses are not included in workflow. + $delayedlink = new moodle_url($popuplink, ['delayed' => $workflowid]); + $delayedhtml = $delayed > 0 ? html_writer::link($delayedlink, $delayed, + ['class' => 'btn btn-outline-secondary mt-1']) : 0; + $data['coursesdelayed'] = $delayedhtml; + // Count in other processes used courses total, displayed in mustache only if there are any. + $used = $amounts['all']->hasotherwf ?? 0; + $usedlink = new moodle_url($popuplink, ['used' => "1"]); + $usedhtml = $used > 0 ? html_writer::link($usedlink, $used, + ['class' => 'btn btn-outline-secondary mt-1']) : 0; + $data['coursesused'] = $usedhtml; } $addtriggerselect = ""; @@ -465,14 +621,18 @@ if ($triggers) { foreach ($triggers as $workflowtrigger) { if ($triggertype == $workflowtrigger->subpluginname) { - continue 2; + $lib = lib_manager::get_trigger_lib($triggertype); + if (!trigger_manager::trigger_multipleuse($workflowtrigger->subpluginname)) { + continue 2; + } } } } else { // After workflow creation only provide course selection (and manual) triggers for the adding a trigger selection field. $lib = lib_manager::get_trigger_lib($triggertype); if (!$lib->is_manual_trigger()) { - if ($lib->check_course(null, null) == trigger_response::triggertime()) { + // Function check_course: We need the response type only. + if ($lib->default_response() == trigger_response::triggertime()) { continue; } } @@ -504,8 +664,8 @@ ]), get_string('activateworkflow', 'tool_lifecycle')); } else { - $activate = get_string('invalid_workflow', 'tool_lifecycle'). - $OUTPUT->pix_icon('i/circleinfo', get_string('invalid_workflow_details', 'tool_lifecycle')); + $activate = get_string('invalid_workflow', 'tool_lifecycle').html_writer::link("#", + $OUTPUT->pix_icon('i/circleinfo', get_string('invalid_workflow_details', 'tool_lifecycle'))); } } } @@ -513,6 +673,17 @@ $data['addstepselect'] = $addstepselect; $data['activate'] = $activate; $data['newworkflow'] = $newworkflow; + $data['activatebutton'] = ""; +} else if ($isdeactivated) { + $activate = $OUTPUT->single_button(new \moodle_url(urls::ACTIVE_WORKFLOWS, + [ + 'action' => action::WORKFLOW_ACTIVATE, + 'sesskey' => sesskey(), + 'workflowid' => $workflow->id, + 'backtooverview' => '1', + ]), + get_string('activateworkflow', 'tool_lifecycle')); + $data['activatebutton'] = $activate; } echo $OUTPUT->render_from_template('tool_lifecycle/workflowoverview', $data);