From 1dab8faafdfccec991e81bb0da7e647a44abf7a0 Mon Sep 17 00:00:00 2001 From: John Beedell Date: Fri, 3 Aug 2018 09:53:54 +0100 Subject: [PATCH] MDL-23545 question: XML import/export add category description --- question/editlib.php | 2 +- question/format.php | 89 +++- .../gift/tests/behat/import_export.feature | 2 +- question/format/upgrade.txt | 15 + question/format/xml/format.php | 16 +- .../fixtures/categories_reverse_order.xml | 84 ++++ .../fixtures/category_with_description.xml | 40 ++ .../xml/tests/fixtures/export_category.xml | 41 ++ .../xml/tests/fixtures/nested_categories.xml | 60 +++ .../nested_categories_with_questions.xml | 181 ++++++++ .../xml/tests/fixtures/old_format_file.xml | 72 +++ .../tests/qformat_xml_import_export_test.php | 427 ++++++++++++++++++ question/format/xml/tests/xmlformat_test.php | 36 ++ 13 files changed, 1037 insertions(+), 28 deletions(-) create mode 100644 question/format/xml/tests/fixtures/categories_reverse_order.xml create mode 100644 question/format/xml/tests/fixtures/category_with_description.xml create mode 100644 question/format/xml/tests/fixtures/export_category.xml create mode 100644 question/format/xml/tests/fixtures/nested_categories.xml create mode 100644 question/format/xml/tests/fixtures/nested_categories_with_questions.xml create mode 100644 question/format/xml/tests/fixtures/old_format_file.xml create mode 100644 question/format/xml/tests/qformat_xml_import_export_test.php diff --git a/question/editlib.php b/question/editlib.php index da255f515dd..d0dfbf87c74 100644 --- a/question/editlib.php +++ b/question/editlib.php @@ -76,7 +76,7 @@ function get_questions_category( $category, $noparent=false, $recurse=true, $exp // Get the list of questions for the category list($usql, $params) = $DB->get_in_or_equal($categorylist); - $questions = $DB->get_records_select('question', "category {$usql} {$npsql}", $params, 'qtype, name'); + $questions = $DB->get_records_select('question', "category {$usql} {$npsql}", $params, 'category, qtype, name'); // Iterate through questions, getting stuff we need $qresults = array(); diff --git a/question/format.php b/question/format.php index d0091aaa6a3..da31797397f 100644 --- a/question/format.php +++ b/question/format.php @@ -52,6 +52,8 @@ class qformat_default { public $translator = null; public $canaccessbackupdata = true; protected $importcontext = null; + /** @var bool $displayprogress Whether to display progress. */ + public $displayprogress = true; // functions to indicate import/export functionality // override to return true if implemented @@ -210,6 +212,17 @@ class qformat_default { $this->canaccessbackupdata = $canaccess; } + /** + * Change whether to display progress messages. + * There is normally no need to use this function as the + * default for $displayprogress is true. + * Set to false for unit tests. + * @param bool $displayprogress + */ + public function set_display_progress($displayprogress) { + $this->displayprogress = $displayprogress; + } + /*********************** * IMPORTING FUNCTIONS ***********************/ @@ -292,7 +305,9 @@ class qformat_default { raise_memory_limit(MEMORY_EXTRA); // STAGE 1: Parse the file - echo $OUTPUT->notification(get_string('parsingquestions', 'question'), 'notifysuccess'); + if ($this->displayprogress) { + echo $OUTPUT->notification(get_string('parsingquestions', 'question'), 'notifysuccess'); + } if (! $lines = $this->readdata($this->filename)) { echo $OUTPUT->notification(get_string('cannotread', 'question')); @@ -305,8 +320,10 @@ class qformat_default { } // STAGE 2: Write data to database - echo $OUTPUT->notification(get_string('importingquestions', 'question', - $this->count_questions($questions)), 'notifysuccess'); + if ($this->displayprogress) { + echo $OUTPUT->notification(get_string('importingquestions', 'question', + $this->count_questions($questions)), 'notifysuccess'); + } // check for errors before we continue if ($this->stoponerror and ($this->importerrors>0)) { @@ -366,7 +383,7 @@ class qformat_default { if ($this->catfromfile) { // find/create category object $catpath = $question->category; - $newcategory = $this->create_category_path($catpath); + $newcategory = $this->create_category_path($catpath, $question); if (!empty($newcategory)) { $this->category = $newcategory; } @@ -378,7 +395,9 @@ class qformat_default { $count++; - echo "

{$count}. ".$this->format_question_text($question)."

"; + if ($this->displayprogress) { + echo "

{$count}. " . $this->format_question_text($question) . "

"; + } $question->category = $this->category->id; $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) @@ -502,10 +521,10 @@ class qformat_default { * but if $getcontext is set then ignore the context and use selected category context. * * @param string catpath delimited category path - * @param int courseid course to search for categories + * @param object $lastcategoryinfo Contains category information * @return mixed category object or null if fails */ - protected function create_category_path($catpath) { + protected function create_category_path($catpath, $lastcategoryinfo = null) { global $DB; $catnames = $this->split_category_path($catpath); $parent = 0; @@ -535,27 +554,47 @@ class qformat_default { $this->importcontext = $context; // Now create any categories that need to be created. - foreach ($catnames as $catname) { + foreach ($catnames as $key => $catname) { if ($parent == 0) { $category = question_get_top_category($context->id, true); $parent = $category->id; } else if ($category = $DB->get_record('question_categories', array('name' => $catname, 'contextid' => $context->id, 'parent' => $parent))) { + // Do nothing unless the child category appears before the parent category + // in the imported xml file. Because the parent was created without info being available + // at that time, this allows the info to be added from the xml data. + if ($key == (count($catnames) - 1) && $lastcategoryinfo && $lastcategoryinfo->info !== null && + $lastcategoryinfo->info !== "" && $category->info == "") { + $category->info = $lastcategoryinfo->info; + if ($lastcategoryinfo->infoformat !== null && $lastcategoryinfo->infoformat !== "") { + $category->infoformat = $lastcategoryinfo->infoformat; + } + $DB->update_record('question_categories', $category); + } $parent = $category->id; } else { if ($catname == 'top') { // Should not happen, but if it does just move on. // Occurs when there has been some import/export that has created // multiple nested 'top' categories (due to old bug solved by MDL-63165). - // Not throwing an error here helps clean up old errors (silently). + // This basically silently cleans up old errors. Not throwing an exception here. continue; } require_capability('moodle/question:managecategory', $context); - // create the new category + // Create the new category. This will create all the categories in the catpath, + // though only the final category will have any info added if available. $category = new stdClass(); $category->contextid = $context->id; $category->name = $catname; $category->info = ''; + // Only add info (category description) for the final category in the catpath. + if ($key == (count($catnames) - 1) && $lastcategoryinfo && $lastcategoryinfo->info !== null && + $lastcategoryinfo->info !== "") { + $category->info = $lastcategoryinfo->info; + if ($lastcategoryinfo->infoformat !== null && $lastcategoryinfo->infoformat !== "") { + $category->infoformat = $lastcategoryinfo->infoformat; + } + } $category->parent = $parent; $category->sortorder = 999; $category->stamp = make_unique_id_code(); @@ -832,14 +871,6 @@ class qformat_default { // Array of categories written to file. $writtencategories = []; - foreach ($parents as $parent) { - $categoryname = $this->get_category_path($parent, $this->contexttofile); - // Create 'dummy' question for category export. - $dummyquestion = $this->create_dummy_question_representing_category($categoryname); - $expout .= $this->writequestion($dummyquestion) . "\n"; - $writtencategories[] = $parent; - } - foreach ($questions as $question) { // used by file api $contextid = $DB->get_field('question_categories', 'contextid', @@ -862,7 +893,6 @@ class qformat_default { if ($question->category != $trackcategory) { $addnewcat = true; $trackcategory = $question->category; - $categoryname = $this->get_category_path($trackcategory, $this->contexttofile); } $trackcategoryparents = question_categorylist_parents($trackcategory); // Check if we need to record empty parents categories. @@ -872,17 +902,23 @@ class qformat_default { // If parent is empty. if (!count($DB->get_records('question', array('category' => $trackcategoryparent)))) { $categoryname = $this->get_category_path($trackcategoryparent, $this->contexttofile); - // Create 'dummy' question for parent category. - $dummyquestion = $this->create_dummy_question_representing_category($categoryname); - $expout .= $this->writequestion($dummyquestion) . "\n"; - $writtencategories[] = $trackcategoryparent; + $categoryinfo = $DB->get_record('question_categories', array('id' => $trackcategoryparent), + 'name, info, infoformat', MUST_EXIST); + if ($categoryinfo->name != 'top') { + // Create 'dummy' question for parent category. + $dummyquestion = $this->create_dummy_question_representing_category($categoryname, $categoryinfo); + $expout .= $this->writequestion($dummyquestion) . "\n"; + $writtencategories[] = $trackcategoryparent; + } } } } if ($addnewcat && !in_array($trackcategory, $writtencategories)) { $categoryname = $this->get_category_path($trackcategory, $this->contexttofile); + $categoryinfo = $DB->get_record('question_categories', array('id' => $trackcategory), + 'info, infoformat', MUST_EXIST); // Create 'dummy' question for category. - $dummyquestion = $this->create_dummy_question_representing_category($categoryname); + $dummyquestion = $this->create_dummy_question_representing_category($categoryname, $categoryinfo); $expout .= $this->writequestion($dummyquestion) . "\n"; $writtencategories[] = $trackcategory; } @@ -913,15 +949,18 @@ class qformat_default { /** * Create 'dummy' question for category export. * @param string $categoryname the name of the category + * @param object $categoryinfo description of the category * @return stdClass 'dummy' question for category */ - protected function create_dummy_question_representing_category(string $categoryname) { + protected function create_dummy_question_representing_category(string $categoryname, $categoryinfo) { $dummyquestion = new stdClass(); $dummyquestion->qtype = 'category'; $dummyquestion->category = $categoryname; $dummyquestion->id = 0; $dummyquestion->questiontextformat = ''; $dummyquestion->contextid = 0; + $dummyquestion->info = $categoryinfo->info; + $dummyquestion->infoformat = $categoryinfo->infoformat; $dummyquestion->name = 'Switch category to ' . $categoryname; return $dummyquestion; } diff --git a/question/format/gift/tests/behat/import_export.feature b/question/format/gift/tests/behat/import_export.feature index 720ef49cfb7..2817dc46fda 100644 --- a/question/format/gift/tests/behat/import_export.feature +++ b/question/format/gift/tests/behat/import_export.feature @@ -33,4 +33,4 @@ Feature: Test importing questions from GIFT format. And I follow "Export" And I set the field "id_format_gift" to "1" And I press "Export questions to file" - And following "click here" should download between "1650" and "1800" bytes + And following "click here" should download between "1600" and "1800" bytes diff --git a/question/format/upgrade.txt b/question/format/upgrade.txt index 48dd14cebbb..70760e6b8a6 100644 --- a/question/format/upgrade.txt +++ b/question/format/upgrade.txt @@ -1,5 +1,20 @@ This files describes API changes for question import/export format plugins. +=== 3.6 === + +* Saving question category descriptions (info) is now supported in Moodle XML import/export format. +New xml-structure snippet for a question category: + + + ${$contexttypename}$/{$category_path} + + + {$info_categorydescription} + + +* The method importprocess() in question/format.php no longer accepts $category as a parameter. +If required in a plugin then please override this method. + === 2.3 === * This plugin type now supports cron in the standard way. If required, Create a diff --git a/question/format/xml/format.php b/question/format/xml/format.php index d3b9c82ae42..4b433f50f87 100644 --- a/question/format/xml/format.php +++ b/question/format/xml/format.php @@ -63,6 +63,7 @@ class qformat_xml extends qformat_default { /** * Translate human readable format name * into internal Moodle code number + * Note the reverse function is called get_format. * @param string name format name from xml file * @return int Moodle format code */ @@ -909,12 +910,20 @@ class qformat_xml extends qformat_default { * import category. The format is: * * tom/dick/harry + * Category description * */ protected function import_category($question) { $qo = new stdClass(); $qo->qtype = 'category'; $qo->category = $this->import_text($question['#']['category'][0]['#']['text']); + $qo->info = ''; + $qo->infoformat = FORMAT_MOODLE; + if (array_key_exists('info', $question['#'])) { + $qo->info = $this->import_text($question['#']['info'][0]['#']['text']); + // The import should have the format in human readable form, so translate to machine readable format. + $qo->infoformat = $this->trans_format($question['#']['info'][0]['@']['format']); + } return $qo; } @@ -1176,10 +1185,15 @@ class qformat_xml extends qformat_default { // Categories are a special case. if ($question->qtype == 'category') { $categorypath = $this->writetext($question->category); + $categoryinfo = $this->writetext($question->info); + $infoformat = $this->format($question->infoformat); $expout .= " \n"; $expout .= " \n"; - $expout .= " {$categorypath}\n"; + $expout .= " {$categorypath}"; $expout .= " \n"; + $expout .= " \n"; + $expout .= " {$categoryinfo}"; + $expout .= " \n"; $expout .= " \n"; return $expout; } diff --git a/question/format/xml/tests/fixtures/categories_reverse_order.xml b/question/format/xml/tests/fixtures/categories_reverse_order.xml new file mode 100644 index 00000000000..77e003287de --- /dev/null +++ b/question/format/xml/tests/fixtures/categories_reverse_order.xml @@ -0,0 +1,84 @@ + + + + + + $course$/Sigma/Tau + + + This is Tau category for test + + + + + + + Tau Question + + + Testing Tau Question + + + + + 1.0000000 + 0.0000000 + 0 + editor + 1 + 15 + 0 + 0 + + + + + + + + + + + + $course$/Sigma + + + This is Sigma category for test + + + + + + + Sigma Question + + + Testing Sigma Question + + + + + 1.0000000 + 0.3333333 + 0 + 0 + + yes + + + + + + no + + + + + + may be + + + + + + diff --git a/question/format/xml/tests/fixtures/category_with_description.xml b/question/format/xml/tests/fixtures/category_with_description.xml new file mode 100644 index 00000000000..5c8ff8331af --- /dev/null +++ b/question/format/xml/tests/fixtures/category_with_description.xml @@ -0,0 +1,40 @@ + + + + + + $course$/Alpha + + + This is Alpha category for test + + + + + + + Alpha Question + + + Testing Alpha Question

]]>
+
+ + + + 1.0000000 + 1.0000000 + 0 + + true + + + + + + false + + + + +
+
diff --git a/question/format/xml/tests/fixtures/export_category.xml b/question/format/xml/tests/fixtures/export_category.xml new file mode 100644 index 00000000000..66e6584459d --- /dev/null +++ b/question/format/xml/tests/fixtures/export_category.xml @@ -0,0 +1,41 @@ + + + + + + $course$/Alpha + + + + This is Alpha category for test + + + + + + + Alpha Question + + + Testing Alpha Question

]]>
+
+ + + + 1.0000000 + 1.0000000 + 0 + + true + + + + + + false + + + + +
+
diff --git a/question/format/xml/tests/fixtures/nested_categories.xml b/question/format/xml/tests/fixtures/nested_categories.xml new file mode 100644 index 00000000000..bf4df7d41dc --- /dev/null +++ b/question/format/xml/tests/fixtures/nested_categories.xml @@ -0,0 +1,60 @@ + + + + + + $course$/Delta + + + This is Delta category for test + + + + + + + $course$/Delta/Epsilon + + + This is Epsilon category for test + + + + + + + $course$/Delta/Epsilon/Zeta + + + This is Zeta category for test + + + + + + + Zeta Question + + + Testing Zeta Question

]]>
+
+ + + + 1.0000000 + 1.0000000 + 0 + + true + + + + + + false + + + + +
+
diff --git a/question/format/xml/tests/fixtures/nested_categories_with_questions.xml b/question/format/xml/tests/fixtures/nested_categories_with_questions.xml new file mode 100644 index 00000000000..80920e3bda4 --- /dev/null +++ b/question/format/xml/tests/fixtures/nested_categories_with_questions.xml @@ -0,0 +1,181 @@ + + + + + + $course$/Iota + + + This is Iota category for test + + + + + + + Iota Question + + + Testing Iota Question

]]>
+
+ + + + 1.0000000 + 1.0000000 + 0 + + true + + + + + + false + + + + +
+ + + + + $course$/Iota/Kappa + + + This is Kappa category for test + + + + + + + Kappa Essay Question + + + Testing Kappa Essay Question + + + + + 1.0000000 + 0.0000000 + 0 + editor + 1 + 10 + 0 + 0 + + + + + + + + + + + + Kappa Question + + + Testing Kappa Question

]]>
+
+ + + + 1.0000000 + 1.0000000 + 0 + + true + + + + + + false + + + + +
+ + + + + $course$/Iota/Kappa/Lambda + + + This is Lambda category for test + + + + + + + Lambda Question + + + Testing Lambda Question

]]>
+
+ + + + 1.0000000 + 1.0000000 + 0 + + true + + + + + + false + + + + +
+ + + + + $course$/Iota/Mu + + + This is Mu category for test + + + + + + + Mu Question + + + Testing Mu Question

]]>
+
+ + + + 1.0000000 + 1.0000000 + 0 + + true + + + + + + false + + + + +
+
diff --git a/question/format/xml/tests/fixtures/old_format_file.xml b/question/format/xml/tests/fixtures/old_format_file.xml new file mode 100644 index 00000000000..efc900702b3 --- /dev/null +++ b/question/format/xml/tests/fixtures/old_format_file.xml @@ -0,0 +1,72 @@ + + + + + + $course$/Pi + + + + + + + Pi Question + + + Testing Pi Question

]]>
+
+ + + + 1.0000000 + 1.0000000 + 0 + + true + + + + + + false + + + + +
+ + + + + $course$/Pi/Rho + + + + + + + Rho Question + + + Testing Rho Question

]]>
+
+ + + + 1.0000000 + 1.0000000 + 0 + + true + + + + + + false + + + + +
+
diff --git a/question/format/xml/tests/qformat_xml_import_export_test.php b/question/format/xml/tests/qformat_xml_import_export_test.php new file mode 100644 index 00000000000..e8890d284e9 --- /dev/null +++ b/question/format/xml/tests/qformat_xml_import_export_test.php @@ -0,0 +1,427 @@ +. +/** + * Unit tests for export/import description (info) for question category in the Moodle XML format. + * + * @package qformat_xml + * @copyright 2014 Nikita Nikitsky, Volgograd State Technical University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/question/format/xml/format.php'); +require_once($CFG->dirroot . '/question/format.php'); +require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); +require_once($CFG->dirroot . '/question/editlib.php'); + +/** + * Unit tests for the XML question format import and export. + * + * @copyright 2014 Nikita Nikitsky, Volgograd State Technical University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qformat_xml_import_export_test extends advanced_testcase { + /** + * Create object qformat_xml for test. + * @param string $filename with name for testing file. + * @param object $course + * @return object qformat_xml. + */ + public function create_qformat($filename, $course) { + global $DB; + $qformat = new qformat_xml(); + $contexts = $DB->get_records('context'); + $importfile = __DIR__ . '/fixtures/' .$filename; + $realfilename = $filename; + $qformat->setContexts($contexts); + $qformat->setCourse($course); + $qformat->setFilename($importfile); + $qformat->setRealfilename($realfilename); + $qformat->setMatchgrades('error'); + $qformat->setCatfromfile(1); + $qformat->setContextfromfile(1); + $qformat->setStoponerror(1); + $qformat->setCattofile(1); + $qformat->setContexttofile(1); + $qformat->set_display_progress(false); + + return $qformat; + } + /** + * Check xml for compliance. + * @param string $expectedxml with correct string. + * @param string $xml you want to check. + */ + public function assert_same_xml($expectedxml, $xml) { + $this->assertEquals(preg_replace('/( +)/', "", str_replace("\n", "", + str_replace("\r\n", "\n", str_replace("\t", "\n", $expectedxml)))), + preg_replace('/( +)/', "", str_replace("\n", "", + str_replace( "\r\n", "\n", str_replace( "\t", "\n", $xml))))); + } + + /** + * Check xml for compliance. + * @param string $expectedxml with correct string. + * @param string $xml you want to check. + */ + public function assert_same_xml_random_category($expectedxml, $xml) { + $str1 = preg_replace('/( +)/', "", + str_replace("\n", "", str_replace("\r\n", "\n", + str_replace("\t", "\n", $expectedxml)))); + + $str2 = preg_replace('/( +)/', "", str_replace("\n", "", + str_replace( "\r\n", "\n", str_replace( "\t", "\n", $xml)))); + + $str1 = str_replace("unknownhost+" + '/[0-9]+/' + "+", "", $str1); + $this->assertEquals($str1, $str2); + } + + /** + * Check imported category. + * @param string $name imported category name. + * @param string $info imported category info field (description of category). + * @param int $infoformat imported category info field format. + */ + public function assert_category_imported($name, $info, $infoformat) { + global $DB; + $category = $DB->get_record('question_categories', ['name' => $name], '*', MUST_EXIST); + $this->assertEquals($info, $category->info); + $this->assertEquals($infoformat, $category->infoformat); + } + + /** + * Check a question category has a given parent. + * @param string $catname Name of the question category + * @param string $parentname Name of the parent category + * @throws dml_exception + */ + public function assert_category_has_parent($catname, $parentname) { + global $DB; + $sql = 'SELECT qc1.* + FROM {question_categories} qc1 + JOIN {question_categories} qc2 ON qc1.parent = qc2.id + WHERE qc1.name = ? + AND qc2.name = ?'; + $categories = $DB->get_records_sql($sql, [$catname, $parentname]); + $this->assertTrue(count($categories) == 1); + } + + /** + * Check a question exists in a category. + * @param string $qname The name of the question + * @param string $catname The name of the category + * @throws dml_exception + */ + public function assert_question_in_category($qname, $catname) { + global $DB; + $question = $DB->get_record('question', ['name' => $qname], '*', MUST_EXIST); + $category = $DB->get_record('question_categories', ['name' => $catname], '*', MUST_EXIST); + $this->assertEquals($category->id, $question->category); + } + + /** + * Simple check for importing a category with a description. + */ + public function test_import_category() { + $this->resetAfterTest(true); + $course = $this->getDataGenerator()->create_course(); + $this->setAdminUser(); + $qformat = $this->create_qformat('category_with_description.xml', $course); + $imported = $qformat->importprocess(); + $this->assertTrue($imported); + $this->assert_category_imported('Alpha', 'This is Alpha category for test', FORMAT_MOODLE); + $this->assert_category_has_parent('Alpha', 'top'); + } + + /** + * Check importing nested categories. + */ + public function test_import_nested_categories() { + $this->resetAfterTest(true); + $course = $this->getDataGenerator()->create_course(); + $this->setAdminUser(); + $qformat = $this->create_qformat('nested_categories.xml', $course); + $imported = $qformat->importprocess(); + $this->assertTrue($imported); + $this->assert_category_imported('Delta', 'This is Delta category for test', FORMAT_PLAIN); + $this->assert_category_imported('Epsilon', 'This is Epsilon category for test', FORMAT_MARKDOWN); + $this->assert_category_imported('Zeta', 'This is Zeta category for test', FORMAT_MOODLE); + $this->assert_category_has_parent('Delta', 'top'); + $this->assert_category_has_parent('Epsilon', 'Delta'); + $this->assert_category_has_parent('Zeta', 'Epsilon'); + } + + /** + * Check importing nested categories contain the right questions. + */ + public function test_import_nested_categories_with_questions() { + $this->resetAfterTest(true); + $course = $this->getDataGenerator()->create_course(); + $this->setAdminUser(); + $qformat = $this->create_qformat('nested_categories_with_questions.xml', $course); + $imported = $qformat->importprocess(); + $this->assertTrue($imported); + $this->assert_category_imported('Iota', 'This is Iota category for test', FORMAT_PLAIN); + $this->assert_category_imported('Kappa', 'This is Kappa category for test', FORMAT_MARKDOWN); + $this->assert_category_imported('Lambda', 'This is Lambda category for test', FORMAT_MOODLE); + $this->assert_category_imported('Mu', 'This is Mu category for test', FORMAT_MOODLE); + $this->assert_question_in_category('Iota Question', 'Iota'); + $this->assert_question_in_category('Kappa Question', 'Kappa'); + $this->assert_question_in_category('Lambda Question', 'Lambda'); + $this->assert_question_in_category('Mu Question', 'Mu'); + $this->assert_category_has_parent('Iota', 'top'); + $this->assert_category_has_parent('Kappa', 'Iota'); + $this->assert_category_has_parent('Lambda', 'Kappa'); + $this->assert_category_has_parent('Mu', 'Iota'); + } + + /** + * Check import of an old file (without format), for backward compatability. + */ + public function test_import_old_format() { + $this->resetAfterTest(true); + $course = $this->getDataGenerator()->create_course(); + $this->setAdminUser(); + $qformat = $this->create_qformat('old_format_file.xml', $course); + $imported = $qformat->importprocess(); + $this->assertTrue($imported); + $this->assert_category_imported('Pi', '', FORMAT_MOODLE); + $this->assert_category_imported('Rho', '', FORMAT_MOODLE); + $this->assert_question_in_category('Pi Question', 'Pi'); + $this->assert_question_in_category('Rho Question', 'Rho'); + $this->assert_category_has_parent('Pi', 'top'); + $this->assert_category_has_parent('Rho', 'Pi'); + } + + /** + * Check the import of an xml file where the child category exists before the parent category. + */ + public function test_import_categories_in_reverse_order() { + $this->resetAfterTest(true); + $course = $this->getDataGenerator()->create_course(); + $this->setAdminUser(); + $qformat = $this->create_qformat('categories_reverse_order.xml', $course); + $imported = $qformat->importprocess(); + $this->assertTrue($imported); + $this->assert_category_imported('Sigma', 'This is Sigma category for test', FORMAT_HTML); + $this->assert_category_imported('Tau', 'This is Tau category for test', FORMAT_HTML); + $this->assert_question_in_category('Sigma Question', 'Sigma'); + $this->assert_question_in_category('Tau Question', 'Tau'); + $this->assert_category_has_parent('Sigma', 'top'); + $this->assert_category_has_parent('Tau', 'Sigma'); + } + + /** + * Simple check for exporting a category. + */ + public function test_export_category() { + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $this->resetAfterTest(true); + $course = $this->getDataGenerator()->create_course(); + $this->setAdminUser(); + // Note while this loads $qformat with all the 'right' data from the xml file, + // the call to setCategory, followed by exportprocess will actually only export data + // from the database (created by the generator). + $qformat = $this->create_qformat('export_category.xml', $course); + + $category = $generator->create_question_category([ + 'name' => 'Alpha', + 'contextid' => '2', + 'info' => 'This is Alpha category for test', + 'infoformat' => '0', + 'stamp' => make_unique_id_code(), + 'parent' => '0', + 'sortorder' => '999']); + $question = $generator->create_question('truefalse', null, [ + 'category' => $category->id, + 'name' => 'AlphaQuestion', + 'questiontext' => ['format' => '1', 'text' => '

TestingAlphaQuestion

'], + 'generalfeedback' => ['format' => '1', 'text' => ''], + 'correctanswer' => '1', + 'feedbacktrue' => ['format' => '1', 'text' => ''], + 'feedbackfalse' => ['format' => '1', 'text' => ''], + 'penalty' => '1']); + $qformat->setCategory($category); + + $xml = preg_replace('/()/', '', $qformat->exportprocess()); + $file = preg_replace('/()/', '', + file_get_contents(__DIR__ . '/fixtures/export_category.xml')); + $this->assert_same_xml($file, $xml); + } + + /** + * Check exporting nested categories. + */ + public function test_export_nested_categories() { + $this->resetAfterTest(true); + $course = $this->getDataGenerator()->create_course(); + $this->setAdminUser(); + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $qformat = $this->create_qformat('nested_categories.zml', $course); + + $categorydelta = $generator->create_question_category([ + 'name' => 'Delta', + 'contextid' => '2', + 'info' => 'This is Delta category for test', + 'infoformat' => '2', + 'stamp' => make_unique_id_code(), + 'parent' => '0', + 'sortorder' => '999']); + $categoryepsilon = $generator->create_question_category([ + 'name' => 'Epsilon', + 'contextid' => '2', + 'info' => 'This is Epsilon category for test', + 'infoformat' => '4', + 'stamp' => make_unique_id_code(), + 'parent' => $categorydelta->id, + 'sortorder' => '999']); + $categoryzeta = $generator->create_question_category([ + 'name' => 'Zeta', + 'contextid' => '2', + 'info' => 'This is Zeta category for test', + 'infoformat' => '0', + 'stamp' => make_unique_id_code(), + 'parent' => $categoryepsilon->id, + 'sortorder' => '999']); + $question = $generator->create_question('truefalse', null, [ + 'category' => $categoryzeta->id, + 'name' => 'Zeta Question', + 'questiontext' => [ + 'format' => '1', + 'text' => '

Testing Zeta Question

'], + 'generalfeedback' => ['format' => '1', 'text' => ''], + 'correctanswer' => '1', + 'feedbacktrue' => ['format' => '1', 'text' => ''], + 'feedbackfalse' => ['format' => '1', 'text' => ''], + 'penalty' => '1']); + $qformat->setCategory($categorydelta); + $qformat->setCategory($categoryepsilon); + $qformat->setCategory($categoryzeta); + + $xml = preg_replace('/()/', '', $qformat->exportprocess()); + $file = preg_replace('/()/', '', + file_get_contents(__DIR__ . '/fixtures/nested_categories.xml')); + $this->assert_same_xml($file, $xml); + } + + /** + * Check exporting nested categories contain the right questions. + */ + public function test_export_nested_categories_with_questions() { + $this->resetAfterTest(true); + $course = $this->getDataGenerator()->create_course(); + $this->setAdminUser(); + $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $qformat = $this->create_qformat('nested_categories_with_questions.xml', $course); + + $categoryiota = $generator->create_question_category([ + 'name' => 'Iota', + 'contextid' => '2', + 'info' => 'This is Iota category for test', + 'infoformat' => '2', + 'stamp' => make_unique_id_code(), + 'parent' => '0', + 'sortorder' => '999']); + $iotaquestion = $generator->create_question('truefalse', null, [ + 'category' => $categoryiota->id, + 'name' => 'Iota Question', + 'questiontext' => [ + 'format' => '1', + 'text' => '

Testing Iota Question

'], + 'generalfeedback' => ['format' => '1', 'text' => ''], + 'correctanswer' => '1', + 'feedbacktrue' => ['format' => '1', 'text' => ''], + 'feedbackfalse' => ['format' => '1', 'text' => ''], + 'penalty' => '1']); + $categorykappa = $generator->create_question_category([ + 'name' => 'Kappa', + 'contextid' => '2', + 'info' => 'This is Kappa category for test', + 'infoformat' => '4', + 'stamp' => make_unique_id_code(), + 'parent' => $categoryiota->id, + 'sortorder' => '999']); + $kappaquestion = $generator->create_question('essay', null, [ + 'category' => $categorykappa->id, + 'name' => 'Kappa Essay Question', + 'questiontext' => ['text' => 'Testing Kappa Essay Question'], + 'generalfeedback' => '', + 'responseformat' => 'editor', + 'responserequired' => 1, + 'responsefieldlines' => 10, + 'attachments' => 0, + 'attachmentsrequired' => 0, + 'graderinfo' => ['format' => '1', 'text' => ''], + 'responsetemplate' => ['format' => '1', 'text' => ''], + ]); + $kappaquestion1 = $generator->create_question('truefalse', null, [ + 'category' => $categorykappa->id, + 'name' => 'Kappa Question', + 'questiontext' => [ + 'format' => '1', + 'text' => '

Testing Kappa Question

'], + 'generalfeedback' => ['format' => '1', 'text' => ''], + 'correctanswer' => '1', + 'feedbacktrue' => ['format' => '1', 'text' => ''], + 'feedbackfalse' => ['format' => '1', 'text' => ''], + 'penalty' => '1']); + $categorylambda = $generator->create_question_category([ + 'name' => 'Lambda', + 'contextid' => '2', + 'info' => 'This is Lambda category for test', + 'infoformat' => '0', + 'stamp' => make_unique_id_code(), + 'parent' => $categorykappa->id, + 'sortorder' => '999']); + $lambdaquestion = $generator->create_question('truefalse', null, [ + 'category' => $categorylambda->id, + 'name' => 'Lambda Question', + 'questiontext' => [ + 'format' => '1', + 'text' => '

Testing Lambda Question

'], + 'generalfeedback' => ['format' => '1', 'text' => ''], + 'correctanswer' => '1', + 'feedbacktrue' => ['format' => '1', 'text' => ''], + 'feedbackfalse' => ['format' => '1', 'text' => ''], + 'penalty' => '1']); + $categorymu = $generator->create_question_category([ + 'name' => 'Mu', + 'contextid' => '2', + 'info' => 'This is Mu category for test', + 'infoformat' => '0', + 'stamp' => make_unique_id_code(), + 'parent' => $categoryiota->id, + 'sortorder' => '999']); + $muquestion = $generator->create_question('truefalse', null, [ + 'category' => $categorymu->id, + 'name' => 'Mu Question', + 'questiontext' => [ + 'format' => '1', + 'text' => '

Testing Mu Question

'], + 'generalfeedback' => ['format' => '1', 'text' => ''], + 'correctanswer' => '1', + 'feedbacktrue' => ['format' => '1', 'text' => ''], + 'feedbackfalse' => ['format' => '1', 'text' => ''], + 'penalty' => '1']); + $qformat->setCategory($categoryiota); + + $xml = preg_replace('/()/', '', $qformat->exportprocess()); + $file = preg_replace('/()/', '', + file_get_contents(__DIR__ . '/fixtures/nested_categories_with_questions.xml')); + $this->assert_same_xml($file, $xml); + } +} diff --git a/question/format/xml/tests/xmlformat_test.php b/question/format/xml/tests/xmlformat_test.php index 5b923a0d48f..6732cbc9545 100644 --- a/question/format/xml/tests/xmlformat_test.php +++ b/question/format/xml/tests/xmlformat_test.php @@ -1583,4 +1583,40 @@ END; $this->assertEquals('/myfolder/', $file->filepath); $this->assertEquals(6, $file->size); } + + public function test_create_dummy_question() { + + $testobject = new mock_qformat_xml(); + $categoryname = 'name1'; + $categoryinfo = new stdClass(); + $categoryinfo->info = 'info1'; + $categoryinfo->infoformat = 'infoformat1'; + $dummyquestion = $testobject->mock_create_dummy_question_representing_category($categoryname, $categoryinfo); + + $this->assertEquals('category', $dummyquestion->qtype); + $this->assertEquals($categoryname, $dummyquestion->category); + $this->assertEquals($categoryinfo->info, $dummyquestion->info); + $this->assertEquals($categoryinfo->infoformat, $dummyquestion->infoformat); + $this->assertEquals('Switch category to ' . $categoryname, $dummyquestion->name); + $this->assertEquals(0, $dummyquestion->id); + $this->assertEquals('', $dummyquestion->questiontextformat); + $this->assertEquals(0, $dummyquestion->contextid); + } +} + +/** + * Class mock_qformat_xml exists only to enable testing of the create dummy question category. + * @package qformat_xml + * @copyright 2018 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mock_qformat_xml extends qformat_xml { + /** + * Make public an otherwise protected function. + * @param string $categoryname the name of the category + * @param object $categoryinfo description of the category + */ + public function mock_create_dummy_question_representing_category(string $categoryname, $categoryinfo) { + return $this->create_dummy_question_representing_category($categoryname, $categoryinfo); + } }