MDL-29644 qtype multianswer, hints lost doing Moodle XML export.

This commit also includes a lot of useful tidying up of some of the unit
test helper code. I turned out that I could not use all the helper code
in my new tests, but despite that it will be useful in the future, so I
am committing it here.
This commit is contained in:
Tim Hunt
2011-10-04 22:28:35 +01:00
parent 22ba72591c
commit 0bfc2dde8f
5 changed files with 442 additions and 103 deletions
+57 -92
View File
@@ -408,34 +408,38 @@ class qformat_xml extends qformat_default {
* @param array question question array from xml tree
* @return object question object
*/
public function import_multianswer($questions) {
public function import_multianswer($question) {
question_bank::get_qtype('multianswer');
$questiontext = array();
$questiontext['text'] = $this->import_text($questions['#']['questiontext'][0]['#']['text']);
$questiontext['text'] = $this->import_text($question['#']['questiontext'][0]['#']['text']);
$questiontext['format'] = '1';
$questiontext['itemid'] = '';
$qo = qtype_multianswer_extract_question($questiontext);
// 'header' parts particular to multianswer
$qo->qtype = MULTIANSWER;
$qo->qtype = 'multianswer';
$qo->course = $this->course;
$qo->generalfeedback = '';
// restore files in generalfeedback
$qo->generalfeedback = $this->getpath($questions,
$qo->generalfeedback = $this->getpath($question,
array('#', 'generalfeedback', 0, '#', 'text', 0, '#'), $qo->generalfeedback, true);
$qo->generalfeedbackformat = $this->trans_format($this->getpath($questions,
$qo->generalfeedbackformat = $this->trans_format($this->getpath($question,
array('#', 'generalfeedback', 0, '@', 'format'), 'moodle_auto_format'));
$qo->generalfeedbackfiles = $this->import_files($this->getpath($questions,
$qo->generalfeedbackfiles = $this->import_files($this->getpath($question,
array('#', 'generalfeedback', 0, '#', 'file'), array(), false));
if (!empty($questions)) {
$qo->name = $this->import_text($questions['#']['name'][0]['#']['text']);
}
$qo->questiontext = $qo->questiontext['text'];
$qo->name = $this->import_text($question['#']['name'][0]['#']['text']);
$qo->questiontext = $qo->questiontext['text'];
$qo->questiontextformat = '';
$this->import_hints($qo, $questions, true);
$qo->penalty = $this->getpath($question,
array('#', 'penalty', 0, '#'), $this->defaultquestion()->penalty);
// Fix problematic rounding from old files:
if (abs($qo->penalty - 0.3333333) < 0.005) {
$qo->penalty = 0.3333333;
}
$this->import_hints($qo, $question);
return $qo;
}
@@ -633,12 +637,12 @@ class qformat_xml extends qformat_default {
* @param array question question array from xml tree
* @return object question object
*/
public function import_matching($question) {
public function import_match($question) {
// get common parts
$qo = $this->import_headers($question);
// header parts particular to matching
$qo->qtype = MATCH;
$qo->qtype = 'match';
$qo->shuffleanswers = $this->trans_single($this->getpath($question,
array('#', 'shuffleanswers', 0, '#'), 1));
@@ -900,9 +904,9 @@ class qformat_xml extends qformat_default {
$qo = $this->import_numerical($question);
} else if ($questiontype == 'description') {
$qo = $this->import_description($question);
} else if ($questiontype == 'matching') {
$qo = $this->import_matching($question);
} else if ($questiontype == 'cloze') {
} else if ($questiontype == 'matching' || $questiontype == 'match') {
$qo = $this->import_match($question);
} else if ($questiontype == 'cloze' || $questiontype == 'multianswer') {
$qo = $this->import_multianswer($question);
} else if ($questiontype == 'essay') {
$qo = $this->import_essay($question);
@@ -942,34 +946,21 @@ class qformat_xml extends qformat_default {
}
/**
* Turn the internal question code into a human readable form
* (The code used to be numeric, but this remains as some of
* the names don't match the new internal format)
* @param mixed $typeid Internal code
* @return string question type string
* Turn the internal question type name into a human readable form.
* (In the past, the code used to use integers internally. Now, it uses
* strings, so there is less need for this, but to maintain
* backwards-compatibility we change two of the type names.)
* @param string $qtype question type plugin name.
* @return string $qtype string to use in the file.
*/
protected function get_qtype($typeid) {
switch($typeid) {
case TRUEFALSE:
return 'truefalse';
case MULTICHOICE:
return 'multichoice';
case SHORTANSWER:
return 'shortanswer';
case NUMERICAL:
return 'numerical';
case MATCH:
protected function get_qtype($qtype) {
switch($qtype) {
case 'match':
return 'matching';
case DESCRIPTION:
return 'description';
case MULTIANSWER:
case 'multianswer':
return 'cloze';
case ESSAY:
return 'essay';
case CALCULATED:
return 'calculated';
default:
return false;
return $qtype;
}
}
@@ -1085,13 +1076,9 @@ class qformat_xml extends qformat_default {
$expout .= "<!-- question: $question->id -->\n";
// Check question type
if (!$questiontype = $this->get_qtype($question->qtype)) {
// must be a plugin then, so just accept the name supplied
$questiontype = $question->qtype;
}
$questiontype = $this->get_qtype($question->qtype);
// add opening tag
// generates specific header for Cloze and category type question
// Categories are a special case.
if ($question->qtype == 'category') {
$categorypath = $this->writetext($question->category);
$expout .= " <question type=\"category\">\n";
@@ -1100,47 +1087,29 @@ class qformat_xml extends qformat_default {
$expout .= " </category>\n";
$expout .= " </question>\n";
return $expout;
} else if ($question->qtype != MULTIANSWER) {
// for all question types except Close
$name_text = $this->writetext($question->name, 3);
$expout .= " <question type=\"$questiontype\">\n";
$expout .= " <name>\n";
$expout .= $name_text;
$expout .= " </name>\n";
$expout .= " <questiontext {$this->format($question->questiontextformat)}>\n";
$expout .= $this->writetext($question->questiontext, 3);
$expout .= $this->writefiles($question->questiontextfiles);
$expout .= " </questiontext>\n";
$expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
$expout .= $this->writetext($question->generalfeedback, 3);
$expout .= $this->writefiles($question->generalfeedbackfiles);
$expout .= " </generalfeedback>\n";
$expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
$expout .= " <penalty>{$question->penalty}</penalty>\n";
$expout .= " <hidden>{$question->hidden}</hidden>\n";
} else {
// for Cloze type only
$name_text = $this->writetext($question->name);
$question_text = $this->writetext($question->questiontext);
$generalfeedback = $this->writetext($question->generalfeedback);
$expout .= " <question type=\"$questiontype\">\n";
$expout .= " <name>\n";
$expout .= $name_text;
$expout .= " </name>\n";
$expout .= " <questiontext>\n";
$expout .= $this->writetext($question->questiontext, 3);
$expout .= $this->writefiles($question->questiontextfiles);
$expout .= " </questiontext>\n";
$expout .= " <generalfeedback>\n";
$expout .= $this->writetext($question->generalfeedback, 3);
$expout .= $this->writefiles($question->generalfeedbackfiles);
$expout .= " </generalfeedback>\n";
}
// output depends on question type
// Now we know we are are handing a real question.
// Output the generic information.
$expout .= " <question type=\"$questiontype\">\n";
$expout .= " <name>\n";
$expout .= $this->writetext($question->name, 3);
$expout .= " </name>\n";
$expout .= " <questiontext {$this->format($question->questiontextformat)}>\n";
$expout .= $this->writetext($question->questiontext, 3);
$expout .= $this->writefiles($question->questiontextfiles);
$expout .= " </questiontext>\n";
$expout .= " <generalfeedback {$this->format($question->generalfeedbackformat)}>\n";
$expout .= $this->writetext($question->generalfeedback, 3);
$expout .= $this->writefiles($question->generalfeedbackfiles);
$expout .= " </generalfeedback>\n";
if ($question->qtype != 'multianswer') {
$expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
}
$expout .= " <penalty>{$question->penalty}</penalty>\n";
$expout .= " <hidden>{$question->hidden}</hidden>\n";
// The rest of the output depends on question type.
switch($question->qtype) {
case 'category':
// not a qtype really - dummy used for category switching
@@ -1238,12 +1207,8 @@ class qformat_xml extends qformat_default {
break;
case 'multianswer':
$acount = 1;
foreach ($question->options->questions as $question) {
$thispattern = "{#".$acount."}";
$thisreplace = $question->questiontext;
$expout = preg_replace("~$thispattern~", $thisreplace, $expout);
$acount++;
foreach ($question->options->questions as $index => $subq) {
$expout = preg_replace('~{#' . $index . '}~', $subq->questiontext, $expout);
}
break;
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/format/xml/format.php');
require_once($CFG->dirroot . '/question/engine/simpletest/helpers.php');
/**
@@ -64,6 +65,52 @@ class qformat_xml_test extends UnitTestCase {
return $q;
}
/**
* The data the XML import format sends to save_question is not exactly
* the same as the data returned from the editing form, so this method
* makes necessary changes to the return value of
* test_question_maker::get_question_form_data so that the tests can work.
* @param object $expectedq as returned by get_question_form_data.
* @return object one more likely to match the return value of import_...().
*/
public function remove_irrelevant_form_data_fields($expectedq) {
return $this->itemid_to_files($expectedq);
}
/**
* Becuase XML import uses a files array instead of an itemid integer to
* handle saving files with a question, we need to covert the output of
* test_question_maker::get_question_form_data to match. This method recursively
* replaces all array elements with key itemid with an array entry with
* key files and value an empty array.
*
* @param mixed $var any data structure.
* @return mixed an equivalent structure with the relacements made.
*/
protected function itemid_to_files($var) {
if (is_object($var)) {
$newvar = new stdClass();
foreach(get_object_vars($var) as $field => $value) {
$newvar->$field = $this->itemid_to_files($value);
}
} else if (is_array($var)) {
$newvar = array();
foreach ($var as $index => $value) {
if ($index === 'itemid') {
$newvar['files'] = array();
} else {
$newvar[$index] = $this->itemid_to_files($value);
}
}
} else {
$newvar = $var;
}
return $newvar;
}
public function test_write_hint_basic() {
$q = $this->make_test_question();
$q->name = 'Short answer question';
@@ -468,7 +515,7 @@ END;
$xmldata = xmlize($xml);
$importer = new qformat_xml();
$q = $importer->import_matching($xmldata['question']);
$q = $importer->import_match($xmldata['question']);
$expectedq = new stdClass();
$expectedq->qtype = 'match';
@@ -1216,4 +1263,136 @@ END;
$this->assert_same_xml($expectedxml, $xml);
}
public function test_import_multianswer() {
$xml = ' <question type="cloze">
<name>
<text>Simple multianswer</text>
</name>
<questiontext format="html">
<text><![CDATA[Complete this opening line of verse: "The {1:SHORTANSWER:Dog#Wrong, silly!~=Owl#Well done!~*#Wrong answer} and the {1:MULTICHOICE:Bow-wow#You seem to have a dog obsessions!~Wiggly worm#Now you are just being ridiculous!~=Pussy-cat#Well done!} went to sea".]]></text>
</questiontext>
<generalfeedback format="html">
<text><![CDATA[General feedback: It\'s from "The Owl and the Pussy-cat" by Lear: "The owl and the pussycat went to sea".]]></text>
</generalfeedback>
<penalty>0.5</penalty>
<hidden>0</hidden>
<hint format="html">
<text>Hint 1</text>
</hint>
<hint format="html">
<text>Hint 2</text>
</hint>
</question>
';
$xmldata = xmlize($xml);
$importer = new qformat_xml();
$q = $importer->import_multianswer($xmldata['question']);
// Annoyingly, import works in a weird way (it duplicates code, rather
// than just calling save_question) so we cannot use
// test_question_maker::get_question_form_data('multianswer', 'twosubq').
$expectedqa = new stdClass();
$expectedqa->name = 'Simple multianswer';
$expectedqa->qtype = 'multianswer';
$expectedqa->questiontext = 'Complete this opening line of verse: "The {#1} and the {#2} went to sea".';
$expectedqa->generalfeedback = 'General feedback: It\'s from "The Owl and the Pussy-cat" by Lear: "The owl and the pussycat went to sea".';
$expectedqa->defaultmark = 2;
$expectedqa->penalty = 0.5;
$expectedqa->hint = array(
array('text' => 'Hint 1', 'format' => FORMAT_HTML, 'files' => array()),
array('text' => 'Hint 2', 'format' => FORMAT_HTML, 'files' => array()),
);
$sa = new stdClass();
$sa->questiontext = array('text' => '{1:SHORTANSWER:Dog#Wrong, silly!~=Owl#Well done!~*#Wrong answer}',
'format' => FORMAT_HTML, 'itemid' => null);
$sa->generalfeedback = array('text' => '', 'format' => FORMAT_HTML, 'itemid' => null);
$sa->defaultmark = 1.0;
$sa->qtype = 'shortanswer';
$sa->usecase = 0;
$sa->answer = array('Dog', 'Owl', '*');
$sa->fraction = array(0, 1, 0);
$sa->feedback = array(
array('text' => 'Wrong, silly!', 'format' => FORMAT_HTML, 'itemid' => null),
array('text' => 'Well done!', 'format' => FORMAT_HTML, 'itemid' => null),
array('text' => 'Wrong answer', 'format' => FORMAT_HTML, 'itemid' => null),
);
$mc = new stdClass();
$mc->generalfeedback = '';
$mc->questiontext = array('text' => '{1:MULTICHOICE:Bow-wow#You seem to have a dog obsessions!~' .
'Wiggly worm#Now you are just being ridiculous!~=Pussy-cat#Well done!}',
'format' => FORMAT_HTML, 'itemid' => null);
$mc->generalfeedback = array('text' => '', 'format' => FORMAT_HTML, 'itemid' => null);
$mc->defaultmark = 1.0;
$mc->qtype = 'multichoice';
$mc->layout = 0;
$mc->single = 1;
$mc->shuffleanswers = 1;
$mc->correctfeedback = array('text' => '', 'format' => FORMAT_HTML, 'itemid' => null);
$mc->partiallycorrectfeedback = array('text' => '', 'format' => FORMAT_HTML, 'itemid' => null);
$mc->incorrectfeedback = array('text' => '', 'format' => FORMAT_HTML, 'itemid' => null);
$mc->answernumbering = 0;
$mc->answer = array(
array('text' => 'Bow-wow', 'format' => FORMAT_HTML, 'itemid' => null),
array('text' => 'Wiggly worm', 'format' => FORMAT_HTML, 'itemid' => null),
array('text' => 'Pussy-cat', 'format' => FORMAT_HTML, 'itemid' => null),
);
$mc->fraction = array(0, 0, 1);
$mc->feedback = array(
array('text' => 'You seem to have a dog obsessions!', 'format' => FORMAT_HTML, 'itemid' => null),
array('text' => 'Now you are just being ridiculous!', 'format' => FORMAT_HTML, 'itemid' => null),
array('text' => 'Well done!', 'format' => FORMAT_HTML, 'itemid' => null),
);
$expectedqa->options = new stdClass();
$expectedqa->options->questions = array(
1 => $sa,
2 => $mc,
);
$this->assertEqual($expectedqa->hint, $q->hint);
$this->assertEqual($expectedqa->options->questions[1], $q->options->questions[1]);
$this->assertEqual($expectedqa->options->questions[2], $q->options->questions[2]);
$this->assert(new CheckSpecifiedFieldsExpectation($expectedqa), $q);
}
public function test_export_multianswer() {
$qdata = test_question_maker::get_question_data('multianswer', 'twosubq');
$exporter = new qformat_xml();
$xml = $exporter->writequestion($qdata);
$expectedxml = '<!-- question: 0 -->
<question type="cloze">
<name>
<text>Simple multianswer</text>
</name>
<questiontext format="html">
<text><![CDATA[Complete this opening line of verse: "The {1:SHORTANSWER:Dog#Wrong, silly!~=Owl#Well done!~*#Wrong answer} and the {1:MULTICHOICE:Bow-wow#You seem to have a dog obsessions!~Wiggly worm#Now you are just being ridiculous!~=Pussy-cat#Well done!} went to sea".]]></text>
</questiontext>
<generalfeedback format="html">
<text><![CDATA[General feedback: It\'s from "The Owl and the Pussy-cat" by Lear: "The owl and the pussycat went to sea]]></text>
</generalfeedback>
<penalty>0.3333333</penalty>
<hidden>0</hidden>
<hint format="html">
<text>Hint 1</text>
</hint>
<hint format="html">
<text>Hint 2</text>
</hint>
</question>
';
$this->assert_same_xml($expectedxml, $xml);
}
}