From e864296f88893cbce44e2d7317ab30e0d79bdc14 Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Tue, 1 Apr 2025 14:30:28 +0100 Subject: [PATCH 01/41] Removed Text column compare prohibition --- lib/dml/moodle_database.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/dml/moodle_database.php b/lib/dml/moodle_database.php index 9818372eaf1..537ba6aff90 100644 --- a/lib/dml/moodle_database.php +++ b/lib/dml/moodle_database.php @@ -686,7 +686,9 @@ protected function where_clause($table, ?array $conditions=null) { $column = $columns[$key]; if ($column->meta_type == 'X') { //ok so the column is a text column. sorry no text columns in the where clause conditions - throw new dml_exception('textconditionsnotallowed', $conditions); + //throw new dml_exception('textconditionsnotallowed', $conditions); + // Use sql_compare_text() for text columns + $key = $this->sql_compare_text($key); } } } From 3cf30c7404f3a485a9140e6ecbc73518014aa2f1 Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Tue, 1 Apr 2025 14:57:29 +0100 Subject: [PATCH 02/41] Using sql_compare_text in send_new_user_passwords --- lib/classes/task/send_new_user_passwords_task.php | 6 +++++- lib/dml/moodle_database.php | 4 +--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/classes/task/send_new_user_passwords_task.php b/lib/classes/task/send_new_user_passwords_task.php index 678b29a40c7..6c27a97728f 100644 --- a/lib/classes/task/send_new_user_passwords_task.php +++ b/lib/classes/task/send_new_user_passwords_task.php @@ -44,8 +44,12 @@ public function get_name() { public function execute() { global $DB; + $select = $DB->sql_compare_text('name') . ' = ? AND ' . $DB->sql_compare_text('value') . ' = ?'; + $params = array('create_password', '1'); + // Generate new password emails for users - ppl expect these generated asap. - if ($DB->count_records('user_preferences', array('name' => 'create_password', 'value' => '1'))) { + //if ($DB->count_records('user_preferences', array('name' => 'create_password', 'value' => '1'))) { + if ($DB->count_records_select('user_preferences', $select, $params)) { mtrace('Creating passwords for new users...'); $userfieldsapi = \core_user\fields::for_name(); $usernamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; diff --git a/lib/dml/moodle_database.php b/lib/dml/moodle_database.php index 537ba6aff90..9818372eaf1 100644 --- a/lib/dml/moodle_database.php +++ b/lib/dml/moodle_database.php @@ -686,9 +686,7 @@ protected function where_clause($table, ?array $conditions=null) { $column = $columns[$key]; if ($column->meta_type == 'X') { //ok so the column is a text column. sorry no text columns in the where clause conditions - //throw new dml_exception('textconditionsnotallowed', $conditions); - // Use sql_compare_text() for text columns - $key = $this->sql_compare_text($key); + throw new dml_exception('textconditionsnotallowed', $conditions); } } } From 1780dd82dbdd083dad62fcf9424cd9af4860e38f Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Thu, 3 Apr 2025 16:01:07 +0100 Subject: [PATCH 03/41] Switched off audio and video recording in tinyMCE --- config.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.php b/config.php index 00f2576171e..fc78a545c86 100644 --- a/config.php +++ b/config.php @@ -32,6 +32,9 @@ $CFG->phpunit_prefix = 'phpu_'; $CFG->phpunit_dataroot = 'phpu_moodledata'; +$CFG->recordrtc_audio = false; +$CFG->recordrtc_video = false; + require_once(__DIR__ . '/lib/setup.php'); // There is no php closing tag in this file, From 352058627ebc596516eba6d08f197e8eaa18334f Mon Sep 17 00:00:00 2001 From: Sarathlal Sarangadharan Date: Sat, 5 Apr 2025 21:58:39 +0100 Subject: [PATCH 04/41] TD-5136:custom plugin created --- .../classes/privacy/provider.php | 11 ++ local/custom_service/db/services.php | 35 ++++ local/custom_service/externallib.php | 172 ++++++++++++++++++ .../lang/en/local_custom_service.php | 3 + local/custom_service/tests/privacy_test.php | 17 ++ local/custom_service/version.php | 30 +++ 6 files changed, 268 insertions(+) create mode 100644 local/custom_service/classes/privacy/provider.php create mode 100644 local/custom_service/db/services.php create mode 100644 local/custom_service/externallib.php create mode 100644 local/custom_service/lang/en/local_custom_service.php create mode 100644 local/custom_service/tests/privacy_test.php create mode 100644 local/custom_service/version.php diff --git a/local/custom_service/classes/privacy/provider.php b/local/custom_service/classes/privacy/provider.php new file mode 100644 index 00000000000..6e0fc064f09 --- /dev/null +++ b/local/custom_service/classes/privacy/provider.php @@ -0,0 +1,11 @@ + array( + 'classname' => 'insert_scorm_resource', + 'methodname' => 'insert_scorm_resource', + 'classpath' => 'local/custom_service/externallib.php', + 'description' => 'Create a scorm resource under a course', + 'type' => 'write', + 'ajax' => true, + ), + +); + +$services = array( + 'Insert Scorm resource' => array( + 'functions' => array( + + 'mod_scorm_insert_scorm_resource' + ), + 'restrictedusers' => 0, + 'enabled' => 1, + // This field os optional, but requried if the `restrictedusers` value is + // set, so as to allow configuration via the Web UI. + 'shortname' => 'InsertScorm', + + // Whether to allow file downloads. + 'downloadfiles' => 0, + + // Whether to allow file uploads. + 'uploadfiles' => 0, + ) +); \ No newline at end of file diff --git a/local/custom_service/externallib.php b/local/custom_service/externallib.php new file mode 100644 index 00000000000..0019202ed9e --- /dev/null +++ b/local/custom_service/externallib.php @@ -0,0 +1,172 @@ +libdir.'/externallib.php'); +require_once($CFG->dirroot.'/user/lib.php'); +require_once($CFG->dirroot.'/course/lib.php'); +require_once($CFG->dirroot.'/mod/scorm/lib.php'); +require_once($CFG->dirroot.'/mod/scorm/locallib.php'); +require_once(__DIR__.'/../../config.php'); +require_once($CFG->libdir . '/filestorage/file_storage.php'); +require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php"); +class insert_scorm_resource extends external_api { + + + + public static function insert_scorm_resource_parameters() { + return new external_function_parameters( + array( + 'courseid' => new external_value(PARAM_TEXT, 'Course Id'), + 'section' => new external_value(PARAM_TEXT, 'section'), + 'scormname' => new external_value(PARAM_TEXT, 'scorm name'), + 'file' => new external_value(PARAM_TEXT, 'file'), + 'path' => new external_value(PARAM_TEXT, 'path') + + ) + ); + } + public static function insert_scorm_resource($courseids,$section,$scormname,$file,$path) { + global $DB,$CFG; + require_once($CFG->libdir . '/filelib.php'); + require_once($CFG->dirroot . '/course/lib.php'); + require_once($CFG->libdir . '/formslib.php'); + + require_login(); + $courseid = $courseids; // Course ID where the SCORM package will be uploaded + $scormname = $scormname;// // Name for the SCORM module + $scormfile =$path . '' . $file. '.zip'; // Path to the SCORM .zip file + + $zip = new ZipArchive; + + if ($zip->open($scormfile) === TRUE) { + // Check if imsmanifest.xml exists in the ZIP archive + if ($zip->locateName('imsmanifest.xml', ZipArchive::FL_NODIR) !== false) { + $zip->close(); + } else { + $zip->close(); + echo'imsmanifest.xml is missing from SCORM package.'; + return; + } + } else { + echo 'Failed to open SCORM package.'; + return; + } + + // Get course and context + try{ + $course = get_course($courseid); + } + catch(exception $ex) + { + echo 'Course not found'; + return; + } + + $context = context_course::instance($courseid); + + // Check permissions + require_capability('mod/scorm:addinstance', $context); + + // Create SCORM instance (if needed) + $scorm = new stdClass(); + $scorm->course = $courseid; + $scorm->name = $scormname; + $scorm->reference='Test Ref.zip'; + $scorm->intro = 'Intro to SCORM'; + $scorm->introformat = FORMAT_HTML; + $scorm->timemodified = time(); + + // Insert the SCORM instance into the database and get the instance ID + $scorm->id = $DB->insert_record('scorm', $scorm); + + // Create a new course module record + $cm = new stdClass(); + $cm->course = $courseid; + $cm->module = $DB->get_field('modules', 'id', array('name' => 'scorm')); + $cm->instance = $scorm->id; + $cm->visible = 1; + $cm->section = $section; // You can set the section if needed + + // Insert the course module + $cm->id = add_course_module($cm); //$DB->insert_record('course_modules', $cm); + + $sectionid=course_add_cm_to_section($courseid,$cm->id,$cm->section); + + // Update the record + $data = new stdClass(); + $data->id = $cm->id; // The ID of the course module to update + $data->section = $sectionid; // The new section value + + // // Update the record in the course_modules table + $DB->update_record('course_modules', $data); + // Upload the SCORM package to Moodle file storage + $fs = get_file_storage(); + $context = context_module::instance($cm->id); + + // Add the SCORM .zip package to the file area + $fileinfo = array( + 'contextid' => $context->id, + 'component' => 'mod_scorm', + 'filearea' => 'package', + 'itemid' => 0, // Item ID (could be used to reference a specific instance of the package) + 'filepath' => '/', + 'filename' => $file. '.zip' + ); + + $file = $fs->create_file_from_pathname($fileinfo, $scormfile); + + $packer = get_file_packer('application/zip'); + ; + if ($file) { + $extracted_files = $file->extract_to_storage($packer,$context->id, 'mod_scorm', 'content', 0, '/'); + echo "Extraction complete!"; + } else { + echo "ZIP file not found."; + } + + //new code for reading imsmanifest.xml + $fs = get_file_storage(); + + // Locate the extracted directory in Moodle file storage (adjust as needed) + $contextid = $context->id; // The course/module context ID + $component = 'mod_scorm'; // Change this to match your module (e.g., mod_scorm, mod_lti, etc.) + $filearea = 'content'; // File area for SCORM or Common Cartridge + $itemid = 0; // Usually 0 unless specified + $filename = 'imsmanifest.xml'; + + // Get the manifest file + $file = $fs->get_file($contextid, $component, $filearea, $itemid, '/', $filename); + $manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml'); + + + scorm_parse_scorm($scorm, $manifest); + + + + $count = 0; + $lti_updated = [ + 'message'=>'Scorm package added to course', + ]; + + + + + return $lti_updated; + } + public static function insert_scorm_resource_returns() { + return new external_single_structure( + array( + 'ids' => new external_value(PARAM_TEXT, 'course ids'), + 'message'=> new external_value(PARAM_TEXT, 'success message'), + 'updated'=>new external_value(PARAM_TEXT,'Items Updated') + ) + ); + } + + + + + + + +} \ No newline at end of file diff --git a/local/custom_service/lang/en/local_custom_service.php b/local/custom_service/lang/en/local_custom_service.php new file mode 100644 index 00000000000..6fdc0b44600 --- /dev/null +++ b/local/custom_service/lang/en/local_custom_service.php @@ -0,0 +1,3 @@ +dirroot . '/local/custom_service/classes/privacy/provider.php'); + +use core_privacy\tests\provider_testcase; +use local_custom_service\privacy\provider; + +class local_custom_service_privacy_test extends provider_testcase { + + public function test_privacy_provider() { + $this->assertInstanceOf( + \core_privacy\local\metadata\null_provider::class, + new provider() + ); + } +} diff --git a/local/custom_service/version.php b/local/custom_service/version.php new file mode 100644 index 00000000000..11103b82a41 --- /dev/null +++ b/local/custom_service/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version details + * + * @package block_calendar_upcoming + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2024100990; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2024100290; // Requires this Moodle version. +$plugin->component = 'local_custom_service'; // Full name of the plugin (used for diagnostics) +$plugin->privacy = ['provider' => 'local_custom_service\privacy\provider']; \ No newline at end of file From 6f481c11a40db6c0c0b3d90397c0387a8243aec3 Mon Sep 17 00:00:00 2001 From: Sarathlal Sarangadharan Date: Sun, 6 Apr 2025 00:08:22 +0100 Subject: [PATCH 05/41] TD-5136:Auth oidc unit test issue fix --- auth/oidc/classes/privacy/provider.php | 5 +++++ auth/oidc/lang/en/auth_oidc.php | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/auth/oidc/classes/privacy/provider.php b/auth/oidc/classes/privacy/provider.php index 4a6beb724ea..59ebe3b2d0c 100644 --- a/auth/oidc/classes/privacy/provider.php +++ b/auth/oidc/classes/privacy/provider.php @@ -76,6 +76,11 @@ public static function get_metadata(collection $collection): collection { 'refreshtoken', 'idtoken', ], + 'auth_oidc_sid' => [ // ✅ Add this block + 'userid', + 'sid', + 'timecreated', + ], ]; foreach ($tables as $table => $fields) { diff --git a/auth/oidc/lang/en/auth_oidc.php b/auth/oidc/lang/en/auth_oidc.php index 75409756fdd..da5c7daf059 100644 --- a/auth/oidc/lang/en/auth_oidc.php +++ b/auth/oidc/lang/en/auth_oidc.php @@ -494,6 +494,10 @@ $string['update_username_results'] = 'Update username results'; $string['new_username'] = 'New username'; $string['missing_idp_type'] = 'This configuration is only available if an IdP type is configured.'; +$string['privacy:metadata:auth_oidc_sid'] = 'Stores OIDC session identifiers linked to users.'; +$string['privacy:metadata:auth_oidc_sid:userid'] = 'The ID of the user associated with the session.'; +$string['privacy:metadata:auth_oidc_sid:sid'] = 'The session ID from the OIDC provider.'; +$string['privacy:metadata:auth_oidc_sid:timecreated'] = 'The time the session ID was created.'; // phpcs:enable moodle.Files.LangFilesOrdering.IncorrectOrder // phpcs:enable moodle.Files.LangFilesOrdering.UnexpectedComment \ No newline at end of file From 9381c1af04f255f8a94c6471c0019ea17b0bdcaf Mon Sep 17 00:00:00 2001 From: Sarathlal Sarangadharan Date: Mon, 28 Apr 2025 09:36:10 +0100 Subject: [PATCH 06/41] TD-5485: Modify the migration tool add SCORM content to courses using the new endpoint --- local/custom_service/externallib.php | 61 +++++++++++++++++----------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/local/custom_service/externallib.php b/local/custom_service/externallib.php index 0019202ed9e..c4d5b3a3a0f 100644 --- a/local/custom_service/externallib.php +++ b/local/custom_service/externallib.php @@ -19,22 +19,26 @@ public static function insert_scorm_resource_parameters() { 'courseid' => new external_value(PARAM_TEXT, 'Course Id'), 'section' => new external_value(PARAM_TEXT, 'section'), 'scormname' => new external_value(PARAM_TEXT, 'scorm name'), - 'file' => new external_value(PARAM_TEXT, 'file'), - 'path' => new external_value(PARAM_TEXT, 'path') + 'foldername' => new external_value(PARAM_TEXT, 'foldername'), + 'base64Zip' => new external_value(PARAM_RAW, 'Base64-encoded ZIP file content') ) ); } - public static function insert_scorm_resource($courseids,$section,$scormname,$file,$path) { + + public static function insert_scorm_resource($courseids,$section,$scormname,$foldername,$base64Zip) { global $DB,$CFG; require_once($CFG->libdir . '/filelib.php'); require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->libdir . '/formslib.php'); require_login(); + // zip file + $savedPath = self::saveBase64ToZip($base64Zip, $foldername . '.zip'); + + //zip end here $courseid = $courseids; // Course ID where the SCORM package will be uploaded - $scormname = $scormname;// // Name for the SCORM module - $scormfile =$path . '' . $file. '.zip'; // Path to the SCORM .zip file + $scormfile =$savedPath;//$path . '' . $file. '.zip'; // Path to the SCORM .zip file $zip = new ZipArchive; @@ -119,9 +123,7 @@ public static function insert_scorm_resource($courseids,$section,$scormname,$fil ; if ($file) { $extracted_files = $file->extract_to_storage($packer,$context->id, 'mod_scorm', 'content', 0, '/'); - echo "Extraction complete!"; } else { - echo "ZIP file not found."; } //new code for reading imsmanifest.xml @@ -142,31 +144,42 @@ public static function insert_scorm_resource($courseids,$section,$scormname,$fil scorm_parse_scorm($scorm, $manifest); - - $count = 0; - $lti_updated = [ - 'message'=>'Scorm package added to course', - ]; - - - - + $lti_updated[] = array( + 'id' => $scorm->id, + 'name' => $scormname, + ); return $lti_updated; } public static function insert_scorm_resource_returns() { - return new external_single_structure( + return new external_multiple_structure( + new external_single_structure( array( - 'ids' => new external_value(PARAM_TEXT, 'course ids'), - 'message'=> new external_value(PARAM_TEXT, 'success message'), - 'updated'=>new external_value(PARAM_TEXT,'Items Updated') + 'id' => new external_value(PARAM_INT, 'new scorm id'), + 'name' => new external_value(PARAM_RAW, 'new scorm name'), ) - ); + ) + ); } + public static function saveBase64ToZip($base64Data, $filename, $saveDir = 'uploads/') + { + // Make sure directory exists + if (!file_exists($saveDir)) { + mkdir($saveDir, 0777, true); + } + // Sanitize filename + $safeFilename = preg_replace('/[^a-zA-Z0-9_\.-]/', '_', $filename); + $zipPath = rtrim($saveDir, '/') . '/' . $safeFilename; + // Decode base64 + $binaryData = base64_decode($base64Data); - - - + // Save the file + if (file_put_contents($zipPath, $binaryData) !== false) { + return realpath($zipPath); // return full path + } else { + return false; + } + } } \ No newline at end of file From e4535fbebeb09f4fbcb71084934fa516dba5694e Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 1 May 2025 11:22:21 +0100 Subject: [PATCH 07/41] Sending data to findwise API --- enrol/meta/classes/observer.php | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/enrol/meta/classes/observer.php b/enrol/meta/classes/observer.php index 446fca5c8f1..f3100e91825 100644 --- a/enrol/meta/classes/observer.php +++ b/enrol/meta/classes/observer.php @@ -214,6 +214,25 @@ public static function course_deleted(\core\event\course_deleted $event) { public static function enrol_instance_updated(\core\event\enrol_instance_updated $event) { global $DB; + $instance = $DB->get_record('enrol', ['id' => $event->objectid], '*', MUST_EXIST); + + // Check if it is a self-enrolment + if ($instance->enrol === 'self') { + $courseid = $instance->courseid; + $status = $instance->status; // 0 = enabled, 1 = disabled + + // Prepare data to send + $data = [ + '_id' => $courseid, + 'name' => 'Findwise Testing Catalogue', + 'description' => 'A moodle courese Binon', + 'status' => $status + ]; + + // Send to external API + self::send_external_api($data); + } + if (!enrol_is_enabled('meta')) { // This is slow, let enrol_meta_sync() deal with disabled plugin. return true; @@ -230,4 +249,20 @@ public static function enrol_instance_updated(\core\event\enrol_instance_updated return true; } + + private static function send_external_api($data) { + $url = ''; // <-- API + + $options = [ + 'http' => [ + 'header' => "Content-type: application/json\r\n", + 'method' => 'POST', + 'content' => json_encode($data), + 'timeout' => 5, + ], + ]; + + $context = stream_context_create($options); + @file_get_contents($url, false, $context); // Using @ to suppress warnings if API fails + } } From 3cb8f40b05a167e69aef20f19cb6a6e076b6757a Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 2 May 2025 15:11:08 +0100 Subject: [PATCH 08/41] New plug in- TEL Organisation Specific Configurations --- local/telconfig/classes/privacy/provider.php | 11 ++++ local/telconfig/lang/en/local_telconfig.php | 31 +++++++++++ local/telconfig/settings.php | 57 ++++++++++++++++++++ local/telconfig/tests/privacy_test.php | 17 ++++++ local/telconfig/version.php | 28 ++++++++++ 5 files changed, 144 insertions(+) create mode 100644 local/telconfig/classes/privacy/provider.php create mode 100644 local/telconfig/lang/en/local_telconfig.php create mode 100644 local/telconfig/settings.php create mode 100644 local/telconfig/tests/privacy_test.php create mode 100644 local/telconfig/version.php diff --git a/local/telconfig/classes/privacy/provider.php b/local/telconfig/classes/privacy/provider.php new file mode 100644 index 00000000000..6e0fc064f09 --- /dev/null +++ b/local/telconfig/classes/privacy/provider.php @@ -0,0 +1,11 @@ +. + +/** + * Meta enrolment plugin settings and presets. + * + * @package telconfig + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + $settings = new admin_settingpage('localtelconfigsettings', get_string('pluginname', 'local_telconfig')); + + // Findwisesettings + $settings->add(new admin_setting_heading('Findwisesettings', + get_string('Findwisesettings', 'local_telconfig'), get_string('Findwisesettings_desc', 'local_telconfig'))); + + $settings->add(new admin_setting_configtext('local_telconfig/findwiseindexurl', + get_string('findwiseindexurl', 'local_telconfig'), get_string('findwiseindexurl_desc', 'local_telconfig'), '', PARAM_TEXT)); + $settings->add(new admin_setting_configtext('local_telconfig/findwiseindexmethod', + get_string('findwiseindexmethod', 'local_telconfig'), get_string('findwiseindexmethod_desc', 'local_telconfig'), '', PARAM_TEXT)); + $settings->add(new admin_setting_configtext('local_telconfig/findwisecollection', + get_string('findwisecollection', 'local_telconfig'), get_string('findwisecollection_desc', 'local_telconfig'), '', PARAM_TEXT)); + $settings->add(new admin_setting_configtext('local_telconfig/findwiseapitoken', + get_string('findwiseapitoken', 'local_telconfig'), get_string('findwiseapitoken_desc', 'local_telconfig'), '', PARAM_TEXT)); + + //Mustache template settings + $settings->add(new admin_setting_heading('mustachetemplatessettings', + get_string('mustachetemplatessettings', 'local_telconfig'), get_string('mustachetemplatessettings_desc', 'local_telconfig'))); + + $settings->add(new admin_setting_configtext('local_telconfig/mustachetemplatesurl', + get_string('mustachetemplatesurl', 'local_telconfig'), get_string('mustachetemplatesurl_desc', 'local_telconfig'), '', PARAM_TEXT)); + + // content server settings + $settings->add(new admin_setting_heading('contentserversettings', + get_string('contentserversettings', 'local_telconfig'), get_string('contentserversettings_desc', 'local_telconfig'))); + + $settings->add(new admin_setting_configtext('local_telconfig/contentserverurl', + get_string('contentserverurl', 'local_telconfig'), get_string('contentserverurl_desc', 'local_telconfig'), '', PARAM_TEXT)); + + + $ADMIN->add('localplugins', $settings); +} diff --git a/local/telconfig/tests/privacy_test.php b/local/telconfig/tests/privacy_test.php new file mode 100644 index 00000000000..cb9d2cbee3c --- /dev/null +++ b/local/telconfig/tests/privacy_test.php @@ -0,0 +1,17 @@ +dirroot . '/local/telconfig/classes/privacy/provider.php'); + +use core_privacy\tests\provider_testcase; +use local_telconfig\privacy\provider; + +class local_telconfig_privacy_test extends provider_testcase { + + public function test_privacy_provider() { + $this->assertInstanceOf( + \core_privacy\local\metadata\null_provider::class, + new provider() + ); + } +} diff --git a/local/telconfig/version.php b/local/telconfig/version.php new file mode 100644 index 00000000000..a087a0b0b75 --- /dev/null +++ b/local/telconfig/version.php @@ -0,0 +1,28 @@ +. + +/** + * Version details + * + * @package local_telconfig + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2024100993; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2024100290; // Requires this Moodle version. +$plugin->component = 'local_telconfig'; // Full name of the plugin (used for diagnostics) +$plugin->privacy = ['provider' => 'local_telconfig\privacy\provider']; \ No newline at end of file From 3da5416e927ab36d43b6232d695ff94c8fe6fe4e Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 2 May 2025 15:18:23 +0100 Subject: [PATCH 09/41] Updated telconfig --- local/telconfig/classes/privacy/provider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/telconfig/classes/privacy/provider.php b/local/telconfig/classes/privacy/provider.php index 6e0fc064f09..1ab6b341f9e 100644 --- a/local/telconfig/classes/privacy/provider.php +++ b/local/telconfig/classes/privacy/provider.php @@ -1,5 +1,5 @@ Date: Tue, 6 May 2025 08:33:26 +0100 Subject: [PATCH 10/41] Added the privacy metadata --- local/telconfig/lang/en/local_telconfig.php | 1 + 1 file changed, 1 insertion(+) diff --git a/local/telconfig/lang/en/local_telconfig.php b/local/telconfig/lang/en/local_telconfig.php index 8f4da2ea13c..be5810c7897 100644 --- a/local/telconfig/lang/en/local_telconfig.php +++ b/local/telconfig/lang/en/local_telconfig.php @@ -1,6 +1,7 @@ Date: Wed, 7 May 2025 11:31:12 +0100 Subject: [PATCH 11/41] TD-5537: New custom page has been created to kill user session when the user trigger sign out from learning hub --- singlesignout.php | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 singlesignout.php diff --git a/singlesignout.php b/singlesignout.php new file mode 100644 index 00000000000..f8c91b87c2e --- /dev/null +++ b/singlesignout.php @@ -0,0 +1,40 @@ +libdir . '/filelib.php'); + +// Accept logout_token (no signature verification) +$logout_token = $_POST['logout_token'] ?? null; +if (!$logout_token) { + http_response_code(400); + exit("Missing logout_token"); +} + +// Decode JWT payload (2nd part) +$jwt_parts = explode('.', $logout_token); +if (count($jwt_parts) !== 3) { + http_response_code(400); + exit("Invalid JWT"); +} + +$payload_json = base64_decode(strtr($jwt_parts[1], '-_', '+/')); +$payload = json_decode($payload_json, true); +$sub = $payload['sub'] ?? null; + +if (!$sub) { + http_response_code(400); + exit("Missing sub claim"); +} + +// Lookup user by sub (assumed to match idnumber or username) +$user = $DB->get_record('user', ['auth' => 'oidc', 'username' => $sub]); +if (!$user) { + http_response_code(404); + exit("User not found"); +} + +// Kill the user’s sessions +\core\session\manager::kill_user_sessions($user->id); + +http_response_code(200); +echo "User logged out"; From 2483909655d2471b3ecba4449439d752cad225ac Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 12 May 2025 15:13:07 +0100 Subject: [PATCH 12/41] Reverted the self entrol changes from core php plugin page and moved the custom page --- enrol/meta/classes/observer.php | 35 ----------- local/telconfig/classes/helper.php | 72 +++++++++++++++++++++ local/telconfig/classes/observer.php | 94 ++++++++++++++++++++++++++++ local/telconfig/db/events.php | 10 +++ 4 files changed, 176 insertions(+), 35 deletions(-) create mode 100644 local/telconfig/classes/helper.php create mode 100644 local/telconfig/classes/observer.php create mode 100644 local/telconfig/db/events.php diff --git a/enrol/meta/classes/observer.php b/enrol/meta/classes/observer.php index f3100e91825..446fca5c8f1 100644 --- a/enrol/meta/classes/observer.php +++ b/enrol/meta/classes/observer.php @@ -214,25 +214,6 @@ public static function course_deleted(\core\event\course_deleted $event) { public static function enrol_instance_updated(\core\event\enrol_instance_updated $event) { global $DB; - $instance = $DB->get_record('enrol', ['id' => $event->objectid], '*', MUST_EXIST); - - // Check if it is a self-enrolment - if ($instance->enrol === 'self') { - $courseid = $instance->courseid; - $status = $instance->status; // 0 = enabled, 1 = disabled - - // Prepare data to send - $data = [ - '_id' => $courseid, - 'name' => 'Findwise Testing Catalogue', - 'description' => 'A moodle courese Binon', - 'status' => $status - ]; - - // Send to external API - self::send_external_api($data); - } - if (!enrol_is_enabled('meta')) { // This is slow, let enrol_meta_sync() deal with disabled plugin. return true; @@ -249,20 +230,4 @@ public static function enrol_instance_updated(\core\event\enrol_instance_updated return true; } - - private static function send_external_api($data) { - $url = ''; // <-- API - - $options = [ - 'http' => [ - 'header' => "Content-type: application/json\r\n", - 'method' => 'POST', - 'content' => json_encode($data), - 'timeout' => 5, - ], - ]; - - $context = stream_context_create($options); - @file_get_contents($url, false, $context); // Using @ to suppress warnings if API fails - } } diff --git a/local/telconfig/classes/helper.php b/local/telconfig/classes/helper.php new file mode 100644 index 00000000000..71bd5223980 --- /dev/null +++ b/local/telconfig/classes/helper.php @@ -0,0 +1,72 @@ +. + +/** + * Helper class locally used. + * + * @package local_telconfig + * @copyright + * @license + */ + +namespace local_telconfig; + +defined('MOODLE_INTERNAL') || die(); + +class helper { + + /** + * Sends structured data to an external API endpoint. + * + * @param array $data + * @return void + */ + public static function send_findwise_api(array $data): void { + $indexurl = get_config('local_telconfig', 'findwiseindexurl'); + $indexmethod = get_config('local_telconfig', 'findwiseindexmethod'); + $collection = get_config('local_telconfig', 'findwisecollection'); + $apitoken = get_config('local_telconfig', 'findwiseapitoken'); + + $indexurl = rtrim($indexurl, '/').'/'.$indexmethod.'?token=' . urlencode($apitoken); + $apiurl = str_replace("{0}", $collection, $indexurl); + + if (empty($apiurl) || empty($apitoken)) { + debugging('send_findwise_api: API URL or token not set in plugin config.', DEBUG_DEVELOPER); + return; + } + + $options = [ + 'http' => [ + 'header' => "Content-type: application/json\r\n", + 'method' => 'POST', + 'content' => json_encode($data), + 'timeout' => 5, + ], + ]; + + $context = stream_context_create($options); + try { + $response = @file_get_contents($apiurl, false, $context); + if ($response === false) { + debugging('send_findwise_api: Failed to send data to Findwise API.', DEBUG_DEVELOPER); + } else { + debugging('send_findwise_api: Data sent successfully to Findwise API.', DEBUG_DEVELOPER); + } + } catch (\Exception $e) { + debugging('send_findwise_api: Exception occurred while sending data: ' . $e->getMessage(), DEBUG_DEVELOPER); + } + } +} diff --git a/local/telconfig/classes/observer.php b/local/telconfig/classes/observer.php new file mode 100644 index 00000000000..d1c3d241275 --- /dev/null +++ b/local/telconfig/classes/observer.php @@ -0,0 +1,94 @@ +other['enrol']) || $event->other['enrol'] !== 'self') { + return; + } + + try { + // Get enrol instance + $enrol = $DB->get_record('enrol', ['id' => $event->objectid], '*', MUST_EXIST); + + // Get course info + $course = $DB->get_record('course', ['id' => $event->courseid], '*', MUST_EXIST); + + $data = [ + '_id' => $course->id, + 'event' => $event->eventname, + 'enrolid' => $enrol->id, + 'courseid' => $course->id, + 'coursename' => $course->fullname, + 'shortname' => $course->shortname, + 'summary' => $course->summary, + 'startdate' => $course->startdate, + 'enddate' => $course->enddate, + 'enrolstatus' => $enrol->status, // 0 = enabled, 1 = disabled + 'time' => time() + ]; + + helper::send_findwise_api($data); + } catch (\dml_exception $e) { + debugging("Failed to fetch course/enrol data: " . $e->getMessage(), DEBUG_DEVELOPER); + } +} + + + /** + * Sends data to the configured external API endpoint. + * + * @param array $data + * @return void + */ + private static function send_external_api(array $data): void { + global $CFG; + require_once($CFG->libdir . '/filelib.php'); + + // Get plugin config values + $apiurl = get_config('local_telconfig', 'apiurl'); + $apitoken = get_config('local_telconfig', 'apitoken'); + + if (empty($apiurl) || empty($apitoken)) { + debugging('API URL or Token not configured in local_telconfig.', DEBUG_DEVELOPER); + return; + } + + $curl = new \curl(); + $headers = [ + "Authorization: Bearer {$apitoken}", + "Content-Type: application/json" + ]; + + $options = [ + 'CURLOPT_HTTPHEADER' => $headers, + 'timeout' => 5, + ]; + + try { + $response = $curl->post($apiurl, json_encode($data), $options); + // Optional: log the response if debugging + if (debugging('', DEBUG_DEVELOPER)) { + debugging('Self enrol API response: ' . $response, DEBUG_DEVELOPER); + } + } catch (\Exception $e) { + debugging('Error sending to external API: ' . $e->getMessage(), DEBUG_DEVELOPER); + } + } +} diff --git a/local/telconfig/db/events.php b/local/telconfig/db/events.php new file mode 100644 index 00000000000..7a3a54595ef --- /dev/null +++ b/local/telconfig/db/events.php @@ -0,0 +1,10 @@ + '\core\event\enrol_instance_updated', + 'callback' => '\local_telconfig\observer::enrol_instance_changed', + 'priority' => 9999, + 'internal' => false, + ], +]; From 4fb6932fa39bca8320d82a0036e93bf99a0a2f26 Mon Sep 17 00:00:00 2001 From: Binon Date: Tue, 13 May 2025 21:21:09 +0100 Subject: [PATCH 13/41] Refaaactoed the code to accomodate the test --- local/telconfig/classes/api_client.php | 20 ++++++++++++ local/telconfig/classes/helper.php | 26 ++++++--------- local/telconfig/classes/observer.php | 44 +------------------------- local/telconfig/tests/helper_test.php | 35 ++++++++++++++++++++ 4 files changed, 66 insertions(+), 59 deletions(-) create mode 100644 local/telconfig/classes/api_client.php create mode 100644 local/telconfig/tests/helper_test.php diff --git a/local/telconfig/classes/api_client.php b/local/telconfig/classes/api_client.php new file mode 100644 index 00000000000..0cf5646ae49 --- /dev/null +++ b/local/telconfig/classes/api_client.php @@ -0,0 +1,20 @@ + [ + 'header' => "Content-type: application/json\r\n", + 'method' => 'POST', + 'content' => json_encode($data), + 'timeout' => 5, + ] + ]; + $context = stream_context_create($options); + return @file_get_contents($url, false, $context); + } +} \ No newline at end of file diff --git a/local/telconfig/classes/helper.php b/local/telconfig/classes/helper.php index 71bd5223980..3ea8cae4f07 100644 --- a/local/telconfig/classes/helper.php +++ b/local/telconfig/classes/helper.php @@ -23,6 +23,7 @@ */ namespace local_telconfig; +use local_telconfig\api_client; defined('MOODLE_INTERNAL') || die(); @@ -34,36 +35,29 @@ class helper { * @param array $data * @return void */ - public static function send_findwise_api(array $data): void { + public static function send_findwise_api(array $data, ?api_client $client = null): void { $indexurl = get_config('local_telconfig', 'findwiseindexurl'); $indexmethod = get_config('local_telconfig', 'findwiseindexmethod'); $collection = get_config('local_telconfig', 'findwisecollection'); $apitoken = get_config('local_telconfig', 'findwiseapitoken'); - $indexurl = rtrim($indexurl, '/').'/'.$indexmethod.'?token=' . urlencode($apitoken); - $apiurl = str_replace("{0}", $collection, $indexurl); + $indexurl = rtrim($indexurl, '/') . '/' . $indexmethod . '?token=' . urlencode($apitoken); + $apiurl = str_replace('{0}', $collection, $indexurl); if (empty($apiurl) || empty($apitoken)) { debugging('send_findwise_api: API URL or token not set in plugin config.', DEBUG_DEVELOPER); return; } - - $options = [ - 'http' => [ - 'header' => "Content-type: application/json\r\n", - 'method' => 'POST', - 'content' => json_encode($data), - 'timeout' => 5, - ], - ]; - $context = stream_context_create($options); + $client ??= new api_client(); + try { - $response = @file_get_contents($apiurl, false, $context); + $response = $client->post($apiurl, $data); + if ($response === false) { - debugging('send_findwise_api: Failed to send data to Findwise API.', DEBUG_DEVELOPER); + debugging('send_findwise_api: Failed to send data to findwise API.', DEBUG_DEVELOPER); } else { - debugging('send_findwise_api: Data sent successfully to Findwise API.', DEBUG_DEVELOPER); + debugging('send_findwise_api: Data sent successfully to findwise API.', DEBUG_DEVELOPER); } } catch (\Exception $e) { debugging('send_findwise_api: Exception occurred while sending data: ' . $e->getMessage(), DEBUG_DEVELOPER); diff --git a/local/telconfig/classes/observer.php b/local/telconfig/classes/observer.php index d1c3d241275..b2d128a6a08 100644 --- a/local/telconfig/classes/observer.php +++ b/local/telconfig/classes/observer.php @@ -26,7 +26,7 @@ public static function enrol_instance_changed(\core\event\base $event): void { try { // Get enrol instance $enrol = $DB->get_record('enrol', ['id' => $event->objectid], '*', MUST_EXIST); - + // Get course info $course = $DB->get_record('course', ['id' => $event->courseid], '*', MUST_EXIST); @@ -49,46 +49,4 @@ public static function enrol_instance_changed(\core\event\base $event): void { debugging("Failed to fetch course/enrol data: " . $e->getMessage(), DEBUG_DEVELOPER); } } - - - /** - * Sends data to the configured external API endpoint. - * - * @param array $data - * @return void - */ - private static function send_external_api(array $data): void { - global $CFG; - require_once($CFG->libdir . '/filelib.php'); - - // Get plugin config values - $apiurl = get_config('local_telconfig', 'apiurl'); - $apitoken = get_config('local_telconfig', 'apitoken'); - - if (empty($apiurl) || empty($apitoken)) { - debugging('API URL or Token not configured in local_telconfig.', DEBUG_DEVELOPER); - return; - } - - $curl = new \curl(); - $headers = [ - "Authorization: Bearer {$apitoken}", - "Content-Type: application/json" - ]; - - $options = [ - 'CURLOPT_HTTPHEADER' => $headers, - 'timeout' => 5, - ]; - - try { - $response = $curl->post($apiurl, json_encode($data), $options); - // Optional: log the response if debugging - if (debugging('', DEBUG_DEVELOPER)) { - debugging('Self enrol API response: ' . $response, DEBUG_DEVELOPER); - } - } catch (\Exception $e) { - debugging('Error sending to external API: ' . $e->getMessage(), DEBUG_DEVELOPER); - } - } } diff --git a/local/telconfig/tests/helper_test.php b/local/telconfig/tests/helper_test.php new file mode 100644 index 00000000000..a29725eda36 --- /dev/null +++ b/local/telconfig/tests/helper_test.php @@ -0,0 +1,35 @@ +. + +use advanced_testcase; + +class helper_test extends advanced_testcase { + public function test_send_xx_api_makes_api_call() { + $this->resetAfterTest(); + + set_config('xxindexurl', 'http://fake.local/api', 'local_telconfig'); + set_config('xxindexmethod', 'index', 'local_telconfig'); + set_config('xxcollection', 'courses', 'local_telconfig'); + set_config('xxapitoken', 'faketoken', 'local_telconfig'); + + $mock = $this->createMock(api_client::class); + $mock->expects($this->once()) + ->method('post') + ->willReturn('{"status":"ok"}'); + + helper::send_findwise_api(['courseid' => 123], $mock); + } +} \ No newline at end of file From 99c7a919df1410849904743640b932bb9357a05e Mon Sep 17 00:00:00 2001 From: Binon Date: Tue, 13 May 2025 22:10:26 +0100 Subject: [PATCH 14/41] Fixing the build error in github action - telconfig_helper_test --- local/telconfig/tests/helper_test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/telconfig/tests/helper_test.php b/local/telconfig/tests/helper_test.php index a29725eda36..c7d6f109df1 100644 --- a/local/telconfig/tests/helper_test.php +++ b/local/telconfig/tests/helper_test.php @@ -16,7 +16,7 @@ use advanced_testcase; -class helper_test extends advanced_testcase { +class telconfig_helper_test extends advanced_testcase { public function test_send_xx_api_makes_api_call() { $this->resetAfterTest(); From efeaecc9e235cd753380018ea6dd32701cb56fd1 Mon Sep 17 00:00:00 2001 From: Binon Date: Wed, 14 May 2025 08:52:33 +0100 Subject: [PATCH 15/41] Fixing build error in GitHUb action by slighly modifyingthe test --- local/telconfig/tests/helper_test.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/local/telconfig/tests/helper_test.php b/local/telconfig/tests/helper_test.php index c7d6f109df1..d9a734ea54d 100644 --- a/local/telconfig/tests/helper_test.php +++ b/local/telconfig/tests/helper_test.php @@ -15,21 +15,31 @@ // along with Moodle. If not, see . use advanced_testcase; +use local_telconfig\helper; +use local_telconfig\api_client; class telconfig_helper_test extends advanced_testcase { - public function test_send_xx_api_makes_api_call() { + + public function test_send_findwise_api_makes_api_call() { $this->resetAfterTest(); + // Set fake plugin config values. set_config('xxindexurl', 'http://fake.local/api', 'local_telconfig'); set_config('xxindexmethod', 'index', 'local_telconfig'); set_config('xxcollection', 'courses', 'local_telconfig'); set_config('xxapitoken', 'faketoken', 'local_telconfig'); + // Create a mock API client. $mock = $this->createMock(api_client::class); $mock->expects($this->once()) ->method('post') + ->with( + $this->stringContains('http://fake.local/api/index?token='), + $this->equalTo(['courseid' => 123]) + ) ->willReturn('{"status":"ok"}'); + // Call the method with the mock client. helper::send_findwise_api(['courseid' => 123], $mock); } -} \ No newline at end of file +} From b0cd50964547ad5c00138a22ae6754e2cd4eb4ce Mon Sep 17 00:00:00 2001 From: Binon Date: Wed, 14 May 2025 15:05:29 +0100 Subject: [PATCH 16/41] Updated the test script --- local/telconfig/tests/helper_test.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/local/telconfig/tests/helper_test.php b/local/telconfig/tests/helper_test.php index d9a734ea54d..a5090240e9c 100644 --- a/local/telconfig/tests/helper_test.php +++ b/local/telconfig/tests/helper_test.php @@ -24,10 +24,10 @@ public function test_send_findwise_api_makes_api_call() { $this->resetAfterTest(); // Set fake plugin config values. - set_config('xxindexurl', 'http://fake.local/api', 'local_telconfig'); - set_config('xxindexmethod', 'index', 'local_telconfig'); - set_config('xxcollection', 'courses', 'local_telconfig'); - set_config('xxapitoken', 'faketoken', 'local_telconfig'); + set_config('findwiseindexurl', 'http://fake.local/api', 'local_telconfig'); + set_config('findwiseindexmethod', 'index', 'local_telconfig'); + set_config('findwisecollection', 'courses', 'local_telconfig'); + set_config('findwiseapitoken', 'faketoken', 'local_telconfig'); // Create a mock API client. $mock = $this->createMock(api_client::class); From 3736362db8473cc30c7c6863b4c5c664b29e1945 Mon Sep 17 00:00:00 2001 From: Binon Date: Wed, 14 May 2025 21:33:56 +0100 Subject: [PATCH 17/41] Created custom constant while testing to fix build error --- local/telconfig/classes/config_exception.php | 29 ++++++++++++++++++++ local/telconfig/classes/helper.php | 6 ++-- local/telconfig/tests/helper_test.php | 1 + 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 local/telconfig/classes/config_exception.php diff --git a/local/telconfig/classes/config_exception.php b/local/telconfig/classes/config_exception.php new file mode 100644 index 00000000000..bae551496ec --- /dev/null +++ b/local/telconfig/classes/config_exception.php @@ -0,0 +1,29 @@ +. + +/** + * Member does not exist exception. + * + * @package local_telconfig + */ + +namespace local_telconfig; + +class config_exception extends \moodle_exception { + public function __construct($message = 'Missing API config for Findwise') { + parent::__construct('error', 'local_telconfig', '', null, $message); + } +} \ No newline at end of file diff --git a/local/telconfig/classes/helper.php b/local/telconfig/classes/helper.php index 3ea8cae4f07..64c2a57e5e1 100644 --- a/local/telconfig/classes/helper.php +++ b/local/telconfig/classes/helper.php @@ -45,8 +45,10 @@ public static function send_findwise_api(array $data, ?api_client $client = null $apiurl = str_replace('{0}', $collection, $indexurl); if (empty($apiurl) || empty($apitoken)) { - debugging('send_findwise_api: API URL or token not set in plugin config.', DEBUG_DEVELOPER); - return; + if (defined('LOCAL_TELCONFIG_DEV_TEST')) { + // Only fail during plugin tests + throw new \local_telconfig\config_exception("send_findwise_api: API URL or token not set in plugin config."); + } } $client ??= new api_client(); diff --git a/local/telconfig/tests/helper_test.php b/local/telconfig/tests/helper_test.php index a5090240e9c..79d0eb96125 100644 --- a/local/telconfig/tests/helper_test.php +++ b/local/telconfig/tests/helper_test.php @@ -21,6 +21,7 @@ class telconfig_helper_test extends advanced_testcase { public function test_send_findwise_api_makes_api_call() { + define('LOCAL_TELCONFIG_DEV_TEST', true); // Flag to enable strict config check $this->resetAfterTest(); // Set fake plugin config values. From 7bde6f30caecc66901b4efb86395d470852ffefe Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 15 May 2025 12:35:22 +0100 Subject: [PATCH 18/41] Updated the logic --- local/telconfig/classes/helper.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/local/telconfig/classes/helper.php b/local/telconfig/classes/helper.php index 64c2a57e5e1..ea132d5d799 100644 --- a/local/telconfig/classes/helper.php +++ b/local/telconfig/classes/helper.php @@ -41,16 +41,18 @@ public static function send_findwise_api(array $data, ?api_client $client = null $collection = get_config('local_telconfig', 'findwisecollection'); $apitoken = get_config('local_telconfig', 'findwiseapitoken'); - $indexurl = rtrim($indexurl, '/') . '/' . $indexmethod . '?token=' . urlencode($apitoken); - $apiurl = str_replace('{0}', $collection, $indexurl); - - if (empty($apiurl) || empty($apitoken)) { + if (empty($indexurl) || empty($apitoken)) { if (defined('LOCAL_TELCONFIG_DEV_TEST')) { - // Only fail during plugin tests throw new \local_telconfig\config_exception("send_findwise_api: API URL or token not set in plugin config."); + } else { + debugging("Findwise API not called: Missing config.", DEBUG_DEVELOPER); // Optional for debugging logs } + return; } + $indexurl = rtrim($indexurl, '/') . '/' . $indexmethod . '?token=' . urlencode($apitoken); + $apiurl = str_replace('{0}', $collection, $indexurl); + $client ??= new api_client(); try { From ab8766c9e7e0943b3685e185ce18ff4ba24fcc79 Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 15 May 2025 15:38:56 +0100 Subject: [PATCH 19/41] Reomoved the condition as it is breaking Test --- local/telconfig/classes/helper.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/local/telconfig/classes/helper.php b/local/telconfig/classes/helper.php index ea132d5d799..8be4938ea27 100644 --- a/local/telconfig/classes/helper.php +++ b/local/telconfig/classes/helper.php @@ -41,12 +41,7 @@ public static function send_findwise_api(array $data, ?api_client $client = null $collection = get_config('local_telconfig', 'findwisecollection'); $apitoken = get_config('local_telconfig', 'findwiseapitoken'); - if (empty($indexurl) || empty($apitoken)) { - if (defined('LOCAL_TELCONFIG_DEV_TEST')) { - throw new \local_telconfig\config_exception("send_findwise_api: API URL or token not set in plugin config."); - } else { - debugging("Findwise API not called: Missing config.", DEBUG_DEVELOPER); // Optional for debugging logs - } + if (empty($indexurl) || empty($apitoken)) { return; } From 1409667068ea9ad04a4ca02280a2c7d265424d57 Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 15 May 2025 21:54:35 +0100 Subject: [PATCH 20/41] Removed unncessary debuggng code to fix the test failure --- local/telconfig/classes/helper.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/local/telconfig/classes/helper.php b/local/telconfig/classes/helper.php index 8be4938ea27..edc9a7bbb2b 100644 --- a/local/telconfig/classes/helper.php +++ b/local/telconfig/classes/helper.php @@ -55,9 +55,7 @@ public static function send_findwise_api(array $data, ?api_client $client = null if ($response === false) { debugging('send_findwise_api: Failed to send data to findwise API.', DEBUG_DEVELOPER); - } else { - debugging('send_findwise_api: Data sent successfully to findwise API.', DEBUG_DEVELOPER); - } + } } catch (\Exception $e) { debugging('send_findwise_api: Exception occurred while sending data: ' . $e->getMessage(), DEBUG_DEVELOPER); } From 716f9e37d3fed8f4a2483dabeca9223eb34ce4ac Mon Sep 17 00:00:00 2001 From: Sarathlal Sarangadharan Date: Fri, 16 May 2025 16:25:13 +0100 Subject: [PATCH 21/41] TD-5485:Max attempts increased --- local/custom_service/externallib.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/local/custom_service/externallib.php b/local/custom_service/externallib.php index c4d5b3a3a0f..be9a18bb4f8 100644 --- a/local/custom_service/externallib.php +++ b/local/custom_service/externallib.php @@ -75,8 +75,7 @@ public static function insert_scorm_resource($courseids,$section,$scormname,$fol $scorm = new stdClass(); $scorm->course = $courseid; $scorm->name = $scormname; - $scorm->reference='Test Ref.zip'; - $scorm->intro = 'Intro to SCORM'; + $scorm->maxattempt=3; $scorm->introformat = FORMAT_HTML; $scorm->timemodified = time(); From 0d5efa8a01bc33acbb36988c0913021d11be29ba Mon Sep 17 00:00:00 2001 From: Sarathlal Sarangadharan Date: Fri, 16 May 2025 16:57:43 +0100 Subject: [PATCH 22/41] TD-5485: Mam attempt increased --- local/custom_service/externallib.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/local/custom_service/externallib.php b/local/custom_service/externallib.php index be9a18bb4f8..b6a995d3852 100644 --- a/local/custom_service/externallib.php +++ b/local/custom_service/externallib.php @@ -75,6 +75,8 @@ public static function insert_scorm_resource($courseids,$section,$scormname,$fol $scorm = new stdClass(); $scorm->course = $courseid; $scorm->name = $scormname; + $scorm->reference='Test Ref.zip'; + $scorm->intro = 'Intro to SCORM'; $scorm->maxattempt=3; $scorm->introformat = FORMAT_HTML; $scorm->timemodified = time(); From 93f134e1c15783d74380eb95b4a21b660e5ff710 Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 29 May 2025 14:13:32 +0100 Subject: [PATCH 23/41] Update the course meta data and added remove funciton --- local/telconfig/classes/api_client.php | 11 +++ .../telconfig/classes/course_data_builder.php | 70 ++++++++++++++ local/telconfig/classes/helper.php | 17 +++- local/telconfig/classes/observer.php | 41 ++++---- local/telconfig/lang/en/local_telconfig.php | 2 +- .../tests/course_data_builder_test.php | 95 +++++++++++++++++++ 6 files changed, 210 insertions(+), 26 deletions(-) create mode 100644 local/telconfig/classes/course_data_builder.php create mode 100644 local/telconfig/tests/course_data_builder_test.php diff --git a/local/telconfig/classes/api_client.php b/local/telconfig/classes/api_client.php index 0cf5646ae49..a909b7ba667 100644 --- a/local/telconfig/classes/api_client.php +++ b/local/telconfig/classes/api_client.php @@ -17,4 +17,15 @@ public function post(string $url, array $data): string|false { $context = stream_context_create($options); return @file_get_contents($url, false, $context); } + + public function delete(string $url): string|false { + $options = [ + 'http' => [ + 'method' => 'DELETE', + 'timeout' => 5, + ] + ]; + $context = stream_context_create($options); + return @file_get_contents($url, false, $context); + } } \ No newline at end of file diff --git a/local/telconfig/classes/course_data_builder.php b/local/telconfig/classes/course_data_builder.php new file mode 100644 index 00000000000..b88be79510e --- /dev/null +++ b/local/telconfig/classes/course_data_builder.php @@ -0,0 +1,70 @@ +. + +/** + * Helper class locally used. + * + * @package local_telconfig + * @copyright + * @license + */ + +namespace local_telconfig; + +defined('MOODLE_INTERNAL') || die(); + +class course_data_builder { + public static function build_course_metadata($course): array { + global $DB, $CFG; + + try { + + // Get course context and teachers (authors) + $context = \context_course::instance($course->id); + $teachers = get_role_users(3, $context); // 3 = editingteacher by default + $authors = array_values(array_map(fn($u) => fullname($u), $teachers)); + + // Extract tags + require_once('../config.php'); + require_once($CFG->dirroot . '../tag/lib.php'); + $tags = \core_tag_tag::get_item_tags('core', 'course', $course->id); + $keywords = array_map(fn($tag) => $tag->rawname, $tags); + + // Prepare data + $data = [ + '_id' => 'M' . $course->id, + 'course_id' => $course->id, + 'authored_date' => date('Y-m-d', $course->startdate), + 'authors' => $authors, + 'catalogue_ids' => [$course->category], + 'description' => format_text($course->summary, FORMAT_HTML), + 'keywords' => array_values($keywords), + 'location_paths' => [], // category hierarchy if needed + 'publication_date' => date('Y-m-d', $course->startdate), + 'rating' => 0, + 'resource_reference_id' => 0, + 'resource_type' => 'Moodle', + 'title' => $course->fullname, + ]; + + return $data; + + } catch (\Throwable $e) { + debugging('Error in course_data_builder: ' . $e->getMessage(), DEBUG_DEVELOPER); + return []; // Always return an array + } + } +} diff --git a/local/telconfig/classes/helper.php b/local/telconfig/classes/helper.php index edc9a7bbb2b..22d42b95f2a 100644 --- a/local/telconfig/classes/helper.php +++ b/local/telconfig/classes/helper.php @@ -35,14 +35,14 @@ class helper { * @param array $data * @return void */ - public static function send_findwise_api(array $data, ?api_client $client = null): void { + public static function send_findwise_api(array $data, string $method = 'POST', ?api_client $client = null): void { $indexurl = get_config('local_telconfig', 'findwiseindexurl'); $indexmethod = get_config('local_telconfig', 'findwiseindexmethod'); $collection = get_config('local_telconfig', 'findwisecollection'); $apitoken = get_config('local_telconfig', 'findwiseapitoken'); if (empty($indexurl) || empty($apitoken)) { - return; + return; } $indexurl = rtrim($indexurl, '/') . '/' . $indexmethod . '?token=' . urlencode($apitoken); @@ -51,7 +51,18 @@ public static function send_findwise_api(array $data, ?api_client $client = null $client ??= new api_client(); try { - $response = $client->post($apiurl, $data); + if ($method === 'DELETE') { + // Add logic to construct a deletion URL with course ID + if (isset($data['course_id'])) { + $deleteurl = rtrim($apiurl, '/') . '&id=M' . $data['course_id']; + $response = $client->delete($deleteurl); + } else { + debugging('send_findwise_api: Cannot perform DELETE without course_id in $data.', DEBUG_DEVELOPER); + return; + } + } else { + $response = $client->post($apiurl, $data); // POST or PUT + } if ($response === false) { debugging('send_findwise_api: Failed to send data to findwise API.', DEBUG_DEVELOPER); diff --git a/local/telconfig/classes/observer.php b/local/telconfig/classes/observer.php index b2d128a6a08..0468733a7f1 100644 --- a/local/telconfig/classes/observer.php +++ b/local/telconfig/classes/observer.php @@ -1,6 +1,7 @@ other['enrol']) || $event->other['enrol'] !== 'self') { - return; - } - try { + // Get enrol instance - $enrol = $DB->get_record('enrol', ['id' => $event->objectid], '*', MUST_EXIST); - + $enrol = $DB->get_record('enrol', ['id' => $event->objectid], '*', MUST_EXIST); + + // Only act if it's for 'self' enrolment. + if (!isset($event->other['enrol']) || $event->other['enrol'] !== 'self') { + return; + } + // Get course info $course = $DB->get_record('course', ['id' => $event->courseid], '*', MUST_EXIST); - $data = [ - '_id' => $course->id, - 'event' => $event->eventname, - 'enrolid' => $enrol->id, - 'courseid' => $course->id, - 'coursename' => $course->fullname, - 'shortname' => $course->shortname, - 'summary' => $course->summary, - 'startdate' => $course->startdate, - 'enddate' => $course->enddate, - 'enrolstatus' => $enrol->status, // 0 = enabled, 1 = disabled - 'time' => time() - ]; - - helper::send_findwise_api($data); + if ((int)$enrol->status === ENROL_INSTANCE_ENABLED) { + // Fetch the enrolment instance data. + $data = course_data_builder::build_course_metadata($course); + helper::send_findwise_api($data); + } else { + // Delete from external API when disabled. + $data = ['course_id' => $course->id]; + helper::send_findwise_api($data,'DELETE'); + } + } catch (\dml_exception $e) { debugging("Failed to fetch course/enrol data: " . $e->getMessage(), DEBUG_DEVELOPER); } diff --git a/local/telconfig/lang/en/local_telconfig.php b/local/telconfig/lang/en/local_telconfig.php index be5810c7897..6047a5b9139 100644 --- a/local/telconfig/lang/en/local_telconfig.php +++ b/local/telconfig/lang/en/local_telconfig.php @@ -12,7 +12,7 @@ $string['Findwisesettings_desc'] = 'Findwise API configurations.'; $string['findwiseindexurl'] = 'Findwise index url'; $string['findwiseindexurl_desc'] = 'Findwise index url desc.'; -$string['findwiseindexmethod'] = 'Findwise index url'; +$string['findwiseindexmethod'] = 'Findwise index method'; $string['findwiseindexmethod_desc'] = 'Findwise index method desc.'; $string['findwisecollection'] = 'Findwise collection'; $string['findwisecollection_desc'] = 'Findwise collection desc.'; diff --git a/local/telconfig/tests/course_data_builder_test.php b/local/telconfig/tests/course_data_builder_test.php new file mode 100644 index 00000000000..1786554886b --- /dev/null +++ b/local/telconfig/tests/course_data_builder_test.php @@ -0,0 +1,95 @@ +. + +// File: local/telconfig/tests/course_data_builder_test.php + +namespace local_telconfig\tests; + +use advanced_testcase; +use context_course; +use core_tag_tag; +use local_telconfig\course_data_builder; + +defined('MOODLE_INTERNAL') || die(); + +class course_data_builder_test extends advanced_testcase { + + public function test_build_course_metadata_returns_expected_array() { + global $CFG; + + $this->resetAfterTest(true); + + // Setup a fake course object + $course = new \stdClass(); + $course->id = 5; + $course->startdate = strtotime('2020-01-01'); + $course->category = 1; + $course->summary = 'Test summary'; + $course->fullname = 'Test course full name'; + + // Mock context_course::instance to return a dummy context + $context = $this->createMock(context_course::class); + $this->mockStaticMethod('context_course', 'instance', $context); + + // Mock get_role_users to return dummy users + // Moodle's get_role_users is a procedural function, so we use a workaround: + // We define a global function for the test that returns dummy users. + // But since we cannot redefine easily, let's simulate authors directly: + // So we override the course_data_builder to mock authors for this test. + + // Mock core_tag_tag::get_item_tags static method + $tag1 = new \stdClass(); + $tag1->rawname = 'tag1'; + $tag2 = new \stdClass(); + $tag2->rawname = 'tag2'; + + // Override core_tag_tag::get_item_tags by monkey patch (not trivial in PHP) + // Instead, we can temporarily redefine the method by extending the class, + // but for simplicity here, just test the output keys and types. + + // Call the method under test + $result = course_data_builder::build_course_metadata($course); + + // Assert returned data is an array with expected keys + $this->assertIsArray($result); + $this->assertArrayHasKey('_id', $result); + $this->assertSame('M' . $course->id, $result['_id']); + $this->assertArrayHasKey('course_id', $result); + $this->assertSame($course->id, $result['course_id']); + $this->assertArrayHasKey('authored_date', $result); + $this->assertSame(date('Y-m-d', $course->startdate), $result['authored_date']); + $this->assertArrayHasKey('authors', $result); + $this->assertIsArray($result['authors']); + $this->assertArrayHasKey('catalogue_ids', $result); + $this->assertEquals([$course->category], $result['catalogue_ids']); + $this->assertArrayHasKey('description', $result); + $this->assertIsString($result['description']); + $this->assertArrayHasKey('keywords', $result); + $this->assertIsArray($result['keywords']); + $this->assertArrayHasKey('location_paths', $result); + $this->assertIsArray($result['location_paths']); + $this->assertArrayHasKey('publication_date', $result); + $this->assertSame(date('Y-m-d', $course->startdate), $result['publication_date']); + $this->assertArrayHasKey('rating', $result); + $this->assertSame(0, $result['rating']); + $this->assertArrayHasKey('resource_reference_id', $result); + $this->assertSame(0, $result['resource_reference_id']); + $this->assertArrayHasKey('resource_type', $result); + $this->assertSame('Moodle', $result['resource_type']); + $this->assertArrayHasKey('title', $result); + $this->assertSame($course->fullname, $result['title']); + } +} From b53054b8b2b5d5f84946c426fd2197594d978ab7 Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 2 Jun 2025 12:07:07 +0100 Subject: [PATCH 24/41] Updated resource type from moodle to course --- local/telconfig/classes/course_data_builder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/telconfig/classes/course_data_builder.php b/local/telconfig/classes/course_data_builder.php index b88be79510e..408c51cc640 100644 --- a/local/telconfig/classes/course_data_builder.php +++ b/local/telconfig/classes/course_data_builder.php @@ -56,7 +56,7 @@ public static function build_course_metadata($course): array { 'publication_date' => date('Y-m-d', $course->startdate), 'rating' => 0, 'resource_reference_id' => 0, - 'resource_type' => 'Moodle', + 'resource_type' => 'Course', 'title' => $course->fullname, ]; From e06198ef3ff51e6447bf4eebe6e91d9d80cfd77a Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 5 Jun 2025 12:06:42 +0100 Subject: [PATCH 25/41] Fixing the test error --- .../telconfig/classes/course_data_builder.php | 3 +- .../tests/course_data_builder_test.php | 66 ++++++------------- 2 files changed, 22 insertions(+), 47 deletions(-) diff --git a/local/telconfig/classes/course_data_builder.php b/local/telconfig/classes/course_data_builder.php index 408c51cc640..e05afa8852f 100644 --- a/local/telconfig/classes/course_data_builder.php +++ b/local/telconfig/classes/course_data_builder.php @@ -38,8 +38,7 @@ public static function build_course_metadata($course): array { $authors = array_values(array_map(fn($u) => fullname($u), $teachers)); // Extract tags - require_once('../config.php'); - require_once($CFG->dirroot . '../tag/lib.php'); + require_once($CFG->dirroot . '/tag/lib.php'); $tags = \core_tag_tag::get_item_tags('core', 'course', $course->id); $keywords = array_map(fn($tag) => $tag->rawname, $tags); diff --git a/local/telconfig/tests/course_data_builder_test.php b/local/telconfig/tests/course_data_builder_test.php index 1786554886b..8d6810c2fe2 100644 --- a/local/telconfig/tests/course_data_builder_test.php +++ b/local/telconfig/tests/course_data_builder_test.php @@ -27,69 +27,45 @@ class course_data_builder_test extends advanced_testcase { + public function test_build_course_metadata_returns_expected_array() { - global $CFG; + global $DB; $this->resetAfterTest(true); - // Setup a fake course object - $course = new \stdClass(); - $course->id = 5; - $course->startdate = strtotime('2020-01-01'); - $course->category = 1; - $course->summary = 'Test summary'; - $course->fullname = 'Test course full name'; + // Create a course using Moodle's generator + $course = $this->getDataGenerator()->create_course([ + 'fullname' => 'Test Course', + 'summary' => 'Test summary', + 'startdate' => strtotime('2022-01-01') + ]); - // Mock context_course::instance to return a dummy context - $context = $this->createMock(context_course::class); - $this->mockStaticMethod('context_course', 'instance', $context); + // Enrol a teacher in the course + $teacher = $this->getDataGenerator()->create_user(['firstname' => 'Alice', 'lastname' => 'Teacher']); + $roleid = 3; // editingteacher + $context = context_course::instance($course->id); + role_assign($roleid, $teacher->id, $context->id); - // Mock get_role_users to return dummy users - // Moodle's get_role_users is a procedural function, so we use a workaround: - // We define a global function for the test that returns dummy users. - // But since we cannot redefine easily, let's simulate authors directly: - // So we override the course_data_builder to mock authors for this test. + // Add tags to course + core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), ['tag1', 'tag2']); - // Mock core_tag_tag::get_item_tags static method - $tag1 = new \stdClass(); - $tag1->rawname = 'tag1'; - $tag2 = new \stdClass(); - $tag2->rawname = 'tag2'; - - // Override core_tag_tag::get_item_tags by monkey patch (not trivial in PHP) - // Instead, we can temporarily redefine the method by extending the class, - // but for simplicity here, just test the output keys and types. - - // Call the method under test + // Call the method $result = course_data_builder::build_course_metadata($course); - // Assert returned data is an array with expected keys + // Assertions $this->assertIsArray($result); - $this->assertArrayHasKey('_id', $result); $this->assertSame('M' . $course->id, $result['_id']); - $this->assertArrayHasKey('course_id', $result); $this->assertSame($course->id, $result['course_id']); - $this->assertArrayHasKey('authored_date', $result); $this->assertSame(date('Y-m-d', $course->startdate), $result['authored_date']); - $this->assertArrayHasKey('authors', $result); - $this->assertIsArray($result['authors']); - $this->assertArrayHasKey('catalogue_ids', $result); + $this->assertContains(fullname($teacher), $result['authors']); $this->assertEquals([$course->category], $result['catalogue_ids']); - $this->assertArrayHasKey('description', $result); - $this->assertIsString($result['description']); - $this->assertArrayHasKey('keywords', $result); - $this->assertIsArray($result['keywords']); - $this->assertArrayHasKey('location_paths', $result); + $this->assertSame(format_text($course->summary, FORMAT_HTML), $result['description']); + $this->assertEquals(['tag1', 'tag2'], $result['keywords']); $this->assertIsArray($result['location_paths']); - $this->assertArrayHasKey('publication_date', $result); $this->assertSame(date('Y-m-d', $course->startdate), $result['publication_date']); - $this->assertArrayHasKey('rating', $result); $this->assertSame(0, $result['rating']); - $this->assertArrayHasKey('resource_reference_id', $result); $this->assertSame(0, $result['resource_reference_id']); - $this->assertArrayHasKey('resource_type', $result); - $this->assertSame('Moodle', $result['resource_type']); - $this->assertArrayHasKey('title', $result); + $this->assertSame('Course', $result['resource_type']); $this->assertSame($course->fullname, $result['title']); } } From fc3d940ae053c8aa480bb20d33aebcb9e4b8c98e Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 5 Jun 2025 14:18:34 +0100 Subject: [PATCH 26/41] Fixed the test --- local/telconfig/tests/helper_test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/telconfig/tests/helper_test.php b/local/telconfig/tests/helper_test.php index 79d0eb96125..0e60cafbe3a 100644 --- a/local/telconfig/tests/helper_test.php +++ b/local/telconfig/tests/helper_test.php @@ -41,6 +41,6 @@ public function test_send_findwise_api_makes_api_call() { ->willReturn('{"status":"ok"}'); // Call the method with the mock client. - helper::send_findwise_api(['courseid' => 123], $mock); + helper::send_findwise_api(['courseid' => 123], 'POST', $mock); } } From 14535fa0657e0676c51901d37073e29112e9b2a2 Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Fri, 6 Jun 2025 11:51:53 +0100 Subject: [PATCH 27/41] Added PersistentVolume and PersistentVolumeClaim for rest uploads --- .../learninghub-moodle_Deploy_dev.yml | 6 ++++ kubectl/deployment-dev.yml | 7 ++++- kubectl/pv-definition-uploads-dev.yml | 31 +++++++++++++++++++ kubectl/pvc-definition-uploads-dev.yml | 13 ++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 kubectl/pv-definition-uploads-dev.yml create mode 100644 kubectl/pvc-definition-uploads-dev.yml diff --git a/.github/workflows/learninghub-moodle_Deploy_dev.yml b/.github/workflows/learninghub-moodle_Deploy_dev.yml index 55ce50f78d0..7ed511ed4a9 100644 --- a/.github/workflows/learninghub-moodle_Deploy_dev.yml +++ b/.github/workflows/learninghub-moodle_Deploy_dev.yml @@ -104,6 +104,9 @@ jobs: - name: Create PersistentVolume for theme run: kubectl apply -f kubectl/pv-definition-theme-dev.yml + + - name: Create PersistentVolume for uploads + run: kubectl apply -f kubectl/pv-definition-uploads-dev.yml - name: Create PersistentVolumeClaim run: kubectl apply -f kubectl/pvc-definition-dev.yml @@ -111,6 +114,9 @@ jobs: - name: Create PersistentVolumeClaim for theme run: kubectl apply -f kubectl/pvc-definition-theme-dev.yml + - name: Create PersistentVolumeClaim for uploads + run: kubectl apply -f kubectl/pvc-definition-uploads-dev.yml + - name: Attach ACR to cluster run: az aks update -n ${{ vars.AZURE_CLUSTER_NAME }} -g ${{ vars.AZURE_RESOURCE_GROUP_NAME }} --attach-acr ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }} continue-on-error: true diff --git a/kubectl/deployment-dev.yml b/kubectl/deployment-dev.yml index bb04de4cd29..17d546770d0 100644 --- a/kubectl/deployment-dev.yml +++ b/kubectl/deployment-dev.yml @@ -32,10 +32,15 @@ spec: readOnly: false - name: moodletheme mountPath: /var/www/html/theme + - name: moodlerestuploads + mountPath: /var/www/html/webservices/rest/uploads volumes: - name: moodledata persistentVolumeClaim: claimName: moodledataclaim - name: moodletheme persistentVolumeClaim: - claimName: moodlethemeclaim \ No newline at end of file + claimName: moodlethemeclaim + - name: moodlerestuploads + persistentVolumeClaim: + claimName: moodleuploadsclaim \ No newline at end of file diff --git a/kubectl/pv-definition-uploads-dev.yml b/kubectl/pv-definition-uploads-dev.yml new file mode 100644 index 00000000000..ea892cdd72c --- /dev/null +++ b/kubectl/pv-definition-uploads-dev.yml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + annotations: + pv.kubernetes.io/provisioned-by: file.csi.azure.com + name: moodlerestuploads +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-csi + csi: + driver: file.csi.azure.com + volumeHandle: "moodleCluster#learninghubmoodledev#moodlerestuploads" # make sure this volumeid is unique for every identical share in the cluster + volumeAttributes: + shareName: moodlerestuploads + server: learninghubmoodledev.privatelink.file.core.windows.net + nodeStageSecretRef: + name: azure-secret + namespace: learninghubmoodle + mountOptions: + - dir_mode=0777 + - file_mode=0777 + - uid=0 + - gid=0 + - mfsymlinks + - cache=strict + - nosharesock + - nobrl # disable sending byte range lock requests to the server and for applications which have challenges with posix locks \ No newline at end of file diff --git a/kubectl/pvc-definition-uploads-dev.yml b/kubectl/pvc-definition-uploads-dev.yml new file mode 100644 index 00000000000..b511922874e --- /dev/null +++ b/kubectl/pvc-definition-uploads-dev.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: moodleuploadsclaim + namespace: learninghubmoodle +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + storageClassName: azurefile-csi + volumeName: moodlerestuploads \ No newline at end of file From 2b1f55ab1b7ebb9bbbc19d9edcbbae465d986a17 Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Fri, 6 Jun 2025 12:22:20 +0100 Subject: [PATCH 28/41] Added moodlerestuploads file store to terraform --- Terraform/dev/main.tf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Terraform/dev/main.tf b/Terraform/dev/main.tf index 0d19d3e8b3a..6a193db45af 100644 --- a/Terraform/dev/main.tf +++ b/Terraform/dev/main.tf @@ -23,6 +23,12 @@ resource "azurerm_storage_share" "storage_share_theme" { quota = var.StorageQuota } +resource "azurerm_storage_share" "storage_share_uploads" { + name = "moodlerestuploads" + storage_account_name = azurerm_storage_account.storage_account.name + quota = var.StorageQuota +} + resource "azurerm_storage_container" "assessment_container" { name = "assessmentstoragecontainer" storage_account_name = azurerm_storage_account.storage_account.name From feb699dbdbd015095c0d68fb337c2916e8e9e0fc Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Fri, 6 Jun 2025 13:28:48 +0100 Subject: [PATCH 29/41] Added uploads folder to give path to persistent volume --- webservice/rest/uploads/null.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 webservice/rest/uploads/null.txt diff --git a/webservice/rest/uploads/null.txt b/webservice/rest/uploads/null.txt new file mode 100644 index 00000000000..e69de29bb2d From 2295c090b82fc45a05483f45f4553f15135317e6 Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Fri, 6 Jun 2025 14:02:54 +0100 Subject: [PATCH 30/41] corrected mount path for webservice/rest/uploads --- kubectl/deployment-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubectl/deployment-dev.yml b/kubectl/deployment-dev.yml index 17d546770d0..496e45cfe4a 100644 --- a/kubectl/deployment-dev.yml +++ b/kubectl/deployment-dev.yml @@ -33,7 +33,7 @@ spec: - name: moodletheme mountPath: /var/www/html/theme - name: moodlerestuploads - mountPath: /var/www/html/webservices/rest/uploads + mountPath: /var/www/html/webservice/rest/uploads volumes: - name: moodledata persistentVolumeClaim: From 4f80648033a19d503dba759f50997cfff6b039bf Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Fri, 6 Jun 2025 16:09:40 +0100 Subject: [PATCH 31/41] Added persistent volumes and claims for uploads for all environments --- .../learninghub-moodle_Deploy_pgvle.yml | 6 ++++ .../learninghub-moodle_Deploy_prod.yml | 6 ++++ .../learninghub-moodle_Deploy_test.yml | 6 ++++ Terraform/pgvle/main.tf | 6 ++++ Terraform/prod/main.tf | 6 ++++ Terraform/test/main.tf | 6 ++++ kubectl/pv-definition-prod.yml | 1 + kubectl/pv-definition-uploads-pgvle.yml | 31 +++++++++++++++++++ kubectl/pv-definition-uploads-prod.yml | 31 +++++++++++++++++++ kubectl/pv-definition-uploads-test.yml | 31 +++++++++++++++++++ kubectl/pvc-definition-uploads-pgvle.yml | 13 ++++++++ kubectl/pvc-definition-uploads-prod.yml | 13 ++++++++ kubectl/pvc-definition-uploads-test.yml | 13 ++++++++ 13 files changed, 169 insertions(+) create mode 100644 kubectl/pv-definition-uploads-pgvle.yml create mode 100644 kubectl/pv-definition-uploads-prod.yml create mode 100644 kubectl/pv-definition-uploads-test.yml create mode 100644 kubectl/pvc-definition-uploads-pgvle.yml create mode 100644 kubectl/pvc-definition-uploads-prod.yml create mode 100644 kubectl/pvc-definition-uploads-test.yml diff --git a/.github/workflows/learninghub-moodle_Deploy_pgvle.yml b/.github/workflows/learninghub-moodle_Deploy_pgvle.yml index fab0acacaaf..7abceff4659 100644 --- a/.github/workflows/learninghub-moodle_Deploy_pgvle.yml +++ b/.github/workflows/learninghub-moodle_Deploy_pgvle.yml @@ -104,6 +104,9 @@ jobs: - name: Create PersistentVolume for theme run: kubectl apply -f kubectl/pv-definition-theme-${{ vars.AZURE_ENVIRONMENT }}.yml + + - name: Create PersistentVolume for uploads + run: kubectl apply -f kubectl/pv-definition-uploads-${{ vars.AZURE_ENVIRONMENT }}.yml - name: Create PersistentVolumeClaim run: kubectl apply -f kubectl/pvc-definition-${{ vars.AZURE_ENVIRONMENT }}.yml @@ -111,6 +114,9 @@ jobs: - name: Create PersistentVolumeClaim for theme run: kubectl apply -f kubectl/pvc-definition-theme-${{ vars.AZURE_ENVIRONMENT }}.yml + - name: Create PersistentVolumeClaim for uploads + run: kubectl apply -f kubectl/pvc-definition-uploads-${{ vars.AZURE_ENVIRONMENT }}.yml + - name: Attach ACR to cluster run: az aks update -n ${{ vars.AZURE_CLUSTER_NAME }} -g ${{ vars.AZURE_RESOURCE_GROUP_NAME }} --attach-acr ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }} continue-on-error: true diff --git a/.github/workflows/learninghub-moodle_Deploy_prod.yml b/.github/workflows/learninghub-moodle_Deploy_prod.yml index 176182d3c98..0e45c16ab9b 100644 --- a/.github/workflows/learninghub-moodle_Deploy_prod.yml +++ b/.github/workflows/learninghub-moodle_Deploy_prod.yml @@ -149,6 +149,9 @@ jobs: - name: Create PersistentVolume for theme run: kubectl apply -f kubectl/pv-definition-theme-prod.yml + + - name: Create PersistentVolume for uploads + run: kubectl apply -f kubectl/pv-definition-uploads-prod.yml - name: Create PersistentVolumeClaim run: kubectl apply -f kubectl/pvc-definition-prod.yml @@ -156,6 +159,9 @@ jobs: - name: Create PersistentVolumeClaim for theme run: kubectl apply -f kubectl/pvc-definition-theme-prod.yml + - name: Create PersistentVolumeClaim for uploads + run: kubectl apply -f kubectl/pvc-definition-uploads-prod.yml + - name: Attach ACR to cluster run: az aks update -n ${{ vars.AZURE_CLUSTER_NAME }} -g ${{ vars.AZURE_RESOURCE_GROUP_NAME }} --attach-acr ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }} continue-on-error: true diff --git a/.github/workflows/learninghub-moodle_Deploy_test.yml b/.github/workflows/learninghub-moodle_Deploy_test.yml index 785f2758336..abfa8eedb08 100644 --- a/.github/workflows/learninghub-moodle_Deploy_test.yml +++ b/.github/workflows/learninghub-moodle_Deploy_test.yml @@ -104,6 +104,9 @@ jobs: - name: Create PersistentVolume for theme run: kubectl apply -f kubectl/pv-definition-theme-test.yml + + - name: Create PersistentVolume for uploads + run: kubectl apply -f kubectl/pv-definition-uploads-test.yml - name: Create PersistentVolumeClaim run: kubectl apply -f kubectl/pvc-definition-test.yml @@ -111,6 +114,9 @@ jobs: - name: Create PersistentVolumeClaim for theme run: kubectl apply -f kubectl/pvc-definition-theme-test.yml + - name: Create PersistentVolumeClaim for uploads + run: kubectl apply -f kubectl/pvc-definition-uploads-test.yml + - name: Attach ACR to cluster run: az aks update -n ${{ vars.AZURE_CLUSTER_NAME }} -g ${{ vars.AZURE_RESOURCE_GROUP_NAME }} --attach-acr ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }} continue-on-error: true diff --git a/Terraform/pgvle/main.tf b/Terraform/pgvle/main.tf index df432d5908a..203c7512e9f 100644 --- a/Terraform/pgvle/main.tf +++ b/Terraform/pgvle/main.tf @@ -23,6 +23,12 @@ resource "azurerm_storage_share" "storage_share_theme" { quota = var.StorageQuota } +resource "azurerm_storage_share" "storage_share_uploads" { + name = "moodlerestuploads" + storage_account_name = azurerm_storage_account.storage_account.name + quota = var.StorageQuota +} + resource "azurerm_storage_container" "assessment_container" { name = "assessmentstoragecontainer" storage_account_name = azurerm_storage_account.storage_account.name diff --git a/Terraform/prod/main.tf b/Terraform/prod/main.tf index 36a0d1d0a88..459b7110efb 100644 --- a/Terraform/prod/main.tf +++ b/Terraform/prod/main.tf @@ -23,6 +23,12 @@ resource "azurerm_storage_share" "storage_share_theme" { quota = var.StorageQuota } +resource "azurerm_storage_share" "storage_share_uploads" { + name = "moodlerestuploads" + storage_account_name = azurerm_storage_account.storage_account.name + quota = var.StorageQuota +} + resource "azurerm_storage_container" "assessment_container" { name = "assessmentstoragecontainer" storage_account_name = azurerm_storage_account.storage_account.name diff --git a/Terraform/test/main.tf b/Terraform/test/main.tf index 40507a17f27..5b5df04a98a 100644 --- a/Terraform/test/main.tf +++ b/Terraform/test/main.tf @@ -23,6 +23,12 @@ resource "azurerm_storage_share" "storage_share_theme" { quota = var.StorageQuota } +resource "azurerm_storage_share" "storage_share_uploads" { + name = "moodlerestuploads" + storage_account_name = azurerm_storage_account.storage_account.name + quota = var.StorageQuota +} + resource "azurerm_storage_container" "assessment_container" { name = "assessmentstoragecontainer" storage_account_name = azurerm_storage_account.storage_account.name diff --git a/kubectl/pv-definition-prod.yml b/kubectl/pv-definition-prod.yml index b93608f45be..80e36120dd0 100644 --- a/kubectl/pv-definition-prod.yml +++ b/kubectl/pv-definition-prod.yml @@ -16,6 +16,7 @@ spec: volumeHandle: "moodleCluster#learninghubmoodleprod#moodledata" # make sure this volumeid is unique for every identical share in the cluster volumeAttributes: shareName: moodledata + server: learninghubmoodleprod.privatelink.file.core.windows.net nodeStageSecretRef: name: azure-secret namespace: learninghubmoodle diff --git a/kubectl/pv-definition-uploads-pgvle.yml b/kubectl/pv-definition-uploads-pgvle.yml new file mode 100644 index 00000000000..66fee38d210 --- /dev/null +++ b/kubectl/pv-definition-uploads-pgvle.yml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + annotations: + pv.kubernetes.io/provisioned-by: file.csi.azure.com + name: moodlerestuploads +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-csi + csi: + driver: file.csi.azure.com + volumeHandle: "moodleCluster#learninghubmoodlepgvle#moodlerestuploads" # make sure this volumeid is unique for every identical share in the cluster + volumeAttributes: + shareName: moodlerestuploads + server: learninghubmoodledev.privatelink.file.core.windows.net + nodeStageSecretRef: + name: azure-secret + namespace: learninghubmoodle + mountOptions: + - dir_mode=0777 + - file_mode=0777 + - uid=0 + - gid=0 + - mfsymlinks + - cache=strict + - nosharesock + - nobrl # disable sending byte range lock requests to the server and for applications which have challenges with posix locks \ No newline at end of file diff --git a/kubectl/pv-definition-uploads-prod.yml b/kubectl/pv-definition-uploads-prod.yml new file mode 100644 index 00000000000..e8e1b977eae --- /dev/null +++ b/kubectl/pv-definition-uploads-prod.yml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + annotations: + pv.kubernetes.io/provisioned-by: file.csi.azure.com + name: moodlerestuploads +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-csi + csi: + driver: file.csi.azure.com + volumeHandle: "moodleCluster#learninghubmoodleprod#moodlerestuploads" # make sure this volumeid is unique for every identical share in the cluster + volumeAttributes: + shareName: moodlerestuploads + server: learninghubmoodleprod.privatelink.file.core.windows.net + nodeStageSecretRef: + name: azure-secret + namespace: learninghubmoodle + mountOptions: + - dir_mode=0777 + - file_mode=0777 + - uid=0 + - gid=0 + - mfsymlinks + - cache=strict + - nosharesock + - nobrl # disable sending byte range lock requests to the server and for applications which have challenges with posix locks \ No newline at end of file diff --git a/kubectl/pv-definition-uploads-test.yml b/kubectl/pv-definition-uploads-test.yml new file mode 100644 index 00000000000..2a3577a19b9 --- /dev/null +++ b/kubectl/pv-definition-uploads-test.yml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + annotations: + pv.kubernetes.io/provisioned-by: file.csi.azure.com + name: moodlerestuploads +spec: + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-csi + csi: + driver: file.csi.azure.com + volumeHandle: "moodleCluster#learninghubmoodletest#moodlerestuploads" # make sure this volumeid is unique for every identical share in the cluster + volumeAttributes: + shareName: moodlerestuploads + server: learninghubmoodletest.privatelink.file.core.windows.net + nodeStageSecretRef: + name: azure-secret + namespace: learninghubmoodle + mountOptions: + - dir_mode=0777 + - file_mode=0777 + - uid=0 + - gid=0 + - mfsymlinks + - cache=strict + - nosharesock + - nobrl # disable sending byte range lock requests to the server and for applications which have challenges with posix locks \ No newline at end of file diff --git a/kubectl/pvc-definition-uploads-pgvle.yml b/kubectl/pvc-definition-uploads-pgvle.yml new file mode 100644 index 00000000000..b511922874e --- /dev/null +++ b/kubectl/pvc-definition-uploads-pgvle.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: moodleuploadsclaim + namespace: learninghubmoodle +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + storageClassName: azurefile-csi + volumeName: moodlerestuploads \ No newline at end of file diff --git a/kubectl/pvc-definition-uploads-prod.yml b/kubectl/pvc-definition-uploads-prod.yml new file mode 100644 index 00000000000..b511922874e --- /dev/null +++ b/kubectl/pvc-definition-uploads-prod.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: moodleuploadsclaim + namespace: learninghubmoodle +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + storageClassName: azurefile-csi + volumeName: moodlerestuploads \ No newline at end of file diff --git a/kubectl/pvc-definition-uploads-test.yml b/kubectl/pvc-definition-uploads-test.yml new file mode 100644 index 00000000000..b511922874e --- /dev/null +++ b/kubectl/pvc-definition-uploads-test.yml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: moodleuploadsclaim + namespace: learninghubmoodle +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + storageClassName: azurefile-csi + volumeName: moodlerestuploads \ No newline at end of file From e3137b5eebd221f0e36bcf4b58c081d773dd4688 Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 13 Jun 2025 11:20:05 +0100 Subject: [PATCH 32/41] Masked the token field --- local/telconfig/settings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/telconfig/settings.php b/local/telconfig/settings.php index 0f21b4e026d..278a247a63f 100644 --- a/local/telconfig/settings.php +++ b/local/telconfig/settings.php @@ -35,7 +35,7 @@ get_string('findwiseindexmethod', 'local_telconfig'), get_string('findwiseindexmethod_desc', 'local_telconfig'), '', PARAM_TEXT)); $settings->add(new admin_setting_configtext('local_telconfig/findwisecollection', get_string('findwisecollection', 'local_telconfig'), get_string('findwisecollection_desc', 'local_telconfig'), '', PARAM_TEXT)); - $settings->add(new admin_setting_configtext('local_telconfig/findwiseapitoken', + $settings->add(new admin_setting_configpasswordunmask('local_telconfig/findwiseapitoken', get_string('findwiseapitoken', 'local_telconfig'), get_string('findwiseapitoken_desc', 'local_telconfig'), '', PARAM_TEXT)); //Mustache template settings From af97d33c3cb16358c2309bdd3d88a49fce733b28 Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Tue, 17 Jun 2025 15:01:57 +0100 Subject: [PATCH 33/41] added uploads folder permissions correction to Action --- .github/workflows/learninghub-moodle_Deploy_dev.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/learninghub-moodle_Deploy_dev.yml b/.github/workflows/learninghub-moodle_Deploy_dev.yml index 7ed511ed4a9..d53d91f60c6 100644 --- a/.github/workflows/learninghub-moodle_Deploy_dev.yml +++ b/.github/workflows/learninghub-moodle_Deploy_dev.yml @@ -194,6 +194,9 @@ jobs: rm -f $(basename $zipfile); \ done + - name: Ensure uploads folder has correct permissions applied + run: chmod ugo+rwx ./webservice/rest/uploads + - name: Build and push Docker image run: | docker build \ From 775b13dfc925fca722bd0f6aa110e7cabb29d0ad Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Tue, 17 Jun 2025 15:48:33 +0100 Subject: [PATCH 34/41] Added permission modified to all CI/CD branches --- .github/workflows/learninghub-moodle_Deploy_pgvle.yml | 3 +++ .github/workflows/learninghub-moodle_Deploy_prod.yml | 3 +++ .github/workflows/learninghub-moodle_Deploy_test.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.github/workflows/learninghub-moodle_Deploy_pgvle.yml b/.github/workflows/learninghub-moodle_Deploy_pgvle.yml index 7abceff4659..aa5c15e4af8 100644 --- a/.github/workflows/learninghub-moodle_Deploy_pgvle.yml +++ b/.github/workflows/learninghub-moodle_Deploy_pgvle.yml @@ -194,6 +194,9 @@ jobs: rm -f $(basename $zipfile); \ done + - name: Ensure uploads folder has correct permissions applied + run: chmod ugo+rwx ./webservice/rest/uploads + - name: Build and push Docker image run: | docker build -t ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }}.azurecr.io/${{ vars.DOCKER_IMAGE_NAME }}:latest . diff --git a/.github/workflows/learninghub-moodle_Deploy_prod.yml b/.github/workflows/learninghub-moodle_Deploy_prod.yml index 0e45c16ab9b..b58d5c3a2d4 100644 --- a/.github/workflows/learninghub-moodle_Deploy_prod.yml +++ b/.github/workflows/learninghub-moodle_Deploy_prod.yml @@ -239,6 +239,9 @@ jobs: rm -f $(basename $zipfile); \ done + - name: Ensure uploads folder has correct permissions applied + run: chmod ugo+rwx ./webservice/rest/uploads + - name: Build and push Docker image run: | docker build \ diff --git a/.github/workflows/learninghub-moodle_Deploy_test.yml b/.github/workflows/learninghub-moodle_Deploy_test.yml index abfa8eedb08..3bcba262dd7 100644 --- a/.github/workflows/learninghub-moodle_Deploy_test.yml +++ b/.github/workflows/learninghub-moodle_Deploy_test.yml @@ -194,6 +194,9 @@ jobs: rm -f $(basename $zipfile); \ done + - name: Ensure uploads folder has correct permissions applied + run: chmod ugo+rwx ./webservice/rest/uploads + - name: Build and push Docker image run: | docker build -t ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }}.azurecr.io/${{ vars.DOCKER_IMAGE_NAME }}:latest . From e67d277832ad17a6116153e72e984c8de8c8b61f Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 19 Jun 2025 12:19:11 +0100 Subject: [PATCH 35/41] When course details updated fire the external API --- .../telconfig/classes/course_data_builder.php | 2 +- local/telconfig/classes/observer.php | 59 +++++++++++++------ local/telconfig/db/events.php | 6 ++ 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/local/telconfig/classes/course_data_builder.php b/local/telconfig/classes/course_data_builder.php index e05afa8852f..3a13a4dff45 100644 --- a/local/telconfig/classes/course_data_builder.php +++ b/local/telconfig/classes/course_data_builder.php @@ -48,7 +48,7 @@ public static function build_course_metadata($course): array { 'course_id' => $course->id, 'authored_date' => date('Y-m-d', $course->startdate), 'authors' => $authors, - 'catalogue_ids' => [$course->category], + 'catalogue_ids' => [],//[$course->category], 'description' => format_text($course->summary, FORMAT_HTML), 'keywords' => array_values($keywords), 'location_paths' => [], // category hierarchy if needed diff --git a/local/telconfig/classes/observer.php b/local/telconfig/classes/observer.php index 0468733a7f1..1eb7a13e75e 100644 --- a/local/telconfig/classes/observer.php +++ b/local/telconfig/classes/observer.php @@ -17,33 +17,54 @@ class observer { * @return void */ public static function enrol_instance_changed(\core\event\base $event): void { - global $DB; + global $DB; - try { + try { - // Get enrol instance - $enrol = $DB->get_record('enrol', ['id' => $event->objectid], '*', MUST_EXIST); + // Get enrol instance + $enrol = $DB->get_record('enrol', ['id' => $event->objectid], '*', MUST_EXIST); - // Only act if it's for 'self' enrolment. - if (!isset($event->other['enrol']) || $event->other['enrol'] !== 'self') { - return; + // Only act if it's for 'self' enrolment. + if (!isset($event->other['enrol']) || $event->other['enrol'] !== 'self') { + return; + } + + // Get course info + $course = $DB->get_record('course', ['id' => $event->courseid], '*', MUST_EXIST); + + if ((int)$enrol->status === ENROL_INSTANCE_ENABLED) { + // Fetch the enrolment instance data. + $data = course_data_builder::build_course_metadata($course); + helper::send_findwise_api($data); + } else { + // Delete from external API when disabled. + $data = ['course_id' => $course->id]; + helper::send_findwise_api($data,'DELETE'); + } + + } catch (\dml_exception $e) { + debugging("Failed to fetch course/enrol data: " . $e->getMessage(), DEBUG_DEVELOPER); } + } - // Get course info - $course = $DB->get_record('course', ['id' => $event->courseid], '*', MUST_EXIST); + /** + * Triggered when a course is updated. + * + * @param \core\event\base $event + * @return void + */ + public static function local_course_updated(\core\event\base $event): void { + global $DB; + + try { + $course = $DB->get_record('course', ['id' => $event->objectid], '*', MUST_EXIST); - if ((int)$enrol->status === ENROL_INSTANCE_ENABLED) { - // Fetch the enrolment instance data. + // Rebuild and send metadata to API (as an update). $data = course_data_builder::build_course_metadata($course); helper::send_findwise_api($data); - } else { - // Delete from external API when disabled. - $data = ['course_id' => $course->id]; - helper::send_findwise_api($data,'DELETE'); - } - } catch (\dml_exception $e) { - debugging("Failed to fetch course/enrol data: " . $e->getMessage(), DEBUG_DEVELOPER); + } catch (\dml_exception $e) { + debugging("Failed to process course update: " . $e->getMessage(), DEBUG_DEVELOPER); + } } } -} diff --git a/local/telconfig/db/events.php b/local/telconfig/db/events.php index 7a3a54595ef..d6b56b72b4c 100644 --- a/local/telconfig/db/events.php +++ b/local/telconfig/db/events.php @@ -7,4 +7,10 @@ 'priority' => 9999, 'internal' => false, ], + [ + 'eventname' => '\core\event\course_updated', + 'callback' => '\local_telconfig\observer::local_course_updated', + 'priority' => 9999, + 'internal' => false, + ], ]; From 5bb1523e4a40d41225dc6f5c3cf5300a5f9bfd7b Mon Sep 17 00:00:00 2001 From: Binon Date: Thu, 19 Jun 2025 15:01:24 +0100 Subject: [PATCH 36/41] Fixing the test and making the keywords all lower before send to findwise --- local/telconfig/classes/course_data_builder.php | 2 +- local/telconfig/tests/course_data_builder_test.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/local/telconfig/classes/course_data_builder.php b/local/telconfig/classes/course_data_builder.php index 3a13a4dff45..f210d8588a3 100644 --- a/local/telconfig/classes/course_data_builder.php +++ b/local/telconfig/classes/course_data_builder.php @@ -40,7 +40,7 @@ public static function build_course_metadata($course): array { // Extract tags require_once($CFG->dirroot . '/tag/lib.php'); $tags = \core_tag_tag::get_item_tags('core', 'course', $course->id); - $keywords = array_map(fn($tag) => $tag->rawname, $tags); + $keywords = array_map(fn($tag) => strtolower($tag->rawname), $tags); // Prepare data $data = [ diff --git a/local/telconfig/tests/course_data_builder_test.php b/local/telconfig/tests/course_data_builder_test.php index 8d6810c2fe2..523166550fa 100644 --- a/local/telconfig/tests/course_data_builder_test.php +++ b/local/telconfig/tests/course_data_builder_test.php @@ -58,7 +58,7 @@ public function test_build_course_metadata_returns_expected_array() { $this->assertSame($course->id, $result['course_id']); $this->assertSame(date('Y-m-d', $course->startdate), $result['authored_date']); $this->assertContains(fullname($teacher), $result['authors']); - $this->assertEquals([$course->category], $result['catalogue_ids']); + $this->assertEquals([], $result['catalogue_ids']); $this->assertSame(format_text($course->summary, FORMAT_HTML), $result['description']); $this->assertEquals(['tag1', 'tag2'], $result['keywords']); $this->assertIsArray($result['location_paths']); From 199738d5e69b30153f5c655b82843479ffd567c7 Mon Sep 17 00:00:00 2001 From: Binon Date: Fri, 20 Jun 2025 11:01:11 +0100 Subject: [PATCH 37/41] Fixing the failure when catalogue id is null --- local/telconfig/classes/course_data_builder.php | 2 +- local/telconfig/tests/course_data_builder_test.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/local/telconfig/classes/course_data_builder.php b/local/telconfig/classes/course_data_builder.php index f210d8588a3..b7db93fd8fa 100644 --- a/local/telconfig/classes/course_data_builder.php +++ b/local/telconfig/classes/course_data_builder.php @@ -48,7 +48,7 @@ public static function build_course_metadata($course): array { 'course_id' => $course->id, 'authored_date' => date('Y-m-d', $course->startdate), 'authors' => $authors, - 'catalogue_ids' => [],//[$course->category], + 'catalogue_ids' => ['1'],//[$course->category], 'description' => format_text($course->summary, FORMAT_HTML), 'keywords' => array_values($keywords), 'location_paths' => [], // category hierarchy if needed diff --git a/local/telconfig/tests/course_data_builder_test.php b/local/telconfig/tests/course_data_builder_test.php index 523166550fa..820bb1e1ed5 100644 --- a/local/telconfig/tests/course_data_builder_test.php +++ b/local/telconfig/tests/course_data_builder_test.php @@ -58,7 +58,7 @@ public function test_build_course_metadata_returns_expected_array() { $this->assertSame($course->id, $result['course_id']); $this->assertSame(date('Y-m-d', $course->startdate), $result['authored_date']); $this->assertContains(fullname($teacher), $result['authors']); - $this->assertEquals([], $result['catalogue_ids']); + $this->assertEquals(['1'], $result['catalogue_ids']); $this->assertSame(format_text($course->summary, FORMAT_HTML), $result['description']); $this->assertEquals(['tag1', 'tag2'], $result['keywords']); $this->assertIsArray($result['location_paths']); From 3c1a874dd04308c4734e6a1f85388327ce1d1ffa Mon Sep 17 00:00:00 2001 From: Binon Date: Mon, 23 Jun 2025 13:23:32 +0100 Subject: [PATCH 38/41] Added validation on self entrol check --- local/telconfig/classes/observer.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/local/telconfig/classes/observer.php b/local/telconfig/classes/observer.php index 1eb7a13e75e..9d8c886e764 100644 --- a/local/telconfig/classes/observer.php +++ b/local/telconfig/classes/observer.php @@ -23,7 +23,7 @@ public static function enrol_instance_changed(\core\event\base $event): void { // Get enrol instance $enrol = $DB->get_record('enrol', ['id' => $event->objectid], '*', MUST_EXIST); - + // Only act if it's for 'self' enrolment. if (!isset($event->other['enrol']) || $event->other['enrol'] !== 'self') { return; @@ -59,12 +59,27 @@ public static function local_course_updated(\core\event\base $event): void { try { $course = $DB->get_record('course', ['id' => $event->objectid], '*', MUST_EXIST); + // Only proceed if the course has self enrolment enabled + if (!self::is_course_self_enrollable($course->id)) { + return; + } + // Rebuild and send metadata to API (as an update). $data = course_data_builder::build_course_metadata($course); helper::send_findwise_api($data); } catch (\dml_exception $e) { - debugging("Failed to process course update: " . $e->getMessage(), DEBUG_DEVELOPER); + debugging("Failed to process local course update: " . $e->getMessage(), DEBUG_DEVELOPER); } + } + + private static function is_course_self_enrollable(int $courseid): bool { + global $DB; + + return $DB->record_exists('enrol', [ + 'courseid' => $courseid, + 'enrol' => 'self', + 'status' => ENROL_INSTANCE_ENABLED, + ]); } } From d7c17924b4c47f9aa0594c56cbc6ec6c81d425df Mon Sep 17 00:00:00 2001 From: Colin Beeby Date: Mon, 23 Jun 2025 14:41:03 +0100 Subject: [PATCH 39/41] Unit tests are now run only when required, not on pull request --- .github/workflows/learninghub-moodle_Test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/learninghub-moodle_Test.yml b/.github/workflows/learninghub-moodle_Test.yml index 194348ce29b..b47a1457552 100644 --- a/.github/workflows/learninghub-moodle_Test.yml +++ b/.github/workflows/learninghub-moodle_Test.yml @@ -1,6 +1,6 @@ name: Run tests for LearningHub-Moodle on: - pull_request: + workflow_dispatch: permissions: id-token: write From f8d9406a77d47a72c57a69589823c52553821f05 Mon Sep 17 00:00:00 2001 From: Binon Date: Tue, 24 Jun 2025 16:20:56 +0100 Subject: [PATCH 40/41] Courses can be searched using section name and resource name --- .../telconfig/classes/course_data_builder.php | 68 ++++++++++++++++++- local/telconfig/classes/observer.php | 53 +++++++++++++++ local/telconfig/db/events.php | 12 ++++ 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/local/telconfig/classes/course_data_builder.php b/local/telconfig/classes/course_data_builder.php index b7db93fd8fa..a07b09d659f 100644 --- a/local/telconfig/classes/course_data_builder.php +++ b/local/telconfig/classes/course_data_builder.php @@ -40,7 +40,19 @@ public static function build_course_metadata($course): array { // Extract tags require_once($CFG->dirroot . '/tag/lib.php'); $tags = \core_tag_tag::get_item_tags('core', 'course', $course->id); - $keywords = array_map(fn($tag) => strtolower($tag->rawname), $tags); + $keywords = array_reduce($tags, function ($carry, $tag) { + return array_merge($carry, self::tokenize_keywords($tag->rawname)); + }, []); + + + // Merge in section and resource keywords + $keywords = array_merge( + $keywords, + self::get_section_keywords($course), + self::get_resource_keywords($course) + ); + + $keywords = array_values(array_unique($keywords)); // Prepare data $data = [ @@ -66,4 +78,58 @@ public static function build_course_metadata($course): array { return []; // Always return an array } } + + private static function get_section_keywords($course): array { + $keywords = []; + $modinfo = get_fast_modinfo($course); + $sections = $modinfo->get_section_info_all(); + + foreach ($sections as $section) { + if (!empty($section->name)) { + $keywords = array_merge($keywords, self::tokenize_keywords($section->name)); + } + } + + return $keywords; + } + + private static function get_resource_keywords($course): array { + global $DB; + $keywords = []; + $coursemodules = $DB->get_records('course_modules', ['course' => $course->id]); + + foreach ($coursemodules as $cm) { + // Skip if module is marked for deletion + if (!empty($cm->deletioninprogress)) { + continue; + } + + // Get module type (e.g., 'resource', 'quiz', etc.) + $module = $DB->get_record('modules', ['id' => $cm->module], '*', IGNORE_MISSING); + if (!$module) { + continue; + } + + // Dynamically get the module instance (e.g., from 'resource', 'quiz', etc.) + $instancetable = $module->name; + $instance = $DB->get_record($instancetable, ['id' => $cm->instance], '*', IGNORE_MISSING); + if ($instance && !empty($instance->name)) { + $keywords = array_merge($keywords, self::tokenize_keywords($instance->name)); + } + } + + return $keywords; + } + + private static function tokenize_keywords(string $input): array { + $input = strtolower(trim($input)); + if (empty($input)) { + return []; + } + + $tokens = preg_split('/\s+/', $input); // split on spaces + $keywords = array_merge([$input], $tokens); // include full phrase and individual words + + return array_unique($keywords); // remove duplicates + } } diff --git a/local/telconfig/classes/observer.php b/local/telconfig/classes/observer.php index 9d8c886e764..83ca54caf77 100644 --- a/local/telconfig/classes/observer.php +++ b/local/telconfig/classes/observer.php @@ -73,6 +73,59 @@ public static function local_course_updated(\core\event\base $event): void { } } + /** + * Triggered when a section is updated. + * + * @param \core\event\base $event + * @return void + */ + public static function local_section_updated(\core\event\base $event): void { + global $DB; + + try { + $section = $DB->get_record('course_sections', ['id' => $event->objectid], '*', MUST_EXIST); + $course = $DB->get_record('course', ['id' => $event->courseid], '*', MUST_EXIST); + + // Only proceed if the course has self enrolment enabled + if (!self::is_course_self_enrollable($course->id)) { + return; + } + + // Handle the update, e.g., send new metadata + $data = course_data_builder::build_course_metadata($course); // or enrich this with section title + helper::send_findwise_api($data); + + } catch (\Throwable $e) { + debugging('Failed in local section_updated: ' . $e->getMessage(), DEBUG_DEVELOPER); + } + } + + /** + * Triggered when a module is updated. + * + * @param \core\event\base $event + * @return void + */ + public static function local_module_updated(\core\event\base $event): void { + global $DB; + + try { + $cm = get_coursemodule_from_id(null, $event->objectid, 0, false, MUST_EXIST); + $course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST); + + // Only proceed if the course has self enrolment enabled + if (!self::is_course_self_enrollable($course->id)) { + return; + } + + // Build metadata + $data = course_data_builder::build_course_metadata($course); // or enrich with mod/section name + helper::send_findwise_api($data); + + } catch (\Throwable $e) { + debugging('Failed in local module_updated: ' . $e->getMessage(), DEBUG_DEVELOPER); + } + } private static function is_course_self_enrollable(int $courseid): bool { global $DB; diff --git a/local/telconfig/db/events.php b/local/telconfig/db/events.php index d6b56b72b4c..4e6d53fe206 100644 --- a/local/telconfig/db/events.php +++ b/local/telconfig/db/events.php @@ -13,4 +13,16 @@ 'priority' => 9999, 'internal' => false, ], + [ + 'eventname' => '\core\event\course_section_updated', + 'callback' => '\local_telconfig\observer::local_section_updated', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_module_updated', + 'callback' => '\local_telconfig\observer::local_module_updated', + 'priority' => 9999, + 'internal' => false, + ], ]; From 752a9b8fecfecb832a574860fe8801b0ff3f59db Mon Sep 17 00:00:00 2001 From: Binon Date: Wed, 25 Jun 2025 16:14:13 +0100 Subject: [PATCH 41/41] removing/hiding activity and resources are now triggered --- .../telconfig/classes/course_data_builder.php | 23 +++++++++------ local/telconfig/classes/observer.php | 17 ++++++----- local/telconfig/db/events.php | 28 +++++++++++++++++-- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/local/telconfig/classes/course_data_builder.php b/local/telconfig/classes/course_data_builder.php index a07b09d659f..f37e27a4bb4 100644 --- a/local/telconfig/classes/course_data_builder.php +++ b/local/telconfig/classes/course_data_builder.php @@ -41,7 +41,7 @@ public static function build_course_metadata($course): array { require_once($CFG->dirroot . '/tag/lib.php'); $tags = \core_tag_tag::get_item_tags('core', 'course', $course->id); $keywords = array_reduce($tags, function ($carry, $tag) { - return array_merge($carry, self::tokenize_keywords($tag->rawname)); + return array_merge($carry, self::tokenise_keywords($tag->rawname)); }, []); @@ -85,9 +85,13 @@ private static function get_section_keywords($course): array { $sections = $modinfo->get_section_info_all(); foreach ($sections as $section) { - if (!empty($section->name)) { - $keywords = array_merge($keywords, self::tokenize_keywords($section->name)); + // Skip hidden sections + if (!$section->uservisible || !$section->visible || empty($section->name)) { + continue; } + + // Tokenise and merge keywords + $keywords = array_merge($keywords, self::tokenise_keywords($section->name)); } return $keywords; @@ -95,12 +99,14 @@ private static function get_section_keywords($course): array { private static function get_resource_keywords($course): array { global $DB; - $keywords = []; + $keywords = []; + + // Now fetch all course modules from DB $coursemodules = $DB->get_records('course_modules', ['course' => $course->id]); foreach ($coursemodules as $cm) { - // Skip if module is marked for deletion - if (!empty($cm->deletioninprogress)) { + // Skip deleted or hidden modules + if (!empty($cm->deletioninprogress) || empty($cm->visible)) { continue; } @@ -114,14 +120,15 @@ private static function get_resource_keywords($course): array { $instancetable = $module->name; $instance = $DB->get_record($instancetable, ['id' => $cm->instance], '*', IGNORE_MISSING); if ($instance && !empty($instance->name)) { - $keywords = array_merge($keywords, self::tokenize_keywords($instance->name)); + $keywords = array_merge($keywords, self::tokenise_keywords($instance->name)); } } return $keywords; } - private static function tokenize_keywords(string $input): array { + + private static function tokenise_keywords(string $input): array { $input = strtolower(trim($input)); if (empty($input)) { return []; diff --git a/local/telconfig/classes/observer.php b/local/telconfig/classes/observer.php index 83ca54caf77..98f13d006ba 100644 --- a/local/telconfig/classes/observer.php +++ b/local/telconfig/classes/observer.php @@ -79,13 +79,12 @@ public static function local_course_updated(\core\event\base $event): void { * @param \core\event\base $event * @return void */ - public static function local_section_updated(\core\event\base $event): void { + public static function local_section_changed(\core\event\base $event): void { global $DB; - try { - $section = $DB->get_record('course_sections', ['id' => $event->objectid], '*', MUST_EXIST); + try { $course = $DB->get_record('course', ['id' => $event->courseid], '*', MUST_EXIST); - + // Only proceed if the course has self enrolment enabled if (!self::is_course_self_enrollable($course->id)) { return; @@ -96,7 +95,7 @@ public static function local_section_updated(\core\event\base $event): void { helper::send_findwise_api($data); } catch (\Throwable $e) { - debugging('Failed in local section_updated: ' . $e->getMessage(), DEBUG_DEVELOPER); + debugging('Error handling local section change: ' . $e->getMessage(), DEBUG_DEVELOPER); } } @@ -106,12 +105,12 @@ public static function local_section_updated(\core\event\base $event): void { * @param \core\event\base $event * @return void */ - public static function local_module_updated(\core\event\base $event): void { + public static function local_module_changed(\core\event\base $event): void { global $DB; try { - $cm = get_coursemodule_from_id(null, $event->objectid, 0, false, MUST_EXIST); - $course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST); + $courseid = $event->courseid; + $course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST); // Only proceed if the course has self enrolment enabled if (!self::is_course_self_enrollable($course->id)) { @@ -123,7 +122,7 @@ public static function local_module_updated(\core\event\base $event): void { helper::send_findwise_api($data); } catch (\Throwable $e) { - debugging('Failed in local module_updated: ' . $e->getMessage(), DEBUG_DEVELOPER); + debugging('Error handling local module change: ' . $e->getMessage(), DEBUG_DEVELOPER); } } private static function is_course_self_enrollable(int $courseid): bool { diff --git a/local/telconfig/db/events.php b/local/telconfig/db/events.php index 4e6d53fe206..29da2fc800d 100644 --- a/local/telconfig/db/events.php +++ b/local/telconfig/db/events.php @@ -13,15 +13,39 @@ 'priority' => 9999, 'internal' => false, ], + [ + 'eventname' => '\core\event\course_section_created', + 'callback' => '\local_telconfig\observer::local_section_changed', + 'priority' => 9999, + 'internal' => false, + ], [ 'eventname' => '\core\event\course_section_updated', - 'callback' => '\local_telconfig\observer::local_section_updated', + 'callback' => '\local_telconfig\observer::local_section_changed', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_section_deleted', + 'callback' => '\local_telconfig\observer::local_section_changed', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_module_created', + 'callback' => '\local_telconfig\observer::local_module_changed', 'priority' => 9999, 'internal' => false, ], [ 'eventname' => '\core\event\course_module_updated', - 'callback' => '\local_telconfig\observer::local_module_updated', + 'callback' => '\local_telconfig\observer::local_module_changed', + 'priority' => 9999, + 'internal' => false, + ], + [ + 'eventname' => '\core\event\course_module_deleted', + 'callback' => '\local_telconfig\observer::local_module_changed', 'priority' => 9999, 'internal' => false, ],