diff --git a/amd/src/syncscale.js b/amd/src/syncscale.js new file mode 100644 index 000000000..fdb196ea9 --- /dev/null +++ b/amd/src/syncscale.js @@ -0,0 +1,96 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/* + * @package local_catquiz + * @copyright Wunderbyte GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {get_string as getString} from 'core/str'; +import Ajax from 'core/ajax'; +import Notification from 'core/notification'; + +const SELECTORS = { + FORMCONTAINER: '#lcq_select_context_form', + CALCULATEBUTTON: '#sync_button' +}; +/** + * Add event listener to buttons. + */ +export const init = () => { + + const syncButton = document.querySelector(SELECTORS.SYNCBUTTON); + // Hardcoded values for testing. + const centralurl = 'https://192.168.56.6'; + const wstoken = '2f36a8dc27525b97a93e50186035d49e'; + const scalelabel = 'simulation'; + + const contextId = parseInt(calculateButton.dataset.contextid); + calculateButton.onclick = () => { + updateParameters(contextId); + }; + + const updateParameters = async(contextid) => { + const urlParams = new URLSearchParams(window.location.search); + const catscaleid = urlParams.get('scaleid'); + + // Fallback if the translation can not be loaded + let errorMessage = 'Something went wrong'; + try { + errorMessage = await getString('somethingwentwrong', 'local_catquiz'); + } catch (error) { + // We already have a fallback message, nothing to do here. + } + Ajax.call([{ + methodname: 'local_catquiz_update_parameters', + args: {contextid, catscaleid}, + done: async function(res) { + if (res.success) { + disableButton(); + // Fallback if the translation can not be loaded + let successMessage = 'Recalculation was scheduled'; + try { + successMessage = await getString('recalculationscheduled', 'local_catquiz'); + } catch (error) { + // We already have a fallback message, nothing to do here. + } + + Notification.addNotification({ + message: successMessage, + type: 'success' + }); + } else { + disableButton(); + Notification.addNotification({ + message: errorMessage, + type: 'danger' + }); + } + }, + fail: () => { + disableButton(); + Notification.addNotification({ + message: errorMessage, + type: 'danger' + }); + }, + }]); + }; + + const disableButton = () => { + document.querySelector(SELECTORS.CALCULATEBUTTON).setAttribute('disabled', true); + }; +}; \ No newline at end of file diff --git a/classes/external/client_fetch_parameters.php b/classes/external/client_fetch_parameters.php new file mode 100644 index 000000000..8d0a780ff --- /dev/null +++ b/classes/external/client_fetch_parameters.php @@ -0,0 +1,205 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->libdir . '/filelib.php'); + +namespace local_catquiz\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; +use core_external\external_multiple_structure; +use local_catquiz\catscale; + +/** + * This class contains a list of webservice functions related to the catquiz Module by Wunderbyte. + * + * @package local_catquiz + * @copyright 2024 Wunderbyte GmbH + * @author David Szkiba + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class fetch_parameters extends external_api { + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters([ + 'centralurl' => new external_value(PARAM_URL, 'URL of central instance'), + 'token' => new external_value(PARAM_TEXT, 'Web service token'), + 'scalelabel' => new external_value(PARAM_TEXT, 'Label of the scale to sync'), + ]); + } + + /** + * Returns description of method return values + * @return external_single_structure + */ + public static function execute_returns() { + return new external_single_structure([ + 'status' => new external_value(PARAM_BOOL, 'Status of the sync'), + 'duration' => new external_value(PARAM_FLOAT, 'Duration in seconds'), + 'synced' => new external_value(PARAM_INT, 'Number of parameters synced'), + 'errors' => new external_value(PARAM_INT, 'Number of errors encountered'), + 'message' => new external_value(PARAM_TEXT, 'Status message'), + 'warnings' => new external_multiple_structure( + new external_single_structure([ + 'item' => new external_value(PARAM_TEXT, 'Item identifier'), + 'warning' => new external_value(PARAM_TEXT, 'Warning message'), + ]) + ), + ]); + } + + /** + * Fetches and synchronizes item parameters from a central instance + * + * @param string $centralurl URL of the central Moodle instance + * @param string $token Web service token for authentication + * @param string $scalelabel Label of the scale to synchronize + * @return array Returns status information about the sync operation including: + * - status (bool): Whether any parameters were successfully synced + * - duration (float): Time taken for the operation in seconds + * - synced (int): Number of parameters successfully synchronized + * - errors (int): Number of errors encountered + * - message (string): Status message describing the operation + * - warnings (array): List of specific warnings/errors encountered + * @throws \invalid_parameter_exception If parameters are invalid + */ + public static function execute(string $centralurl, string $token, string $scalelabel) { + global $DB; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'centralurl' => $centralurl, + 'token' => $token, + 'scalelabel' => $scalelabel, + ]); + + $starttime = microtime(true); + $warnings = []; + $stored = 0; + $errors = 0; + + // Get the local scale. + $scale = $DB->get_record('local_catquiz_catscales', ['label' => $params['scalelabel']], '*', MUST_EXIST); + + // Call central instance. + $serverurl = rtrim($params['centralurl'], '/') . '/webservice/rest/server.php'; + $wsparams = [ + 'wstoken' => $params['token'], + 'wsfunction' => 'local_catquiz_fetch_item_parameters', + 'moodlewsrestformat' => 'json', + 'scalelabel' => $params['scalelabel'], + ]; + $CFG->curlsecurityblockedhosts = ''; // TODO: Remove. + $curl = new \curl(); + $curl->setopt(['CURLOPT_SSL_VERIFYPEER' => false, 'CURLOPT_SSL_VERIFYHOST' => false]); + $response = $curl->post($serverurl, $wsparams); + unset($CFG->curlsecurityblockedhosts); + $result = json_decode($response); + + if (!$result || isset($result->exception)) { + return [ + 'status' => false, + 'duration' => 0, + 'synced' => 0, + 'errors' => 1, + 'message' => $result->message ?? 'Invalid response from server', + 'warnings' => [], + ]; + } + + // Create a mapping of catscale labels to IDs. + $scalemapping = []; + $catscaleids = [$scale->id, ...catscale::get_subscale_ids($scale->id)]; + [$inscalesql, $inscaleparams] = $DB->get_in_or_equal($catscaleids, SQL_PARAMS_NAMED, 'scaleid'); + + $sql = <<get_records_sql($sql, $inscaleparams); + foreach ($scalerecords as $s) { + $scalemapping[$s->label] = $s->id; + } + + // Create new context. + $source = "Fetch from " . parse_url($params['centralurl'], PHP_URL_HOST); + $newcontext = \local_catquiz\data\dataapi::create_new_context_for_scale( + $scale->id, + $scale->name, + $source, + false + ); + + // Process and store parameters. + foreach ($result->parameters as $param) { + $questionid = \local_catquiz\remote\hash\question_hasher::get_questionid_from_hash($param->questionhash); + if (!$questionid) { + $warnings[] = ['item' => $param->questionhash, 'warning' => 'Question not found locally']; + $errors++; + continue; + } + + try { + $itemrecord = new \stdClass(); + $itemrecord->componentid = $questionid; + $itemrecord->componentname = 'question'; + $itemrecord->catscaleid = $scale->id; + $itemrecord->contextid = $newcontext->id; + $itemrecord->status = LOCAL_CATQUIZ_TESTITEM_STATUS_ACTIVE; + $itemid = $DB->insert_record('local_catquiz_items', $itemrecord); + + $paramrecord = new \stdClass(); + $paramrecord->itemid = $itemid; + $paramrecord->componentid = $questionid; + $paramrecord->componentname = 'question'; + $paramrecord->contextid = $newcontext->id; + $paramrecord->model = $param->model; + $paramrecord->difficulty = $param->difficulty; + $paramrecord->discrimination = $param->discrimination; + $paramrecord->status = $param->status; + $paramrecord->json = $param->json; + + $itemparam = \local_catquiz\local\model\model_item_param::from_record($paramrecord); + $itemparam->save(); + $stored++; + } catch (\Exception $e) { + debugging('Error storing parameter: ' . $e->getMessage(), DEBUG_DEVELOPER); + $warnings[] = ['item' => $param->questionhash, 'warning' => $e->getMessage()]; + $errors++; + } + } + + $duration = microtime(true) - $starttime; + + return [ + 'status' => $stored > 0, + 'duration' => round($duration, 2), + 'synced' => $stored, + 'errors' => $errors, + 'message' => $stored > 0 ? "Successfully synced $stored parameters" : 'No parameters were synced', + 'warnings' => $warnings, + ]; + } +} diff --git a/classes/external/fetch_item_parameters.php b/classes/external/fetch_item_parameters.php index 17d5ac975..1e1c55944 100644 --- a/classes/external/fetch_item_parameters.php +++ b/classes/external/fetch_item_parameters.php @@ -129,11 +129,7 @@ public static function execute($scalelabel, $questionhashes = [], $models = []) // Get all relevant scale IDs (parent scale and subscales) $catscaleids = [$scale->id, ...catscale::get_subscale_ids($scale->id)]; // Get the latest context for this scale. - $contextid = catscale::get_context_id($scale->id); - if (!$contextid) { - throw new moodle_exception('nocontextfound', 'local_catquiz'); - } - + $contextid = $scale->contextid; // Get all questions assigned to this scale. [$inscalesql, $inscaleparams] = $DB->get_in_or_equal($catscaleids, SQL_PARAMS_NAMED, 'scaleid'); diff --git a/db/upgrade.php b/db/upgrade.php index 7e6f9bf74..92761d6c4 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -989,7 +989,7 @@ function xmldb_local_catquiz_upgrade($oldversion) { // This is a bit unconventional. The table already exists with old, long // names on a moodle instance that supports longer table names but can't be // created on a different instance that has stricter naming rules. - if ($oldversion < 2024110802) { + if ($oldversion < 2024111804) { // Check if old table exists first. if ($dbman->table_exists('local_catquiz_question_hashmap')) { // Rename the table. @@ -1054,7 +1054,7 @@ function xmldb_local_catquiz_upgrade($oldversion) { } } - upgrade_plugin_savepoint(true, 2024110802, 'local', 'catquiz'); + upgrade_plugin_savepoint(true, 2024111804, 'local', 'catquiz'); } diff --git a/templates/catscalemanager/catscales.mustache b/templates/catscalemanager/catscales.mustache index b13b1b690..d21f89a2c 100644 --- a/templates/catscalemanager/catscales.mustache +++ b/templates/catscalemanager/catscales.mustache @@ -41,6 +41,9 @@ require(['local_catquiz/calculatescales'], function(init) { init.init(); }); + require(['local_catquiz/syncscale'], function(init) { + init.init(); + }); }); {{/js}} {{/scaledetailview}} diff --git a/version.php b/version.php index 3f5c70d36..c9193fdcb 100644 --- a/version.php +++ b/version.php @@ -26,7 +26,7 @@ $plugin->component = 'local_catquiz'; $plugin->release = '1.1.1'; -$plugin->version = 2024111802; +$plugin->version = 2024111804; $plugin->requires = 2022041900; $plugin->maturity = MATURITY_STABLE; $plugin->dependencies = [