From 8f3663e353dd6d56118f0b50e74b3089cbf2290e Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 28 May 2025 04:21:21 +0200 Subject: [PATCH 001/170] fix adminapprove sql error single approve --- step/adminapprove/approvestep.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/step/adminapprove/approvestep.php b/step/adminapprove/approvestep.php index c2711cae..ad921b04 100644 --- a/step/adminapprove/approvestep.php +++ b/step/adminapprove/approvestep.php @@ -106,7 +106,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) { From 99fc6f063074be084781c517f51fe48079dea777 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 3 Jun 2025 06:47:50 +0200 Subject: [PATCH 002/170] catch,prevent missing workflow error --- classes/local/manager/delayed_courses_manager.php | 3 +++ classes/processor.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/classes/local/manager/delayed_courses_manager.php b/classes/local/manager/delayed_courses_manager.php index b298aad7..7097175f 100644 --- a/classes/local/manager/delayed_courses_manager.php +++ b/classes/local/manager/delayed_courses_manager.php @@ -53,6 +53,9 @@ public static function set_course_delayed_for_workflow($courseid, $becauserollba $workflow = $workfloworid; } else { $workflow = workflow_manager::get_workflow($workfloworid); + if ($workflow === null) { + throw new \moodle_exception('Set course delayed: no workflow found. '. var_dump($workfloworid)); + } } if ($becauserollback) { $duration = $workflow->rollbackdelay; diff --git a/classes/processor.php b/classes/processor.php index 37be60a5..0264ce66 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -89,7 +89,7 @@ public function call_trigger() { public function process_courses() { foreach (process_manager::get_processes() as $process) { $workflow = workflow_manager::get_workflow($process->workflowid); - while (true) { + while ($workflow) { try { $course = get_course($process->courseid); From c32ae4b8b745731672c67a14497200fb4da0e531 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 3 Jun 2025 10:44:58 +0200 Subject: [PATCH 003/170] fix uploadworkflow redirect --- uploadworkflow.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uploadworkflow.php b/uploadworkflow.php index 45e1ba01..eb0bc6f8 100644 --- a/uploadworkflow.php +++ b/uploadworkflow.php @@ -44,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()) { @@ -68,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(); From c84ecf1efabe1db42d9fb06b8cfaf3fb8d55f055 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 4 Jun 2025 08:32:51 +0200 Subject: [PATCH 004/170] workflowoverview: show also delayed trigger courses but only when still in delay --- classes/processor.php | 36 ++++++++++++++++++++--------- templates/overview_trigger.mustache | 3 +++ workflowoverview.php | 1 + 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 0264ce66..1bf8d825 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -254,23 +254,32 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) * @throws \coding_exception * @throws \dml_exception */ - public function get_triggercourses_forcounting($trigger, $exclude) { + public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { 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); + // We just want the triggered courses here, no matter of including or excluding. $where = str_replace(" NOT ", " ", $sql); + // Now get the amount of courses triggered by this trigger. + $sql = 'SELECT {course}.id from {course} WHERE '. $where; + $triggercoursesall = $DB->get_fieldset_sql($sql, $whereparams); + + // Get number of delayed courses which would be triggered by this trigger. + $delayedcourses = array_intersect($triggercoursesall, $delayed); + // 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); + if (!empty($excluded)) { + [$insql, $inparams] = $DB->get_in_or_equal($excluded, SQL_PARAMS_NAMED); $where .= " AND NOT {course}.id {$insql}"; $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 = 'SELECT count({course}.id) from {course} WHERE '. $where; + $triggercourses = $DB->count_records_sql($sql, $whereparams); + + // Only get courses which are not part of this workflow yet. $sql .= " AND {course}.id NOT IN (". "SELECT {course}.id from {course} LEFT JOIN {tool_lifecycle_process} @@ -278,9 +287,9 @@ public function get_triggercourses_forcounting($trigger, $exclude) { 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); + $newcourses = $DB->count_records_sql($sql, $whereparams); - return [count($triggercourses), count($newcourses)]; + return [$triggercourses, $newcourses, count($delayedcourses)]; } /** @@ -374,16 +383,21 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { if ($obj->sql != "false") { if ($obj->response == trigger_response::exclude()) { // Get courses excluded amount. - [$triggercourses, $newcourses] = $this->get_triggercourses_forcounting($trigger, $excludedcourses); + [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting($trigger, $excludedcourses, + $delayedcourses); $obj->excluded = $triggercourses; + $obj->delayed = $delayed; $obj->alreadyin = $triggercourses - $newcourses; } else if ($obj->response == trigger_response::trigger()) { // Get courses triggered amount. - [$triggercourses, $newcourses] = $this->get_triggercourses_forcounting($trigger, $excludedcourses); + [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting($trigger, $excludedcourses, + $delayedcourses); if ($trigger->exclude) { $obj->excluded = $triggercourses; + $obj->delayed = $delayed; } else { $obj->triggered = $triggercourses; + $obj->delayed = $delayed; } $obj->alreadyin = $triggercourses - $newcourses; } @@ -420,7 +434,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { if ($course->workflowid && ($course->workflowid != $workflow->id)) { $usedcourses[] = $course->id; } - } else if ($course->delay) { + } else if ($course->delay && $course->delay > time()) { $delayedcourses[] = $course->id; } else { $counttriggered++; diff --git a/templates/overview_trigger.mustache b/templates/overview_trigger.mustache index 0067c3f4..c8f4bf14 100644 --- a/templates/overview_trigger.mustache +++ b/templates/overview_trigger.mustache @@ -71,6 +71,9 @@ 0 {{/excludedcourses}} {{/triggeredcourses}} + {{#delayedcourses}} + {{delayedcourses}} + {{/delayedcourses}} {{#alreadyin}} {{alreadyin}} {{/alreadyin}} diff --git a/workflowoverview.php b/workflowoverview.php index 81278f3e..41985d02 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -260,6 +260,7 @@ } $trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded; $trigger->triggeredcourses = $amounts[$trigger->sortindex]->triggered; + $trigger->delayedcourses = $amounts[$trigger->sortindex]->delayed; $trigger->alreadyin = $amounts[$trigger->sortindex]->alreadyin; } } From 1b9c6d425f0e8a38efa367d376f6b211ae02ae86 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 4 Jun 2025 09:05:42 +0200 Subject: [PATCH 005/170] codechecker issue --- classes/processor.php | 5 +++-- lang/de/tool_lifecycle.php | 2 +- lang/en/tool_lifecycle.php | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 1bf8d825..8deade3b 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -249,8 +249,9 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) * Returns the amount 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. + * @param int[] $excluded List of course id, which should be excluded from counting. + * @param int[] $delayed List of course ids of delayed courses (globally and for workflow). + * @return int[] amount of triggered courses and amount which courses of them would be added and which courses are delayed. * @throws \coding_exception * @throws \dml_exception */ diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index ca346201..8e49212f 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -147,7 +147,7 @@ Seitenadministration/Plugins/Dienstprogramme/Kurs-Lebenszyklus/Allgemein & Subplugins."; $string['errornobackup'] = "Es wurde kein Backup in dem angegebenen Pfad erstellt."; $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'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 0db8b4d7..1f886e81 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -147,7 +147,7 @@ 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['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'; From a877bbc0444f9ff8de30c8199e3dde1d42665144 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 08:22:16 +0200 Subject: [PATCH 006/170] shift showdetails icon to block trigger --- templates/workflowoverview.mustache | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 98b595b3..df251c6a 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -97,7 +97,16 @@ {{#counttriggers}}
-
{{#str}} courseselection_title, tool_lifecycle{{/str}}
+
{{#str}} courseselection_title, tool_lifecycle{{/str}} + + {{#showdetailsicon}} + + {{/showdetailsicon}} + {{^showdetailsicon}} + + {{/showdetailsicon}} + +
{{#showcoursecounts}} {{#displaytotaltriggered}}
From 911144bcbd24b061dd1d019f6bdee8b72302c222 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 10:39:47 +0200 Subject: [PATCH 007/170] activestep.php: make approval tab an active link --- classes/event/process_rollback.php | 2 +- classes/local/manager/trigger_manager.php | 2 +- classes/tabs.php | 9 +++++++-- step/adminapprove/approvestep.php | 2 +- workflowoverview.php | 8 ++++++-- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/classes/event/process_rollback.php b/classes/event/process_rollback.php index 9a76c675..3fbeb611 100644 --- a/classes/event/process_rollback.php +++ b/classes/event/process_rollback.php @@ -45,7 +45,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 diff --git a/classes/local/manager/trigger_manager.php b/classes/local/manager/trigger_manager.php index 030c1e98..eb27d4f9 100644 --- a/classes/local/manager/trigger_manager.php +++ b/classes/local/manager/trigger_manager.php @@ -262,7 +262,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 diff --git a/classes/tabs.php b/classes/tabs.php index c5a54875..166c81e1 100644 --- a/classes/tabs.php +++ b/classes/tabs.php @@ -44,7 +44,12 @@ class tabs { * @throws \dml_exception * @throws moodle_exception */ - public static function get_tabrow($activelink = false, $deactivatelink = false, $draftlink = false) { + public static function get_tabrow( + $activelink = false, + $deactivatelink = false, + $draftlink = false, + $approvelink = false + ) { global $DB; $classnotnull = 'badge badge-primary badge-pill ml-1'; @@ -143,7 +148,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/step/adminapprove/approvestep.php b/step/adminapprove/approvestep.php index ad921b04..e5579665 100644 --- a/step/adminapprove/approvestep.php +++ b/step/adminapprove/approvestep.php @@ -144,7 +144,7 @@ $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); +$tabrow = tabs::get_tabrow(false, false, false, true); $id = optional_param('id', 'adminapprove', PARAM_TEXT); $renderer->tabs($tabrow, $id); diff --git a/workflowoverview.php b/workflowoverview.php index 41985d02..b0d3c05f 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -120,8 +120,12 @@ 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); From 76ff4c8af3095ec381f48be25a3474d94071b525 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 10:49:45 +0200 Subject: [PATCH 008/170] codechecker issue --- classes/tabs.php | 1 + 1 file changed, 1 insertion(+) diff --git a/classes/tabs.php b/classes/tabs.php index 166c81e1..89e783b3 100644 --- a/classes/tabs.php +++ b/classes/tabs.php @@ -39,6 +39,7 @@ class 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 bool $approvelink display approvals tab as link * @return array of tabobjects * @throws \coding_exception * @throws \dml_exception From 8fa492cb1ca5bfa455c169c899350f7f9a752f74 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 10:58:29 +0200 Subject: [PATCH 009/170] exclude trigger customfieldsemester from git --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b45ed724..b843897e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ # Used for simulating cron during development development.php +trigger/customfieldsemester From 336f75bce44b55496015547d3577ba7eca6a6a65 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 12:27:59 +0200 Subject: [PATCH 010/170] mtrace processes without workflow and delete processes of removed courses --- classes/local/manager/delayed_courses_manager.php | 4 ++-- classes/local/manager/process_manager.php | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/classes/local/manager/delayed_courses_manager.php b/classes/local/manager/delayed_courses_manager.php index 7097175f..5313bbd3 100644 --- a/classes/local/manager/delayed_courses_manager.php +++ b/classes/local/manager/delayed_courses_manager.php @@ -53,8 +53,8 @@ public static function set_course_delayed_for_workflow($courseid, $becauserollba $workflow = $workfloworid; } else { $workflow = workflow_manager::get_workflow($workfloworid); - if ($workflow === null) { - throw new \moodle_exception('Set course delayed: no workflow found. '. var_dump($workfloworid)); + if (!$workflow) { + mtrace("Set course delayed: no workflow found. Course-ID:{$courseid}, var_dump workfloworid:". var_dump($workfloworid)); } } if ($becauserollback) { diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index 49b4c1a1..f4688959 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -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) { From c0faee49e9b15ccc383d1214ff9115a2dee949d5 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 12:35:33 +0200 Subject: [PATCH 011/170] codechecker issue --- classes/local/manager/delayed_courses_manager.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/classes/local/manager/delayed_courses_manager.php b/classes/local/manager/delayed_courses_manager.php index 5313bbd3..98d2e900 100644 --- a/classes/local/manager/delayed_courses_manager.php +++ b/classes/local/manager/delayed_courses_manager.php @@ -54,7 +54,8 @@ public static function set_course_delayed_for_workflow($courseid, $becauserollba } 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)); + mtrace("Set course delayed: no workflow found. Course-ID:{$courseid}, + var_dump workfloworid:". var_dump($workfloworid)); } } if ($becauserollback) { From 7a202e6c67237ea1dfbb917adf33a5ca207b7b78 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 13:42:09 +0200 Subject: [PATCH 012/170] remove customfield_semester dependency --- classes/local/form/form_trigger_instance.php | 4 ++-- classes/local/manager/trigger_manager.php | 12 +++++++++++- trigger/customfielddelay/version.php | 3 +-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/classes/local/form/form_trigger_instance.php b/classes/local/form/form_trigger_instance.php index 64aa2bc0..bf775e23 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 diff --git a/classes/local/manager/trigger_manager.php b/classes/local/manager/trigger_manager.php index eb27d4f9..bb004f71 100644 --- a/classes/local/manager/trigger_manager.php +++ b/classes/local/manager/trigger_manager.php @@ -238,7 +238,17 @@ 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); + } } return $result; } diff --git a/trigger/customfielddelay/version.php b/trigger/customfielddelay/version.php index dffdae73..d9e0eabf 100644 --- a/trigger/customfielddelay/version.php +++ b/trigger/customfielddelay/version.php @@ -30,5 +30,4 @@ $plugin->supported = [401, 405]; $plugin->component = 'lifecycletrigger_customfielddelay'; $plugin->release = 'v4.5-r1'; -$plugin->maturity = MATURITY_STABLE; -$plugin->dependencies = ['customfield_semester' => 2020041304]; +$plugin->maturity = MATURITY_STABLE; \ No newline at end of file From 70581fb371b62ff34c318c9c8d64a48bf89aabbc Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 13:51:35 +0200 Subject: [PATCH 013/170] codechecker issue --- trigger/customfielddelay/version.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trigger/customfielddelay/version.php b/trigger/customfielddelay/version.php index d9e0eabf..edb4b10d 100644 --- a/trigger/customfielddelay/version.php +++ b/trigger/customfielddelay/version.php @@ -30,4 +30,4 @@ $plugin->supported = [401, 405]; $plugin->component = 'lifecycletrigger_customfielddelay'; $plugin->release = 'v4.5-r1'; -$plugin->maturity = MATURITY_STABLE; \ No newline at end of file +$plugin->maturity = MATURITY_STABLE; From 8cecd99695ecbc01008893accf1d5383be32c941 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 15:58:54 +0200 Subject: [PATCH 014/170] remove old showdetailicon --- templates/workflowoverview.mustache | 8 +------- trigger/customfieldsemester | 1 + 2 files changed, 2 insertions(+), 7 deletions(-) create mode 160000 trigger/customfieldsemester diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index df251c6a..0267ef5b 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -76,13 +76,7 @@
- {{#pix}} i/edit, core, {{/pix}}
- {{#showdetailsicon}} - - {{/showdetailsicon}} - {{^showdetailsicon}} - - {{/showdetailsicon}} + {{#pix}} i/edit, core, {{/pix}}

{{title}} {{#includedelayedcourses}}{{/includedelayedcourses}} diff --git a/trigger/customfieldsemester b/trigger/customfieldsemester new file mode 160000 index 00000000..23d4f9dc --- /dev/null +++ b/trigger/customfieldsemester @@ -0,0 +1 @@ +Subproject commit 23d4f9dc8bc69beebd3b6fe4d22ba5a890c9e6be From b32a04fc08a965c2bb516ef85ea785c0da2700f5 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 16:29:16 +0200 Subject: [PATCH 015/170] improve display of next run time --- workflowoverview.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/workflowoverview.php b/workflowoverview.php index b0d3c05f..205efbf7 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -204,26 +204,31 @@ $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_int($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')); + $nextrunout = min($nextrunt, $nextrun); } else if (!is_int($nextrunt) && is_int($nextrun)) { // Only trigger nextrun is valid time. - $nextrun = userdate($nextrun, get_string('strftimedatetimeshort', 'langconfig')); + $nextrun = $nextrun; } else if (is_int($nextrunt)) { // Only task next run is valid time. - $nextrun = userdate($nextrunt, get_string('strftimedatetimeshort', 'langconfig')); + $nextrunout = $nextrunt; } else { // There is no valid next run time. Print the task message. - $nextrun = $nextrunt; + $nextrunout = $nextrunt; +} +if (is_int($nextrunout)) { + if ($nextrunout) { + $nextrunout = userdate($nextrunout, get_string('strftimedatetimeshort', 'langconfig')); + } else { + $nextrunout = get_string('statusunknown'); + } } -$displaytriggers = []; -$displaytimetriggers = []; -$displaysteps = []; $nomanualtriggerinvolved = true; foreach ($triggers as $trigger) { // The array from the DB Function uses ids as keys. @@ -429,7 +434,7 @@ 'showdetailslink' => $showdetailslink, 'showdetailsicon' => $showdetails == 0, 'isactive' => $isactive || $isdeactivated, - 'nextrun' => $nextrun, + 'nextrun' => $nextrunout, 'lastrun' => userdate($lastrun, get_string('strftimedatetimeshort', 'langconfig')), 'nomanualtriggerinvolved' => $nomanualtriggerinvolved, ]; From 087929b559d7a02c2b193ac4ee8136093f3f1e91 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 17:10:26 +0200 Subject: [PATCH 016/170] fix .gitignore syntax error --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b843897e..5e256d5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ # Used for simulating cron during development development.php -trigger/customfieldsemester +trigger/customfieldsemester/ From b36266c74f8d7d59697a56bd69b44ec7b80df411 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 17:24:29 +0200 Subject: [PATCH 017/170] remove trigger customfieldsemester from github --- trigger/customfieldsemester | 1 - 1 file changed, 1 deletion(-) delete mode 160000 trigger/customfieldsemester diff --git a/trigger/customfieldsemester b/trigger/customfieldsemester deleted file mode 160000 index 23d4f9dc..00000000 --- a/trigger/customfieldsemester +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 23d4f9dc8bc69beebd3b6fe4d22ba5a890c9e6be From 7eb4874c8844690075a37f55a25b32f241105d7e Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 17:25:22 +0200 Subject: [PATCH 018/170] another fix in .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5e256d5d..861e95e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ # Used for simulating cron during development development.php -trigger/customfieldsemester/ +customfieldsemester/ From 8c6f58929272585202e6383aa6e52ef953ed2861 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 21:19:44 +0200 Subject: [PATCH 019/170] fix behat error after unintentionally deleting code lines --- workflowoverview.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workflowoverview.php b/workflowoverview.php index 205efbf7..f5bcb11f 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -230,6 +230,8 @@ } $nomanualtriggerinvolved = true; +$displaytriggers = []; +$displaytimetriggers = []; 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. @@ -286,6 +288,7 @@ } } +$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', From 30d4fefe2adcefa6c59c61c611f42d291821d14a Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 23:04:23 +0200 Subject: [PATCH 020/170] remove exceptions for external customfieldsemester plugin --- lang/de/tool_lifecycle.php | 1 - lang/en/tool_lifecycle.php | 1 - settings.php | 21 ++++++++------------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 8e49212f..12c66353 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -103,7 +103,6 @@ $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'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 1f886e81..f52f2897 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -104,7 +104,6 @@ $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'; diff --git a/settings.php b/settings.php index c97c413f..0fff323e 100644 --- a/settings.php +++ b/settings.php @@ -76,20 +76,15 @@ if ($trigger == 'sitecourse' || $trigger == 'delayedcourses') { $uninstall = html_writer::span(' Depracated. Will be removed with version 5.0.', 'text-danger'); } - if ($trigger == 'customfieldsemester') { - $settings->add(new admin_setting_description('lifecycletriggersetting_'.$trigger, - $triggername, - get_string('customfieldsemesterdescription', 'tool_lifecycle'))); - } else { - try { - $plugindescription = get_string('plugindescription', 'lifecycletrigger_' . $trigger); - } catch (Exception $e) { - $plugindescription = ""; - } - $settings->add(new admin_setting_description('lifecycletriggersetting_'.$trigger, - $triggername, - $plugindescription.$uninstall)); + try { + $plugindescription = get_string('plugindescription', 'lifecycletrigger_' . $trigger); + } catch (Exception $e) { + $plugindescription = ""; } + $settings->add(new admin_setting_description('lifecycletriggersetting_'.$trigger, + $triggername, + $plugindescription.$uninstall + )); } } else { $settings->add(new admin_setting_heading('adminsettings_notriggers', From 933cd99f6ddfff69a6241e49b6b4627c02202deb Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 23:34:36 +0200 Subject: [PATCH 021/170] trigger byrole: introduce invert function --- trigger/byrole/lang/de/lifecycletrigger_byrole.php | 2 ++ trigger/byrole/lang/en/lifecycletrigger_byrole.php | 2 ++ trigger/byrole/lib.php | 8 +++++++- 3 files changed, 11 insertions(+), 1 deletion(-) 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..a5c48ecd 100644 --- a/trigger/byrole/lib.php +++ b/trigger/byrole/lib.php @@ -100,9 +100,11 @@ private function update_courses($triggerid) { list($insql, $inparams) = $DB->get_in_or_equal($this->get_roles($triggerid), SQL_PARAMS_NAMED); + $invert = settings_manager::get_settings($triggerid, settings_type::TRIGGER)['invert'] && true; + $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 +161,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 +191,9 @@ 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'); From 94a9c2d390c87423df8666d6bd5e3de41cf7dce9 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 5 Jun 2025 23:55:57 +0200 Subject: [PATCH 022/170] prevent from deleting course 1 --- step/deletecourse/lib.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/step/deletecourse/lib.php b/step/deletecourse/lib.php index 1509cdaf..36531247 100644 --- a/step/deletecourse/lib.php +++ b/step/deletecourse/lib.php @@ -44,7 +44,7 @@ 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. @@ -59,11 +59,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->id); /* Fix 'delete & backup (other) course aftwerwards' error, which is created by moodle core issue MDL-65228 (https://tracker.moodle.org/browse/MDL-65228) */ @@ -77,7 +82,7 @@ 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. @@ -86,6 +91,8 @@ public function process_course($processid, $instanceid, $course) { * @param int $instanceid of the step instance. * @param mixed $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); From 978abe540f2efbbc762fa8b4adb753d2301e25d0 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 6 Jun 2025 01:01:39 +0200 Subject: [PATCH 023/170] add step movecategory --- step/movecategory/README.md | 12 ++ step/movecategory/interactionlib.php | 106 ++++++++++++++++++ .../lang/de/lifecyclestep_movecategory.php | 28 +++++ .../lang/en/lifecyclestep_movecategory.php | 28 +++++ step/movecategory/lib.php | 94 ++++++++++++++++ step/movecategory/version.php | 35 ++++++ 6 files changed, 303 insertions(+) create mode 100644 step/movecategory/README.md create mode 100644 step/movecategory/interactionlib.php create mode 100644 step/movecategory/lang/de/lifecyclestep_movecategory.php create mode 100644 step/movecategory/lang/en/lifecyclestep_movecategory.php create mode 100644 step/movecategory/lib.php create mode 100644 step/movecategory/version.php diff --git a/step/movecategory/README.md b/step/movecategory/README.md new file mode 100644 index 00000000..1604044d --- /dev/null +++ b/step/movecategory/README.md @@ -0,0 +1,12 @@ +# moodle-lifecycle-movecategorystep + +[WIP] This is a step-subplugin for the admin tool [moodle-tool_lifecycle](https://github.com/learnweb/moodle-tool_lifecycle). +It simply moves the triggered courses to a category which can be specified in the step configuration. +For example, here at the University of Würzburg we use this plugin to move older courses to an invisible category (before we finally delete them). + +## Settings +Site administrators selects one category (from a dropdown) where the courses will be moved to. + +## More information +For detailed information on step plugins visit the +[Wiki](https://github.com/learnweb/moodle-tool_lifecycle/wiki) of the moodle-tool_lifecycle admin tool. diff --git a/step/movecategory/interactionlib.php b/step/movecategory/interactionlib.php new file mode 100644 index 00000000..a85d89b8 --- /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(); + } +} \ No newline at end of file diff --git a/step/movecategory/lang/de/lifecyclestep_movecategory.php b/step/movecategory/lang/de/lifecyclestep_movecategory.php new file mode 100644 index 00000000..fc0e4d70 --- /dev/null +++ b/step/movecategory/lang/de/lifecyclestep_movecategory.php @@ -0,0 +1,28 @@ +. + +/** + * 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'; diff --git a/step/movecategory/lang/en/lifecyclestep_movecategory.php b/step/movecategory/lang/en/lifecyclestep_movecategory.php new file mode 100644 index 00000000..0f978687 --- /dev/null +++ b/step/movecategory/lang/en/lifecyclestep_movecategory.php @@ -0,0 +1,28 @@ +. + +/** + * 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'; diff --git a/step/movecategory/lib.php b/step/movecategory/lib.php new file mode 100644 index 00000000..211ba603 --- /dev/null +++ b/step/movecategory/lib.php @@ -0,0 +1,94 @@ +. + +/** + * 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 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 movecategory extends libbase { + + /** + * Processes the course and returns a repsonse. + * 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. + * @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( + array($course->id), $categoryid + ); + + if ($success) { + return step_response::proceed(); + } else { + return step_response::rollback(); + } + + } + + public function instance_settings() { + return array( + new instance_setting('categorytomoveto', PARAM_INT), + ); + } + + public function extend_add_instance_form_definition($mform) { + global $DB; + + $elementname = 'categorytomoveto'; + $categories = $DB->get_records('course_categories'); + + + //Fetch a complete list of courses and let it be shown in a flat hierachical 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); + } + + public function get_subpluginname() { + return 'movecategory'; + } +} diff --git a/step/movecategory/version.php b/step/movecategory/version.php new file mode 100644 index 00000000..11f50d0f --- /dev/null +++ b/step/movecategory/version.php @@ -0,0 +1,35 @@ +. + +/** + * 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->dependencies = [ + 'tool_lifecycle' => 2025050400, +]; +$plugin->release = 'v4.5-r1'; +$plugin->maturity = MATURITY_STABLE; From 61a98ddb4736238d9b8803ae0ef77781730d4124 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 6 Jun 2025 01:45:12 +0200 Subject: [PATCH 024/170] codechecker issues --- step/movecategory/interactionlib.php | 2 +- step/movecategory/lib.php | 37 +++++++++++++++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/step/movecategory/interactionlib.php b/step/movecategory/interactionlib.php index a85d89b8..e3bc78d0 100644 --- a/step/movecategory/interactionlib.php +++ b/step/movecategory/interactionlib.php @@ -103,4 +103,4 @@ public function get_action_string($action, $userlink) { public function handle_interaction($process, $step, $action = 'default') { return step_interactive_response::no_action(); } -} \ No newline at end of file +} diff --git a/step/movecategory/lib.php b/step/movecategory/lib.php index 211ba603..42d770a9 100644 --- a/step/movecategory/lib.php +++ b/step/movecategory/lib.php @@ -33,10 +33,13 @@ require_once(__DIR__ . '/../lib.php'); +/** + * Class for processing courses. + */ class movecategory 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. @@ -56,7 +59,7 @@ public function process_course($processid, $instanceid, $course) { )['categorytomoveto']; $success = move_courses( - array($course->id), $categoryid + [$course->id], $categoryid ); if ($success) { @@ -67,27 +70,43 @@ public function process_course($processid, $instanceid, $course) { } + /** + * Settings for the movecategory step + * + * @return instance_setting[] categorymoveto + */ public function instance_settings() { - return array( + 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) { - global $DB; $elementname = 'categorytomoveto'; - $categories = $DB->get_records('course_categories'); - - //Fetch a complete list of courses and let it be shown in a flat hierachical view with all parent branches + // 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->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'; } From 985de878c4a1462ea571c193588233cf0a532ba5 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 6 Jun 2025 01:53:20 +0200 Subject: [PATCH 025/170] codechecker issue --- trigger/byrole/lib.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trigger/byrole/lib.php b/trigger/byrole/lib.php index a5c48ecd..7473022e 100644 --- a/trigger/byrole/lib.php +++ b/trigger/byrole/lib.php @@ -191,7 +191,8 @@ 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->addElement('advcheckbox', 'invert', get_string('invert', 'lifecycletrigger_byrole'), + get_string('invert_help', 'lifecycletrigger_byrole')); $mform->setType('invert', PARAM_BOOL); $elementname = 'delay'; From 9f2c41e3ac2d2a9af232491e533f232c1134895b Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 6 Jun 2025 12:39:38 +0200 Subject: [PATCH 026/170] add trigger lastaccess --- step/movecategory/README.md | 12 -- .../movecategory/classes/privacy/provider.php | 39 +++++ .../lang/de/lifecyclestep_movecategory.php | 1 + .../lang/en/lifecyclestep_movecategory.php | 1 + trigger/customfielddelay/README.md | 5 - .../de/lifecycletrigger_customfielddelay.php | 12 +- .../en/lifecycletrigger_customfielddelay.php | 4 +- .../lastaccess/classes/privacy/provider.php | 39 +++++ .../lang/de/lifecycletrigger_lastaccess.php | 29 ++++ .../lang/en/lifecycletrigger_lastaccess.php | 29 ++++ trigger/lastaccess/lib.php | 134 ++++++++++++++++++ trigger/lastaccess/version.php | 35 +++++ 12 files changed, 315 insertions(+), 25 deletions(-) delete mode 100644 step/movecategory/README.md create mode 100644 step/movecategory/classes/privacy/provider.php delete mode 100644 trigger/customfielddelay/README.md create mode 100644 trigger/lastaccess/classes/privacy/provider.php create mode 100644 trigger/lastaccess/lang/de/lifecycletrigger_lastaccess.php create mode 100644 trigger/lastaccess/lang/en/lifecycletrigger_lastaccess.php create mode 100644 trigger/lastaccess/lib.php create mode 100644 trigger/lastaccess/version.php diff --git a/step/movecategory/README.md b/step/movecategory/README.md deleted file mode 100644 index 1604044d..00000000 --- a/step/movecategory/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# moodle-lifecycle-movecategorystep - -[WIP] This is a step-subplugin for the admin tool [moodle-tool_lifecycle](https://github.com/learnweb/moodle-tool_lifecycle). -It simply moves the triggered courses to a category which can be specified in the step configuration. -For example, here at the University of Würzburg we use this plugin to move older courses to an invisible category (before we finally delete them). - -## Settings -Site administrators selects one category (from a dropdown) where the courses will be moved to. - -## More information -For detailed information on step plugins visit the -[Wiki](https://github.com/learnweb/moodle-tool_lifecycle/wiki) of the moodle-tool_lifecycle admin tool. 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/lang/de/lifecyclestep_movecategory.php b/step/movecategory/lang/de/lifecyclestep_movecategory.php index fc0e4d70..db75a915 100644 --- a/step/movecategory/lang/de/lifecyclestep_movecategory.php +++ b/step/movecategory/lang/de/lifecyclestep_movecategory.php @@ -26,3 +26,4 @@ $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 index 0f978687..d1e95ff8 100644 --- a/step/movecategory/lang/en/lifecyclestep_movecategory.php +++ b/step/movecategory/lang/en/lifecyclestep_movecategory.php @@ -26,3 +26,4 @@ $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/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..37f0a6c0 100644 --- a/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php +++ b/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php @@ -23,10 +23,10 @@ * @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['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..280ecf60 100644 --- a/trigger/customfielddelay/lang/en/lifecycletrigger_customfielddelay.php +++ b/trigger/customfielddelay/lang/en/lifecycletrigger_customfielddelay.php @@ -23,8 +23,8 @@ * @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['plugindescription'] = 'Triggers if the value of a specifiable datetype customfield is after a point in the future.'; 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..145ae629 --- /dev/null +++ b/trigger/lastaccess/lib.php @@ -0,0 +1,134 @@ +. + +/** + * 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 { + /** + * Checks the course and returns a response, which tells if the course should be further processed. + * + * @param \stdClass $course DEPRECATED + * @param int $triggerid DEPRECATED + * @return trigger_response + */ + public function check_course($course, $triggerid) { + // Every decision is already in the where statement. + return trigger_response::trigger(); + } + + /** + * @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 $triggerid int id of the trigger instance. + * @return array + * @throws \coding_exception + * @throws \dml_exception + */ + public function get_course_recordset_where($triggerid) { + global $DB; + + $sql = "SELECT la.courseid + FROM mdl_user_enrolments AS ue + JOIN mdl_enrol AS e ON (ue.enrolid = e.id) + JOIN mdl_user_lastaccess AS 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(); + + try { + $records = $DB->get_records_sql($sql, ["lastaccessthreshold" => $now - $delay]); + } catch (\dml_exception $e) { + $records = []; + } + + $courseids = array_column($records, 'courseid'); + + [$insql, $inparams] = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); + $where = "{course}.id {$insql}"; + return [$where, $inparams]; + } + + /** + * 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'); + } + + /** + * @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 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..a73d1a16 --- /dev/null +++ b/trigger/lastaccess/version.php @@ -0,0 +1,35 @@ +. + +/** + * 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->dependencies = [ + 'tool_lifecycle' => 2025050400, +]; +$plugin->release = 'v4.5-r1'; +$plugin->maturity = MATURITY_STABLE; From fbe51ca68139527b3f12afc2d29375a3318aa007 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 6 Jun 2025 13:06:09 +0200 Subject: [PATCH 027/170] codechecker issues --- trigger/lastaccess/lib.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/trigger/lastaccess/lib.php b/trigger/lastaccess/lib.php index 145ae629..575590cf 100644 --- a/trigger/lastaccess/lib.php +++ b/trigger/lastaccess/lib.php @@ -51,6 +51,8 @@ public function check_course($course, $triggerid) { } /** + * Instance setting delay. + * * @return instance_setting[] */ public function instance_settings() { @@ -64,21 +66,20 @@ public function instance_settings() { * 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 $triggerid int id of the trigger instance. - * @return array + * @return string[] * @throws \coding_exception * @throws \dml_exception */ public function get_course_recordset_where($triggerid) { global $DB; - $sql = "SELECT la.courseid - FROM mdl_user_enrolments AS ue - JOIN mdl_enrol AS e ON (ue.enrolid = e.id) - JOIN mdl_user_lastaccess AS la ON (ue.userid = la.userid) - WHERE e.courseid = la.courseid - GROUP BY la.courseid + $sql = "SELECT la.courseid + FROM mdl_user_enrolments AS ue + JOIN mdl_enrol AS e ON (ue.enrolid = e.id) + JOIN mdl_user_lastaccess AS 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']; @@ -111,6 +112,8 @@ public function extend_add_instance_form_definition($mform) { } /** + * Extend add instance form + * * @param \MoodleQuickForm $mform * @param array $settings * @return void @@ -125,6 +128,8 @@ public function extend_add_instance_form_definition_after_data($mform, $settings } /** + * Return subplugin name + * * @return string */ public function get_subpluginname() { From cc0d6d49bbc5bc052b3cb0224ae82f3675f9fc32 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 6 Jun 2025 13:24:03 +0200 Subject: [PATCH 028/170] codechecker issue --- trigger/lastaccess/lib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trigger/lastaccess/lib.php b/trigger/lastaccess/lib.php index 575590cf..77207793 100644 --- a/trigger/lastaccess/lib.php +++ b/trigger/lastaccess/lib.php @@ -66,7 +66,7 @@ public function instance_settings() { * 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 $triggerid int id of the trigger instance. + * @param int $triggerid id of the trigger instance. * @return string[] * @throws \coding_exception * @throws \dml_exception From 1cb01ac303be61d135a7eab465a66c52916c7f9b Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 10 Jun 2025 16:31:44 +0200 Subject: [PATCH 029/170] overall workflow filter - part 1 --- activeprocesses.php | 4 +++- classes/tabs.php | 13 +++++++------ renderer.php | 9 ++++++++- step/adminapprove/approvestep.php | 4 +++- workflowoverview.php | 9 +++++++-- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/activeprocesses.php b/activeprocesses.php index ff60506a..05eb5b40 100644 --- a/activeprocesses.php +++ b/activeprocesses.php @@ -70,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/classes/tabs.php b/classes/tabs.php index 89e783b3..dd692a2e 100644 --- a/classes/tabs.php +++ b/classes/tabs.php @@ -45,14 +45,15 @@ class tabs { * @throws \dml_exception * @throws moodle_exception */ - public static function get_tabrow( - $activelink = false, - $deactivatelink = false, - $draftlink = false, - $approvelink = false - ) { + public static function get_tabrow($params = null) { global $DB; + $activelink = isset($params->activelink); + $deactivatelink = isset($params->deactivatelink); + $draftlink = isset($params->draftlink); + $approvelink = isset($params->approvelink); + $wfid = isset($params->wfid); + $classnotnull = 'badge badge-primary badge-pill ml-1'; $classnull = 'badge badge-secondary badge-pill ml-1'; diff --git a/renderer.php b/renderer.php index dbafe5c7..d27892cf 100644 --- a/renderer.php +++ b/renderer.php @@ -59,7 +59,14 @@ public function header($title = null) { * @param array $tabs the tabs * @param string $id ID of current page (can be empty) */ - public function tabs($tabs, $id) { + public function tabs($tabs, $id, $wf = null) { + $wffilterchoicelist = new core\output\choicelist(); + $wffilterchoicelist->add_option("0", get_string('all')); + if ($wf !== null) { + $wffilterchoicelist->add_option($wf->id, $wf->name); + $wffilterchoicelist->set_selected_value($wf->id); + } + echo $this->output->tabtree($tabs, $id); } } diff --git a/step/adminapprove/approvestep.php b/step/adminapprove/approvestep.php index e5579665..c30825bf 100644 --- a/step/adminapprove/approvestep.php +++ b/step/adminapprove/approvestep.php @@ -144,7 +144,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(false, false, false, true); +$tabparams = new stdClass(); +$tabparams->approvelink = true; +$tabrow = tabs::get_tabrow($tabparams); $id = optional_param('id', 'adminapprove', PARAM_TEXT); $renderer->tabs($tabrow, $id); diff --git a/workflowoverview.php b/workflowoverview.php index f5bcb11f..c46a605f 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -171,8 +171,13 @@ $classdetails = "bg-light"; } } -$tabrow = tabs::get_tabrow($activelink, $deactivatedlink, $draftlink); -$renderer->tabs($tabrow, $id); +$tabparams = new stdClass(); +$tabparams->activelink = true; +$tabparams->deactivatedlink = true; +$tabparams->draftlink = true; +$tabparams->wfid = $workflowid->id; +$tabrow = tabs::get_tabrow($tabparams); +$renderer->tabs($tabrow, $id, $workflow); $steps = step_manager::get_step_instances($workflow->id); $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); From 41c6fa3ed1a924e56f30a3c7d8134eafa058ebcb Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 10 Jun 2025 16:43:33 +0200 Subject: [PATCH 030/170] overall workflow filter - phpdoc issues --- classes/tabs.php | 5 +---- renderer.php | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/classes/tabs.php b/classes/tabs.php index dd692a2e..d670c68b 100644 --- a/classes/tabs.php +++ b/classes/tabs.php @@ -36,10 +36,7 @@ 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 bool $approvelink display approvals tab as link + * @param object $params * @return array of tabobjects * @throws \coding_exception * @throws \dml_exception diff --git a/renderer.php b/renderer.php index d27892cf..15fcab15 100644 --- a/renderer.php +++ b/renderer.php @@ -57,7 +57,9 @@ 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) + * @param object $wf current workflow object, optional + * @throws coding_exception */ public function tabs($tabs, $id, $wf = null) { $wffilterchoicelist = new core\output\choicelist(); From 37acfd52f16a305d030a194e902eea23287ed9d3 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 10 Jun 2025 17:50:15 +0200 Subject: [PATCH 031/170] overall workflow filter - part 2 --- renderer.php | 10 +--------- workflowoverview.php | 7 ++++++- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/renderer.php b/renderer.php index 15fcab15..3a59bbdd 100644 --- a/renderer.php +++ b/renderer.php @@ -58,17 +58,9 @@ public function header($title = null) { * * @param array $tabs the tabs * @param string $id ID of current page (can be empty) - * @param object $wf current workflow object, optional * @throws coding_exception */ - public function tabs($tabs, $id, $wf = null) { - $wffilterchoicelist = new core\output\choicelist(); - $wffilterchoicelist->add_option("0", get_string('all')); - if ($wf !== null) { - $wffilterchoicelist->add_option($wf->id, $wf->name); - $wffilterchoicelist->set_selected_value($wf->id); - } - + public function tabs($tabs, $id) { echo $this->output->tabtree($tabs, $id); } } diff --git a/workflowoverview.php b/workflowoverview.php index c46a605f..1699d550 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -177,7 +177,12 @@ $tabparams->draftlink = true; $tabparams->wfid = $workflowid->id; $tabrow = tabs::get_tabrow($tabparams); -$renderer->tabs($tabrow, $id, $workflow); +$renderer->tabs($tabrow, $id); +$wffilterchoicelist = new core\output\choicelist(); +$wffilterchoicelist->add_option("0", get_string('all')); +$wffilterchoicelist->add_option($workflow->id, $workflow->title); +$wffilterchoicelist->set_selected_value($workflow->id); +echo $renderer->render($wffilterchoicelist); $steps = step_manager::get_step_instances($workflow->id); $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); From c4ef87520012542e3173b67a618677575f56cec5 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 10 Jun 2025 18:08:58 +0200 Subject: [PATCH 032/170] fix typo --- workflowoverview.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflowoverview.php b/workflowoverview.php index 1699d550..2580dcc6 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -175,7 +175,7 @@ $tabparams->activelink = true; $tabparams->deactivatedlink = true; $tabparams->draftlink = true; -$tabparams->wfid = $workflowid->id; +$tabparams->wfid = $workflow->id; $tabrow = tabs::get_tabrow($tabparams); $renderer->tabs($tabrow, $id); $wffilterchoicelist = new core\output\choicelist(); From e2086e6a0f60ff8c43a0d6f16149e7893ad07157 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 10 Jun 2025 18:17:13 +0200 Subject: [PATCH 033/170] fix lastaccess error when no courses found --- trigger/lastaccess/lib.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/trigger/lastaccess/lib.php b/trigger/lastaccess/lib.php index 77207793..09aa8607 100644 --- a/trigger/lastaccess/lib.php +++ b/trigger/lastaccess/lib.php @@ -93,8 +93,13 @@ public function get_course_recordset_where($triggerid) { $courseids = array_column($records, 'courseid'); - [$insql, $inparams] = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); - $where = "{course}.id {$insql}"; + if ($courseids) { + [$insql, $inparams] = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); + $where = "{course}.id {$insql}"; + } else { + $inparams = []; + $where = ""; + } return [$where, $inparams]; } From 04fceb0f1970910a9fe4c6c4c5130cabcfe7c206 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 10 Jun 2025 18:29:05 +0200 Subject: [PATCH 034/170] fix lastaccess error when no courses found 2 --- trigger/lastaccess/lib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trigger/lastaccess/lib.php b/trigger/lastaccess/lib.php index 09aa8607..0dfe80e9 100644 --- a/trigger/lastaccess/lib.php +++ b/trigger/lastaccess/lib.php @@ -98,7 +98,7 @@ public function get_course_recordset_where($triggerid) { $where = "{course}.id {$insql}"; } else { $inparams = []; - $where = ""; + $where = "true"; } return [$where, $inparams]; } From a0b181d429d11e0e02f5f834d704ad4a922245df Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 11 Jun 2025 10:59:20 +0200 Subject: [PATCH 035/170] overall workflow filter new start --- classes/local/manager/workflow_manager.php | 2 +- classes/tabs.php | 42 ++++++++++++++-------- delayedcourses.php | 8 +++-- renderer.php | 1 - templates/search_input.mustache | 2 +- workflowoverview.php | 16 ++++----- 6 files changed, 42 insertions(+), 29 deletions(-) diff --git a/classes/local/manager/workflow_manager.php b/classes/local/manager/workflow_manager.php index 26c444bf..addf0ec8 100644 --- a/classes/local/manager/workflow_manager.php +++ b/classes/local/manager/workflow_manager.php @@ -175,7 +175,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); diff --git a/classes/tabs.php b/classes/tabs.php index d670c68b..c38383e7 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 @@ -42,14 +44,21 @@ class tabs { * @throws \dml_exception * @throws moodle_exception */ - public static function get_tabrow($params = null) { + public static function get_tabrow($params = new stdClass()) { global $DB; $activelink = isset($params->activelink); $deactivatelink = isset($params->deactivatelink); $draftlink = isset($params->draftlink); $approvelink = isset($params->approvelink); - $wfid = isset($params->wfid); + $wfid = 0; + $wfarr = []; + if (isset($params->wfid)) { + if ($params->wfid) { + $wfid = $params->wfid; + $wfarr[] = ['wfid' => $params->wfid]; + } + } $classnotnull = 'badge badge-primary badge-pill ml-1'; $classnull = 'badge badge-secondary badge-pill ml-1'; @@ -77,17 +86,21 @@ public static function get_tabrow($params = null) { $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"; - $i = $DB->count_records_sql($sql); + if ($wfid) { + $i = count(delayed_courses_manager::get_delayed_courses_for_workflow($wfid)); + } else { + $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"; + $i = $DB->count_records_sql($sql); + } $delayedcourses = \html_writer::span($i, $i > 0 ? $classnotnull : $classnull); // Get number of outstanding admin approvals. @@ -138,7 +151,8 @@ public static function get_tabrow($params = null) { get_string('deactivated_workflows_header_title', 'tool_lifecycle'), $deactivatelink); // Tab to the delayed courses list page. - $targeturl = new \moodle_url('/admin/tool/lifecycle/delayedcourses.php', ['id' => 'delayedcourses']); + $targeturl = new \moodle_url('/admin/tool/lifecycle/delayedcourses.php', + array_merge(['id' => 'delayedcourses'], $wfarr)); $tabrow[] = new \tabobject('delayedcourses', $targeturl, get_string('delayed_courses_header', 'tool_lifecycle').$delayedcourses, get_string('delayed_courses_header_title', 'tool_lifecycle')); diff --git a/delayedcourses.php b/delayedcourses.php index 28702872..9e67bd6d 100644 --- a/delayedcourses.php +++ b/delayedcourses.php @@ -33,8 +33,10 @@ require_login(); +$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). @@ -132,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); diff --git a/renderer.php b/renderer.php index 3a59bbdd..d0c09fd6 100644 --- a/renderer.php +++ b/renderer.php @@ -58,7 +58,6 @@ public function header($title = null) { * * @param array $tabs the tabs * @param string $id ID of current page (can be empty) - * @throws coding_exception */ public function tabs($tabs, $id) { echo $this->output->tabtree($tabs, $id); 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/workflowoverview.php b/workflowoverview.php index 2580dcc6..7e796f47 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -178,11 +178,7 @@ $tabparams->wfid = $workflow->id; $tabrow = tabs::get_tabrow($tabparams); $renderer->tabs($tabrow, $id); -$wffilterchoicelist = new core\output\choicelist(); -$wffilterchoicelist->add_option("0", get_string('all')); -$wffilterchoicelist->add_option($workflow->id, $workflow->title); -$wffilterchoicelist->set_selected_value($workflow->id); -echo $renderer->render($wffilterchoicelist); +echo html_writer::tag('div', get_string('filter') . ': ' . $workflow->title); $steps = step_manager::get_step_instances($workflow->id); $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); @@ -219,19 +215,19 @@ $nextrunt = get_string('plugindisabled', 'tool_task'); } else if ($task->get_disabled()) { $nextrunt = get_string('taskdisabled', 'tool_task'); -} else if (is_int($nextrunt) && $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. +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_int($nextrunt) && is_int($nextrun)) { // Only trigger nextrun is valid time. +} else if (!is_numeric($nextrunt) && is_numeric($nextrun)) { // Only trigger nextrun is valid time. $nextrun = $nextrun; -} else if (is_int($nextrunt)) { // Only task next run is valid time. +} 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. $nextrunout = $nextrunt; } -if (is_int($nextrunout)) { +if (is_numeric($nextrunout)) { if ($nextrunout) { $nextrunout = userdate($nextrunout, get_string('strftimedatetimeshort', 'langconfig')); } else { From dfaaa1dd04ba35e78ca7998400221d7a20b00af2 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 16 Jun 2025 07:48:52 +0200 Subject: [PATCH 036/170] workflow id instead of workflow object in processor --- classes/processor.php | 12 ++++---- run.php | 45 +++++++++++++++++++++++++++++ templates/workflowoverview.mustache | 2 +- 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 run.php diff --git a/classes/processor.php b/classes/processor.php index 8deade3b..28b88f17 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -88,8 +88,7 @@ public function call_trigger() { */ public function process_courses() { foreach (process_manager::get_processes() as $process) { - $workflow = workflow_manager::get_workflow($process->workflowid); - while ($workflow) { + while ($process->workflowid) { try { $course = get_course($process->courseid); @@ -102,7 +101,8 @@ public function process_courses() { 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; } } @@ -125,11 +125,13 @@ public function process_courses() { 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); + delayed_courses_manager::set_course_delayed_for_workflow($course->id, false, + $process->workflowid); 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); break; } else { diff --git a/run.php b/run.php new file mode 100644 index 00000000..85b40ca5 --- /dev/null +++ b/run.php @@ -0,0 +1,45 @@ +. + +/** + * Trigger manually the task for working on lifecycle processes + * + * @package tool_lifecycle + * @copyright 2025 Thomas Niedermaier University Münster + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use tool_lifecycle\local\manager\lib_manager; +use tool_lifecycle\local\manager\step_manager; +use \tool_lifecycle\processor; + +require_once(__DIR__ . '/../../../config.php'); + +require_login(); + +$processor = new processor(); +$processor->call_trigger(); + +$steps = step_manager::get_step_types(); +$steplibs = []; +foreach ($steps as $id => $step) { + $steplibs[$id] = lib_manager::get_step_lib($id); + $steplibs[$id]->pre_processing_bulk_operation(); +} +$processor->process_courses(); +foreach ($steps as $id => $step) { + $steplibs[$id]->post_processing_bulk_operation(); +} diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 0267ef5b..0526c1b4 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -74,7 +74,7 @@
-
+
{{#pix}} i/edit, core, {{/pix}} From 4bf3c089df702c4d330319001679e29eb1b5a3d1 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 16 Jun 2025 08:22:21 +0200 Subject: [PATCH 037/170] fix triggered courses counting error --- classes/processor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 28b88f17..bfa6b644 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -396,10 +396,10 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting($trigger, $excludedcourses, $delayedcourses); if ($trigger->exclude) { - $obj->excluded = $triggercourses; + $obj->excluded = $newcourses; $obj->delayed = $delayed; } else { - $obj->triggered = $triggercourses; + $obj->triggered = $newcourses; $obj->delayed = $delayed; } $obj->alreadyin = $triggercourses - $newcourses; From c30aaa55e38511bec48ea5c7ef6a9725b3f4e142 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 16 Jun 2025 08:35:18 +0200 Subject: [PATCH 038/170] codechecker issues --- classes/tabs.php | 3 ++- run.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/classes/tabs.php b/classes/tabs.php index c38383e7..56958800 100644 --- a/classes/tabs.php +++ b/classes/tabs.php @@ -90,7 +90,8 @@ public static function get_tabrow($params = new stdClass()) { $i = count(delayed_courses_manager::get_delayed_courses_for_workflow($wfid)); } else { $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 + (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 diff --git a/run.php b/run.php index 85b40ca5..65a79800 100644 --- a/run.php +++ b/run.php @@ -24,7 +24,7 @@ use tool_lifecycle\local\manager\lib_manager; use tool_lifecycle\local\manager\step_manager; -use \tool_lifecycle\processor; +use tool_lifecycle\processor; require_once(__DIR__ . '/../../../config.php'); From 5bcf39b2b931e185e63216ae96f3b86669a047df Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 17 Jun 2025 10:36:24 +0200 Subject: [PATCH 039/170] add filter form to procerror page --- classes/local/form/form_errors_filter.php | 104 +++++++++++++++++++ classes/local/table/interaction_table.php | 4 +- classes/local/table/process_errors_table.php | 28 +++-- classes/tabs.php | 53 ++++------ errors.php | 35 ++++++- version.php | 2 +- workflowoverview.php | 1 - 7 files changed, 186 insertions(+), 41 deletions(-) create mode 100644 classes/local/form/form_errors_filter.php diff --git a/classes/local/form/form_errors_filter.php b/classes/local/form/form_errors_filter.php new file mode 100644 index 00000000..ed55779d --- /dev/null +++ b/classes/local/form/form_errors_filter.php @@ -0,0 +1,104 @@ +. + +/** + * 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']; + $course = $this->_customdata['course']; + $step = $this->_customdata['step']; + + // 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)"; + $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); + + // 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; + } + $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); + + // 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; + } + $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); + + $this->add_action_buttons(true, get_string('apply', 'tool_lifecycle')); + } + +} diff --git a/classes/local/table/interaction_table.php b/classes/local/table/interaction_table.php index a387deba..bc55c65c 100644 --- a/classes/local/table/interaction_table.php +++ b/classes/local/table/interaction_table.php @@ -138,10 +138,10 @@ public function col_category($row): String { $categoryhierachy = array_map('intval', $categoryhierachy); if (isset($categoryhierachy[$categorydepth])) { $category = $this->coursecategories[$categoryhierachy[$categorydepth]]; - return $category->name; + return $category->name.'111'; } else { $category = $this->coursecategories[end($categoryhierachy)]; - return $category->name; + return $category->name.'222'.$row->categorypath; } } } diff --git a/classes/local/table/process_errors_table.php b/classes/local/table/process_errors_table.php index 5b99cbe8..216be2cf 100644 --- a/classes/local/table/process_errors_table.php +++ b/classes/local/table/process_errors_table.php @@ -42,11 +42,11 @@ class process_errors_table extends \table_sql { private $strings; /** - * Constructor for delayed_courses_table. + * Constructor for process_errors_table. * * @throws \coding_exception */ - public function __construct() { + public function __construct($filterdata) { global $OUTPUT; parent::__construct('tool_lifecycle-process_errors'); @@ -62,8 +62,24 @@ public function __construct() { '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 '; - - $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_headers([ @@ -170,9 +186,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/tabs.php b/classes/tabs.php index 56958800..6c76f3cf 100644 --- a/classes/tabs.php +++ b/classes/tabs.php @@ -44,20 +44,18 @@ class tabs { * @throws \dml_exception * @throws moodle_exception */ - public static function get_tabrow($params = new stdClass()) { + public static function get_tabrow($params = null) { global $DB; - $activelink = isset($params->activelink); - $deactivatelink = isset($params->deactivatelink); - $draftlink = isset($params->draftlink); - $approvelink = isset($params->approvelink); - $wfid = 0; - $wfarr = []; - if (isset($params->wfid)) { - if ($params->wfid) { - $wfid = $params->wfid; - $wfarr[] = ['wfid' => $params->wfid]; - } + $activelink = false; + $deactivatelink = false; + $draftlink = false; + $approvelink = false; + if ($params !== null) { + $activelink = isset($params->activelink); + $deactivatelink = isset($params->deactivatelink); + $draftlink = isset($params->draftlink); + $approvelink = isset($params->approvelink); } $classnotnull = 'badge badge-primary badge-pill ml-1'; @@ -86,22 +84,18 @@ public static function get_tabrow($params = new stdClass()) { $time = time(); // Get number of delayed courses. - if ($wfid) { - $i = count(delayed_courses_manager::get_delayed_courses_for_workflow($wfid)); - } else { - $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"; - $i = $DB->count_records_sql($sql); - } + $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"; + $i = $DB->count_records_sql($sql); $delayedcourses = \html_writer::span($i, $i > 0 ? $classnotnull : $classnull); // Get number of outstanding admin approvals. @@ -152,8 +146,7 @@ public static function get_tabrow($params = new stdClass()) { get_string('deactivated_workflows_header_title', 'tool_lifecycle'), $deactivatelink); // Tab to the delayed courses list page. - $targeturl = new \moodle_url('/admin/tool/lifecycle/delayedcourses.php', - array_merge(['id' => 'delayedcourses'], $wfarr)); + $targeturl = new \moodle_url('/admin/tool/lifecycle/delayedcourses.php', ['id' => 'delayedcourses']); $tabrow[] = new \tabobject('delayedcourses', $targeturl, get_string('delayed_courses_header', 'tool_lifecycle').$delayedcourses, get_string('delayed_courses_header_title', 'tool_lifecycle')); diff --git a/errors.php b/errors.php index dff2ffe6..af43b629 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; @@ -60,6 +61,27 @@ $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'); @@ -68,11 +90,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 + 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"; +$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/version.php b/version.php index 3f06f144..94d7dc8e 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die; $plugin->maturity = MATURITY_STABLE; -$plugin->version = 2025050403; +$plugin->version = 2025050403.01; $plugin->component = 'tool_lifecycle'; $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; diff --git a/workflowoverview.php b/workflowoverview.php index 7e796f47..d7bdfd1e 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -175,7 +175,6 @@ $tabparams->activelink = true; $tabparams->deactivatedlink = true; $tabparams->draftlink = true; -$tabparams->wfid = $workflow->id; $tabrow = tabs::get_tabrow($tabparams); $renderer->tabs($tabrow, $id); echo html_writer::tag('div', get_string('filter') . ': ' . $workflow->title); From f46007fca5f22c394453e07356d589a268252934 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 17 Jun 2025 10:46:38 +0200 Subject: [PATCH 040/170] phpdoc issue --- classes/local/table/process_errors_table.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/classes/local/table/process_errors_table.php b/classes/local/table/process_errors_table.php index 216be2cf..f1866462 100644 --- a/classes/local/table/process_errors_table.php +++ b/classes/local/table/process_errors_table.php @@ -23,6 +23,8 @@ */ namespace tool_lifecycle\local\table; +use core\exception\coding_exception; + defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir . '/tablelib.php'); @@ -44,6 +46,7 @@ class process_errors_table extends \table_sql { /** * Constructor for process_errors_table. * + * @param object $filterdata the previously submitted filter data * @throws \coding_exception */ public function __construct($filterdata) { From 99c70530eb94b63a0bdb6b21efc4b2c3829930ae Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 17 Jun 2025 10:52:53 +0200 Subject: [PATCH 041/170] codechecker issue --- classes/local/form/form_errors_filter.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/classes/local/form/form_errors_filter.php b/classes/local/form/form_errors_filter.php index ed55779d..be551447 100644 --- a/classes/local/form/form_errors_filter.php +++ b/classes/local/form/form_errors_filter.php @@ -51,10 +51,11 @@ public function definition() { $step = $this->_customdata['step']; // 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)"; + $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)"; $workflows = $DB->get_records_sql($sql); $workflowoptions[''] = get_string('choose').'...'; foreach ($workflows as $wf) { From 9ad097a436509388ca23375ae7f2fd950633de5e Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 17 Jun 2025 11:40:35 +0200 Subject: [PATCH 042/170] show proc error filter selects only if there are options --- classes/local/form/form_errors_filter.php | 43 +++++++++++++---------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/classes/local/form/form_errors_filter.php b/classes/local/form/form_errors_filter.php index be551447..5108fcf6 100644 --- a/classes/local/form/form_errors_filter.php +++ b/classes/local/form/form_errors_filter.php @@ -47,8 +47,7 @@ public function definition() { $mform = $this->_form; $workflow = $this->_customdata['workflow']; - $course = $this->_customdata['course']; - $step = $this->_customdata['step']; + $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 @@ -56,12 +55,14 @@ public function definition() { 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)"; - $workflows = $DB->get_records_sql($sql); - $workflowoptions[''] = get_string('choose').'...'; - foreach ($workflows as $wf) { - $workflowoptions[$wf->id] = $wf->title; + 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; } - $mform->addElement('select', 'workflow', get_string('workflow', 'tool_lifecycle'), $workflowoptions); // Get distinct workflow steps with process errors and populate step filter with them. // If workflow filter is active use it here as well. @@ -74,12 +75,14 @@ public function definition() { $sql .= " and pe.workflowid = :workflow"; $params["workflow"] = $workflow; } - $steps = $DB->get_records_sql($sql, $params); - $stepsoptions[''] = get_string('choose').'...'; - foreach ($steps as $step) { - $stepsoptions[$step->id] = $step->instancename; + 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; } - $mform->addElement('select', 'step', get_string('step', 'tool_lifecycle'), $stepsoptions); // Get distinct courses with process errors and populate courses filter with them. // If workflow filter is active use it here as well. @@ -92,14 +95,18 @@ public function definition() { $sql .= " and pe.workflowid = :workflow"; $params["workflow"] = $workflow; } - $courses = $DB->get_records_sql($sql, $params); - $coursesoptions[''] = get_string('choose').'...'; - foreach ($courses as $course) { - $coursesoptions[$course->id] = $course->fullname; + 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; } - $mform->addElement('select', 'course', get_string('course'), $coursesoptions); - $this->add_action_buttons(true, get_string('apply', 'tool_lifecycle')); + if ($addbuttons) { + $this->add_action_buttons(true, get_string('apply', 'tool_lifecycle')); + } } } From b8defd80c69c577778daa8545434eedfac9c30ae Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 18 Jun 2025 04:15:47 +0200 Subject: [PATCH 043/170] add step pushbackuptask --- classes/processor.php | 5 +++-- step/createbackup/lang/de/lifecyclestep_createbackup.php | 2 +- step/pushbackuptask | 1 + workflowoverview.php | 1 - 4 files changed, 5 insertions(+), 4 deletions(-) create mode 160000 step/pushbackuptask diff --git a/classes/processor.php b/classes/processor.php index bfa6b644..986b28d4 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -396,13 +396,14 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting($trigger, $excludedcourses, $delayedcourses); if ($trigger->exclude) { - $obj->excluded = $newcourses; + $obj->excluded = $triggercourses; $obj->delayed = $delayed; + $obj->alreadyin = 0; } else { $obj->triggered = $newcourses; $obj->delayed = $delayed; + $obj->alreadyin = $triggercourses - $newcourses; } - $obj->alreadyin = $triggercourses - $newcourses; } $autotriggers[] = $trigger; } else if ($obj->response == trigger_response::triggertime()) { 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/pushbackuptask b/step/pushbackuptask new file mode 160000 index 00000000..c9a9304d --- /dev/null +++ b/step/pushbackuptask @@ -0,0 +1 @@ +Subproject commit c9a9304dfb40194f735f6ec791db0ecad109a6d5 diff --git a/workflowoverview.php b/workflowoverview.php index d7bdfd1e..b09e7fde 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -177,7 +177,6 @@ $tabparams->draftlink = true; $tabrow = tabs::get_tabrow($tabparams); $renderer->tabs($tabrow, $id); -echo html_writer::tag('div', get_string('filter') . ': ' . $workflow->title); $steps = step_manager::get_step_instances($workflow->id); $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); From eca4b679c8c5bd77d364e76b098153ce348f2543 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 18 Jun 2025 04:29:33 +0200 Subject: [PATCH 044/170] categories trigger: instance settings should not be editable --- step/adminapprove/lib.php | 2 +- trigger/categories/lib.php | 4 ++-- trigger/customfielddelay/lib.php | 5 ++++- trigger/manual/lib.php | 5 +++-- trigger/specificdate/lib.php | 1 - 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/step/adminapprove/lib.php b/step/adminapprove/lib.php index 8862fe57..346e80bf 100644 --- a/step/adminapprove/lib.php +++ b/step/adminapprove/lib.php @@ -137,7 +137,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/trigger/categories/lib.php b/trigger/categories/lib.php index 73cf85b9..489e558f 100644 --- a/trigger/categories/lib.php +++ b/trigger/categories/lib.php @@ -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), ]; } diff --git a/trigger/customfielddelay/lib.php b/trigger/customfielddelay/lib.php index 1aabec35..bea2d908 100644 --- a/trigger/customfielddelay/lib.php +++ b/trigger/customfielddelay/lib.php @@ -79,7 +79,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), + ]; } /** 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/specificdate/lib.php b/trigger/specificdate/lib.php index eb0a1ddb..12497fad 100644 --- a/trigger/specificdate/lib.php +++ b/trigger/specificdate/lib.php @@ -172,7 +172,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), ]; From ab0127e4d95039733ca0a2796d412f0adf9074d9 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 19 Jun 2025 05:34:23 +0200 Subject: [PATCH 045/170] re-add step pushbackuptask to fix git submodule error --- step/pushbackuptask | 1 - step/pushbackuptask/README.md | 5 ++ .../classes/task/course_backup_task.php | 73 ++++++++++++++++++ .../lang/de/lifecyclestep_pushbackuptask.php | 27 +++++++ .../lang/en/lifecyclestep_pushbackuptask.php | 28 +++++++ step/pushbackuptask/lib.php | 77 +++++++++++++++++++ step/pushbackuptask/version.php | 36 +++++++++ 7 files changed, 246 insertions(+), 1 deletion(-) delete mode 160000 step/pushbackuptask create mode 100644 step/pushbackuptask/README.md create mode 100644 step/pushbackuptask/classes/task/course_backup_task.php create mode 100644 step/pushbackuptask/lang/de/lifecyclestep_pushbackuptask.php create mode 100644 step/pushbackuptask/lang/en/lifecyclestep_pushbackuptask.php create mode 100644 step/pushbackuptask/lib.php create mode 100644 step/pushbackuptask/version.php diff --git a/step/pushbackuptask b/step/pushbackuptask deleted file mode 160000 index c9a9304d..00000000 --- a/step/pushbackuptask +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c9a9304dfb40194f735f6ec791db0ecad109a6d5 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..67c98870 --- /dev/null +++ b/step/pushbackuptask/classes/task/course_backup_task.php @@ -0,0 +1,73 @@ +. + +/** + * 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; + +defined('MOODLE_INTERNAL') || die(); + +use tool_lifecycle\local\manager\backup_manager; + +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', array('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..a9bb0f97 --- /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'] = 'Ad hoc Kurssicherungs-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..daf9764d --- /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'] = 'Adhoc course 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..18970fef --- /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 tool_lifecycle\local\response\step_response; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../lib.php'); + +global $CFG; + +class pushbackuptask extends libbase { + /** + * Processes the course and returns a repsonse. + * 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. + * @return step_response + * @throws \coding_exception + * @throws \dml_exception + */ + public function process_course($processid, $instanceid, $course) { + $asynctask = new course_backup_task(); + $asynctask->set_custom_data(array('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 mixed $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..94fb4ecc --- /dev/null +++ b/step/pushbackuptask/version.php @@ -0,0 +1,36 @@ +. + +/** + * 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->dependencies = [ + 'tool_lifecycle' => 2025050400, +]; +$plugin->release = 'v4.5-r1'; +$plugin->maturity = MATURITY_STABLE; From b9ece2320581b4d1bad138db14d5e7021107dc71 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 19 Jun 2025 21:44:52 +0200 Subject: [PATCH 046/170] codechecker issues --- step/pushbackuptask/classes/task/course_backup_task.php | 7 ++++--- step/pushbackuptask/lib.php | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/step/pushbackuptask/classes/task/course_backup_task.php b/step/pushbackuptask/classes/task/course_backup_task.php index 67c98870..93bec1a1 100644 --- a/step/pushbackuptask/classes/task/course_backup_task.php +++ b/step/pushbackuptask/classes/task/course_backup_task.php @@ -24,10 +24,11 @@ namespace lifecyclestep_pushbackuptask\task; -defined('MOODLE_INTERNAL') || die(); - use tool_lifecycle\local\manager\backup_manager; +/** + * Task for adhoc backups of courses. + */ class course_backup_task extends \core\task\adhoc_task { /** @@ -45,7 +46,7 @@ public function execute() { $courseid = $this->get_custom_data()->courseid; try { - $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + $course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST); } catch (\moodle_exception $e) { mtrace('Invalid course id: ' . $courseid . ', task aborted.'); return; diff --git a/step/pushbackuptask/lib.php b/step/pushbackuptask/lib.php index 18970fef..7340cc2c 100644 --- a/step/pushbackuptask/lib.php +++ b/step/pushbackuptask/lib.php @@ -31,11 +31,12 @@ require_once(__DIR__ . '/../lib.php'); -global $CFG; - +/** + * Class for processing courses. + */ class pushbackuptask 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. @@ -49,7 +50,7 @@ class pushbackuptask extends libbase { */ public function process_course($processid, $instanceid, $course) { $asynctask = new course_backup_task(); - $asynctask->set_custom_data(array('courseid' => $course->id)); + $asynctask->set_custom_data(['courseid' => $course->id]); \core\task\manager::queue_adhoc_task($asynctask); return step_response::proceed(); } From 1bf9bb9c5e2f0c29983c28fa57a843c359140814 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 23 Jun 2025 11:04:22 +0200 Subject: [PATCH 047/170] shift subplugins list to own page --- classes/tabs.php | 8 +- classes/urls.php | 2 + lang/de/tool_lifecycle.php | 3 +- lang/en/tool_lifecycle.php | 3 +- settings.php | 48 ---------- step/adminapprove/index.php | 3 +- .../lang/de/lifecyclestep_pushbackuptask.php | 2 +- .../lang/en/lifecyclestep_pushbackuptask.php | 2 +- subplugins.php | 94 +++++++++++++++++++ 9 files changed, 111 insertions(+), 54 deletions(-) create mode 100644 subplugins.php diff --git a/classes/tabs.php b/classes/tabs.php index 6c76f3cf..3c6e6d86 100644 --- a/classes/tabs.php +++ b/classes/tabs.php @@ -121,12 +121,18 @@ public static function get_tabrow($params = null) { $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, diff --git a/classes/urls.php b/classes/urls.php index dec4a786..e80b7af8 100644 --- a/classes/urls.php +++ b/classes/urls.php @@ -59,5 +59,7 @@ 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'; } diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 12c66353..f195c56d 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -133,6 +133,7 @@ $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'] = 'Dokumenation auf github'; $string['dontshowextendeddetails'] = 'Zeige detaillierte Workflow-Informationen nicht an'; $string['download'] = 'Herunterladen'; $string['draft'] = 'Entwurf'; @@ -262,7 +263,7 @@ $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'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index f52f2897..28a30eb0 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -134,6 +134,7 @@ $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['draft'] = 'Draft'; @@ -243,7 +244,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'; diff --git a/settings.php b/settings.php index 0fff323e..e9c524f0 100644 --- a/settings.php +++ b/settings.php @@ -65,53 +65,5 @@ get_string('config_logreceivedmails_desc', 'tool_lifecycle'), 0)); - $triggers = core_component::get_plugin_list('lifecycletrigger'); - if ($triggers) { - $settings->add(new admin_setting_heading('lifecycletriggerheader', - get_string('triggers_installed', 'tool_lifecycle'), '')); - foreach ($triggers as $trigger => $path) { - $triggername = html_writer::span(get_string('pluginname', 'lifecycletrigger_' . $trigger), - "font-weight-bold"); - $uninstall = ''; - if ($trigger == 'sitecourse' || $trigger == 'delayedcourses') { - $uninstall = html_writer::span(' Depracated. Will be removed with version 5.0.', 'text-danger'); - } - try { - $plugindescription = get_string('plugindescription', 'lifecycletrigger_' . $trigger); - } catch (Exception $e) { - $plugindescription = ""; - } - $settings->add(new admin_setting_description('lifecycletriggersetting_'.$trigger, - $triggername, - $plugindescription.$uninstall - )); - } - } else { - $settings->add(new admin_setting_heading('adminsettings_notriggers', - get_string('adminsettings_notriggers', 'tool_lifecycle'), '')); - } - - $steps = core_component::get_plugin_list('lifecyclestep'); - if ($steps) { - $settings->add(new admin_setting_heading('lifecyclestepheader', - get_string('steps_installed', 'tool_lifecycle'), '')); - foreach ($steps as $step => $path) { - $stepname = html_writer::span(get_string('pluginname', 'lifecyclestep_' . $step), - "font-weight-bold"); - try { - $plugindescription = get_string('plugindescription', 'lifecyclestep_' . $step); - } catch (Exception $e) { - $plugindescription = ""; - } - $settings->add(new admin_setting_description('lifecyclestepsetting_'.$step, - $stepname, - $plugindescription)); - } - } else { - $settings->add(new admin_setting_heading('adminsettings_nosteps', - get_string('adminsettings_nosteps', 'tool_lifecycle'), '')); - } - $settings->add(new admin_setting_description('spacer', "", " ")); - $ADMIN->add('tools', $settings); } diff --git a/step/adminapprove/index.php b/step/adminapprove/index.php index 86e85634..4ad2899f 100644 --- a/step/adminapprove/index.php +++ b/step/adminapprove/index.php @@ -23,6 +23,7 @@ */ use tool_lifecycle\tabs; +use tool_lifecycle\urls; require_once(__DIR__ . '/../../../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); @@ -30,7 +31,7 @@ require_login(); $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/pushbackuptask/lang/de/lifecyclestep_pushbackuptask.php b/step/pushbackuptask/lang/de/lifecyclestep_pushbackuptask.php index a9bb0f97..0a426a57 100644 --- a/step/pushbackuptask/lang/de/lifecyclestep_pushbackuptask.php +++ b/step/pushbackuptask/lang/de/lifecyclestep_pushbackuptask.php @@ -23,5 +23,5 @@ */ $string['plugindescription'] = 'Ermöglicht augenblickliche Sicherungen von Kursen durch die Verwendung von ad hoc-Tasks.'; -$string['pluginname'] = 'Ad hoc Kurssicherungs-Schritt'; +$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 index daf9764d..b48700ba 100644 --- a/step/pushbackuptask/lang/en/lifecyclestep_pushbackuptask.php +++ b/step/pushbackuptask/lang/en/lifecyclestep_pushbackuptask.php @@ -23,6 +23,6 @@ */ $string['plugindescription'] = 'Enforces the backup of scheduled courses immediately by using adhoc tasks.'; -$string['pluginname'] = 'Adhoc course backup Step'; +$string['pluginname'] = 'Create Adhoc Backup Step'; $string['privacy:metadata'] = 'This subplugin does not store any personal data.'; diff --git a/subplugins.php b/subplugins.php new file mode 100644 index 00000000..082beebe --- /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_login(); + +$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(); From 3dac675a4555cfff8514043b1f31e6dfe80b4d8b Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 23 Jun 2025 16:32:31 +0200 Subject: [PATCH 048/170] change db field name manual to manually --- CHANGES.md | 23 +++++++++++++++++++ README.md | 8 ++----- classes/local/entity/workflow.php | 12 +++++----- classes/local/manager/process_manager.php | 2 +- classes/local/manager/workflow_manager.php | 8 +++---- .../active_automatic_workflows_table.php | 2 +- .../table/active_manual_workflows_table.php | 2 +- db/install.xml | 2 +- db/upgrade.php | 13 +++++++++++ tests/active_workflow_is_manual_test.php | 8 +++---- version.php | 4 ++-- 11 files changed, 58 insertions(+), 26 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0c42ad85..ffc14304 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,29 @@ CHANGELOG ========= +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..2462991f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ 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 @@ -30,10 +30,6 @@ 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. +The plugin is continuously 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. diff --git a/classes/local/entity/workflow.php b/classes/local/entity/workflow.php index 777decee..be1302a9 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; @@ -90,7 +90,7 @@ private function __construct($id, $title, $timeactive, $timedeactive, $sortindex $this->timeactive = $timeactive; $this->timedeactive = $timedeactive; $this->sortindex = $sortindex; - $this->manual = $manual; + $this->manually = $manual; $this->displaytitle = $displaytitle; $this->rollbackdelay = $rollbackdelay; $this->finishdelay = $finishdelay; @@ -129,10 +129,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; } } diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index f4688959..b3431b7c 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -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); diff --git a/classes/local/manager/workflow_manager.php b/classes/local/manager/workflow_manager.php index addf0ec8..472c1e0e 100644 --- a/classes/local/manager/workflow_manager.php +++ b/classes/local/manager/workflow_manager.php @@ -194,7 +194,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 +211,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 +264,10 @@ 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) { + if (!$workflow->manually) { $workflow->sortindex = count(self::get_active_automatic_workflows()) + 1; } self::insert_or_update($workflow); 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/db/install.xml b/db/install.xml index a2262cba..8fbfa6cc 100644 --- a/db/install.xml +++ b/db/install.xml @@ -105,7 +105,7 @@ - + diff --git a/db/upgrade.php b/db/upgrade.php index 2a0e4ad6..691cd7ba 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -588,5 +588,18 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { } + if ($oldversion < 2025050404) { + + // 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'); + + // Launch rename field key. + $dbman->rename_field($table, $field, 'manually'); + + // Lifecycle savepoint reached. + upgrade_plugin_savepoint(true, 2025050404, 'tool', 'lifecycle'); + } + return true; } 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/version.php b/version.php index 94d7dc8e..4b6e2dd4 100644 --- a/version.php +++ b/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die; $plugin->maturity = MATURITY_STABLE; -$plugin->version = 2025050403.01; +$plugin->version = 2025050404; $plugin->component = 'tool_lifecycle'; $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; -$plugin->release = 'v4.5-r4'; +$plugin->release = 'v4.5-r5'; From a9e7c6bb0f6d02b18cf9b785e64c44ef5e45402f Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 23 Jun 2025 16:50:49 +0200 Subject: [PATCH 049/170] fix phpunit test --- tests/backup_and_restore_workflow_test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From b8891dabedd9a25065766790bd2520ff1c6ce2fa Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 24 Jun 2025 04:02:23 +0200 Subject: [PATCH 050/170] fix typos in comments --- step/adminapprove/lib.php | 2 +- step/createbackup/lib.php | 2 +- step/duplicate/lib.php | 4 ++-- step/email/lib.php | 4 ++-- step/lib.php | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/step/adminapprove/lib.php b/step/adminapprove/lib.php index 346e80bf..8a15ee56 100644 --- a/step/adminapprove/lib.php +++ b/step/adminapprove/lib.php @@ -44,7 +44,7 @@ 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. diff --git a/step/createbackup/lib.php b/step/createbackup/lib.php index 18724439..90999c91 100644 --- a/step/createbackup/lib.php +++ b/step/createbackup/lib.php @@ -50,7 +50,7 @@ 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. diff --git a/step/duplicate/lib.php b/step/duplicate/lib.php index d730b8df..1dc87413 100644 --- a/step/duplicate/lib.php +++ b/step/duplicate/lib.php @@ -49,7 +49,7 @@ 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. @@ -84,7 +84,7 @@ 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. diff --git a/step/email/lib.php b/step/email/lib.php index e4f5bd6b..900b5ead 100644 --- a/step/email/lib.php +++ b/step/email/lib.php @@ -44,7 +44,7 @@ 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. @@ -72,7 +72,7 @@ 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. diff --git a/step/lib.php b/step/lib.php index 6c439f9d..3a644711 100644 --- a/step/lib.php +++ b/step/lib.php @@ -41,7 +41,7 @@ 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. @@ -54,7 +54,7 @@ abstract class libbase { 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. From 089b973d06d4cc6b69479a6a35b231e72e02c6f3 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 24 Jun 2025 04:32:36 +0200 Subject: [PATCH 051/170] no get_course in process_courses anymore --- classes/processor.php | 25 ++++++++++--------------- step/adminapprove/lib.php | 4 ++-- step/createbackup/lib.php | 6 +++--- step/deletecourse/lib.php | 8 ++++---- step/duplicate/lib.php | 3 ++- step/email/lib.php | 8 ++++---- step/lib.php | 2 +- step/pushbackuptask/lib.php | 6 +++--- 8 files changed, 29 insertions(+), 33 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 986b28d4..9ac343bf 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -87,33 +87,28 @@ public function call_trigger() { * Calls the process_course() method of each step submodule currently responsible for a given course. */ public function process_courses() { + // For each process in process table. foreach (process_manager::get_processes() as $process) { + // Process only if the process has workflow id. while ($process->workflowid) { - - 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; - } - + // New workflows with lifecycle version 4.4 and beneath did not have to have a step. 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, + delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, false, $process->workflowid); break; } } - + // Get current step of process and its step lib. $step = step_manager::get_step_instance_by_workflow_index($process->workflowid, $process->stepindex); $lib = lib_manager::get_step_lib($step->subpluginname); + // Process course. try { if ($process->waiting) { - $result = $lib->process_waiting_course($process->id, $step->id, $course); + $result = $lib->process_waiting_course($process->id, $step->id, $process->courseid); } else { - $result = $lib->process_course($process->id, $step->id, $course); + $result = $lib->process_course($process->id, $step->id, $process->courseid); } } catch (\Exception $e) { unset($process->context); @@ -125,12 +120,12 @@ public function process_courses() { break; } else if ($result == step_response::proceed()) { if (!process_manager::proceed_process($process)) { - delayed_courses_manager::set_course_delayed_for_workflow($course->id, false, + delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, false, $process->workflowid); break; } } else if ($result == step_response::rollback()) { - delayed_courses_manager::set_course_delayed_for_workflow($course->id, true, + delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, $process->workflowid); process_manager::rollback_process($process); break; diff --git a/step/adminapprove/lib.php b/step/adminapprove/lib.php index 8a15ee56..a23e81bc 100644 --- a/step/adminapprove/lib.php +++ b/step/adminapprove/lib.php @@ -47,7 +47,7 @@ class adminapprove extends libbase { * 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 int $course to be processed. * @return step_response */ public function process_course($processid, $instanceid, $course) { @@ -85,7 +85,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 */ diff --git a/step/createbackup/lib.php b/step/createbackup/lib.php index 90999c91..7ca0c514 100644 --- a/step/createbackup/lib.php +++ b/step/createbackup/lib.php @@ -57,7 +57,7 @@ class createbackup extends libbase { * - 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 int $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception @@ -67,7 +67,7 @@ public function process_course($processid, $instanceid, $course) { $instanceid, settings_type::STEP)['maximumbackupspercron']) { return step_response::waiting(); // Wait with further backups til the next cron run. } - if (backup_manager::create_course_backup($course->id)) { + if (backup_manager::create_course_backup($course)) { self::$numberofbackups++; return step_response::proceed(); } @@ -78,7 +78,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 int $course * @return step_response * @throws \coding_exception * @throws \dml_exception diff --git a/step/deletecourse/lib.php b/step/deletecourse/lib.php index 36531247..54b321fc 100644 --- a/step/deletecourse/lib.php +++ b/step/deletecourse/lib.php @@ -51,7 +51,7 @@ class deletecourse extends libbase { * - 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 int $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception @@ -59,7 +59,7 @@ class deletecourse extends libbase { public function process_course($processid, $instanceid, $course) { global $CFG; - if ($course->id == 1) { + if ($course == 1) { return step_response::rollback(); } @@ -68,7 +68,7 @@ public function process_course($processid, $instanceid, $course) { return step_response::waiting(); // Wait with further deletions til the next cron run. } - delete_course($course->id); + 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) */ @@ -89,7 +89,7 @@ public function process_course($processid, $instanceid, $course) { * - 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 int $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception diff --git a/step/duplicate/lib.php b/step/duplicate/lib.php index 1dc87413..a281c22d 100644 --- a/step/duplicate/lib.php +++ b/step/duplicate/lib.php @@ -61,6 +61,7 @@ class duplicate extends libbase { * @throws \dml_exception */ public function process_course($processid, $instanceid, $course) { + $course = get_course($course); $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)) { @@ -91,7 +92,7 @@ public function process_course($processid, $instanceid, $course) { * - 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 int $course to be processed. * @return step_response * @throws \dml_exception */ diff --git a/step/email/lib.php b/step/email/lib.php index 900b5ead..beaf0731 100644 --- a/step/email/lib.php +++ b/step/email/lib.php @@ -51,20 +51,20 @@ class email extends libbase { * - 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 int $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception */ public function process_course($processid, $instanceid, $course) { global $DB; - $coursecontext = \context_course::instance($course->id); + $coursecontext = \context_course::instance($course); $userstobeinformed = get_enrolled_users($coursecontext, 'lifecyclestep/email:preventdeletion', 0, 'u.id', null, null, null, true); foreach ($userstobeinformed as $user) { $record = new \stdClass(); $record->touser = $user->id; - $record->courseid = $course->id; + $record->courseid = $course; $record->instanceid = $instanceid; $DB->insert_record('lifecyclestep_email', $record); } @@ -79,7 +79,7 @@ public function process_course($processid, $instanceid, $course) { * - 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 int $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception diff --git a/step/lib.php b/step/lib.php index 3a644711..e22d13c2 100644 --- a/step/lib.php +++ b/step/lib.php @@ -61,7 +61,7 @@ abstract public function process_course($processid, $instanceid, $course); * - 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 int $course to be processed. * @return step_response */ public function process_waiting_course($processid, $instanceid, $course) { diff --git a/step/pushbackuptask/lib.php b/step/pushbackuptask/lib.php index 7340cc2c..8e37e66a 100644 --- a/step/pushbackuptask/lib.php +++ b/step/pushbackuptask/lib.php @@ -43,14 +43,14 @@ class pushbackuptask extends libbase { * - 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 int $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception */ public function process_course($processid, $instanceid, $course) { $asynctask = new course_backup_task(); - $asynctask->set_custom_data(['courseid' => $course->id]); + $asynctask->set_custom_data(['courseid' => $course]); \core\task\manager::queue_adhoc_task($asynctask); return step_response::proceed(); } @@ -59,7 +59,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 int $course * @return step_response * @throws \coding_exception * @throws \dml_exception From 170ceb6ca1c466c3d646335bf4bcc72b4a6529f0 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 25 Jun 2025 18:07:06 +0200 Subject: [PATCH 052/170] display active workflow processes and errors - the counting --- classes/local/manager/process_manager.php | 13 ++++ classes/local/manager/workflow_manager.php | 1 + classes/processor.php | 5 +- classes/tabs.php | 24 ++++--- lang/de/tool_lifecycle.php | 3 +- lang/en/tool_lifecycle.php | 3 +- renderer.php | 3 + templates/overview_processeslink.mustache | 34 ++++++++++ templates/workflowoverview.mustache | 7 +- workflowoverview.php | 77 +++++++++++++++++++--- 10 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 templates/overview_processeslink.mustache diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index b3431b7c..f12c4091 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -131,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 @@ -326,6 +337,8 @@ public static function proceed_process_after_error(int $processid) { * Rolls back a process from procerror table * @param int $processid the processid * @return void + * @throws \coding_exception + * @throws \dml_exception */ public static function rollback_process_after_error(int $processid) { global $DB; diff --git a/classes/local/manager/workflow_manager.php b/classes/local/manager/workflow_manager.php index 472c1e0e..24d232ee 100644 --- a/classes/local/manager/workflow_manager.php +++ b/classes/local/manager/workflow_manager.php @@ -491,6 +491,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/processor.php b/classes/processor.php index 9ac343bf..0ec5cfb2 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -248,7 +248,8 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) * @param trigger_subplugin $trigger trigger, which will be asked for additional where requirements. * @param int[] $excluded List of course id, which should be excluded from counting. * @param int[] $delayed List of course ids of delayed courses (globally and for workflow). - * @return int[] amount of triggered courses and amount which courses of them would be added and which courses are delayed. + * @return int[] $triggered, $new, $delayed Triggered courses and amount which courses of them would + * be added and which courses are delayed. * @throws \coding_exception * @throws \dml_exception */ @@ -277,7 +278,7 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { $sql = 'SELECT count({course}.id) from {course} WHERE '. $where; $triggercourses = $DB->count_records_sql($sql, $whereparams); - // Only get courses which are not part of this workflow yet. + // Only get courses which are not part of this workflow yet. Exclude processes and proc_errors of this wf. $sql .= " AND {course}.id NOT IN (". "SELECT {course}.id from {course} LEFT JOIN {tool_lifecycle_process} diff --git a/classes/tabs.php b/classes/tabs.php index 3c6e6d86..ed6444f2 100644 --- a/classes/tabs.php +++ b/classes/tabs.php @@ -48,14 +48,22 @@ public static function get_tabrow($params = null) { global $DB; $activelink = false; - $deactivatelink = false; + $deactivatedlink = false; $draftlink = false; $approvelink = false; if ($params !== null) { - $activelink = isset($params->activelink); - $deactivatelink = isset($params->deactivatelink); - $draftlink = isset($params->draftlink); - $approvelink = isset($params->approvelink); + 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'; @@ -80,7 +88,7 @@ public static function get_tabrow($params = null) { 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. @@ -148,8 +156,8 @@ public static function get_tabrow($params = null) { // 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']); diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index f195c56d..ab24cd40 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -79,7 +79,7 @@ $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 diesem Prozess oder bei seinen Prozessfehlern.'; $string['courses_are_delayed'] = '{$a} Kurse sind verzögert'; $string['courses_are_used_total'] = '{$a} Kurse bereits in anderem Prozess'; $string['courses_excluded'] = 'Kurse insgesamt ausgeschlossen: {$a}'; @@ -287,6 +287,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 28a30eb0..70b442d3 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -79,7 +79,7 @@ $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 this process or of the process errors'; $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'; @@ -288,6 +288,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/renderer.php b/renderer.php index d0c09fd6..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 * @@ -62,4 +64,5 @@ public function header($title = null) { public function tabs($tabs, $id) { echo $this->output->tabtree($tabs, $id); } + } diff --git a/templates/overview_processeslink.mustache b/templates/overview_processeslink.mustache new file mode 100644 index 00000000..25ef4eda --- /dev/null +++ b/templates/overview_processeslink.mustache @@ -0,0 +1,34 @@ +{{! + 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_addinstance + + Add trigger and add step select fields + + Example context (json): + { + "addtriggerselect": "Add trigger select", + "addstepselect": "Add step select", + "activate": "Activate Button", + "newworkflow": true + } +}} + + + {{processes}} + + diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 0526c1b4..31537e42 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -74,9 +74,12 @@
-
+
{{#pix}} i/edit, core, {{/pix}} + {{{disableworkflowlink}}} + {{{abortdisableworkflowlink}}} + {{{workflowprocesseslink}}}

{{title}} {{#includedelayedcourses}}{{/includedelayedcourses}} @@ -87,6 +90,8 @@ {{^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}} diff --git a/workflowoverview.php b/workflowoverview.php index b09e7fde..bc24fdd6 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -31,6 +31,7 @@ define('PAGESIZE', 20); use core\output\notification; +use core\output\single_button; use core\task\manager; use tool_lifecycle\action; use tool_lifecycle\event\process_triggered; @@ -56,6 +57,7 @@ $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); @@ -87,6 +89,8 @@ $params['delayed'] = $delayed; } else if ($excluded) { $params['excluded'] = $excluded; +} else if ($processes) { + $params['processes'] = $processes; } if ($search) { $params['search'] = $search; @@ -153,28 +157,23 @@ $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"; } } -$tabparams = new stdClass(); -$tabparams->activelink = true; -$tabparams->deactivatedlink = true; -$tabparams->draftlink = true; $tabrow = tabs::get_tabrow($tabparams); $renderer->tabs($tabrow, $id); @@ -390,6 +389,15 @@ ob_end_clean(); $hiddenfieldssearch[] = ['name' => 'delayed', 'value' => $delayed]; $tablecoursesamount = count($coursesdelayed); +} else if ($processes) { // Display courses table with courses in a process or in state process error for this workflow. + $table = new processes_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); } 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', @@ -416,6 +424,42 @@ 'hiddenfields' => $hiddenfieldssearch, ]); } +$disableworkflowlink = ""; +$abortdisableworkflowlink = ""; +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. + $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; +} $data = [ 'editsettingslink' => (new moodle_url(urls::EDIT_WORKFLOW, ['wf' => $workflow->id]))->out(false), @@ -444,6 +488,9 @@ 'nextrun' => $nextrunout, 'lastrun' => userdate($lastrun, get_string('strftimedatetimeshort', 'langconfig')), 'nomanualtriggerinvolved' => $nomanualtriggerinvolved, + 'disableworkflowlink' => $disableworkflowlink, + 'abortdisableworkflowlink' => $abortdisableworkflowlink, + 'workflowprocesseslink' => $workflowprocesseslink, ]; if ($showdetails) { // The triggers total box. @@ -529,6 +576,16 @@ $data['addstepselect'] = $addstepselect; $data['activate'] = $activate; $data['newworkflow'] = $newworkflow; +} 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); From 0f6bf43a18120a13123541135938278c065aa468 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 6 Jul 2025 14:37:09 +0200 Subject: [PATCH 053/170] trigger countings partial as tooltip and show courses already in workflow process or process errors --- classes/local/manager/process_manager.php | 16 -- classes/local/table/delayed_courses_table.php | 11 +- classes/local/table/process_courses_table.php | 139 ++++++++++++++++++ .../local/table/triggered_courses_table.php | 2 +- classes/processor.php | 104 ++++++++----- db/upgrade.php | 4 +- lang/de/tool_lifecycle.php | 2 + lang/en/tool_lifecycle.php | 2 + templates/overview_trigger.mustache | 28 ++-- templates/workflowoverview.mustache | 5 +- workflowoverview.php | 61 +++++--- 11 files changed, 275 insertions(+), 99 deletions(-) create mode 100644 classes/local/table/process_courses_table.php diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index f12c4091..92e2761f 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -142,22 +142,6 @@ public static function count_process_errors_by_workflow($workflowid) { return $DB->count_records('tool_lifecycle_proc_error', ['workflowid' => $workflowid]); } - /** - * Returns all processes for given workflow id - * @param int $workflowid id of the workflow - * @return array of proccesses initiated by specifed workflow id - * @throws \dml_exception - */ - public static function get_processes_by_workflow($workflowid) { - global $DB; - $records = $DB->get_records('tool_lifecycle_process', ['workflowid' => $workflowid]); - $processes = []; - foreach ($records as $record) { - $processes[] = process::from_record($record); - } - return $processes; - } - /** * Proceeds the process to the next step. * @param process $process diff --git a/classes/local/table/delayed_courses_table.php b/classes/local/table/delayed_courses_table.php index f2237698..db47ef39 100644 --- a/classes/local/table/delayed_courses_table.php +++ b/classes/local/table/delayed_courses_table.php @@ -150,12 +150,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'), ]); } diff --git a/classes/local/table/process_courses_table.php b/classes/local/table/process_courses_table.php new file mode 100644 index 00000000..15abf188 --- /dev/null +++ b/classes/local/table/process_courses_table.php @@ -0,0 +1,139 @@ +. + +/** + * Table listing all courses of this workflow in an active process or with a process error. + * + * @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; + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->libdir . '/tablelib.php'); + +/** + * Table listing all workflow courses in an active process or with a process error. + * + * @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 process_courses_table extends \table_sql { + + /** @var string $type of the courses list */ + private $type; + + /** @var int $workflowid Id of the workflow */ + private $workflowid; + + /** + * Builds a table of courses. + * @param array $courseids of the courses to list + * @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, $workflowname = '', $workflowid = null, $filterdata = '') { + parent::__construct('tool_lifecycle-workflow-courses-in-process'); + global $DB, $PAGE; + + if (!$courseids) { + return; + } + + $this->define_baseurl($PAGE->url); + $this->caption = get_string('workflow_processesanderrors', 'tool_lifecycle')." '".$workflowname."' (".count($courseids).")"; + $this->workflowid = $workflowid; + $this->captionattributes = ['class' => 'ml-3']; + $columns = ['courseid', 'coursefullname', 'coursecategory', 'step', 'error']; + $this->define_columns($columns); + $headers = [ + get_string('courseid', 'tool_lifecycle'), + get_string('coursename', 'tool_lifecycle'), + get_string('coursecategory', 'moodle'), + get_string('step', 'tool_lifecycle'), + get_string('error'), + ]; + $this->define_headers($headers); + + $fields = "c.id as courseid, c.fullname as coursefullname, c.shortname as courseshortname, + cc.name as coursecategory, s.instancename as step, pe.errormessage as errormessage, pe.errortrace as errortrace"; + $from = " + {course} c + JOIN {course_categories} cc ON c.category = cc.id + LEFT JOIN {tool_lifecycle_process} p ON p.courseid = c.id AND p.workflowid = $workflowid + LEFT JOIN {tool_lifecycle_proc_error} pe ON pe.courseid = c.id AND pe.workflowid = $workflowid + JOIN {tool_lifecycle_step} s ON (p.workflowid = s.workflowid AND p.stepindex = s.sortindex) + OR (pe.workflowid = s.workflowid AND pe.stepindex = s.sortindex) + "; + [$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 error column. + * + * @param object $row Row data. + * @return string error cell + */ + public function col_error($row) { + if ($row->errormessage) { + return "
" . + nl2br(htmlentities($row->errormessage, ENT_COMPAT)) . + "" . + nl2br(htmlentities($row->errortrace, ENT_COMPAT)) . + "
"; + } 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.php b/classes/local/table/triggered_courses_table.php index 23129130..b48c2a67 100644 --- a/classes/local/table/triggered_courses_table.php +++ b/classes/local/table/triggered_courses_table.php @@ -25,7 +25,7 @@ use tool_lifecycle\local\manager\delayed_courses_manager; use tool_lifecycle\local\manager\workflow_manager; -use tool_lifecycle\urls;; +use tool_lifecycle\urls; defined('MOODLE_INTERNAL') || die; diff --git a/classes/processor.php b/classes/processor.php index 0ec5cfb2..81e59d70 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -210,7 +210,7 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) } if ($forcounting) { - // Get course hasotherprocess and delay with the sql. + // Get course hasprocess and delay with the sql. $sql = "SELECT {course}.id, COALESCE(p.courseid, pe.courseid, 0) as hasprocess, CASE @@ -224,20 +224,17 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) ELSE 0 END as delay FROM {course} - LEFT JOIN {tool_lifecycle_process} p - ON {course}.id = p.courseid + 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; } else { // 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 {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 null AND pe.courseid IS NULL AND " . $where; } return $DB->get_recordset_sql($sql, $whereparams); } @@ -256,39 +253,75 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { global $DB; + $triggercourses = 0; + $delayedcourses = 0; + $newcourses = 0; + $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); + // Now get the amount of courses triggered by this trigger. $sql = 'SELECT {course}.id from {course} WHERE '. $where; $triggercoursesall = $DB->get_fieldset_sql($sql, $whereparams); + echo \html_writer::div("triggercoursesall:"); + foreach($triggercoursesall as $t) { + echo \html_writer::div($t); + } + echo \html_writer::div("delayed:"); + foreach($delayed as $t) { + echo \html_writer::div($t); + } + // Get delayed courses which would be triggered by this trigger. + $delayedcourses = count(array_intersect($triggercoursesall, $delayed)); - // Get number of delayed courses which would be triggered by this trigger. - $delayedcourses = array_intersect($triggercoursesall, $delayed); - - // Exclude courses in steps of this wf, delayed courses and sitecourse according to the workflow settings. + // Exclude delayed courses and sitecourse according to the workflow settings. if (!empty($excluded)) { [$insql, $inparams] = $DB->get_in_or_equal($excluded, SQL_PARAMS_NAMED); $where .= " AND NOT {course}.id {$insql}"; $whereparams = array_merge($whereparams, $inparams); + echo \html_writer::div("excluded:"); + foreach($excluded as $t) { + echo \html_writer::div($t); + } } $sql = 'SELECT count({course}.id) from {course} WHERE '. $where; + $sqldeb = 'SELECT {course}.id from {course} WHERE '. $where; + $triggercoursesrs = $DB->get_fieldset_sql($sqldeb, $whereparams); + echo \html_writer::div("triggercoursesrs:"); + foreach($triggercoursesrs as $t) { + echo \html_writer::div($t); + } + echo \html_writer::div($sqldeb); $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 {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))"; + $sql .= " AND {course}.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 = $trigger->workflowid) + OR (pe.courseid IS NOT NULL AND pe.workflowid = $trigger->workflowid) + )"; + $sqldeb .= " AND {course}.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 = $trigger->workflowid) + OR (pe.courseid IS NOT NULL AND pe.workflowid = $trigger->workflowid) + )"; + $newcoursesrs = $DB->get_fieldset_sql($sqldeb, $whereparams); + echo \html_writer::div("newcoursesrs:"); + foreach($newcoursesrs as $t) { + echo \html_writer::div($t); + } + echo \html_writer::div($sqldeb); $newcourses = $DB->count_records_sql($sql, $whereparams); - return [$triggercourses, $newcourses, count($delayedcourses)]; + return [$triggercourses, $newcourses, $delayedcourses]; } /** @@ -346,7 +379,6 @@ public function get_triggercourses($trigger, $workflow) { public function get_count_of_courses_to_trigger_for_workflow($workflow) { global $DB; - $counttriggered = 0; $coursestriggered = []; $usedcourses = []; @@ -380,19 +412,19 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $obj->sql = trigger_manager::get_trigger_sqlresult($trigger); $obj->response = $lib->check_course(null, null); if ($obj->sql != "false") { + // Get courses amount. + // 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. + [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting($trigger, $excludedcourses, + $delayedcourses); if ($obj->response == trigger_response::exclude()) { - // Get courses excluded amount. - [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting($trigger, $excludedcourses, - $delayedcourses); - $obj->excluded = $triggercourses; + $obj->excluded = $newcourses; $obj->delayed = $delayed; - $obj->alreadyin = $triggercourses - $newcourses; + $obj->alreadyin = 0; } else if ($obj->response == trigger_response::trigger()) { - // Get courses triggered amount. - [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting($trigger, $excludedcourses, - $delayedcourses); if ($trigger->exclude) { - $obj->excluded = $triggercourses; + $obj->excluded = $newcourses; $obj->delayed = $delayed; $obj->alreadyin = 0; } else { @@ -425,26 +457,32 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { 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); + echo \html_writer::div("excluded for workflow:"); + foreach($excludedcourses as $t) { + echo \html_writer::div($t); + } $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(); + echo \html_writer::div("courseid: ".$course->id); if ($course->hasprocess) { if ($course->workflowid && ($course->workflowid != $workflow->id)) { $usedcourses[] = $course->id; + echo \html_writer::div("used other wfid: ".$course->workflowid); } } else if ($course->delay && $course->delay > time()) { $delayedcourses[] = $course->id; + echo \html_writer::div("delayed: ".$course->delay); } else { - $counttriggered++; $coursestriggered[] = $course->id; + echo \html_writer::div("triggered: ".$course->id); } $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; diff --git a/db/upgrade.php b/db/upgrade.php index 691cd7ba..95fac7bc 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -595,7 +595,9 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { $field = new xmldb_field('manual', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'sortindex'); // Launch rename field key. - $dbman->rename_field($table, $field, 'manually'); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'manually'); + } // Lifecycle savepoint reached. upgrade_plugin_savepoint(true, 2025050404, 'tool', 'lifecycle'); diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index ab24cd40..9d1ff3fb 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -82,6 +82,8 @@ $string['courses_are_alreadyin'] = '{$a} Kurse befinden sich schon in diesem Prozess oder bei seinen Prozessfehlern.'; $string['courses_are_delayed'] = '{$a} Kurse sind verzögert'; $string['courses_are_used_total'] = '{$a} Kurse bereits in anderem Prozess'; +$string['courses_candidates_alreadyin'] = ', weitere {$a} Kandidaten sind bereits in Verarbeitung oder bei den Verarbeitungsfehlern'; +$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}'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 70b442d3..9ccdb1e9 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -83,6 +83,8 @@ $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_candidates_alreadyin'] = ', another {$a} candidates are already part of this process or of the process errors'; +$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}'; diff --git a/templates/overview_trigger.mustache b/templates/overview_trigger.mustache index c8f4bf14..d2b32f43 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" } @@ -54,29 +55,18 @@ {{^additionalinfo}} {{#triggeredcourses}} - + {{triggeredcourses}} {{/triggeredcourses}} - {{^triggeredcourses}} - {{#excludedcourses}} - - - {{excludedcourses}} - - - {{/excludedcourses}} - {{^excludedcourses}} - 0 - {{/excludedcourses}} - {{/triggeredcourses}} - {{#delayedcourses}} - {{delayedcourses}} - {{/delayedcourses}} - {{#alreadyin}} - {{alreadyin}} - {{/alreadyin}} + {{#excludedcourses}} + + + {{excludedcourses}} + + + {{/excludedcourses}} {{/additionalinfo}} {{/automatic}} {{^automatic}} diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 31537e42..ed0e236a 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -118,7 +118,10 @@ {{#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}}
diff --git a/workflowoverview.php b/workflowoverview.php index bc24fdd6..8b752824 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -43,6 +43,7 @@ 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\process_courses_table; use tool_lifecycle\local\table\triggered_courses_table; use tool_lifecycle\processor; use tool_lifecycle\settings_type; @@ -272,10 +273,23 @@ } else if ($amounts[$trigger->sortindex]->excluded) { $trigger->classfires = "border-danger"; } - $trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded; - $trigger->triggeredcourses = $amounts[$trigger->sortindex]->triggered; - $trigger->delayedcourses = $amounts[$trigger->sortindex]->delayed; - $trigger->alreadyin = $amounts[$trigger->sortindex]->alreadyin; + $trigger->tooltip = ""; + if ($trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded) { + $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); + } + } } } } @@ -390,14 +404,16 @@ $hiddenfieldssearch[] = ['name' => 'delayed', 'value' => $delayed]; $tablecoursesamount = count($coursesdelayed); } else if ($processes) { // Display courses table with courses in a process or in state process error for this workflow. - $table = new processes_courses_table( $coursesdelayed, 'delayed', - null, $workflow->title, $workflowid, $search); + $coursesinprocess = $DB->get_fieldset('tool_lifecycle_process', 'courseid', ['workflowid' => $workflow->id]); + $coursesprocesserrors = $DB->get_fieldset('tool_lifecycle_proc_error', 'courseid', ['workflowid' => $workflow->id]); + $coursesprocess = array_merge($coursesinprocess, $coursesprocesserrors); + $table = new process_courses_table($coursesprocess, $workflow->title, $workflow->id, $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); ob_end_clean(); - $hiddenfieldssearch[] = ['name' => 'delayed', 'value' => $delayed]; - $tablecoursesamount = count($coursesdelayed); + $hiddenfieldssearch[] = ['name' => 'processes', 'value' => $processes]; + $tablecoursesamount = count($coursesprocess); } 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', @@ -426,6 +442,7 @@ } $disableworkflowlink = ""; $abortdisableworkflowlink = ""; +$workflowprocesseslink = ""; if ($isactive) { // Disable workflow link. $alt = get_string('disableworkflow', 'tool_lifecycle'); @@ -495,24 +512,22 @@ if ($showdetails) { // The triggers total box. $data['displaytotaltriggered'] = $displaytotaltriggered; - $triggered = $amounts['all']->triggered ?? 0; + $triggered = count($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 = 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; } $addtriggerselect = ""; From 448d68c7a86f46908bfa56308c9e02b2c02ae742 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 6 Jul 2025 14:42:11 +0200 Subject: [PATCH 054/170] debug messages in processor.php removed --- classes/processor.php | 41 +---------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 81e59d70..77fb65a1 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -267,14 +267,7 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { // Now get the amount of courses triggered by this trigger. $sql = 'SELECT {course}.id from {course} WHERE '. $where; $triggercoursesall = $DB->get_fieldset_sql($sql, $whereparams); - echo \html_writer::div("triggercoursesall:"); - foreach($triggercoursesall as $t) { - echo \html_writer::div($t); - } - echo \html_writer::div("delayed:"); - foreach($delayed as $t) { - echo \html_writer::div($t); - } + // Get delayed courses which would be triggered by this trigger. $delayedcourses = count(array_intersect($triggercoursesall, $delayed)); @@ -283,19 +276,8 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { [$insql, $inparams] = $DB->get_in_or_equal($excluded, SQL_PARAMS_NAMED); $where .= " AND NOT {course}.id {$insql}"; $whereparams = array_merge($whereparams, $inparams); - echo \html_writer::div("excluded:"); - foreach($excluded as $t) { - echo \html_writer::div($t); - } } $sql = 'SELECT count({course}.id) from {course} WHERE '. $where; - $sqldeb = 'SELECT {course}.id from {course} WHERE '. $where; - $triggercoursesrs = $DB->get_fieldset_sql($sqldeb, $whereparams); - echo \html_writer::div("triggercoursesrs:"); - foreach($triggercoursesrs as $t) { - echo \html_writer::div($t); - } - echo \html_writer::div($sqldeb); $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. @@ -306,19 +288,6 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { WHERE (p.courseid IS NOT NULL AND p.workflowid = $trigger->workflowid) OR (pe.courseid IS NOT NULL AND pe.workflowid = $trigger->workflowid) )"; - $sqldeb .= " AND {course}.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 = $trigger->workflowid) - OR (pe.courseid IS NOT NULL AND pe.workflowid = $trigger->workflowid) - )"; - $newcoursesrs = $DB->get_fieldset_sql($sqldeb, $whereparams); - echo \html_writer::div("newcoursesrs:"); - foreach($newcoursesrs as $t) { - echo \html_writer::div($t); - } - echo \html_writer::div($sqldeb); $newcourses = $DB->count_records_sql($sql, $whereparams); return [$triggercourses, $newcourses, $delayedcourses]; @@ -457,27 +426,19 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { 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); - echo \html_writer::div("excluded for workflow:"); - foreach($excludedcourses as $t) { - echo \html_writer::div($t); - } $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(); - echo \html_writer::div("courseid: ".$course->id); if ($course->hasprocess) { if ($course->workflowid && ($course->workflowid != $workflow->id)) { $usedcourses[] = $course->id; - echo \html_writer::div("used other wfid: ".$course->workflowid); } } else if ($course->delay && $course->delay > time()) { $delayedcourses[] = $course->id; - echo \html_writer::div("delayed: ".$course->delay); } else { $coursestriggered[] = $course->id; - echo \html_writer::div("triggered: ".$course->id); } $recordset->next(); } From fae6a3fff7ef1865486e4dbd9fc6f68ed079e2cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 6 Jul 2025 12:43:21 +0000 Subject: [PATCH 055/170] Sync with Learnweb's centralized pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..81ba4368 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,53 @@ +> **Note:** Please fill out all required sections and remove irrelevant ones. +### 🔀 Purpose of this PR: + +- [ ] Fixes a bug +- [ ] Updates for a new Moodle version +- [ ] Adds a new feature of functionality +- [ ] Improves or enhances existing features +- [ ] Refactoring: restructures code for better performance or maintainability +- [ ] Testing: add missing or improve existing tests +- [ ] Miscellaneous: code cleaning (without functional changes), documentation, configuration, ... + +--- + +### 📝 Description: + +Please describe the purpose of this PR in a few sentences. + +- What feature or bug does it address? +- Why is this change or addition necessary? +- What is the expected behavior after the change? + +--- + +### 📋 Checklist + +Please confirm the following (check all that apply): + +- [ ] I have `phpunit` and/or `behat` tests that cover my changes or additions. +- [ ] Code passes the code checker without errors and warnings. +- [ ] Code passes the moodle-ci/cd pipeline on all supported Moodle versions or the ones the plugin supports. +- [ ] Code does not have `var_dump()` or `var_export` or any other debugging statements (or commented out code) that + should not appear on the productive branch. +- [ ] Code only uses language strings instead of hard-coded strings. +- [ ] If there are changes in the database: I updated/created the necessary upgrade steps in `db/upgrade.php` and + updated the `version.php`. +- [ ] If there are changes in javascript: I build new `.min` files with the `grunt amd` command. +- [ ] If it is a Moodle update PR: I read the release notes, updated the `version.php` and the `CHANGES.md`. + I ran all tests thoroughly checking for errors. I checked if bootstrap had any changes/deprecations that require + changes in the plugins UI. + +--- + +### 🔍 Related Issues + +- Related to #[IssueNumber] + +--- + +### 🧾📸🌐 Additional Information (like screenshots, documentation, links, etc.) + +Any other relevant information. + +--- \ No newline at end of file From 903aea05dc03799afac3340f232606b4177e9924 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 6 Jul 2025 15:37:19 +0200 Subject: [PATCH 056/170] get back erroneously removed function get_processes_by_workflow --- classes/local/manager/process_manager.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index 92e2761f..f12c4091 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -142,6 +142,22 @@ public static function count_process_errors_by_workflow($workflowid) { return $DB->count_records('tool_lifecycle_proc_error', ['workflowid' => $workflowid]); } + /** + * Returns all processes for given workflow id + * @param int $workflowid id of the workflow + * @return array of proccesses initiated by specifed workflow id + * @throws \dml_exception + */ + public static function get_processes_by_workflow($workflowid) { + global $DB; + $records = $DB->get_records('tool_lifecycle_process', ['workflowid' => $workflowid]); + $processes = []; + foreach ($records as $record) { + $processes[] = process::from_record($record); + } + return $processes; + } + /** * Proceeds the process to the next step. * @param process $process From 9232de7e28acdf1cc9867df70d6ec9d18a7f6c80 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 6 Jul 2025 17:25:32 +0200 Subject: [PATCH 057/170] workflowoverview: show also 0 courses in exclude trigger. Make instancenames in supplugin form of active workflows static --- classes/local/form/form_step_instance.php | 16 +++++++++++----- classes/local/form/form_trigger_instance.php | 16 +++++++++++----- styles.css | 3 ++- templates/overview_trigger.mustache | 7 +++++++ workflowoverview.php | 3 +++ 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/classes/local/form/form_step_instance.php b/classes/local/form/form_step_instance.php index 564b6526..86d8d1bf 100644 --- a/classes/local/form/form_step_instance.php +++ b/classes/local/form/form_step_instance.php @@ -111,16 +111,22 @@ 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')); + $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); diff --git a/classes/local/form/form_trigger_instance.php b/classes/local/form/form_trigger_instance.php index bf775e23..e3d075a3 100644 --- a/classes/local/form/form_trigger_instance.php +++ b/classes/local/form/form_trigger_instance.php @@ -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, diff --git a/styles.css b/styles.css index 61b7dd2e..991eb78c 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; } diff --git a/templates/overview_trigger.mustache b/templates/overview_trigger.mustache index d2b32f43..e3c071a3 100644 --- a/templates/overview_trigger.mustache +++ b/templates/overview_trigger.mustache @@ -67,6 +67,13 @@ {{/excludedcourses}} + {{^excludedcourses}} + {{#exclude}} + + 0 + + {{/exclude}} + {{/excludedcourses}} {{/additionalinfo}} {{/automatic}} {{^automatic}} diff --git a/workflowoverview.php b/workflowoverview.php index 8b752824..4677e2c9 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -38,6 +38,7 @@ 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; @@ -267,6 +268,8 @@ $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"; From 12e3159284d721ade363513006fe56c2ed8a75a0 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 11 Jul 2025 14:46:49 +0200 Subject: [PATCH 058/170] proceed, rollback event: take course context when contex missing --- classes/event/process_proceeded.php | 3 ++- classes/event/process_rollback.php | 3 ++- classes/local/manager/workflow_manager.php | 1 + workflowoverview.php | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) 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 3fbeb611..429f8617 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; @@ -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, diff --git a/classes/local/manager/workflow_manager.php b/classes/local/manager/workflow_manager.php index 24d232ee..510e6400 100644 --- a/classes/local/manager/workflow_manager.php +++ b/classes/local/manager/workflow_manager.php @@ -267,6 +267,7 @@ public static function activate_workflow($workflowid) { $workflow->manually |= $lib->is_manual_trigger(); } $workflow->timeactive = time(); + $workflow->timedeactive = null; if (!$workflow->manually) { $workflow->sortindex = count(self::get_active_automatic_workflows()) + 1; } diff --git a/workflowoverview.php b/workflowoverview.php index 4677e2c9..0a5ec46f 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -594,6 +594,7 @@ $data['addstepselect'] = $addstepselect; $data['activate'] = $activate; $data['newworkflow'] = $newworkflow; + $data['activatebutton'] = ""; } else if ($isdeactivated) { $activate = $OUTPUT->single_button(new \moodle_url(urls::ACTIVE_WORKFLOWS, [ From 4437a9789d92aa286d611bebf9cb8e881f18b2ea Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 11 Jul 2025 21:10:41 +0200 Subject: [PATCH 059/170] show amount of delays that would be deleted next to button --- delayedcourses.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/delayedcourses.php b/delayedcourses.php index 9e67bd6d..644e9dc1 100644 --- a/delayedcourses.php +++ b/delayedcourses.php @@ -182,6 +182,9 @@ $button = new single_button(new moodle_url('confirmation.php'), 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'); } From 7d4f0591931f2dd95fbe30eac88a191fb8525f4c Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 15 Jul 2025 07:22:18 +0200 Subject: [PATCH 060/170] fix customfielddelay missing field error message --- trigger/customfielddelay/lib.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trigger/customfielddelay/lib.php b/trigger/customfielddelay/lib.php index bea2d908..f2b57797 100644 --- a/trigger/customfielddelay/lib.php +++ b/trigger/customfielddelay/lib.php @@ -57,7 +57,8 @@ 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 " . "ON d.contextid = cxt.id AND cxt.contextlevel=" . CONTEXT_COURSE . " " . From c7287d265489c581e2fb69ccf54cde2fb2a451d2 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 15 Jul 2025 08:12:22 +0200 Subject: [PATCH 061/170] fix invert query in byrole trigger --- trigger/byrole/lib.php | 6 +++++- trigger/customfielddelay/lib.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/trigger/byrole/lib.php b/trigger/byrole/lib.php index 7473022e..8d893e2a 100644 --- a/trigger/byrole/lib.php +++ b/trigger/byrole/lib.php @@ -100,7 +100,11 @@ private function update_courses($triggerid) { list($insql, $inparams) = $DB->get_in_or_equal($this->get_roles($triggerid), SQL_PARAMS_NAMED); - $invert = settings_manager::get_settings($triggerid, settings_type::TRIGGER)['invert'] && true; + 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 diff --git a/trigger/customfielddelay/lib.php b/trigger/customfielddelay/lib.php index f2b57797..aae451b2 100644 --- a/trigger/customfielddelay/lib.php +++ b/trigger/customfielddelay/lib.php @@ -58,7 +58,7 @@ public function get_course_recordset_where($triggerid) { $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', - 'lifecycletrigger_customfielddelay', '', $fieldname); + 'moodle', '', $fieldname); } $where = "{course}.id in (select cxt.instanceid from {context} cxt join {customfield_data} d " . "ON d.contextid = cxt.id AND cxt.contextlevel=" . CONTEXT_COURSE . " " . From 38c17c532ff5e5e86b9876b448b3c8cc43c48cb4 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 15 Jul 2025 09:07:46 +0200 Subject: [PATCH 062/170] exclude trigger: show excluded 0 as well --- .../lang/de/lifecycletrigger_customfielddelay.php | 1 + .../lang/en/lifecycletrigger_customfielddelay.php | 1 + trigger/customfielddelay/lib.php | 2 +- workflowoverview.php | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php b/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php index 37f0a6c0..a7b51e29 100644 --- a/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php +++ b/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php @@ -27,6 +27,7 @@ $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['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 280ecf60..0670a652 100644 --- a/trigger/customfielddelay/lang/en/lifecycletrigger_customfielddelay.php +++ b/trigger/customfielddelay/lang/en/lifecycletrigger_customfielddelay.php @@ -27,6 +27,7 @@ $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['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 aae451b2..f2b57797 100644 --- a/trigger/customfielddelay/lib.php +++ b/trigger/customfielddelay/lib.php @@ -58,7 +58,7 @@ public function get_course_recordset_where($triggerid) { $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', - 'moodle', '', $fieldname); + 'lifecycletrigger_customfielddelay', '', $fieldname); } $where = "{course}.id in (select cxt.instanceid from {context} cxt join {customfield_data} d " . "ON d.contextid = cxt.id AND cxt.contextlevel=" . CONTEXT_COURSE . " " . diff --git a/workflowoverview.php b/workflowoverview.php index 0a5ec46f..3337673f 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -277,7 +277,7 @@ $trigger->classfires = "border-danger"; } $trigger->tooltip = ""; - if ($trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded) { + if ($trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded || $trigger->exclude) { $trigger->tooltip = get_string('courses_will_be_excluded', 'tool_lifecycle', $trigger->excludedcourses); } else { From 70bc14a15952cfab9a88c0c831ae44801668864c Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 15 Jul 2025 09:14:19 +0200 Subject: [PATCH 063/170] consider trigger setting invert as exclude --- classes/processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/processor.php b/classes/processor.php index 77fb65a1..32acafdc 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -368,7 +368,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { foreach ($triggers as $trigger) { $trigger = (object)(array) $trigger; // Cast to normal object to be able to set dynamic properties. $settings = settings_manager::get_settings($trigger->id, settings_type::TRIGGER); - $trigger->exclude = $settings['exclude'] ?? false; + $trigger->exclude = $settings['exclude'] ?? ($settings['invert'] ?? false); $obj = new \stdClass(); $lib = lib_manager::get_trigger_lib($trigger->subpluginname); if ($lib->is_manual_trigger()) { From 385bcbe36eec6058035b11393c9ae07cb4944b8f Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 15 Jul 2025 09:30:21 +0200 Subject: [PATCH 064/170] exclude trigger: show excluded 0 as well - part 2 --- classes/processor.php | 2 +- workflowoverview.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 32acafdc..77fb65a1 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -368,7 +368,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { foreach ($triggers as $trigger) { $trigger = (object)(array) $trigger; // Cast to normal object to be able to set dynamic properties. $settings = settings_manager::get_settings($trigger->id, settings_type::TRIGGER); - $trigger->exclude = $settings['exclude'] ?? ($settings['invert'] ?? false); + $trigger->exclude = $settings['exclude'] ?? false; $obj = new \stdClass(); $lib = lib_manager::get_trigger_lib($trigger->subpluginname); if ($lib->is_manual_trigger()) { diff --git a/workflowoverview.php b/workflowoverview.php index 3337673f..fff0d29b 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -277,7 +277,8 @@ $trigger->classfires = "border-danger"; } $trigger->tooltip = ""; - if ($trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded || $trigger->exclude) { + if (isset($amounts[$trigger->sortindex]->excluded)) { + $trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded; $trigger->tooltip = get_string('courses_will_be_excluded', 'tool_lifecycle', $trigger->excludedcourses); } else { From 9c46f8756af2f3ae1f5b6a3bd393a0a412c3a4b5 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 15 Jul 2025 09:34:24 +0200 Subject: [PATCH 065/170] exclude trigger: show excluded 0 as well - part 3 --- classes/processor.php | 1 - 1 file changed, 1 deletion(-) diff --git a/classes/processor.php b/classes/processor.php index 77fb65a1..c2cf7b0b 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -376,7 +376,6 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { } else { $obj->automatic = true; $obj->triggered = 0; - $obj->excluded = 0; // 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); From d7954e813887ef8560e5a44ef29dfb0ff60e4cee Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 15 Jul 2025 10:09:55 +0200 Subject: [PATCH 066/170] workflowoverview: show triggered 0 as well --- classes/processor.php | 1 + templates/overview_trigger.mustache | 9 +++++++++ workflowoverview.php | 8 ++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index c2cf7b0b..ccc37f8a 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -376,6 +376,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { } else { $obj->automatic = true; $obj->triggered = 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); diff --git a/templates/overview_trigger.mustache b/templates/overview_trigger.mustache index e3c071a3..509bea29 100644 --- a/templates/overview_trigger.mustache +++ b/templates/overview_trigger.mustache @@ -60,6 +60,15 @@ {{/triggeredcourses}} + {{^triggeredcourses}} + {{^excludedcourses}} + {{^exclude}} + + 0 + + {{/exclude}} + {{/excludedcourses}} + {{/triggeredcourses}} {{#excludedcourses}} diff --git a/workflowoverview.php b/workflowoverview.php index fff0d29b..216fe1b0 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -277,7 +277,7 @@ $trigger->classfires = "border-danger"; } $trigger->tooltip = ""; - if (isset($amounts[$trigger->sortindex]->excluded)) { + if ($amounts[$trigger->sortindex]->excluded !== false) { $trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded; $trigger->tooltip = get_string('courses_will_be_excluded', 'tool_lifecycle', $trigger->excludedcourses); @@ -482,6 +482,9 @@ $workflowprocesseslink = "
".$workflowprocesseslink; } +if (!($isactive || $isdeactivated)) { + $lastrun = 0; +} $data = [ 'editsettingslink' => (new moodle_url(urls::EDIT_WORKFLOW, ['wf' => $workflow->id]))->out(false), 'title' => $workflow->title, @@ -507,7 +510,8 @@ 'showdetailsicon' => $showdetails == 0, 'isactive' => $isactive || $isdeactivated, 'nextrun' => $nextrunout, - 'lastrun' => userdate($lastrun, get_string('strftimedatetimeshort', 'langconfig')), + 'lastrun' => $lastrun ? + userdate($lastrun, get_string('strftimedatetimeshort', 'langconfig')) : '-', 'nomanualtriggerinvolved' => $nomanualtriggerinvolved, 'disableworkflowlink' => $disableworkflowlink, 'abortdisableworkflowlink' => $abortdisableworkflowlink, From a9dd0c6c6ae5cb101dd63ff6ce284518129cc8b5 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 15 Jul 2025 12:32:31 +0200 Subject: [PATCH 067/170] repair sorting in triggered courses and step courses lists --- classes/local/table/process_courses_table.php | 5 ++--- classes/local/table/triggered_courses_table.php | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/classes/local/table/process_courses_table.php b/classes/local/table/process_courses_table.php index 15abf188..d94f9670 100644 --- a/classes/local/table/process_courses_table.php +++ b/classes/local/table/process_courses_table.php @@ -45,8 +45,8 @@ class process_courses_table extends \table_sql { /** * Builds a table of courses. * @param array $courseids of the courses to list - * @param string $workflowname optional, if type delayed - * @param null $workflowid optional, if type delayed + * @param string $workflowname + * @param null $workflowid * @param string $filterdata optional, term to filter the table by course id or -name * @throws \coding_exception * @throws \dml_exception @@ -96,7 +96,6 @@ public function __construct($courseids, $workflowname = '', $workflowid = null, } $this->set_sql($fields, $from, $where, $inparams); - $this->set_sortdata([['sortby' => 'fullname', 'sortorder' => '1']]); } /** diff --git a/classes/local/table/triggered_courses_table.php b/classes/local/table/triggered_courses_table.php index b48c2a67..e4af551f 100644 --- a/classes/local/table/triggered_courses_table.php +++ b/classes/local/table/triggered_courses_table.php @@ -134,7 +134,7 @@ public function __construct($courseids, $type, $triggername = '', $workflowname } $this->set_sql($fields, $from, $where, $inparams); - $this->set_sortdata([['sortby' => 'fullname', 'sortorder' => '1']]); + // $this->set_sortdata([['sortby' => 'fullname', 'sortorder' => '1']]); } /** From fbb759331c94f5aa57a8819ef1eb3d1522cd6c40 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 15 Jul 2025 12:42:13 +0200 Subject: [PATCH 068/170] codechecker issue --- classes/local/table/triggered_courses_table.php | 1 - 1 file changed, 1 deletion(-) diff --git a/classes/local/table/triggered_courses_table.php b/classes/local/table/triggered_courses_table.php index e4af551f..adff596e 100644 --- a/classes/local/table/triggered_courses_table.php +++ b/classes/local/table/triggered_courses_table.php @@ -134,7 +134,6 @@ public function __construct($courseids, $type, $triggername = '', $workflowname } $this->set_sql($fields, $from, $where, $inparams); - // $this->set_sortdata([['sortby' => 'fullname', 'sortorder' => '1']]); } /** From 3beb41b231aa8b8eb9f093802a4ca335346ab0fb Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 16 Jul 2025 09:00:28 +0200 Subject: [PATCH 069/170] process_courses: no processing if there is no process workflowid --- classes/local/manager/process_manager.php | 2 +- classes/local/manager/workflow_manager.php | 2 +- classes/processor.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index f12c4091..1a29ff21 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. diff --git a/classes/local/manager/workflow_manager.php b/classes/local/manager/workflow_manager.php index 510e6400..0765c060 100644 --- a/classes/local/manager/workflow_manager.php +++ b/classes/local/manager/workflow_manager.php @@ -132,7 +132,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 diff --git a/classes/processor.php b/classes/processor.php index ccc37f8a..61f2934d 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -89,8 +89,8 @@ public function call_trigger() { public function process_courses() { // For each process in process table. foreach (process_manager::get_processes() as $process) { - // Process only if the process has workflow id. - while ($process->workflowid) { + // Process only if the process has a valid workflow id. + if (is_int($process->workflowid)) { // New workflows with lifecycle version 4.4 and beneath did not have to have a step. if ($process->stepindex == 0) { if (!process_manager::proceed_process($process)) { From 1f6ea63c68b93f5f061895542c6d9139adfd0657 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 07:20:26 +0200 Subject: [PATCH 070/170] restore version 4.5 of function process_courses --- classes/processor.php | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 61f2934d..b3dacc99 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -87,31 +87,34 @@ public function call_trigger() { * Calls the process_course() method of each step submodule currently responsible for a given course. */ public function process_courses() { - // For each process in process table. foreach (process_manager::get_processes() as $process) { - // Process only if the process has a valid workflow id. - if (is_int($process->workflowid)) { - // New workflows with lifecycle version 4.4 and beneath did not have to have a step. + $workflow = workflow_manager::get_workflow($process->workflowid); + while (true) { + + try { + $course = get_course($process->courseid); + } catch (\dml_missing_record_exception $e) { + // 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($process->courseid, false, - $process->workflowid); + delayed_courses_manager::set_course_delayed_for_workflow($course->id, false, $workflow); break; } } - // Get current step of process and its step lib. + $step = step_manager::get_step_instance_by_workflow_index($process->workflowid, $process->stepindex); $lib = lib_manager::get_step_lib($step->subpluginname); - // Process course. try { if ($process->waiting) { - $result = $lib->process_waiting_course($process->id, $step->id, $process->courseid); + $result = $lib->process_waiting_course($process->id, $step->id, $course); } else { - $result = $lib->process_course($process->id, $step->id, $process->courseid); + $result = $lib->process_course($process->id, $step->id, $course); } } catch (\Exception $e) { - unset($process->context); process_manager::insert_process_error($process, $e); break; } @@ -120,13 +123,11 @@ public function process_courses() { break; } else if ($result == step_response::proceed()) { if (!process_manager::proceed_process($process)) { - delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, false, - $process->workflowid); + delayed_courses_manager::set_course_delayed_for_workflow($course->id, false, $workflow); break; } } else if ($result == step_response::rollback()) { - delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, - $process->workflowid); + delayed_courses_manager::set_course_delayed_for_workflow($course->id, true, $workflow); process_manager::rollback_process($process); break; } else { From fdb876da004f26ae970e61318ef66726b6f8566f Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 08:25:47 +0200 Subject: [PATCH 071/170] restore version 4.5 of function call_trigger --- classes/processor.php | 43 +++++++++++++++++++++++++++++------------ run.php | 45 ------------------------------------------- 2 files changed, 31 insertions(+), 57 deletions(-) delete mode 100644 run.php diff --git a/classes/processor.php b/classes/processor.php index b3dacc99..5a2a6b2a 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -54,32 +54,51 @@ class processor { */ public function call_trigger() { $activeworkflows = workflow_manager::get_active_automatic_workflows(); - $globallydelayedcourses = delayed_courses_manager::get_globally_delayed_courses(); + $exclude = []; foreach ($activeworkflows as $workflow) { $countcourses = 0; + $counttriggered = 0; + $countexcluded = 0; mtrace('Calling triggers for workflow "' . $workflow->title . '"'); $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), + $exclude); } - $recordset = $this->get_course_recordset($triggers, array_merge($delayedcourses, $sitecourse)); + $recordset = $this->get_course_recordset($triggers, $exclude); while ($recordset->valid()) { $course = $recordset->current(); $countcourses++; + 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()) { + $recordset->next(); + continue 2; + } + if ($response == trigger_response::exclude()) { + array_push($exclude, $course->id); + $countexcluded++; + $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++; $recordset->next(); } mtrace(" $countcourses courses processed."); + mtrace(" $counttriggered courses triggered."); + mtrace(" $countexcluded courses excluded."); } } @@ -299,7 +318,7 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { * 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. + * @return array with relevant courses. * @throws \coding_exception * @throws \dml_exception */ diff --git a/run.php b/run.php deleted file mode 100644 index 65a79800..00000000 --- a/run.php +++ /dev/null @@ -1,45 +0,0 @@ -. - -/** - * Trigger manually the task for working on lifecycle processes - * - * @package tool_lifecycle - * @copyright 2025 Thomas Niedermaier University Münster - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -use tool_lifecycle\local\manager\lib_manager; -use tool_lifecycle\local\manager\step_manager; -use tool_lifecycle\processor; - -require_once(__DIR__ . '/../../../config.php'); - -require_login(); - -$processor = new processor(); -$processor->call_trigger(); - -$steps = step_manager::get_step_types(); -$steplibs = []; -foreach ($steps as $id => $step) { - $steplibs[$id] = lib_manager::get_step_lib($id); - $steplibs[$id]->pre_processing_bulk_operation(); -} -$processor->process_courses(); -foreach ($steps as $id => $step) { - $steplibs[$id]->post_processing_bulk_operation(); -} From b1deea165a6f2e6f96ce1557b18fab430241aac7 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 15:03:47 +0200 Subject: [PATCH 072/170] add run link to timetrigger row --- classes/urls.php | 2 + db/subplugins.json | 4 +- lang/de/tool_lifecycle.php | 1 + lang/en/tool_lifecycle.php | 1 + run.php | 57 +++++++++++++++++++ .../adminapprove/tests/admin_approve_test.php | 3 +- templates/workflowoverview.mustache | 8 ++- workflowoverview.php | 1 + 8 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 run.php diff --git a/classes/urls.php b/classes/urls.php index e80b7af8..28f9fcef 100644 --- a/classes/urls.php +++ b/classes/urls.php @@ -61,5 +61,7 @@ class urls { 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/db/subplugins.json b/db/subplugins.json index e21b91d6..79de0157 100644 --- a/db/subplugins.json +++ b/db/subplugins.json @@ -1,6 +1,6 @@ { "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/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 9d1ff3fb..5186e81c 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -227,6 +227,7 @@ $string['restore_workflow_not_found'] = 'Falsches Format der Sicherungsdatei. Der Workflow konnte nicht gefunden werden.'; $string['rollback'] = 'Zurücksetzung'; $string['rolledback'] = 'Zurückgesetzt'; +$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'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 9ccdb1e9..cfe50bf9 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -227,6 +227,7 @@ $string['restore_workflow_not_found'] = 'Wrong format of the backup file. The workflow could not be found.'; $string['rollback'] = 'Rollback'; $string['rolledback'] = 'Rolled back'; +$string['runtask'] = 'Run scheduled lifecycle task'; $string['searchcourses'] = 'Search for courses'; $string['see_in_workflow'] = 'See in workflow'; $string['show_delays'] = 'Kind of view'; diff --git a/run.php b/run.php new file mode 100644 index 00000000..6138a2c1 --- /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_login(); + +$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/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/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index ed0e236a..51032789 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -151,7 +151,13 @@
{{#str}} courseselectionrun_title, tool_lifecycle{{/str}}
{{#nomanualtriggerinvolved}}
{{/nomanualtriggerinvolved}} diff --git a/workflowoverview.php b/workflowoverview.php index 216fe1b0..1b758821 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -516,6 +516,7 @@ 'disableworkflowlink' => $disableworkflowlink, 'abortdisableworkflowlink' => $abortdisableworkflowlink, 'workflowprocesseslink' => $workflowprocesseslink, + 'runlink' => new \moodle_url(urls::RUN), ]; if ($showdetails) { // The triggers total box. From c57034773f41f2adca74a566158f9466c8bae4c7 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 15:11:35 +0200 Subject: [PATCH 073/170] add run string for plugin --- lang/de/tool_lifecycle.php | 1 + lang/en/tool_lifecycle.php | 1 + templates/workflowoverview.mustache | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 5186e81c..7f447b07 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -227,6 +227,7 @@ $string['restore_workflow_not_found'] = 'Falsches Format der Sicherungsdatei. Der Workflow konnte nicht gefunden werden.'; $string['rollback'] = 'Zurücksetzung'; $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'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index cfe50bf9..baf44074 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -227,6 +227,7 @@ $string['restore_workflow_not_found'] = 'Wrong format of the backup file. The workflow could not be found.'; $string['rollback'] = 'Rollback'; $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'; diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 51032789..f3109e11 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -154,7 +154,7 @@ {{#str}} nextrun, tool_lifecycle, {{{nextrun}}} {{/str}} - {{#str}} run {{/str}} + {{#str}} run, tool_lifecycle {{/str}}
From d24a888a0d72a92dec2110232f5696091d84ca99 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 15:37:35 +0200 Subject: [PATCH 074/170] call_trigger: no debug messages when behat testing --- classes/processor.php | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 5a2a6b2a..ddc72ff2 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -60,7 +60,14 @@ public function call_trigger() { $countcourses = 0; $counttriggered = 0; $countexcluded = 0; - mtrace('Calling triggers for workflow "' . $workflow->title . '"'); + + if (!defined('BEHAT_SITE_RUNNING')) { + if (isloggedin()) { + \html_writer::div('Calling triggers for workflow "' . $workflow->title . '"'); + } else { + mtrace('Calling triggers for workflow "' . $workflow->title . '"'); + } + } $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); if (!$workflow->includesitecourse) { $exclude[] = 1; @@ -96,9 +103,17 @@ public function call_trigger() { $counttriggered++; $recordset->next(); } - mtrace(" $countcourses courses processed."); - mtrace(" $counttriggered courses triggered."); - mtrace(" $countexcluded courses excluded."); + if (!defined('BEHAT_SITE_RUNNING')) { + if (isloggedin()) { + \html_writer::div(" $countcourses courses processed."); + \html_writer::div(" $counttriggered courses triggered."); + \html_writer::div(" $countexcluded courses excluded."); + } else { + mtrace(" $countcourses courses processed."); + mtrace(" $counttriggered courses triggered."); + mtrace(" $countexcluded courses excluded."); + } + } } } From e43980c1d91151a4247cf3bcdb24324487aa82a0 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 15:45:55 +0200 Subject: [PATCH 075/170] fix step email context course id --- step/email/lib.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/step/email/lib.php b/step/email/lib.php index beaf0731..22384461 100644 --- a/step/email/lib.php +++ b/step/email/lib.php @@ -51,14 +51,14 @@ class email extends libbase { * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param int $course to be processed. + * @param object $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception */ public function process_course($processid, $instanceid, $course) { global $DB; - $coursecontext = \context_course::instance($course); + $coursecontext = \context_course::instance($course->id); $userstobeinformed = get_enrolled_users($coursecontext, 'lifecyclestep/email:preventdeletion', 0, 'u.id', null, null, null, true); foreach ($userstobeinformed as $user) { @@ -79,7 +79,7 @@ public function process_course($processid, $instanceid, $course) { * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param int $course to be processed. + * @param object $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception From 4f05744c41245152d1286afe41be2354230ef23f Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 16:35:05 +0200 Subject: [PATCH 076/170] echo run.php debug messages --- classes/processor.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index ddc72ff2..02b8482d 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -53,6 +53,7 @@ class processor { * Processes the trigger plugins for all relevant courses. */ public function call_trigger() { + $activeworkflows = workflow_manager::get_active_automatic_workflows(); $exclude = []; @@ -63,7 +64,7 @@ public function call_trigger() { if (!defined('BEHAT_SITE_RUNNING')) { if (isloggedin()) { - \html_writer::div('Calling triggers for workflow "' . $workflow->title . '"'); + echo \html_writer::div('Calling triggers for workflow "' . $workflow->title . '"'); } else { mtrace('Calling triggers for workflow "' . $workflow->title . '"'); } @@ -105,9 +106,9 @@ public function call_trigger() { } if (!defined('BEHAT_SITE_RUNNING')) { if (isloggedin()) { - \html_writer::div(" $countcourses courses processed."); - \html_writer::div(" $counttriggered courses triggered."); - \html_writer::div(" $countexcluded courses excluded."); + 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."); From a94ceac00a78612df2c02b3d9bf8e79f610740c9 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 17:12:02 +0200 Subject: [PATCH 077/170] call_trigger: mtrace only when called by cron --- classes/processor.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 02b8482d..a6b86e39 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -53,6 +53,9 @@ class processor { * Processes the trigger plugins for all relevant courses. */ public function call_trigger() { + global $PAGE; + + $run = isset($PAGE->url) && strpos($PAGE->url, 'run.php') !== false; $activeworkflows = workflow_manager::get_active_automatic_workflows(); $exclude = []; @@ -63,7 +66,7 @@ public function call_trigger() { $countexcluded = 0; if (!defined('BEHAT_SITE_RUNNING')) { - if (isloggedin()) { + if ($run) { echo \html_writer::div('Calling triggers for workflow "' . $workflow->title . '"'); } else { mtrace('Calling triggers for workflow "' . $workflow->title . '"'); @@ -105,7 +108,7 @@ public function call_trigger() { $recordset->next(); } if (!defined('BEHAT_SITE_RUNNING')) { - if (isloggedin()) { + if ($run) { echo \html_writer::div(" $countcourses courses processed."); echo \html_writer::div(" $counttriggered courses triggered."); echo \html_writer::div(" $countexcluded courses excluded."); From 97c84e7938f1c041c4faf38af11b540bf5617454 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 17:15:59 +0200 Subject: [PATCH 078/170] call_trigger: debug msg with timestamp --- classes/processor.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index a6b86e39..fa3f6d61 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -67,9 +67,11 @@ public function call_trigger() { if (!defined('BEHAT_SITE_RUNNING')) { if ($run) { - echo \html_writer::div('Calling triggers for workflow "' . $workflow->title . '"'); + echo \html_writer::div('Calling triggers for workflow "' . $workflow->title . '" '. + userdate(time(), get_string('strftimedatetime'))); } else { - mtrace('Calling triggers for workflow "' . $workflow->title . '"'); + mtrace('Calling triggers for workflow "' . $workflow->title . '" '. + userdate(time(), get_string('strftimedatetime'))); } } $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); From 283a8ebd21dba2eaf193eb590d5054a625f427eb Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 17:34:14 +0200 Subject: [PATCH 079/170] call_trigger: debug msg with timestamp 2 --- classes/processor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index fa3f6d61..bb4971d9 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -53,9 +53,9 @@ class processor { * Processes the trigger plugins for all relevant courses. */ public function call_trigger() { - global $PAGE; + global $FULLSCRIPT; - $run = isset($PAGE->url) && strpos($PAGE->url, 'run.php') !== false; + $run = str_contains($FULLSCRIPT, 'run.php'); $activeworkflows = workflow_manager::get_active_automatic_workflows(); $exclude = []; From bdccc7cd18a2304c20be8d734bb5681096ec3bcd Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 17:39:41 +0200 Subject: [PATCH 080/170] call_trigger: debug msg with timestamp 3 --- classes/processor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index bb4971d9..ee452315 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -68,10 +68,10 @@ public function call_trigger() { if (!defined('BEHAT_SITE_RUNNING')) { if ($run) { echo \html_writer::div('Calling triggers for workflow "' . $workflow->title . '" '. - userdate(time(), get_string('strftimedatetime'))); + userdate(time(), get_string('strftimedatetimeaccurate'))); } else { mtrace('Calling triggers for workflow "' . $workflow->title . '" '. - userdate(time(), get_string('strftimedatetime'))); + userdate(time(), get_string('strftimedatetimeaccurate'))); } } $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); From 4ec66e77e4a5fadc59d016b6af150332b5b916f5 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 17:42:03 +0200 Subject: [PATCH 081/170] fix step email context course id 2 --- step/email/lib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/step/email/lib.php b/step/email/lib.php index 22384461..7123d707 100644 --- a/step/email/lib.php +++ b/step/email/lib.php @@ -64,7 +64,7 @@ public function process_course($processid, $instanceid, $course) { foreach ($userstobeinformed as $user) { $record = new \stdClass(); $record->touser = $user->id; - $record->courseid = $course; + $record->courseid = $course->id; $record->instanceid = $instanceid; $DB->insert_record('lifecyclestep_email', $record); } From e32cc2f1b2850e91462e662d0e24cbe8eab8d967 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 17:54:22 +0200 Subject: [PATCH 082/170] call_trigger: debug msg with timestamp 4 --- classes/processor.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/classes/processor.php b/classes/processor.php index ee452315..ee2040d7 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -60,6 +60,16 @@ public function call_trigger() { $activeworkflows = workflow_manager::get_active_automatic_workflows(); $exclude = []; + 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)); + } + } + foreach ($activeworkflows as $workflow) { $countcourses = 0; $counttriggered = 0; From 397b88e84840719a909ffbfaf59d145033a4113e Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 21:14:30 +0200 Subject: [PATCH 083/170] try to fix interaction.feature --- tests/behat/interaction.feature | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/behat/interaction.feature b/tests/behat/interaction.feature index 5477260d..721be5e2 100644 --- a/tests/behat/interaction.feature +++ b/tests/behat/interaction.feature @@ -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,11 @@ 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 run the scheduled task "tool_lifecycle\task\lifecycle_task" + 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_remaining" "table" From f9e85214250b7ea753fc45dc0627017d5624a8be Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 21:26:47 +0200 Subject: [PATCH 084/170] codechecker issue --- classes/processor.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/classes/processor.php b/classes/processor.php index ee2040d7..3a40eeb2 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -22,9 +22,11 @@ * @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 tool_lifecycle\local\entity\trigger_subplugin; use tool_lifecycle\event\process_triggered; use tool_lifecycle\local\manager\process_manager; From a045f655e5ff9e057ad18dbc2bb089cff2c4a6dd Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 22:14:01 +0200 Subject: [PATCH 085/170] fix unit test proc_error --- tests/process_error_test.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/process_error_test.php b/tests/process_error_test.php index f9052c07..6ef4c0b8 100644 --- a/tests/process_error_test.php +++ b/tests/process_error_test.php @@ -117,7 +117,8 @@ public function test_process_error_in_table(): void { } 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->assertStringContainsString("Object of class stdClass could not be converted to int", + $record->errormessage); } $this->assertEquals($process->id, $record->id); } From f5b90319253ee5937787a1383a493ea06b7b1d81 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 17 Jul 2025 22:57:39 +0200 Subject: [PATCH 086/170] fix deletecourse step --- step/deletecourse/lib.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/step/deletecourse/lib.php b/step/deletecourse/lib.php index 54b321fc..9bb6de05 100644 --- a/step/deletecourse/lib.php +++ b/step/deletecourse/lib.php @@ -51,7 +51,7 @@ class deletecourse extends libbase { * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param int $course to be processed. + * @param object $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception @@ -59,7 +59,7 @@ class deletecourse extends libbase { public function process_course($processid, $instanceid, $course) { global $CFG; - if ($course == 1) { + if ($course->id == 1) { return step_response::rollback(); } From a3a600a601fda69b03478fbda53a540d115d4335 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 18 Jul 2025 06:43:39 +0200 Subject: [PATCH 087/170] step libs function process_course: make sure course is of type stdClass --- step/adminapprove/lib.php | 2 +- step/createbackup/lib.php | 7 ++++--- step/deletecourse/lib.php | 5 +++-- step/duplicate/lib.php | 7 ++++--- step/lib.php | 5 +++-- step/makeinvisible/lib.php | 11 +++++++---- step/movecategory/lib.php | 3 ++- step/pushbackuptask/lib.php | 9 ++++----- 8 files changed, 28 insertions(+), 21 deletions(-) diff --git a/step/adminapprove/lib.php b/step/adminapprove/lib.php index a23e81bc..3bf648bd 100644 --- a/step/adminapprove/lib.php +++ b/step/adminapprove/lib.php @@ -47,7 +47,7 @@ class adminapprove extends libbase { * Process a single course. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param int $course to be processed. + * @param stdClass $course to be processed. * @return step_response */ public function process_course($processid, $instanceid, $course) { diff --git a/step/createbackup/lib.php b/step/createbackup/lib.php index 7ca0c514..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; @@ -57,7 +58,7 @@ class createbackup extends libbase { * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param int $course to be processed. + * @param stdClass $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception @@ -67,7 +68,7 @@ public function process_course($processid, $instanceid, $course) { $instanceid, settings_type::STEP)['maximumbackupspercron']) { return step_response::waiting(); // Wait with further backups til the next cron run. } - if (backup_manager::create_course_backup($course)) { + if (backup_manager::create_course_backup($course->id)) { self::$numberofbackups++; return step_response::proceed(); } @@ -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 int $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 9bb6de05..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; @@ -51,7 +52,7 @@ class deletecourse extends libbase { * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param object $course to be processed. + * @param stdClass $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception @@ -89,7 +90,7 @@ public function process_course($processid, $instanceid, $course) { * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param int $course to be processed. + * @param stdClass $course to be processed. * @return step_response * @throws \coding_exception * @throws \dml_exception diff --git a/step/duplicate/lib.php b/step/duplicate/lib.php index a281c22d..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; @@ -56,12 +57,12 @@ class duplicate extends libbase { * - 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); + $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)) { @@ -92,7 +93,7 @@ public function process_course($processid, $instanceid, $course) { * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param int $course to be processed. + * @param stdClass $course to be processed. * @return step_response * @throws \dml_exception */ diff --git a/step/lib.php b/step/lib.php index e22d13c2..955a2bf7 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; @@ -48,7 +49,7 @@ abstract class libbase { * - 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); @@ -61,7 +62,7 @@ abstract public function process_course($processid, $instanceid, $course); * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param int $course to be processed. + * @param stdClass $course to be processed. * @return step_response */ public function process_waiting_course($processid, $instanceid, $course) { 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/movecategory/lib.php b/step/movecategory/lib.php index 42d770a9..a2ac984f 100644 --- a/step/movecategory/lib.php +++ b/step/movecategory/lib.php @@ -25,6 +25,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; @@ -47,7 +48,7 @@ class movecategory 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 * @throws \coding_exception * @throws \dml_exception diff --git a/step/pushbackuptask/lib.php b/step/pushbackuptask/lib.php index 8e37e66a..577f4a03 100644 --- a/step/pushbackuptask/lib.php +++ b/step/pushbackuptask/lib.php @@ -25,6 +25,7 @@ namespace tool_lifecycle\step; use lifecyclestep_pushbackuptask\task\course_backup_task; +use stdClass; use tool_lifecycle\local\response\step_response; defined('MOODLE_INTERNAL') || die(); @@ -43,14 +44,12 @@ class pushbackuptask extends libbase { * - that a rollback for this course is necessary. * @param int $processid of the respective process. * @param int $instanceid of the step instance. - * @param int $course to be processed. + * @param stdClass $course to be processed. * @return step_response - * @throws \coding_exception - * @throws \dml_exception */ public function process_course($processid, $instanceid, $course) { $asynctask = new course_backup_task(); - $asynctask->set_custom_data(['courseid' => $course]); + $asynctask->set_custom_data(['courseid' => $course->id]); \core\task\manager::queue_adhoc_task($asynctask); return step_response::proceed(); } @@ -59,7 +58,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 int $course + * @param stdClass $course * @return step_response * @throws \coding_exception * @throws \dml_exception From 9770def7f35086f24df832bb18900bfb61181f12 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 18 Jul 2025 09:11:23 +0200 Subject: [PATCH 088/170] fix process_error_test --- tests/process_error_test.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/process_error_test.php b/tests/process_error_test.php index 6ef4c0b8..f9052c07 100644 --- a/tests/process_error_test.php +++ b/tests/process_error_test.php @@ -117,8 +117,7 @@ public function test_process_error_in_table(): void { } else if (version_compare(PHP_VERSION, '8.3', '<')) { $this->assertStringContainsString("Attempt to read property \"id\" on bool", $record->errormessage); } else { - $this->assertStringContainsString("Object of class stdClass could not be converted to int", - $record->errormessage); + $this->assertStringContainsString("Attempt to read property \"id\" on false", $record->errormessage); } $this->assertEquals($process->id, $record->id); } From cadda91eca16713a7eea47ddf3cc00f46a8ea148 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 18 Jul 2025 09:52:50 +0200 Subject: [PATCH 089/170] add tests in ci config-file --- .github/workflows/config.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/config.json b/.github/workflows/config.json index 3dd0a5ff..a75c5b69 100644 --- a/.github/workflows/config.json +++ b/.github/workflows/config.json @@ -2,8 +2,11 @@ "main-moodle": "MOODLE_405_STABLE", "main-php": "8.3", "moodle-php": { - "MOODLE_401_STABLE": ["8.0","8.1"], - "MOODLE_405_STABLE": ["8.1","8.2","8.3"] + "MOODLE_401_STABLE": ["8.0", "8.1"], + "MOODLE_402_STABLE": ["8.1", "8.2"], + "MOODLE_403_STABLE": ["8.1", "8.2"], + "MOODLE_404_STABLE": ["8.2", "8.3"], + "MOODLE_405_STABLE": ["8.1", "8.2", "8.3"] }, - "moodle-plugin-ci": "4.4.5" -} + "moodle-plugin-ci": "4.5.5" +} \ No newline at end of file From 8af9705eb93257674fc0b3ff25ec4bacb5bf4087 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 23 Jul 2025 17:19:28 +0200 Subject: [PATCH 090/170] fixed manual->manually error when upgrading --- db/upgrade.php | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/db/upgrade.php b/db/upgrade.php index 95fac7bc..fc90dc4d 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -55,6 +55,22 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { global $DB; $dbman = $DB->get_manager(); + $writesavepoint = false; + + if ($oldversion < 2025050404) { + + // 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'); + } + + // Lifecycle savepoint reached. + $writesavepoint = true; + } if ($oldversion < 2017081101) { @@ -588,18 +604,7 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { } - if ($oldversion < 2025050404) { - - // 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'); - - // Launch rename field key. - if ($dbman->field_exists($table, $field)) { - $dbman->rename_field($table, $field, 'manually'); - } - - // Lifecycle savepoint reached. + if ($writesavepoint) { upgrade_plugin_savepoint(true, 2025050404, 'tool', 'lifecycle'); } From 29a0ef2fc09d0b8c3c721022849256ec54eb3740 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 24 Jul 2025 00:57:17 +0200 Subject: [PATCH 091/170] update changed.md --- CHANGES.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index ffc14304..baff9ed3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,26 @@ CHANGELOG ========= + +4.5.5 (2025-07-24) +------------------ +* [FEATURE] Display trigger countings 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 +* [FIXED] call_trigger: mtrace only when called by cron +* [FIXED] Fix behat test interaction.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 From 82e9a8e892c8e49c79b752e512dcb993201b1e9f Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 24 Jul 2025 07:26:50 +0200 Subject: [PATCH 092/170] fix upgrade.php to rename field manual before the field is used --- db/upgrade.php | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/db/upgrade.php b/db/upgrade.php index fc90dc4d..b527cfb6 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; @@ -55,22 +57,6 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { global $DB; $dbman = $DB->get_manager(); - $writesavepoint = false; - - if ($oldversion < 2025050404) { - - // 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'); - } - - // Lifecycle savepoint reached. - $writesavepoint = true; - } if ($oldversion < 2017081101) { @@ -471,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'); @@ -520,6 +506,15 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { if ($oldversion < 2025050400) { + // 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'); $field = new xmldb_field('instancename', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'id'); @@ -604,7 +599,15 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { } - if ($writesavepoint) { + if ($oldversion < 2025050404) { + // 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'); + } upgrade_plugin_savepoint(true, 2025050404, 'tool', 'lifecycle'); } From 96b36ec5199bfc6e76e046ca77903b39079fa7b4 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 24 Jul 2025 09:41:01 +0200 Subject: [PATCH 093/170] upgrading from older versions: execute statements of last upgrade.php block in any case --- db/upgrade.php | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/db/upgrade.php b/db/upgrade.php index b527cfb6..b35271f2 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -504,7 +504,7 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { } - if ($oldversion < 2025050400) { + if ($oldversion < 2025050404) { // Define field manual to be renamed to manually in table tool_lifecycle_workflow. $table = new xmldb_table('tool_lifecycle_workflow'); @@ -595,20 +595,8 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { workflow_manager::remove($trigger->workflowid); } - upgrade_plugin_savepoint(true, 2025050400, 'tool', 'lifecycle'); - - } - - if ($oldversion < 2025050404) { - // 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'); - } upgrade_plugin_savepoint(true, 2025050404, 'tool', 'lifecycle'); + } return true; From 7fcb60eed7a5970158faf654508b5355af240014 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 31 Jul 2025 03:16:25 +0200 Subject: [PATCH 094/170] no need to unset context when rollback and return if no workflow when delaying a course --- classes/local/manager/delayed_courses_manager.php | 1 + classes/local/manager/process_manager.php | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/local/manager/delayed_courses_manager.php b/classes/local/manager/delayed_courses_manager.php index 98d2e900..74713c5d 100644 --- a/classes/local/manager/delayed_courses_manager.php +++ b/classes/local/manager/delayed_courses_manager.php @@ -56,6 +56,7 @@ public static function set_course_delayed_for_workflow($courseid, $becauserollba if (!$workflow) { mtrace("Set course delayed: no workflow found. Course-ID:{$courseid}, var_dump workfloworid:". var_dump($workfloworid)); + return; } } if ($becauserollback) { diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index 1a29ff21..5c694c9d 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -201,7 +201,6 @@ public static function set_process_waiting(&$process) { */ public static function rollback_process($process) { process_rollback::event_from_process($process)->trigger(); - unset($process->context); for ($i = $process->stepindex; $i >= 1; $i--) { $step = step_manager::get_step_instance_by_workflow_index($process->workflowid, $i); $lib = lib_manager::get_step_lib($step->subpluginname); @@ -224,7 +223,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]); } /** From 32b2da95bd3a579a5e6ae20223354afa8203070a Mon Sep 17 00:00:00 2001 From: Justus Dieckmann <45795270+justusdieckmann@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:22:19 +0200 Subject: [PATCH 095/170] Fix permissions (#247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Justus Dieckmann --- activeprocesses.php | 2 +- activeworkflows.php | 2 +- confirmation.php | 2 +- coursebackups.php | 2 +- createworkflowfromexisting.php | 2 +- deactivatedworkflows.php | 2 +- delayedcourses.php | 2 +- editelement.php | 2 +- editworkflow.php | 2 +- errors.php | 2 +- run.php | 2 +- step/adminapprove/index.php | 2 +- subplugins.php | 2 +- uploadworkflow.php | 2 +- workflowdrafts.php | 2 +- workflowoverview.php | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/activeprocesses.php b/activeprocesses.php index 05eb5b40..16dec68c 100644 --- a/activeprocesses.php +++ b/activeprocesses.php @@ -31,7 +31,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::ACTIVE_PROCESSES)); diff --git a/activeworkflows.php b/activeworkflows.php index 988543bc..1784c74d 100644 --- a/activeworkflows.php +++ b/activeworkflows.php @@ -32,7 +32,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::ACTIVE_WORKFLOWS)); diff --git a/confirmation.php b/confirmation.php index d212d228..67f55d84 100644 --- a/confirmation.php +++ b/confirmation.php @@ -28,7 +28,7 @@ require_once(__DIR__ . '/../../../config.php'); -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::CONFIRMATION)); diff --git a/coursebackups.php b/coursebackups.php index bdd44234..98f8d92b 100644 --- a/coursebackups.php +++ b/coursebackups.php @@ -30,7 +30,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::COURSE_BACKUPS)); diff --git a/createworkflowfromexisting.php b/createworkflowfromexisting.php index d98a8f7a..c87937d0 100644 --- a/createworkflowfromexisting.php +++ b/createworkflowfromexisting.php @@ -32,7 +32,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $workflowid = optional_param('wf', null, PARAM_INT); diff --git a/deactivatedworkflows.php b/deactivatedworkflows.php index c27133c1..71ef6bd0 100644 --- a/deactivatedworkflows.php +++ b/deactivatedworkflows.php @@ -31,7 +31,7 @@ use tool_lifecycle\tabs; use tool_lifecycle\urls; -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::DEACTIVATED_WORKFLOWS)); diff --git a/delayedcourses.php b/delayedcourses.php index 644e9dc1..b071dae0 100644 --- a/delayedcourses.php +++ b/delayedcourses.php @@ -31,7 +31,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $wfid = optional_param('wfid', 0, PARAM_INT); diff --git a/editelement.php b/editelement.php index 807a914a..969705b4 100644 --- a/editelement.php +++ b/editelement.php @@ -38,7 +38,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $type = required_param('type', PARAM_ALPHA); $elementid = optional_param('elementid', null, PARAM_INT); diff --git a/editworkflow.php b/editworkflow.php index 4a030357..af92b758 100644 --- a/editworkflow.php +++ b/editworkflow.php @@ -32,7 +32,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_context($syscontext); diff --git a/errors.php b/errors.php index af43b629..0f331b5c 100644 --- a/errors.php +++ b/errors.php @@ -32,7 +32,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::PROCESS_ERRORS)); diff --git a/run.php b/run.php index 6138a2c1..87b2150f 100644 --- a/run.php +++ b/run.php @@ -30,7 +30,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::RUN)); diff --git a/step/adminapprove/index.php b/step/adminapprove/index.php index 4ad2899f..9211fa8a 100644 --- a/step/adminapprove/index.php +++ b/step/adminapprove/index.php @@ -28,7 +28,7 @@ require_once(__DIR__ . '/../../../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $PAGE->set_context(context_system::instance()); $PAGE->set_url(new \moodle_url(urls::SUBPLUGINS)); diff --git a/subplugins.php b/subplugins.php index 082beebe..30f293aa 100644 --- a/subplugins.php +++ b/subplugins.php @@ -29,7 +29,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::SUBPLUGINS)); diff --git a/uploadworkflow.php b/uploadworkflow.php index eb0bc6f8..c4b91f04 100644 --- a/uploadworkflow.php +++ b/uploadworkflow.php @@ -32,7 +32,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::UPLOAD_WORKFLOW)); diff --git a/workflowdrafts.php b/workflowdrafts.php index 9657d4e6..e35cd79a 100644 --- a/workflowdrafts.php +++ b/workflowdrafts.php @@ -30,7 +30,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); $syscontext = context_system::instance(); $PAGE->set_url(new \moodle_url(urls::WORKFLOW_DRAFTS)); diff --git a/workflowoverview.php b/workflowoverview.php index 1b758821..719feae4 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -26,7 +26,7 @@ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_login(); +require_admin(); define('PAGESIZE', 20); From 8a2fc1e86aebe54abe52cee97d92f43e1165ceb8 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 4 Aug 2025 18:17:23 +0200 Subject: [PATCH 096/170] Introduce trigger lib function check_course_code, new ci file (#2) * no need to unset context when rollback process, no action if workflow is missing while delaying a course * small phpdoc changes * introduce trigger lib function check_course_code, new ci file * codechecker issues * phpdoc issues * fix unit tests * increase ci version * ci: remove not supported Moodle 4.2 --- .github/workflows/config.json | 22 +++++++++++++------ classes/processor.php | 10 +++------ step/email/lib.php | 2 +- step/email/tests/lib_test.php | 6 +---- trigger/byrole/lib.php | 4 ++-- trigger/categories/lib.php | 5 +++-- trigger/customfielddelay/lib.php | 4 ++-- trigger/delayedcourses/lib.php | 4 ++-- trigger/lastaccess/lib.php | 4 ++-- trigger/lib.php | 12 ++++++++-- trigger/semindependent/lib.php | 4 ++-- trigger/semindependent/tests/trigger_test.php | 4 ++++ trigger/sitecourse/lib.php | 4 ++-- trigger/specificdate/lib.php | 4 ++-- trigger/startdatedelay/lib.php | 4 ++-- 15 files changed, 53 insertions(+), 40 deletions(-) diff --git a/.github/workflows/config.json b/.github/workflows/config.json index a75c5b69..653703ef 100644 --- a/.github/workflows/config.json +++ b/.github/workflows/config.json @@ -1,12 +1,20 @@ { "main-moodle": "MOODLE_405_STABLE", "main-php": "8.3", - "moodle-php": { - "MOODLE_401_STABLE": ["8.0", "8.1"], - "MOODLE_402_STABLE": ["8.1", "8.2"], - "MOODLE_403_STABLE": ["8.1", "8.2"], - "MOODLE_404_STABLE": ["8.2", "8.3"], - "MOODLE_405_STABLE": ["8.1", "8.2", "8.3"] + "main-db": "pgsql", + "moodle-testmatrix": { + "MOODLE_401_STABLE": { + "php": ["8.0", "8.1"] + }, + "MOODLE_403_STABLE": { + "php": ["8.1", "8.2"] + }, + "MOODLE_404_STABLE": { + "php": ["8.2", "8.3"] + }, + "MOODLE_405_STABLE": { + "php": ["8.1", "8.2", "8.3"] + } }, - "moodle-plugin-ci": "4.5.5" + "moodle-plugin-ci": "4.5.6" } \ No newline at end of file diff --git a/classes/processor.php b/classes/processor.php index 3a40eeb2..ce9d4b45 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -298,18 +298,14 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) * @param trigger_subplugin $trigger trigger, which will be asked for additional where requirements. * @param int[] $excluded List of course id, which should be excluded from counting. * @param int[] $delayed List of course ids of delayed courses (globally and for workflow). - * @return int[] $triggered, $new, $delayed Triggered courses and amount which courses of them would - * be added and which courses are delayed. + * @return int[] $triggered, $new, $delayed Triggered courses, amount which courses of them would + * be added, which courses are delayed. * @throws \coding_exception * @throws \dml_exception */ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { global $DB; - $triggercourses = 0; - $delayedcourses = 0; - $newcourses = 0; - $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); // Get SQL for this trigger. [$sql, $whereparams] = $lib->get_course_recordset_where($trigger->id); @@ -317,7 +313,7 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { // We just want the triggered courses here, no matter of including or excluding. $where = str_replace(" NOT ", " ", $sql); - // Now get the amount of courses triggered by this trigger. + // Now get all the courses triggered by this trigger. $sql = 'SELECT {course}.id from {course} WHERE '. $where; $triggercoursesall = $DB->get_fieldset_sql($sql, $whereparams); diff --git a/step/email/lib.php b/step/email/lib.php index 7123d707..d412f56b 100644 --- a/step/email/lib.php +++ b/step/email/lib.php @@ -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/trigger/byrole/lib.php b/trigger/byrole/lib.php index 8d893e2a..12b97f2b 100644 --- a/trigger/byrole/lib.php +++ b/trigger/byrole/lib.php @@ -56,8 +56,8 @@ public function get_course_recordset_where($triggerid) { /** * Returns triggertype of trigger: trigger, triggertime or exclude. - * @param \stdClass $course DEPRECATED - * @param int $triggerid DEPRECATED + * @param \stdClass $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { diff --git a/trigger/categories/lib.php b/trigger/categories/lib.php index 489e558f..4f452ee3 100644 --- a/trigger/categories/lib.php +++ b/trigger/categories/lib.php @@ -43,8 +43,8 @@ class categories 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) { @@ -137,6 +137,7 @@ public function extend_add_instance_form_definition($mform) { * 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 = []; diff --git a/trigger/customfielddelay/lib.php b/trigger/customfielddelay/lib.php index f2b57797..8a0f9744 100644 --- a/trigger/customfielddelay/lib.php +++ b/trigger/customfielddelay/lib.php @@ -35,8 +35,8 @@ 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. + * @param object $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { diff --git a/trigger/delayedcourses/lib.php b/trigger/delayedcourses/lib.php index d4e8cd55..f2b41ea7 100644 --- a/trigger/delayedcourses/lib.php +++ b/trigger/delayedcourses/lib.php @@ -40,8 +40,8 @@ class delayedcourses 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) { diff --git a/trigger/lastaccess/lib.php b/trigger/lastaccess/lib.php index 0dfe80e9..0f42cf01 100644 --- a/trigger/lastaccess/lib.php +++ b/trigger/lastaccess/lib.php @@ -41,8 +41,8 @@ class lastaccess extends base_automatic { /** * Checks the course and returns a response, which tells if the course should be further processed. * - * @param \stdClass $course DEPRECATED - * @param int $triggerid DEPRECATED + * @param \stdClass $course + * @param int $triggerid * @return trigger_response */ public function check_course($course, $triggerid) { diff --git a/trigger/lib.php b/trigger/lib.php index d9a63cff..52b3fe04 100644 --- a/trigger/lib.php +++ b/trigger/lib.php @@ -139,12 +139,20 @@ abstract class base_automatic extends base { /** * Returns triggertype of trigger: trigger, triggertime or exclude. - * @param int $course DEPRECATED - * @param int $triggerid DEPRECATED + * @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; + } + /** * Defines if the trigger subplugin is started manually or automatically. * @return bool diff --git a/trigger/semindependent/lib.php b/trigger/semindependent/lib.php index 9949b3b9..2ce45bf4 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) { diff --git a/trigger/semindependent/tests/trigger_test.php b/trigger/semindependent/tests/trigger_test.php index 5087defa..7a679e93 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 diff --git a/trigger/sitecourse/lib.php b/trigger/sitecourse/lib.php index dfaba56c..f66d7011 100644 --- a/trigger/sitecourse/lib.php +++ b/trigger/sitecourse/lib.php @@ -38,8 +38,8 @@ class sitecourse 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) { diff --git a/trigger/specificdate/lib.php b/trigger/specificdate/lib.php index 12497fad..f061d0cd 100644 --- a/trigger/specificdate/lib.php +++ b/trigger/specificdate/lib.php @@ -43,8 +43,8 @@ class specificdate 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) { diff --git a/trigger/startdatedelay/lib.php b/trigger/startdatedelay/lib.php index 286271e9..bb322a6c 100644 --- a/trigger/startdatedelay/lib.php +++ b/trigger/startdatedelay/lib.php @@ -41,8 +41,8 @@ class startdatedelay 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) { From 867d0f04d1e59fcce918d5faf7a8b7f6bcf6e656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20B=C3=B6sch?= Date: Mon, 4 Aug 2025 20:11:05 +0200 Subject: [PATCH 097/170] Subplugins both ways of describing following MDL-83705. (#250) --- db/subplugins.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/db/subplugins.json b/db/subplugins.json index 79de0157..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" } -} \ No newline at end of file +} From 03e856ae032ad61d1c4b16b26e2dcc45f861e2a3 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 5 Aug 2025 09:51:15 +0200 Subject: [PATCH 098/170] reimplement trigger lib function course_check --- classes/processor.php | 74 ++++++++++++++++++++++++++++++++++++++++--- workflowoverview.php | 2 ++ 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index ce9d4b45..47840a7b 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -317,7 +317,7 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { $sql = 'SELECT {course}.id from {course} WHERE '. $where; $triggercoursesall = $DB->get_fieldset_sql($sql, $whereparams); - // Get delayed courses which would be triggered by this trigger. + // Get amount of delayed courses which would be triggered by this trigger. $delayedcourses = count(array_intersect($triggercoursesall, $delayed)); // Exclude delayed courses and sitecourse according to the workflow settings. @@ -342,6 +342,52 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { return [$triggercourses, $newcourses, $delayedcourses]; } + + /** + * Also returns the amount of courses for a trigger for counting. BUT the trigger lib function check_courses is used + * to select a course for triggering/excluding. + * 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[] $excluded List of course id, which should be excluded from counting. + * @param int[] $delayed List of course ids of delayed courses (globally and for workflow). + * @return int[] $triggered, $new, $delayed Triggered courses, amount which courses of them would + * be added, which courses are delayed. + * @throws \coding_exception + * @throws \dml_exception + */ + public function get_triggercourses_forcounting_check_course($trigger, $excluded, $delayed) { + global $DB; + + $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); + + $triggercoursesall = []; + $recordset = $this->get_course_recordset([$trigger], [], true); + while ($recordset->valid()) { + $course = $recordset->current(); + $response = $lib->check_course($course, $trigger->id); + if ($response !== trigger_response::next()) { + $triggercoursesall[] = $course->id; + } + $recordset->next(); + } + + // Get delayed courses which would be triggered by this trigger. + $delayedcourses = array_intersect($triggercoursesall, $delayed); + + $triggercourses = array_diff($triggercoursesall, $excluded); + + // 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)]; + } + /** * 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. @@ -384,7 +430,22 @@ public function get_triggercourses($trigger, $workflow) { } // 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); + $triggercourses = $DB->get_fieldset_sql($sql, $whereparams); + // If trigger lib has check_course code go and check every course individually. + if ($lib->check_course_code()) { + $triggercourseschecked = []; + $recordset = $this->get_course_recordset([$trigger], [], true); + while ($recordset->valid()) { + $course = $recordset->current(); + $response = $lib->check_course($course, $trigger->id); + if ($response !== trigger_response::next()) { + $triggercourseschecked[] = $course->id; + } + $recordset->next(); + } + $triggercourses = array_intersect($triggercourseschecked, $triggercourses); + } + return $triggercourses; } /** @@ -434,8 +495,13 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { // 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. - [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting($trigger, $excludedcourses, - $delayedcourses); + if ($lib->check_course_code()) { + [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting_check_course( + $trigger, $excludedcourses, $delayedcourses); + } else { + [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting( + $trigger, $excludedcourses, $delayedcourses); + } if ($obj->response == trigger_response::exclude()) { $obj->excluded = $newcourses; $obj->delayed = $delayed; diff --git a/workflowoverview.php b/workflowoverview.php index 719feae4..1c86f747 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -258,6 +258,7 @@ $response = null; $lib = lib_manager::get_trigger_lib($trigger->subpluginname); if ($trigger->automatic = !$lib->is_manual_trigger()) { + // We need the response type only. $response = $lib->check_course(null, null); } $nomanualtriggerinvolved &= $trigger->automatic; @@ -559,6 +560,7 @@ // 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()) { + // Function check_course: We need the response type only. if ($lib->check_course(null, null) == trigger_response::triggertime()) { continue; } From e5d066e7508b5d6c61bc0a0ee6695e9d4c7ed0bf Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 7 Aug 2025 03:52:07 +0200 Subject: [PATCH 099/170] refactor last access trigger: no single course ids in sql --- trigger/lastaccess/lib.php | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/trigger/lastaccess/lib.php b/trigger/lastaccess/lib.php index 0f42cf01..ec5c6c3e 100644 --- a/trigger/lastaccess/lib.php +++ b/trigger/lastaccess/lib.php @@ -72,35 +72,22 @@ public function instance_settings() { * @throws \dml_exception */ public function get_course_recordset_where($triggerid) { - global $DB; - $sql = "SELECT la.courseid - FROM mdl_user_enrolments AS ue - JOIN mdl_enrol AS e ON (ue.enrolid = e.id) - JOIN mdl_user_lastaccess AS la ON (ue.userid = la.userid) - WHERE e.courseid = la.courseid - GROUP BY la.courseid - HAVING MAX(la.timeaccess) < :lastaccessthreshold"; + $where = '{course}.id IN + (SELECT la.courseid + FROM mdl_user_enrolments AS ue + JOIN mdl_enrol AS e ON (ue.enrolid = e.id) + JOIN mdl_user_lastaccess AS 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]; - try { - $records = $DB->get_records_sql($sql, ["lastaccessthreshold" => $now - $delay]); - } catch (\dml_exception $e) { - $records = []; - } - - $courseids = array_column($records, 'courseid'); - - if ($courseids) { - [$insql, $inparams] = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); - $where = "{course}.id {$insql}"; - } else { - $inparams = []; - $where = "true"; - } - return [$where, $inparams]; + return [$where, $params]; } /** From 695db87a95a1e5606dd7b7e6eff5383140e02d02 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 7 Aug 2025 14:24:24 +0200 Subject: [PATCH 100/170] process_courses: no need to catch workflow --- classes/processor.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 47840a7b..3bac0452 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -140,7 +140,6 @@ public function call_trigger() { */ public function process_courses() { foreach (process_manager::get_processes() as $process) { - $workflow = workflow_manager::get_workflow($process->workflowid); while (true) { try { @@ -153,7 +152,8 @@ public function process_courses() { 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; } } @@ -175,11 +175,13 @@ public function process_courses() { 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); + delayed_courses_manager::set_course_delayed_for_workflow($course->id, + false, $process->workflowid); 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); break; } else { From 126a95092de026b78d313965b201ca610e9bc58f Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 8 Aug 2025 06:10:03 +0200 Subject: [PATCH 101/170] processor.php: change countings to take check course code into account --- classes/processor.php | 59 ++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 3bac0452..e71ddb78 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -477,12 +477,14 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $amounts = []; $autotriggers = []; $nextrun = 0; + $checkcoursecode = false; foreach ($triggers as $trigger) { $trigger = (object)(array) $trigger; // Cast to normal 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(); $lib = lib_manager::get_trigger_lib($trigger->subpluginname); + $checkcoursecode = $lib->check_course_code(); if ($lib->is_manual_trigger()) { $obj->automatic = false; } else { @@ -491,6 +493,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $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); + // We only need the trigger response here. $obj->response = $lib->check_course(null, null); if ($obj->sql != "false") { // Get courses amount. @@ -535,27 +538,53 @@ 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); + $recordset = $this->get_course_recordset($autotriggers, $sitecourse, true); $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; + 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) { + if ($course->workflowid && ($course->workflowid != $workflow->id)) { + $usedcourses[] = $course->id; + } + } else if ($course->delay && $course->delay > time()) { + $delayedcourses[] = $course->id; + } else { + $coursestriggered[] = $course->id; + } } - } else if ($course->delay && $course->delay > time()) { - $delayedcourses[] = $course->id; } else { - $coursestriggered[] = $course->id; + if ($course->hasprocess) { + if ($course->workflowid && ($course->workflowid != $workflow->id)) { + $usedcourses[] = $course->id; + } + } else if ($course->delay && $course->delay > time()) { + $delayedcourses[] = $course->id; + } else { + $coursestriggered[] = $course->id; + } } $recordset->next(); } From 23bbb2e9f7df4c85519ef22f6fca5021c2167590 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 13 Aug 2025 05:36:13 +0200 Subject: [PATCH 102/170] avoid parameter restriction errors on large moodle sites #249 --- classes/local/manager/trigger_manager.php | 2 +- .../local/table/triggered_courses_table.php | 208 ----------- .../table/triggered_courses_table_trigger.php | 151 ++++++++ .../triggered_courses_table_workflow.php | 338 ++++++++++++++++++ classes/processor.php | 166 ++++----- lang/de/tool_lifecycle.php | 10 +- lang/en/tool_lifecycle.php | 10 +- trigger/byrole/lib.php | 2 +- trigger/categories/lib.php | 2 +- trigger/customfielddelay/lib.php | 2 +- trigger/semindependent/lib.php | 4 +- workflowoverview.php | 106 +++--- 12 files changed, 629 insertions(+), 372 deletions(-) delete mode 100644 classes/local/table/triggered_courses_table.php create mode 100644 classes/local/table/triggered_courses_table_trigger.php create mode 100644 classes/local/table/triggered_courses_table_workflow.php diff --git a/classes/local/manager/trigger_manager.php b/classes/local/manager/trigger_manager.php index bb004f71..491ff514 100644 --- a/classes/local/manager/trigger_manager.php +++ b/classes/local/manager/trigger_manager.php @@ -361,7 +361,7 @@ public static function duplicate_triggers($oldworkflowid, $newworkflowid) { 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; } } diff --git a/classes/local/table/triggered_courses_table.php b/classes/local/table/triggered_courses_table.php deleted file mode 100644 index adff596e..00000000 --- a/classes/local/table/triggered_courses_table.php +++ /dev/null @@ -1,208 +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); - } - - /** - * 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..51b6588f --- /dev/null +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -0,0 +1,151 @@ +. + +/** + * 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\entity\trigger_subplugin; +use tool_lifecycle\local\manager\delayed_courses_manager; +use tool_lifecycle\local\manager\lib_manager; +use tool_lifecycle\local\manager\workflow_manager; +use tool_lifecycle\processor; + +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 { + + /** + * 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 -name + * @throws \coding_exception + * @throws \dml_exception + */ + public function __construct($trigger, $type, $filterdata = '') { + parent::__construct('tool_lifecycle-courses-in-trigger'); + global $PAGE; + + $workflow = workflow_manager::get_workflow($trigger->workflowid); + + $processor = new processor(); + $lib = lib_manager::get_trigger_lib($trigger->subpluginname); + + // 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); + if ($lib->check_course_code()) { + [$triggercourses, , ] = $processor->get_triggercourses_forcounting_check_course( + $trigger, $excludedcourses, $delayedcourses); + } else { + [$triggercourses, , ] = $processor->get_triggercourses_forcounting( + $trigger, $excludedcourses, $delayedcourses); + } + if (!$triggercourses) { + return; + } + + $this->define_baseurl($PAGE->url); + $a = new \stdClass(); + $a->title = $trigger->instancename; + $a->courses = $triggercourses; + 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']; + $this->define_columns($columns); + $headers = [ + get_string('courseid', 'tool_lifecycle'), + get_string('coursename', 'tool_lifecycle'), + get_string('coursecategory', 'moodle'), + ]; + $this->define_headers($headers); + + [$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"; + $from = "{course} c LEFT JOIN {course_categories} cc ON c.category = cc.id "; + + if (!$workflow->includesitecourse) { + $where .= " AND c.id <> 1 "; + } + + if (!$workflow->includedelayedcourses && $excludedcourses) { + $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) "; + $whereparams = array_merge($whereparams, + ['time1' => time(),'time2' => time(), 'workflowid' => $workflow->id]); + } + + if ($filterdata) { + if (is_numeric($filterdata)) { + $where .= " AND {course}.id = $filterdata "; + } else { + $where .= " AND ( {course}.fullname LIKE '%$filterdata%' OR {course}.shortname LIKE '%$filterdata%')"; + } + } + + $this->set_sql($fields, $from, $where, $whereparams); + } + + /** + * 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 . ''; + } + + /** + * 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_workflow.php b/classes/local/table/triggered_courses_table_workflow.php new file mode 100644 index 00000000..9dea7428 --- /dev/null +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -0,0 +1,338 @@ +. + +/** + * 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 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 or used */ + 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; + + /** + * 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 + * @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; + + $this->define_baseurl($PAGE->url); + $this->type = $type; + $this->workflowid = $workflow->id; + + $a = new \stdClass(); + $a->title = $workflow->title; + $a->courses = $courses; + 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); + } + $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, + 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, + 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 "; + if ($type == 'used') { + $fields .= ", COALESCE(wfp.title, wfpe.title) as otherworkflow"; + } + $from = " {course} c LEFT JOIN {course_categories} cc ON c.category = cc.id + 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_delayed} d ON c.id = d.courseid + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid "; + if ($type == 'used') { + $from .= " LEFT JOIN {tool_lifecycle_workflow} wfp ON p.workflowid = wfp.id + LEFT JOIN {tool_lifecycle_workflow} wfpe ON pe.workflowid = wfpe.id"; + } + + $where = 'true'; + if (!$workflow->includesitecourse) { + $where .= " AND c.id <> 1 "; + } + $inparams = []; + $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); + 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 = str_replace("{course}", "c", $sql); + if (!empty($sql)) { + $where .= ' AND ' . $sql; + $inparams = array_merge($inparams, $params); + } + } + } + + if ($filterdata) { + if (is_numeric($filterdata)) { + $where .= " AND c.id = $filterdata "; + } else { + $where .= " AND ( c.fullname LIKE '%$filterdata%' OR c.shortname LIKE '%$filterdata%')"; + } + } + + $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 returns NULL then put the data straight into the + * table. + * + * After calling this function, don't forget to call close_recordset. + */ + public function build_table() { + if (!$this->rawdata) { + return; + } + + if ($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; + } + } + } + foreach ($this->rawdata as $row) { + if ($this->checkcoursecode) { + $action = false; + foreach ($autotriggers as $trigger) { + $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); + $response = $lib->check_course($row->id, $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 ($row->hasprocess) { + if ($this->workflowid && ($row->workflowid != $this->workflowid)) { + if ($this->type == 'used') { + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + } + } + } else if ($row->delay && $row->delay > time()) { + if ($this->type == 'delayed') { + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + } + } else { + if ($this->type == 'triggeredworkflow') { + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + } + } + } + } else { + if ($row->hasprocess) { + if ($row->workflowid && ($row->workflowid != $this->workflowid)) { + if ($this->type == 'used') { + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + } + } + } else if ($row->delay && $row->delay > time()) { + if ($this->type == 'delayed') { + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + } + } else { + if ($this->type == 'triggeredworkflow') { + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + } + } + } + } + } + + /** + * 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/processor.php b/classes/processor.php index e71ddb78..616d55f6 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -92,7 +92,7 @@ public function call_trigger() { } if (!$workflow->includedelayedcourses) { $exclude = array_merge(delayed_courses_manager::get_delayed_courses_for_workflow($workflow->id), - $exclude); + delayed_courses_manager::get_globally_delayed_courses(), $exclude); } $recordset = $this->get_course_recordset($triggers, $exclude); while ($recordset->valid()) { @@ -249,24 +249,29 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) $where = 'true'; $whereparams = []; + $workflowid = false; foreach ($triggers as $trigger) { $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); [$sql, $params] = $lib->get_course_recordset_where($trigger->id); + $sql = str_replace("{course}", "c", $sql); if (!empty($sql)) { $where .= ' AND ' . $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 (!$workflowid) { + $workflowid = $trigger->workflowid; + } } if ($forcounting) { + if ($exclude) { + $workflow = workflow_manager::get_workflow($workflowid); + if (!$workflow->includesitecourse) { + $where .= " AND c.id <> 1 "; + } + } // Get course hasprocess and delay with the sql. - $sql = "SELECT {course}.id, + $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 @@ -278,17 +283,30 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) 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 + 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 + LEFT JOIN {tool_lifecycle_delayed} d ON c.id = d.courseid + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid WHERE " . $where; } else { + if ($exclude) { + $workflow = workflow_manager::get_workflow($workflowid); + 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' => $workflowid]; + $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} p ON {course}.id = p.courseid - LEFT JOIN {tool_lifecycle_proc_error} pe ON {course}.id = pe.courseid + $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; } return $DB->get_recordset_sql($sql, $whereparams); @@ -310,13 +328,14 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { $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); + $where = str_replace("<>", "=", str_replace(" NOT ", " ", $where)); // Now get all the courses triggered by this trigger. - $sql = 'SELECT {course}.id from {course} WHERE '. $where; + $sql = 'SELECT c.id from {course} c WHERE '. $where; $triggercoursesall = $DB->get_fieldset_sql($sql, $whereparams); // Get amount of delayed courses which would be triggered by this trigger. @@ -324,15 +343,23 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { // Exclude delayed courses and sitecourse according to the workflow settings. if (!empty($excluded)) { - [$insql, $inparams] = $DB->get_in_or_equal($excluded, SQL_PARAMS_NAMED); - $where .= " AND NOT {course}.id {$insql}"; + $workflow = workflow_manager::get_workflow($trigger->workflowid); + 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); } - $sql = 'SELECT count({course}.id) from {course} WHERE '. $where; + $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 {course}.id NOT IN ( + $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 @@ -363,7 +390,7 @@ public function get_triggercourses_forcounting_check_course($trigger, $excluded, $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); $triggercoursesall = []; - $recordset = $this->get_course_recordset([$trigger], [], true); + $recordset = $this->get_course_recordset([$trigger], $excluded, true); while ($recordset->valid()) { $course = $recordset->current(); $response = $lib->check_course($course, $trigger->id); @@ -390,66 +417,6 @@ public function get_triggercourses_forcounting_check_course($trigger, $excluded, return [count($triggercourses), count($newcourses), count($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 array with relevant courses. - * @throws \coding_exception - * @throws \dml_exception - */ - public function get_triggercourses($trigger, $workflow) { - global $DB; - - // 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()); - } - // 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); - - $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); - } - // Now get the list of course IDs triggered by this trigger. - $sql = "SELECT {course}.id FROM {course} WHERE " . $where; - $triggercourses = $DB->get_fieldset_sql($sql, $whereparams); - // If trigger lib has check_course code go and check every course individually. - if ($lib->check_course_code()) { - $triggercourseschecked = []; - $recordset = $this->get_course_recordset([$trigger], [], true); - while ($recordset->valid()) { - $course = $recordset->current(); - $response = $lib->check_course($course, $trigger->id); - if ($response !== trigger_response::next()) { - $triggercourseschecked[] = $course->id; - } - $recordset->next(); - } - $triggercourses = array_intersect($triggercourseschecked, $triggercourses); - } - return $triggercourses; - } - /** * Calculates triggered and excluded courses for every trigger of a workflow, and in total. * @param object $workflow @@ -458,10 +425,6 @@ public function get_triggercourses($trigger, $workflow) { * @throws \dml_exception */ public function get_count_of_courses_to_trigger_for_workflow($workflow) { - global $DB; - - $coursestriggered = []; - $usedcourses = []; // Exclude delayed courses and sitecourse according to the workflow settings. $sitecourse = $workflow->includesitecourse ? [] : [1]; @@ -477,6 +440,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $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. @@ -484,7 +448,9 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $trigger->exclude = $settings['exclude'] ?? false; $obj = new \stdClass(); $lib = lib_manager::get_trigger_lib($trigger->subpluginname); - $checkcoursecode = $lib->check_course_code(); + if (!$checkcoursecode) { + $checkcoursecode = $lib->check_course_code(); + } if ($lib->is_manual_trigger()) { $obj->automatic = false; } else { @@ -496,7 +462,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { // We only need the trigger response here. $obj->response = $lib->check_course(null, null); if ($obj->sql != "false") { - // Get courses amount. + // 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. @@ -538,9 +504,11 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $amounts[$trigger->sortindex] = $obj; } - $recordset = $this->get_course_recordset($autotriggers, $sitecourse, true); + $recordset = $this->get_course_recordset($autotriggers, $excludedcourses, true); - $delayedcourses = []; // Only delayed courses of selected courses are of interest here. + $coursestriggered = 0; + $usedcourses = 0; + $coursesdelayed = 0; // Only delayed courses of selected courses are of interest here. while ($recordset->valid()) { $course = $recordset->current(); if ($checkcoursecode) { @@ -567,23 +535,23 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { if (!$action) { if ($course->hasprocess) { if ($course->workflowid && ($course->workflowid != $workflow->id)) { - $usedcourses[] = $course->id; + $usedcourses++; } } else if ($course->delay && $course->delay > time()) { - $delayedcourses[] = $course->id; + $coursesdelayed++; } else { - $coursestriggered[] = $course->id; + $coursestriggered++; } } } else { if ($course->hasprocess) { if ($course->workflowid && ($course->workflowid != $workflow->id)) { - $usedcourses[] = $course->id; + $usedcourses++; } } else if ($course->delay && $course->delay > time()) { - $delayedcourses[] = $course->id; + $coursesdelayed++; } else { - $coursestriggered[] = $course->id; + $coursestriggered++; } } $recordset->next(); @@ -591,7 +559,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $all = new \stdClass(); $all->coursestriggered = $coursestriggered; - $all->delayedcourses = $delayedcourses; // Delayed courses for workflow and globally. Excluded per default. + $all->delayedcourses = $coursesdelayed; // Delayed courses for workflow and globally. Excluded per default. $all->used = $usedcourses; $all->nextrun = $nextrun; $amounts['all'] = $all; diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 7f447b07..84f8d031 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -92,15 +92,15 @@ $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}\' ({$a->courses})'; $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}\' ({$a->courses})'; $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'] = 'Getriggerte Kurse für Trigger \'{$a->title}\' ({$a->courses})'; +$string['coursestriggeredworkflow'] = 'Getriggerte Kurse für diesen Workflow \'{$a->title}\' ({$a->courses})'; +$string['coursesused'] = 'Kurse schon in einem anderen Prozess für Workflow \'{$a->title}\' ({$a->courses})'; $string['create_copy'] = 'Kopie erstellen'; $string['create_step'] = 'Step erstellen'; $string['create_trigger'] = 'Trigger erstellen'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index baf44074..d2d38cbe 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -93,15 +93,15 @@ $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}\' ({$a->courses})'; $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}\' ({$a->courses})'; $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 triggered by trigger \'{$a->title}\' ({$a->courses})'; +$string['coursestriggeredworkflow'] = 'Courses triggered for this workflow \'{$a->title}\' ({$a->courses})'; +$string['coursesused'] = 'Courses already used in another process for workflow \'{$a->title}\' ({$a->courses})'; $string['create_copy'] = 'Create copy'; $string['create_step'] = 'Create step'; $string['create_trigger'] = 'Create trigger'; diff --git a/trigger/byrole/lib.php b/trigger/byrole/lib.php index 12b97f2b..7f62ca37 100644 --- a/trigger/byrole/lib.php +++ b/trigger/byrole/lib.php @@ -49,7 +49,7 @@ 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, []]; } diff --git a/trigger/categories/lib.php b/trigger/categories/lib.php index 4f452ee3..3a49a239 100644 --- a/trigger/categories/lib.php +++ b/trigger/categories/lib.php @@ -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]; } diff --git a/trigger/customfielddelay/lib.php b/trigger/customfielddelay/lib.php index 8a0f9744..38053bcc 100644 --- a/trigger/customfielddelay/lib.php +++ b/trigger/customfielddelay/lib.php @@ -60,7 +60,7 @@ public function get_course_recordset_where($triggerid) { 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]; diff --git a/trigger/semindependent/lib.php b/trigger/semindependent/lib.php index 2ce45bf4..33d6fb91 100644 --- a/trigger/semindependent/lib.php +++ b/trigger/semindependent/lib.php @@ -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 = "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/workflowoverview.php b/workflowoverview.php index 1c86f747..f9a6aa92 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -45,7 +45,8 @@ use tool_lifecycle\local\response\trigger_response; use tool_lifecycle\local\table\courses_in_step_table; use tool_lifecycle\local\table\process_courses_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; @@ -369,68 +370,72 @@ $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(); $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, 'exclude', $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); ob_end_clean(); $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(); + $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(); + $hiddenfieldssearch[] = ['name' => 'delayed', 'value' => $delayed]; + $tablecoursesamount = $coursesdelayed; + } +} else if ($used) { // Display courses triggered by this workflow but involved in other processes already. + if ($amounts['all']->used ?? null) { + $table = new triggered_courses_table_workflow($amounts['all']->used, $workflow, 'used', $search); + ob_start(); + $table->out(PAGESIZE, false); + $out = ob_get_contents(); + ob_end_clean(); + $hiddenfieldssearch[] = ['name' => 'used', 'value' => $used]; + $tablecoursesamount = $amounts['all']->used; + } } else if ($processes) { // Display courses table with courses in a process or in state process error for this workflow. $coursesinprocess = $DB->get_fieldset('tool_lifecycle_process', 'courseid', ['workflowid' => $workflow->id]); $coursesprocesserrors = $DB->get_fieldset('tool_lifecycle_proc_error', 'courseid', ['workflowid' => $workflow->id]); - $coursesprocess = array_merge($coursesinprocess, $coursesprocesserrors); - $table = new process_courses_table($coursesprocess, $workflow->title, $workflow->id, $search); - ob_start(); - $table->out(PAGESIZE, false); - $out = ob_get_contents(); - ob_end_clean(); - $hiddenfieldssearch[] = ['name' => 'processes', 'value' => $processes]; - $tablecoursesamount = count($coursesprocess); -} 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 ($coursesprocess = array_merge($coursesinprocess, $coursesprocesserrors)) { + $table = new process_courses_table_workflow(count($coursesprocess), $workflow, 'processes', $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); ob_end_clean(); - $hiddenfieldssearch[] = ['name' => 'used', 'value' => $used]; - $tablecoursesamount = count($courseids); + $hiddenfieldssearch[] = ['name' => 'processes', 'value' => $processes]; + $tablecoursesamount = count($coursesprocess); } } + // Search box for courses list. $searchhtml = ''; if ($tablecoursesamount > PAGESIZE ) { @@ -474,13 +479,16 @@ ); $abortdisableworkflowlink = "
".$abortdisableworkflowlink; // Workflow processes and process errors link. - $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 ($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)) { @@ -522,18 +530,18 @@ if ($showdetails) { // The triggers total box. $data['displaytotaltriggered'] = $displaytotaltriggered; - $triggered = count($amounts['all']->coursestriggered) ?? 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; // 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. + $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 = count($amounts['all']->used) ?? 0; + $used = $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; From 420640d624fe80e9a600283670c5282ac940f2f7 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 13 Aug 2025 09:43:09 +0200 Subject: [PATCH 103/170] introduce trigger api function multipleuse to allow multiple use per workflow --- classes/local/manager/trigger_manager.php | 28 ++++++++++++++++++----- editelement.php | 4 +++- trigger/categories/lib.php | 8 +++++++ trigger/lib.php | 9 +++++++- workflowoverview.php | 10 +++++++- 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/classes/local/manager/trigger_manager.php b/classes/local/manager/trigger_manager.php index 491ff514..19c57e47 100644 --- a/classes/local/manager/trigger_manager.php +++ b/classes/local/manager/trigger_manager.php @@ -341,27 +341,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, ] = $lib->get_course_recordset_where($trigger->id); return $sql; } + + /** + * Returns if 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/editelement.php b/editelement.php index 969705b4..d9e89584 100644 --- a/editelement.php +++ b/editelement.php @@ -134,7 +134,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/trigger/categories/lib.php b/trigger/categories/lib.php index 3a49a239..eac88e4a 100644 --- a/trigger/categories/lib.php +++ b/trigger/categories/lib.php @@ -155,4 +155,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/lib.php b/trigger/lib.php index 52b3fe04..d4966c16 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. @@ -161,6 +160,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/workflowoverview.php b/workflowoverview.php index f9a6aa92..ac160180 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -561,7 +561,15 @@ if ($triggers) { foreach ($triggers as $workflowtrigger) { if ($triggertype == $workflowtrigger->subpluginname) { - continue 2; + $lib = lib_manager::get_trigger_lib($triggertype); + if (!$lib->is_manual_trigger()) { + $lib = lib_manager::get_automatic_trigger_lib($triggertype); + if (!$lib->multiple_use()) { + continue 2; + } + } else { + continue 2; + } } } } else { From 3874c604b4b44b1bf6f52ebde55a9caede7101d2 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 13 Aug 2025 11:07:54 +0200 Subject: [PATCH 104/170] use function multipleuse in workflowoverview --- classes/local/manager/trigger_manager.php | 2 +- workflowoverview.php | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/classes/local/manager/trigger_manager.php b/classes/local/manager/trigger_manager.php index 19c57e47..0d847ffb 100644 --- a/classes/local/manager/trigger_manager.php +++ b/classes/local/manager/trigger_manager.php @@ -362,7 +362,7 @@ public static function get_trigger_sqlresult($trigger) { } /** - * Returns if this triggertype is allowed to create more than one instance for a workflow. + * 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 diff --git a/workflowoverview.php b/workflowoverview.php index ac160180..d1e954e0 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -562,12 +562,7 @@ foreach ($triggers as $workflowtrigger) { if ($triggertype == $workflowtrigger->subpluginname) { $lib = lib_manager::get_trigger_lib($triggertype); - if (!$lib->is_manual_trigger()) { - $lib = lib_manager::get_automatic_trigger_lib($triggertype); - if (!$lib->multiple_use()) { - continue 2; - } - } else { + if (!trigger_manager::trigger_multipleuse($workflowtrigger->subpluginname)) { continue 2; } } From 523a41d2ec1146e8a6bef296d3ca278448fe3e13 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 13 Aug 2025 11:17:31 +0200 Subject: [PATCH 105/170] trigger customfielddelay: better handling when no date fields present --- .../de/lifecycletrigger_customfielddelay.php | 2 ++ .../en/lifecycletrigger_customfielddelay.php | 2 ++ trigger/customfielddelay/lib.php | 17 ++++++++++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php b/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php index a7b51e29..b3871590 100644 --- a/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php +++ b/trigger/customfielddelay/lang/de/lifecycletrigger_customfielddelay.php @@ -28,6 +28,8 @@ $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 0670a652..df80e4c1 100644 --- a/trigger/customfielddelay/lang/en/lifecycletrigger_customfielddelay.php +++ b/trigger/customfielddelay/lang/en/lifecycletrigger_customfielddelay.php @@ -28,6 +28,8 @@ $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 38053bcc..c98e1d00 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; @@ -94,15 +95,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'))); + } } /** From cf2722660aa7eae71a657092e8fd8e72f0cfeb71 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 13 Aug 2025 11:50:54 +0200 Subject: [PATCH 106/170] unique sql parameter for customfielddelay trigger, remove hardcoded table prefix in last access trigger --- classes/processor.php | 6 +++--- trigger/customfielddelay/lib.php | 4 ++-- trigger/lastaccess/lib.php | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 616d55f6..4985a8ee 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -313,11 +313,11 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) } /** - * 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[] $excluded List of course id, which should be excluded from counting. - * @param int[] $delayed List of course ids of delayed courses (globally and for workflow). + * @param int[] $excluded List of course IDs, which should be excluded from counting. + * @param int[] $delayed List of course IDs of delayed courses (globally and for workflow). * @return int[] $triggered, $new, $delayed Triggered courses, amount which courses of them would * be added, which courses are delayed. * @throws \coding_exception diff --git a/trigger/customfielddelay/lib.php b/trigger/customfielddelay/lib.php index c98e1d00..98c1f155 100644 --- a/trigger/customfielddelay/lib.php +++ b/trigger/customfielddelay/lib.php @@ -63,8 +63,8 @@ public function get_course_recordset_where($triggerid) { } $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]; } diff --git a/trigger/lastaccess/lib.php b/trigger/lastaccess/lib.php index ec5c6c3e..a7d4dae7 100644 --- a/trigger/lastaccess/lib.php +++ b/trigger/lastaccess/lib.php @@ -75,9 +75,9 @@ public function get_course_recordset_where($triggerid) { $where = '{course}.id IN (SELECT la.courseid - FROM mdl_user_enrolments AS ue - JOIN mdl_enrol AS e ON (ue.enrolid = e.id) - JOIN mdl_user_lastaccess AS la ON (ue.userid = la.userid) + FROM {user_enrolments} AS ue + JOIN {enrol} AS e ON (ue.enrolid = e.id) + JOIN {user_lastaccess} AS la ON (ue.userid = la.userid) WHERE e.courseid = la.courseid GROUP BY la.courseid HAVING MAX(la.timeaccess) < :lastaccessthreshold From af00410afa553551c1326894377c370fb4f118b4 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 13 Aug 2025 15:47:47 +0200 Subject: [PATCH 107/170] small cosmetics list courses window, fix semindependent counting --- classes/processor.php | 8 ++++---- styles.css | 8 +++++--- templates/workflowoverview.mustache | 2 +- trigger/semindependent/lib.php | 4 ++-- workflowoverview.php | 2 +- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 4985a8ee..719c4bac 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -373,12 +373,12 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { /** - * Also returns the amount of courses for a trigger for counting. BUT the trigger lib function check_courses is used + * Also returns the number of courses for a trigger for counting. BUT the trigger lib function check_courses is used * to select a course for triggering/excluding. * 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[] $excluded List of course id, which should be excluded from counting. - * @param int[] $delayed List of course ids of delayed courses (globally and for workflow). + * @param trigger_subplugin $trigger trigger which will be asked for additional where requirements. + * @param int[] $excluded List of course IDs which should be excluded from counting. + * @param int[] $delayed List of course IDs of delayed courses (globally and for workflow). * @return int[] $triggered, $new, $delayed Triggered courses, amount which courses of them would * be added, which courses are delayed. * @throws \coding_exception diff --git a/styles.css b/styles.css index 991eb78c..ed64dc3b 100644 --- a/styles.css +++ b/styles.css @@ -111,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 { diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index f3109e11..73b44a71 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -108,7 +108,7 @@ {{#showcoursecounts}} {{#displaytotaltriggered}} -
+
{{#coursestriggeredcount}} diff --git a/trigger/semindependent/lib.php b/trigger/semindependent/lib.php index 33d6fb91..3d0aeb8b 100644 --- a/trigger/semindependent/lib.php +++ b/trigger/semindependent/lib.php @@ -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 = "c.startdate > :semindepdate"; + $where = " NOT c.startdate > :semindepdate"; } else { - $where = "c.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/workflowoverview.php b/workflowoverview.php index d1e954e0..75602159 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -382,7 +382,7 @@ } else if ($excluded) { // Display courses table with excluded courses of this trigger. $trigger = trigger_manager::get_instance($excluded); if ($amounts[$trigger->sortindex]->excluded ?? false) { - $table = new triggered_courses_table_trigger($trigger, 'exclude', $search); + $table = new triggered_courses_table_trigger($trigger, 'excluded', $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); From 80a82e1c7a73b73527e3438393f8e93941148c10 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 15 Aug 2025 04:58:12 +0200 Subject: [PATCH 108/170] trigger selection sql: conjunction(AND) and disjunction(OR) now possible --- classes/local/entity/workflow.php | 15 +++++++-- classes/local/form/form_workflow_instance.php | 14 ++++++++ .../triggered_courses_table_workflow.php | 15 +++++---- classes/processor.php | 32 +++++++++++-------- db/install.xml | 1 + db/upgrade.php | 10 ++++-- editworkflow.php | 1 + lang/de/tool_lifecycle.php | 2 ++ lang/en/tool_lifecycle.php | 2 ++ templates/workflowoverview.mustache | 4 +-- workflowoverview.php | 15 +++++++-- 11 files changed, 82 insertions(+), 29 deletions(-) diff --git a/classes/local/entity/workflow.php b/classes/local/entity/workflow.php index be1302a9..700b77c4 100644 --- a/classes/local/entity/workflow.php +++ b/classes/local/entity/workflow.php @@ -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,9 +85,11 @@ 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; @@ -97,6 +102,7 @@ private function __construct($id, $title, $timeactive, $timedeactive, $sortindex $this->delayforallworkflows = $delayforallworkflows; $this->includedelayedcourses = $includedelayedcourses; $this->includesitecourse = $includesitecourse; + $this->andor = $andor; } /** @@ -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_workflow_instance.php b/classes/local/form/form_workflow_instance.php index a4c0db1d..890a2e4a 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 = array( + $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/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index 9dea7428..ea80441e 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -141,11 +141,9 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { } $where = 'true'; - if (!$workflow->includesitecourse) { - $where .= " AND c.id <> 1 "; - } $inparams = []; $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); + $andor = $workflow->andor ? 'AND' : 'OR'; foreach ($triggers as $trigger) { $lib = lib_manager::get_trigger_lib($trigger->subpluginname); if ($lib->is_manual_trigger()) { @@ -155,19 +153,22 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { $this->checkcoursecode = $lib->check_course_code(); } [$sql, $params] = $lib->get_course_recordset_where($trigger->id); - $sql = str_replace("{course}", "c", $sql); + $sql = preg_replace("/{course}/", "c", $sql, 1); if (!empty($sql)) { - $where .= ' AND ' . $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 .= " AND c.id = $filterdata "; + $where = "($where) AND c.id = $filterdata "; } else { - $where .= " AND ( c.fullname LIKE '%$filterdata%' OR c.shortname LIKE '%$filterdata%')"; + $where = "($where) AND (c.fullname LIKE '%$filterdata%' OR c.shortname LIKE '%$filterdata%')"; } } diff --git a/classes/processor.php b/classes/processor.php index 719c4bac..0e73891a 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -29,6 +29,7 @@ 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; @@ -40,8 +41,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. * @@ -245,29 +244,30 @@ public function process_course_interactive($processid) { * @throws \dml_exception */ public function get_course_recordset($triggers, $exclude, $forcounting = false) { - global $DB; + global $DB, $SESSION; - $where = 'true'; $whereparams = []; $workflowid = false; foreach ($triggers as $trigger) { + if (!$workflowid) { + $workflowid = $trigger->workflowid; + $workflow = workflow_manager::get_workflow($trigger->workflowid); + $andor = $workflow->andor == 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 = str_replace("{course}", "c", $sql); + $sql = preg_replace("/{course}/", "c", $sql, 1); if (!empty($sql)) { - $where .= ' AND ' . $sql; + $where .= " $andor " . $sql; $whereparams = array_merge($whereparams, $params); } - if (!$workflowid) { - $workflowid = $trigger->workflowid; - } } if ($forcounting) { if ($exclude) { - $workflow = workflow_manager::get_workflow($workflowid); if (!$workflow->includesitecourse) { - $where .= " AND c.id <> 1 "; + $where = "($where) AND c.id <> 1 "; } } // Get course hasprocess and delay with the sql. @@ -291,12 +291,11 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) WHERE " . $where; } else { if ($exclude) { - $workflow = workflow_manager::get_workflow($workflowid); if (!$workflow->includesitecourse) { - $where .= " AND c.id <> 1 "; + $where = "($where) AND c.id <> 1 "; } if (!$workflow->includedelayedcourses) { - $where .= " AND NOT c.id in (select courseid FROM {tool_lifecycle_delayed_workf} WHERE delayeduntil > :time1 + $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' => $workflowid]; @@ -309,6 +308,11 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) 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); } diff --git a/db/install.xml b/db/install.xml index 8fbfa6cc..9c6eefe1 100644 --- a/db/install.xml +++ b/db/install.xml @@ -112,6 +112,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index b35271f2..abe96a60 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -571,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); @@ -580,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); diff --git a/editworkflow.php b/editworkflow.php index af92b758..ad79970d 100644 --- a/editworkflow.php +++ b/editworkflow.php @@ -75,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/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 84f8d031..984a0f81 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -49,6 +49,8 @@ $string['adminsettings_notriggers'] = 'Keine Trigger-Subplugins installiert'; $string['adminsettings_workflow_definition_steps_heading'] = 'Workflowschritte'; $string['all_delays'] = 'Alle Verzögerungen'; +$string['andor'] = 'Trigger-Verknüpfungstyp'; +$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'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index d2d38cbe..103a040c 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -49,6 +49,8 @@ $string['adminsettings_notriggers'] = 'No trigger subplugins installed'; $string['adminsettings_workflow_definition_steps_heading'] = 'Workflow steps'; $string['all_delays'] = 'All delays'; +$string['andor'] = 'Combine triggers'; +$string['andor_help'] = 'Combine triggers by conjunction(AND) or disjunction(OR).'; $string['anonymous_user'] = 'Anonymous User'; $string['apply'] = 'Apply'; $string['backupcreated'] = 'Created at'; diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 73b44a71..9bcf8153 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -97,14 +97,14 @@ {{#counttriggers}}
{{#str}} courseselection_title, tool_lifecycle{{/str}} - + {{#showdetailsicon}} {{/showdetailsicon}} {{^showdetailsicon}} +     {{/showdetailsicon}} -
{{#showcoursecounts}} {{#displaytotaltriggered}} diff --git a/workflowoverview.php b/workflowoverview.php index 75602159..27b3818e 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -64,6 +64,8 @@ $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); +$debugtriggersql = ""; if ($showdetails == 0) { if (isset($SESSION->showdetails)) { if ($SESSION->showdetails == $workflowid) { @@ -72,8 +74,12 @@ } } else if ($showdetails == -1) { $SESSION->showdetails = $showdetails = 0; + $SESSION->debugtriggersql = $debugtriggersql = ''; } else { $SESSION->showdetails = $workflowid; + if (isset($SESSION->debugtriggersql)) { + $debugtriggersql = $SESSION->debugtriggersql; + } } $workflow = workflow_manager::get_workflow($workflowid); @@ -401,7 +407,6 @@ $hiddenfieldssearch[] = ['name' => 'triggered', 'value' => $triggered]; $tablecoursesamount = $coursestriggered; } - } else if ($delayed) { // Display courses table with courses delayed for this workflow. if ($coursesdelayed ?? false) { $table = new triggered_courses_table_workflow($coursesdelayed, $workflow, 'delayed', $search); @@ -434,9 +439,14 @@ $hiddenfieldssearch[] = ['name' => 'processes', 'value' => $processes]; $tablecoursesamount = count($coursesprocess); } +} else if ($showsql && $debugtriggersql) { // Display a sql statement stored in session. + $out = \html_writer::div( + str_replace("}", "", str_replace("{", $CFG->prefix, $debugtriggersql)), + "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', [ @@ -526,6 +536,7 @@ 'abortdisableworkflowlink' => $abortdisableworkflowlink, 'workflowprocesseslink' => $workflowprocesseslink, 'runlink' => new \moodle_url(urls::RUN), + 'debugtriggersql' => $debugtriggersql, ]; if ($showdetails) { // The triggers total box. From 14170c7e44261b850f3210f1387252de90596884 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 15 Aug 2025 08:55:19 +0200 Subject: [PATCH 109/170] codechecker issues --- classes/local/form/form_workflow_instance.php | 6 +++--- .../table/triggered_courses_table_trigger.php | 4 ++-- classes/processor.php | 14 +++++++------- trigger/lastaccess/lib.php | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/classes/local/form/form_workflow_instance.php b/classes/local/form/form_workflow_instance.php index 890a2e4a..d49b764a 100644 --- a/classes/local/form/form_workflow_instance.php +++ b/classes/local/form/form_workflow_instance.php @@ -131,10 +131,10 @@ public function definition() { } $elementname = 'andor'; - $groupelements = array( + $groupelements = [ $mform->createElement('radio', $elementname, '', 'AND', '0'), - $mform->createElement('radio', $elementname, '', 'OR', '1') - ); + $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); diff --git a/classes/local/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index 51b6588f..bfa4294e 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -112,10 +112,10 @@ public function __construct($trigger, $type, $filterdata = '') { if (!$workflow->includedelayedcourses && $excludedcourses) { $where .= " AND NOT c.id in (select courseid FROM {tool_lifecycle_delayed_workf} WHERE delayeduntil > :time1 - AND workflowid = :workflowid) + AND workflowid = :workflowid) AND NOT c.id in (select courseid FROM {tool_lifecycle_delayed} WHERE delayeduntil > :time2) "; $whereparams = array_merge($whereparams, - ['time1' => time(),'time2' => time(), 'workflowid' => $workflow->id]); + ['time1' => time(), 'time2' => time(), 'workflowid' => $workflow->id]); } if ($filterdata) { diff --git a/classes/processor.php b/classes/processor.php index 0e73891a..d7e4530b 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -295,10 +295,10 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) $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) + $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' => $workflowid]; + $inparams = ['time1' => time(), 'time2' => time(), 'workflowid' => $workflowid]; $whereparams = array_merge($whereparams, $inparams); } } @@ -352,10 +352,10 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { $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]; + $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); } diff --git a/trigger/lastaccess/lib.php b/trigger/lastaccess/lib.php index a7d4dae7..f5ae2401 100644 --- a/trigger/lastaccess/lib.php +++ b/trigger/lastaccess/lib.php @@ -73,11 +73,11 @@ public function instance_settings() { */ public function get_course_recordset_where($triggerid) { - $where = '{course}.id IN + $where = 'c.id IN (SELECT la.courseid - FROM {user_enrolments} AS ue - JOIN {enrol} AS e ON (ue.enrolid = e.id) - JOIN {user_lastaccess} AS la ON (ue.userid = la.userid) + 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 From 147aa680dc459cc985be58197ac5de36e3b15313 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 17 Aug 2025 10:25:59 +0200 Subject: [PATCH 110/170] we call it download if we download a workflow definition --- classes/local/table/active_workflows_table.php | 2 +- classes/local/table/workflow_definition_table.php | 4 ++-- lang/de/tool_lifecycle.php | 2 +- lang/en/tool_lifecycle.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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/workflow_definition_table.php b/classes/local/table/workflow_definition_table.php index 56862afe..2d712c95 100644 --- a/classes/local/table/workflow_definition_table.php +++ b/classes/local/table/workflow_definition_table.php @@ -122,8 +122,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/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 984a0f81..7ed8473a 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -54,7 +54,6 @@ $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.'; @@ -140,6 +139,7 @@ $string['documentationlink'] = 'Dokumenation 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'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 103a040c..9273c14a 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -54,7 +54,6 @@ $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.'; @@ -141,6 +140,7 @@ $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'; From 0cd8bcc3eeda8db4868893efcf14387dcd62d7c1 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 17 Aug 2025 15:24:05 +0200 Subject: [PATCH 111/170] small phpdoc changes in process_manager --- classes/local/manager/process_manager.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index 5c694c9d..9f3f12e2 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -318,11 +318,13 @@ public static function insert_process_error(process $process, Exception $e) { * Proceed process from procerror 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. unset($process->errormessage); unset($process->errortrace); unset($process->errorhash); @@ -343,7 +345,7 @@ 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->errormessage); unset($process->errortrace); unset($process->errorhash); From 53af52315d7740160e3b85115676e5764e78de21 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 18 Aug 2025 16:15:01 +0200 Subject: [PATCH 112/170] tiny fixes not important --- classes/local/backup/restore_lifecycle_workflow.php | 7 +++++-- lang/de/tool_lifecycle.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/classes/local/backup/restore_lifecycle_workflow.php b/classes/local/backup/restore_lifecycle_workflow.php index cd05dba4..95d624cc 100644 --- a/classes/local/backup/restore_lifecycle_workflow.php +++ b/classes/local/backup/restore_lifecycle_workflow.php @@ -154,6 +154,9 @@ private function load_subplugins() { $this->settings[] = $setting; } } +// foreach ($this->settings as $setting) { +// mtrace(\html_writer::div($setting->name.": ".$setting->value.": ".$setting->type.": ".$setting->pluginid)); +// } } /** @@ -203,7 +206,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 +215,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/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 7ed8473a..13ffe2cd 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -136,7 +136,7 @@ $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'] = 'Dokumenation auf github'; +$string['documentationlink'] = 'Dokumentation auf github'; $string['dontshowextendeddetails'] = 'Zeige detaillierte Workflow-Informationen nicht an'; $string['download'] = 'Herunterladen'; $string['downloadworkflow'] = 'Workflow-Definition herunterladen'; From 770ffd66f76c351955212114499c090244aa1705 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 18 Aug 2025 20:41:31 +0200 Subject: [PATCH 113/170] remove debug statements --- classes/local/backup/restore_lifecycle_workflow.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/classes/local/backup/restore_lifecycle_workflow.php b/classes/local/backup/restore_lifecycle_workflow.php index 95d624cc..1a216e1e 100644 --- a/classes/local/backup/restore_lifecycle_workflow.php +++ b/classes/local/backup/restore_lifecycle_workflow.php @@ -154,9 +154,6 @@ private function load_subplugins() { $this->settings[] = $setting; } } -// foreach ($this->settings as $setting) { -// mtrace(\html_writer::div($setting->name.": ".$setting->value.": ".$setting->type.": ".$setting->pluginid)); -// } } /** From b5d0e9302e7f22c5173bfd922d34f3cf2bc56539 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 21 Aug 2025 10:57:06 +0200 Subject: [PATCH 114/170] new feature for every workflow step: to define to which step to rollback to (optional) --- classes/event/process_rollback.php | 15 ++ classes/local/entity/step_subplugin.php | 6 + classes/local/form/form_step_instance.php | 22 +++ classes/local/manager/interaction_manager.php | 4 +- classes/local/manager/process_manager.php | 12 +- classes/local/manager/step_manager.php | 12 +- .../table/triggered_courses_table_trigger.php | 137 ++++++++++++++---- .../triggered_courses_table_workflow.php | 14 +- classes/processor.php | 54 ++++--- db/install.xml | 1 + db/upgrade.php | 8 + editelement.php | 3 + lang/de/tool_lifecycle.php | 7 +- lang/en/tool_lifecycle.php | 7 +- templates/overview_step.mustache | 10 +- templates/overview_trigger.mustache | 6 +- version.php | 2 +- workflowoverview.php | 28 +++- 18 files changed, 276 insertions(+), 72 deletions(-) diff --git a/classes/event/process_rollback.php b/classes/event/process_rollback.php index 429f8617..01574650 100644 --- a/classes/event/process_rollback.php +++ b/classes/event/process_rollback.php @@ -143,4 +143,19 @@ public static function get_other_mapping() { // No backup and restore. return false; } + + /** + * Gives back the stepindex of the step to which the current step has to be rolled back, if any. + * @param int $workflowid workflow ID + * @param int $stepindex sort order of the step to be rolled back + * @return bool + * @throws \dml_exception + */ + public static function get_rollbacksortindex($workflowid, $stepindex) { + global $DB; + + $rollbacksortindex = $DB->get_field('tool_lifecycle_step', + 'rollbacksortindex', ['workflowid' => $workflowid, 'stepindex' => $stepindex]); + return $rollbacksortindex ?? false; + } } 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/form/form_step_instance.php b/classes/local/form/form_step_instance.php index 86d8d1bf..f839b883 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; @@ -131,6 +132,27 @@ public function definition() { $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')); diff --git a/classes/local/manager/interaction_manager.php b/classes/local/manager/interaction_manager.php index f263fb8a..1f699cf0 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; @@ -85,7 +86,8 @@ public static function handle_interaction($stepid, $processid, $action) { break; case step_interactive_response::rollback(): delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, $process->workflowid); - process_manager::rollback_process($process); + $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, $process->stepindex); + process_manager::rollback_process($process, $rollbacksortindex); break; } return true; diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index 9f3f12e2..27e8b170 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -196,12 +196,13 @@ public static function set_process_waiting(&$process) { /** * Currently only removes the current process. * @param process $process process the rollback should be triggered for. + * @param int $tosortindex to which step the process has to rollback. * @throws \coding_exception * @throws \dml_exception */ - public static function rollback_process($process) { + public static function rollback_process($process, $tosortindex = 0) { process_rollback::event_from_process($process)->trigger(); - for ($i = $process->stepindex; $i >= 1; $i--) { + 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 { @@ -212,7 +213,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); + } } /** @@ -355,6 +358,7 @@ public static function rollback_process_after_error(int $processid) { $DB->delete_records('tool_lifecycle_proc_error', ['id' => $process->id]); delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, $process->workflowid); - self::rollback_process($process); + $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, $process->stepindex); + self::rollback_process($process, $rollbacksortindex); } } diff --git a/classes/local/manager/step_manager.php b/classes/local/manager/step_manager.php index 3158cd40..e12b4a4b 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->rollbacksortindex) { + if (!$up && $step->sortindex == $step->rollbacksortindex + 1) { + return; + } + + } + $index = $step->sortindex; if ($up) { $otherindex = $index - 1; @@ -323,7 +334,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/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index bfa4294e..745b6043 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -24,10 +24,13 @@ namespace tool_lifecycle\local\table; use tool_lifecycle\local\entity\trigger_subplugin; -use tool_lifecycle\local\manager\delayed_courses_manager; 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\processor; +use tool_lifecycle\local\response\trigger_response; +use tool_lifecycle\settings_type; +use tool_lifecycle\urls; defined('MOODLE_INTERNAL') || die; @@ -42,6 +45,18 @@ */ 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; + /** * Builds a table of courses. * @param trigger_subplugin $trigger of which the courses are listed @@ -50,45 +65,33 @@ class triggered_courses_table_trigger extends \table_sql { * @throws \coding_exception * @throws \dml_exception */ - public function __construct($trigger, $type, $filterdata = '') { + public function __construct($numbercourses, $trigger, $type, $filterdata = '') { parent::__construct('tool_lifecycle-courses-in-trigger'); - global $PAGE; + global $PAGE, $SESSION; $workflow = workflow_manager::get_workflow($trigger->workflowid); - $processor = new processor(); - $lib = lib_manager::get_trigger_lib($trigger->subpluginname); + $this->triggerid = $trigger->id; + $this->workflowid = $workflow->id; + $this->type = $type; - // 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); - if ($lib->check_course_code()) { - [$triggercourses, , ] = $processor->get_triggercourses_forcounting_check_course( - $trigger, $excludedcourses, $delayedcourses); - } else { - [$triggercourses, , ] = $processor->get_triggercourses_forcounting( - $trigger, $excludedcourses, $delayedcourses); - } - if (!$triggercourses) { - return; - } + $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; - $a->courses = $triggercourses; + $a->courses = $numbercourses; 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']; + $this->caption .= "   ".\html_writer::link(new \moodle_url(urls::WORKFLOW_DETAILS, + ["wf" => $workflow->id, "showsql" => "1", "showtablesql" => "1", "showdetails" => "1"]), + "   ", ["class" => "text-muted fs-6 text-decoration-none"]); + $columns = ['courseid', 'coursefullname', 'coursecategory']; $this->define_columns($columns); $headers = [ @@ -98,19 +101,38 @@ public function __construct($trigger, $type, $filterdata = '') { ]; $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"; - $from = "{course} c LEFT JOIN {course_categories} cc ON c.category = cc.id "; + $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, + 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, + 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} c LEFT JOIN {course_categories} cc ON c.category = cc.id + 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_delayed} d ON c.id = d.courseid + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid "; if (!$workflow->includesitecourse) { $where .= " AND c.id <> 1 "; } - - if (!$workflow->includedelayedcourses && $excludedcourses) { + if (($workflow->includedelayedcourses ?? "0") != "1") { $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) "; @@ -126,9 +148,61 @@ public function __construct($trigger, $type, $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. + */ + public function build_table() { + + if (!$this->rawdata) { + return; + } + + $trigger = trigger_manager::get_instance($this->triggerid); + $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); + + foreach ($this->rawdata as $row) { + $response = $lib->check_course($row->courseid, $this->triggerid); + + if (!($response == trigger_response::exclude() || $response == trigger_response::trigger())) { + continue; + } + if ($row->hasprocess) { + continue; + } + if ($row->delay && $row->delay > time()) { + continue; + } else { + if ($this->type == 'triggerid') { + if ($response == trigger_response::trigger()) { + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + } + } else if ($this->type == 'excluded') { + if ($response == trigger_response::exclude() || $this->triggerexclude) { + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + } + } + } + } + } + /** * Render coursefullname column. * @param object $row Row data. @@ -147,5 +221,8 @@ 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 index ea80441e..859afc17 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -69,7 +69,7 @@ class triggered_courses_table_workflow extends \table_sql { */ public function __construct($courses, $workflow, $type, $filterdata = '') { parent::__construct('tool_lifecycle-courses-in-trigger'); - global $PAGE; + global $PAGE, $SESSION; $this->define_baseurl($PAGE->url); $this->type = $type; @@ -86,7 +86,11 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { } else if ($type == 'used') { $this->caption = get_string('coursesused', 'tool_lifecycle', $a); } + $this->caption .= "   ".\html_writer::link(new \moodle_url(urls::WORKFLOW_DETAILS, + ["wf" => $workflow->id, "showsql" => "1", "showtablesql" => "1", "showdetails" => "1"]), + "   ", ["class" => "text-muted fs-6 text-decoration-none"]); $this->captionattributes = ['class' => 'ml-3']; + $columns = ['courseid', 'coursefullname', 'coursecategory']; if ($type == 'triggeredworkflow' && $this->selectable) { $columns[] = 'tools'; @@ -143,7 +147,7 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { $where = 'true'; $inparams = []; $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); - $andor = $workflow->andor ? 'AND' : 'OR'; + $andor = ($workflow->andor ?? 0) == 0 ? 'AND' : 'OR'; foreach ($triggers as $trigger) { $lib = lib_manager::get_trigger_lib($trigger->subpluginname); if ($lib->is_manual_trigger()) { @@ -172,6 +176,12 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { } } + $debugsql = $fields.$from.$where; + foreach ($inparams as $key => $value) { + $debugsql = str_replace(":".$key, $value, $debugsql); + } + $SESSION->debugtablesql = $debugsql; + $this->set_sql($fields, $from, $where, $inparams); } diff --git a/classes/processor.php b/classes/processor.php index d7e4530b..c8aa2866 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -27,6 +27,7 @@ namespace tool_lifecycle; +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; @@ -181,7 +182,8 @@ public function process_courses() { } else if ($result == step_response::rollback()) { delayed_courses_manager::set_course_delayed_for_workflow($course->id, true, $process->workflowid); - process_manager::rollback_process($process); + $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, $process->stepindex); + process_manager::rollback_process($process, $rollbacksortindex); break; } else { throw new \moodle_exception('Return code \''. var_dump($result) . '\' is not allowed!'); @@ -225,7 +227,8 @@ public function process_course_interactive($processid) { return $this->process_course_interactive($processid); case step_interactive_response::rollback(): delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, $process->workflowid); - process_manager::rollback_process($process); + $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, $process->stepindex); + process_manager::rollback_process($process, $rollbacksortindex); break; } return true; @@ -252,7 +255,7 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) if (!$workflowid) { $workflowid = $trigger->workflowid; $workflow = workflow_manager::get_workflow($trigger->workflowid); - $andor = $workflow->andor == 0 ? 'AND' : 'OR'; + $andor = ($workflow->andor ?? 0) == 0 ? 'AND' : 'OR'; $where = $andor == 'AND' ? 'true ' : 'false '; } $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); @@ -320,14 +323,13 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) * 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[] $excluded List of course IDs, which should be excluded from counting. - * @param int[] $delayed List of course IDs of delayed courses (globally and for workflow). + * @param int $excluded Are courses to exclude? 0 if not. * @return int[] $triggered, $new, $delayed Triggered courses, amount which courses of them would * be added, which courses are delayed. * @throws \coding_exception * @throws \dml_exception */ - public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { + public function get_triggercourses_forcounting($trigger, $excluded) { global $DB; $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); @@ -343,11 +345,17 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { $triggercoursesall = $DB->get_fieldset_sql($sql, $whereparams); // Get amount 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 sitecourse according to the workflow settings. - if (!empty($excluded)) { - $workflow = workflow_manager::get_workflow($trigger->workflowid); + if ($excluded) { if (!$workflow->includesitecourse) { $where .= " AND c.id <> 1 "; } @@ -367,8 +375,7 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { 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) + WHERE p.courseid IS NOT NULL OR pe.courseid IS NOT NULL )"; $newcourses = $DB->count_records_sql($sql, $whereparams); @@ -381,20 +388,31 @@ public function get_triggercourses_forcounting($trigger, $excluded, $delayed) { * to select a course for triggering/excluding. * 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[] $excluded List of course IDs which should be excluded from counting. - * @param int[] $delayed List of course IDs of delayed courses (globally and for workflow). + * @param int $excluded Are courses to exclude? 0 if not. * @return int[] $triggered, $new, $delayed Triggered courses, amount which courses of them would * be added, which courses are delayed. * @throws \coding_exception * @throws \dml_exception */ - public function get_triggercourses_forcounting_check_course($trigger, $excluded, $delayed) { + public function get_triggercourses_forcounting_check_course($trigger, $excluded) { global $DB; $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); + $excludedcourses = []; + $delayedcourses = []; + if ($excluded) { + $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 = array_merge(delayed_courses_manager::get_delayed_courses_for_workflow($workflow->id), + delayed_courses_manager::get_globally_delayed_courses()); + } + $excludedcourses = array_merge($sitecourse, $delayedcourses); + } $triggercoursesall = []; - $recordset = $this->get_course_recordset([$trigger], $excluded, true); + $recordset = $this->get_course_recordset([$trigger], $excludedcourses, true); while ($recordset->valid()) { $course = $recordset->current(); $response = $lib->check_course($course, $trigger->id); @@ -405,9 +423,9 @@ public function get_triggercourses_forcounting_check_course($trigger, $excluded, } // Get delayed courses which would be triggered by this trigger. - $delayedcourses = array_intersect($triggercoursesall, $delayed); + $delayedcourses = array_intersect($triggercoursesall, $delayedcourses); - $triggercourses = array_diff($triggercoursesall, $excluded); + $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} @@ -472,10 +490,10 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { // Delayed: Courses in current selection, which are delayed. if ($lib->check_course_code()) { [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting_check_course( - $trigger, $excludedcourses, $delayedcourses); + $trigger, count($excludedcourses)); } else { [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting( - $trigger, $excludedcourses, $delayedcourses); + $trigger, count($excludedcourses)); } if ($obj->response == trigger_response::exclude()) { $obj->excluded = $newcourses; diff --git a/db/install.xml b/db/install.xml index 9c6eefe1..6c27f072 100644 --- a/db/install.xml +++ b/db/install.xml @@ -51,6 +51,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index abe96a60..cd3d8388 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -601,6 +601,14 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { workflow_manager::remove($trigger->workflowid); } + $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, 2025050404, 'tool', 'lifecycle'); } diff --git a/editelement.php b/editelement.php index d9e89584..1fa4c887 100644 --- a/editelement.php +++ b/editelement.php @@ -120,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); } diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 13ffe2cd..c750005e 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -80,10 +80,10 @@ $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 oder bei seinen Prozessfehlern.'; +$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_candidates_alreadyin'] = ', weitere {$a} Kandidaten sind bereits in Verarbeitung oder bei den Verarbeitungsfehlern'; +$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}'; @@ -229,6 +229,9 @@ $string['restore_workflow_not_found'] = 'Falsches Format der Sicherungsdatei. Der Workflow konnte nicht gefunden werden.'; $string['rollback'] = 'Zurücksetzung'; $string['rolledback'] = 'Zurückgesetzt'; +$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['run'] = 'Ausführen'; $string['runtask'] = 'Führe Lifecycle System-Task aus'; $string['searchcourses'] = 'Kurs-Suche'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 9273c14a..1d3682bd 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -80,11 +80,11 @@ $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 or of the process errors'; +$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_candidates_alreadyin'] = ', another {$a} candidates are already part of this process or of the process errors'; +$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}'; @@ -229,6 +229,9 @@ $string['restore_workflow_not_found'] = 'Wrong format of the backup file. The workflow could not be found.'; $string['rollback'] = 'Rollback'; $string['rolledback'] = 'Rolled back'; +$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['run'] = 'Run'; $string['runtask'] = 'Run scheduled lifecycle task'; $string['searchcourses'] = 'Search for courses'; diff --git a/templates/overview_step.mustache b/templates/overview_step.mustache index d2faf758..92f899bd 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_trigger.mustache b/templates/overview_trigger.mustache index 509bea29..0e996189 100644 --- a/templates/overview_trigger.mustache +++ b/templates/overview_trigger.mustache @@ -39,7 +39,7 @@
- {{#shortentext}} 25, {{instancename}} {{/shortentext}}
+ {{#shortentext}} 50, {{instancename}} {{/shortentext}}
{{subpluginname}}
@@ -63,7 +63,7 @@ {{^triggeredcourses}} {{^excludedcourses}} {{^exclude}} - + 0 {{/exclude}} @@ -78,7 +78,7 @@ {{/excludedcourses}} {{^excludedcourses}} {{#exclude}} - + 0 {{/exclude}} diff --git a/version.php b/version.php index 4b6e2dd4..3e850a36 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die; $plugin->maturity = MATURITY_STABLE; -$plugin->version = 2025050404; +$plugin->version = 2025050404.01; $plugin->component = 'tool_lifecycle'; $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; diff --git a/workflowoverview.php b/workflowoverview.php index 27b3818e..e90b0d0e 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -34,6 +34,7 @@ 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; @@ -65,7 +66,9 @@ $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) { @@ -75,9 +78,12 @@ } else if ($showdetails == -1) { $SESSION->showdetails = $showdetails = 0; $SESSION->debugtriggersql = $debugtriggersql = ''; + $SESSION->debugtablesql = $debugtablesql = ''; } else { $SESSION->showdetails = $workflowid; - if (isset($SESSION->debugtriggersql)) { + if ($showtablesql && isset($SESSION->debugtablesql)) { + $debugtablesql = $SESSION->debugtablesql; + } else if ($showsql && isset($SESSION->debugtriggersql)) { $debugtriggersql = $SESSION->debugtriggersql; } } @@ -143,7 +149,8 @@ if ($processid) { $process = process_manager::get_process_by_id($processid); if ($action === 'rollback') { - process_manager::rollback_process($process); + $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, $process->stepindex); + process_manager::rollback_process($process, $rollbacksortindex); delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, $workflow); $msg = get_string('courserolledback', 'tool_lifecycle'); } else if ($action === 'proceed') { @@ -353,6 +360,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; } @@ -377,7 +388,8 @@ } else if ($triggerid) { // Display courses table with triggered courses of this trigger. $trigger = trigger_manager::get_instance($triggerid); if ($amounts[$trigger->sortindex]->triggered ?? false) { - $table = new triggered_courses_table_trigger($trigger, 'triggerid', $search); + $table = new triggered_courses_table_trigger($amounts[$trigger->sortindex]->triggered, + $trigger, 'triggerid', $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); @@ -388,7 +400,8 @@ } else if ($excluded) { // Display courses table with excluded courses of this trigger. $trigger = trigger_manager::get_instance($excluded); if ($amounts[$trigger->sortindex]->excluded ?? false) { - $table = new triggered_courses_table_trigger($trigger, 'excluded', $search); + $table = new triggered_courses_table_trigger($amounts[$trigger->sortindex]->excluded, + $trigger, 'excluded', $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); @@ -439,11 +452,15 @@ $hiddenfieldssearch[] = ['name' => 'processes', 'value' => $processes]; $tablecoursesamount = count($coursesprocess); } -} else if ($showsql && $debugtriggersql) { // Display a sql statement stored in session. +} 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 the course list. @@ -536,7 +553,6 @@ 'abortdisableworkflowlink' => $abortdisableworkflowlink, 'workflowprocesseslink' => $workflowprocesseslink, 'runlink' => new \moodle_url(urls::RUN), - 'debugtriggersql' => $debugtriggersql, ]; if ($showdetails) { // The triggers total box. From c619dcfeeab2acf7c13935fe20fcffc655a192ea Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 21 Aug 2025 11:43:22 +0200 Subject: [PATCH 115/170] change tooltip mouseover icon to hand --- .../table/triggered_courses_table_trigger.php | 2 +- .../triggered_courses_table_workflow.php | 99 +++++++++---------- lang/de/tool_lifecycle.php | 1 + lang/en/tool_lifecycle.php | 1 + templates/overview_processeslink.mustache | 13 ++- templates/overview_step.mustache | 6 +- templates/overview_timetrigger.mustache | 2 +- templates/overview_trigger.mustache | 20 ++-- templates/workflowoverview.mustache | 22 ++--- workflowoverview.php | 14 +-- 10 files changed, 85 insertions(+), 95 deletions(-) diff --git a/classes/local/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index 745b6043..fce1febb 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -132,7 +132,7 @@ public function __construct($numbercourses, $trigger, $type, $filterdata = '') { if (!$workflow->includesitecourse) { $where .= " AND c.id <> 1 "; } - if (($workflow->includedelayedcourses ?? "0") != "1") { + if (1==2 && ($workflow->includedelayedcourses ?? "0") != "1") { $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) "; diff --git a/classes/local/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index 859afc17..2dcc6c02 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -46,7 +46,7 @@ */ class triggered_courses_table_workflow extends \table_sql { - /** @var string $type of the courses list: triggeredworkflow, delayed or used */ + /** @var string $type of the courses list: triggeredworkflow, delayed, used, processes */ private $type; /** @var int $workflowid Id of the workflow */ @@ -62,7 +62,7 @@ class triggered_courses_table_workflow extends \table_sql { * 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 + * @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 @@ -85,6 +85,8 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { $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->caption .= "   ".\html_writer::link(new \moodle_url(urls::WORKFLOW_DETAILS, ["wf" => $workflow->id, "showsql" => "1", "showtablesql" => "1", "showdetails" => "1"]), @@ -146,21 +148,28 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { $where = 'true'; $inparams = []; - $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 ($type == 'processes') { +// $where .= " AND c.id IN (SELECT courseid FROM {tool_lifecycle_process} WHERE workflowid = :pworkflowid1 UNION +// SELECT courseid FROM {tool_lifecycle_proc_error} WHERE workflowid = :pworkflowid2)"; + $where .= " AND (p.id IS NOT NULL OR pe.id IS NOT NULL) "; + $inparams = array_merge($inparams, ['pworkflowid1' => $workflow->id, 'pworkflowid2' => $workflow->id]); + } else { + $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); + } } } } @@ -168,6 +177,7 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { if (!$workflow->includesitecourse) { $where = "($where) AND c.id <> 1 "; } + if ($filterdata) { if (is_numeric($filterdata)) { $where = "($where) AND c.id = $filterdata "; @@ -176,7 +186,7 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { } } - $debugsql = $fields.$from.$where; + $debugsql = "SELECT ".$fields." FROM ".$from." WHERE ".$where; foreach ($inparams as $key => $value) { $debugsql = str_replace(":".$key, $value, $debugsql); } @@ -234,45 +244,27 @@ public function build_table() { continue; } } - if (!$action) { - if ($row->hasprocess) { - if ($this->workflowid && ($row->workflowid != $this->workflowid)) { - if ($this->type == 'used') { - $formattedrow = $this->format_row($row); - $this->add_data_keyed($formattedrow, $this->get_row_class($row)); - } - } - } else if ($row->delay && $row->delay > time()) { - if ($this->type == 'delayed') { - $formattedrow = $this->format_row($row); - $this->add_data_keyed($formattedrow, $this->get_row_class($row)); - } - } else { - if ($this->type == 'triggeredworkflow') { - $formattedrow = $this->format_row($row); - $this->add_data_keyed($formattedrow, $this->get_row_class($row)); - } - } + if ($action) { + continue; } - } else { - if ($row->hasprocess) { - if ($row->workflowid && ($row->workflowid != $this->workflowid)) { - if ($this->type == 'used') { - $formattedrow = $this->format_row($row); - $this->add_data_keyed($formattedrow, $this->get_row_class($row)); - } - } - } else if ($row->delay && $row->delay > time()) { - if ($this->type == 'delayed') { - $formattedrow = $this->format_row($row); - $this->add_data_keyed($formattedrow, $this->get_row_class($row)); - } - } else { - if ($this->type == 'triggeredworkflow') { + } + if ($row->hasprocess) { + if ($row->workflowid) { + if ( ($row->workflowid != $this->workflowid && $this->type == 'used') OR $this->type == 'processes' ) { $formattedrow = $this->format_row($row); $this->add_data_keyed($formattedrow, $this->get_row_class($row)); } } + } else if ($row->delay && $row->delay > time()) { + if ($this->type == 'delayed') { + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + } + } else { + if ($this->type == 'triggeredworkflow') { + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + } } } } @@ -345,5 +337,8 @@ 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/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index c750005e..bb522a2d 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -102,6 +102,7 @@ $string['coursestriggered'] = 'Getriggerte Kurse für Trigger \'{$a->title}\' ({$a->courses})'; $string['coursestriggeredworkflow'] = 'Getriggerte Kurse für diesen Workflow \'{$a->title}\' ({$a->courses})'; $string['coursesused'] = 'Kurse schon in einem anderen Prozess für Workflow \'{$a->title}\' ({$a->courses})'; +$string['coursesinprocess'] = 'Kurse schon in einem Prozess des Workflows \'{$a->title}\' ({$a->courses})'; $string['create_copy'] = 'Kopie erstellen'; $string['create_step'] = 'Step erstellen'; $string['create_trigger'] = 'Trigger erstellen'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 1d3682bd..15b723e9 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -103,6 +103,7 @@ $string['coursestriggered'] = 'Courses triggered by trigger \'{$a->title}\' ({$a->courses})'; $string['coursestriggeredworkflow'] = 'Courses triggered for this workflow \'{$a->title}\' ({$a->courses})'; $string['coursesused'] = 'Courses already used in another process for workflow \'{$a->title}\' ({$a->courses})'; +$string['coursesinprocess'] = 'Courses already in a process of the workflow \'{$a->title}\' ({$a->courses})'; $string['create_copy'] = 'Create copy'; $string['create_step'] = 'Create step'; $string['create_trigger'] = 'Create trigger'; diff --git a/templates/overview_processeslink.mustache b/templates/overview_processeslink.mustache index 25ef4eda..f73c879c 100644 --- a/templates/overview_processeslink.mustache +++ b/templates/overview_processeslink.mustache @@ -15,20 +15,19 @@ along with Moodle. If not, see . }} {{! - @template tool_lifecycle/overview_addinstance + @template tool_lifecycle/overview_processlink Add trigger and add step select fields Example context (json): { - "addtriggerselect": "Add trigger select", - "addstepselect": "Add step select", - "activate": "Activate Button", - "newworkflow": true + "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 92f899bd..f2639733 100644 --- a/templates/overview_step.mustache +++ b/templates/overview_step.mustache @@ -39,11 +39,11 @@
- {{#shortentext}} 50, {{instancename}} {{/shortentext}} + {{#shortentext}} 50, {{instancename}} {{/shortentext}} {{#rollbacktosortindex}} - + {{rollbacktosortindex}} - + {{/rollbacktosortindex}}
{{subpluginname}} diff --git a/templates/overview_timetrigger.mustache b/templates/overview_timetrigger.mustache index 82add35c..c3158098 100644 --- a/templates/overview_timetrigger.mustache +++ b/templates/overview_timetrigger.mustache @@ -34,7 +34,7 @@
- {{#shortentext}} 25, {{instancename}} {{/shortentext}}
+ {{#shortentext}} 25, {{instancename}} {{/shortentext}}
{{subpluginname}}
diff --git a/templates/overview_trigger.mustache b/templates/overview_trigger.mustache index 0e996189..222dbc1e 100644 --- a/templates/overview_trigger.mustache +++ b/templates/overview_trigger.mustache @@ -39,7 +39,7 @@
- {{#shortentext}} 50, {{instancename}} {{/shortentext}}
+ {{#shortentext}} 50, {{instancename}} {{/shortentext}}
{{subpluginname}}
@@ -55,32 +55,26 @@ {{^additionalinfo}} {{#triggeredcourses}} - - {{triggeredcourses}} - + + {{triggeredcourses}} + {{/triggeredcourses}} {{^triggeredcourses}} {{^excludedcourses}} {{^exclude}} - - 0 - + 0 {{/exclude}} {{/excludedcourses}} {{/triggeredcourses}} {{#excludedcourses}} - - {{excludedcourses}} - + {{excludedcourses}} {{/excludedcourses}} {{^excludedcourses}} {{#exclude}} - - 0 - + 0 {{/exclude}} {{/excludedcourses}} {{/additionalinfo}} diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 9bcf8153..30e2986a 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -82,10 +82,10 @@ {{{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. }} @@ -97,7 +97,7 @@ {{#counttriggers}}
{{#str}} courseselection_title, tool_lifecycle{{/str}} - + {{#showdetailsicon}} {{/showdetailsicon}} @@ -133,7 +133,7 @@ {{/displaytotaltriggered}} {{^displaytotaltriggered}}
- {{#str}} manualtriggerenvolved, tool_lifecycle {{/str}} + {{#str}} manualtriggerenvolved, tool_lifecycle {{/str}}
{{/displaytotaltriggered}} {{/showcoursecounts}} @@ -148,14 +148,14 @@
-
{{#str}} courseselectionrun_title, tool_lifecycle{{/str}}
+
{{#str}} courseselectionrun_title, tool_lifecycle{{/str}}
{{#nomanualtriggerinvolved}}
{{#str}} nextrun, tool_lifecycle, {{{nextrun}}} {{/str}} - - {{#str}} run, tool_lifecycle {{/str}} - + + {{#str}} run, tool_lifecycle {{/str}} +
{{#str}} lastrun, tool_lifecycle, {{lastrun}} {{/str}} @@ -163,7 +163,7 @@ {{/nomanualtriggerinvolved}} {{^nomanualtriggerinvolved}}
- {{#str}} manualtriggerenvolved, tool_lifecycle {{/str}} + {{#str}} manualtriggerenvolved, tool_lifecycle {{/str}}
{{/nomanualtriggerinvolved}} {{#counttimetriggers}} diff --git a/workflowoverview.php b/workflowoverview.php index e90b0d0e..93ed1268 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -441,16 +441,16 @@ $tablecoursesamount = $amounts['all']->used; } } else if ($processes) { // Display courses table with courses in a process or in state process error for this workflow. - $coursesinprocess = $DB->get_fieldset('tool_lifecycle_process', 'courseid', ['workflowid' => $workflow->id]); - $coursesprocesserrors = $DB->get_fieldset('tool_lifecycle_proc_error', 'courseid', ['workflowid' => $workflow->id]); - if ($coursesprocess = array_merge($coursesinprocess, $coursesprocesserrors)) { - $table = new process_courses_table_workflow(count($coursesprocess), $workflow, 'processes', $search); + $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(); $hiddenfieldssearch[] = ['name' => 'processes', 'value' => $processes]; - $tablecoursesamount = count($coursesprocess); + $tablecoursesamount = $coursesinprocess; } } else if ($showsql && $debugtriggersql) { // Display the trigger sql statement stored in session. $out = \html_writer::div( @@ -631,8 +631,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'))); } } } From 1cc0a7e0aed02f4a80a5b0aa3330103f6353dacb Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 21 Aug 2025 11:55:51 +0200 Subject: [PATCH 116/170] course selection run: show run-link only when workflow is active --- templates/workflowoverview.mustache | 2 ++ workflowoverview.php | 1 + 2 files changed, 3 insertions(+) diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 30e2986a..1cf4bad2 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -152,12 +152,14 @@ {{#nomanualtriggerinvolved}}
{{#str}} nextrun, tool_lifecycle, {{{nextrun}}} {{/str}} + {{#runnable}} {{#str}} run, tool_lifecycle {{/str}}
+ {{/runnable}} {{#str}} lastrun, tool_lifecycle, {{lastrun}} {{/str}}
{{/nomanualtriggerinvolved}} diff --git a/workflowoverview.php b/workflowoverview.php index 93ed1268..4ed0dc25 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -552,6 +552,7 @@ 'disableworkflowlink' => $disableworkflowlink, 'abortdisableworkflowlink' => $abortdisableworkflowlink, 'workflowprocesseslink' => $workflowprocesseslink, + 'runnable' => $isactive, 'runlink' => new \moodle_url(urls::RUN), ]; if ($showdetails) { From 9b6a7b9b2b050585e03fc08c5431d4d1e4c22908 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 22 Aug 2025 04:29:40 +0200 Subject: [PATCH 117/170] Minus for indicating exclude triggers, tiny css cosmetics, fix lastrun of timetriggers --- classes/processor.php | 13 ++++++++----- styles.css | 4 ++++ templates/overview_processeslink.mustache | 4 ++-- templates/overview_timetrigger.mustache | 2 +- templates/overview_trigger.mustache | 2 +- templates/workflowoverview.mustache | 4 ++-- workflowoverview.php | 8 +++++--- 7 files changed, 23 insertions(+), 14 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index c8aa2866..b5067a9c 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -364,8 +364,8 @@ public function get_triggercourses_forcounting($trigger, $excluded) { 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); } - $whereparams = array_merge($whereparams, $inparams); } $sql = 'SELECT count(c.id) from {course} c WHERE '. $where; $triggercourses = $DB->count_records_sql($sql, $whereparams); @@ -513,11 +513,14 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $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'))); + } else { + $obj->additionalinfo = "--"; + } } else { - $obj->additionalinfo = '-'; + $obj->additionalinfo = "--"; } $obj->sql = "---"; $autotriggers[] = $trigger; diff --git a/styles.css b/styles.css index ed64dc3b..303962d8 100644 --- a/styles.css +++ b/styles.css @@ -152,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/templates/overview_processeslink.mustache b/templates/overview_processeslink.mustache index f73c879c..dd56ff1c 100644 --- a/templates/overview_processeslink.mustache +++ b/templates/overview_processeslink.mustache @@ -27,7 +27,7 @@ } }} - + {{processes}} - + diff --git a/templates/overview_timetrigger.mustache b/templates/overview_timetrigger.mustache index c3158098..84f9ec3b 100644 --- a/templates/overview_timetrigger.mustache +++ b/templates/overview_timetrigger.mustache @@ -34,7 +34,7 @@
diff --git a/templates/overview_trigger.mustache b/templates/overview_trigger.mustache index 222dbc1e..9a942405 100644 --- a/templates/overview_trigger.mustache +++ b/templates/overview_trigger.mustache @@ -69,7 +69,7 @@ {{/triggeredcourses}} {{#excludedcourses}} - {{excludedcourses}} + - {{excludedcourses}} {{/excludedcourses}} {{^excludedcourses}} diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 1cf4bad2..e8e580c0 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -82,8 +82,8 @@ {{{workflowprocesseslink}}}

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

+ {{#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 }} diff --git a/workflowoverview.php b/workflowoverview.php index 4ed0dc25..42d20fdf 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -316,8 +316,10 @@ } 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; @@ -546,8 +548,8 @@ 'showdetailsicon' => $showdetails == 0, 'isactive' => $isactive || $isdeactivated, 'nextrun' => $nextrunout, - 'lastrun' => $lastrun ? - userdate($lastrun, get_string('strftimedatetimeshort', 'langconfig')) : '-', + 'lastrun' => $lastrun != 0 ? + userdate($lastrun, get_string('strftimedatetimeshort', 'langconfig')) : '--', 'nomanualtriggerinvolved' => $nomanualtriggerinvolved, 'disableworkflowlink' => $disableworkflowlink, 'abortdisableworkflowlink' => $abortdisableworkflowlink, From 95c46e132aceb35bf143bac05f7c04779474517b Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 22 Aug 2025 15:28:07 +0200 Subject: [PATCH 118/170] codechecker issues --- classes/local/table/triggered_courses_table_trigger.php | 8 +------- classes/local/table/triggered_courses_table_workflow.php | 6 +++--- lang/de/tool_lifecycle.php | 4 ++-- lang/en/tool_lifecycle.php | 4 ++-- templates/overview_processeslink.mustache | 4 ++-- 5 files changed, 10 insertions(+), 16 deletions(-) diff --git a/classes/local/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index fce1febb..d9ead637 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -59,6 +59,7 @@ class triggered_courses_table_trigger extends \table_sql { /** * Builds a table of courses. + * @param int $numbercourses number of courses listed here * @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 -name @@ -132,13 +133,6 @@ public function __construct($numbercourses, $trigger, $type, $filterdata = '') { if (!$workflow->includesitecourse) { $where .= " AND c.id <> 1 "; } - if (1==2 && ($workflow->includedelayedcourses ?? "0") != "1") { - $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) "; - $whereparams = array_merge($whereparams, - ['time1' => time(), 'time2' => time(), 'workflowid' => $workflow->id]); - } if ($filterdata) { if (is_numeric($filterdata)) { diff --git a/classes/local/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index 2dcc6c02..d13b005f 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -149,8 +149,8 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { $where = 'true'; $inparams = []; if ($type == 'processes') { -// $where .= " AND c.id IN (SELECT courseid FROM {tool_lifecycle_process} WHERE workflowid = :pworkflowid1 UNION -// SELECT courseid FROM {tool_lifecycle_proc_error} WHERE workflowid = :pworkflowid2)"; + $where .= " AND c.id IN (SELECT courseid FROM {tool_lifecycle_process} WHERE workflowid = :pworkflowid1 UNION + SELECT courseid FROM {tool_lifecycle_proc_error} WHERE workflowid = :pworkflowid2)"; $where .= " AND (p.id IS NOT NULL OR pe.id IS NOT NULL) "; $inparams = array_merge($inparams, ['pworkflowid1' => $workflow->id, 'pworkflowid2' => $workflow->id]); } else { @@ -250,7 +250,7 @@ public function build_table() { } if ($row->hasprocess) { if ($row->workflowid) { - if ( ($row->workflowid != $this->workflowid && $this->type == 'used') OR $this->type == 'processes' ) { + if ( ($row->workflowid != $this->workflowid && $this->type == 'used') || $this->type == 'processes' ) { $formattedrow = $this->format_row($row); $this->add_data_keyed($formattedrow, $this->get_row_class($row)); } diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index bb522a2d..6d2e2a15 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -101,8 +101,8 @@ $string['coursesinstep'] = 'Kurse im Schritt \'{$a}\''; $string['coursestriggered'] = 'Getriggerte Kurse für Trigger \'{$a->title}\' ({$a->courses})'; $string['coursestriggeredworkflow'] = 'Getriggerte Kurse für diesen Workflow \'{$a->title}\' ({$a->courses})'; -$string['coursesused'] = 'Kurse schon in einem anderen Prozess für Workflow \'{$a->title}\' ({$a->courses})'; $string['coursesinprocess'] = 'Kurse schon in einem Prozess des Workflows \'{$a->title}\' ({$a->courses})'; +$string['coursesused'] = 'Kurse schon in einem anderen Prozess für Workflow \'{$a->title}\' ({$a->courses})'; $string['create_copy'] = 'Kopie erstellen'; $string['create_step'] = 'Step erstellen'; $string['create_trigger'] = 'Trigger erstellen'; @@ -229,10 +229,10 @@ $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['rolledback'] = 'Zurückgesetzt'; $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'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 15b723e9..6ef8ec78 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -102,8 +102,8 @@ $string['coursesinstep'] = 'Courses in step \'{$a}\''; $string['coursestriggered'] = 'Courses triggered by trigger \'{$a->title}\' ({$a->courses})'; $string['coursestriggeredworkflow'] = 'Courses triggered for this workflow \'{$a->title}\' ({$a->courses})'; -$string['coursesused'] = 'Courses already used in another process for workflow \'{$a->title}\' ({$a->courses})'; $string['coursesinprocess'] = 'Courses already in a process of the workflow \'{$a->title}\' ({$a->courses})'; +$string['coursesused'] = 'Courses already used in another process for workflow \'{$a->title}\' ({$a->courses})'; $string['create_copy'] = 'Create copy'; $string['create_step'] = 'Create step'; $string['create_trigger'] = 'Create trigger'; @@ -229,10 +229,10 @@ $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['rolledback'] = 'Rolled back'; $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'; diff --git a/templates/overview_processeslink.mustache b/templates/overview_processeslink.mustache index dd56ff1c..aa698a2a 100644 --- a/templates/overview_processeslink.mustache +++ b/templates/overview_processeslink.mustache @@ -15,13 +15,13 @@ along with Moodle. If not, see . }} {{! - @template tool_lifecycle/overview_processlink + @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", + "url": "admin/tool/lifecycle/workflowoverview.php?wf=1&processes=4&showdetails=1", "alt": "Active workflow processes and process errors", "processes": "4" } From 02168a5209ea28025bc393acfe94f2f9b6c19765 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 22 Aug 2025 15:32:37 +0200 Subject: [PATCH 119/170] codechecker issues --- lang/de/tool_lifecycle.php | 2 +- lang/en/tool_lifecycle.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 6d2e2a15..0bd4bf37 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -98,10 +98,10 @@ $string['courseselection_title'] = 'Kurs-Selektion'; $string['courseselectionrun_title'] = 'Nächster Selektionslauf'; $string['coursesexcluded'] = 'Ausgeschlossene Kurse für Trigger \'{$a->title}\' ({$a->courses})'; +$string['coursesinprocess'] = 'Kurse schon in einem Prozess des Workflows \'{$a->title}\' ({$a->courses})'; $string['coursesinstep'] = 'Kurse im Schritt \'{$a}\''; $string['coursestriggered'] = 'Getriggerte Kurse für Trigger \'{$a->title}\' ({$a->courses})'; $string['coursestriggeredworkflow'] = 'Getriggerte Kurse für diesen Workflow \'{$a->title}\' ({$a->courses})'; -$string['coursesinprocess'] = 'Kurse schon in einem Prozess des Workflows \'{$a->title}\' ({$a->courses})'; $string['coursesused'] = 'Kurse schon in einem anderen Prozess für Workflow \'{$a->title}\' ({$a->courses})'; $string['create_copy'] = 'Kopie erstellen'; $string['create_step'] = 'Step erstellen'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 6ef8ec78..0b8c20ae 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -99,10 +99,10 @@ $string['courseselection_title'] = 'Course Selection'; $string['courseselectionrun_title'] = 'Course Selection Run'; $string['coursesexcluded'] = 'Courses excluded by trigger \'{$a->title}\' ({$a->courses})'; +$string['coursesinprocess'] = 'Courses already in a process of the workflow \'{$a->title}\' ({$a->courses})'; $string['coursesinstep'] = 'Courses in step \'{$a}\''; $string['coursestriggered'] = 'Courses triggered by trigger \'{$a->title}\' ({$a->courses})'; $string['coursestriggeredworkflow'] = 'Courses triggered for this workflow \'{$a->title}\' ({$a->courses})'; -$string['coursesinprocess'] = 'Courses already in a process of the workflow \'{$a->title}\' ({$a->courses})'; $string['coursesused'] = 'Courses already used in another process for workflow \'{$a->title}\' ({$a->courses})'; $string['create_copy'] = 'Create copy'; $string['create_step'] = 'Create step'; From ace85318225fdcfc4d270baafafefd19359760ac Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 22 Aug 2025 20:10:41 +0200 Subject: [PATCH 120/170] fix unit test --- classes/event/process_rollback.php | 8 ++++---- classes/processor.php | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/classes/event/process_rollback.php b/classes/event/process_rollback.php index 01574650..b4fb520e 100644 --- a/classes/event/process_rollback.php +++ b/classes/event/process_rollback.php @@ -145,17 +145,17 @@ public static function get_other_mapping() { } /** - * Gives back the stepindex of the step to which the current step has to be rolled back, if any. + * Gives back the sortindex of the step to which the current step has to be rolled back, if any. * @param int $workflowid workflow ID - * @param int $stepindex sort order of the step to be rolled back + * @param int $sortindex sort order of the step to be rolled back * @return bool * @throws \dml_exception */ - public static function get_rollbacksortindex($workflowid, $stepindex) { + public static function get_rollbacksortindex($workflowid, $sortindex) { global $DB; $rollbacksortindex = $DB->get_field('tool_lifecycle_step', - 'rollbacksortindex', ['workflowid' => $workflowid, 'stepindex' => $stepindex]); + 'rollbacksortindex', ['workflowid' => $workflowid, 'sortindex' => $sortindex]); return $rollbacksortindex ?? false; } } diff --git a/classes/processor.php b/classes/processor.php index b5067a9c..22db788e 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -182,7 +182,8 @@ public function process_courses() { } else if ($result == step_response::rollback()) { delayed_courses_manager::set_course_delayed_for_workflow($course->id, true, $process->workflowid); - $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, $process->stepindex); + $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, + $process->stepindex); process_manager::rollback_process($process, $rollbacksortindex); break; } else { From 633c11049aca4a57517fb046b1df901083cb3597 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sat, 23 Aug 2025 15:31:44 +0200 Subject: [PATCH 121/170] fix behat tests --- classes/event/process_rollback.php | 14 -------------- classes/local/manager/interaction_manager.php | 3 +-- classes/local/manager/process_manager.php | 12 ++++++++---- classes/local/manager/step_manager.php | 4 ++-- classes/processor.php | 7 ++----- tests/behat/activate_workflow.feature | 2 +- tests/behat/behat_tool_lifecycle.php | 6 +++--- workflowoverview.php | 3 +-- 8 files changed, 18 insertions(+), 33 deletions(-) diff --git a/classes/event/process_rollback.php b/classes/event/process_rollback.php index b4fb520e..bffc8271 100644 --- a/classes/event/process_rollback.php +++ b/classes/event/process_rollback.php @@ -144,18 +144,4 @@ public static function get_other_mapping() { return false; } - /** - * Gives back the sortindex of the step to which the current step has to be rolled back, if any. - * @param int $workflowid workflow ID - * @param int $sortindex sort order of the step to be rolled back - * @return bool - * @throws \dml_exception - */ - public static function get_rollbacksortindex($workflowid, $sortindex) { - global $DB; - - $rollbacksortindex = $DB->get_field('tool_lifecycle_step', - 'rollbacksortindex', ['workflowid' => $workflowid, 'sortindex' => $sortindex]); - return $rollbacksortindex ?? false; - } } diff --git a/classes/local/manager/interaction_manager.php b/classes/local/manager/interaction_manager.php index 1f699cf0..1aef8745 100644 --- a/classes/local/manager/interaction_manager.php +++ b/classes/local/manager/interaction_manager.php @@ -86,8 +86,7 @@ public static function handle_interaction($stepid, $processid, $action) { break; case step_interactive_response::rollback(): delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, $process->workflowid); - $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, $process->stepindex); - process_manager::rollback_process($process, $rollbacksortindex); + process_manager::rollback_process($process); break; } return true; diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index 27e8b170..27f02afb 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -196,12 +196,17 @@ public static function set_process_waiting(&$process) { /** * Currently only removes the current process. * @param process $process process the rollback should be triggered for. - * @param int $tosortindex to which step the process has to rollback. * @throws \coding_exception * @throws \dml_exception */ - public static function rollback_process($process, $tosortindex = 0) { + public static function rollback_process($process) { + global $DB; + process_rollback::event_from_process($process)->trigger(); + 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); @@ -358,7 +363,6 @@ public static function rollback_process_after_error(int $processid) { $DB->delete_records('tool_lifecycle_proc_error', ['id' => $process->id]); delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, $process->workflowid); - $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, $process->stepindex); - self::rollback_process($process, $rollbacksortindex); + self::rollback_process($process); } } diff --git a/classes/local/manager/step_manager.php b/classes/local/manager/step_manager.php index e12b4a4b..bfb67d3b 100644 --- a/classes/local/manager/step_manager.php +++ b/classes/local/manager/step_manager.php @@ -169,8 +169,8 @@ public static function change_sortindex($stepid, $up) { return; } // Prevent reducing step sortindex under the step's target rollback sortindex, if any. - if ($step->rollbacksortindex) { - if (!$up && $step->sortindex == $step->rollbacksortindex + 1) { + if ($step->rollbacktosortindex) { + if (!$up && $step->sortindex == $step->rollbacktosortindex + 1) { return; } diff --git a/classes/processor.php b/classes/processor.php index 22db788e..a07f6fbc 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -182,9 +182,7 @@ public function process_courses() { } else if ($result == step_response::rollback()) { delayed_courses_manager::set_course_delayed_for_workflow($course->id, true, $process->workflowid); - $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, - $process->stepindex); - process_manager::rollback_process($process, $rollbacksortindex); + process_manager::rollback_process($process); break; } else { throw new \moodle_exception('Return code \''. var_dump($result) . '\' is not allowed!'); @@ -228,8 +226,7 @@ public function process_course_interactive($processid) { return $this->process_course_interactive($processid); case step_interactive_response::rollback(): delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, $process->workflowid); - $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, $process->stepindex); - process_manager::rollback_process($process, $rollbacksortindex); + process_manager::rollback_process($process); break; } return true; 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/workflowoverview.php b/workflowoverview.php index 42d20fdf..75d3342f 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -149,8 +149,7 @@ if ($processid) { $process = process_manager::get_process_by_id($processid); if ($action === 'rollback') { - $rollbacksortindex = process_rollback::get_rollbacksortindex($process->workflowid, $process->stepindex); - process_manager::rollback_process($process, $rollbacksortindex); + process_manager::rollback_process($process); delayed_courses_manager::set_course_delayed_for_workflow($process->courseid, true, $workflow); $msg = get_string('courserolledback', 'tool_lifecycle'); } else if ($action === 'proceed') { From 13a9b0f825b09f5fab534b10cb6d61f319e2eadb Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sat, 23 Aug 2025 15:55:17 +0200 Subject: [PATCH 122/170] ci: workflow run without config --- .github/workflows/config.json | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/workflows/config.json diff --git a/.github/workflows/config.json b/.github/workflows/config.json deleted file mode 100644 index 653703ef..00000000 --- a/.github/workflows/config.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "main-moodle": "MOODLE_405_STABLE", - "main-php": "8.3", - "main-db": "pgsql", - "moodle-testmatrix": { - "MOODLE_401_STABLE": { - "php": ["8.0", "8.1"] - }, - "MOODLE_403_STABLE": { - "php": ["8.1", "8.2"] - }, - "MOODLE_404_STABLE": { - "php": ["8.2", "8.3"] - }, - "MOODLE_405_STABLE": { - "php": ["8.1", "8.2", "8.3"] - } - }, - "moodle-plugin-ci": "4.5.6" -} \ No newline at end of file From d5ea54d3897597ea5e86b03b9c2b7ae813413a74 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sat, 23 Aug 2025 16:37:41 +0200 Subject: [PATCH 123/170] version.php, changelog, ci --- .github/workflows/config.json | 33 +++++++++++++++++++++++++++++++++ CHANGES.md | 9 +++++++++ version.php | 4 ++-- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/config.json diff --git a/.github/workflows/config.json b/.github/workflows/config.json new file mode 100644 index 00000000..68bf2ffc --- /dev/null +++ b/.github/workflows/config.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 baff9ed3..35436300 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,15 @@ CHANGELOG ========= +4.5.6 (2025-08-23) +------------------ +* [FEATURE] New lib function multiple_use() makes a trigger type choosable for a single workflow n-times +* [FEATURE] Introduce step option to define individual target step in case of rollback (issue #213) +* [FEATURE] 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) 4.5.5 (2025-07-24) ------------------ diff --git a/version.php b/version.php index 3e850a36..37b30297 100644 --- a/version.php +++ b/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die; $plugin->maturity = MATURITY_STABLE; -$plugin->version = 2025050404.01; +$plugin->version = 2025050405; $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'; From 395a6837695c9cfa2786d0505a0bd94c62ab5ff0 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 25 Aug 2025 15:16:39 +0200 Subject: [PATCH 124/170] very small and unimportant changes --- step/adminapprove/lib.php | 1 + templates/workflowoverview.mustache | 2 +- view.php | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/step/adminapprove/lib.php b/step/adminapprove/lib.php index 3bf648bd..c53fb3b7 100644 --- a/step/adminapprove/lib.php +++ b/step/adminapprove/lib.php @@ -49,6 +49,7 @@ class adminapprove extends libbase { * @param int $instanceid of the step instance. * @param stdClass $course to be processed. * @return step_response + * @throws \dml_exception */ public function process_course($processid, $instanceid, $course) { global $DB; diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index e8e580c0..d362da1d 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", 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 */ From c2dbaa387592d033754c92c26da018e1dda455bd Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 25 Aug 2025 15:53:45 +0200 Subject: [PATCH 125/170] new icons for proc_error_table col tools --- classes/local/table/process_errors_table.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/classes/local/table/process_errors_table.php b/classes/local/table/process_errors_table.php index f1866462..bc0bbc01 100644 --- a/classes/local/table/process_errors_table.php +++ b/classes/local/table/process_errors_table.php @@ -131,20 +131,20 @@ public function col_tools($row) { global $OUTPUT; $actionmenu = new \action_menu(); - $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'] - ) - ); $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']), + new \pix_icon('i/previous', $this->strings['rollback'], ['class' => 'text-secondary rounded']), $this->strings['rollback'] ) ); + $actionmenu->add_primary_action( + new \action_menu_link_primary( + new \moodle_url('', ['action' => 'proceed', 'id[]' => $row->id, 'sesskey' => sesskey()]), + new \pix_icon('i/next', $this->strings['proceed'], 'moodle', ['class' => 'text-success rounded']), + $this->strings['proceed'] + ) + ); return $OUTPUT->render($actionmenu); } From eb1d57ed7d9c78b29e6cfdd2f7f48ed7b6c47145 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 25 Aug 2025 16:24:30 +0200 Subject: [PATCH 126/170] adminapprove button proceed class primary --- step/adminapprove/classes/decision_table.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/step/adminapprove/classes/decision_table.php b/step/adminapprove/classes/decision_table.php index 2b811068..c9703cc5 100644 --- a/step/adminapprove/classes/decision_table.php +++ b/step/adminapprove/classes/decision_table.php @@ -153,14 +153,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; } From 6d1e3db143dceb254779e613fe199e2f99423792 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 25 Aug 2025 16:31:47 +0200 Subject: [PATCH 127/170] adminapprove button proceed class primary 2 --- step/adminapprove/approvestep.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/step/adminapprove/approvestep.php b/step/adminapprove/approvestep.php index c30825bf..69091274 100644 --- a/step/adminapprove/approvestep.php +++ b/step/adminapprove/approvestep.php @@ -167,24 +167,23 @@ $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 html_writer::start_div('singlebutton'); + echo html_writer::tag('button', get_string('proceedselected', 'lifecyclestep_adminapprove'), + ['type' => 'submit', 'name' => 'act', 'value' => PROCEED, 'class' => 'btn btn-primary']); + echo html_writer::end_div() . html_writer::start_div('singlebutton'); } echo ''; echo '
'; - $button = new \single_button(new moodle_url($PAGE->url, ['act' => PROCEED_ALL]), - get_string(PROCEED_ALL, 'lifecyclestep_adminapprove')); - echo $OUTPUT->render($button); - $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'), 'post', \single_button::BUTTON_PRIMARY); + echo $OUTPUT->render($button); echo '
'; $PAGE->requires->js_call_amd('lifecyclestep_adminapprove/init', 'init', [sesskey(), $PAGE->url->out()]); } else { From 2a8b4830f45669f5de7e3650535cf125913d8b69 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Mon, 25 Aug 2025 16:35:48 +0200 Subject: [PATCH 128/170] adminapprove button proceed class primary 3 --- step/adminapprove/approvestep.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/step/adminapprove/approvestep.php b/step/adminapprove/approvestep.php index 69091274..f0c369de 100644 --- a/step/adminapprove/approvestep.php +++ b/step/adminapprove/approvestep.php @@ -167,13 +167,14 @@ $table->out(100, false); if ($table->totalrows) { echo get_string('bulkactions') . ':
'; + echo 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 html_writer::start_div('singlebutton'); echo html_writer::tag('button', get_string('proceedselected', 'lifecyclestep_adminapprove'), ['type' => 'submit', 'name' => 'act', 'value' => PROCEED, 'class' => 'btn btn-primary']); - echo html_writer::end_div() . html_writer::start_div('singlebutton'); + echo html_writer::end_div(); } echo ''; From af1958a6d477e57563178cfb546e60c55193c106 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 26 Aug 2025 03:28:41 +0200 Subject: [PATCH 129/170] Not the same IDs for table process and proc_error --- classes/local/manager/process_manager.php | 15 ++++++++------- classes/local/table/process_errors_table.php | 16 ++++++++-------- step/adminapprove/approvestep.php | 2 +- tests/process_error_test.php | 3 ++- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index 27f02afb..9f8e889b 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -293,17 +293,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; @@ -318,12 +318,12 @@ 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 procerror back into the process board. * @param int $processid the processid * @return void * @throws \dml_exception @@ -332,14 +332,15 @@ public static function proceed_process_after_error(int $processid) { global $DB; $process = $DB->get_record('tool_lifecycle_proc_error', ['id' => $processid]); - // Unset process error only 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]); } /** diff --git a/classes/local/table/process_errors_table.php b/classes/local/table/process_errors_table.php index bc0bbc01..f1866462 100644 --- a/classes/local/table/process_errors_table.php +++ b/classes/local/table/process_errors_table.php @@ -133,17 +133,17 @@ public function col_tools($row) { $actionmenu = new \action_menu(); $actionmenu->add_primary_action( new \action_menu_link_primary( - new \moodle_url('', ['action' => 'rollback', 'id[]' => $row->id, 'sesskey' => sesskey()]), - new \pix_icon('i/previous', $this->strings['rollback'], ['class' => 'text-secondary rounded']), - $this->strings['rollback'] + new \moodle_url('', ['action' => 'proceed', 'id[]' => $row->id, 'sesskey' => sesskey()]), + new \pix_icon('e/tick', $this->strings['proceed']), + $this->strings['proceed'] ) ); $actionmenu->add_primary_action( - new \action_menu_link_primary( - new \moodle_url('', ['action' => 'proceed', 'id[]' => $row->id, 'sesskey' => sesskey()]), - new \pix_icon('i/next', $this->strings['proceed'], 'moodle', ['class' => 'text-success rounded']), - $this->strings['proceed'] - ) + 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'] + ) ); return $OUTPUT->render($actionmenu); } diff --git a/step/adminapprove/approvestep.php b/step/adminapprove/approvestep.php index f0c369de..bdc09dcf 100644 --- a/step/adminapprove/approvestep.php +++ b/step/adminapprove/approvestep.php @@ -183,7 +183,7 @@ 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'), 'post', \single_button::BUTTON_PRIMARY); + get_string(PROCEED_ALL, 'lifecyclestep_adminapprove'), 'post', 'primary'); echo $OUTPUT->render($button); echo '
'; $PAGE->requires->js_call_amd('lifecyclestep_adminapprove/init', 'init', [sesskey(), $PAGE->url->out()]); diff --git a/tests/process_error_test.php b/tests/process_error_test.php index f9052c07..44a9a127 100644 --- a/tests/process_error_test.php +++ b/tests/process_error_test.php @@ -112,6 +112,8 @@ public function test_process_error_in_table(): void { $record = reset($records); $this->assertEquals($this->course->id, $record->courseid); + $this->assertEquals($process->workflowid, $record->workflowid); + $this->assertEquals(1, $record->stepindex); 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', '<')) { @@ -119,7 +121,6 @@ public function test_process_error_in_table(): void { } else { $this->assertStringContainsString("Attempt to read property \"id\" on false", $record->errormessage); } - $this->assertEquals($process->id, $record->id); } } From 7932bee023155545e73179f40d457f1743abcc06 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 26 Aug 2025 11:48:06 +0200 Subject: [PATCH 130/170] lean config.json for developing --- .github/workflows/config.json | 21 ++--------------- .github/workflows/config_dev.json | 18 +++++++++++++++ .github/workflows/config_publish.json | 33 +++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/config_dev.json create mode 100644 .github/workflows/config_publish.json diff --git a/.github/workflows/config.json b/.github/workflows/config.json index 68bf2ffc..a764e5e2 100644 --- a/.github/workflows/config.json +++ b/.github/workflows/config.json @@ -4,29 +4,12 @@ "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" + "8.2" ], "db": [ - "pgsql", - "mariadb", - "mysqli" + "mariadb" ] } } 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" + ] + } + } +} From a92558868aa47e1e33167a9cd302af5ee641ad25 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 27 Aug 2025 06:23:22 +0200 Subject: [PATCH 131/170] display error time in errors table --- classes/local/manager/workflow_manager.php | 1 + classes/local/table/process_errors_table.php | 20 +++++++++++++++++--- errors.php | 4 ++-- lang/de/tool_lifecycle.php | 1 + lang/en/tool_lifecycle.php | 1 + 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/classes/local/manager/workflow_manager.php b/classes/local/manager/workflow_manager.php index 0765c060..c524587b 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]); } } diff --git a/classes/local/table/process_errors_table.php b/classes/local/table/process_errors_table.php index f1866462..d8b951af 100644 --- a/classes/local/table/process_errors_table.php +++ b/classes/local/table/process_errors_table.php @@ -62,8 +62,8 @@ public function __construct($filterdata) { $fields = 'c.id, c.fullname as course, w.title as workflow, s.instancename as step, pe.*'; $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 '; $where = 'TRUE'; $params = []; @@ -84,7 +84,7 @@ public function __construct($filterdata) { } $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', @@ -98,6 +98,7 @@ public function __construct($filterdata) { 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'), ]); @@ -119,6 +120,19 @@ 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) { + return userdate($row->errortimecreated, + get_string('strftimedatetimeshortaccurate', 'core_langconfig')); + } + /** * Render tools column. * diff --git a/errors.php b/errors.php index 0f331b5c..213c8362 100644 --- a/errors.php +++ b/errors.php @@ -92,8 +92,8 @@ // Get number of process errors. $sql = "select count(c.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 {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); diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 0bd4bf37..bb0fa737 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -151,6 +151,7 @@ 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['find_course_list_header'] = 'Kurse finden'; $string['finished'] = 'Fortgeführt/Beendet'; $string['followedby_none'] = 'Keine'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 0b8c20ae..e5b1e792 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -151,6 +151,7 @@ $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['find_course_list_header'] = 'Find courses'; $string['finished'] = 'Proceeded/Finished'; $string['followedby_none'] = 'None'; From 0bcfcb32eeed967631b8e5e712ad13254425ceca Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 28 Aug 2025 11:02:52 +0200 Subject: [PATCH 132/170] provide possibility to delete invalid process error entries --- classes/local/table/process_errors_table.php | 48 +++++++++++++------ .../triggered_courses_table_workflow.php | 24 +++++++--- .../local/table/workflow_definition_table.php | 12 ++--- classes/local/table/workflow_table.php | 1 + errors.php | 8 ++-- lang/de/tool_lifecycle.php | 4 +- lang/en/tool_lifecycle.php | 4 +- step/adminapprove/amd/build/init.min.js | 2 +- step/adminapprove/amd/build/init.min.js.map | 2 +- step/adminapprove/amd/src/init.js | 5 +- step/adminapprove/approvestep.php | 47 +++++++++--------- step/adminapprove/classes/decision_table.php | 21 ++++---- 12 files changed, 106 insertions(+), 72 deletions(-) diff --git a/classes/local/table/process_errors_table.php b/classes/local/table/process_errors_table.php index d8b951af..2150d88f 100644 --- a/classes/local/table/process_errors_table.php +++ b/classes/local/table/process_errors_table.php @@ -57,9 +57,13 @@ public function __construct($filterdata) { $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 ' . 'LEFT JOIN {tool_lifecycle_workflow} w ON pe.workflowid = w.id ' . @@ -145,20 +149,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->id, '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->id, '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); } @@ -167,11 +181,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', @@ -179,9 +196,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 ''; + } } /** diff --git a/classes/local/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index d13b005f..fc33cdcb 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -94,7 +94,7 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { $this->captionattributes = ['class' => 'ml-3']; $columns = ['courseid', 'coursefullname', 'coursecategory']; - if ($type == 'triggeredworkflow' && $this->selectable) { + if ( ($type == 'triggeredworkflow' && $this->selectable) || $type == 'processes' ) { $columns[] = 'tools'; } else if ($type == 'delayed') { $columns[] = 'delayeduntil'; @@ -108,7 +108,7 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { get_string('coursename', 'tool_lifecycle'), get_string('coursecategory', 'moodle'), ]; - if ($type == 'triggeredworkflow' && $this->selectable) { + if ( ($type == 'triggeredworkflow' && $this->selectable) || $type == 'processes' ) { $headers[] = get_string('tools', 'tool_lifecycle'); } else if ($type == 'delayed') { $headers[] = get_string('delayeduntil', 'tool_lifecycle'); @@ -122,6 +122,7 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { c.fullname as coursefullname, c.shortname as courseshortname, cc.name as coursecategory, + pe.id as errorid, COALESCE(p.courseid, pe.courseid, 0) as hasprocess, CASE WHEN COALESCE(p.workflowid, 0) > COALESCE(pe.workflowid, 0) THEN p.workflowid @@ -304,7 +305,6 @@ public function col_delayeduntil($row) { public function col_tools($row) { global $OUTPUT, $PAGE; - $button = ""; if ($this->type == 'delayed') { $params = [ 'action' => 'deletedelay', @@ -314,6 +314,7 @@ public function col_tools($row) { ]; $button = new \single_button(new \moodle_url(urls::WORKFLOW_DETAILS, $params), get_string('delete_delay', 'tool_lifecycle')); + return $OUTPUT->render($button); } else if ($this->type == 'triggeredworkflow' && $this->selectable) { $params = [ 'action' => 'select', @@ -322,12 +323,21 @@ public function col_tools($row) { 'wf' => $this->workflowid, ]; $button = new \single_button(new \moodle_url($PAGE->url, $params), get_string('select')); - } - if ($button) { return $OUTPUT->render($button); - } else { - return ''; + } else if ($this->type == 'processes') { + if ($row->errorid){ + $params = [ + 'workflow' => $this->workflowid, + 'course' => $row->courseid, + ]; + return \html_writer::link( + new \moodle_url(urls::PROCESS_ERRORS, $params), + get_string('error'), + ['class' => 'error']); + } } + + return ''; } /** diff --git a/classes/local/table/workflow_definition_table.php b/classes/local/table/workflow_definition_table.php index 2d712c95..baf61a53 100644 --- a/classes/local/table/workflow_definition_table.php +++ b/classes/local/table/workflow_definition_table.php @@ -51,7 +51,7 @@ class workflow_definition_table extends workflow_table { public function __construct($uniqueid) { parent::__construct($uniqueid); global $PAGE; - $this->set_sql("id, title, timeactive, displaytitle", + $this->set_sql("id, title, displaytitle", '{tool_lifecycle_workflow}', "timeactive IS NULL AND timedeactive IS NULL"); $this->define_baseurl($PAGE->url); @@ -63,10 +63,11 @@ public function __construct($uniqueid) { * Initialize the table. */ public function init() { - $this->define_columns(['title', 'timeactive', 'tools']); + $this->define_columns(['title', 'trigger', 'activate', 'tools']); $this->define_headers([ get_string('workflow_title', 'tool_lifecycle'), - get_string('workflow_timeactive', 'tool_lifecycle'), + get_string('trigger', 'tool_lifecycle'), + get_string('activateworkflow', 'tool_lifecycle'), get_string('workflow_tools', 'tool_lifecycle'), ]); $this->sortable(false, 'title'); @@ -80,11 +81,8 @@ public function init() { * @throws \coding_exception * @throws \moodle_exception */ - public function col_timeactive($row) { + public function col_activate($row) { global $OUTPUT; - if ($row->timeactive) { - return userdate($row->timeactive, get_string('strftimedatetime'), 0); - } if (workflow_manager::is_valid($row->id)) { return $OUTPUT->single_button(new \moodle_url(urls::ACTIVE_WORKFLOWS, ['action' => action::WORKFLOW_ACTIVATE, diff --git a/classes/local/table/workflow_table.php b/classes/local/table/workflow_table.php index 1763c0a5..7e558053 100644 --- a/classes/local/table/workflow_table.php +++ b/classes/local/table/workflow_table.php @@ -98,6 +98,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; diff --git a/errors.php b/errors.php index 213c8362..04cc5609 100644 --- a/errors.php +++ b/errors.php @@ -52,10 +52,12 @@ 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'); diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index bb0fa737..c72db438 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -127,6 +127,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ß'; @@ -169,7 +171,7 @@ $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.'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index e5b1e792..c82171d8 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -128,6 +128,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'; @@ -169,7 +171,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'; 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 bdc09dcf..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); @@ -160,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('rollbackselected', 'lifecyclestep_adminapprove'), - ['type' => 'submit', 'name' => 'act', 'value' => ROLLBACK, 'class' => 'btn btn-secondary']); - echo html_writer::end_div(); - 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-primary']); - 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')); + 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'), 'post', 'primary'); echo $OUTPUT->render($button); + $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 c9703cc5..2844c6e4 100644 --- a/step/adminapprove/classes/decision_table.php +++ b/step/adminapprove/classes/decision_table.php @@ -34,17 +34,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 +44,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 +77,18 @@ 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); } + /** + * This function is not part of the public api. + */ + public function print_row($row, $classname = '') { + echo $this->get_row_html($row, $classname); + } + /** * Column of checkboxes. * @param object $row From 73b350cab07dfa4edaec9c77924231857e17d36b Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 28 Aug 2025 11:17:24 +0200 Subject: [PATCH 133/170] codechecker issues --- classes/local/table/triggered_courses_table_workflow.php | 2 +- step/adminapprove/classes/decision_table.php | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/classes/local/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index fc33cdcb..3946d5ec 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -325,7 +325,7 @@ public function col_tools($row) { $button = new \single_button(new \moodle_url($PAGE->url, $params), get_string('select')); return $OUTPUT->render($button); } else if ($this->type == 'processes') { - if ($row->errorid){ + if ($row->errorid) { $params = [ 'workflow' => $this->workflowid, 'course' => $row->courseid, diff --git a/step/adminapprove/classes/decision_table.php b/step/adminapprove/classes/decision_table.php index 2844c6e4..f509c887 100644 --- a/step/adminapprove/classes/decision_table.php +++ b/step/adminapprove/classes/decision_table.php @@ -81,14 +81,6 @@ public function __construct($stepid, $courseid, $category, $coursename) { $this->set_sql($fields, $from, $where, $params); } - - /** - * This function is not part of the public api. - */ - public function print_row($row, $classname = '') { - echo $this->get_row_html($row, $classname); - } - /** * Column of checkboxes. * @param object $row From e7fa54ea51793df6dfbddd521442b71e5d472073 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 29 Aug 2025 02:20:58 +0200 Subject: [PATCH 134/170] try to fix behat tests --- classes/local/table/workflow_table.php | 50 ++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/classes/local/table/workflow_table.php b/classes/local/table/workflow_table.php index 7e558053..c837c5bb 100644 --- a/classes/local/table/workflow_table.php +++ b/classes/local/table/workflow_table.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\table; +use html_writer; use tool_lifecycle\action; use tool_lifecycle\local\manager\process_manager; use tool_lifecycle\local\manager\trigger_manager; @@ -162,4 +163,53 @@ protected function format_icon_link($action, $workflowid, $icon, $alt) { null , ['title' => $alt]) . ' '; } + /** + * This function is not part of the public api. + */ + 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; + } } From 35b9f94d9cf6e2ab8114c386bf0ee4dd244fa34e Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 29 Aug 2025 02:37:59 +0200 Subject: [PATCH 135/170] codechecker issue --- classes/local/table/workflow_table.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/classes/local/table/workflow_table.php b/classes/local/table/workflow_table.php index c837c5bb..3c313d8e 100644 --- a/classes/local/table/workflow_table.php +++ b/classes/local/table/workflow_table.php @@ -165,6 +165,9 @@ protected function format_icon_link($action, $workflowid, $icon, $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); From 14732984b94ea0e9840739afba2e3e14a58fdccc Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sat, 30 Aug 2025 03:57:51 +0200 Subject: [PATCH 136/170] revert buggy last changes to workflow definitions table and dont show triggers there --- classes/local/table/workflow_definition_table.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/classes/local/table/workflow_definition_table.php b/classes/local/table/workflow_definition_table.php index baf61a53..2d712c95 100644 --- a/classes/local/table/workflow_definition_table.php +++ b/classes/local/table/workflow_definition_table.php @@ -51,7 +51,7 @@ class workflow_definition_table extends workflow_table { public function __construct($uniqueid) { parent::__construct($uniqueid); global $PAGE; - $this->set_sql("id, title, displaytitle", + $this->set_sql("id, title, timeactive, displaytitle", '{tool_lifecycle_workflow}', "timeactive IS NULL AND timedeactive IS NULL"); $this->define_baseurl($PAGE->url); @@ -63,11 +63,10 @@ public function __construct($uniqueid) { * Initialize the table. */ public function init() { - $this->define_columns(['title', 'trigger', 'activate', 'tools']); + $this->define_columns(['title', 'timeactive', 'tools']); $this->define_headers([ get_string('workflow_title', 'tool_lifecycle'), - get_string('trigger', 'tool_lifecycle'), - get_string('activateworkflow', 'tool_lifecycle'), + get_string('workflow_timeactive', 'tool_lifecycle'), get_string('workflow_tools', 'tool_lifecycle'), ]); $this->sortable(false, 'title'); @@ -81,8 +80,11 @@ public function init() { * @throws \coding_exception * @throws \moodle_exception */ - public function col_activate($row) { + public function col_timeactive($row) { global $OUTPUT; + if ($row->timeactive) { + return userdate($row->timeactive, get_string('strftimedatetime'), 0); + } if (workflow_manager::is_valid($row->id)) { return $OUTPUT->single_button(new \moodle_url(urls::ACTIVE_WORKFLOWS, ['action' => action::WORKFLOW_ACTIVATE, From 0b103047d9a6b659d9a396872c8604a276f55bf8 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 5 Sep 2025 05:44:44 +0200 Subject: [PATCH 137/170] trace course processing as well, categories trigger: improve cat listing in frozen edit form --- classes/processor.php | 26 ++++++++++++++++- trigger/categories/lib.php | 60 +++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index a07f6fbc..7717b865 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -60,7 +60,6 @@ public function call_trigger() { $run = str_contains($FULLSCRIPT, 'run.php'); $activeworkflows = workflow_manager::get_active_automatic_workflows(); - $exclude = []; if (!defined('BEHAT_SITE_RUNNING')) { if ($run) { @@ -76,6 +75,7 @@ public function call_trigger() { $countcourses = 0; $counttriggered = 0; $countexcluded = 0; + $exclude = []; if (!defined('BEHAT_SITE_RUNNING')) { if ($run) { @@ -139,6 +139,19 @@ 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; + + $run = str_contains($FULLSCRIPT, 'run.php'); + 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) { while (true) { @@ -166,8 +179,10 @@ public function process_courses() { } else { $result = $lib->process_course($process->id, $step->id, $course); } + $coursesprocessed++; } catch (\Exception $e) { process_manager::insert_process_error($process, $e); + $coursesprocesserrors++; break; } if ($result == step_response::waiting()) { @@ -189,6 +204,15 @@ public function process_courses() { } } } + 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')."."); + } + } } diff --git a/trigger/categories/lib.php b/trigger/categories/lib.php index eac88e4a..706a87d4 100644 --- a/trigger/categories/lib.php +++ b/trigger/categories/lib.php @@ -119,18 +119,56 @@ 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 badge-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); + } } /** From a233dade0bf5e28f2d5a002629ed6064a51223cb Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 5 Sep 2025 14:31:24 +0200 Subject: [PATCH 138/170] list every course selection and process with run.php in debug mode --- classes/processor.php | 26 ++++++++++++++++++++++--- templates/overview_step.mustache | 2 +- templates/overview_timetrigger.mustache | 2 +- templates/overview_trigger.mustache | 2 +- workflowoverview.php | 8 ++++++-- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/classes/processor.php b/classes/processor.php index 7717b865..0d50155a 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -55,9 +55,10 @@ class processor { * Processes the trigger plugins for all relevant courses. */ public function call_trigger() { - global $FULLSCRIPT; + global $FULLSCRIPT, $CFG; $run = str_contains($FULLSCRIPT, 'run.php'); + $debug = $run && $CFG->debugdeveloper; $activeworkflows = workflow_manager::get_active_automatic_workflows(); @@ -102,12 +103,18 @@ public function call_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; } @@ -119,6 +126,9 @@ public function call_trigger() { $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(); } if (!defined('BEHAT_SITE_RUNNING')) { @@ -139,9 +149,10 @@ 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; + global $FULLSCRIPT, $CFG; $run = str_contains($FULLSCRIPT, 'run.php'); + $debug = $run && $CFG->debugdeveloper; if (!defined('BEHAT_SITE_RUNNING')) { if ($run) { echo \html_writer::div(get_string ('lifecycle_task', 'tool_lifecycle')); @@ -187,17 +198,26 @@ public function process_courses() { } 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, $process->workflowid); - break; } + 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, $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!'); diff --git a/templates/overview_step.mustache b/templates/overview_step.mustache index f2639733..12fae71b 100644 --- a/templates/overview_step.mustache +++ b/templates/overview_step.mustache @@ -39,7 +39,7 @@
- {{#shortentext}} 50, {{instancename}} {{/shortentext}} + {{#shortentext}} 50, {{instancename}} {{/shortentext}} {{#rollbacktosortindex}} {{rollbacktosortindex}} diff --git a/templates/overview_timetrigger.mustache b/templates/overview_timetrigger.mustache index 84f9ec3b..b09fcc19 100644 --- a/templates/overview_timetrigger.mustache +++ b/templates/overview_timetrigger.mustache @@ -34,7 +34,7 @@
diff --git a/templates/overview_trigger.mustache b/templates/overview_trigger.mustache index 9a942405..692701b3 100644 --- a/templates/overview_trigger.mustache +++ b/templates/overview_trigger.mustache @@ -39,7 +39,7 @@
diff --git a/workflowoverview.php b/workflowoverview.php index 75d3342f..2fe9038e 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -255,9 +255,11 @@ // 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) { @@ -334,9 +336,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) { From 97a71f3e95318d75a0c84dd67acbf53a5d7ead1f Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 7 Sep 2025 08:04:01 +0200 Subject: [PATCH 139/170] fix error with remaining courses in interaction table --- classes/local/table/interaction_table.php | 7 ++++--- classes/processor.php | 5 +++-- classes/view_controller.php | 6 +++--- lib.php | 6 ++---- trigger/categories/lib.php | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/classes/local/table/interaction_table.php b/classes/local/table/interaction_table.php index bc55c65c..9c169049 100644 --- a/classes/local/table/interaction_table.php +++ b/classes/local/table/interaction_table.php @@ -131,19 +131,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.'111'; + $categoryname = $category->name; } else { $category = $this->coursecategories[end($categoryhierachy)]; - return $category->name.'222'.$row->categorypath; + $categoryname = $category->name; } } + return \html_writer::div($categoryname, "badge text-bg-secondary"); } /** diff --git a/classes/processor.php b/classes/processor.php index 0d50155a..043671e9 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -58,7 +58,7 @@ public function call_trigger() { global $FULLSCRIPT, $CFG; $run = str_contains($FULLSCRIPT, 'run.php'); - $debug = $run && $CFG->debugdeveloper; + $debug = $run && $CFG->debugdeveloper && !defined('BEHAT_SITE_RUNNING'); $activeworkflows = workflow_manager::get_active_automatic_workflows(); @@ -152,7 +152,8 @@ public function process_courses() { global $FULLSCRIPT, $CFG; $run = str_contains($FULLSCRIPT, 'run.php'); - $debug = $run && $CFG->debugdeveloper; + $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')); diff --git a/classes/view_controller.php b/classes/view_controller.php index 1f93a580..e628d498 100644 --- a/classes/view_controller.php +++ b/classes/view_controller.php @@ -75,16 +75,16 @@ public function handle_view($renderer, $filterdata) { $processes = $DB->get_recordset_sql($sql, $inparams); $requiresinteraction = []; - $remainingcourses = $inparams; + $remainingcourses = []; foreach ($processes as $process) { $step = step_manager::get_step_instance($process->stepinstanceid); $capability = interaction_manager::get_relevant_capability($step->subpluginname); - 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]); + } else { + $remainingcourses[] = $process->courseid; } } diff --git a/lib.php b/lib.php index b742b32a..5a8c1088 100644 --- a/lib.php +++ b/lib.php @@ -48,12 +48,10 @@ 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; } diff --git a/trigger/categories/lib.php b/trigger/categories/lib.php index 706a87d4..d7d4a555 100644 --- a/trigger/categories/lib.php +++ b/trigger/categories/lib.php @@ -154,7 +154,7 @@ public function extend_add_instance_form_definition_after_data($mform, $settings $categorieshtml = ""; foreach ($categories as $key => $value) { if (in_array($key, $triggercategories)) { - $categorieshtml .= \html_writer::div($value, "badge badge-secondary mr-1"); + $categorieshtml .= \html_writer::div($value, "badge text-bg-secondary mr-1"); } } $mform->insertElementBefore($mform->createElement( From 2ce9840c72b9ae30c0d5c13b735d29570e79c55f Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 10 Sep 2025 01:11:30 +0200 Subject: [PATCH 140/170] small fixes regarding manual trigger --- .../table/triggered_courses_table_trigger.php | 3 +- .../triggered_courses_table_workflow.php | 2 +- classes/processor.php | 38 ++++++++++--------- lang/de/tool_lifecycle.php | 12 +++--- lang/en/tool_lifecycle.php | 6 +-- step/email/interactionlib.php | 8 ++-- .../tests/lifecycletrigger_byrole_test.php | 8 ++-- trigger/categories/tests/trigger_test.php | 12 +++--- .../customfielddelay/tests/trigger_test.php | 6 +-- trigger/semindependent/tests/trigger_test.php | 4 +- trigger/startdatedelay/tests/trigger_test.php | 4 +- 11 files changed, 53 insertions(+), 50 deletions(-) diff --git a/classes/local/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index d9ead637..ae1ecb3e 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -128,7 +128,8 @@ public function __construct($numbercourses, $trigger, $type, $filterdata = '') { 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_delayed} d ON c.id = d.courseid - LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid "; + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid + AND dw.workflowid = $workflow->id "; if (!$workflow->includesitecourse) { $where .= " AND c.id <> 1 "; diff --git a/classes/local/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index 3946d5ec..71d1f64d 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -141,7 +141,7 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { 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_delayed} d ON c.id = d.courseid - LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid "; + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid AND dw.workflowid = $workflow->id "; if ($type == 'used') { $from .= " LEFT JOIN {tool_lifecycle_workflow} wfp ON p.workflowid = wfp.id LEFT JOIN {tool_lifecycle_workflow} wfpe ON pe.workflowid = wfpe.id"; diff --git a/classes/processor.php b/classes/processor.php index 043671e9..bb6ef8c8 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -95,7 +95,7 @@ public function call_trigger() { $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, $exclude); + $recordset = $this->get_course_recordset($triggers, (bool)$exclude); while ($recordset->valid()) { $course = $recordset->current(); $countcourses++; @@ -283,15 +283,16 @@ 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 $exclude Are the site course and/or the delayed courses 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) { + public function get_course_recordset($triggers, $exclude = false, $forcounting = false) { global $DB, $SESSION; + $where = " TRUE "; $whereparams = []; $workflowid = false; foreach ($triggers as $trigger) { @@ -311,10 +312,8 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) } if ($forcounting) { - if ($exclude) { - if (!$workflow->includesitecourse) { - $where = "($where) AND c.id <> 1 "; - } + if ($exclude) { // We include delayed courses here anyway, so we only take the site course into account. + $where = "($where) AND c.id <> 1 "; } // Get course hasprocess and delay with the sql. $sql = "SELECT c.id, @@ -333,8 +332,11 @@ public function get_course_recordset($triggers, $exclude, $forcounting = false) 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_delayed} d ON c.id = d.courseid - LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid - WHERE " . $where; + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid "; + if ($workflowid) { + $sql .= " AND dw.workflowid = $workflowid "; + } + $sql .= " WHERE $where "; } else { if ($exclude) { if (!$workflow->includesitecourse) { @@ -446,16 +448,16 @@ public function get_triggercourses_forcounting_check_course($trigger, $excluded) if ($excluded) { $workflow = workflow_manager::get_workflow($trigger->workflowid); // Exclude delayed courses and sitecourse according to the workflow settings. - $sitecourse = $workflow->includesitecourse ? [] : [1]; + $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()); } - $excludedcourses = array_merge($sitecourse, $delayedcourses); + $excludedcourses = array_merge($excludesitecourse, $delayedcourses); } $triggercoursesall = []; - $recordset = $this->get_course_recordset([$trigger], $excludedcourses, true); + $recordset = $this->get_course_recordset([$trigger], (bool)$excludesitecourse, true); while ($recordset->valid()) { $course = $recordset->current(); $response = $lib->check_course($course, $trigger->id); @@ -492,14 +494,14 @@ public function get_triggercourses_forcounting_check_course($trigger, $excluded) public function get_count_of_courses_to_trigger_for_workflow($workflow) { // Exclude delayed courses and sitecourse according to the workflow settings. - $sitecourse = $workflow->includesitecourse ? [] : [1]; + $excludesitecourse = $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); + $excludedcourses = array_merge($excludesitecourse, $delayedcourses); $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); $amounts = []; @@ -513,12 +515,12 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $trigger->exclude = $settings['exclude'] ?? false; $obj = new \stdClass(); $lib = lib_manager::get_trigger_lib($trigger->subpluginname); - if (!$checkcoursecode) { - $checkcoursecode = $lib->check_course_code(); - } if ($lib->is_manual_trigger()) { $obj->automatic = false; } else { + if (!$checkcoursecode) { + $checkcoursecode = $lib->check_course_code(); + } $obj->automatic = true; $obj->triggered = 0; $obj->excluded = false; @@ -572,7 +574,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $amounts[$trigger->sortindex] = $obj; } - $recordset = $this->get_course_recordset($autotriggers, $excludedcourses, true); + $recordset = $this->get_course_recordset($autotriggers, (bool)$excludesitecourse, true); $coursestriggered = 0; $usedcourses = 0; diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index c72db438..24e8c777 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -49,7 +49,7 @@ $string['adminsettings_notriggers'] = 'Keine Trigger-Subplugins installiert'; $string['adminsettings_workflow_definition_steps_heading'] = 'Workflowschritte'; $string['all_delays'] = 'Alle Verzögerungen'; -$string['andor'] = 'Trigger-Verknüpfungstyp'; +$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'; @@ -166,7 +166,7 @@ $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'; @@ -193,7 +193,7 @@ $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! @@ -280,11 +280,11 @@ $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.'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index c82171d8..e5d7ad27 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -49,7 +49,7 @@ $string['adminsettings_notriggers'] = 'No trigger subplugins installed'; $string['adminsettings_workflow_definition_steps_heading'] = 'Workflow steps'; $string['all_delays'] = 'All delays'; -$string['andor'] = 'Combine triggers'; +$string['andor'] = 'Combine triggers (experimental)'; $string['andor_help'] = 'Combine triggers by conjunction(AND) or disjunction(OR).'; $string['anonymous_user'] = 'Anonymous User'; $string['apply'] = 'Apply'; @@ -194,7 +194,7 @@ $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! @@ -280,7 +280,7 @@ $string['triggertype_settings_header'] = 'Specific settings of the trigger type'; $string['type'] = 'Type'; $string['upload_workflow'] = 'Upload workflow'; -$string['viewheading'] = 'Manage courses'; +$string['viewheading'] = 'Lifecycle - Manage courses'; $string['viewsteps'] = 'View workflow steps'; $string['workflow'] = 'Workflow'; $string['workflow_active'] = 'Active'; 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/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/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/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/semindependent/tests/trigger_test.php b/trigger/semindependent/tests/trigger_test.php index 7a679e93..1a94799e 100644 --- a/trigger/semindependent/tests/trigger_test.php +++ b/trigger/semindependent/tests/trigger_test.php @@ -68,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) { @@ -93,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/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) { From bfb9b6c5fb6ef020ebc1f7d3c533f3e6fda7760f Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 10 Sep 2025 02:02:20 +0200 Subject: [PATCH 141/170] codechecker issue --- classes/local/table/triggered_courses_table_trigger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/local/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index ae1ecb3e..56258fab 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -129,7 +129,7 @@ public function __construct($numbercourses, $trigger, $type, $filterdata = '') { LEFT JOIN {tool_lifecycle_proc_error} pe ON c.id = pe.courseid 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 "; + AND dw.workflowid = $workflow->id"; if (!$workflow->includesitecourse) { $where .= " AND c.id <> 1 "; From 3b3fc4ee2df616a157959340b914f99e89b34099 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 10 Sep 2025 02:09:40 +0200 Subject: [PATCH 142/170] codechecker issue 2 --- classes/local/table/triggered_courses_table_trigger.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/local/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index 56258fab..45751fc9 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -128,8 +128,8 @@ public function __construct($numbercourses, $trigger, $type, $filterdata = '') { 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_delayed} d ON c.id = d.courseid - LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid - AND dw.workflowid = $workflow->id"; + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid + AND dw.workflowid = $workflow->id "; if (!$workflow->includesitecourse) { $where .= " AND c.id <> 1 "; From 6d22e26aed1ed9518f5c80a7457aa8db32a729d4 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Wed, 10 Sep 2025 03:32:16 +0200 Subject: [PATCH 143/170] manual workflow:reintroduce remaining courses again --- classes/view_controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/classes/view_controller.php b/classes/view_controller.php index e628d498..1f93a580 100644 --- a/classes/view_controller.php +++ b/classes/view_controller.php @@ -75,16 +75,16 @@ public function handle_view($renderer, $filterdata) { $processes = $DB->get_recordset_sql($sql, $inparams); $requiresinteraction = []; - $remainingcourses = []; + $remainingcourses = $inparams; foreach ($processes as $process) { $step = step_manager::get_step_instance($process->stepinstanceid); $capability = interaction_manager::get_relevant_capability($step->subpluginname); + 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; - } else { - $remainingcourses[] = $process->courseid; + unset($remainingcourses[$process->courseid]); } } From 4d8edaca839ddd754537943116878c4a110a6d14 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 11 Sep 2025 08:33:10 +0200 Subject: [PATCH 144/170] fix error when aborting processes after course deletion --- classes/local/entity/process.php | 11 +++++--- classes/local/manager/process_manager.php | 27 +++++++++++++++---- .../table/interaction_remaining_table.php | 4 +-- classes/view_controller.php | 2 +- tests/behat/interaction.feature | 17 +++++++----- 5 files changed, 43 insertions(+), 18 deletions(-) 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/manager/process_manager.php b/classes/local/manager/process_manager.php index 9f8e889b..92a60a0b 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -158,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 @@ -242,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; @@ -274,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); + } } } diff --git a/classes/local/table/interaction_remaining_table.php b/classes/local/table/interaction_remaining_table.php index d2ba5423..8ffb88ef 100644 --- a/classes/local/table/interaction_remaining_table.php +++ b/classes/local/table/interaction_remaining_table.php @@ -124,10 +124,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) { 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/tests/behat/interaction.feature b/tests/behat/interaction.feature index 721be5e2..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" @@ -83,7 +83,10 @@ Feature: Add a workflow with an email step and test the interaction as a teacher 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" From f9d9cd535baea06e5587347140b203bb953199a4 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 12 Sep 2025 03:52:47 +0200 Subject: [PATCH 145/170] interaction behat tests: increase waiting time when executing task --- tests/behat/interaction.feature | 2 -- tests/behat/manual_trigger.feature | 9 ++++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/behat/interaction.feature b/tests/behat/interaction.feature index 0fb15324..f7738be2 100644 --- a/tests/behat/interaction.feature +++ b/tests/behat/interaction.feature @@ -83,8 +83,6 @@ Feature: Add a workflow with an email step and test the interaction as a teacher 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" 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" From ddfc06dfac0d6087e4da5c05a13acc2a17902a66 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 12 Sep 2025 04:07:56 +0200 Subject: [PATCH 146/170] interaction behat tests: increase waiting time when executing task 2 --- tests/behat/interaction.feature | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/behat/interaction.feature b/tests/behat/interaction.feature index f7738be2..0fb15324 100644 --- a/tests/behat/interaction.feature +++ b/tests/behat/interaction.feature @@ -83,6 +83,8 @@ Feature: Add a workflow with an email step and test the interaction as a teacher 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" From 5d7719173cc392905cc0407e1c61220c0c18524d Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 12 Sep 2025 05:26:20 +0200 Subject: [PATCH 147/170] update changes.md --- CHANGES.md | 26 ++++++++++++++------------ lang/de/tool_lifecycle.php | 2 +- lang/en/tool_lifecycle.php | 4 ++-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 35436300..812a16d6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,30 +1,32 @@ CHANGELOG ========= -4.5.6 (2025-08-23) +4.5.5 (2025-09-14) ------------------ +* [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 step option to define individual target step in case of rollback (issue #213) -* [FEATURE] Trigger selection sql: conjunction(AND) and disjunction(OR) now possible (workflow option) +* [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) - -4.5.5 (2025-07-24) ------------------- -* [FEATURE] Display trigger countings 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 +* [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 +* [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 +* [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 test interaction.feature +* [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 diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 24e8c777..be2b5255 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -179,7 +179,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.'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index e5d7ad27..e4e39d59 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -180,7 +180,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.'; @@ -280,7 +280,7 @@ $string['triggertype_settings_header'] = 'Specific settings of the trigger type'; $string['type'] = 'Type'; $string['upload_workflow'] = 'Upload workflow'; -$string['viewheading'] = 'Lifecycle - Manage courses'; +$string['viewheading'] = 'Manage courses'; $string['viewsteps'] = 'View workflow steps'; $string['workflow'] = 'Workflow'; $string['workflow_active'] = 'Active'; From 3b7a0befeec6e3c74b2ad4dc2852c00a6dab152b Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 12 Sep 2025 05:42:06 +0200 Subject: [PATCH 148/170] github workflow: change to the version of config.json for publishing --- .github/workflows/config.json | 21 +++++++++++++++++++-- CHANGES.md | 3 +++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/config.json b/.github/workflows/config.json index a764e5e2..71a177e2 100644 --- a/.github/workflows/config.json +++ b/.github/workflows/config.json @@ -4,12 +4,29 @@ "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.2" + "8.1", + "8.2", + "8.3" ], "db": [ - "mariadb" + "pgsql", + "mariadb", + "mysqli" ] } } diff --git a/CHANGES.md b/CHANGES.md index 812a16d6..1d6fc530 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ CHANGELOG ========= +Further information to the 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-09-14) ------------------ * [FIXED] Workflowoverview Course selection: show run-link only when workflow is active From 51e93de61181a8469685c690e07cee0cf64d0aa9 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 12 Sep 2025 09:09:53 +0200 Subject: [PATCH 149/170] fix unit test PR #230 --- CHANGES.md | 1 + tests/process_error_test.php | 8 +------- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1d6fc530..ce5e0b19 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ and here: https://github.com/learnweb/moodle-tool_lifecycle/wiki/Changes-of-vers 4.5.5 (2025-09-14) ------------------ +* [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 diff --git a/tests/process_error_test.php b/tests/process_error_test.php index 44a9a127..4238155a 100644 --- a/tests/process_error_test.php +++ b/tests/process_error_test.php @@ -114,13 +114,7 @@ public function test_process_error_in_table(): void { $this->assertEquals($this->course->id, $record->courseid); $this->assertEquals($process->workflowid, $record->workflowid); $this->assertEquals(1, $record->stepindex); - 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->assertNotEmpty($record->errormessage); } } From 2d217bfee3539d6d9a635871ab3a19dc364e5ce1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Sep 2025 09:44:54 +0200 Subject: [PATCH 150/170] Sync with Learnweb's centralized pull request template (#257) Co-authored-by: github-actions[bot] --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a791fde1d2b286d80840ce257d3a96b74f8db59b Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 12 Sep 2025 09:56:42 +0200 Subject: [PATCH 151/170] a lack of capability is now the only reason not to render the manage courses sec nav link issue #198 --- lib.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib.php b/lib.php index 5a8c1088..1cc6c697 100644 --- a/lib.php +++ b/lib.php @@ -52,9 +52,6 @@ function tool_lifecycle_extend_navigation_course($navigation, $course, $context) " 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, From 5793ed671fc917d20265b4fac6b109cfe40ded9f Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 14 Sep 2025 05:13:29 +0200 Subject: [PATCH 152/170] update changes.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index ce5e0b19..e90c47b4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ and here: https://github.com/learnweb/moodle-tool_lifecycle/wiki/Changes-of-vers 4.5.5 (2025-09-14) ------------------ +* [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 From fd6345546014c086d38f64269fdb1e3fa70721bb Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 16 Sep 2025 11:49:09 +0200 Subject: [PATCH 153/170] edit page: make field elementname longer --- classes/local/form/form_step_instance.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/local/form/form_step_instance.php b/classes/local/form/form_step_instance.php index f839b883..0543b54f 100644 --- a/classes/local/form/form_step_instance.php +++ b/classes/local/form/form_step_instance.php @@ -116,7 +116,7 @@ public function definition() { $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')); + $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'); From 6d70352a1fd9bd2e369c1d51d62e3117af810322 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 16 Sep 2025 12:19:57 +0200 Subject: [PATCH 154/170] remove unused process_courses_table --- classes/local/table/process_courses_table.php | 138 ------------------ workflowoverview.php | 1 - 2 files changed, 139 deletions(-) delete mode 100644 classes/local/table/process_courses_table.php diff --git a/classes/local/table/process_courses_table.php b/classes/local/table/process_courses_table.php deleted file mode 100644 index d94f9670..00000000 --- a/classes/local/table/process_courses_table.php +++ /dev/null @@ -1,138 +0,0 @@ -. - -/** - * Table listing all courses of this workflow in an active process or with a process error. - * - * @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; - -defined('MOODLE_INTERNAL') || die; - -require_once($CFG->libdir . '/tablelib.php'); - -/** - * Table listing all workflow courses in an active process or with a process error. - * - * @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 process_courses_table extends \table_sql { - - /** @var string $type of the courses list */ - private $type; - - /** @var int $workflowid Id of the workflow */ - private $workflowid; - - /** - * Builds a table of courses. - * @param array $courseids of the courses to list - * @param string $workflowname - * @param null $workflowid - * @param string $filterdata optional, term to filter the table by course id or -name - * @throws \coding_exception - * @throws \dml_exception - */ - public function __construct($courseids, $workflowname = '', $workflowid = null, $filterdata = '') { - parent::__construct('tool_lifecycle-workflow-courses-in-process'); - global $DB, $PAGE; - - if (!$courseids) { - return; - } - - $this->define_baseurl($PAGE->url); - $this->caption = get_string('workflow_processesanderrors', 'tool_lifecycle')." '".$workflowname."' (".count($courseids).")"; - $this->workflowid = $workflowid; - $this->captionattributes = ['class' => 'ml-3']; - $columns = ['courseid', 'coursefullname', 'coursecategory', 'step', 'error']; - $this->define_columns($columns); - $headers = [ - get_string('courseid', 'tool_lifecycle'), - get_string('coursename', 'tool_lifecycle'), - get_string('coursecategory', 'moodle'), - get_string('step', 'tool_lifecycle'), - get_string('error'), - ]; - $this->define_headers($headers); - - $fields = "c.id as courseid, c.fullname as coursefullname, c.shortname as courseshortname, - cc.name as coursecategory, s.instancename as step, pe.errormessage as errormessage, pe.errortrace as errortrace"; - $from = " - {course} c - JOIN {course_categories} cc ON c.category = cc.id - LEFT JOIN {tool_lifecycle_process} p ON p.courseid = c.id AND p.workflowid = $workflowid - LEFT JOIN {tool_lifecycle_proc_error} pe ON pe.courseid = c.id AND pe.workflowid = $workflowid - JOIN {tool_lifecycle_step} s ON (p.workflowid = s.workflowid AND p.stepindex = s.sortindex) - OR (pe.workflowid = s.workflowid AND pe.stepindex = s.sortindex) - "; - [$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); - } - - /** - * 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 error column. - * - * @param object $row Row data. - * @return string error cell - */ - public function col_error($row) { - if ($row->errormessage) { - return "
" . - nl2br(htmlentities($row->errormessage, ENT_COMPAT)) . - "" . - nl2br(htmlentities($row->errortrace, ENT_COMPAT)) . - "
"; - } 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/workflowoverview.php b/workflowoverview.php index 2fe9038e..a9fa32db 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -45,7 +45,6 @@ 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\process_courses_table; use tool_lifecycle\local\table\triggered_courses_table_trigger; use tool_lifecycle\local\table\triggered_courses_table_workflow; use tool_lifecycle\processor; From f2b547cf2c1228e189518e9f54b8878d49ab0e48 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 19 Sep 2025 04:04:15 +0200 Subject: [PATCH 155/170] correction of maturity level to BETA --- version.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.php b/version.php index 37b30297..fe9ac396 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die; -$plugin->maturity = MATURITY_STABLE; +$plugin->maturity = MATURITY_BETA; $plugin->version = 2025050405; $plugin->component = 'tool_lifecycle'; $plugin->requires = 2022112800; // Requires Moodle 4.1+. From e8e10c872a576f72627debc711d5b39c4e015fdd Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 19 Sep 2025 05:24:07 +0200 Subject: [PATCH 156/170] use USER-timezone in function userdate() to display correct dates --- classes/local/manager/workflow_manager.php | 2 +- classes/local/table/course_backups_table.php | 4 +++- classes/local/table/courses_in_step_table.php | 5 ++++- classes/local/table/delayed_courses_table.php | 19 ++++++++++++------- .../table/interaction_remaining_table.php | 6 +++++- classes/local/table/interaction_table.php | 5 ++++- classes/local/table/process_errors_table.php | 6 +++++- classes/local/table/select_workflow_table.php | 9 +++++++-- .../triggered_courses_table_workflow.php | 5 ++++- .../local/table/workflow_definition_table.php | 6 ++++-- classes/local/table/workflow_table.php | 10 +++++++--- classes/processor.php | 13 +++++++++---- step/adminapprove/classes/decision_table.php | 7 ++++++- workflowoverview.php | 6 ++++-- 14 files changed, 75 insertions(+), 28 deletions(-) diff --git a/classes/local/manager/workflow_manager.php b/classes/local/manager/workflow_manager.php index c524587b..e1867f51 100644 --- a/classes/local/manager/workflow_manager.php +++ b/classes/local/manager/workflow_manager.php @@ -267,7 +267,7 @@ public static function activate_workflow($workflowid) { $lib = lib_manager::get_trigger_lib($trigger->subpluginname); $workflow->manually |= $lib->is_manual_trigger(); } - $workflow->timeactive = time(); + $workflow->timeactive = (new \DateTime())->getTimestamp(); $workflow->timedeactive = null; if (!$workflow->manually) { $workflow->sortindex = count(self::get_active_automatic_workflows()) + 1; diff --git a/classes/local/table/course_backups_table.php b/classes/local/table/course_backups_table.php index 0739ab3c..bd69a1fa 100644 --- a/classes/local/table/course_backups_table.php +++ b/classes/local/table/course_backups_table.php @@ -135,7 +135,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..4dde73cb 100644 --- a/classes/local/table/courses_in_step_table.php +++ b/classes/local/table/courses_in_step_table.php @@ -157,9 +157,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 db47ef39..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; @@ -169,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()) { @@ -182,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; @@ -192,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]); @@ -216,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]); @@ -239,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 8ffb88ef..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; @@ -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 9c169049..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 "-"; } diff --git a/classes/local/table/process_errors_table.php b/classes/local/table/process_errors_table.php index 2150d88f..6f944163 100644 --- a/classes/local/table/process_errors_table.php +++ b/classes/local/table/process_errors_table.php @@ -24,6 +24,7 @@ namespace tool_lifecycle\local\table; use core\exception\coding_exception; +use core_date; defined('MOODLE_INTERNAL') || die; @@ -133,8 +134,11 @@ public function col_error($row) { * @throws \moodle_exception */ public function col_errortime($row) { + global $USER; + return userdate($row->errortimecreated, - get_string('strftimedatetimeshortaccurate', 'core_langconfig')); + get_string('strftimedatetimeshortaccurate', 'core_langconfig'), + core_date::get_user_timezone($USER)); } /** 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_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index 71d1f64d..5085cb02 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\table; +use core_date; use tool_lifecycle\local\entity\trigger_subplugin; use tool_lifecycle\local\entity\workflow; use tool_lifecycle\local\manager\delayed_courses_manager; @@ -288,8 +289,10 @@ public function col_coursefullname($row) { * @throws \coding_exception */ public function col_delayeduntil($row) { + global $USER; if ($delay = delayed_courses_manager::get_course_delayed($row->courseid)) { - return userdate($delay, get_string('strftimedatetime', 'core_langconfig')); + return userdate($delay, get_string('strftimedatetime', 'core_langconfig'), + core_date::get_user_timezone($USER)); } return "-"; } diff --git a/classes/local/table/workflow_definition_table.php b/classes/local/table/workflow_definition_table.php index 2d712c95..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, diff --git a/classes/local/table/workflow_table.php b/classes/local/table/workflow_table.php index 3c313d8e..881bab82 100644 --- a/classes/local/table/workflow_table.php +++ b/classes/local/table/workflow_table.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\table; +use core_date; use html_writer; use tool_lifecycle\action; use tool_lifecycle\local\manager\process_manager; @@ -69,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, @@ -87,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'); } diff --git a/classes/processor.php b/classes/processor.php index bb6ef8c8..972cbf41 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -27,6 +27,7 @@ namespace tool_lifecycle; +use core_date; use tool_lifecycle\event\process_rollback; use tool_lifecycle\local\entity\trigger_subplugin; use tool_lifecycle\event\process_triggered; @@ -55,7 +56,7 @@ class processor { * Processes the trigger plugins for all relevant courses. */ public function call_trigger() { - global $FULLSCRIPT, $CFG; + global $FULLSCRIPT, $CFG, $USER; $run = str_contains($FULLSCRIPT, 'run.php'); $debug = $run && $CFG->debugdeveloper && !defined('BEHAT_SITE_RUNNING'); @@ -81,10 +82,12 @@ public function call_trigger() { if (!defined('BEHAT_SITE_RUNNING')) { if ($run) { echo \html_writer::div('Calling triggers for workflow "' . $workflow->title . '" '. - userdate(time(), get_string('strftimedatetimeaccurate'))); + userdate(time(), get_string('strftimedatetimeaccurate'), + core_date::get_user_timezone($USER))); } else { mtrace('Calling triggers for workflow "' . $workflow->title . '" '. - userdate(time(), get_string('strftimedatetimeaccurate'))); + userdate(time(), get_string('strftimedatetimeaccurate'), + core_date::get_user_timezone($USER))); } } $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); @@ -492,6 +495,7 @@ public function get_triggercourses_forcounting_check_course($trigger, $excluded) * @throws \dml_exception */ public function get_count_of_courses_to_trigger_for_workflow($workflow) { + global $USER; // Exclude delayed courses and sitecourse according to the workflow settings. $excludesitecourse = $workflow->includesitecourse ? [] : [1]; @@ -560,7 +564,8 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { if ($nextrun = $lib->get_next_run_time($trigger->id)) { if ($obj->lastrun = $settings['timelastrun'] ?? 0) { $obj->additionalinfo = get_string('lastrun', 'tool_lifecycle', - userdate($settings['timelastrun'], get_string('strftimedatetimeshort', 'langconfig'))); + userdate($settings['timelastrun'], get_string('strftimedatetimeshort', 'langconfig'), + core_date::get_user_timezone($USER))); } else { $obj->additionalinfo = "--"; } diff --git a/step/adminapprove/classes/decision_table.php b/step/adminapprove/classes/decision_table.php index f509c887..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; @@ -124,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 "-"; } diff --git a/workflowoverview.php b/workflowoverview.php index a9fa32db..495f848b 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -240,7 +240,8 @@ } if (is_numeric($nextrunout)) { if ($nextrunout) { - $nextrunout = userdate($nextrunout, get_string('strftimedatetimeshort', 'langconfig')); + $nextrunout = userdate($nextrunout, get_string('strftimedatetimeshort', 'langconfig'), + core_date::get_user_timezone($USER)); } else { $nextrunout = get_string('statusunknown'); } @@ -551,7 +552,8 @@ 'isactive' => $isactive || $isdeactivated, 'nextrun' => $nextrunout, 'lastrun' => $lastrun != 0 ? - userdate($lastrun, get_string('strftimedatetimeshort', 'langconfig')) : '--', + userdate($lastrun, get_string('strftimedatetimeshort', 'langconfig'), + core_date::get_user_timezone($USER)) : '--', 'nomanualtriggerinvolved' => $nomanualtriggerinvolved, 'disableworkflowlink' => $disableworkflowlink, 'abortdisableworkflowlink' => $abortdisableworkflowlink, From c2158699af5e2c25b671fe7bf03e676ad67d15ab Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Fri, 19 Sep 2025 05:51:42 +0200 Subject: [PATCH 157/170] fix behat test --- classes/local/table/course_backups_table.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/classes/local/table/course_backups_table.php b/classes/local/table/course_backups_table.php index bd69a1fa..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'); From ac1414f5254d1b578646564f08fd5f11f9c3f472 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 28 Sep 2025 19:13:49 +0200 Subject: [PATCH 158/170] small fixes in display-courselist-tables and additional trigger description in workflowoverview --- CHANGES.md | 5 +- classes/local/manager/process_manager.php | 9 +- classes/local/table/courses_in_step_table.php | 1 + classes/local/table/process_errors_table.php | 4 +- .../table/triggered_courses_table_trigger.php | 5 +- .../triggered_courses_table_workflow.php | 4 +- classes/processor.php | 132 +++++++++--------- db/upgrade.php | 4 +- lang/de/tool_lifecycle.php | 11 +- lang/en/tool_lifecycle.php | 6 +- templates/overview_trigger.mustache | 1 + templates/workflowoverview.mustache | 34 +++-- tests/process_error_test.php | 4 +- workflowoverview.php | 2 +- 14 files changed, 117 insertions(+), 105 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e90c47b4..5f4ccab9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,12 @@ CHANGELOG ========= -Further information to the changes can be found here: https://github.com/learnweb/moodle-tool_lifecycle/wiki/Changes-of-version-4.5.4-and-4.5.5 +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-09-14) +4.5.5 (2025-09-28) ------------------ +* [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 diff --git a/classes/local/manager/process_manager.php b/classes/local/manager/process_manager.php index 92a60a0b..8d61e60a 100644 --- a/classes/local/manager/process_manager.php +++ b/classes/local/manager/process_manager.php @@ -340,7 +340,7 @@ public static function insert_process_error(process $process, Exception $e) { } /** - * Return 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 @@ -361,7 +361,7 @@ public static function proceed_process_after_error(int $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 @@ -371,13 +371,14 @@ public static function rollback_process_after_error(int $processid) { global $DB; $process = $DB->get_record('tool_lifecycle_proc_error', ['id' => $processid]); - // Unset process error only 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/table/courses_in_step_table.php b/classes/local/table/courses_in_step_table.php index 4dde73cb..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; diff --git a/classes/local/table/process_errors_table.php b/classes/local/table/process_errors_table.php index 6f944163..3156b649 100644 --- a/classes/local/table/process_errors_table.php +++ b/classes/local/table/process_errors_table.php @@ -156,14 +156,14 @@ public function col_tools($row) { 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 \moodle_url('', ['action' => 'proceed', 'id[]' => $row->errorid, 'sesskey' => sesskey()]), new \pix_icon('e/tick', $this->strings['proceed']), $this->strings['proceed'] ) ); $actionmenu->add_primary_action( new \action_menu_link_primary( - new \moodle_url('', ['action' => 'rollback', 'id[]' => $row->id, 'sesskey' => sesskey()]), + new \moodle_url('', ['action' => 'rollback', 'id[]' => $row->errorid, 'sesskey' => sesskey()]), new \pix_icon('e/undo', $this->strings['rollback']), $this->strings['rollback'] ) diff --git a/classes/local/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index 45751fc9..b39c1181 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -180,7 +180,7 @@ public function build_table() { if ($row->hasprocess) { continue; } - if ($row->delay && $row->delay > time()) { + if ($row->delay && $row->delay > time() && !$this->triggerexclude) { continue; } else { if ($this->type == 'triggerid') { @@ -189,7 +189,8 @@ public function build_table() { $this->add_data_keyed($formattedrow, $this->get_row_class($row)); } } else if ($this->type == 'excluded') { - if ($response == trigger_response::exclude() || $this->triggerexclude) { + if ($response == trigger_response::exclude() || + ($response == trigger_response::trigger() && $this->triggerexclude)) { $formattedrow = $this->format_row($row); $this->add_data_keyed($formattedrow, $this->get_row_class($row)); } diff --git a/classes/local/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index 5085cb02..01c9fe7f 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -252,7 +252,9 @@ public function build_table() { } if ($row->hasprocess) { if ($row->workflowid) { - if ( ($row->workflowid != $this->workflowid && $this->type == 'used') || $this->type == 'processes' ) { + if ( ($row->workflowid != $this->workflowid && $this->type == 'used') + || $this->type == 'processes' + || $this->type == 'delayed' ) { $formattedrow = $this->format_row($row); $this->add_data_keyed($formattedrow, $this->get_row_class($row)); } diff --git a/classes/processor.php b/classes/processor.php index 972cbf41..8a8cb5d5 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -98,7 +98,7 @@ public function call_trigger() { $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, (bool)$exclude); + $recordset = $this->get_course_recordset($triggers, !$workflow->includesitecourse); while ($recordset->valid()) { $course = $recordset->current(); $countcourses++; @@ -286,13 +286,13 @@ 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 bool $exclude Are the site course and/or the delayed courses to be excluded or not. + * @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 = false, $forcounting = false) { + public function get_course_recordset($triggers, $nositecourse = true, $forcounting = false) { global $DB, $SESSION; $where = " TRUE "; @@ -315,7 +315,7 @@ public function get_course_recordset($triggers, $exclude = false, $forcounting = } if ($forcounting) { - if ($exclude) { // We include delayed courses here anyway, so we only take the site course into account. + if ($nositecourse) { //We include delayed courses here anyway, so we only take the site course into account. $where = "($where) AND c.id <> 1 "; } // Get course hasprocess and delay with the sql. @@ -341,17 +341,15 @@ public function get_course_recordset($triggers, $exclude = false, $forcounting = } $sql .= " WHERE $where "; } else { - if ($exclude) { - 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' => $workflowid]; - $whereparams = array_merge($whereparams, $inparams); - } + 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' => $workflowid]; + $whereparams = array_merge($whereparams, $inparams); } // Get only courses which are not part of an existing process. $sql = "SELECT c.id from {course} c @@ -370,14 +368,15 @@ public function get_course_recordset($triggers, $exclude = false, $forcounting = /** * 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 $excluded Are courses to exclude? 0 if not. - * @return int[] $triggered, $new, $delayed Triggered courses, amount which courses of them would - * be added, which courses are delayed. + * 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, $excluded) { + public function get_triggercourses_forcounting($trigger) { global $DB; $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); @@ -392,7 +391,7 @@ public function get_triggercourses_forcounting($trigger, $excluded) { $sql = 'SELECT c.id from {course} c WHERE '. $where; $triggercoursesall = $DB->get_fieldset_sql($sql, $whereparams); - // Get amount of delayed courses which would be triggered by this trigger. + // Get number of delayed courses which would be triggered by this trigger. $workflow = workflow_manager::get_workflow($trigger->workflowid); if ($workflow->includedelayedcourses) { $delayed = []; @@ -402,20 +401,20 @@ public function get_triggercourses_forcounting($trigger, $excluded) { } $delayedcourses = count(array_intersect($triggercoursesall, $delayed)); - // Exclude delayed courses and sitecourse according to the workflow settings. - if ($excluded) { - 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); - } + // 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); } + $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. @@ -423,8 +422,11 @@ public function get_triggercourses_forcounting($trigger, $excluded) { 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 OR pe.courseid IS NOT NULL + 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]; @@ -432,35 +434,32 @@ public function get_triggercourses_forcounting($trigger, $excluded) { /** - * Also returns the number of courses for a trigger for counting. BUT the trigger lib function check_courses is used - * to select a course for triggering/excluding. - * 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 $excluded Are courses to exclude? 0 if not. - * @return int[] $triggered, $new, $delayed Triggered courses, amount which courses of them would - * be added, which courses are delayed. + * 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_forcounting_check_course($trigger, $excluded) { + public function get_triggercourses_forcounting_check_course($trigger) { global $DB; $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); - $excludedcourses = []; $delayedcourses = []; - if ($excluded) { - $workflow = workflow_manager::get_workflow($trigger->workflowid); - // Exclude delayed courses and sitecourse according to the workflow settings. - $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()); - } - $excludedcourses = array_merge($excludesitecourse, $delayedcourses); + $workflow = workflow_manager::get_workflow($trigger->workflowid); + // Exclude delayed courses and sitecourse according to the workflow settings. + $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()); } + $excludedcourses = array_merge($excludesitecourse, $delayedcourses); $triggercoursesall = []; - $recordset = $this->get_course_recordset([$trigger], (bool)$excludesitecourse, true); + $recordset = $this->get_course_recordset([$trigger], !$workflow->includesitecourse, true); while ($recordset->valid()) { $course = $recordset->current(); $response = $lib->check_course($course, $trigger->id); @@ -497,24 +496,14 @@ public function get_triggercourses_forcounting_check_course($trigger, $excluded) public function get_count_of_courses_to_trigger_for_workflow($workflow) { global $USER; - // Exclude delayed courses and sitecourse according to the workflow settings. - $excludesitecourse = $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($excludesitecourse, $delayedcourses); - $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. + // 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(); @@ -539,10 +528,9 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { // Delayed: Courses in current selection, which are delayed. if ($lib->check_course_code()) { [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting_check_course( - $trigger, count($excludedcourses)); + $trigger); } else { - [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting( - $trigger, count($excludedcourses)); + [$triggercourses, $newcourses, $delayed] = $this->get_triggercourses_forcounting($trigger); } if ($obj->response == trigger_response::exclude()) { $obj->excluded = $newcourses; @@ -579,7 +567,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $amounts[$trigger->sortindex] = $obj; } - $recordset = $this->get_course_recordset($autotriggers, (bool)$excludesitecourse, true); + $recordset = $this->get_course_recordset($autotriggers, !$workflow->includesitecourse, true); $coursestriggered = 0; $usedcourses = 0; @@ -612,6 +600,9 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { if ($course->workflowid && ($course->workflowid != $workflow->id)) { $usedcourses++; } + if ($course->delay && $course->delay > time()) { + $coursesdelayed++; + } } else if ($course->delay && $course->delay > time()) { $coursesdelayed++; } else { @@ -623,6 +614,9 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { if ($course->workflowid && ($course->workflowid != $workflow->id)) { $usedcourses++; } + if ($course->delay && $course->delay > time()) { + $coursesdelayed++; + } } else if ($course->delay && $course->delay > time()) { $coursesdelayed++; } else { diff --git a/db/upgrade.php b/db/upgrade.php index cd3d8388..14475a69 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -504,7 +504,7 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { } - if ($oldversion < 2025050404) { + if ($oldversion < 2025050405) { // Define field manual to be renamed to manually in table tool_lifecycle_workflow. $table = new xmldb_table('tool_lifecycle_workflow'); @@ -609,7 +609,7 @@ function xmldb_tool_lifecycle_upgrade($oldversion) { $dbman->add_field($table, $field); } - upgrade_plugin_savepoint(true, 2025050404, 'tool', 'lifecycle'); + upgrade_plugin_savepoint(true, 2025050405, 'tool', 'lifecycle'); } diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index be2b5255..11915d1b 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -82,7 +82,7 @@ $string['courserolledback'] = 'Ein Kurs wurde zurückgesetzt.'; $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}'; @@ -204,10 +204,11 @@ $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.'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index e4e39d59..d3adc216 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -83,7 +83,7 @@ $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}'; @@ -206,8 +206,10 @@ $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.'; diff --git a/templates/overview_trigger.mustache b/templates/overview_trigger.mustache index 692701b3..5de7f760 100644 --- a/templates/overview_trigger.mustache +++ b/templates/overview_trigger.mustache @@ -77,6 +77,7 @@ 0 {{/exclude}} {{/excludedcourses}} + {{/additionalinfo}} {{/automatic}} {{^automatic}} diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index d362da1d..213b43de 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -108,26 +108,34 @@ {{#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}} - - {{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}} {{#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}} diff --git a/tests/process_error_test.php b/tests/process_error_test.php index 4238155a..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); diff --git a/workflowoverview.php b/workflowoverview.php index 495f848b..540fddfb 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -294,7 +294,7 @@ } $trigger->tooltip = ""; if ($amounts[$trigger->sortindex]->excluded !== false) { - $trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded; + $trigger->excludedcourses = $amounts[$trigger->sortindex]->excluded ?? 0; $trigger->tooltip = get_string('courses_will_be_excluded', 'tool_lifecycle', $trigger->excludedcourses); } else { From a953c9fac5b191430ad532f00f40927339140bd8 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 28 Sep 2025 19:24:33 +0200 Subject: [PATCH 159/170] codechecker issue --- classes/processor.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/classes/processor.php b/classes/processor.php index 8a8cb5d5..8bacb418 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -315,7 +315,8 @@ public function get_course_recordset($triggers, $nositecourse = true, $forcounti } if ($forcounting) { - if ($nositecourse) { //We include delayed courses here anyway, so we only take the site course into account. + // 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. From 61f55828ececa3e2cea6d8b31890c516b372e5bb Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Sun, 28 Sep 2025 19:41:29 +0200 Subject: [PATCH 160/170] change maturity level to stable --- version.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.php b/version.php index fe9ac396..37b30297 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die; -$plugin->maturity = MATURITY_BETA; +$plugin->maturity = MATURITY_STABLE; $plugin->version = 2025050405; $plugin->component = 'tool_lifecycle'; $plugin->requires = 2022112800; // Requires Moodle 4.1+. From 33ae8cae248ea10db4d205203a2641c083c541c5 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 30 Sep 2025 13:28:01 +0200 Subject: [PATCH 161/170] triggered_courses_tables: optimize some columns and the display of course numbers --- .../table/triggered_courses_table_trigger.php | 90 +++++---- .../triggered_courses_table_workflow.php | 187 +++++++++--------- classes/processor.php | 71 +++---- delayedcourses.php | 4 + lang/de/tool_lifecycle.php | 17 +- lang/en/tool_lifecycle.php | 17 +- workflowoverview.php | 41 +++- 7 files changed, 246 insertions(+), 181 deletions(-) diff --git a/classes/local/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index b39c1181..e1d2f28a 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\table; +use core\exception\moodle_exception; use tool_lifecycle\local\entity\trigger_subplugin; use tool_lifecycle\local\manager\lib_manager; use tool_lifecycle\local\manager\settings_manager; @@ -57,12 +58,21 @@ class triggered_courses_table_trigger extends \table_sql { /** @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 int $numbercourses number of courses listed here * @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 -name + * @param string $filterdata optional, term to filter the table by course id or course name * @throws \coding_exception * @throws \dml_exception */ @@ -82,23 +92,20 @@ public function __construct($numbercourses, $trigger, $type, $filterdata = '') { $this->define_baseurl($PAGE->url); $a = new \stdClass(); $a->title = $trigger->instancename; - $a->courses = $numbercourses; 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']; - $this->caption .= "   ".\html_writer::link(new \moodle_url(urls::WORKFLOW_DETAILS, - ["wf" => $workflow->id, "showsql" => "1", "showtablesql" => "1", "showdetails" => "1"]), - "   ", ["class" => "text-muted fs-6 text-decoration-none"]); - $columns = ['courseid', 'coursefullname', 'coursecategory']; + $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); @@ -113,33 +120,31 @@ public function __construct($numbercourses, $trigger, $type, $filterdata = '') { c.shortname as courseshortname, cc.name as coursecategory, 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 "; - + 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 - LEFT JOIN {tool_lifecycle_proc_error} pe ON c.id = pe.courseid + 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 {course}.id = $filterdata "; + $where .= " AND c.id = $filterdata "; } else { - $where .= " AND ( {course}.fullname LIKE '%$filterdata%' OR {course}.shortname LIKE '%$filterdata%')"; + $where .= " AND ( c.fullname LIKE '%$filterdata%' OR c.shortname LIKE '%$filterdata%')"; } } @@ -161,6 +166,7 @@ public function __construct($numbercourses, $trigger, $type, $filterdata = '') { * table. * * After calling this function, don't forget to call close_recordset. + * @throws \dml_exception */ public function build_table() { @@ -170,32 +176,26 @@ public function build_table() { $trigger = trigger_manager::get_instance($this->triggerid); $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); - foreach ($this->rawdata as $row) { + if ($row->hasotherwfprocess) { + $this->otherwf++; + } + if ($row->delaycourse && $row->delaycourse > time() && !$this->triggerexclude) { + $this->delayed++; + } $response = $lib->check_course($row->courseid, $this->triggerid); - if (!($response == trigger_response::exclude() || $response == trigger_response::trigger())) { continue; } - if ($row->hasprocess) { + if ($this->type == 'triggerid' && !$response == trigger_response::trigger()) { continue; - } - if ($row->delay && $row->delay > time() && !$this->triggerexclude) { + } else if ($this->type == 'excluded' && + (!$response == trigger_response::exclude() || !($response == trigger_response::trigger() && $this->triggerexclude))) { continue; - } else { - if ($this->type == 'triggerid') { - if ($response == trigger_response::trigger()) { - $formattedrow = $this->format_row($row); - $this->add_data_keyed($formattedrow, $this->get_row_class($row)); - } - } else if ($this->type == 'excluded') { - if ($response == trigger_response::exclude() || - ($response == trigger_response::trigger() && $this->triggerexclude)) { - $formattedrow = $this->format_row($row); - $this->add_data_keyed($formattedrow, $this->get_row_class($row)); - } - } } + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + $this->tablerows++; } } @@ -210,6 +210,26 @@ public function col_coursefullname($row) { return $courselink . '
' . $row->courseshortname . ''; } + /** + * Render trigger status of the course (triggered, already in process, other 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 ($out == "") { + $out .= \html_writer::div(get_string('ok'), 'text-success'); + } + return $out; + } + /** * Prints a customized "nothing to display" message. */ diff --git a/classes/local/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index 01c9fe7f..9a81d00f 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -23,6 +23,7 @@ */ namespace tool_lifecycle\local\table; +use core\exception\moodle_exception; use core_date; use tool_lifecycle\local\entity\trigger_subplugin; use tool_lifecycle\local\entity\workflow; @@ -59,6 +60,9 @@ class triggered_courses_table_workflow extends \table_sql { /** @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 @@ -78,7 +82,6 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { $a = new \stdClass(); $a->title = $workflow->title; - $a->courses = $courses; if ($type == 'triggeredworkflow') { $this->caption = get_string('coursestriggeredworkflow', 'tool_lifecycle', $a); $this->selectable = workflow_manager::is_active($workflow->id); @@ -89,19 +92,17 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { } else if ($type == 'processes') { $this->caption = get_string('coursesinprocess', 'tool_lifecycle', $a); } - $this->caption .= "   ".\html_writer::link(new \moodle_url(urls::WORKFLOW_DETAILS, - ["wf" => $workflow->id, "showsql" => "1", "showtablesql" => "1", "showdetails" => "1"]), - "   ", ["class" => "text-muted fs-6 text-decoration-none"]); $this->captionattributes = ['class' => 'ml-3']; $columns = ['courseid', 'coursefullname', 'coursecategory']; - if ( ($type == 'triggeredworkflow' && $this->selectable) || $type == 'processes' ) { + if ( $type == 'triggeredworkflow' && $this->selectable) { $columns[] = 'tools'; } else if ($type == 'delayed') { $columns[] = 'delayeduntil'; - $columns[] = 'tools'; } else if ($type == 'used') { $columns[] = 'otherworkflow'; + } else if ($type == 'processes') { + $columns[] = 'processtype'; } $this->define_columns($columns); $headers = [ @@ -109,13 +110,14 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { get_string('coursename', 'tool_lifecycle'), get_string('coursecategory', 'moodle'), ]; - if ( ($type == 'triggeredworkflow' && $this->selectable) || $type == 'processes' ) { + 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'); + } else if ($type == 'processes') { + $headers[] = get_string('type', 'tool_lifecycle'); } $this->define_headers($headers); @@ -125,37 +127,35 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { cc.name as coursecategory, pe.id as errorid, 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 "; - if ($type == 'used') { - $fields .= ", COALESCE(wfp.title, wfpe.title) as otherworkflow"; - } + 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 - LEFT JOIN {tool_lifecycle_proc_error} pe ON c.id = pe.courseid + 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 "; - if ($type == 'used') { - $from .= " LEFT JOIN {tool_lifecycle_workflow} wfp ON p.workflowid = wfp.id - LEFT JOIN {tool_lifecycle_workflow} wfpe ON pe.workflowid = wfpe.id"; - } + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON c.id = dw.courseid"; $where = 'true'; $inparams = []; if ($type == 'processes') { - $where .= " AND c.id IN (SELECT courseid FROM {tool_lifecycle_process} WHERE workflowid = :pworkflowid1 UNION - SELECT courseid FROM {tool_lifecycle_proc_error} WHERE workflowid = :pworkflowid2)"; - $where .= " AND (p.id IS NOT NULL OR pe.id IS NOT NULL) "; - $inparams = array_merge($inparams, ['pworkflowid1' => $workflow->id, 'pworkflowid2' => $workflow->id]); + $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) { @@ -202,17 +202,17 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { * * 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 + * method or if other_cols return NULL, then put the data straight into the * table. * - * After calling this function, don't forget to call close_recordset. + * After calling this function, remember to call close_recordset. */ public function build_table() { if (!$this->rawdata) { return; } - if ($this->checkcoursecode) { + if ($this->type == 'triggeredworkflow' && $this->checkcoursecode) { $autotriggers = []; $triggers = trigger_manager::get_triggers_for_workflow($this->workflowid); foreach ($triggers as $trigger) { @@ -225,51 +225,28 @@ public function build_table() { } } foreach ($this->rawdata as $row) { - if ($this->checkcoursecode) { - $action = false; - foreach ($autotriggers as $trigger) { - $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); - $response = $lib->check_course($row->id, $trigger->id); - if ($response == trigger_response::next()) { - if (!$action) { - $action = true; + if ($this->type == 'triggeredworkflow') { + if ($this->checkcoursecode) { + foreach ($autotriggers as $trigger) { + $lib = lib_manager::get_automatic_trigger_lib($trigger->subpluginname); + $response = $lib->check_course($row->id, $trigger->id); + if ($response == trigger_response::next()) { + continue 2; } - continue; - } - if ($response == trigger_response::exclude()) { - if (!$action) { - $action = true; + if ($response == trigger_response::exclude()) { + continue 2; } - continue; } - if ($response == trigger_response::trigger()) { + } else { + if ($row->hasprocess || $row->hasotherwfprocess || + ($row->delaycourse && $row->delaycourse > time())) { continue; } } - if ($action) { - continue; - } - } - if ($row->hasprocess) { - if ($row->workflowid) { - if ( ($row->workflowid != $this->workflowid && $this->type == 'used') - || $this->type == 'processes' - || $this->type == 'delayed' ) { - $formattedrow = $this->format_row($row); - $this->add_data_keyed($formattedrow, $this->get_row_class($row)); - } - } - } else if ($row->delay && $row->delay > time()) { - if ($this->type == 'delayed') { - $formattedrow = $this->format_row($row); - $this->add_data_keyed($formattedrow, $this->get_row_class($row)); - } - } else { - if ($this->type == 'triggeredworkflow') { - $formattedrow = $this->format_row($row); - $this->add_data_keyed($formattedrow, $this->get_row_class($row)); - } } + $formattedrow = $this->format_row($row); + $this->add_data_keyed($formattedrow, $this->get_row_class($row)); + $this->tablerows++; } } @@ -292,8 +269,10 @@ public function col_coursefullname($row) { */ public function col_delayeduntil($row) { global $USER; - if ($delay = delayed_courses_manager::get_course_delayed($row->courseid)) { - return userdate($delay, get_string('strftimedatetime', 'core_langconfig'), + $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 "-"; @@ -303,24 +282,14 @@ public function col_delayeduntil($row) { * Render tools column. * * @param object $row Row data. - * @return string html of the delete button + * @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 == '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')); - return $OUTPUT->render($button); - } else if ($this->type == 'triggeredworkflow' && $this->selectable) { + if ($this->type == 'triggeredworkflow' && $this->selectable) { $params = [ 'action' => 'select', 'cid' => $row->courseid, @@ -329,22 +298,52 @@ public function col_tools($row) { ]; $button = new \single_button(new \moodle_url($PAGE->url, $params), get_string('select')); return $OUTPUT->render($button); - } else if ($this->type == 'processes') { - if ($row->errorid) { - $params = [ - 'workflow' => $this->workflowid, - 'course' => $row->courseid, - ]; - return \html_writer::link( - new \moodle_url(urls::PROCESS_ERRORS, $params), - get_string('error'), - ['class' => 'error']); - } } 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. */ diff --git a/classes/processor.php b/classes/processor.php index 8bacb418..5cdc0f23 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -125,7 +125,7 @@ public function call_trigger() { continue; } } - // If all trigger instances agree, that they want to trigger a process, we do so. + // 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++; @@ -297,10 +297,9 @@ public function get_course_recordset($triggers, $nositecourse = true, $forcounti $where = " TRUE "; $whereparams = []; - $workflowid = false; + $workflow = false; foreach ($triggers as $trigger) { - if (!$workflowid) { - $workflowid = $trigger->workflowid; + if (!$workflow) { $workflow = workflow_manager::get_workflow($trigger->workflowid); $andor = ($workflow->andor ?? 0) == 0 ? 'AND' : 'OR'; $where = $andor == 'AND' ? 'true ' : 'false '; @@ -322,35 +321,33 @@ public function get_course_recordset($triggers, $nositecourse = true, $forcounti // 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 + END as delaycourse 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 + 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 "; - if ($workflowid) { - $sql .= " AND dw.workflowid = $workflowid "; - } + LEFT JOIN {tool_lifecycle_delayed_workf} dw ON + c.id = dw.courseid AND dw.workflowid = $workflow->id"; $sql .= " WHERE $where "; } else { - 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} + 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' => $workflowid]; - $whereparams = array_merge($whereparams, $inparams); + $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 c.id from {course} c @@ -571,7 +568,8 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $recordset = $this->get_course_recordset($autotriggers, !$workflow->includesitecourse, true); $coursestriggered = 0; - $usedcourses = 0; + $hasprocess = 0; + $hasotherwfprocess = 0; $coursesdelayed = 0; // Only delayed courses of selected courses are of interest here. while ($recordset->valid()) { $course = $recordset->current(); @@ -598,13 +596,16 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { } if (!$action) { if ($course->hasprocess) { - if ($course->workflowid && ($course->workflowid != $workflow->id)) { - $usedcourses++; + $hasprocess++; + if ($course->delaycourse && $course->delaycourse > time()) { + $coursesdelayed++; } - if ($course->delay && $course->delay > time()) { + } else if ($course->hasotherwfprocess) { + $hasotherwfprocess++; + if ($course->delaycourse && $course->delaycourse > time()) { $coursesdelayed++; } - } else if ($course->delay && $course->delay > time()) { + } else if ($course->delaycourse && $course->delaycourse > time()) { $coursesdelayed++; } else { $coursestriggered++; @@ -612,13 +613,16 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { } } else { if ($course->hasprocess) { - if ($course->workflowid && ($course->workflowid != $workflow->id)) { - $usedcourses++; + $hasprocess++; + if ($course->delaycourse && $course->delaycourse > time()) { + $coursesdelayed++; } - if ($course->delay && $course->delay > time()) { + } else if ($course->hasotherwfprocess) { + $hasotherwfprocess++; + if ($course->delaycourse && $course->delaycourse > time()) { $coursesdelayed++; } - } else if ($course->delay && $course->delay > time()) { + } else if ($course->delaycourse && $course->delaycourse > time()) { $coursesdelayed++; } else { $coursestriggered++; @@ -630,7 +634,8 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $all = new \stdClass(); $all->coursestriggered = $coursestriggered; $all->delayedcourses = $coursesdelayed; // Delayed courses for workflow and globally. Excluded per default. - $all->used = $usedcourses; + $all->used = $hasprocess; + $all->hasotherwf = $hasotherwfprocess; $all->nextrun = $nextrun; $amounts['all'] = $all; return $amounts; diff --git a/delayedcourses.php b/delayedcourses.php index b071dae0..b537cdfa 100644 --- a/delayedcourses.php +++ b/delayedcourses.php @@ -190,4 +190,8 @@ $table->out(100, false); +if ($table->totalrows) { + echo \html_writer::div($table->totalrows." ".get_string('courses'), 'mt-3'); +} + echo $OUTPUT->footer(); diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index 11915d1b..edac00f1 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -49,6 +49,8 @@ $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'; @@ -93,16 +95,16 @@ $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->title}\' ({$a->courses})'; +$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->title}\' ({$a->courses})'; -$string['coursesinprocess'] = 'Kurse schon in einem Prozess des Workflows \'{$a->title}\' ({$a->courses})'; +$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->title}\' ({$a->courses})'; -$string['coursestriggeredworkflow'] = 'Getriggerte Kurse für diesen Workflow \'{$a->title}\' ({$a->courses})'; -$string['coursesused'] = 'Kurse schon in einem anderen Prozess für Workflow \'{$a->title}\' ({$a->courses})'; +$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'; @@ -114,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}'; @@ -200,6 +203,7 @@ 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?'; @@ -220,6 +224,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'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index d3adc216..6561defb 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -49,6 +49,8 @@ $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'; @@ -94,16 +96,16 @@ $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->title}\' ({$a->courses})'; +$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->title}\' ({$a->courses})'; -$string['coursesinprocess'] = 'Courses already in a process of the workflow \'{$a->title}\' ({$a->courses})'; +$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->title}\' ({$a->courses})'; -$string['coursestriggeredworkflow'] = 'Courses triggered for this workflow \'{$a->title}\' ({$a->courses})'; -$string['coursesused'] = 'Courses already used in another process for workflow \'{$a->title}\' ({$a->courses})'; +$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'; @@ -115,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}'; @@ -201,6 +204,7 @@ 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?'; @@ -221,6 +225,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'; diff --git a/workflowoverview.php b/workflowoverview.php index 540fddfb..3a39c0ad 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -28,7 +28,7 @@ require_admin(); -define('PAGESIZE', 20); +define('PAGESIZE', 10); use core\output\notification; use core\output\single_button; @@ -134,7 +134,7 @@ $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 { @@ -399,6 +399,16 @@ $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 = $amounts[$trigger->sortindex]->triggered; } @@ -411,17 +421,28 @@ $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 = $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); + $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; } @@ -432,16 +453,20 @@ $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 ($amounts['all']->used ?? null) { - $table = new triggered_courses_table_workflow($amounts['all']->used, $workflow, 'used', $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 = $amounts['all']->used; } @@ -454,6 +479,8 @@ $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; } @@ -575,7 +602,7 @@ ['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']->used ?? 0; + $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; From 71e04146a93c0e929f0c3d2fbc344de8da127aaf Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Tue, 30 Sep 2025 13:40:51 +0200 Subject: [PATCH 162/170] return to beta in main, codechecker issues --- classes/local/table/triggered_courses_table_trigger.php | 3 ++- classes/local/table/triggered_courses_table_workflow.php | 4 ++-- step/adminapprove/version.php | 3 --- step/makeinvisible/version.php | 3 --- step/movecategory/version.php | 3 --- step/pushbackuptask/version.php | 3 --- trigger/byrole/version.php | 3 --- trigger/lastaccess/version.php | 3 --- trigger/semindependent/version.php | 3 --- version.php | 2 +- 10 files changed, 5 insertions(+), 25 deletions(-) diff --git a/classes/local/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index e1d2f28a..6fb5e70d 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -190,7 +190,8 @@ public function build_table() { if ($this->type == 'triggerid' && !$response == trigger_response::trigger()) { continue; } else if ($this->type == 'excluded' && - (!$response == trigger_response::exclude() || !($response == trigger_response::trigger() && $this->triggerexclude))) { + (!$response == trigger_response::exclude() || + !($response == trigger_response::trigger() && $this->triggerexclude))) { continue; } $formattedrow = $this->format_row($row); diff --git a/classes/local/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index 9a81d00f..99e2599e 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -151,8 +151,8 @@ public function __construct($courses, $workflow, $type, $filterdata = '') { } 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 + $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()]); } 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/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/version.php b/step/movecategory/version.php index 11f50d0f..941b534a 100644 --- a/step/movecategory/version.php +++ b/step/movecategory/version.php @@ -28,8 +28,5 @@ $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; $plugin->component = 'lifecyclestep_movecategory'; -$plugin->dependencies = [ - 'tool_lifecycle' => 2025050400, -]; $plugin->release = 'v4.5-r1'; $plugin->maturity = MATURITY_STABLE; diff --git a/step/pushbackuptask/version.php b/step/pushbackuptask/version.php index 94fb4ecc..b4695f3d 100644 --- a/step/pushbackuptask/version.php +++ b/step/pushbackuptask/version.php @@ -29,8 +29,5 @@ $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; $plugin->component = 'lifecyclestep_pushbackuptask'; -$plugin->dependencies = [ - 'tool_lifecycle' => 2025050400, -]; $plugin->release = 'v4.5-r1'; $plugin->maturity = MATURITY_STABLE; 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/lastaccess/version.php b/trigger/lastaccess/version.php index a73d1a16..09e8d0e8 100644 --- a/trigger/lastaccess/version.php +++ b/trigger/lastaccess/version.php @@ -28,8 +28,5 @@ $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; $plugin->component = 'lifecycletrigger_lastaccess'; -$plugin->dependencies = [ - 'tool_lifecycle' => 2025050400, -]; $plugin->release = 'v4.5-r1'; $plugin->maturity = MATURITY_STABLE; 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/version.php b/version.php index 37b30297..fe9ac396 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die; -$plugin->maturity = MATURITY_STABLE; +$plugin->maturity = MATURITY_BETA; $plugin->version = 2025050405; $plugin->component = 'tool_lifecycle'; $plugin->requires = 2022112800; // Requires Moodle 4.1+. From 8ffccf98cd1f931c5eeba9e97d9eae6e2807a774 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 2 Oct 2025 06:24:44 +0200 Subject: [PATCH 163/170] fix problems with check_course_code --- classes/local/manager/interaction_manager.php | 8 +- .../table/triggered_courses_table_trigger.php | 21 +++- .../triggered_courses_table_workflow.php | 5 +- classes/processor.php | 105 +++++++++++------- delayedcourses.php | 2 +- lang/de/tool_lifecycle.php | 1 + lang/en/tool_lifecycle.php | 1 + templates/workflowoverview.mustache | 21 ++-- trigger/byrole/lib.php | 2 +- trigger/categories/lib.php | 2 +- trigger/customfielddelay/lib.php | 21 +++- trigger/delayedcourses/lib.php | 10 +- trigger/lastaccess/lib.php | 3 +- trigger/lib.php | 10 +- trigger/sitecourse/lib.php | 2 +- trigger/specificdate/lib.php | 10 +- trigger/startdatedelay/lib.php | 2 +- workflowoverview.php | 10 +- 18 files changed, 155 insertions(+), 81 deletions(-) diff --git a/classes/local/manager/interaction_manager.php b/classes/local/manager/interaction_manager.php index 1aef8745..edb1e635 100644 --- a/classes/local/manager/interaction_manager.php +++ b/classes/local/manager/interaction_manager.php @@ -136,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/table/triggered_courses_table_trigger.php b/classes/local/table/triggered_courses_table_trigger.php index 6fb5e70d..7e88cd02 100644 --- a/classes/local/table/triggered_courses_table_trigger.php +++ b/classes/local/table/triggered_courses_table_trigger.php @@ -24,6 +24,7 @@ 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; @@ -69,14 +70,13 @@ class triggered_courses_table_trigger extends \table_sql { /** * Builds a table of courses. - * @param int $numbercourses number of courses listed here * @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($numbercourses, $trigger, $type, $filterdata = '') { + public function __construct($trigger, $type, $filterdata = '') { parent::__construct('tool_lifecycle-courses-in-trigger'); global $PAGE, $SESSION; @@ -176,6 +176,8 @@ public function build_table() { $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++; @@ -183,7 +185,10 @@ public function build_table() { if ($row->delaycourse && $row->delaycourse > time() && !$this->triggerexclude) { $this->delayed++; } - $response = $lib->check_course($row->courseid, $this->triggerid); + 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; } @@ -194,6 +199,7 @@ public function build_table() { !($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++; @@ -212,7 +218,7 @@ public function col_coursefullname($row) { } /** - * Render trigger status of the course (triggered, already in process, other process, delayed). + * Render trigger status of the course (triggered, already in process, another process, delayed). * @param object $row Row data. * @return string status * @throws \coding_exception @@ -220,11 +226,16 @@ public function col_coursefullname($row) { public function col_status($row) { $out = ""; if ($row->hasotherwfprocess) { - $out .= \html_writer::div(get_string('alreadyinprocessotherworkflow', 'tool_lifecycle'), 'text-warning'); + $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'); } diff --git a/classes/local/table/triggered_courses_table_workflow.php b/classes/local/table/triggered_courses_table_workflow.php index 99e2599e..297a9ec9 100644 --- a/classes/local/table/triggered_courses_table_workflow.php +++ b/classes/local/table/triggered_courses_table_workflow.php @@ -25,6 +25,7 @@ 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; @@ -224,12 +225,14 @@ public function build_table() { } } } + $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); - $response = $lib->check_course($row->id, $trigger->id); + $course->id = $row->courseid; + $response = $lib->check_course($course, $trigger->id); if ($response == trigger_response::next()) { continue 2; } diff --git a/classes/processor.php b/classes/processor.php index 5cdc0f23..3652561f 100644 --- a/classes/processor.php +++ b/classes/processor.php @@ -58,11 +58,14 @@ class processor { public function call_trigger() { global $FULLSCRIPT, $CFG, $USER; - $run = str_contains($FULLSCRIPT, 'run.php'); + $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(); + // 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'). @@ -73,12 +76,14 @@ public function call_trigger() { } } + // Walk through the active workflows. foreach ($activeworkflows as $workflow) { $countcourses = 0; $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 . '" '. @@ -90,6 +95,8 @@ public function call_trigger() { core_date::get_user_timezone($USER))); } } + + // Get workflow triggers and settings. $triggers = trigger_manager::get_triggers_for_workflow($workflow->id); if (!$workflow->includesitecourse) { $exclude[] = 1; @@ -98,10 +105,13 @@ public function call_trigger() { $exclude = array_merge(delayed_courses_manager::get_delayed_courses_for_workflow($workflow->id), delayed_courses_manager::get_globally_delayed_courses(), $exclude); } + // 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); @@ -134,6 +144,7 @@ public function call_trigger() { } $recordset->next(); } + // Final debug messages if this is not a behat test. if (!defined('BEHAT_SITE_RUNNING')) { if ($run) { echo \html_writer::div(" $countcourses courses processed."); @@ -458,9 +469,12 @@ public function get_triggercourses_forcounting_check_course($trigger) { $triggercoursesall = []; $recordset = $this->get_course_recordset([$trigger], !$workflow->includesitecourse, true); + $response = $lib->default_response(); while ($recordset->valid()) { $course = $recordset->current(); - $response = $lib->check_course($course, $trigger->id); + if ($lib->check_course_code()) { + $response = $lib->check_course($course, $trigger->id); + } if ($response !== trigger_response::next()) { $triggercoursesall[] = $course->id; } @@ -518,7 +532,7 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { // 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); // We only need the trigger response here. - $obj->response = $lib->check_course(null, null); + $obj->response = $lib->default_response(); if ($obj->sql != "false") { // Get courses amounts. // Triggercourses: Courses in current selection without defined excluded courses. @@ -565,36 +579,57 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $amounts[$trigger->sortindex] = $obj; } - $recordset = $this->get_course_recordset($autotriggers, !$workflow->includesitecourse, true); + $recordset = false; + if ($autotriggers) { + $recordset = $this->get_course_recordset($autotriggers, !$workflow->includesitecourse, true); + } $coursestriggered = 0; - $hasprocess = 0; - $hasotherwfprocess = 0; $coursesdelayed = 0; // Only delayed courses of selected courses are of interest here. - 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; + $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; } - continue; - } - if ($response == trigger_response::exclude()) { - if (!$action) { - $action = true; + if ($response == trigger_response::exclude()) { + if (!$action) { + $action = true; + } + continue; + } + if ($response == trigger_response::trigger()) { + continue; } - 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++; + } } - } - if (!$action) { + } else { if ($course->hasprocess) { $hasprocess++; if ($course->delaycourse && $course->delaycourse > time()) { @@ -611,24 +646,8 @@ public function get_count_of_courses_to_trigger_for_workflow($workflow) { $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++; - } + $recordset->next(); } - $recordset->next(); } $all = new \stdClass(); diff --git a/delayedcourses.php b/delayedcourses.php index b537cdfa..60b16472 100644 --- a/delayedcourses.php +++ b/delayedcourses.php @@ -179,7 +179,7 @@ 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'; diff --git a/lang/de/tool_lifecycle.php b/lang/de/tool_lifecycle.php index edac00f1..090ab369 100644 --- a/lang/de/tool_lifecycle.php +++ b/lang/de/tool_lifecycle.php @@ -157,6 +157,7 @@ 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'] = 'Fortgeführt/Beendet'; $string['followedby_none'] = 'Keine'; diff --git a/lang/en/tool_lifecycle.php b/lang/en/tool_lifecycle.php index 6561defb..5ede3984 100644 --- a/lang/en/tool_lifecycle.php +++ b/lang/en/tool_lifecycle.php @@ -157,6 +157,7 @@ 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'] = 'Proceeded/Finished'; $string['followedby_none'] = 'None'; diff --git a/templates/workflowoverview.mustache b/templates/workflowoverview.mustache index 213b43de..3acb48b7 100644 --- a/templates/workflowoverview.mustache +++ b/templates/workflowoverview.mustache @@ -158,24 +158,27 @@
{{#str}} courseselectionrun_title, tool_lifecycle{{/str}}
{{#nomanualtriggerinvolved}} -
+
{{#str}} nextrun, tool_lifecycle, {{{nextrun}}} {{/str}} - {{#runnable}} - - - {{#str}} run, tool_lifecycle {{/str}} - -
- {{/runnable}} {{#str}} lastrun, tool_lifecycle, {{lastrun}} {{/str}}
{{/nomanualtriggerinvolved}} {{^nomanualtriggerinvolved}} -
+
{{#str}} manualtriggerenvolved, tool_lifecycle {{/str}}
{{/nomanualtriggerinvolved}} + {{#runnable}} + + {{/runnable}} + {{#counttimetriggers}}
diff --git a/trigger/byrole/lib.php b/trigger/byrole/lib.php index 7f62ca37..7d5d2dd3 100644 --- a/trigger/byrole/lib.php +++ b/trigger/byrole/lib.php @@ -55,7 +55,7 @@ public function get_course_recordset_where($triggerid) { } /** - * Returns triggertype of trigger: trigger, triggertime or exclude. + * If check_course_code() returns true, code to check the given course is placed here * @param \stdClass $course * @param int $triggerid * @return trigger_response diff --git a/trigger/categories/lib.php b/trigger/categories/lib.php index d7d4a555..fc9c84be 100644 --- a/trigger/categories/lib.php +++ b/trigger/categories/lib.php @@ -42,7 +42,7 @@ class categories extends base_automatic { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. + * If check_course_code() returns true, code to check the given course is placed here * @param object $course * @param int $triggerid * @return trigger_response diff --git a/trigger/customfielddelay/lib.php b/trigger/customfielddelay/lib.php index 98c1f155..3efb836e 100644 --- a/trigger/customfielddelay/lib.php +++ b/trigger/customfielddelay/lib.php @@ -35,14 +35,29 @@ class customfielddelay extends base_automatic { /** - * Checks the course and returns a response, which tells if the course should be further processed. + * 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(); + if ($course && $triggerid) { + if ($course->id % 2 == 1) { + return trigger_response::trigger(); + } else { + return trigger_response::exclude(); + } + } else { + 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 true; } /** diff --git a/trigger/delayedcourses/lib.php b/trigger/delayedcourses/lib.php index f2b41ea7..6859f533 100644 --- a/trigger/delayedcourses/lib.php +++ b/trigger/delayedcourses/lib.php @@ -39,7 +39,7 @@ class delayedcourses extends base_automatic { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. + * If check_course_code() returns true, code to check the given course is placed here * @param object $course * @param int $triggerid * @return trigger_response @@ -48,6 +48,14 @@ 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/lib.php b/trigger/lastaccess/lib.php index f5ae2401..9450f3d6 100644 --- a/trigger/lastaccess/lib.php +++ b/trigger/lastaccess/lib.php @@ -39,8 +39,7 @@ */ class lastaccess extends base_automatic { /** - * Checks the course and returns a response, which tells if the course should be further processed. - * + * If check_course_code() returns true, code to check the given course is placed here * @param \stdClass $course * @param int $triggerid * @return trigger_response diff --git a/trigger/lib.php b/trigger/lib.php index d4966c16..fba36cc9 100644 --- a/trigger/lib.php +++ b/trigger/lib.php @@ -137,7 +137,7 @@ public function ensure_validity(array $settings): array { abstract class base_automatic extends base { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. + * If check_course_code() returns true, code to check the given course is placed here * @param \stdClass $course * @param int $triggerid * @return trigger_response @@ -152,6 +152,14 @@ 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 diff --git a/trigger/sitecourse/lib.php b/trigger/sitecourse/lib.php index f66d7011..7581703e 100644 --- a/trigger/sitecourse/lib.php +++ b/trigger/sitecourse/lib.php @@ -37,7 +37,7 @@ class sitecourse extends base_automatic { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. + * If check_course_code() returns true, code to check the given course is placed here * @param object $course * @param int $triggerid * @return trigger_response diff --git a/trigger/specificdate/lib.php b/trigger/specificdate/lib.php index f061d0cd..d5a67d6d 100644 --- a/trigger/specificdate/lib.php +++ b/trigger/specificdate/lib.php @@ -42,7 +42,7 @@ class specificdate extends base_automatic { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. + * If check_course_code() returns true, code to check the given course is placed here * @param object $course * @param int $triggerid * @return trigger_response @@ -51,6 +51,14 @@ 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. diff --git a/trigger/startdatedelay/lib.php b/trigger/startdatedelay/lib.php index bb322a6c..d1c934e3 100644 --- a/trigger/startdatedelay/lib.php +++ b/trigger/startdatedelay/lib.php @@ -40,7 +40,7 @@ class startdatedelay extends base_automatic { /** - * Returns triggertype of trigger: trigger, triggertime or exclude. + * If check_course_code() returns true, code to check the given course is placed here * @param object $course * @param int $triggerid * @return trigger_response diff --git a/workflowoverview.php b/workflowoverview.php index 3a39c0ad..ff82ae7a 100644 --- a/workflowoverview.php +++ b/workflowoverview.php @@ -274,7 +274,7 @@ $lib = lib_manager::get_trigger_lib($trigger->subpluginname); if ($trigger->automatic = !$lib->is_manual_trigger()) { // We need the response type only. - $response = $lib->check_course(null, null); + $response = $lib->default_response(); } $nomanualtriggerinvolved &= $trigger->automatic; if ($showdetails) { @@ -393,8 +393,7 @@ } else if ($triggerid) { // Display courses table with triggered courses of this trigger. $trigger = trigger_manager::get_instance($triggerid); if ($amounts[$trigger->sortindex]->triggered ?? false) { - $table = new triggered_courses_table_trigger($amounts[$trigger->sortindex]->triggered, - $trigger, 'triggerid', $search); + $table = new triggered_courses_table_trigger($trigger, 'triggerid', $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); @@ -415,8 +414,7 @@ } else if ($excluded) { // Display courses table with excluded courses of this trigger. $trigger = trigger_manager::get_instance($excluded); if ($amounts[$trigger->sortindex]->excluded ?? false) { - $table = new triggered_courses_table_trigger($amounts[$trigger->sortindex]->excluded, - $trigger, 'excluded', $search); + $table = new triggered_courses_table_trigger($trigger, 'excluded', $search); ob_start(); $table->out(PAGESIZE, false); $out = ob_get_contents(); @@ -633,7 +631,7 @@ $lib = lib_manager::get_trigger_lib($triggertype); if (!$lib->is_manual_trigger()) { // Function check_course: We need the response type only. - if ($lib->check_course(null, null) == trigger_response::triggertime()) { + if ($lib->default_response() == trigger_response::triggertime()) { continue; } } From c75e1f73b1fe850be864824699cfc67cbf28c6db Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 2 Oct 2025 09:32:17 +0200 Subject: [PATCH 164/170] codechecker issue --- trigger/lib.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trigger/lib.php b/trigger/lib.php index fba36cc9..00826d39 100644 --- a/trigger/lib.php +++ b/trigger/lib.php @@ -156,9 +156,9 @@ public function check_course_code() { * Returns the default response of this trigger. * @return trigger_response */ - public function default_response() { - return trigger_response::trigger(); - } + public function default_response() { + return trigger_response::trigger(); + } /** * Defines if the trigger subplugin is started manually or automatically. From 528e801e65d972b314f330009ca89d63fa768563 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 2 Oct 2025 10:24:27 +0200 Subject: [PATCH 165/170] erase test code --- trigger/customfielddelay/lib.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/trigger/customfielddelay/lib.php b/trigger/customfielddelay/lib.php index 3efb836e..35adcb73 100644 --- a/trigger/customfielddelay/lib.php +++ b/trigger/customfielddelay/lib.php @@ -41,15 +41,7 @@ class customfielddelay extends base_automatic { * @return trigger_response */ public function check_course($course, $triggerid) { - if ($course && $triggerid) { - if ($course->id % 2 == 1) { - return trigger_response::trigger(); - } else { - return trigger_response::exclude(); - } - } else { - return trigger_response::trigger(); - } + return trigger_response::trigger(); } /** @@ -57,7 +49,7 @@ public function check_course($course, $triggerid) { * @return bool */ public function check_course_code() { - return true; + return false; } /** From 39ba470ea826271802dee0c7d690a10402fcbe2b Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 2 Oct 2025 10:28:01 +0200 Subject: [PATCH 166/170] update changes.md --- CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 5f4ccab9..d8e86195 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,8 +4,9 @@ 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-09-28) +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 From cb46b60fbaa192aef470c31fd6b6c63ff766fe33 Mon Sep 17 00:00:00 2001 From: Thomas Niedermaier Date: Thu, 2 Oct 2025 11:12:37 +0200 Subject: [PATCH 167/170] update readme and make it maturity_stable again --- README.md | 7 +++++-- version.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2462991f..811aa7c4 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,13 @@ It provides instructions for administrators as well as for developers to impleme 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 continuously 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. diff --git a/version.php b/version.php index fe9ac396..37b30297 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die; -$plugin->maturity = MATURITY_BETA; +$plugin->maturity = MATURITY_STABLE; $plugin->version = 2025050405; $plugin->component = 'tool_lifecycle'; $plugin->requires = 2022112800; // Requires Moodle 4.1+. From 78d4812e979d1f59f3d1f106e2545e43504812ed Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Mon, 4 Sep 2023 10:13:03 +1000 Subject: [PATCH 168/170] issue#178 refactor subplugins calls --- classes/local/form/form_step_instance.php | 7 +- classes/local/form/form_trigger_instance.php | 7 +- classes/local/manager/lib_manager.php | 27 ++-- classes/local/manager/step_manager.php | 16 +++ classes/local/manager/trigger_manager.php | 15 +++ settings.php | 64 +++++++++- step/lib.php | 24 ++++ .../classes/lifecycle/interaction.php | 33 +++++ .../samplestep/classes/lifecycle/step.php | 27 ++++ .../samplestep/classes/privacy/provider.php | 38 ++++++ .../samplestep/lang/en/tool_samplestep.php | 18 +++ .../fakeplugins/samplestep/version.php | 28 +++++ .../classes/lifecycle/trigger.php | 28 +++++ .../classes/privacy/provider.php | 38 ++++++ .../lang/en/tool_sampletrigger.php | 18 +++ .../fakeplugins/sampletrigger/version.php | 28 +++++ tests/subplugin_test.php | 116 ++++++++++++++++++ trigger/lib.php | 24 ++++ version.php | 2 +- 19 files changed, 542 insertions(+), 16 deletions(-) create mode 100644 tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php create mode 100644 tests/fixtures/fakeplugins/samplestep/classes/lifecycle/step.php create mode 100644 tests/fixtures/fakeplugins/samplestep/classes/privacy/provider.php create mode 100644 tests/fixtures/fakeplugins/samplestep/lang/en/tool_samplestep.php create mode 100644 tests/fixtures/fakeplugins/samplestep/version.php create mode 100644 tests/fixtures/fakeplugins/sampletrigger/classes/lifecycle/trigger.php create mode 100644 tests/fixtures/fakeplugins/sampletrigger/classes/privacy/provider.php create mode 100644 tests/fixtures/fakeplugins/sampletrigger/lang/en/tool_sampletrigger.php create mode 100644 tests/fixtures/fakeplugins/sampletrigger/version.php create mode 100644 tests/subplugin_test.php diff --git a/classes/local/form/form_step_instance.php b/classes/local/form/form_step_instance.php index 0543b54f..85d36689 100644 --- a/classes/local/form/form_step_instance.php +++ b/classes/local/form/form_step_instance.php @@ -191,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 e3d075a3..d7c9834f 100644 --- a/classes/local/form/form_trigger_instance.php +++ b/classes/local/form/form_trigger_instance.php @@ -173,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/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/step_manager.php b/classes/local/manager/step_manager.php index bfb67d3b..fa26279c 100644 --- a/classes/local/manager/step_manager.php +++ b/classes/local/manager/step_manager.php @@ -239,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; } diff --git a/classes/local/manager/trigger_manager.php b/classes/local/manager/trigger_manager.php index 0d847ffb..b24ad41e 100644 --- a/classes/local/manager/trigger_manager.php +++ b/classes/local/manager/trigger_manager.php @@ -250,6 +250,21 @@ public static function get_trigger_types() { $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; } diff --git a/settings.php b/settings.php index e9c524f0..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) { @@ -65,5 +68,62 @@ get_string('config_logreceivedmails_desc', 'tool_lifecycle'), 0)); + $triggers = core_component::get_plugin_list('lifecycletrigger'); + if ($triggers) { + $settings->add(new admin_setting_heading('lifecycletriggerheader', + get_string('triggers_installed', 'tool_lifecycle'), '')); + foreach ($triggers as $trigger => $path) { + $triggername = html_writer::span(get_string('pluginname', 'lifecycletrigger_' . $trigger), + "font-weight-bold"); + $uninstall = ''; + if ($trigger == 'sitecourse' || $trigger == 'delayedcourses') { + $uninstall = html_writer::span(' Depracated. Will be removed with version 5.0.', 'text-danger'); + } + if ($trigger == 'customfieldsemester') { + $settings->add(new admin_setting_description('lifecycletriggersetting_'.$trigger, + $triggername, + get_string('customfieldsemesterdescription', 'tool_lifecycle'))); + } else { + try { + $plugindescription = get_string('plugindescription', 'lifecycletrigger_' . $trigger); + } catch (Exception $e) { + $plugindescription = ""; + } + $settings->add(new admin_setting_description('lifecycletriggersetting_'.$trigger, + $triggername, + $plugindescription.$uninstall)); + $lib = lib_manager::get_trigger_lib($trigger); + $lib->get_plugin_settings(); + } + } + } else { + $settings->add(new admin_setting_heading('adminsettings_notriggers', + get_string('adminsettings_notriggers', 'tool_lifecycle'), '')); + } + + $steps = core_component::get_plugin_list('lifecyclestep'); + if ($steps) { + $settings->add(new admin_setting_heading('lifecyclestepheader', + get_string('steps_installed', 'tool_lifecycle'), '')); + foreach ($steps as $step => $path) { + $stepname = html_writer::span(get_string('pluginname', 'lifecyclestep_' . $step), + "font-weight-bold"); + try { + $plugindescription = get_string('plugindescription', 'lifecyclestep_' . $step); + } catch (Exception $e) { + $plugindescription = ""; + } + $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', + get_string('adminsettings_nosteps', 'tool_lifecycle'), '')); + } + $settings->add(new admin_setting_description('spacer', "", " ")); + $ADMIN->add('tools', $settings); } diff --git a/step/lib.php b/step/lib.php index 955a2bf7..a4757ac0 100644 --- a/step/lib.php +++ b/step/lib.php @@ -161,6 +161,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/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php b/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php new file mode 100644 index 00000000..ac126624 --- /dev/null +++ b/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php @@ -0,0 +1,33 @@ +dirroot . '/admin/tool/lifecycle/step/interactionlib.php'); + +use tool_lifecycle\step\interactionlibbase; + +defined('MOODLE_INTERNAL') || die(); + +class interaction extends interactionlibbase { + + public function get_relevant_capability() + { + } + + public function get_action_tools($process) + { + } + + public function get_status_message($process) + { + } + + public function get_action_string($action, $user) + { + } + + 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..7fdc574e --- /dev/null +++ b/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/step.php @@ -0,0 +1,27 @@ +dirroot . '/admin/tool/lifecycle/step/lib.php'); + +use tool_lifecycle\step\libbase; + +defined('MOODLE_INTERNAL') || die(); + +class step extends libbase { + public function get_subpluginname() + { + return 'sample step'; + } + + public function get_plugin_description() { + return "Sample step plugin"; + } + + 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..de402f95 --- /dev/null +++ b/tests/fixtures/fakeplugins/samplestep/classes/privacy/provider.php @@ -0,0 +1,38 @@ +. + +namespace tool_samplestep\privacy; + +use core_privacy\local\metadata\null_provider; + +/** + * Privacy subsystem implementation for tool_samplestep. + * + * @package tool_samplestep + * @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..cbd7087c --- /dev/null +++ b/tests/fixtures/fakeplugins/samplestep/lang/en/tool_samplestep.php @@ -0,0 +1,18 @@ +. + +$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..440d216e --- /dev/null +++ b/tests/fixtures/fakeplugins/samplestep/version.php @@ -0,0 +1,28 @@ +. + +/** + * Fake component for testing + * + * @package core + * @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..6ec21230 --- /dev/null +++ b/tests/fixtures/fakeplugins/sampletrigger/classes/lifecycle/trigger.php @@ -0,0 +1,28 @@ +dirroot . '/admin/tool/lifecycle/trigger/lib.php'); + +use tool_lifecycle\trigger\base_automatic; + +defined('MOODLE_INTERNAL') || die(); + +class trigger extends base_automatic { + + public function get_subpluginname() + { + return 'sample trigger'; + } + + public function get_plugin_description() { + return "Sample trigger"; + } + + 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..8ba4c99f --- /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. + * + * @package tool_sampletrigger + * @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..ed3104d6 --- /dev/null +++ b/tests/fixtures/fakeplugins/sampletrigger/lang/en/tool_sampletrigger.php @@ -0,0 +1,18 @@ +. + +$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..38a6277f --- /dev/null +++ b/tests/fixtures/fakeplugins/sampletrigger/version.php @@ -0,0 +1,28 @@ +. + +/** + * Fake component for testing + * + * @package core + * @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/subplugin_test.php b/tests/subplugin_test.php new file mode 100644 index 00000000..338931b9 --- /dev/null +++ b/tests/subplugin_test.php @@ -0,0 +1,116 @@ +. + +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; + + +class subplugin_test extends \advanced_testcase { + + public function test_builtin_triggers() { + $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)); + } + } + + public function test_builtin_steps() { + $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)); + } + } + + public function test_additional_triggers() { + $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); + } + + public function test_additional_steps() { + $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/lib.php b/trigger/lib.php index 00826d39..e842525d 100644 --- a/trigger/lib.php +++ b/trigger/lib.php @@ -124,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); + } + } + } /** diff --git a/version.php b/version.php index 37b30297..ca9b2fa0 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die; $plugin->maturity = MATURITY_STABLE; -$plugin->version = 2025050405; +$plugin->version = 2025092300; $plugin->component = 'tool_lifecycle'; $plugin->requires = 2022112800; // Requires Moodle 4.1+. $plugin->supported = [401, 405]; From 1137f8418e9338e0db859f892c13e4dd76eb824d Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Mon, 4 Sep 2023 10:13:03 +1000 Subject: [PATCH 169/170] issue#178 refactor subplugins calls --- step/lib.php | 1 - 1 file changed, 1 deletion(-) diff --git a/step/lib.php b/step/lib.php index a4757ac0..5918c26d 100644 --- a/step/lib.php +++ b/step/lib.php @@ -151,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 From 52dd0e9dfea94d8f7c4d5c0140e9e69e91b11718 Mon Sep 17 00:00:00 2001 From: dustinhuynh Date: Tue, 23 Sep 2025 15:20:40 +1000 Subject: [PATCH 170/170] Reformat code to resolve CI errors --- .../classes/lifecycle/interaction.php | 70 +++++++++++++++---- .../samplestep/classes/lifecycle/step.php | 47 +++++++++++-- .../samplestep/classes/privacy/provider.php | 5 +- .../samplestep/lang/en/tool_samplestep.php | 6 ++ .../fakeplugins/samplestep/version.php | 4 +- .../classes/lifecycle/trigger.php | 44 ++++++++++-- .../classes/privacy/provider.php | 6 +- .../lang/en/tool_sampletrigger.php | 6 ++ .../fakeplugins/sampletrigger/version.php | 4 +- tests/subplugin_test.php | 38 ++++++++-- 10 files changed, 188 insertions(+), 42 deletions(-) diff --git a/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php b/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php index ac126624..4fa861f4 100644 --- a/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php +++ b/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/interaction.php @@ -1,33 +1,75 @@ . -namespace tool_samplestep\lifecycle; +/** + * 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 + */ -global $CFG; -require_once($CFG->dirroot . '/admin/tool/lifecycle/step/interactionlib.php'); +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; -defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->dirroot . '/admin/tool/lifecycle/step/interactionlib.php'); +/** + * Fake class + */ class interaction extends interactionlibbase { - public function get_relevant_capability() - { + /** + * Fake function + */ + public function get_relevant_capability() { } - public function get_action_tools($process) - { + /** + * Fake function + * @param process $process process + */ + public function get_action_tools($process) { } - public function get_status_message($process) - { + /** + * Fake function + * @param process $process process + */ + public function get_status_message($process) { } - public function get_action_string($action, $user) - { + /** + * 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) { } - public function handle_interaction($process, $step, $action = 'default') - { + /** + * 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 index 7fdc574e..6f74bb6f 100644 --- a/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/step.php +++ b/tests/fixtures/fakeplugins/samplestep/classes/lifecycle/step.php @@ -1,26 +1,59 @@ . + +/** + * 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; -defined('MOODLE_INTERNAL') || die(); - +/** + * Fake class + */ class step extends libbase { - public function get_subpluginname() - { + /** + * Fake function + */ + public function get_subpluginname() { return 'sample step'; } + /** + * Fake function + */ public function get_plugin_description() { return "Sample step plugin"; } - public function process_course($processid, $instanceid, $course) - { + /** + * 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 index de402f95..a28bfc96 100644 --- a/tests/fixtures/fakeplugins/samplestep/classes/privacy/provider.php +++ b/tests/fixtures/fakeplugins/samplestep/classes/privacy/provider.php @@ -21,7 +21,8 @@ /** * Privacy subsystem implementation for tool_samplestep. * - * @package 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 { @@ -32,7 +33,7 @@ class provider implements null_provider { * * @return string the reason */ - public static function get_reason() : string { + 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 index cbd7087c..dcf24499 100644 --- a/tests/fixtures/fakeplugins/samplestep/lang/en/tool_samplestep.php +++ b/tests/fixtures/fakeplugins/samplestep/lang/en/tool_samplestep.php @@ -14,5 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * 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 index 440d216e..7029ad2e 100644 --- a/tests/fixtures/fakeplugins/samplestep/version.php +++ b/tests/fixtures/fakeplugins/samplestep/version.php @@ -16,8 +16,8 @@ /** * Fake component for testing - * - * @package core + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @package tool_lifecycle * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/tests/fixtures/fakeplugins/sampletrigger/classes/lifecycle/trigger.php b/tests/fixtures/fakeplugins/sampletrigger/classes/lifecycle/trigger.php index 6ec21230..08f756bf 100644 --- a/tests/fixtures/fakeplugins/sampletrigger/classes/lifecycle/trigger.php +++ b/tests/fixtures/fakeplugins/sampletrigger/classes/lifecycle/trigger.php @@ -1,27 +1,61 @@ . + +/** + * 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; -defined('MOODLE_INTERNAL') || die(); +/** + * Fake class + */ class trigger extends base_automatic { - public function get_subpluginname() - { + /** + * Fake function + */ + public function get_subpluginname() { return 'sample trigger'; } + /** + * Fake function + */ public function get_plugin_description() { return "Sample trigger"; } - public function check_course($course, $triggerid) - { + /** + * 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 index 8ba4c99f..9d37e207 100644 --- a/tests/fixtures/fakeplugins/sampletrigger/classes/privacy/provider.php +++ b/tests/fixtures/fakeplugins/sampletrigger/classes/privacy/provider.php @@ -20,8 +20,8 @@ /** * Privacy subsystem implementation for tool_sampletrigger. - * - * @package 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 { @@ -32,7 +32,7 @@ class provider implements null_provider { * * @return string the reason */ - public static function get_reason() : string { + 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 index ed3104d6..bc8a5fa6 100644 --- a/tests/fixtures/fakeplugins/sampletrigger/lang/en/tool_sampletrigger.php +++ b/tests/fixtures/fakeplugins/sampletrigger/lang/en/tool_sampletrigger.php @@ -14,5 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * 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 index 38a6277f..8bad1333 100644 --- a/tests/fixtures/fakeplugins/sampletrigger/version.php +++ b/tests/fixtures/fakeplugins/sampletrigger/version.php @@ -16,8 +16,8 @@ /** * Fake component for testing - * - * @package core + * @copyright 2025 Catalyst IT Australia Pty Ltd + * @package tool_lifecycle * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/tests/subplugin_test.php b/tests/subplugin_test.php index 338931b9..fa71e846 100644 --- a/tests/subplugin_test.php +++ b/tests/subplugin_test.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace tool_lifecycle; + defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -23,10 +25,20 @@ 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 { -class subplugin_test extends \advanced_testcase { - public function test_builtin_triggers() { + /** + * 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(); @@ -40,7 +52,11 @@ public function test_builtin_triggers() { } } - public function test_builtin_steps() { + /** + * 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(); @@ -54,11 +70,15 @@ public function test_builtin_steps() { } } - public function test_additional_triggers() { + /** + * 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); + $mockedcomponent = new \ReflectionClass(\core_component::class); $mockedplugins = $mockedcomponent->getProperty('plugins'); $mockedplugins->setAccessible(true); $plugins = $mockedplugins->getValue(); @@ -81,11 +101,15 @@ public function test_additional_triggers() { $mockedplugins->setValue($plugins); } - public function test_additional_steps() { + /** + * 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); + $mockedcomponent = new \ReflectionClass(\core_component::class); $mockedplugins = $mockedcomponent->getProperty('plugins'); $mockedplugins->setAccessible(true); $plugins = $mockedplugins->getValue();