From 25e9b6921d4f30603d04cfb6b34b79d5f5a71f03 Mon Sep 17 00:00:00 2001
From: tamaro
Date: Tue, 26 May 2026 16:46:50 +0200
Subject: [PATCH 1/6] lib_his: fix dmlreadexception in get_courses_categories()
PostgreSQL requires ORDER BY expressions to appear in the select list
for SELECT DISTINCT queries.
---
lib_his.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib_his.php b/lib_his.php
index 84295c3..e3f006d 100644
--- a/lib_his.php
+++ b/lib_his.php
@@ -677,7 +677,7 @@ function get_courses_categories(int $veranstid, bool $updatehelptablesifnecessar
$otherueidssql . ") OR ueid in (" . $ueids . ")";
$origins = implode(", ", array_map($helpfuntion1, $DB->get_records_sql($originssql)));
if (!empty($origins)) {
- $categoriessql = "SELECT DISTINCT lsfcat.mdlid, coursecat.name
+ $categoriessql = "SELECT DISTINCT lsfcat.mdlid, coursecat.name, coursecat.sortorder
FROM {local_lsf_unification_category} lsfcat
JOIN {course_categories} coursecat ON lsfcat.mdlid = coursecat.id
WHERE lsfcat.ueid in (" . $origins . ")
From 0315daec9a7416b38180ae2a500cf75593d094f8 Mon Sep 17 00:00:00 2001
From: tamaro
Date: Fri, 20 Mar 2026 21:25:57 +0100
Subject: [PATCH 2/6] Remove function print_res_selection from request.php
This function was added a long time ago but never used. For now it
will be removed.
---
request.php | 89 -----------------------------------------------------
1 file changed, 89 deletions(-)
diff --git a/request.php b/request.php
index e2a226c..a8d7f36 100644
--- a/request.php
+++ b/request.php
@@ -168,95 +168,6 @@ function print_final() {
" . $linktext['course'] . " ");
}
-/**
- * Print result selection.
- * @return void
- * @throws coding_exception
- * @throws dml_exception
- */
-function print_res_selection() {
- global $CFG, $OUTPUT, $courseid;
-
- $acceptorid = get_course_acceptor($courseid);
- if (get_config('local_lsf_unification', 'restore_old_courses')) {
- $backupfiles = get_backup_files($acceptorid);
- }
- if (get_config('local_lsf_unification', 'restore_templates')) {
- $templatefiles = get_template_files();
- }
- if (empty($backupfiles) && empty($templatefiles)) {
- print_final();
- } else if (!empty($templatefiles)) {
- $alternativecounter = 1;
-
- // Continue with the course template.
- if (get_config('local_lsf_unification', 'restore_templates') && !empty($templatefiles)) {
- $cats = [];
- $i = 0;
- foreach ($templatefiles as $id => $fileinfo) {
- $cats[$fileinfo->category][$id] = $fileinfo;
- }
- // If there are items without a category move them to the end.
- $catkeys = array_keys($cats);
- if (!empty($cats) && array_pop($catkeys) == "") {
- array_unshift($cats, array_pop($cats));
- }
- // Render.
- $pretemplate = get_string('pre_template', 'local_lsf_unification', $alternativecounter++);
- echo "" . $pretemplate . ' ';
- foreach ($cats as $name => $catfiles) {
- if (!empty($name)) {
- // LEARNWEB-TODO: move the two js-lines to a new file.
- $jscheck = "(document.getElementById('reslist" . $i . "').style.display == 'none') ? 'block' : 'none'";
- $js = "document.getElementById('reslist" . ($i) . "').style.display=(" . $jscheck . ");";
-
- $exp = "'url(" . $OUTPUT->image_url("t/expanded")->out() . ")'";
- $col = "'url(" . $OUTPUT->image_url("t/collapsed")->out() . ")'";
- $js2check = "(document.getElementById('reslist" . $i . "').style.display == 'none') ? " . $col . ":" . $exp;
- $js2 = "document.getElementById('reslistselector" . ($i) . "').style.listStyleImage=(" . $js2check . ");";
- $alement = '[' . $name . ' ] ';
- $reslist = '';
- echo '' . $alement . $reslist;
- }
- foreach ($catfiles as $id => $fileinfo) {
- $lines = explode("\n", trim($fileinfo->info, " \t\r\n"), 2);
- $text = mb_convert_encoding($lines[0], 'UTF-8', 'ISO-8859-1');
- $imgnavitem = $OUTPUT->image_url("i/navigationitem")->out();
- $href = "duplicate_course.php?courseid=" . $courseid . "&filetype=t&fileid=" . $id;
- echo ' ' . $text . " ";
- if (count($lines) == 2) {
- echo " " . mb_convert_encoding($lines[1], 'UTF-8', 'ISO-8859-1');
- }
- echo " ";
- }
- if (!empty($name)) {
- echo " ";
- }
- }
- echo " ";
- }
-
- // Continue with a blank course.
- $notemplate = get_string('no_template', 'local_lsf_unification', $alternativecounter++);
- $emptycourse = get_string('continue_with_empty_course', 'local_lsf_unification');
- $href = "request.php?courseid=" . $courseid . "&answer=7";
- echo "" . $notemplate . " ";
-
- // Duplicate course from the course .
- if (get_config('local_lsf_unification', 'restore_old_courses') && !empty($backupfiles)) {
- echo "" . get_string('template_from_course', 'local_lsf_unification', $alternativecounter++) . " ";
- // Sortiere die Backups alphabetisch.
- uasort($backupfiles, function ($a, $b) {
- return strcmp($a->course->fullname, $b->course->fullname);
- });
- foreach ($backupfiles as $id => $fileinfo) {
- $href = "duplicate_course.php?courseid=" . $courseid . "&filetype=b&fileid=" . $id;
- echo "" . $fileinfo->course->fullname . " (" . $fileinfo->datetime . ") ";
- }
- echo " ";
- }
- }
-}
/**
* Prints the remote course creation process.
From d20e45e4c1d0807978c158c860ae2967a24afdb8 Mon Sep 17 00:00:00 2001
From: tamaro
Date: Sun, 22 Mar 2026 00:14:50 +0100
Subject: [PATCH 3/6] =?UTF-8?q?Update=20comment=20for=20=E2=80=98distance?=
=?UTF-8?q?=E2=80=99=20of=20=E2=80=98local=5Flsf=5Funification=5Fcategoryp?=
=?UTF-8?q?arenthood=E2=80=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
db/install.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/db/install.xml b/db/install.xml
index fe93cdd..b0da12b 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -25,7 +25,7 @@
-
+
From 8f88424e5ba640ff45872d2be6742bc38e16825b Mon Sep 17 00:00:00 2001
From: Tamaro Walter
Date: Thu, 5 Mar 2026 12:02:18 +0100
Subject: [PATCH 4/6] Create pg_lite object in
insert_missing_helptable_entries()
---
.../task/insert_missing_helptable_entries.php | 11 ++--------
lib_his.php | 20 +++++++++++++++++--
update_helptable.php | 9 ---------
3 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/classes/task/insert_missing_helptable_entries.php b/classes/task/insert_missing_helptable_entries.php
index 3e0a15e..87c6d2f 100644
--- a/classes/task/insert_missing_helptable_entries.php
+++ b/classes/task/insert_missing_helptable_entries.php
@@ -36,15 +36,8 @@ public function get_name(): string {
#[\Override]
public function execute(): void {
- global $pgdb, $CFG;
- include_once($CFG->dirroot . '/local/lsf_unification/class_pg_lite.php');
- include_once($CFG->dirroot . '/local/lsf_unification/lib_features.php');
- $pgdb = new pg_lite();
- $connected = $pgdb->connect();
- $recourceid = pg_connection_status($pgdb->connection);
- mtrace('! = unknown category found, ? = unknown linkage found;' . 'Verbindung: ' .
- ($connected ? 'ja' : 'nein') . ' (' . $recourceid . ')');
+ global $CFG;
+ require_once($CFG->dirroot . '/local/lsf_unification/lib_his.php');
insert_missing_helptable_entries(true);
- $pgdb->dispose();
}
}
diff --git a/lib_his.php b/lib_his.php
index e3f006d..28186e7 100644
--- a/lib_his.php
+++ b/lib_his.php
@@ -727,8 +727,21 @@ function get_courses_categories(int $veranstid, bool $updatehelptablesifnecessar
* @return void
*/
function insert_missing_helptable_entries(bool $debugoutput = false, bool $tryeverything = false): void {
+ // LEARNWEB-TODO: Please refactor this horrible function.
+ global $pgdb, $DB, $CFG;
+ require_once($CFG->dirroot . '/local/lsf_unification/class_pg_lite.php');
+ require_once($CFG->dirroot . '/local/lsf_unification/lib_features.php');
+
+ // Build db connection.
+ $pgdb = new pg_lite();
+ $connected = $pgdb->connect();
+ $recourceid = pg_connection_status($pgdb->connection);
+ if ($debugoutput) {
+ mtrace('! = unknown category found, ? = unknown linkage found;' . 'Verbindung: ' .
+ ($connected ? 'ja' : 'nein') . ' (' . $recourceid . ')');
+ }
+
$a = 1;
- global $pgdb, $DB;
$list1 = "";
$list2 = "";
$records1 = $DB->get_recordset('local_lsf_unification_category', null, '', 'ueid');
@@ -755,7 +768,9 @@ function insert_missing_helptable_entries(bool $debugoutput = false, bool $tryev
$records2unique[$hislsftitle->ueid][$hislsftitle->uebergeord] != true)
) {
$a++;
- echo $hislsftitle->ueid . " ";
+ if ($debugoutput) {
+ echo $hislsftitle->ueid . " ";
+ }
}
if (!isset($records1unique[$hislsftitle->ueid])) {
// Create match-table-entry if not existing.
@@ -863,6 +878,7 @@ function insert_missing_helptable_entries(bool $debugoutput = false, bool $tryev
flush();
}
}
+ $pgdb->dispose();
}
/**
diff --git a/update_helptable.php b/update_helptable.php
index 4114850..94311e6 100644
--- a/update_helptable.php
+++ b/update_helptable.php
@@ -29,19 +29,10 @@
require_once(dirname(__FILE__) . "/class_pg_lite.php");
require_once(dirname(__FILE__) . "/lib.php");
require_once(dirname(__FILE__) . "/lib_features.php");
-
-// Check permissions.
-require_admin();
-
// HIS category origin id.
$tryeverything = optional_param('tryeverything', false, PARAM_INT);
set_time_limit(30 * 60);
-
-$pgdb = new pg_lite();
-$pgdb->connect();
insert_missing_helptable_entries(false, $tryeverything);
-$pgdb->dispose();
-
$returnto = new moodle_url('/local/lsf_unification/helptablemanager.php');
redirect($returnto, get_string('update_helptable_notification', 'local_lsf_unification'));
From adc6ccad76a7c6d502525b373d30ef3b51cf40a1 Mon Sep 17 00:00:00 2001
From: Tamaro Walter
Date: Thu, 5 Mar 2026 12:03:14 +0100
Subject: [PATCH 5/6] Redesign mapping table with templates and adhoc task
---
...insert_missing_helptable_entries_adhoc.php | 33 ++++
helptablemanager.php | 170 +++++++++---------
lang/de/local_lsf_unification.php | 2 +
lang/en/local_lsf_unification.php | 5 +-
.../helptable_manager/childlist.mustache | 105 +++++++++++
.../helptable_manager/mapping_submit.mustache | 77 ++++++++
templates/helptable_manager/overview.mustache | 42 +++++
update_helptable.php | 38 ----
version.php | 2 +-
9 files changed, 348 insertions(+), 126 deletions(-)
create mode 100644 classes/task/insert_missing_helptable_entries_adhoc.php
create mode 100644 templates/helptable_manager/childlist.mustache
create mode 100644 templates/helptable_manager/mapping_submit.mustache
create mode 100644 templates/helptable_manager/overview.mustache
delete mode 100644 update_helptable.php
diff --git a/classes/task/insert_missing_helptable_entries_adhoc.php b/classes/task/insert_missing_helptable_entries_adhoc.php
new file mode 100644
index 0000000..25bf0b1
--- /dev/null
+++ b/classes/task/insert_missing_helptable_entries_adhoc.php
@@ -0,0 +1,33 @@
+.
+
+namespace local_lsf_unification\task;
+
+/**
+ * Ad hoc task to update the lsf unification tables.
+ *
+ * @package local_lsf_unification
+ * @copyright 2026 Tamaro Walter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class insert_missing_helptable_entries_adhoc extends \core\task\adhoc_task {
+ #[\Override]
+ public function execute(): void {
+ global $CFG;
+ require_once($CFG->dirroot . '/local/lsf_unification/lib_his.php');
+ insert_missing_helptable_entries(true);
+ }
+}
diff --git a/helptablemanager.php b/helptablemanager.php
index c87a17f..b66a2ba 100644
--- a/helptablemanager.php
+++ b/helptablemanager.php
@@ -21,7 +21,13 @@
* @copyright 2025 Tamaro Walter
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
+use core\output\notification;
+use core\task\manager;
+use local_lsf_unification\task\insert_missing_helptable_entries_adhoc;
+
define('NO_OUTPUT_BUFFERING', true);
+
require_once("../../config.php");
require_once("$CFG->libdir/adminlib.php");
require_once("./lib_his.php");
@@ -29,38 +35,52 @@
require_login();
admin_externalpage_setup('local_lsf_unification_helptable');
-echo $OUTPUT->header();
-echo $OUTPUT->heading('HISLSF Helptable Management');
-
$originid = optional_param('originid', -1, PARAM_INT); // HIS category origin id.
$mainid = optional_param('mainid', -1, PARAM_INT); // HIS catecory id.
$mdlid = optional_param('mdlid', -1, PARAM_INT); // Moodle category id.
$maxorigin = optional_param('maxorigin', 0, PARAM_INT); // Max (his origin ids).
$delete = optional_param('delete', 0, PARAM_INT); // Category id where to remove a matching.
+$updatehelptable = optional_param('updatehelptable', 0, PARAM_BOOL);
+$basepath = '/local/lsf_unification/helptablemanager.php';
+$output = "";
if ($originid == -1) {
- $updatetable = get_string('update_helptable', 'local_lsf_unification');
- $createmappings = get_string('create_mappings', 'local_lsf_unification');
- echo "" . $OUTPUT->box('' . $updatetable . ' ') . "
";
- echo "" . $OUTPUT->box('' . $createmappings . ' ') . "
";
+ require_capability('moodle/site:config', context_system::instance());
+ if ($updatehelptable) {
+ $existingtasks = \core\task\manager::get_adhoc_tasks(insert_missing_helptable_entries_adhoc::class);
+ if (empty($existingtasks)) {
+ $updatetime = time() + 60;
+ $task = new insert_missing_helptable_entries_adhoc();
+ $task->set_next_run_time($updatetime);
+ manager::queue_adhoc_task($task, true);
+ $params = ['time' => userdate($updatetime), 'link' => (new moodle_url("/admin/tool/task/adhoctasks.php"))->out()];
+ $info = get_string('update_helptable_notification', 'local_lsf_unification', $params);
+ } else {
+ $params = ['link' => (new moodle_url("/admin/tool/task/adhoctasks.php"))->out()];
+ $info = get_string('update_helptable_already_queued', 'local_lsf_unification', $params);
+ }
+ redirect(new moodle_url($basepath), $info);
+ }
+ $mustachedata = [
+ 'isadmin' => has_capability('moodle/site:config', context_system::instance()),
+ 'createmappings' => new moodle_url($basepath, ['originid' => 0]),
+ ];
+ $output = $OUTPUT->render_from_template('local_lsf_unification/helptable_manager/overview', $mustachedata);
} else if ($mainid == -1) {
if (!empty($delete)) {
set_cat_mapping($delete, 0);
}
- $prefix = "-";
if (empty($originid)) {
$origins = implode(", ", get_his_toplevel_originids());
- $parents = ["" => "- Lehrveranstaltungen"];
- $prefix .= "-";
+ $parents = ["" => "Lehrveranstaltungen"];
} else {
$origins = $originid;
$id = $originid;
$parents = [$id => " " . get_newest_element($id)->txt];
while ($parent = get_newest_parent($id)) {
foreach ($parents as $key => $txt) {
- $parents[$key] = "-" . $parents[$key];
+ $parents[$key] = $parents[$key];
}
- $prefix .= "-";
if (($id == $parent->ueid) || ($id == $parent->origin)) {
break;
}
@@ -68,111 +88,89 @@
$parents[$id] = " " . $parent->txt;
}
}
- $parentstxt = "";
- foreach ($parents as $id => $txt) {
+ $parentstxt = [];
+ foreach (array_reverse($parents, true) as $id => $txt) {
$navigate = get_string('navigate', 'local_lsf_unification');
- $parentstxt = " [" . $navigate . " ] " . $txt . " " . $parentstxt;
+ $parentstxt[] = [
+ 'parentlink' => new moodle_url($basepath, ['originid' => $id]),
+ 'parenttext' => $txt,
+ ];
}
$sublevels = get_newest_sublevels($origins);
- $childlist = "";
+ $childs = [];
foreach ($sublevels as $child) {
$child->mdlid = get_mdlid($child->origin);
$child->name = empty($child->mdlid) ? "" : get_mdlname($child->origin);
$maxorigin = ($child->origin > $maxorigin) ? $child->origin : $maxorigin;
- $str = [
- 0 => get_string('navigate', 'local_lsf_unification'),
- 1 => get_string('not_mapped', 'local_lsf_unification'),
- 2 => get_string(empty($child->mdlid) ? 'map' : 'overwrite', 'local_lsf_unification'),
- 3 => get_string('delete', 'local_lsf_unification'),
+ $childs[] = [
+ 'sublevels' => has_sublevels($child->ueid),
+ 'childoriginlink' => new moodle_url($basepath, ['originid' => $child->origin]),
+ 'idchildorigin' => "idch_" . ($child->origin),
+ 'prefixchildtxt' => $child->txt,
+ 'emptychildmdlid' => empty($child->mdlid),
+ 'categorylink' => new moodle_url('/course/index.php', ['categoryid' => $child->mdlid]),
+ 'childname' => $child->name,
+ 'namechildorigin' => 'ch_' . ($child->origin),
+ 'deletelink' => new moodle_url($basepath, ['originid' => $originid, 'delete' => $child->origin]),
];
- $childlist .=
- "" . (
- (!has_sublevels($child->ueid))
- ? " "
- : (" [
- " . $str[0] . " ]
- ")
- ) . "
-
- " . $prefix . " " . ($child->txt) . "
-
- [" .
- (empty($child->mdlid)
- ? $str[1]
- : ("" . ($child->name) . " ")) . "]
-
- [
-
- " . $str[2] . "
- ]
- " . (
- (empty($child->mdlid))
- ? " "
- : (" [
- " . $str[3] . "
- ]
- ")
- ) .
- " ";
}
$maincategories = get_mdl_toplevels();
- $options = "";
+ $options = [];
foreach ($maincategories as $id => $txt) {
- $options .= "" . $txt->name . " ";
+ $options[] = ['value' => $id, 'text' => $txt->name];
}
- $catchoice = "" . get_string('main_category', 'local_lsf_unification') . " :
- " . $options . "
-
-
- ";
- echo "
- " . $OUTPUT->box($catchoice) . "
- ";
+ $mustachedata = [
+ 'parents' => $parentstxt,
+ 'childs' => $childs,
+ 'options' => $options,
+ 'originid' => $originid,
+ 'maxorigin' => $maxorigin,
+ ];
+ $output = $OUTPUT->render_from_template('local_lsf_unification/helptable_manager/childlist', $mustachedata);
} else if ($mdlid == -1) {
$hiddenfields = "";
- $childlist = "";
+ $childs = [];
+ $hiddenfields = [];
for ($i = 0; $i <= $maxorigin; $i++) {
if (isset($_GET["ch_" . $i])) {
- $hiddenfields .= " ";
- $childlist .= (empty($childlist) ? "" : " ") . "-" . (get_newest_element($i)->txt) . "";
+ $hiddenfields[] = ['hiddenname' => 'ch_' . $i];
+ $childs[] = ['childtext' => get_newest_element($i)->txt];
}
}
$subcats = get_mdl_sublevels($mainid);
$displaylist = [];
- $displaylist = core_course_category::make_categories_list();
- $options = "";
+ $displaylist = core_course_category::make_categories_list();
+ $options = [];
foreach ($displaylist as $id => $txt) {
if (isset($subcats[$id])) {
- $options .= "" . $txt . " ";
+ $options[] = ['optionid' => $id, 'optiontext' => $txt];
}
}
- echo "";
+ // Additional variable if only 1 category is available for mapping.
+ $singlecat = count($options) == 1 ? ['category' => $options[0]['optiontext'], 'categoryid' => $options[0]['optionid']] : [];
+ $mustachedata = [
+ 'childs' => $childs,
+ 'options' => $options,
+ 'originid' => $originid,
+ 'mainid' => $mainid,
+ 'maxorigin' => $maxorigin,
+ 'hiddenfields' => $hiddenfields,
+ 'single_category' => $singlecat,
+ ];
+ $output = $OUTPUT->render_from_template('local_lsf_unification/helptable_manager/mapping_submit', $mustachedata);
} else {
- $mapchilds = [];
- $maptxt = "";
$count = 0;
for ($i = 0; $i <= $maxorigin; $i++) {
if (isset($_GET["ch_" . $i])) {
- $maptxt .= $i . "-" . $mdlid . " ";
set_cat_mapping($i, $mdlid);
$count++;
}
}
- echo "" . $OUTPUT->box($maptxt . "" . $count . " " . get_string('map_done', 'local_lsf_unification')) . "
";
- echo "" . $OUTPUT->box('' . get_string('create_mappings', 'local_lsf_unification') . ' ') . "
";
+ $string = $count . " " . get_string('map_done', 'local_lsf_unification');
+ redirect(new moodle_url($basepath, ['originid' => 0]), $string);
}
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading('HISLSF Helptable Management');
+echo $output;
echo $OUTPUT->footer();
diff --git a/lang/de/local_lsf_unification.php b/lang/de/local_lsf_unification.php
index a448c87..fa85cf7 100644
--- a/lang/de/local_lsf_unification.php
+++ b/lang/de/local_lsf_unification.php
@@ -108,6 +108,8 @@
$string['main_category'] = 'Hauptkategorie';
$string['map'] = 'zuordnen';
$string['map_done'] = 'Zuordnung(en) wurde(n) festgehalten';
+$string['map_list_empty'] = "Keine Elemente ausgewählt";
+$string['map_submit'] = 'Folgende Veranstaltungen werden zugeordnet:';
$string['mapped'] = 'zugeordnet';
$string['max_import_age'] = 'Maximales Kurs-Alter';
$string['max_import_age_description'] = 'Maximales Alter, dass ein Kurs haben darf, danach wird er nicht mehr zur Auswahl angezeigt.';
diff --git a/lang/en/local_lsf_unification.php b/lang/en/local_lsf_unification.php
index f5c355d..daf6c3a 100644
--- a/lang/en/local_lsf_unification.php
+++ b/lang/en/local_lsf_unification.php
@@ -121,6 +121,8 @@
$string['main_category'] = 'Maincateogry';
$string['map'] = 'map';
$string['map_done'] = 'mapping(s) saved';
+$string['map_list_empty'] = "No items selected";
+$string['map_submit'] = "You are about to map the following courses:";
$string['mapped'] = 'mapped';
$string['max_import_age'] = 'Max Import Age';
$string['max_import_age_description'] = 'The maximum age, a course may have before being imported.';
@@ -177,7 +179,8 @@
$string['task_missing_helptable_entries'] = 'Update table of LSF categories.';
$string['template_from_course'] = 'Alternative {$a}: Include contents from an existing course ...';
$string['update_helptable'] = 'Update Helptable With HIS-LSF data';
-$string['update_helptable_notification'] = 'All tables of lsf_unification were updated';
+$string['update_helptable_already_queued'] = 'Task to update tables is already queued. Click here for more information.';
+$string['update_helptable_notification'] = 'The Tables will be updated at {$a->time}. Click here for more information.';
$string['warning_cannot_enrol_nologin'] = "person wasn't enrolled (no username found)";
$string['warning_cannot_enrol_nouser'] = "person wasn't enrolled (no user found)";
$string['warning_cannot_enrol_other'] = "person wasn't enrolled";
diff --git a/templates/helptable_manager/childlist.mustache b/templates/helptable_manager/childlist.mustache
new file mode 100644
index 0000000..555bd9f
--- /dev/null
+++ b/templates/helptable_manager/childlist.mustache
@@ -0,0 +1,105 @@
+{{!
+ 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 local_lsf_unification/helptable_manager/childlist
+
+ This template renders the Admin settings page to create/edit/delete category matching from the lsf to moodle.
+
+ Example (json):
+ {
+
+ }
+}}
+
+
+ {{! Breadcrumb navigation }}
+
+
+ {{#parents}}
+
+ {{parenttext}}
+
+ {{/parents}}
+
+
+
+
+
diff --git a/templates/helptable_manager/mapping_submit.mustache b/templates/helptable_manager/mapping_submit.mustache
new file mode 100644
index 0000000..eb6ffb0
--- /dev/null
+++ b/templates/helptable_manager/mapping_submit.mustache
@@ -0,0 +1,77 @@
+{{!
+ 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 local_lsf_unification/helptable_manager/mapping_submit
+
+ This template renders the mapping submit step.
+}}
+
+
diff --git a/templates/helptable_manager/overview.mustache b/templates/helptable_manager/overview.mustache
new file mode 100644
index 0000000..a82734b
--- /dev/null
+++ b/templates/helptable_manager/overview.mustache
@@ -0,0 +1,42 @@
+{{!
+ 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 local_lsf_unification/helptable_manager/overview
+
+ This template renders the Admin settings page to create/edit/delete category matching from the lsf to moodle.
+
+ Example (json):
+ {
+
+ }
+}}
+
+{{#isadmin}}
+
+{{/isadmin}}
+
+
+
+ {{#str}} create_mappings, local_lsf_unification {{/str}}
+
+
diff --git a/update_helptable.php b/update_helptable.php
deleted file mode 100644
index 94311e6..0000000
--- a/update_helptable.php
+++ /dev/null
@@ -1,38 +0,0 @@
-.
-
-/**
- * Page that shows the update_helptable process.
- *
- * @package local_lsf_unification
- * @copyright 2025 Tamaro Walter
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-use local_lsf_unification\pg_lite;
-
-require_once("../../config.php");
-global $CFG;
-require_once(dirname(__FILE__) . "/class_pg_lite.php");
-require_once(dirname(__FILE__) . "/lib.php");
-require_once(dirname(__FILE__) . "/lib_features.php");
-// HIS category origin id.
-$tryeverything = optional_param('tryeverything', false, PARAM_INT);
-
-set_time_limit(30 * 60);
-insert_missing_helptable_entries(false, $tryeverything);
-$returnto = new moodle_url('/local/lsf_unification/helptablemanager.php');
-redirect($returnto, get_string('update_helptable_notification', 'local_lsf_unification'));
diff --git a/version.php b/version.php
index ab0cb25..ae06e49 100644
--- a/version.php
+++ b/version.php
@@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die;
-$plugin->version = 2025123101;
+$plugin->version = 2025123102;
$plugin->component = 'local_lsf_unification';
$plugin->requires = 2024100700; // Require Moodle 4.5.
$plugin->supported = [405, 501];
From f7a28f9d506b7d9d4ea07047819e06124c7b52ab Mon Sep 17 00:00:00 2001
From: tamaro
Date: Sun, 22 Mar 2026 00:15:19 +0100
Subject: [PATCH 6/6] Refactor insert_missing_helptable_entries: improve
performance, error handling and correctnes
- Replace per-iteration pg_query in do-while with in-memory lookup via $lsfcategories array
- Replace find_origin_category DB queries with in-memory traversal
- Use insert_records for bulk relation inserts instead of individual insert_record calls
- Replace get_record + update_record with set_field for txt2 updates
- Wrap all DB writes in a single transaction; roll back on failure
- Remove nested try-catch blocks that silently swallowed errors
- Improve mtrace logging for sync summary and failure messages
---
lib_his.php | 278 ++++++++++++++++++++++------------------------------
1 file changed, 119 insertions(+), 159 deletions(-)
diff --git a/lib_his.php b/lib_his.php
index 28186e7..c443635 100644
--- a/lib_his.php
+++ b/lib_his.php
@@ -301,32 +301,27 @@ function is_course_of_teacher(int $veranstid, string $username): bool {
}
/**
- * Find_origin_category is NOT a required function for the lsf_unification plugin, it is used
- * internally only
+ * Finds the origin category of a given category by traversing the quellid chain in memory.
*
- * @param int $quellid
- * @return int $origin
+ * Categories in HIS_UEBERSCHRIFT are duplicated each semester, each with a new ueid but
+ * referencing their original via quellid. This function walks the quellid chain until it
+ * reaches the root, i.e. the category where quellid == ueid.
+ *
+ * Example: ueid=71420 (SS 2012) → quellid=66656 (WS 2011) → quellid=66656 (root) → returns 66656
+ *
+ * @param int $ueid The ueid of the category to find the origin for.
+ * @param array $lsfcategories All categories indexed by ueid, as returned by pg_fetch_all + array_column.
+ * @return int The ueid of the origin (root) category.
*/
-function find_origin_category(int $quellid): int {
- global $pgdb;
- $origin = $quellid;
+function find_origin_category(int $ueid, array $lsfcategories): int {
+ $origin = $ueid;
do {
- $quellid = $origin;
- $q = pg_query(
- $pgdb->connection,
- "SELECT quellid FROM " . HIS_UEBERSCHRIFT . " WHERE ueid = '" . $quellid . "'"
- );
- if ($hislsftitle = pg_fetch_object($q)) {
- $q2 = pg_query(
- $pgdb->connection,
- "SELECT quellid FROM " . HIS_UEBERSCHRIFT . " WHERE ueid = '" .
- ($hislsftitle->quellid) . "'"
- );
- if ($hislsftitle2 = pg_fetch_object($q2)) {
- $origin = $hislsftitle->quellid;
- }
+ $ueid = $origin;
+ $node = $lsfcategories[$ueid] ?? null;
+ if ($node && isset($lsfcategories[$node->quellid])) {
+ $origin = $node->quellid;
}
- } while (!empty($origin) && $quellid != $origin);
+ } while (!empty($origin) && $ueid !== $origin);
return $origin;
}
@@ -719,166 +714,131 @@ function get_courses_categories(int $veranstid, bool $updatehelptablesifnecessar
}
/**
- * updates the helptables
- * insert_missing_helptable_entries is a required function for the lsf_unification plugin
+ * Syncs missing categories and parenthood relations from HIS_UEBERSCHRIFT into lsf_unfication tables.
+ *
+ * Loads all known categories and relations from the Moodle DB into memory, then fetches
+ * all categories from the lsf_view and inserts any missing entries:
*
- * @param bool $debugoutput
- * @param bool $tryeverything
+ * - local_lsf_unification_category: one entry per unknown ueid, including its origin
+ * (root of the quellid chain) and direct parent.
+ * - local_lsf_unification_categoryparenthood: full closure table entries for each unknown
+ * relation, storing the distance from a category to each of its ancestors.
+ * - txt2 on local_lsf_unification_category: the full path from root to category as a
+ * slash-separated string (e.g. "Vorlesungen/Mathematik und Informatik").
+ *
+ * @param bool $debugoutput If true, logs a summary of inserted categories and relations via mtrace.
* @return void
+ * @throws Exception If any DB operation fails, after rolling back the transaction.
*/
-function insert_missing_helptable_entries(bool $debugoutput = false, bool $tryeverything = false): void {
- // LEARNWEB-TODO: Please refactor this horrible function.
+function insert_missing_helptable_entries(bool $debugoutput = false): void {
global $pgdb, $DB, $CFG;
require_once($CFG->dirroot . '/local/lsf_unification/class_pg_lite.php');
require_once($CFG->dirroot . '/local/lsf_unification/lib_features.php');
// Build db connection.
$pgdb = new pg_lite();
- $connected = $pgdb->connect();
- $recourceid = pg_connection_status($pgdb->connection);
- if ($debugoutput) {
- mtrace('! = unknown category found, ? = unknown linkage found;' . 'Verbindung: ' .
- ($connected ? 'ja' : 'nein') . ' (' . $recourceid . ')');
- }
+ $pgdb->connect();
- $a = 1;
- $list1 = "";
- $list2 = "";
+ // Get current categories and relationships between categories (parent-child) as recordsets.
$records1 = $DB->get_recordset('local_lsf_unification_category', null, '', 'ueid');
$records2 = $DB->get_recordset('local_lsf_unification_categoryparenthood', null, '', 'child, parent');
- $records1unique = [];
- $records2unique = [];
+ $knowncat = [];
+ $knownrelation = [];
+
+ // Create lookup arrays.
foreach ($records1 as $record1) {
- $records1unique[$record1->ueid] = true;
+ // Save already known categories in lsf_unification.
+ $knowncat[$record1->ueid] = true;
}
+ $records1->close();
foreach ($records2 as $record2) {
- $records2unique[$record2->child][$record2->parent] = ($tryeverything === false);
- }
- $qmain = pg_query(
- $pgdb->connection,
- "SELECT ueid, uebergeord, uebergeord, quellid, txt, zeitstempel FROM " . HIS_UEBERSCHRIFT .
- " " .
- ((!empty($tryeverything)) ? ("WHERE ueid >= '" . $tryeverything . "'") : "")
+ // Save already known child-parent relationships (by category ueid).
+ $knownrelation[$record2->child][$record2->parent] = true;
+ }
+ $records2->close();
+
+ // Get every category (parents and childs) from the lsf_view and iterate over it.
+ $sql = "SELECT ueid, uebergeord, quellid, txt, zeitstempel FROM " . HIS_UEBERSCHRIFT . ";";
+ $qmain = pg_query($pgdb->connection, $sql);
+
+ // Get all categories from the lsf_view in an array 'ueid' => (object) categoryrecord.
+ // LEARNWEB-TODO: The use of pg_fetch_all can potentially uses a lot of RAM on big datasets (~900MB on our staging system). This
+ // is for now acceptable as the web server has enough RAM. Please make sure that the Cronjob has a sufficient memory limit.
+ // Review in the future how the memory usage can be reduced.
+ $lsfcategories = array_column(
+ array_map(fn($item) => (object)$item, pg_fetch_all($qmain) ?: []),
+ null,
+ 'ueid'
);
- while ($hislsftitle = pg_fetch_object($qmain)) {
- if (
- !isset($records1unique[$hislsftitle->ueid]) || (!isset(
- $records2unique[$hislsftitle->ueid][$hislsftitle->uebergeord]
- ) ||
- $records2unique[$hislsftitle->ueid][$hislsftitle->uebergeord] != true)
- ) {
- $a++;
- if ($debugoutput) {
- echo $hislsftitle->ueid . " ";
- }
- }
- if (!isset($records1unique[$hislsftitle->ueid])) {
- // Create match-table-entry if not existing.
- $entry = new stdClass();
- $entry->ueid = $hislsftitle->ueid;
- $entry->parent = empty($hislsftitle->uebergeord) ? ($hislsftitle->ueid) : ($hislsftitle->uebergeord);
- $entry->origin = find_origin_category($hislsftitle->ueid);
- $entry->mdlid = 0;
- $entry->timestamp = isset($hislsftitle->zeitstempel) ? strtotime($hislsftitle->zeitstempel) : null;
- $entry->txt = mb_convert_encoding($hislsftitle->txt, 'UTF-8', 'ISO-8859-1');
- if ($debugoutput) {
- echo "!";
- }
- try {
- $DB->insert_record("local_lsf_unification_category", $entry, true);
- $records1unique[$hislsftitle->ueid] = true;
- if ($debugoutput) {
- echo "x";
- }
- } catch (Exception $e) {
- try {
- $entry->txt = mb_convert_encoding(delete_bad_chars($hislsftitle->txt), 'UTF-8', 'ISO-8859-1');
- $DB->insert_record("local_lsf_unification_category", $entry, true);
- $records1unique[$hislsftitle->ueid] = true;
- if ($debugoutput) {
- echo "x";
- }
- } catch (Exception $e) {
- if ($debugoutput) {
- print("FEHLER1 " . var_export($e, true) . "" . var_export($DB->get_last_error(), true));
- }
- }
+ $addedcats = 0;
+ $addedrels = 0;
+ $transaction = $DB->start_delegated_transaction();
+ try {
+ foreach ($lsfcategories as $hislsftitle) {
+ $categoryunkown = !isset($knowncat[$hislsftitle->ueid]);
+ $relationunknown = !isset($knownrelation[$hislsftitle->ueid][$hislsftitle->uebergeord]);
+
+ if ($categoryunkown) {
+ // Create category if not existing.
+ $entry = (object) [
+ 'ueid' => (int) $hislsftitle->ueid,
+ 'parent' => empty($hislsftitle->uebergeord) ? (int) ($hislsftitle->ueid) : (int) ($hislsftitle->uebergeord),
+ 'origin' => find_origin_category((int) $hislsftitle->ueid, $lsfcategories),
+ 'mdlid' => 0,
+ 'timestamp' => isset($hislsftitle->zeitstempel) ? strtotime($hislsftitle->zeitstempel) : null,
+ 'txt' => mb_convert_encoding($hislsftitle->txt, 'UTF-8', 'ISO-8859-1'),
+ ];
+ $DB->insert_record("local_lsf_unification_category", $entry);
+ $knowncat[$hislsftitle->ueid] = true;
+ $addedcats++;
}
- }
- if (
- !isset($records2unique[$hislsftitle->ueid][$hislsftitle->uebergeord]) ||
- $records2unique[$hislsftitle->ueid][$hislsftitle->uebergeord] != true
- ) {
- // Create parenthood-table-entry if not existing.
- $child = $hislsftitle->ueid;
- $ueid = $hislsftitle->ueid;
- $parent = $hislsftitle->ueid;
- $fullname = "";
- $distance = 0;
- do {
- $ueid = $parent;
- $distance++;
- $q2 = pg_query(
- $pgdb->connection,
- "SELECT ueid, uebergeord, txt FROM " . HIS_UEBERSCHRIFT . " WHERE ueid = '" .
- $ueid . "'"
- );
- if (($hislsftitle2 = pg_fetch_object($q2)) && ($hislsftitle2->uebergeord != $ueid)) {
- $parent = $hislsftitle2->uebergeord;
- $fullname = ($hislsftitle2->txt) . (empty($fullname) ? "" : ("/" . $fullname));
- if (!empty($parent) && !isset($records2unique[$child][$parent])) {
- try {
- $entry = new stdClass();
- $entry->child = $child;
- $entry->parent = $parent;
- $entry->distance = $distance;
- $DB->insert_record("local_lsf_unification_categoryparenthood", $entry, true);
- if ($debugoutput) {
- echo "?";
- }
- } catch (Exception $e) {
- if ($debugoutput) {
- mtrace(
- "FEHLER2 " . var_export($e, true) . "" .
- var_export($DB->get_last_error(), true),
- ''
- );
- }
+
+ if ($relationunknown) {
+ // Create relation if not existing.
+ $child = $hislsftitle->ueid;
+ $parent = $hislsftitle->ueid;
+ $fullname = "";
+ $distance = 0;
+ $entries = [];
+ // Save the distance from the current category to each of its parents.
+ do {
+ $ueid = $parent;
+ $distance++;
+ $hislsftitle2 = $lsfcategories[$ueid] ?? null;
+ if ($hislsftitle2 && ($hislsftitle2->uebergeord != $ueid)) {
+ $parent = $hislsftitle2->uebergeord;
+ $fullname = ($hislsftitle2->txt) . (empty($fullname) ? "" : ("/" . $fullname));
+ if (!empty($parent) && !isset($knownrelation[$child][$parent])) {
+ $entries[] = (object) ['child' => $child, 'parent' => $parent, 'distance' => $distance];
}
+ $knownrelation[$child][$parent] = true;
}
- $records2unique[$child][$parent] = true;
- }
- } while (!empty($parent) && ($ueid != $parent));
- $entry = $DB->get_record(
- 'local_lsf_unification_category',
- ["ueid" => $hislsftitle->ueid,
- ]
- );
- $entry->txt2 = mb_convert_encoding($fullname, 'UTF-8', 'ISO-8859-1');
- try {
- $DB->update_record('local_lsf_unification_category', $entry, true);
- } catch (Exception $e) {
- try {
- $entry->txt2 = delete_bad_chars($entry->txt2);
- $DB->update_record('local_lsf_unification_category', $entry, true);
- } catch (Exception $e) {
- if ($debugoutput) {
- mtrace(
- "FEHLER2 " . var_export($e, true) . "" .
- var_export($DB->get_last_error(), true),
- ''
- );
- }
- }
+ } while (!empty($parent) && ($ueid != $parent));
+ // Save all path distances at once.
+ $DB->insert_records("local_lsf_unification_categoryparenthood", $entries);
+ $addedrels += count($entries);
+
+ // Update the categories full path.
+ $DB->set_field(
+ 'local_lsf_unification_category',
+ 'txt2',
+ mb_convert_encoding($fullname, 'UTF-8', 'ISO-8859-1'),
+ ['ueid' => $hislsftitle->ueid]
+ );
}
}
- if ($debugoutput && (($a % 101) == 0)) {
- mtrace(" ");
- $a++;
- flush();
+ $transaction->allow_commit();
+ if ($debugoutput) {
+ mtrace("Sync completed: {$addedcats} categories, {$addedrels} relations inserted.");
}
+ } catch (Exception $e) {
+ if ($debugoutput) {
+ mtrace("Sync failed: " . $e->getMessage());
+ }
+ $transaction->rollback($e);
+ } finally {
+ $pgdb->dispose();
}
- $pgdb->dispose();
}
/**