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);
+ }
}