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/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/db/install.xml b/db/install.xml
index fe93cdd..b0da12b 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -25,7 +25,7 @@
" . $OUTPUT->box('' . $updatetable . '') . " " . $OUTPUT->box('' . $createmappings . '') . "
" . $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))
- ? " ";
}
$maincategories = get_mdl_toplevels();
- $options = "";
+ $options = [];
foreach ($maincategories as $id => $txt) {
- $options .= "";
+ $options[] = ['value' => $id, 'text' => $txt->name];
}
- $catchoice = "" . get_string('main_category', 'local_lsf_unification') . ":
-
-
-
- ";
- echo " "
- : (" [
- " . $str[0] . "]
- ")
- ) . "
-
-
-
- [" .
- (empty($child->mdlid)
- ? $str[1]
- : ("" . ($child->name) . "")) . "]
-
- [
-
-
- ]
- " . (
- (empty($child->mdlid))
- ? " "
- : (" [
- " . $str[3] . "
- ]
- ")
- ) .
- "
" . $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) ? "" : "" . $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/lib_his.php b/lib_his.php index 84295c3..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; } @@ -677,7 +672,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 . ") @@ -719,149 +714,130 @@ 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. * - * @param bool $debugoutput - * @param bool $tryeverything + * 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: + * + * - 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 { - $a = 1; - global $pgdb, $DB; - $list1 = ""; - $list2 = ""; +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(); + $pgdb->connect(); + + // 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++; - 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 "!"; + $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++; } - 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));
- }
- }
- }
- }
- 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();
}
}
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 . '+{{#isadmin}} +
+{{/isadmin}} + ++ + {{#str}} create_mappings, local_lsf_unification {{/str}} + +
diff --git a/update_helptable.php b/update_helptable.php deleted file mode 100644 index 4114850..0000000 --- a/update_helptable.php +++ /dev/null @@ -1,47 +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"); - -// 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')); 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];