From 23f34c7cf47e1291653b0b646d821a5238a47acd Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Fri, 24 May 2024 15:43:04 +0100 Subject: [PATCH 1/3] ignore-cat - Allow users to set a regex for ignoring certain categories and their descendants --- classes/cli_helper.php | 11 +++++++ classes/create_repo.php | 8 +++++ classes/export_repo.php | 12 ++++++++ classes/export_trait.php | 1 + classes/external/get_question_list.php | 19 +++++++++--- classes/external/import_question.php | 3 -- classes/import_repo.php | 42 ++++++++++++++++++++++++-- cli/config_sample.txt | 9 ++++++ cli/createrepo.php | 8 +++++ cli/deletefrommoodle.php | 8 +++++ cli/exportrepofrommoodle.php | 8 +++++ cli/importrepotomoodle.php | 8 +++++ doc/createrepo.md | 1 + doc/deletefrommoodle.md | 1 + doc/exportrepofrommoodle.md | 1 + doc/importrepotomoodle.md | 1 + version.php | 4 +-- 17 files changed, 132 insertions(+), 13 deletions(-) diff --git a/classes/cli_helper.php b/classes/cli_helper.php index 9a00e13..99a12d6 100644 --- a/classes/cli_helper.php +++ b/classes/cli_helper.php @@ -203,6 +203,13 @@ public function validate_and_clean_args(): void { static::call_exit(); } } + if (isset($cliargs['ignorecat']) && $cliargs['ignorecat']) { + if (@preg_match($cliargs['ignorecat'], '') === false) { + echo "\nThere is a problem with your regular expression for ignoring categories:\n"; + echo error_get_last()["message"] . "\n"; + static::call_exit(); + } + } if (isset($cliargs['contextlevel'])) { switch ($cliargs['contextlevel']) { case 'system': @@ -454,6 +461,7 @@ public static function create_manifest_file(object $manifestcontents, string $te $manifestcontents->context->instanceid = $questioninfo->instanceid; $manifestcontents->context->defaultsubcategoryid = $subcategoryid; $manifestcontents->context->defaultsubdirectory = $subdirectory; + $manifestcontents->context->defaultignorecat = $questioninfo->ignorecat; $manifestcontents->context->moodleurl = $moodleurl; } } @@ -720,6 +728,9 @@ public function check_context(object $activity, bool $defaultwarning=false, bool if ($moodlequestionlist->contextinfo->modulename) { echo "Quiz: {$moodlequestionlist->contextinfo->modulename}\n"; } + if (isset($activity->ignorecat)) { + echo "Ignoring categories (and their descendants) in form: {$activity->ignorecat}\n"; + } if (isset($activity->subdirectory)) { echo "Question subdirectory: {$activity->subdirectory}\n"; if ($defaultwarning) { diff --git a/classes/create_repo.php b/classes/create_repo.php index 6ac53a2..deb9dd3 100644 --- a/classes/create_repo.php +++ b/classes/create_repo.php @@ -72,6 +72,12 @@ class create_repo { * @var int|null */ public ?int $qcategoryid = null; + /** + * Regex of categories to ignore. + * + * @var string|null + */ + public ?string $ignorecat; /** * Path to actual manifest file. * @@ -132,6 +138,7 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { $coursecategory = $arguments['coursecategory']; $qcategoryid = $arguments['qcategoryid']; $instanceid = $arguments['instanceid']; + $this->ignorecat = $arguments['ignorecat']; $this->moodleurl = $moodleinstances[$moodleinstance]; @@ -161,6 +168,7 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { 'instanceid' => $instanceid, 'contextonly' => 0, 'qbankentryids[]' => null, + 'ignorecat' => $this->ignorecat, ]; $this->listcurlrequest->set_option(CURLOPT_RETURNTRANSFER, true); $this->listcurlrequest->set_option(CURLOPT_POST, 1); diff --git a/classes/export_repo.php b/classes/export_repo.php index 659525f..21aec2b 100644 --- a/classes/export_repo.php +++ b/classes/export_repo.php @@ -92,6 +92,12 @@ class export_repo { * @var string */ public string $subcategory; + /** + * Regex of categories to ignore. + * + * @var string|null + */ + public ?string $ignorecat; /** * Constructor @@ -129,6 +135,11 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { // Subcategory will be properly set later. $this->subcategory = 'top'; } + if ($arguments['ignorecat']) { + $this->ignorecat = $arguments['ignorecat']; + } else { + $this->ignorecat = $this->manifestcontents->context->defaultignorecat; + } $this->tempfilepath = str_replace(cli_helper::MANIFEST_FILE, '_export' . cli_helper::TEMP_MANIFEST_FILE, @@ -160,6 +171,7 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { 'instanceid' => $this->manifestcontents->context->instanceid, 'contextonly' => 0, 'qbankentryids[]' => null, + 'ignorecat' => $this->ignorecat, ]; $this->listcurlrequest->set_option(CURLOPT_RETURNTRANSFER, true); $this->listcurlrequest->set_option(CURLOPT_POST, 1); diff --git a/classes/export_trait.php b/classes/export_trait.php index 642a140..afee379 100644 --- a/classes/export_trait.php +++ b/classes/export_trait.php @@ -209,6 +209,7 @@ public function export_to_repo_main_process(object $moodlequestionlist):void { 'coursecategory' => $this->listpostsettings['coursecategory'], 'instanceid' => $this->listpostsettings['instanceid'], 'format' => 'xml', + 'ignorecat' => $this->ignorecat, ]; fwrite($tempfile, json_encode($fileoutput) . "\n"); } diff --git a/classes/external/get_question_list.php b/classes/external/get_question_list.php index 4627b83..fa0a40f 100644 --- a/classes/external/get_question_list.php +++ b/classes/external/get_question_list.php @@ -56,8 +56,9 @@ public static function execute_parameters() { 'instanceid' => new external_value(PARAM_SEQUENCE, 'Course, module or coursecategory id'), 'contextonly' => new external_value(PARAM_BOOL, 'Only return context info?'), 'qbankentryids' => new external_multiple_structure( - new external_value(PARAM_SEQUENCE, 'QUestion bank entry id') + new external_value(PARAM_SEQUENCE, 'Question bank entry id') ), + 'ignorecat' => new external_value(PARAM_TEXT, 'Regex of categories to ignore'), ]); } @@ -75,6 +76,7 @@ public static function execute_returns():external_single_structure { 'instanceid' => new external_value(PARAM_SEQUENCE, 'id of course category, course or module'), 'qcategoryname' => new external_value(PARAM_TEXT, 'name of question category'), 'qcategoryid' => new external_value(PARAM_SEQUENCE, 'id of question category'), + 'ignorecat' => new external_value(PARAM_TEXT, 'regex of categories ignored'), ]), 'questions' => new external_multiple_structure( new external_single_structure([ @@ -100,13 +102,14 @@ public static function execute_returns():external_single_structure { * for course level) to search for questions (supercedes $coursename, $modulename & $coursecategory) * @param bool $contextonly Only return info on context and not questions * @param array|null $qbankentryids Array of qbankentryids to check + * @param string|null $ignorecat Regex of categories to ignore (along with their descendants) * @return object containing context info and an array of question data */ public static function execute(?string $qcategoryname, int $contextlevel, ?string $coursename = null, ?string $modulename = null, ?string $coursecategory = null, ?string $qcategoryid = null, ?string $instanceid = null, bool $contextonly = false, - ?array $qbankentryids = ['']):object { + ?array $qbankentryids = [''], ?string $ignorecat = null):object { global $CFG, $DB; $params = self::validate_parameters(self::execute_parameters(), [ 'qcategoryname' => $qcategoryname, @@ -118,6 +121,7 @@ public static function execute(?string $qcategoryname, 'instanceid' => $instanceid, 'contextonly' => $contextonly, 'qbankentryids' => $qbankentryids, + 'ignorecat' => $ignorecat, ]); $contextinfo = get_context($params['contextlevel'], $params['coursecategory'], $params['coursename'], $params['modulename'], @@ -137,6 +141,7 @@ public static function execute(?string $qcategoryname, $response->questions = []; $response->contextinfo->qcategoryname = ''; $response->contextinfo->qcategoryid = null; + $response->contextinfo->ignorecat = $ignorecat; if (count($qbankentryids) === 1 && $qbankentryids[0] === '') { if (is_null($qcategoryid) || $qcategoryid === '') { @@ -176,7 +181,7 @@ public static function execute(?string $qcategoryname, return $response; } - $categoriestosearch = array_merge([$category], self::get_category_descendants($category->id)); + $categoriestosearch = array_merge([$category], self::get_category_descendants($category->id, $params['ignorecat'])); $categoryids = array_map(fn($catinfo) => $catinfo->id, $categoriestosearch); @@ -217,15 +222,19 @@ public static function execute(?string $qcategoryname, * Recursive function to return the ids of all the question categories below a given category. * * @param int $parentid ID of the category to search below + * @param string|null $ignorecat Regex of categories to ignore (along with their descendants) * @return array of question categories */ - public static function get_category_descendants(int $parentid):array { + public static function get_category_descendants(int $parentid, ?string $ignorecat):array { global $DB; $children = $DB->get_records('question_categories', ['parent' => $parentid], null, 'id, parent, name'); + if ($ignorecat) { + $children = array_filter($children, fn($ch) => !preg_match($ignorecat, $ch->name)); + } // Copy array. $descendants = array_merge([], $children); foreach ($children as $child) { - $childdescendants = self::get_category_descendants($child->id); + $childdescendants = self::get_category_descendants($child->id, $ignorecat); $descendants = array_merge($descendants, $childdescendants); } return $descendants; diff --git a/classes/external/import_question.php b/classes/external/import_question.php index e22f755..53ba2ba 100644 --- a/classes/external/import_question.php +++ b/classes/external/import_question.php @@ -87,9 +87,6 @@ public static function execute_returns() { /** * Import a question from XML file * - * Initially just create a new one in Moodle DB. Will need to expand to - * use importasversion if question already exists. - * * @param string|null $questionbankentryid questionbankentry id * @param string|null $importedversion last exported version of question * @param string|null $exportedversion last imported version of question diff --git a/classes/import_repo.php b/classes/import_repo.php index d150dc0..a0b7115 100644 --- a/classes/import_repo.php +++ b/classes/import_repo.php @@ -139,6 +139,12 @@ class import_repo { * @var string */ public string $subdirectory; + /** + * Regex of categories to ignore. + * + * @var string|null + */ + public ?string $ignorecat; /** * Are we using git?. * Set in config. Adds commit hash to manifest. @@ -184,6 +190,7 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { $modulename = $arguments['modulename']; $coursecategory = $arguments['coursecategory']; $instanceid = $arguments['instanceid']; + $this->ignorecat = $arguments['ignorecat']; $this->usegit = $arguments['usegit']; $this->moodleurl = $moodleinstances[$moodleinstance]; @@ -237,6 +244,7 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { 'instanceid' => $instanceid, 'contextonly' => 0, 'qbankentryids[]' => null, + 'ignorecat' => $this->ignorecat, ]; $this->listcurlrequest->set_option(CURLOPT_RETURNTRANSFER, true); $this->listcurlrequest->set_option(CURLOPT_POST, 1); @@ -257,7 +265,7 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { $this->listpostsettings['instanceid'] = $instanceinfo->contextinfo->instanceid; $this->listpostsettings['coursename'] = $instanceinfo->contextinfo->coursename; $this->listpostsettings['modulename'] = $instanceinfo->contextinfo->modulename; - $this->listpostsettings['coursecategory'] = $instanceinfo->contextinfo->categoryname; + $this->listpostsettings['ignorecat'] = $this->ignorecat; } $this->tempfilepath = str_replace(cli_helper::MANIFEST_FILE, '_import' . cli_helper::TEMP_MANIFEST_FILE, @@ -300,6 +308,9 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { $this->listpostsettings['coursename'] = $this->manifestcontents->context->coursename; $this->listpostsettings['modulename'] = $this->manifestcontents->context->modulename; $this->listpostsettings['coursecategory'] = $this->manifestcontents->context->coursecategory; + $this->ignorecat = isset($arguments['ignorecat']) ? + $arguments['ignorecat'] : $this->manifestcontents->context->defaultignorecat; + $this->listpostsettings['ignorecat'] = $this->ignorecat; $this->listcurlrequest->set_option(CURLOPT_POSTFIELDS, $this->listpostsettings); if ($arguments['subdirectory']) { $this->subdirectory = $arguments['subdirectory']; @@ -381,6 +392,22 @@ public function import_categories():void { if (pathinfo($repoitem, PATHINFO_EXTENSION) === 'xml' && pathinfo($repoitem, PATHINFO_FILENAME) === cli_helper::CATEGORY_FILE) { $this->postsettings['qcategoryname'] = ''; + if ($this->ignorecat) { + $qcategoryname = cli_helper::get_question_category_from_file($repoitem); + if ($qcategoryname) { + $catparts = explode('/', $qcategoryname); + foreach ($catparts as $catpart) { + if (preg_match($this->ignorecat, $catpart)) { + continue 2; + } + } + } else { + echo "\n{$repoitem->getPathname()} not imported?\n"; + echo "Stopping before trying to import questions.\n"; + $this->call_exit(); + } + } + $this->upload_file($repoitem); $this->curlrequest->set_option(CURLOPT_POSTFIELDS, $this->postsettings); $response = $this->curlrequest->execute(); @@ -390,7 +417,7 @@ public function import_categories():void { echo $response . "\n"; echo "{$repoitem->getPathname()} not imported?\n"; echo "Stopping before trying to import questions.\n"; - $this->call_exit();; + $this->call_exit(); } else if (property_exists($responsejson, 'exception')) { echo "{$responsejson->message}\n"; if (property_exists($responsejson, 'debuginfo')) { @@ -398,7 +425,7 @@ public function import_categories():void { } echo "{$repoitem->getPathname()} not imported.\n"; echo "Stopping before trying to import questions.\n"; - $this->call_exit();; + $this->call_exit(); } } } @@ -479,6 +506,14 @@ public function import_questions() { $this->postsettings['qcategoryname'] = $qcategoryname; // Path of file (without filename) relative to base $directory. if ($qcategoryname) { + if ($this->ignorecat) { + $catparts = explode('/', $qcategoryname); + foreach ($catparts as $catpart) { + if (preg_match($this->ignorecat, $catpart)) { + continue 2; + } + } + } $relpath = str_replace(dirname($this->manifestpath), '', $repoitem->getPathname()); $relpath = str_replace( '\\', '/', $relpath); $existingentry = $existingentries[$relpath] ?? false; @@ -526,6 +561,7 @@ public function import_questions() { 'coursecategory' => $this->postsettings['coursecategory'], 'instanceid' => $this->postsettings['instanceid'], 'format' => 'xml', + 'ignorecat' => $this->ignorecat, ]; if ($existingentry && isset($existingentry->currentcommit)) { $fileoutput['moodlecommit'] = $existingentry->currentcommit; diff --git a/cli/config_sample.txt b/cli/config_sample.txt index 8bc9446..e9a930f 100644 --- a/cli/config_sample.txt +++ b/cli/config_sample.txt @@ -55,3 +55,12 @@ $manifestpath = null; // Are you using Git and wanting repository checks performed automatically? $usegit = true; + +// Category regex to ignore. +// Set this to ignore categories matching the regexp and the descendants of those categories. +// Example: '/^.*DO_NOT_SHARE$/' will ignore 'DO_NOT_SHARE', 'really DO_NOT_SHARE' but +// not 'DO_NOT_SHARE really', etc. +// Alternatively, use -x CLI paramater. +// If this setting is null and -x is not used, the setting +// saved in the manifest file during repo creation will be used on import/export. +$ignorecat = null; diff --git a/cli/createrepo.php b/cli/createrepo.php index 9982985..6b8b4c8 100755 --- a/cli/createrepo.php +++ b/cli/createrepo.php @@ -139,6 +139,14 @@ 'variable' => 'usegit', 'valuerequired' => false, ], + [ + 'longopt' => 'ignorecat', + 'shortopt' => 'x', + 'description' => 'Regex of categories to ignore', + 'default' => $ignorecat, + 'variable' => 'ignorecat', + 'valuerequired' => true, + ], ]; if (!function_exists('simplexml_load_file')) { diff --git a/cli/deletefrommoodle.php b/cli/deletefrommoodle.php index b1c0ab9..e4a2ca0 100755 --- a/cli/deletefrommoodle.php +++ b/cli/deletefrommoodle.php @@ -137,6 +137,14 @@ 'variable' => 'usegit', 'valuerequired' => false, ], + [ + 'longopt' => 'ignorecat', + 'shortopt' => 'x', + 'description' => 'Regex of categories to ignore', + 'default' => $ignorecat, + 'variable' => 'ignorecat', + 'valuerequired' => true, + ], ]; $clihelper = new cli_helper($options); diff --git a/cli/exportrepofrommoodle.php b/cli/exportrepofrommoodle.php index 370b726..49c7a8f 100644 --- a/cli/exportrepofrommoodle.php +++ b/cli/exportrepofrommoodle.php @@ -97,6 +97,14 @@ 'variable' => 'usegit', 'valuerequired' => false, ], + [ + 'longopt' => 'ignorecat', + 'shortopt' => 'x', + 'description' => 'Regex of categories to ignore', + 'default' => $ignorecat, + 'variable' => 'ignorecat', + 'valuerequired' => true, + ], ]; if (!function_exists('simplexml_load_file')) { diff --git a/cli/importrepotomoodle.php b/cli/importrepotomoodle.php index 58ebf16..c4075fb 100755 --- a/cli/importrepotomoodle.php +++ b/cli/importrepotomoodle.php @@ -137,6 +137,14 @@ 'variable' => 'usegit', 'valuerequired' => false, ], + [ + 'longopt' => 'ignorecat', + 'shortopt' => 'x', + 'description' => 'Regex of categories to ignore', + 'default' => $ignorecat, + 'variable' => 'ignorecat', + 'valuerequired' => true, + ], ]; if (!function_exists('simplexml_load_file')) { diff --git a/doc/createrepo.md b/doc/createrepo.md index 95958ee..38dc8d0 100644 --- a/doc/createrepo.md +++ b/doc/createrepo.md @@ -23,6 +23,7 @@ |n|instanceid|Numerical id of the course, module of course category. |t|token|Security token for webservice. |h|help| +|x|ignorecat|Regex of categories to ignore ### Example 1: diff --git a/doc/deletefrommoodle.md b/doc/deletefrommoodle.md index 8ad22b6..f99a655 100644 --- a/doc/deletefrommoodle.md +++ b/doc/deletefrommoodle.md @@ -25,6 +25,7 @@ |n|instanceid|Numerical id of the course, module of course category. |t|token|Security token for webservice. |h|help| +|x|ignorecat|Regex of categories to ignore Examples: diff --git a/doc/exportrepofrommoodle.md b/doc/exportrepofrommoodle.md index e69c9fe..2a84619 100644 --- a/doc/exportrepofrommoodle.md +++ b/doc/exportrepofrommoodle.md @@ -17,6 +17,7 @@ |q|questioncategoryid|Numerical id of subcategory to actually export. |t|token|Security token for webservice.| |h|help| +|x|ignorecat|Regex of categories to ignore Examples: diff --git a/doc/importrepotomoodle.md b/doc/importrepotomoodle.md index 5af1e39..24850ec 100644 --- a/doc/importrepotomoodle.md +++ b/doc/importrepotomoodle.md @@ -28,6 +28,7 @@ Commit this update. |n|instanceid|Numerical id of the course, module of course category. |t|token|Security token for webservice. |h|help| +|x|ignorecat|Regex of categories to ignore Examples: diff --git a/version.php b/version.php index f1c4837..0e9a653 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2023120400; +$plugin->version = 2024052400; // Question versions functionality of Moodle 4 required. // Question delete fix for Moodle 4.1.5 required. // NB 4.2.0 and 4.2.1 do not have the fix. @@ -34,5 +34,5 @@ $plugin->release = '0.9.0 for Moodle 4.1+'; $plugin->dependencies = [ - 'qbank_importasversion' => 2023102700, + 'qbank_importasversion' => 2024041600, ]; From bec858af80621cc5f4cb0a872133978a034e40ce Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Mon, 27 May 2024 13:38:43 +0100 Subject: [PATCH 2/3] ignore-cat - Unit tests --- classes/cli_helper.php | 4 +- classes/export_repo.php | 2 +- classes/external/import_question.php | 2 +- classes/import_repo.php | 2 +- doc/usinggit.md | 6 +- .../fakeignore_system_question_manifest.json | 44 +++ tests/cli_helper_test.php | 25 +- tests/create_repo_test.php | 1 + tests/export_repo_test.php | 1 + tests/external/get_question_list_test.php | 117 +++++++ tests/import_repo_test.php | 288 +++++++++++++++++- 11 files changed, 476 insertions(+), 16 deletions(-) create mode 100644 testrepo/fakeignore_system_question_manifest.json diff --git a/classes/cli_helper.php b/classes/cli_helper.php index 99a12d6..7ba328f 100644 --- a/classes/cli_helper.php +++ b/classes/cli_helper.php @@ -203,8 +203,8 @@ public function validate_and_clean_args(): void { static::call_exit(); } } - if (isset($cliargs['ignorecat']) && $cliargs['ignorecat']) { - if (@preg_match($cliargs['ignorecat'], '') === false) { + if (isset($cliargs['ignorecat'])) { + if (@preg_match($cliargs['ignorecat'], 'zzzzzzzz') === false) { echo "\nThere is a problem with your regular expression for ignoring categories:\n"; echo error_get_last()["message"] . "\n"; static::call_exit(); diff --git a/classes/export_repo.php b/classes/export_repo.php index 21aec2b..5b5e028 100644 --- a/classes/export_repo.php +++ b/classes/export_repo.php @@ -138,7 +138,7 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { if ($arguments['ignorecat']) { $this->ignorecat = $arguments['ignorecat']; } else { - $this->ignorecat = $this->manifestcontents->context->defaultignorecat; + $this->ignorecat = $this->manifestcontents->context->defaultignorecat ?? null; } $this->tempfilepath = str_replace(cli_helper::MANIFEST_FILE, diff --git a/classes/external/import_question.php b/classes/external/import_question.php index 53ba2ba..4a13fb5 100644 --- a/classes/external/import_question.php +++ b/classes/external/import_question.php @@ -154,7 +154,7 @@ public static function execute(?string $questionbankentryid, ?string $importedve $iscategory = false; if ($params['questionbankentryid']) { - $question = question_bank::load_question_data($questiondata->questionid); + $question = question_bank::load_question($questiondata->questionid); } else if (isset($params['qcategoryid']) && $params['qcategoryid'] !== '') { $category = $DB->get_record('question_categories', ['id' => $qcategoryid]); $qformat->setCategory($category); diff --git a/classes/import_repo.php b/classes/import_repo.php index a0b7115..d94dfa3 100644 --- a/classes/import_repo.php +++ b/classes/import_repo.php @@ -309,7 +309,7 @@ public function __construct(cli_helper $clihelper, array $moodleinstances) { $this->listpostsettings['modulename'] = $this->manifestcontents->context->modulename; $this->listpostsettings['coursecategory'] = $this->manifestcontents->context->coursecategory; $this->ignorecat = isset($arguments['ignorecat']) ? - $arguments['ignorecat'] : $this->manifestcontents->context->defaultignorecat; + $arguments['ignorecat'] : $this->manifestcontents->context->defaultignorecat ?? null; $this->listpostsettings['ignorecat'] = $this->ignorecat; $this->listcurlrequest->set_option(CURLOPT_POSTFIELDS, $this->listpostsettings); if ($arguments['subdirectory']) { diff --git a/doc/usinggit.md b/doc/usinggit.md index f6f1297..e00de62 100644 --- a/doc/usinggit.md +++ b/doc/usinggit.md @@ -35,7 +35,11 @@ You can use context instance id and subcategory id instead: * `-n` the context instance id, which is the course id in this case. * `-q` the question category id -You'll get a confirmation of the context and category with the option to abort before performing the actual export. A manifest file will be created in the specified directory. +You can also ignore certain categories (and their descendants) using `-x` and a regex expression to match the category name(s) within Moodle. + +`php createrepo.php -l course -n 2 -d "master" -q 80 -x "/^.*DO_NOT_SHARE$/"` + +You'll get a confirmation of the context and category with the option to abort before performing the actual export. A manifest file will be created in the specified directory. The subdirectory/subcategory and any regex for ignoring categories you use will be stored in the manifest and used on import and export unless you override them using `-s` and/or `-x` (e.g. `-s "top" -x "/^\b$/"`). To import/export changes: diff --git a/testrepo/fakeignore_system_question_manifest.json b/testrepo/fakeignore_system_question_manifest.json new file mode 100644 index 0000000..c3786dc --- /dev/null +++ b/testrepo/fakeignore_system_question_manifest.json @@ -0,0 +1,44 @@ +{ + "context": { + "contextlevel": 10, + "coursename": "", + "modulename": "", + "coursecategory": "", + "qcategoryname": "/top", + "instanceid": "", + "defaultsubdirectory": "top\/cat-2", + "defaultsubcategoryid": 5, + "defaultignorecat": "/subcat 2_1/" + }, + "questions": [ + { + "questionbankentryid": 35001, + "filepath": "\/top\/cat-1\/First-Question.xml", + "importedversion": "1", + "exportedversion": "1", + "format": "xml" + }, + { + "questionbankentryid": 35002, + "filepath": "\/top\/cat-2\/subcat-2_1\/Third-Question.xml", + "importedversion": "6", + "exportedversion": "7", + "format": "xml" + }, + { + "questionbankentryid": 35004, + "filepath": "\/top\/cat-2\/subcat-2_1\/Fourth-Question.xml", + "importedversion": "1", + "exportedversion": "1", + "currentcommit": "35004test", + "format": "xml" + }, + { + "questionbankentryid": 35003, + "filepath": "\/top\/cat-2\/Second-Question.xml", + "importedversion": "1", + "exportedversion": "1", + "format": "xml" + } + ] +} \ No newline at end of file diff --git a/tests/cli_helper_test.php b/tests/cli_helper_test.php index 262013c..9ad4a48 100644 --- a/tests/cli_helper_test.php +++ b/tests/cli_helper_test.php @@ -385,7 +385,7 @@ public function test_validation_contextlevel_course(): void { * Validation * @covers \gitsync\cli_helper\validate_and_clean_args() */ - public function test_validation_contextlevel_modul(): void { + public function test_validation_contextlevel_module(): void { $helper = new fake_cli_helper([]); $helper->processedoptions = ['token' => 'X', 'contextlevel' => 'module', 'coursecategory' => 'cat1', @@ -484,7 +484,6 @@ public function test_validation_contextlevel_manifestpath(): void { $this->expectOutputString(''); } - /** * Validation * @covers \gitsync\cli_helper\validate_and_clean_args() @@ -495,4 +494,26 @@ public function test_validation_contextlevel_wrong(): void { $helper->validate_and_clean_args(); $this->expectOutputRegex('/Contextlevel should be/'); } + + /** + * Validation + * @covers \gitsync\cli_helper\validate_and_clean_args() + */ + public function test_validation_ignorecat(): void { + $helper = new fake_cli_helper([]); + $helper->processedoptions = ['token' => 'X', 'manifestpath' => 'path/subpath', 'ignorecat' => '/hello/']; + $helper->validate_and_clean_args(); + $this->expectOutputString(''); + } + + /** + * Validation + * @covers \gitsync\cli_helper\validate_and_clean_args() + */ + public function test_validation_ignorecat_error(): void { + $helper = new fake_cli_helper([]); + $helper->processedoptions = ['token' => 'X', 'manifestpath' => 'path/subpath', 'ignorecat' => '/hello']; + @$helper->validate_and_clean_args(); + $this->expectOutputRegex('/problem with your regular expression/'); + } } diff --git a/tests/create_repo_test.php b/tests/create_repo_test.php index 73d34c3..75fbd84 100644 --- a/tests/create_repo_test.php +++ b/tests/create_repo_test.php @@ -73,6 +73,7 @@ public function setUp(): void { 'instanceid' => null, 'token' => 'XXXXXX', 'help' => false, + 'ignorecat' => null, ]; $this->clihelper = $this->getMockBuilder(\qbank_gitsync\cli_helper::class)->onlyMethods([ 'get_arguments', 'check_context', diff --git a/tests/export_repo_test.php b/tests/export_repo_test.php index a572ae9..dd7d362 100644 --- a/tests/export_repo_test.php +++ b/tests/export_repo_test.php @@ -94,6 +94,7 @@ public function setUp(): void { 'manifestpath' => '/' . self::MOODLE . '_system' . cli_helper::MANIFEST_FILE, 'token' => 'XXXXXX', 'help' => false, + 'ignorecat' => null, ]; $this->clihelper = $this->getMockBuilder(\qbank_gitsync\cli_helper::class)->onlyMethods([ 'get_arguments', 'check_context', diff --git a/tests/external/get_question_list_test.php b/tests/external/get_question_list_test.php index 27ec743..29fab33 100644 --- a/tests/external/get_question_list_test.php +++ b/tests/external/get_question_list_test.php @@ -456,4 +456,121 @@ public function test_list_with_subcategory_id(): void { $events = $sink->get_events(); $this->assertEquals(count($events), 0); } + + /** + * Test output of execute function when subcategory name supplied and ignore category. + */ + public function test_list_with_subcategory_name_and_ignore(): void { + global $DB; + // Set the required capabilities - webservice access and list rights on course. + // Q1 in original category. Q2 in Cat2. Q3 in SubCat1. Q4 in SubCat2. + $context = context_course::instance($this->course->id); + $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']); + role_assign($managerroleid, $this->user->id, $context->id); + $qcategory2 = $this->generator->create_question_category( + ['contextid' => \context_course::instance($this->course->id)->id, 'name' => 'Cat2_DO_NOT_SHARE']); + $qcategory3 = $this->generator->create_question_category( + ['contextid' => \context_course::instance($this->course->id)->id, 'name' => 'SubCat1', + 'parent' => $qcategory2->id]); + $qcategory4 = $this->generator->create_question_category( + ['contextid' => \context_course::instance($this->course->id)->id, 'name' => 'SubCat2', + 'parent' => $qcategory2->id]); + + $q2 = $this->generator->create_question('shortanswer', null, + ['name' => self::QNAME . '2', 'category' => $qcategory2->id]); + $q3 = $this->generator->create_question('shortanswer', null, + ['name' => self::QNAME . '3', 'category' => $qcategory3->id]); + $q4 = $this->generator->create_question('shortanswer', null, + ['name' => self::QNAME . '4', 'category' => $qcategory4->id]); + + $sink = $this->redirectEvents(); + $returnvalue = get_question_list::execute('top', 50, $this->course->fullname, null, null, + null, null, false, [''], '/.*DO_NOT_SHARE/'); + + $returnvalue = external_api::clean_returnvalue( + get_question_list::execute_returns(), + $returnvalue + ); + + $wrongq = false; + $this->assertEquals(1, count($returnvalue['questions'])); + foreach ($returnvalue['questions'] as $returnedq) { + if (array_search($returnedq['questionbankentryid'], [$this->qbankentryid]) === false) { + $wrongq = true; + } + } + + $this->assertEquals($wrongq, false); + + $this->assertEquals($this->course->fullname, $returnvalue['contextinfo']['coursename']); + $this->assertEquals($this->course->id, $returnvalue['contextinfo']['instanceid']); + $this->assertEquals(null, $returnvalue['contextinfo']['categoryname']); + $this->assertEquals(null, $returnvalue['contextinfo']['modulename']); + $this->assertEquals('/.*DO_NOT_SHARE/', $returnvalue['contextinfo']['ignorecat']); + + $events = $sink->get_events(); + $this->assertEquals(count($events), 0); + } + + /** + * Test output of execute function when ignore category. + */ + public function test_list_with_ignore(): void { + global $DB; + // Set the required capabilities - webservice access and list rights on course. + // Q1 in original category. Q2 in Cat2. Q3 in SubCat1. Q4 in SubCat2. + $context = context_course::instance($this->course->id); + $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']); + role_assign($managerroleid, $this->user->id, $context->id); + $qcategory2 = $this->generator->create_question_category( + ['contextid' => \context_course::instance($this->course->id)->id, 'name' => 'Cat2']); + $qcategory3 = $this->generator->create_question_category( + ['contextid' => \context_course::instance($this->course->id)->id, 'name' => 'SubCat1', + 'parent' => $qcategory2->id]); + $qcategory4 = $this->generator->create_question_category( + ['contextid' => \context_course::instance($this->course->id)->id, 'name' => 'SubCat2_DO_NOT_SHARE', + 'parent' => $qcategory2->id]); + + $q2 = $this->generator->create_question('shortanswer', null, + ['name' => self::QNAME . '2', 'category' => $qcategory2->id]); + $q3 = $this->generator->create_question('shortanswer', null, + ['name' => self::QNAME . '3', 'category' => $qcategory3->id]); + $q4 = $this->generator->create_question('shortanswer', null, + ['name' => self::QNAME . '4', 'category' => $qcategory4->id]); + + + $qbankentryid2 = $DB->get_field('question_versions', 'questionbankentryid', + ['questionid' => $q2->id], $strictness = MUST_EXIST); + $qbankentryid3 = $DB->get_field('question_versions', 'questionbankentryid', + ['questionid' => $q3->id], $strictness = MUST_EXIST); + + $sink = $this->redirectEvents(); + $returnvalue = get_question_list::execute('top/' . $qcategory2->name, 50, $this->course->fullname, null, null, + null, null, false, [''], '/.*DO_NOT_SHARE/'); + + $returnvalue = external_api::clean_returnvalue( + get_question_list::execute_returns(), + $returnvalue + ); + + $wrongq = false; + $this->assertEquals(2, count($returnvalue['questions'])); + foreach ($returnvalue['questions'] as $returnedq) { + // Q1 excluded by subcategory. Q4 excluded by ignore. + if (array_search($returnedq['questionbankentryid'], [$qbankentryid2, $qbankentryid3]) === false) { + $wrongq = true; + } + } + + $this->assertEquals($wrongq, false); + + $this->assertEquals($this->course->fullname, $returnvalue['contextinfo']['coursename']); + $this->assertEquals($this->course->id, $returnvalue['contextinfo']['instanceid']); + $this->assertEquals(null, $returnvalue['contextinfo']['categoryname']); + $this->assertEquals(null, $returnvalue['contextinfo']['modulename']); + $this->assertEquals('/.*DO_NOT_SHARE/', $returnvalue['contextinfo']['ignorecat']); + + $events = $sink->get_events(); + $this->assertEquals(count($events), 0); + } } diff --git a/tests/import_repo_test.php b/tests/import_repo_test.php index ac7037e..2400c1b 100644 --- a/tests/import_repo_test.php +++ b/tests/import_repo_test.php @@ -107,6 +107,7 @@ public function setUp(): void { 'token' => 'XXXXXX', 'help' => false, 'usegit' => false, + 'ignorecat' => null, ]; $this->clihelper = $this->getMockBuilder(\qbank_gitsync\cli_helper::class)->onlyMethods([ 'get_arguments', 'check_context', @@ -264,6 +265,69 @@ public function test_process_manifest_path_and_subdirectory(): void { $this->assertEquals("top/cat-2/subcat-2_1", $this->importrepo->subdirectory); } + /** + * Test the full process with manifest path and defaultignorecat. + */ + public function test_process_manifest_path_and_defaultignore(): void { + $this->options["manifestpath"] = 'fakeignore_system_question_manifest.json'; + $this->replace_mock_default(); + // The test repo has 2 categories and 1 subcategory. 1 question in each category and 2 in subcategory. + // We expect 2 category calls to the webservice and 1 question calls as using defaultsubdir cat-2 + // and default ignore of subcat 2_1. + $this->curl->expects($this->exactly(3))->method('execute')->willReturnOnConsecutiveCalls( + '{"questionbankentryid": null}', + '{"questionbankentryid": null}', + '{"questionbankentryid": "35004", "version": "2"}', + ); + + $this->listcurl->expects($this->exactly(1))->method('execute')->willReturn( + '{"contextinfo":{"contextlevel": "module", "categoryname":"", "coursename":"Course 1", + "modulename":"Module 1", "instanceid":"", "qcategoryname":"top"}, + "questions": []}', + ); + + $this->importrepo->process(); + + // Check manifest file created. + $this->assertEquals(file_exists($this->rootpath . '/' . self::MOODLE . '_system' . cli_helper::MANIFEST_FILE), true); + $this->expectOutputRegex('/^\nAdded 0 questions.*Updated 1 question.*\n$/s'); + // Use default ignore parameter. + $this->assertEquals("/subcat 2_1/", $this->importrepo->ignorecat); + } + + /** + * Test the full process with manifest path and defaultignorecat and ignorecat param. + */ + public function test_process_manifest_path_and_defaultignore_and_param(): void { + $this->options["manifestpath"] = 'fakeignore_system_question_manifest.json'; + $this->options["ignorecat"] = '/cat 1/'; + $this->options["subdirectory"] = null; + $this->replace_mock_default(); + // The test repo has 2 categories and 1 subcategory. 1 question in each category and 2 in subcategory. + // We expect 2 category calls to the webservice and 3 question calls as ignoring cat 1. + $this->curl->expects($this->exactly(5))->method('execute')->willReturnOnConsecutiveCalls( + '{"questionbankentryid": null}', + '{"questionbankentryid": null}', + '{"questionbankentryid": "35002", "version": "2"}', + '{"questionbankentryid": "35004", "version": "2"}', + '{"questionbankentryid": "35003", "version": "2"}', + ); + + $this->listcurl->expects($this->exactly(1))->method('execute')->willReturn( + '{"contextinfo":{"contextlevel": "module", "categoryname":"", "coursename":"Course 1", + "modulename":"Module 1", "instanceid":"", "qcategoryname":"top"}, + "questions": []}', + ); + + $this->importrepo->process(); + + // Check manifest file created. + $this->assertEquals(file_exists($this->rootpath . '/' . self::MOODLE . '_system' . cli_helper::MANIFEST_FILE), true); + $this->expectOutputRegex('/^\nAdded 0 questions.*Updated 3 questions.*\n$/s'); + // Use default ignore parameter. + $this->assertEquals("/cat 1/", $this->importrepo->ignorecat); + } + /** * Test importing categories. * @covers \gitsync\import_repo\import_categories() @@ -282,6 +346,46 @@ function() { $this->assertContains($this->rootpath . '/top/cat-2/subcat-2_1/gitsync_category.xml', $this->results); } + /** + * Test importing categories with ignore. + * @covers \gitsync\import_repo\import_categories() + */ + public function test_import_categories_with_ignore(): void { + $this->options["ignorecat"] = '/^cat 2$/'; + $this->replace_mock_default(); + $this->results = []; + $this->curl->expects($this->exactly(1))->method('execute')->will($this->returnCallback( + function() { + $this->results[] = $this->importrepo->repoiterator->getPathname(); + return '{"questionbankentryid": null, "version" : null}'; + }) + ); + $this->importrepo->import_categories(); + $this->assertContains($this->rootpath . '/top/cat-1/gitsync_category.xml', $this->results); + $this->assertNotContains($this->rootpath . '/top/cat-2/gitsync_category.xml', $this->results); + $this->assertNotContains($this->rootpath . '/top/cat-2/subcat-2_1/gitsync_category.xml', $this->results); + } + + /** + * Test importing categories with ignore subcat. + * @covers \gitsync\import_repo\import_categories() + */ + public function test_import_categories_with_ignore_subcat(): void { + $this->options["ignorecat"] = '/subcat 2_1/'; + $this->replace_mock_default(); + $this->results = []; + $this->curl->expects($this->exactly(2))->method('execute')->will($this->returnCallback( + function() { + $this->results[] = $this->importrepo->repoiterator->getPathname(); + return '{"questionbankentryid": null, "version" : null}'; + }) + ); + $this->importrepo->import_categories(); + $this->assertContains($this->rootpath . '/top/cat-1/gitsync_category.xml', $this->results); + $this->assertContains($this->rootpath . '/top/cat-2/gitsync_category.xml', $this->results); + $this->assertNotContains($this->rootpath . '/top/cat-2/subcat-2_1/gitsync_category.xml', $this->results); + } + /** * Test importing categories broken JSON. * @covers \gitsync\import_repo\import_categories() @@ -356,6 +460,78 @@ function() { $this->assertEquals($firstline->format, 'xml'); } + /** + * Test importing questions with ignore. + * @covers \gitsync\import_repo\import_questions() + */ + public function test_import_questions_with_ignore(): void { + $this->options["ignorecat"] = '/^subcat 2.*/'; + $this->replace_mock_default(); + $this->results = []; + $this->curl->expects($this->exactly(2))->method('execute')->willReturnOnConsecutiveCalls( + '{"questionbankentryid": "35001", "version": "2"}', + '{"questionbankentryid": "35002", "version": "2"}', + ); + $this->curl->expects($this->exactly(2))->method('execute')->will($this->returnCallback( + function() { + $this->results[] = [ + $this->importrepo->subdirectoryiterator->getPathname(), + $this->importrepo->postsettings['qcategoryname'], + ]; + }) + ); + $this->importrepo->postsettings = [ + 'contextlevel' => '10', + 'coursename' => 'Course 1', + 'modulename' => 'Test 1', + 'coursecategory' => 'Cat 1', + 'instanceid' => null, + ]; + $this->importrepo->import_questions(); + $this->assertContains([$this->rootpath . '/top/cat-1/First-Question.xml', 'top/cat 1'], $this->results); + $this->assertNotContains([$this->rootpath . '/top/cat-2/subcat-2_1/Third-Question.xml', 'top/cat 2/subcat 2_1'], + $this->results); + $this->assertNotContains([$this->rootpath . '/top/cat-2/subcat-2_1/Fourth-Question.xml', 'top/cat 2/subcat 2_1'], + $this->results); + $this->assertContains([$this->rootpath . '/top/cat-2/Second-Question.xml', 'top/cat 2'], $this->results); + } + + /** + * Test importing questions with subcat and ignore. + * @covers \gitsync\import_repo\import_questions() + */ + public function test_import_questions_with_subcat_and_ignore(): void { + $this->options["subdirectory"] = 'top/cat-2'; + $this->options["ignorecat"] = '/^subcat 2.*/'; + $this->replace_mock_default(); + $this->results = []; + $this->curl->expects($this->exactly(1))->method('execute')->willReturnOnConsecutiveCalls( + '{"questionbankentryid": "35002", "version": "2"}', + ); + $this->curl->expects($this->exactly(1))->method('execute')->will($this->returnCallback( + function() { + $this->results[] = [ + $this->importrepo->subdirectoryiterator->getPathname(), + $this->importrepo->postsettings['qcategoryname'], + ]; + }) + ); + $this->importrepo->postsettings = [ + 'contextlevel' => '10', + 'coursename' => 'Course 1', + 'modulename' => 'Test 1', + 'coursecategory' => 'Cat 1', + 'instanceid' => null, + ]; + $this->importrepo->import_questions(); + $this->assertNotContains([$this->rootpath . '/top/cat-1/First-Question.xml', 'top/cat 1'], $this->results); + $this->assertNotContains([$this->rootpath . '/top/cat-2/subcat-2_1/Third-Question.xml', 'top/cat 2/subcat 2_1'], + $this->results); + $this->assertNotContains([$this->rootpath . '/top/cat-2/subcat-2_1/Fourth-Question.xml', 'top/cat 2/subcat 2_1'], + $this->results); + $this->assertContains([$this->rootpath . '/top/cat-2/Second-Question.xml', 'top/cat 2'], $this->results); + } + /** * Test importing questions broken JSON. * @covers \gitsync\import_repo\import_questions() @@ -603,11 +779,11 @@ public function test_manifest_file(): void { $manifestcontents = json_decode(file_get_contents($this->importrepo->manifestpath)); $this->assertCount(4, $manifestcontents->questions); - $manifestentries = array_column($manifestcontents->questions, null, 'questionbankentryid'); - $this->assertArrayHasKey('35001', $manifestentries); - $this->assertArrayHasKey('35002', $manifestentries); - $this->assertArrayHasKey('35003', $manifestentries); - $this->assertArrayHasKey('35004', $manifestentries); + $manifestentries = array_column($manifestcontents->questions, null, 'filepath'); + $this->assertArrayHasKey('/top/cat-1/First-Question.xml', $manifestentries); + $this->assertArrayHasKey('/top/cat-2/Second-Question.xml', $manifestentries); + $this->assertArrayHasKey('/top/cat-2/subcat-2_1/Third-Question.xml', $manifestentries); + $this->assertArrayHasKey('/top/cat-2/subcat-2_1/Fourth-Question.xml', $manifestentries); $context = $manifestcontents->context; $this->assertEquals($context->contextlevel, '10'); @@ -616,6 +792,7 @@ public function test_manifest_file(): void { $this->assertEquals($context->coursecategory, ''); $this->assertEquals($context->defaultsubcategoryid, 123); $this->assertEquals($context->defaultsubdirectory, 'top'); + $this->assertEquals($context->defaultignorecat, null); $this->expectOutputRegex('/^\nManifest file is empty.*\nAdded 4 questions.*Updated 0 questions.*\n$/s'); } @@ -652,9 +829,9 @@ public function test_manifest_file_with_subdirectory(): void { $manifestcontents = json_decode(file_get_contents($this->importrepo->manifestpath)); $this->assertCount(2, $manifestcontents->questions); - $manifestentries = array_column($manifestcontents->questions, null, 'questionbankentryid'); - $this->assertArrayHasKey('35004', $manifestentries); - $this->assertArrayHasKey('35003', $manifestentries); + $manifestentries = array_column($manifestcontents->questions, null, 'filepath'); + $this->assertArrayHasKey('/top/cat-2/subcat-2_1/Third-Question.xml', $manifestentries); + $this->assertArrayHasKey('/top/cat-2/subcat-2_1/Fourth-Question.xml', $manifestentries); $context = $manifestcontents->context; $this->assertEquals($context->contextlevel, '10'); @@ -663,10 +840,105 @@ public function test_manifest_file_with_subdirectory(): void { $this->assertEquals($context->coursecategory, ''); $this->assertEquals($context->defaultsubcategoryid, 123); $this->assertEquals($context->defaultsubdirectory, 'top/cat-2/subcat-2_1'); + $this->assertEquals($context->defaultignorecat, null); $this->expectOutputRegex('/^\nManifest file is empty.*\nAdded 2 questions.*Updated 0 questions.*\n$/s'); } + /** + * Test creation of manifest file with subdirectory and ignore. + * @covers \gitsync\cli_helper\create_manifest_file() + * + * (Run the entire process and check the output to avoid lots of additonal setup of tempfile etc.) + */ + public function test_manifest_file_with_subdirectory_and_ignore(): void { + unlink($this->importrepo->manifestpath); + $this->options["subdirectory"] = 'top/cat-2'; + $this->options["ignorecat"] = '/subcat 2_1/'; + $this->replace_mock_default(); + // The test repo has 2 categories and 1 subcategory. 1 question in each category and 2 in subcategory. + // We expect 2 category calls to the webservice and 1 question call. + $this->importrepo->curlrequest->expects($this->exactly(3))->method('execute')->willReturnOnConsecutiveCalls( + '{"questionbankentryid": null}', + '{"questionbankentryid": null}', + '{"questionbankentryid": "35003", "version": "2"}', + ); + + $this->importrepo->listcurlrequest->expects($this->exactly(1))->method('execute')->willReturn( + '{"contextinfo":{"contextlevel": "module", "categoryname":"", "coursename":"Course 1", + "modulename":"Module 1", "instanceid":"", "qcategoryname":"top/ds", "qcategoryid":1}, + "questions": []}' + ); + $this->importrepo->process(); + + // Manifest file is a single array. + $this->assertEquals(1, count(file($this->importrepo->manifestpath))); + $manifestcontents = json_decode(file_get_contents($this->importrepo->manifestpath)); + $this->assertCount(1, $manifestcontents->questions); + + $manifestentries = array_column($manifestcontents->questions, null, 'filepath'); + $this->assertArrayHasKey('/top/cat-2/Second-Question.xml', $manifestentries); + + $context = $manifestcontents->context; + $this->assertEquals($context->contextlevel, '10'); + $this->assertEquals($context->coursename, 'Course 1'); + $this->assertEquals($context->modulename, 'Module 1'); + $this->assertEquals($context->coursecategory, ''); + $this->assertEquals($context->defaultsubcategoryid, 123); + $this->assertEquals($context->defaultsubdirectory, 'top/cat-2'); + $this->assertEquals($context->defaultignorecat, '/subcat 2_1/'); + + $this->expectOutputRegex('/^\nManifest file is empty.*\nAdded 1 question.*Updated 0 questions.*\n$/s'); + } + + /** + * Test creation of manifest file with ignore. + * @covers \gitsync\cli_helper\create_manifest_file() + * + * (Run the entire process and check the output to avoid lots of additonal setup of tempfile etc.) + */ + public function test_manifest_file_with_ignore(): void { + unlink($this->importrepo->manifestpath); + $this->options["ignorecat"] = '/subcat 2_1/'; + $this->replace_mock_default(); + // The test repo has 2 categories and 1 subcategory. 1 question in each category and 2 in subcategory. + // We expect 2 category calls to the webservice and 2 question calls. + $this->importrepo->curlrequest->expects($this->exactly(4))->method('execute')->willReturnOnConsecutiveCalls( + '{"questionbankentryid": null}', + '{"questionbankentryid": null}', + '{"questionbankentryid": "35003", "version": "2"}', + '{"questionbankentryid": "35005", "version": "2"}', + ); + + $this->importrepo->listcurlrequest->expects($this->exactly(1))->method('execute')->willReturn( + '{"contextinfo":{"contextlevel": "module", "categoryname":"", "coursename":"Course 1", + "modulename":"Module 1", "instanceid":"", "qcategoryname":"top/ds", "qcategoryid":1}, + "questions": []}' + ); + $this->importrepo->process(); + + // Manifest file is a single array. + $this->assertEquals(1, count(file($this->importrepo->manifestpath))); + $manifestcontents = json_decode(file_get_contents($this->importrepo->manifestpath)); + $this->assertCount(2, $manifestcontents->questions); + + $manifestentries = array_column($manifestcontents->questions, null, 'filepath'); + $this->assertArrayHasKey('/top/cat-2/Second-Question.xml', $manifestentries); + $this->assertArrayHasKey('/top/cat-1/First-Question.xml', $manifestentries); + + $context = $manifestcontents->context; + $this->assertEquals($context->contextlevel, '10'); + $this->assertEquals($context->coursename, 'Course 1'); + $this->assertEquals($context->modulename, 'Module 1'); + $this->assertEquals($context->coursecategory, ''); + $this->assertEquals($context->defaultsubcategoryid, 123); + $this->assertEquals($context->defaultsubdirectory, 'top'); + $this->assertEquals($context->defaultignorecat, '/subcat 2_1/'); + + $this->expectOutputRegex('/^\nManifest file is empty.*\nAdded 2 question.*Updated 0 questions.*\n$/s'); + } + + /** * Test update of manifest file. * @covers \gitsync\cli_helper\create_manifest_file() From c06a2e13163db549c60d6cfc78a21f69667e0d53 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Mon, 27 May 2024 13:54:55 +0100 Subject: [PATCH 3/3] ignore-cat - Tidy up --- tests/export_trait_test.php | 1 + tests/external/get_question_list_test.php | 1 - tests/tidy_trait_test.php | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/export_trait_test.php b/tests/export_trait_test.php index 197ded8..b162a9a 100644 --- a/tests/export_trait_test.php +++ b/tests/export_trait_test.php @@ -70,6 +70,7 @@ public function setUp(): void { 'manifestpath' => '/' . self::MOODLE . '_system' . cli_helper::MANIFEST_FILE, 'token' => 'XXXXXX', 'help' => false, + 'ignorecat' => null, ]; $this->clihelper = $this->getMockBuilder(\qbank_gitsync\cli_helper::class)->onlyMethods([ 'get_arguments', 'check_context', diff --git a/tests/external/get_question_list_test.php b/tests/external/get_question_list_test.php index 29fab33..a8d1864 100644 --- a/tests/external/get_question_list_test.php +++ b/tests/external/get_question_list_test.php @@ -538,7 +538,6 @@ public function test_list_with_ignore(): void { $q4 = $this->generator->create_question('shortanswer', null, ['name' => self::QNAME . '4', 'category' => $qcategory4->id]); - $qbankentryid2 = $DB->get_field('question_versions', 'questionbankentryid', ['questionid' => $q2->id], $strictness = MUST_EXIST); $qbankentryid3 = $DB->get_field('question_versions', 'questionbankentryid', diff --git a/tests/tidy_trait_test.php b/tests/tidy_trait_test.php index 8717bae..0af8dc4 100644 --- a/tests/tidy_trait_test.php +++ b/tests/tidy_trait_test.php @@ -70,6 +70,7 @@ public function setUp(): void { 'manifestpath' => '/' . self::MOODLE . '_system' . cli_helper::MANIFEST_FILE, 'token' => 'XXXXXX', 'help' => false, + 'ignorecat' => null, ]; $this->clihelper = $this->getMockBuilder(\qbank_gitsync\cli_helper::class)->onlyMethods([ 'get_arguments', 'check_context',