From a3baa9aa0ac6b0ebb72f5b391b860451ba0621fc Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Thu, 17 Oct 2024 17:06:34 +0100 Subject: [PATCH] quiz-data - Import CLI Need to fix import of booleans --- classes/export_quiz.php | 36 +++- classes/external/import_quiz_data.php | 2 +- classes/import_quiz.php | 260 ++++++++++++++++++++++++++ cli/exportquizstructurefrommoodle.php | 8 +- cli/importquizstructuretomoodle.php | 131 +++++++++++++ 5 files changed, 423 insertions(+), 14 deletions(-) create mode 100644 classes/import_quiz.php create mode 100644 cli/importquizstructuretomoodle.php diff --git a/classes/export_quiz.php b/classes/export_quiz.php index 0786f1f..c815320 100644 --- a/classes/export_quiz.php +++ b/classes/export_quiz.php @@ -17,7 +17,7 @@ /** * Wrapper class for processing performed by command line interface for exporting quiz data from Moodle. * - * Utilised in cli\exportquizdatafrommoodle.php + * Utilised in cli\exportquizstructurefrommoodle.php * * Allows mocking and unit testing via PHPUnit. * Used outside Moodle. @@ -29,7 +29,7 @@ namespace qbank_gitsync; /** - * Export a Git repo. + * Export structure data of a Moodle quiz. */ class export_quiz { /** @@ -49,9 +49,9 @@ class export_quiz { /** * Full path to manifest file * - * @var string + * @var string|null */ - public string $quizmanifestpath; + public ?string $quizmanifestpath; /** * Parsed content of JSON manifest file * @@ -64,6 +64,23 @@ class export_quiz { * @var string */ public string $moodleurl; + /** + * Full path to manifest file + * + * @var string|null + */ + public ?string $nonquizmanifestpath; + /** + * Parsed content of JSON manifest file + * + * @var \stdClass|null + */ + public ?\stdClass $nonquizmanifestcontents; + /** + * URL of Moodle instance + * + * @var string + */ /** * Full path to output file * @@ -82,7 +99,10 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { $arguments = $clihelper->get_arguments(); $moodleinstance = $arguments['moodleinstance']; // TODO Use additional manifest file as well. - $this->quizmanifestpath = $arguments['rootdirectory'] . '/' . $arguments['quizmanifestpath']; + $this->quizmanifestpath = ($arguments['quizmanifestpath']) ? + $arguments['rootdirectory'] . '/' . $arguments['quizmanifestpath'] : null; + $this->nonquizmanifestpath = ($arguments['nonquizmanifestpath']) ? + $arguments['rootdirectory'] . '/' . $arguments['nonquizmanifestpath'] : null; if (is_array($arguments['token'])) { $token = $arguments['token'][$moodleinstance]; } else { @@ -94,9 +114,6 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { $this->call_exit(); } - $this->tempfilepath = str_replace(cli_helper::MANIFEST_FILE, - '_export' . cli_helper::TEMP_MANIFEST_FILE, - $this->quizmanifestpath); $this->moodleurl = $moodleinstances[$moodleinstance]; $wsurl = $this->moodleurl . '/webservice/rest/server.php'; @@ -134,7 +151,8 @@ public function get_curl_request($wsurl):curl_request { } /** - * Loop through questions in manifest file, export each from Moodle and update local copy + * Get quiz data from webservice, convert question ids to file locations + * and then write to file. * * @return void */ diff --git a/classes/external/import_quiz_data.php b/classes/external/import_quiz_data.php index 71061e3..42df2e1 100644 --- a/classes/external/import_quiz_data.php +++ b/classes/external/import_quiz_data.php @@ -57,7 +57,7 @@ public static function execute_parameters() { 'coursename' => new external_value(PARAM_TEXT, 'course to import quiz into'), 'courseid' => new external_value(PARAM_SEQUENCE, 'course to import quiz into'), 'questionsperpage' => new external_value(PARAM_SEQUENCE, 'default questions per page'), - 'grade' => new external_value(PARAM_SEQUENCE, 'maximum grade'), + 'grade' => new external_value(PARAM_TEXT, 'maximum grade'), 'navmethod' => new external_value(PARAM_TEXT, 'navigation method'), ]), 'sections' => new external_multiple_structure( diff --git a/classes/import_quiz.php b/classes/import_quiz.php new file mode 100644 index 0000000..70cccd5 --- /dev/null +++ b/classes/import_quiz.php @@ -0,0 +1,260 @@ +. + +/** + * Wrapper class for processing performed by command line interface for importing quiz data to Moodle. + * + * Utilised in cli\importquizstructuretomoodle.php + * + * Allows mocking and unit testing via PHPUnit. + * Used outside Moodle. + * + * @package qbank_gitsync + * @copyright 2024 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace qbank_gitsync; + +/** + * Import structure data of a Moodle quiz. + */ +class import_quiz { + /** + * CLI helper for this import + * + * @var cli_helper + */ + public cli_helper $clihelper; + /** + * Settings for POST request + * + * These are the parameters for the webservice call. + * + * @var array + */ + public array $postsettings; + /** + * cURL request handle for file upload + * + * @var curl_request + */ + public curl_request $curlrequest; + /** + * cURL request handle for question list retrieve + * + * @var curl_request + */ + public curl_request $listcurlrequest; + /** + * Settings for question list request + * + * These are the parameters for the webservice list call. + * + * @var array + */ + public array $listpostsettings; + /** + * Full path to manifest file + * + * @var string|null + */ + public ?string $quizmanifestpath; + /** + * Parsed content of JSON manifest file + * + * @var \stdClass|null + */ + public ?\stdClass $quizmanifestcontents; + /** + * Full path to manifest file + * + * @var string|null + */ + public ?string $nonquizmanifestpath; + /** + * Parsed content of JSON manifest file + * + * @var \stdClass|null + */ + public ?\stdClass $nonquizmanifestcontents; + /** + * URL of Moodle instance + * + * @var string + */ + public string $moodleurl; + /** + * Full path to data file + * + * @var string + */ + public string $quizdatapath; + /** + * Parsed content of JSON data file + * + * @var \stdClass|null + */ + public ?\stdClass $quizdatacontents; + + /** + * Constructor + * + * @param cli_helper $clihelper + * @param array $moodleinstances pairs of names and URLs + */ + public function __construct(cli_helper $clihelper, array $moodleinstances) { + // Convert command line options into variables. + $this->clihelper = $clihelper; + $arguments = $clihelper->get_arguments(); + $moodleinstance = $arguments['moodleinstance']; + // TODO Use additional manifest file as well. + $this->quizmanifestpath = ($arguments['quizmanifestpath']) ? + $arguments['rootdirectory'] . '/' . $arguments['quizmanifestpath'] : null; + $this->nonquizmanifestpath = ($arguments['nonquizmanifestpath']) ? + $arguments['rootdirectory'] . '/' . $arguments['nonquizmanifestpath'] : null; + $this->quizdatapath = $arguments['rootdirectory'] . '/' . $arguments['quizdatapath']; + if (is_array($arguments['token'])) { + $token = $arguments['token'][$moodleinstance]; + } else { + $token = $arguments['token']; + } + $this->quizmanifestcontents = json_decode(file_get_contents($this->quizmanifestpath)); + if (!$this->quizmanifestcontents) { + echo "\nUnable to access or parse manifest file: {$this->quizmanifestpath}\nAborting.\n"; + $this->call_exit(); + } + $this->quizdatacontents = json_decode(file_get_contents($this->quizdatapath)); + if (!$this->quizdatacontents) { + echo "\nUnable to access or parse data file: {$this->quizdatapath}\nAborting.\n"; + $this->call_exit(); + } + + $this->moodleurl = $moodleinstances[$moodleinstance]; + $wsurl = $this->moodleurl . '/webservice/rest/server.php'; + + $this->listcurlrequest = $this->get_curl_request($wsurl); + $this->listpostsettings = [ + 'wstoken' => $token, + 'wsfunction' => 'qbank_gitsync_get_question_list', + 'moodlewsrestformat' => 'json', + 'contextlevel' => 50, + 'coursename' => $arguments['coursename'], + 'modulename' => null, + 'coursecategory' => null, + 'qcategoryname' => 'top', + 'qcategoryid' => null, + 'instanceid' => $arguments['instanceid'], + 'contextonly' => 1, + 'qbankentryids[]' => null, + 'ignorecat' => null, + ]; + $this->listcurlrequest->set_option(CURLOPT_RETURNTRANSFER, true); + $this->listcurlrequest->set_option(CURLOPT_POST, 1); + + $this->curlrequest = $this->get_curl_request($wsurl); + $this->postsettings = [ + 'wstoken' => $token, + 'wsfunction' => 'qbank_gitsync_import_quiz_data', + 'moodlewsrestformat' => 'json', + ]; + $this->curlrequest->set_option(CURLOPT_RETURNTRANSFER, true); + $this->curlrequest->set_option(CURLOPT_POST, 1); + } + + /** + * Get quiz data from file, convert question file locations to ids + * and then import to Moodle. + * + * @return void + */ + public function process():void { + $this->import_quiz_data(); + } + + /** + * Wrapper for cURL request to allow mocking. + * + * @param string $wsurl webservice URL + * @return curl_request + */ + public function get_curl_request($wsurl):curl_request { + return new \qbank_gitsync\curl_request($wsurl); + } + + /** + * Get quiz data from file, convert question file locations to ids + * and then import to Moodle. + * + * @return void + */ + public function import_quiz_data() { + $instanceinfo = $this->clihelper->check_context($this, false, true); + // TODO - Message to user. + $manifestentries = array_column($this->quizmanifestcontents->questions, null, 'filepath'); + foreach ($this->quizdatacontents->quiz as $key => $quizparam) { + $this->postsettings["quiz[{$key}]"] = $quizparam; + } + $this->postsettings['quiz[coursename]'] = $instanceinfo->contextinfo->coursename; + $this->postsettings['quiz[courseid]'] = $instanceinfo->contextinfo->instanceid; + foreach ($this->quizdatacontents->sections as $sectionkey => $section) { + foreach ($section as $key => $sectionparam) { + $this->postsettings["sections[{$sectionkey}][{$key}]"] = $sectionparam; + } + } + foreach ($this->quizdatacontents->questions as $questionkey => $question) { + foreach ($question as $key => $questionparam) { + $this->postsettings["questions[{$questionkey}][{$key}]"] = $questionparam; + } + $manifestentry = $manifestentries["{$question->quizfilepath}"] ?? false; + if ($manifestentry) { + $this->postsettings["questions[{$questionkey}][questionbankentryid]"] = $manifestentry->questionbankentryid; + unset($this->postsettings["questions[{$questionkey}][quizfilepath]"]); + } else { + // TODO - what happens here? + } + } + foreach ($this->quizdatacontents->feedback as $feedbackkey => $feedback) { + foreach ($feedback as $key => $feedbackparam) { + $this->postsettings["feedback[{$feedbackkey}][{$key}]"] = $feedbackparam; + } + } + $this->curlrequest->set_option(CURLOPT_POSTFIELDS,$this->postsettings); + $response = $this->curlrequest->execute(); + $responsejson = json_decode($response); + if (!$responsejson) { + echo "Broken JSON returned from Moodle:\n"; + echo $response . "\n"; + $this->call_exit(); + } else if (property_exists($responsejson, 'exception')) { + echo "{$responsejson->message}\n"; + if (property_exists($responsejson, 'debuginfo')) { + echo "{$responsejson->debuginfo}\n"; + } + $this->call_exit(); + } + } + + /** + * Mockable function that just exits code. + * + * Required to stop PHPUnit displaying output after exit. + * + * @return void + */ + public function call_exit():void { + exit; + } +} diff --git a/cli/exportquizstructurefrommoodle.php b/cli/exportquizstructurefrommoodle.php index 487e0ff..58fc797 100644 --- a/cli/exportquizstructurefrommoodle.php +++ b/cli/exportquizstructurefrommoodle.php @@ -51,7 +51,7 @@ 'longopt' => 'nonquizmanifestpath', 'shortopt' => 'p', 'description' => 'Filepath of non-quiz manifest file relative to root directory.', - 'default' => $manifestpath, + 'default' => null, 'variable' => 'nonquizmanifestpath', 'valuerequired' => true, ], @@ -111,9 +111,9 @@ } $clihelper = new cli_helper($options); $exportquiz = new export_quiz($clihelper, $moodleinstances); -if ($exportquiz->coursemanifestpath) { - echo 'Checking course repo...'; - $clihelper->check_for_changes($exportquiz->coursemanifestpath); +if ($exportquiz->nonquizmanifestpath) { + echo 'Checking repo...'; + $clihelper->check_for_changes($exportquiz->nonquizmanifestpath); } if ($exportquiz->quizmanifestpath) { echo 'Checking quiz repo...'; diff --git a/cli/importquizstructuretomoodle.php b/cli/importquizstructuretomoodle.php new file mode 100644 index 0000000..1322503 --- /dev/null +++ b/cli/importquizstructuretomoodle.php @@ -0,0 +1,131 @@ +. + +/** + * Import structure (not questions) of a quiz to Moodle. + * + * @package qbank_gitsync + * @copyright 2024 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbank_gitsync; +define('CLI_SCRIPT', true); +require_once('./config.php'); +require_once('../classes/curl_request.php'); +require_once('../classes/cli_helper.php'); +require_once('../classes/import_quiz.php'); + +$options = [ + [ + 'longopt' => 'moodleinstance', + 'shortopt' => 'i', + 'description' => 'Key of Moodle instance in $moodleinstances to use. ' . + 'Should match end of instance URL.', + 'default' => $instance, + 'variable' => 'moodleinstance', + 'valuerequired' => true, + ], + [ + 'longopt' => 'rootdirectory', + 'shortopt' => 'r', + 'description' => "Directory on user's computer containing repos.", + 'default' => $rootdirectory, + 'variable' => 'rootdirectory', + 'valuerequired' => true, + ], + [ + 'longopt' => 'nonquizmanifestpath', + 'shortopt' => 'p', + 'description' => 'Filepath of non-quiz manifest file relative to root directory.', + 'default' => null, + 'variable' => 'nonquizmanifestpath', + 'valuerequired' => true, + ], + [ + 'longopt' => 'quizmanifestpath', + 'shortopt' => 'f', + 'description' => 'Filepath of quiz manifest file relative to root directory.', + 'default' => null, + 'variable' => 'quizmanifestpath', + 'valuerequired' => true, + ], + [ + 'longopt' => 'quizdatapath', + 'shortopt' => 'a', + 'description' => 'Filepath of quiz data file relative to root directory.', + 'default' => null, + 'variable' => 'quizdatapath', + 'valuerequired' => true, + ], + [ + 'longopt' => 'coursename', + 'shortopt' => 'c', + 'description' => 'Unique course name of course.', + 'default' => null, + 'variable' => 'coursename', + 'valuerequired' => true, + ], + [ + 'longopt' => 'instanceid', + 'shortopt' => 'n', + 'description' => 'Numerical id of the course.', + 'default' => null, + 'variable' => 'instanceid', + 'valuerequired' => true, + ], + [ + 'longopt' => 'token', + 'shortopt' => 't', + 'description' => 'Security token for webservice.', + 'default' => $token, + 'variable' => 'token', + 'valuerequired' => true, + ], + [ + 'longopt' => 'help', + 'shortopt' => 'h', + 'description' => '', + 'default' => false, + 'variable' => 'help', + 'valuerequired' => false, + ], + [ + 'longopt' => 'usegit', + 'shortopt' => 'u', + 'description' => 'Is the repo controlled using Git?', + 'default' => $usegit, + 'variable' => 'usegit', + 'valuerequired' => false, + ] +]; + +if (!function_exists('simplexml_load_file')) { + echo 'Please install the PHP library SimpleXML.' . "\n"; + exit; +} + +$clihelper = new cli_helper($options); +$importquiz = new import_quiz($clihelper, $moodleinstances); +if ($importquiz->nonquizmanifestpath) { + echo 'Checking repo...'; + $clihelper->check_for_changes($importquiz->nonquizmanifestpath); +} +if ($importquiz->quizmanifestpath) { + echo 'Checking quiz repo...'; + $clihelper->check_for_changes($importquiz->quizmanifestpath); +} +$importquiz->process();