From 2ebe5cd1070767be636084c94dba86456fd81adb Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Thu, 17 Oct 2024 13:36:33 +0100 Subject: [PATCH] quiz-data - Basic export CLI --- classes/cli_helper.php | 21 ++- classes/export_quiz.php | 181 ++++++++++++++++++++++++++ cli/exportquizstructurefrommoodle.php | 122 +++++++++++++++++ 3 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 classes/export_quiz.php create mode 100644 cli/exportquizstructurefrommoodle.php diff --git a/classes/cli_helper.php b/classes/cli_helper.php index ff42a9d..2b6c08f 100644 --- a/classes/cli_helper.php +++ b/classes/cli_helper.php @@ -58,6 +58,10 @@ class cli_helper { * Appended to name of moodle instance. */ public const MANIFEST_FILE = '_question_manifest.json'; + /** + * QUIZ_FILE - File name ending for quiz structure file. + */ + public const QUIZ_FILE = '_quiz.json'; /** * TEMP_MANIFEST_FILE - File name ending for temporary manifest file. * Appended to name of moodle instance. @@ -270,7 +274,7 @@ public function validate_and_clean_args(): void { break; } } - if (!isset($cliargs['manifestpath']) && !isset($cliargs['contextlevel'])) { + if (!(isset($cliargs['manifestpath']) || isset($cliargs['quizmanifestpath']) || isset($cliargs['nonquizmanifestpath'])) && !isset($cliargs['contextlevel'])) { echo "\nYou have not specified context. " . "You must specify context level (--contextlevel) unless " . "using a function where this information can be read from a manifest file, in which case " . @@ -399,6 +403,21 @@ public static function get_manifest_path(string $moodleinstance, string $context return $filename; } + /** + * Create quiz structure path. + * + * @param string|null $modulename + * @param string $directory + * @return string + */ + public static function get_quiz_structure_path(string $modulename, string $directory):string { + $filename = substr($modulename, 0, 100); + $filename = $directory . '/' . + preg_replace(self::BAD_CHARACTERS, '-', strtolower($filename)) . + self::QUIZ_FILE; + return $filename; + } + /** * Create manifest file from temporary file. * diff --git a/classes/export_quiz.php b/classes/export_quiz.php new file mode 100644 index 0000000..0786f1f --- /dev/null +++ b/classes/export_quiz.php @@ -0,0 +1,181 @@ +. + +/** + * Wrapper class for processing performed by command line interface for exporting quiz data from Moodle. + * + * Utilised in cli\exportquizdatafrommoodle.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; + +/** + * Export a Git repo. + */ +class export_quiz { + /** + * 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; + /** + * Full path to manifest file + * + * @var string + */ + public string $quizmanifestpath; + /** + * Parsed content of JSON manifest file + * + * @var \stdClass|null + */ + public ?\stdClass $quizmanifestcontents; + /** + * URL of Moodle instance + * + * @var string + */ + public string $moodleurl; + /** + * Full path to output file + * + * @var string + */ + public string $filepath; + + /** + * 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. + $arguments = $clihelper->get_arguments(); + $moodleinstance = $arguments['moodleinstance']; + // TODO Use additional manifest file as well. + $this->quizmanifestpath = $arguments['rootdirectory'] . '/' . $arguments['quizmanifestpath']; + 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->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'; + + $this->curlrequest = $this->get_curl_request($wsurl); + $this->postsettings = [ + 'wstoken' => $token, + 'wsfunction' => 'qbank_gitsync_export_quiz_data', + 'moodlewsrestformat' => 'json', + 'quizname' => $arguments['moodlename'], + 'moduleid' => $arguments['instanceid'], + ]; + $this->curlrequest->set_option(CURLOPT_RETURNTRANSFER, true); + $this->curlrequest->set_option(CURLOPT_POST, 1); + $this->curlrequest->set_option(CURLOPT_POSTFIELDS, $this->postsettings); + } + + /** + * Get quiz data from webservice, convert question ids to file locations + * and then write to file. + * + * @return void + */ + public function process():void { + $this->export_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); + } + + /** + * Loop through questions in manifest file, export each from Moodle and update local copy + * + * @return void + */ + public function export_quiz_data() { + $response = $this->curlrequest->execute(); + $responsejson = json_decode($response); + if (!$responsejson) { + echo "Broken JSON returned from Moodle:\n"; + echo $response . "\n"; + echo "{$this->filepath} not updated.\n"; + $this->call_exit(); + } else if (property_exists($responsejson, 'exception')) { + echo "{$responsejson->message}\n"; + if (property_exists($responsejson, 'debuginfo')) { + echo "{$responsejson->debuginfo}\n"; + } + echo "{$this->filepath} not updated.\n"; + $this->call_exit(); + } + $this->filepath = cli_helper::get_quiz_structure_path($responsejson->quiz->name, dirname($this->quizmanifestpath)); + $manifestentries = array_column($this->quizmanifestcontents->questions, null, 'questionbankentryid'); + foreach ($responsejson->questions as $question) { + $manifestentry = $manifestentries["{$question->questionbankentryid}"] ?? false; + if ($manifestentry) { + $question->quizfilepath = $manifestentry->filepath; + unset($question->questionbankentryid); + } else { + // TODO - what happens here? + } + } + file_put_contents($this->filepath, json_encode($responsejson)); + } + + /** + * 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 new file mode 100644 index 0000000..487e0ff --- /dev/null +++ b/cli/exportquizstructurefrommoodle.php @@ -0,0 +1,122 @@ +. + +/** + * Export structure (not questions) of a quiz. + * + * @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/export_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' => $manifestpath, + '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' => '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, + ], + [ + 'longopt' => 'modulename', + 'shortopt' => 'm', + 'description' => 'Unique (within course) quiz name.', + 'default' => null, + 'variable' => 'modulename', + 'valuerequired' => true, + ], + [ + 'longopt' => 'instanceid', + 'shortopt' => 'n', + 'description' => 'Numerical course module id of quiz.', + 'default' => null, + 'variable' => 'instanceid', + 'valuerequired' => true, + ] +]; + +if (!function_exists('simplexml_load_file')) { + echo 'Please install the PHP library SimpleXML.' . "\n"; + exit; +} +$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->quizmanifestpath) { + echo 'Checking quiz repo...'; + $clihelper->check_for_changes($exportquiz->quizmanifestpath); +} +$exportquiz->process();