/** * Helper method used by {@link export_to_xml()}. Handle the data for one question text. * @param array $xml the bit of the XML representing one question text. * @param qformat_xml $format the importer/exporter object. * @return stack_question_test the question test. */ protected function import_xml_qtest($xml, qformat_xml $format) { $number = $format->getpath($xml, array('#', 'testcase', 0, '#'), null, false, 'Missing testcase number in the XML.'); $inputs = array(); if (isset($xml['#']['testinput'])) { foreach ($xml['#']['testinput'] as $inputxml) { $name = $format->getpath($inputxml, array('#', 'name', 0, '#'), ''); $value = $format->getpath($inputxml, array('#', 'value', 0, '#'), ''); $inputs[$name] = $value; } } $testcase = new stack_question_test($inputs); if (isset($xml['#']['expected'])) { foreach ($xml['#']['expected'] as $expectedxml) { $name = $format->getpath($expectedxml, array('#', 'name', 0, '#'), ''); $expectedscore = $format->getpath($expectedxml, array('#', 'expectedscore', 0, '#'), ''); $expectedpenalty = $format->getpath($expectedxml, array('#', 'expectedpenalty', 0, '#'), ''); $expectedanswernote = $format->getpath($expectedxml, array('#', 'expectedanswernote', 0, '#'), ''); $testcase->add_expected_result($name, new stack_potentialresponse_tree_state(1, true, $expectedscore, $expectedpenalty, '', array($expectedanswernote))); } } return array($number, $testcase); }
/** * Process a single question into an array * @param SimpleXMLElement $assessmentitem * @return the question as an array */ protected function questiontoformfrom($assessmentitem) { // Set this to false to enable a range of conversions. $strictimport = true; $errors = array(); $question = new stdClass(); $question->qtype = 'stack'; $question->name = (string) $assessmentitem->MetaData->dctitle->selection; $question->variantsselectionseed = ''; $question->defaultmark = 1; $question->length = 1; // Note, new tags for [[input:ans1]] and validation are converted below, after inputs. $question->questiontext = (string) $assessmentitem->questionCasValues->questionStem->castext; $question->questiontextformat = FORMAT_HTML; // Always blank on import - we assume PRT feedback is embedded in the question. $question->specificfeedback = array('text' => '', 'format' => FORMAT_HTML, 'files' => array()); $question->generalfeedback = (string) $assessmentitem->questionCasValues->workedSolution->castext; $question->generalfeedbackformat = FORMAT_HTML; $question->questionvariables = $this->convert_keyvals((string) $assessmentitem->questionCasValues->questionVariables->rawKeyVals); $newnote = (string) $assessmentitem->questionCasValues->questionNote->castext; if (strlen($newnote) > 255) { $question->questiontext .= "Question note too long on import:\n\n" . $newnote; $question->questionnote = "ERROR on import: question note too long. See question text."; } else { $question->questionnote = $newnote; } // Question level options. $itemoptions = array(); foreach ($assessmentitem->ItemOptions->stackoption as $stackoptionxml) { $name = (string) $stackoptionxml->name; $value = $this->convert_bools((string) $stackoptionxml->selected); $itemoptions[$name] = $value; } // Not all the STACK 2 options are used. Some are thrown away. $question->questionsimplify = $itemoptions['Simplify']; // Bug in STACK 2 exporter: Penalty is not written... if (array_key_exists('Penalty', $itemoptions)) { $question->penalty = $itemoptions['Penalty']; } else { $question->penalty = 0.1; } $question->assumepositive = $itemoptions['AssumePos']; if ('(none)' == $itemoptions['MultiplicationSign']) { $itemoptions['MultiplicationSign'] = 'none'; } $question->multiplicationsign = $itemoptions['MultiplicationSign']; $question->sqrtsign = $itemoptions['SqrtSign']; $question->complexno = $itemoptions['ComplexNo']; $question->inversetrig = 'cos-1'; $question->matrixparens = '['; $question->prtcorrect = array('text' => $itemoptions['FeedbackGenericCorrect'], 'format' => FORMAT_HTML, 'files' => array()); $question->prtpartiallycorrect = array('text' => $itemoptions['FeedbackGenericPCorrect'], 'format' => FORMAT_HTML, 'files' => array()); $question->prtincorrect = array('text' => $itemoptions['FeedbackGenericIncorrect'], 'format' => FORMAT_HTML, 'files' => array()); // Input elements. $inputtypemapping = array('Algebraic Input' => 'algebraic', 'True/False' => 'boolean', 'Textarea' => 'textarea', 'Single Character' => 'singlechar', 'Matrix' => 'matrix'); $questionparts = array(); foreach ($assessmentitem->questionparts->questionpart as $questionpartxml) { $questionpart = array(); $inputtype = (string) $questionpartxml->inputType->selection; if (array_key_exists($inputtype, $inputtypemapping)) { $questionpart['type'] = $inputtypemapping[$inputtype]; } else { if ($strictimport) { $errors[] = 'Tried to set an input type named ' . $inputtype . ' for input ' . (string) $questionpartxml->name . ' in question ' . $question->name . '. This has not yet been implemented in STACK 3.'; } else { $questionpart['type'] = 'algebraic'; } } $inputoptions = array(); foreach ($questionpartxml->stackoption as $stackoptionxml) { $name = (string) $stackoptionxml->name; $value = $this->convert_bools((string) $stackoptionxml->selected); $inputoptions[$name] = $value; } $questionpart['modelans'] = (string) $questionpartxml->teachersAns->casString; if (strlen($questionpart['modelans']) > 255) { $question->questionvariables .= "\n/*Automatically added by the importer*/"; $question->questionvariables .= "\nlonganswer" . $name . ':' . $questionpart['modelans'] . "\n"; $questionpart['modelans'] = 'longanswer' . $name; } $questionpart['boxsize'] = (string) $questionpartxml->boxsize; $questionpart['insertstars'] = $inputoptions['insertStars']; $questionpart['syntaxhint'] = (string) $questionpartxml->syntax; $questionpart['forbidwords'] = (string) $questionpartxml->forbiddenWords->Forbid; $questionpart['allowwords'] = ''; $questionpart['forbidfloat'] = $inputoptions['forbidFloats']; // Export error: STACK 2 exporter does not seem to export these correctly. if (array_key_exists('lowestTerms', $inputoptions)) { $questionpart['requirelowestterms'] = (bool) $inputoptions['lowestTerms']; } else { $questionpart['requirelowestterms'] = true; } $questionpart['checkanswertype'] = $inputoptions['sameType']; $questionpart['strictsyntax'] = $inputoptions['formalSyntax']; // STACK 2 exporter does not seem to export these correctly anyway! $questionpart['mustverify'] = 1; $questionpart['showvalidation'] = 1; $questionpart['options'] = ''; $name = (string) $questionpartxml->name; $questionparts[$name] = $questionpart; } $inputnames = array(); foreach ($questionparts as $anskey => $questionpart) { $inputnames[] = $anskey; foreach ($questionpart as $key => $val) { $questionkey = $anskey . $key; $question->{$questionkey} = $val; } } // Change the input tags for the new versions. $question->questiontext = $this->convert_questiontext($question->questiontext, $inputnames); $question->specificfeedback['text'] = $this->fix_maths_delimiters($question->specificfeedback['text']); $question->generalfeedback = $this->fix_maths_delimiters($question->generalfeedback); $question->prtcorrect['text'] = $this->fix_maths_delimiters($question->prtcorrect['text']); $question->prtpartiallycorrect['text'] = $this->fix_maths_delimiters($question->prtpartiallycorrect['text']); $question->prtincorrect['text'] = $this->fix_maths_delimiters($question->prtincorrect['text']); // Potential response trees. $potentialresponsetrees = array(); foreach ($assessmentitem->PotentialResponseTrees->PotentialResponseTree as $prtxml) { $name = (string) $prtxml->prtname; // STACK adds this for export purposes, because it can't cope with PRTs which are just a number. $name = str_replace('PotResTree_', '', $name); if (strlen($name) > 32) { if ($strictimport) { $errors[] = 'The PRT name "' . $name . '" exceeds 32 characters and is too long.'; } $name = substr($name, 0, 31); } $prt = array(); $prt['value'] = (int) $prtxml->questionValue; $prt['autosimplify'] = $this->convert_bools((string) $prtxml->autoSimplify); $prt['feedbackvariables'] = $this->convert_keyvals((string) $prtxml->feedbackVariables); $potentialresponses = array(); $autonumber = 0; foreach ($prtxml->PotentialResponses->PR as $prxml) { $id = (string) $prxml['id']; $pr = array(); $pr['answertest'] = (string) $prxml->answerTest; if ('Equal_Com_Ass' == trim($pr['answertest'])) { $pr['answertest'] = 'EqualComAss'; } $pr['tans'] = (string) $prxml->teachersAns; if (strlen($pr['tans']) > 255) { $prt['feedbackvariables'] .= "\n/*Automatically added by the importer*/"; $prt['feedbackvariables'] .= "\nlongexpr" . $autonumber . ':' . $pr['tans'] . "\n"; $autonumber += 1; $pr['tans'] = 'longexpr' . $autonumber; } $pr['sans'] = (string) $prxml->studentAns; if (strlen($pr['sans']) > 255) { $prt['feedbackvariables'] .= "\n/*Automatically added by the importer*/"; $prt['feedbackvariables'] .= "\nlongexpr" . $autonumber . ':' . $pr['sans'] . "\n"; $autonumber += 1; $pr['sans'] = 'longexpr' . $autonumber; } $pr['testoptions'] = (string) $prxml->testoptions; if (strlen($pr['testoptions']) > 255) { $prt['feedbackvariables'] .= "\n/*Automatically added by the importer*/"; $prt['feedbackvariables'] .= "\nlongexpr" . $autonumber . ':' . $pr['testoptions'] . "\n"; $autonumber += 1; $pr['testoptions'] = 'longexpr' . $autonumber; } $pr['quiet'] = (string) $prxml->quietAnsTest; foreach (array('true', 'false') as $branchname) { $branch = $this->getnextPR($branchname, $prxml); foreach ($branch as $key => $val) { if ('answernote' == $key and '' == trim($val)) { $ids = (string) ($id + 1); $val = $name . '-' . $ids . '-'; if ('true' == $branchname) { $val .= 'T'; } else { $val .= 'F'; } } $pr[$branchname . $key] = $val; } } $potentialresponses[$id] = $pr; } $numericalfields = array('truescore', 'falsescore', 'truepenalty', 'falsepenalty'); foreach ($potentialresponses as $prname => $pr) { foreach ($pr as $key => $val) { $prt[$key][$prname] = $val; if (in_array($key, $numericalfields) and '' != $val) { // Tidy up numerical values. $val = trim($val); if (substr($val, 0, 1) == '.') { $val = '0' . $val; } if (!($this->convert_floats($val) === (string) $val)) { if ($strictimport) { $errors[] = 'Tried to set a numerical field "' . $key . '" in potential response tree "' . $prname . '" in question "' . $question->name . '". with the illegal value "' . $val . '". This must be a float. '; } else { $prt[$key][$prname] = $this->convert_floats($val); } } } } } $potentialresponsetrees[$name] = $prt; } $prtnames = array(); foreach ($potentialresponsetrees as $name => $prt) { // STACK 3 (moodle forms?) can't cope with PRT names which are only a number. So prepend "prt". $newname = $this->convert_prt_name($name); $prtnames[$name] = $newname; foreach ($prt as $key => $val) { $question->{$newname . $key} = $val; } } // Change the input tags for the new versions. // Single PRT questions are treated as a special case. if (1 == count($prtnames)) { foreach ($prtnames as $oldname => $newname) { $question->questiontext = str_replace('<PRTfeedback>' . $oldname . '</PRTfeedback>', '', $question->questiontext); $question->specificfeedback = array('text' => "<p>[[feedback:{$newname}]]</p>", 'format' => FORMAT_HTML, 'files' => array()); } } else { foreach ($prtnames as $oldname => $newname) { $question->questiontext = str_replace('<PRTfeedback>' . $oldname . '</PRTfeedback>', "[[feedback:{$newname}]]", $question->questiontext); } } // Question tests. $itemtests = array(); if ($assessmentitem->ItemTests) { $question->testcases[0] = null; foreach ($assessmentitem->ItemTests->test as $testxml) { $inputs = array(); $prts = array(); foreach ($testxml->children() as $col) { $key = (string) $col->key; $val = (string) $col->value; if ('IE_' == substr($key, 0, 3)) { $inputs[substr($key, 3)] = $val; } if ('PRT_' == substr($key, 0, 4)) { // Knock off PR_PotResTree_. if ('NONE' == $val) { $val = 'NULL'; } $key = $this->convert_prt_name(substr($key, 15)); if (strlen($key) > 32) { $key = substr($key, 0, 31); } $prts[$key] = $val; } } $qtest = new stack_question_test($inputs); foreach ($prts as $key => $val) { $qtest->add_expected_result($key, new stack_potentialresponse_tree_state(1, true, 0, 0.1, '', array($val))); } $question->testcases[] = $qtest; } unset($question->testcases[0]); } return array($question, $errors); }
public function test_xml_import() { $xml = '<!-- question: 0 --> <question type="stack"> <name> <text>test-0</text> </name> <questiontext format="html"> <text>What is $1+1$? [[input:ans1]] [[validation:ans1]]</text> </questiontext> <generalfeedback format="html"> <text></text> </generalfeedback> <defaultgrade>1</defaultgrade> <penalty>0.3333333</penalty> <hidden>0</hidden> <questionvariables> <text></text> </questionvariables> <specificfeedback format="html"> <text>[[feedback:firsttree]]</text> </specificfeedback> <questionnote> <text></text> </questionnote> <questionsimplify>1</questionsimplify> <assumepositive>0</assumepositive> <prtcorrect format="html"> <text><![CDATA[<p>Correct answer, well done.</p>]]></text> </prtcorrect> <prtpartiallycorrect format="html"> <text><![CDATA[<p>Your answer is partially correct.</p>]]></text> </prtpartiallycorrect> <prtincorrect format="html"> <text><![CDATA[<p>Incorrect answer.</p>]]></text> </prtincorrect> <multiplicationsign>dot</multiplicationsign> <sqrtsign>1</sqrtsign> <complexno>i</complexno> <inversetrig>cos-1</inversetrig> <matrixparens>[</matrixparens> <variantsselectionseed></variantsselectionseed> <input> <name>ans1</name> <type>algebraic</type> <tans>2</tans> <boxsize>5</boxsize> <strictsyntax>1</strictsyntax> <insertstars>0</insertstars> <syntaxhint></syntaxhint> <forbidwords></forbidwords> <allowwords></allowwords> <forbidfloat>1</forbidfloat> <requirelowestterms>0</requirelowestterms> <checkanswertype>0</checkanswertype> <mustverify>1</mustverify> <showvalidation>1</showvalidation> <options></options> </input> <prt> <name>firsttree</name> <value>1</value> <autosimplify>1</autosimplify> <feedbackvariables> <text></text> </feedbackvariables> <node> <name>0</name> <answertest>EqualComAss</answertest> <sans>ans1</sans> <tans>2</tans> <testoptions></testoptions> <quiet>0</quiet> <truescoremode>=</truescoremode> <truescore>1</truescore> <truepenalty>0</truepenalty> <truenextnode>-1</truenextnode> <trueanswernote>firsttree-1-T</trueanswernote> <truefeedback format="html"> <text></text> </truefeedback> <falsescoremode>=</falsescoremode> <falsescore>0</falsescore> <falsepenalty>0</falsepenalty> <falsenextnode>-1</falsenextnode> <falseanswernote>firsttree-1-F</falseanswernote> <falsefeedback format="html"> <text></text> </falsefeedback> </node> </prt> <deployedseed>12345</deployedseed> <qtest> <testcase>1</testcase> <testinput> <name>ans1</name> <value>2</value> </testinput> <expected> <name>firsttree</name> <expectedscore>1</expectedscore> <expectedpenalty>0</expectedpenalty> <expectedanswernote>firsttree-1-T</expectedanswernote> </expected> </qtest> </question> '; $xmldata = xmlize($xml); $importer = new qformat_xml(); $q = $importer->try_importing_using_qtypes($xmldata['question'], null, null, 'stack'); $expectedq = new stdClass(); $expectedq->qtype = 'stack'; $expectedq->name = 'test-0'; $expectedq->questiontext = 'What is $1+1$? [[input:ans1]] [[validation:ans1]]'; $expectedq->questiontextformat = FORMAT_HTML; $expectedq->generalfeedback = ''; $expectedq->generalfeedbackformat = FORMAT_HTML; $expectedq->defaultmark = 1; $expectedq->length = 1; $expectedq->penalty = 0.3333333; $expectedq->questionvariables = ''; $expectedq->specificfeedback = array('text' => '[[feedback:firsttree]]', 'format' => FORMAT_HTML, 'files' => array()); $expectedq->questionnote = ''; $expectedq->questionsimplify = 1; $expectedq->assumepositive = 0; $expectedq->prtcorrect = array('text' => '<p>Correct answer, well done.</p>', 'format' => FORMAT_HTML, 'files' => array()); $expectedq->prtpartiallycorrect = array('text' => '<p>Your answer is partially correct.</p>', 'format' => FORMAT_HTML, 'files' => array()); $expectedq->prtincorrect = array('text' => '<p>Incorrect answer.</p>', 'format' => FORMAT_HTML, 'files' => array()); $expectedq->multiplicationsign = 'dot'; $expectedq->sqrtsign = 1; $expectedq->complexno = 'i'; $expectedq->inversetrig = 'cos-1'; $expectedq->matrixparens = '['; $expectedq->variantsselectionseed = ''; $expectedq->ans1type = 'algebraic'; $expectedq->ans1modelans = 2; $expectedq->ans1boxsize = 5; $expectedq->ans1strictsyntax = 1; $expectedq->ans1insertstars = 0; $expectedq->ans1syntaxhint = ''; $expectedq->ans1forbidwords = ''; $expectedq->ans1allowwords = ''; $expectedq->ans1forbidfloat = 1; $expectedq->ans1requirelowestterms = 0; $expectedq->ans1checkanswertype = 0; $expectedq->ans1mustverify = 1; $expectedq->ans1showvalidation = 1; $expectedq->ans1options = ''; $expectedq->firsttreevalue = 1; $expectedq->firsttreeautosimplify = 1; $expectedq->firsttreefeedbackvariables = ''; $expectedq->firsttreeanswertest[0] = 'EqualComAss'; $expectedq->firsttreesans[0] = 'ans1'; $expectedq->firsttreetans[0] = '2'; $expectedq->firsttreetestoptions[0] = ''; $expectedq->firsttreequiet[0] = 0; $expectedq->firsttreetruescoremode[0] = '='; $expectedq->firsttreetruescore[0] = 1; $expectedq->firsttreetruepenalty[0] = 0; $expectedq->firsttreetruenextnode[0] = -1; $expectedq->firsttreetrueanswernote[0] = 'firsttree-1-T'; $expectedq->firsttreetruefeedback[0] = array('text' => '', 'format' => FORMAT_HTML, 'files' => array()); $expectedq->firsttreefalsescoremode[0] = '='; $expectedq->firsttreefalsescore[0] = 0; $expectedq->firsttreefalsepenalty[0] = 0; $expectedq->firsttreefalsenextnode[0] = -1; $expectedq->firsttreefalseanswernote[0] = 'firsttree-1-F'; $expectedq->firsttreefalsefeedback[0] = array('text' => '', 'format' => FORMAT_HTML, 'files' => array()); $expectedq->deployedseeds = array('12345'); $qtest = new stack_question_test(array('ans1' => '2')); $qtest->add_expected_result('firsttree', new stack_potentialresponse_tree_state(1, true, 1, 0, '', array('firsttree-1-T'))); $expectedq->testcases[1] = $qtest; $this->assert(new question_check_specified_fields_expectation($expectedq), $q); $this->assertEquals($expectedq->deployedseeds, $q->deployedseeds); // Redundant, but gives better fail messages. $this->assertEquals($expectedq->testcases, $q->testcases); // Redundant, but gives better fail messages. }
$mform->set_data($currentdata); } // Process the form. if ($mform->is_cancelled()) { unset($urlparams['testcase']); redirect($backurl); } else { if ($data = $mform->get_data()) { // Process form submission. $inputs = array(); foreach ($question->inputs as $inputname => $notused) { $inputs[$inputname] = $data->{$inputname}; } $qtest = new stack_question_test($inputs); foreach ($question->prts as $prtname => $notused) { $qtest->add_expected_result($prtname, new stack_potentialresponse_tree_state(1, true, $data->{$prtname . 'score'}, $data->{$prtname . 'penalty'}, '', array($data->{$prtname . 'answernote'}))); } question_bank::get_qtype('stack')->save_question_test($questionid, $qtest, $testcase); redirect($backurl); } } // Prepare the display options. $options = new question_display_options(); $options->readonly = true; $options->flags = question_display_options::HIDDEN; $options->suppressruntestslink = true; // Display the page. echo $OUTPUT->header(); // Show the question read-only. echo $quba->render_question($slot, $options); // Display the question variables.
/** * @return qtype_stack_question the question from the test0.xml file. */ public static function get_stack_question_data_test0() { question_bank::load_question_definition_classes('stack'); $qdata = new stdClass(); test_question_maker::initialise_question_data($qdata); $qdata->qtype = 'stack'; $qdata->name = 'test-0'; $qdata->questiontext = 'What is $1+1$? [[input:ans1]] [[validation:ans1]]'; $qdata->generalfeedback = ''; $qdata->options = new stdClass(); $qdata->options->id = 0; $qdata->options->questionvariables = ''; $qdata->options->specificfeedback = '[[feedback:firsttree]]'; $qdata->options->specificfeedbackformat = FORMAT_HTML; $qdata->options->questionnote = ''; $qdata->options->questionsimplify = 1; $qdata->options->assumepositive = 0; $qdata->options->prtcorrect = self::DEFAULT_CORRECT_FEEDBACK; $qdata->options->prtcorrectformat = FORMAT_HTML; $qdata->options->prtpartiallycorrect = self::DEFAULT_PARTIALLYCORRECT_FEEDBACK; $qdata->options->prtpartiallycorrectformat = FORMAT_HTML; $qdata->options->prtincorrect = self::DEFAULT_INCORRECT_FEEDBACK; $qdata->options->prtincorrectformat = FORMAT_HTML; $qdata->options->multiplicationsign = 'dot'; $qdata->options->sqrtsign = 1; $qdata->options->complexno = 'i'; $qdata->options->inversetrig = 'cos-1'; $qdata->options->matrixparens = '['; $qdata->options->variantsselectionseed = ''; $input = new stdClass(); $input->name = 'ans1'; $input->id = 0; $input->questionid = 0; $input->type = 'algebraic'; $input->tans = '2'; $input->boxsize = 5; $input->strictsyntax = 1; $input->insertstars = 0; $input->syntaxhint = ''; $input->forbidwords = ''; $input->allowwords = ''; $input->forbidfloat = 1; $input->requirelowestterms = 0; $input->checkanswertype = 0; $input->mustverify = 1; $input->showvalidation = 1; $input->options = ''; $qdata->inputs['ans1'] = $input; $prt = new stdClass(); $prt->name = 'firsttree'; $prt->id = '0'; $prt->questionid = '0'; $prt->value = 1; $prt->autosimplify = 1; $prt->feedbackvariables = ''; $prt->firstnodename = '0'; $node = new stdClass(); $node->id = 0; $node->questionid = 0; $node->prtname = 'firsttree'; $node->nodename = '0'; $node->answertest = 'EqualComAss'; $node->sans = 'ans1'; $node->tans = '2'; $node->testoptions = ''; $node->quiet = 0; $node->truescoremode = '='; $node->truescore = 1; $node->truepenalty = 0; $node->truenextnode = -1; $node->trueanswernote = 'firsttree-1-T'; $node->truefeedback = ''; $node->truefeedbackformat = FORMAT_HTML; $node->falsescoremode = '='; $node->falsescore = 0; $node->falsepenalty = 0; $node->falsenextnode = -1; $node->falseanswernote = 'firsttree-1-F'; $node->falsefeedback = ''; $node->falsefeedbackformat = FORMAT_HTML; $prt->nodes['0'] = $node; $qdata->prts['firsttree'] = $prt; $qdata->deployedseeds = array('12345'); $qtest = new stack_question_test(array('ans1' => '2')); $qtest->add_expected_result('firsttree', new stack_potentialresponse_tree_state(1, true, 1, 0, '', array('firsttree-1-T'))); $qdata->testcases[1] = $qtest; return $qdata; }