From 8a8dd7d8c8a4330d306f6d34edd0c7b7b68a38b4 Mon Sep 17 00:00:00 2001 From: Jean-Michel Vedrine Date: Mon, 6 Aug 2012 18:16:00 +0200 Subject: [PATCH] MDL-34738 qformat blackboard: blackboard format is broken --- question/format.php | 54 ++ question/format/blackboard/format.php | 641 ++++++++++-------- .../blackboard/lang/en/qformat_blackboard.php | 6 +- .../tests/blackboardformat_test.php | 469 +++++++++++++ .../tests/fixtures/sample_blackboard.dat | 142 ++++ question/format/blackboard/version.php | 3 +- 6 files changed, 1046 insertions(+), 269 deletions(-) create mode 100644 question/format/blackboard/tests/blackboardformat_test.php create mode 100644 question/format/blackboard/tests/fixtures/sample_blackboard.dat diff --git a/question/format.php b/question/format.php index 0ccdbce99e9..a0e9df4e437 100644 --- a/question/format.php +++ b/question/format.php @@ -898,3 +898,57 @@ class qformat_default { $question->questiontextformat, $formatoptions), 0, false); } } + +class qformat_based_on_xml extends qformat_default { + + /** + * Return the array moodle is expecting + * for an HTML text. No processing is done on $text. + * qformat classes that want to process $text + * for instance to import external images files + * and recode urls in $text must overwrite this method. + * @param array $text some HTML text string + * @return array with keys text, format and files. + */ + public function text_field($text) { + return array( + 'text' => trim($text), + 'format' => FORMAT_HTML, + 'files' => array(), + ); + } + + /** + * Return the value of a node, given a path to the node + * if it doesn't exist return the default value. + * @param array xml data to read + * @param array path path to node expressed as array + * @param mixed default + * @param bool istext process as text + * @param string error if set value must exist, return false and issue message if not + * @return mixed value + */ + public function getpath($xml, $path, $default, $istext=false, $error='') { + foreach ($path as $index) { + if (!isset($xml[$index])) { + if (!empty($error)) { + $this->error($error); + return false; + } else { + return $default; + } + } + + $xml = $xml[$index]; + } + + if ($istext) { + if (!is_string($xml)) { + $this->error(get_string('invalidxml', 'qformat_xml')); + } + $xml = trim($xml); + } + + return $xml; + } +} diff --git a/question/format/blackboard/format.php b/question/format/blackboard/format.php index 9ce23d0e9a3..16264898c0a 100644 --- a/question/format/blackboard/format.php +++ b/question/format/blackboard/format.php @@ -17,8 +17,7 @@ /** * Blackboard question importer. * - * @package qformat - * @subpackage blackboard + * @package qformat_blackboard * @copyright 2003 Scott Elliott * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -26,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -require_once ($CFG->libdir . '/xmlize.php'); +require_once($CFG->libdir . '/xmlize.php'); /** @@ -35,19 +34,67 @@ require_once ($CFG->libdir . '/xmlize.php'); * @copyright 2003 Scott Elliott * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class qformat_blackboard extends qformat_default { +class qformat_blackboard extends qformat_based_on_xml { + // Is the current question's question text escaped HTML (true for most if not all Blackboard files). + public $ishtml = true; + public function provide_import() { return true; } - function readquestions($lines) { - /// Parses an array of lines into an array of questions, - /// where each item is a question object as defined by - /// readquestion(). + public function mime_type() { + return mimeinfo('type', '.dat'); + } + + /** + * Some softwares put entities in exported files. + * This method try to clean up known problems. + * @param string str string to correct + * @return string the corrected string + */ + public function cleaninput($str) { + if (!$this->ishtml) { + return $str; + } + $html_code_list = array( + "'" => "'", + "’" => "'", + "[" => "[", + "“" => "\"", + "”" => "\"", + "]" => "]", + "'" => "'", + "–" => "-", + "—" => "-", + ); + $str = strtr($str, $html_code_list); + // Use textlib entities_to_utf8 function to convert only numerical entities. + $str = textlib::entities_to_utf8($str, false); + return $str; + } - $text = implode($lines, " "); - $xml = xmlize($text, 0); + /** + * Parse the array of lines into an array of questions + * this *could* burn memory - but it won't happen that much + * so fingers crossed! + * @param array of lines from the input file. + * @param stdClass $context + * @return array (of objects) question objects. + */ + protected function readquestions($lines) { + + $text = implode($lines, ' '); + unset($lines); + + // This converts xml to big nasty data structure, + // the 0 means keep white space as it is. + try { + $xml = xmlize($text, 0, 'UTF-8', true); + } catch (xml_format_exception $e) { + $this->error($e->getMessage(), ''); + return false; + } $questions = array(); @@ -61,341 +108,405 @@ class qformat_blackboard extends qformat_default { return $questions; } -//---------------------------------------- -// Process Essay Questions -//---------------------------------------- - function process_essay($xml, &$questions ) { - - if (isset($xml["POOL"]["#"]["QUESTION_ESSAY"])) { - $essayquestions = $xml["POOL"]["#"]["QUESTION_ESSAY"]; + /** + * Do question import processing common to every qtype. + * @param array $questiondata the xml tree related to the current question + * @return object initialized question object. + */ + public function process_common($questiondata) { + global $CFG; + + // This routine initialises the question object. + $question = $this->defaultquestion(); + + // Determine if the question is already escaped html. + $this->ishtml = $this->getpath($questiondata, + array('#', 'BODY', 0, '#', 'FLAGS', 0, '#', 'ISHTML', 0, '@', 'value'), + false, false); + + // Put questiontext in question object. + $text = $this->getpath($questiondata, + array('#', 'BODY', 0, '#', 'TEXT', 0, '#'), + '', true, get_string('importnotext', 'qformat_blackboard')); + + if ($this->ishtml) { + $question->questiontext = $this->cleaninput($text); + $question->questiontextformat = FORMAT_HTML; + $question->questiontextfiles = array(); + + } else { + $question->questiontext = $text; } - else { - return; + // Put name in question object. We must ensure it is not empty and it is less than 250 chars. + $question->name = shorten_text(strip_tags($question->questiontext), 200); + $question->name = substr($question->name, 0, 250); + if (!$question->name) { + $id = $this->getpath($questiondata, + array('@', 'id'), '', true); + $question->name = get_string('defaultname', 'qformat_blackboard' , $id); } - foreach ($essayquestions as $essayquestion) { + $question->generalfeedback = ''; + $question->generalfeedbackformat = FORMAT_HTML; + $question->generalfeedbackfiles = array(); - $question = $this->defaultquestion(); + // TODO : read the mark from the POOL TITLE QUESTIONLIST section. + $question->defaultmark = 1; + return $question; + } + + /** + * Process Essay Questions + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_essay($xml, &$questions) { + + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_ESSAY'), false, false)) { + $essayquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_ESSAY'), false, false); + } else { + return; + } - $question->qtype = ESSAY; + foreach ($essayquestions as $thisquestion) { - // determine if the question is already escaped html - $ishtml = $essayquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"]; + $question = $this->process_common($thisquestion); - // put questiontext in question object - if ($ishtml) { - $question->questiontext = html_entity_decode(trim($essayquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"])); - } + $question->qtype = 'essay'; - // put name in question object - $question->name = substr($question->questiontext, 0, 254); $question->answer = ''; + $answer = $this->getpath($thisquestion, + array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true); + $question->graderinfo = $this->text_field($this->cleaninput($answer)); $question->feedback = ''; + $question->responseformat = 'editor'; + $question->responsefieldlines = 15; + $question->attachments = 0; $question->fraction = 0; $questions[] = $question; } } - //---------------------------------------- - // Process True / False Questions - //---------------------------------------- - function process_tf($xml, &$questions) { - - if (isset($xml["POOL"]["#"]["QUESTION_TRUEFALSE"])) { - $tfquestions = $xml["POOL"]["#"]["QUESTION_TRUEFALSE"]; - } - else { + /** + * Process True / False Questions + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_tf($xml, &$questions) { + + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false)) { + $tfquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false); + } else { return; } - for ($i = 0; $i < sizeof ($tfquestions); $i++) { - - $question = $this->defaultquestion(); + foreach ($tfquestions as $thisquestion) { - $question->qtype = TRUEFALSE; - $question->single = 1; // Only one answer is allowed + $question = $this->process_common($thisquestion); - $thisquestion = $tfquestions[$i]; + $question->qtype = 'truefalse'; + $question->single = 1; // Only one answer is allowed. - // determine if the question is already escaped html - $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"]; + $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false); - // put questiontext in question object - if ($ishtml) { - $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8'); - } - // put name in question object - $question->name = shorten_text($question->questiontext, 254); + $correct_answer = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'), + '', true); - $choices = $thisquestion["#"]["ANSWER"]; - - $correct_answer = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"][0]["@"]["answer_id"]; - - // first choice is true, second is false. - $id = $choices[0]["@"]["id"]; - - if (strcmp($id, $correct_answer) == 0) { // true is correct + // First choice is true, second is false. + $id = $this->getpath($choices[0], array('@', 'id'), '', true); + $correctfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), + '', true); + $incorrectfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), + '', true); + if (strcmp($id, $correct_answer) == 0) { // True is correct. $question->answer = 1; - $question->feedbacktrue = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]); - $question->feedbackfalse = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]); - } else { // false is correct + $question->feedbacktrue = $this->text_field($this->cleaninput($correctfeedback)); + $question->feedbackfalse = $this->text_field($this->cleaninput($incorrectfeedback)); + } else { // False is correct. $question->answer = 0; - $question->feedbacktrue = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]); - $question->feedbackfalse = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]); + $question->feedbacktrue = $this->text_field($this->cleaninput($incorrectfeedback)); + $question->feedbackfalse = $this->text_field($this->cleaninput($correctfeedback)); } $question->correctanswer = $question->answer; $questions[] = $question; - } + } } - //---------------------------------------- - // Process Multiple Choice Questions - //---------------------------------------- - function process_mc($xml, &$questions) { - - if (isset($xml["POOL"]["#"]["QUESTION_MULTIPLECHOICE"])) { - $mcquestions = $xml["POOL"]["#"]["QUESTION_MULTIPLECHOICE"]; - } - else { + /** + * Process Multiple Choice Questions with single answer + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_mc($xml, &$questions) { + + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false)) { + $mcquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false); + } else { return; } - for ($i = 0; $i < sizeof ($mcquestions); $i++) { - - $question = $this->defaultquestion(); - - $question->qtype = MULTICHOICE; - $question->single = 1; // Only one answer is allowed - - $thisquestion = $mcquestions[$i]; - - // determine if the question is already escaped html - $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"]; - - // put questiontext in question object - if ($ishtml) { - $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8'); - } - - // put name of question in question object, careful of length - $question->name = shorten_text($question->questiontext, 254); - - $choices = $thisquestion["#"]["ANSWER"]; - for ($j = 0; $j < sizeof ($choices); $j++) { - - $choice = trim($choices[$j]["#"]["TEXT"][0]["#"]); - // put this choice in the question object. - if ($ishtml) { - $question->answer[$j] = html_entity_decode($choice,ENT_QUOTES,'UTF-8'); - } - $question->answer[$j] = $question->answer[$j]; - - $id = $choices[$j]["@"]["id"]; - $correct_answer_id = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"][0]["@"]["answer_id"]; - // if choice is the answer, give 100%, otherwise give 0% - if (strcmp ($id, $correct_answer_id) == 0) { - $question->fraction[$j] = 1; - if ($ishtml) { - $question->feedback[$j] = html_entity_decode(trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]),ENT_QUOTES,'UTF-8'); - } - $question->feedback[$j] = $question->feedback[$j]; + foreach ($mcquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + + $correctfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), + '', true); + $incorrectfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), + '', true); + $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback)); + $question->partiallycorrectfeedback = $this->text_field(''); + $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback)); + + $question->qtype = 'multichoice'; + $question->single = 1; // Only one answer is allowed. + + $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); + $correct_answer_id = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'), + '', true); + foreach ($choices as $choice) { + $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true); + // Put this choice in the question object. + $question->answer[] = $this->text_field($this->cleaninput($choicetext)); + + $choice_id = $this->getpath($choice, array('@', 'id'), '', true); + // If choice is the right answer, give 100% mark, otherwise give 0%. + if (strcmp ($choice_id, $correct_answer_id) == 0) { + $question->fraction[] = 1; } else { - $question->fraction[$j] = 0; - if ($ishtml) { - $question->feedback[$j] = html_entity_decode(trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]),ENT_QUOTES,'UTF-8'); - } - $question->feedback[$j] = $question->feedback[$j]; + $question->fraction[] = 0; } + // There is never feedback specific to each choice. + $question->feedback[] = $this->text_field(''); } $questions[] = $question; } } - //---------------------------------------- - // Process Multiple Choice Questions With Multiple Answers - //---------------------------------------- - function process_ma($xml, &$questions) { - - if (isset($xml["POOL"]["#"]["QUESTION_MULTIPLEANSWER"])) { - $maquestions = $xml["POOL"]["#"]["QUESTION_MULTIPLEANSWER"]; - } - else { + /** + * Process Multiple Choice Questions With Multiple Answers + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_ma($xml, &$questions) { + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false)) { + $maquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false); + } else { return; } - for ($i = 0; $i < sizeof ($maquestions); $i++) { - - $question = $this->defaultquestion(); - - $question->qtype = MULTICHOICE; + foreach ($maquestions as $thisquestion) { + $question = $this->process_common($thisquestion); + + $correctfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), + '', true); + $incorrectfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), + '', true); + $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback)); + // As there is no partially correct feedback we use incorrect one. + $question->partiallycorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback)); + $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback)); + + $question->qtype = 'multichoice'; $question->defaultmark = 1; - $question->single = 0; // More than one answers allowed - $question->image = ""; // No images with this format - - $thisquestion = $maquestions[$i]; - - // determine if the question is already escaped html - $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"]; - - // put questiontext in question object - if ($ishtml) { - $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8'); + $question->single = 0; // More than one answers allowed. + + $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); + $correct_answer_ids = array(); + foreach ($this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) { + if ($correctanswer) { + $correct_answer_ids[] = $this->getpath($correctanswer, + array('@', 'answer_id'), + '', true); + } } - // put name of question in question object - $question->name = shorten_text($question->questiontext, 254); + $fraction = 1/count($correct_answer_ids); - $choices = $thisquestion["#"]["ANSWER"]; - $correctanswers = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"]; + foreach ($choices as $choice) { + $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true); + // Put this choice in the question object. + $question->answer[] = $this->text_field($this->cleaninput($choicetext)); - for ($j = 0; $j < sizeof ($choices); $j++) { + $choice_id = $this->getpath($choice, array('@', 'id'), '', true); - $choice = trim($choices[$j]["#"]["TEXT"][0]["#"]); - // put this choice in the question object. - $question->answer[$j] = $choice; + $iscorrect = in_array($choice_id, $correct_answer_ids); - $correctanswercount = sizeof($correctanswers); - $id = $choices[$j]["@"]["id"]; - $iscorrect = 0; - for ($k = 0; $k < $correctanswercount; $k++) { - - $correct_answer_id = trim($correctanswers[$k]["@"]["answer_id"]); - if (strcmp ($id, $correct_answer_id) == 0) { - $iscorrect = 1; - } - - } if ($iscorrect) { - $question->fraction[$j] = floor(100000/$correctanswercount)/100000; // strange behavior if we have more than 5 decimal places - $question->feedback[$j] = trim($thisquestion["#"]["GRADABLE"][$j]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]); + $question->fraction[] = $fraction; } else { - $question->fraction[$j] = 0; - $question->feedback[$j] = trim($thisquestion["#"]["GRADABLE"][$j]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]); + $question->fraction[] = 0; } + // There is never feedback specific to each choice. + $question->feedback[] = $this->text_field(''); } - $questions[] = $question; } } - //---------------------------------------- - // Process Fill in the Blank Questions - //---------------------------------------- - function process_fib($xml, &$questions) { - - if (isset($xml["POOL"]["#"]["QUESTION_FILLINBLANK"])) { - $fibquestions = $xml["POOL"]["#"]["QUESTION_FILLINBLANK"]; - } - else { + /** + * Process Fill in the Blank Questions + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_fib($xml, &$questions) { + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false)) { + $fibquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false); + } else { return; } - for ($i = 0; $i < sizeof ($fibquestions); $i++) { - $question = $this->defaultquestion(); - - $question->qtype = SHORTANSWER; - $question->usecase = 0; // Ignore case - - $thisquestion = $fibquestions[$i]; - - // determine if the question is already escaped html - $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"]; - - // put questiontext in question object - if ($ishtml) { - $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8'); - } - // put name of question in question object - $question->name = shorten_text($question->questiontext, 254); - - $answer = trim($thisquestion["#"]["ANSWER"][0]["#"]["TEXT"][0]["#"]); - - $question->answer[] = $answer; - $question->fraction[] = 1; - $question->feedback = array(); - - if (is_array( $thisquestion['#']['GRADABLE'][0]['#'] )) { - $question->feedback[0] = trim($thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]); - } - else { - $question->feedback[0] = ''; - } - if (is_array( $thisquestion["#"]["GRADABLE"][0]["#"] )) { - $question->feedback[1] = trim($thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]); - } - else { - $question->feedback[1] = ''; + foreach ($fibquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + + $question->qtype = 'shortanswer'; + $question->usecase = 0; // Ignore case. + + $correctfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), + '', true); + $incorrectfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), + '', true); + $answers = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); + foreach ($answers as $answer) { + $question->answer[] = $this->getpath($answer, + array('#', 'TEXT', 0, '#'), '', true); + $question->fraction[] = 1; + $question->feedback[] = $this->text_field($this->cleaninput($correctfeedback)); } + $question->answer[] = '*'; + $question->fraction[] = 0; + $question->feedback[] = $this->text_field($this->cleaninput($incorrectfeedback)); $questions[] = $question; } } - //---------------------------------------- - // Process Matching Questions - //---------------------------------------- - function process_matching($xml, &$questions) { - - if (isset($xml["POOL"]["#"]["QUESTION_MATCH"])) { - $matchquestions = $xml["POOL"]["#"]["QUESTION_MATCH"]; - } - else { + /** + * Process Matching Questions + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_matching($xml, &$questions) { + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MATCH'), false, false)) { + $matchquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_MATCH'), false, false); + } else { return; } - - for ($i = 0; $i < sizeof ($matchquestions); $i++) { - - $question = $this->defaultquestion(); - - $question->qtype = MATCH; - - $thisquestion = $matchquestions[$i]; - - // determine if the question is already escaped html - $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"]; - - // put questiontext in question object - if ($ishtml) { - $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8'); + // Blackboard questions can't be imported in core Moodle without a loss in data, + // as core match question don't allow HTML in subanswers. The contributed ddmatch + // question type support HTML in subanswers. + // The ddmatch question type is not part of core, so we need to check if it is defined. + $ddmatch_is_installed = question_bank::is_qtype_installed('ddmatch'); + + foreach ($matchquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + if ($ddmatch_is_installed) { + $question->qtype = 'ddmatch'; + } else { + $question->qtype = 'match'; } - // put name of question in question object - $question->name = shorten_text($question->questiontext, 254); - - $choices = $thisquestion["#"]["CHOICE"]; - for ($j = 0; $j < sizeof ($choices); $j++) { - - $subquestion = NULL; - - $choice = $choices[$j]["#"]["TEXT"][0]["#"]; - $choice_id = $choices[$j]["@"]["id"]; - - $question->subanswers[] = trim($choice); - - $correctanswers = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"]; - for ($k = 0; $k < sizeof ($correctanswers); $k++) { - - if (strcmp($choice_id, $correctanswers[$k]["@"]["choice_id"]) == 0) { - - $answer_id = $correctanswers[$k]["@"]["answer_id"]; - $answers = $thisquestion["#"]["ANSWER"]; - for ($m = 0; $m < sizeof ($answers); $m++) { + $correctfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), + '', true); + $incorrectfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), + '', true); + $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback)); + // As there is no partially correct feedback we use incorrect one. + $question->partiallycorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback)); + $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback)); + + $choices = $this->getpath($thisquestion, + array('#', 'CHOICE'), false, false); // Blackboard "choices" are Moodle subanswers. + $answers = $this->getpath($thisquestion, + array('#', 'ANSWER'), false, false); // Blackboard "answers" are Moodle subquestions. + $correctanswers = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false); // Mapping between choices and answers. + $mappings = array(); + foreach ($correctanswers as $correctanswer) { + if ($correctanswer) { + $correct_choice_id = $this->getpath($correctanswer, + array('@', 'choice_id'), '', true); + $correct_answer_id = $this->getpath($correctanswer, + array('@', 'answer_id'), + '', true); + $mappings[$correct_answer_id] = $correct_choice_id; + } + } - $answer = $answers[$m]; - $current_ans_id = $answer["@"]["id"]; - if (strcmp ($current_ans_id, $answer_id) == 0) { + foreach ($choices as $choice) { + if ($ddmatch_is_installed) { + $choicetext = $this->text_field($this->cleaninput($this->getpath($choice, + array('#', 'TEXT', 0, '#'), '', true))); + } else { + $choicetext = trim(strip_tags($this->getpath($choice, + array('#', 'TEXT', 0, '#'), '', true))); + } - $answer = $answer["#"]["TEXT"][0]["#"]; - $question->subquestions[] = trim($answer); + if ($choicetext != '') { // Only import non empty subanswers. + $subquestion = ''; + $choice_id = $this->getpath($choice, + array('@', 'id'), '', true); + $fiber = array_search($choice_id, $mappings); + $fiber = array_keys ($mappings, $choice_id); + foreach ($fiber as $correct_answer_id) { + // We have found a correspondance for this choice so we need to take the associated answer. + foreach ($answers as $answer) { + $current_ans_id = $this->getpath($answer, + array('@', 'id'), '', true); + if (strcmp ($current_ans_id, $correct_answer_id) == 0) { + $subquestion = $this->getpath($answer, + array('#', 'TEXT', 0, '#'), '', true); break; } } - break; + $question->subquestions[] = $this->text_field($this->cleaninput($subquestion)); + $question->subanswers[] = $choicetext; + } + + if ($subquestion == '') { // Then in this case, $choice is a distractor. + $question->subquestions[] = $this->text_field(''); + $question->subanswers[] = $choicetext; } } } - $questions[] = $question; + // Verify that this matching question has enough subquestions and subanswers. + $subquestioncount = 0; + $subanswercount = 0; + $subanswers = $question->subanswers; + foreach ($question->subquestions as $key => $subquestion) { + $subquestion = $subquestion['text']; + $subanswer = $subanswers[$key]; + if ($subquestion != '') { + $subquestioncount++; + } + $subanswercount++; + } + if ($subquestioncount < 2 || $subanswercount < 3) { + $this->error(get_string('notenoughtsubans', 'qformat_blackboard', $question->questiontext)); + } else { + $questions[] = $question; + } } } diff --git a/question/format/blackboard/lang/en/qformat_blackboard.php b/question/format/blackboard/lang/en/qformat_blackboard.php index 6f68e214657..f5fa9576ea4 100644 --- a/question/format/blackboard/lang/en/qformat_blackboard.php +++ b/question/format/blackboard/lang/en/qformat_blackboard.php @@ -17,11 +17,13 @@ /** * Strings for component 'qformat_blackboard', language 'en', branch 'MOODLE_20_STABLE' * - * @package qformat - * @subpackage blackboard + * @package qformat_blackboard * @copyright 2010 Helen Foster * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['defaultname'] = 'Imported question {$a}'; +$string['importnotext'] = 'Missing question text in XML file'; +$string['notenoughtsubans'] = 'Unable to import matching question \'{$a}\' because a matching question must comprise at least two questions and three answers.'; $string['pluginname'] = 'Blackboard'; $string['pluginname_help'] = 'Blackboard format enables questions saved in the Blackboard version 5 "POOL" type export format to be imported.'; diff --git a/question/format/blackboard/tests/blackboardformat_test.php b/question/format/blackboard/tests/blackboardformat_test.php new file mode 100644 index 00000000000..222b6b37660 --- /dev/null +++ b/question/format/blackboard/tests/blackboardformat_test.php @@ -0,0 +1,469 @@ +. + +/** + * Unit tests for the Moodle Blackboard format. + * + * @package qformat_blackboard + * @copyright 2012 Jean-Michel Vedrine + * @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.php'); +require_once($CFG->dirroot . '/question/format/blackboard/format.php'); +require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); + + +/** + * Unit tests for the blackboard question import format. + * + * @copyright 2012 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qformat_blackboard_test extends question_testcase { + + public function make_test_xml() { + $xml = " + + + <QUESTIONLIST> + <QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/> + <QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/> + <QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/> + <QUESTION id='q39-44' class='QUESTION_MATCH' points='2'/> + <QUESTION id='q9' class='QUESTION_ESSAY' points='1'/> + <QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/> + </QUESTIONLIST> + <QUESTION_TRUEFALSE id='q1'> + <BODY> + <TEXT><![CDATA[<span style=\"font-size:12pt\">42 is the Absolute Answer to everything.</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q1_a1'> + <TEXT>False</TEXT> + </ANSWER> + <ANSWER id='q1_a2'> + <TEXT>True</TEXT> + </ANSWER> + <GRADABLE> + <CORRECTANSWER answer_id='q1_a2'/> + <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT> + <FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT> + </GRADABLE> + </QUESTION_TRUEFALSE> + <QUESTION_MULTIPLECHOICE id='q7'> + <BODY> + <TEXT><![CDATA[<span style=\"font-size:12pt\">What's between orange and green in the spectrum?</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q7_a1' position='1'> + <TEXT><![CDATA[<span style=\"font-size:12pt\">red</span>]]></TEXT> + </ANSWER> + <ANSWER id='q7_a2' position='2'> + <TEXT><![CDATA[<span style=\"font-size:12pt\">yellow</span>]]></TEXT> + </ANSWER> + <ANSWER id='q7_a3' position='3'> + <TEXT><![CDATA[<span style=\"font-size:12pt\">blue</span>]]></TEXT> + </ANSWER> + <GRADABLE> + <CORRECTANSWER answer_id='q7_a2'/> + <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT> + <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT> + </GRADABLE> + </QUESTION_MULTIPLECHOICE> + <QUESTION_MULTIPLEANSWER id='q8'> + <BODY> + <TEXT><![CDATA[<span style=\"font-size:12pt\">What's between orange and green in the spectrum?</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q8_a1' position='1'> + <TEXT><![CDATA[<span style=\"font-size:12pt\">yellow</span>]]></TEXT> + </ANSWER> + <ANSWER id='q8_a2' position='2'> + <TEXT><![CDATA[<span style=\"font-size:12pt\">red</span>]]></TEXT> + </ANSWER> + <ANSWER id='q8_a3' position='3'> + <TEXT><![CDATA[<span style=\"font-size:12pt\">off-beige</span>]]></TEXT> + </ANSWER> + <ANSWER id='q8_a4' position='4'> + <TEXT><![CDATA[<span style=\"font-size:12pt\">blue</span>]]></TEXT> + </ANSWER> + <GRADABLE> + <CORRECTANSWER answer_id='q8_a1'/> + <CORRECTANSWER answer_id='q8_a3'/> + <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT> + <FEEDBACK_WHEN_INCORRECT> + <![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]> + </FEEDBACK_WHEN_INCORRECT> + </GRADABLE> + </QUESTION_MULTIPLEANSWER> + <QUESTION_MATCH id='q39-44'> + <BODY> + <TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q39-44_a1' position='1'> + <TEXT><![CDATA[frog]]></TEXT> + </ANSWER> + <ANSWER id='q39-44_a2' position='2'> + <TEXT><![CDATA[cat]]></TEXT> + </ANSWER> + <ANSWER id='q39-44_a3' position='3'> + <TEXT><![CDATA[newt]]></TEXT> + </ANSWER> + <CHOICE id='q39-44_c1' position='1'> + <TEXT><![CDATA[mammal]]></TEXT> + </CHOICE> + <CHOICE id='q39-44_c2' position='2'> + <TEXT><![CDATA[insect]]></TEXT> + </CHOICE> + <CHOICE id='q39-44_c3' position='3'> + <TEXT><![CDATA[amphibian]]></TEXT> + </CHOICE> + <GRADABLE> + <CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/> + <CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/> + <CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/> + </GRADABLE> + </QUESTION_MATCH> + <QUESTION_ESSAY id='q9'> + <BODY> + <TEXT><![CDATA[How are you?]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q9_a1'> + <TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT> + </ANSWER> + <GRADABLE> + </GRADABLE> + </QUESTION_ESSAY> + <QUESTION_FILLINBLANK id='q27'> + <BODY> + <TEXT><![CDATA[<span style=\"font-size:12pt\">Name an amphibian: __________.</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q27_a1' position='1'> + <TEXT>frog</TEXT> + </ANSWER> + <GRADABLE> + </GRADABLE> + </QUESTION_FILLINBLANK></POOL>"; + return $xml; + } + public function test_import_match() { + + $xmldata = xmlize($this->make_test_xml()); + $questions = array(); + + $importer = new qformat_blackboard(); + $importer->process_matching($xmldata, $questions); + $q = $questions[0]; + $expectedq = new stdClass(); + $expectedq->qtype = 'match'; + $expectedq->name = 'Classify the animals.'; + $expectedq->questiontext = '<i>Classify the animals.</i>'; + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array('text' => '', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->partiallycorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->incorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->penalty = 0.3333333; + $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers'); + $expectedq->subquestions = array( + array('text' => 'cat', 'format' => FORMAT_HTML, 'files' => array()), + array('text' => '', 'format' => FORMAT_HTML, 'files' => array()), + array('text' => 'frog', 'format' => FORMAT_HTML, 'files' => array()), + array('text' => 'newt', 'format' => FORMAT_HTML, 'files' => array())); + $expectedq->subanswers = array('mammal', 'insect', 'amphibian', 'amphibian'); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_multichoice_single() { + + $xmldata = xmlize($this->make_test_xml()); + $questions = array(); + + $importer = new qformat_blackboard(); + $importer->process_mc($xmldata, $questions); + $q = $questions[0]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'multichoice'; + $expectedq->single = 1; + $expectedq->name = 'What\'s between orange and green in the spectrum?'; + $expectedq->questiontext = '<span style="font-size:12pt">What\'s between orange and green in the spectrum?</span>'; + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array('text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->partiallycorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->incorrectfeedback = array('text' => 'Only yellow is between orange and green in the spectrum.', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->penalty = 0.3333333; + $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers'); + $expectedq->answer = array( + 0 => array( + 'text' => '<span style="font-size:12pt">red</span>', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 1 => array( + 'text' => '<span style="font-size:12pt">yellow</span>', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 2 => array( + 'text' => '<span style="font-size:12pt">blue</span>', + 'format' => FORMAT_HTML, + 'files' => array(), + ) + ); + $expectedq->fraction = array(0, 1, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 2 => array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_multichoice_multi() { + + $xmldata = xmlize($this->make_test_xml()); + $questions = array(); + + $importer = new qformat_blackboard(); + $importer->process_ma($xmldata, $questions); + $q = $questions[0]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'multichoice'; + $expectedq->single = 0; + $expectedq->name = 'What\'s between orange and green in the spectrum?'; + $expectedq->questiontext = '<span style="font-size:12pt">What\'s between orange and green in the spectrum?</span>'; + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array( + 'text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, + 'files' => array()); + $expectedq->partiallycorrectfeedback = array( + 'text' => 'Only yellow and off-beige are between orange and green in the spectrum.', + 'format' => FORMAT_HTML, + 'files' => array()); + $expectedq->incorrectfeedback = array( + 'text' => 'Only yellow and off-beige are between orange and green in the spectrum.', + 'format' => FORMAT_HTML, + 'files' => array()); + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->penalty = 0.3333333; + $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers'); + $expectedq->answer = array( + 0 => array( + 'text' => '<span style="font-size:12pt">yellow</span>', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 1 => array( + 'text' => '<span style="font-size:12pt">red</span>', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 2 => array( + 'text' => '<span style="font-size:12pt">off-beige</span>', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 3 => array( + 'text' => '<span style="font-size:12pt">blue</span>', + 'format' => FORMAT_HTML, + 'files' => array(), + ) + ); + $expectedq->fraction = array(0.5, 0, 0.5, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 2 => array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 3 => array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_truefalse() { + + $xmldata = xmlize($this->make_test_xml()); + $questions = array(); + + $importer = new qformat_blackboard(); + $importer->process_tf($xmldata, $questions); + $q = $questions[0]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'truefalse'; + $expectedq->name = '42 is the Absolute Answer to everything.'; + $expectedq->questiontext = '<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>'; + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->correctanswer = 0; + $expectedq->feedbacktrue = array( + 'text' => '42 is the Ultimate Answer.', + 'format' => FORMAT_HTML, + 'files' => array(), + ); + $expectedq->feedbackfalse = array( + 'text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, + 'files' => array(), + ); + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_fill_in_the_blank() { + + $xmldata = xmlize($this->make_test_xml()); + $questions = array(); + + $importer = new qformat_blackboard(); + $importer->process_fib($xmldata, $questions); + $q = $questions[0]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'shortanswer'; + $expectedq->name = 'Name an amphibian: __________.'; + $expectedq->questiontext = '<span style="font-size:12pt">Name an amphibian: __________.</span>'; + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->usecase = 0; + $expectedq->answer = array('frog', '*'); + $expectedq->fraction = array(1, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_essay() { + + $xmldata = xmlize($this->make_test_xml()); + $questions = array(); + + $importer = new qformat_blackboard(); + $importer->process_essay($xmldata, $questions); + $q = $questions[0]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'essay'; + $expectedq->name = 'How are you?'; + $expectedq->questiontext = 'How are you?'; + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->responseformat = 'editor'; + $expectedq->responsefieldlines = 15; + $expectedq->attachments = 0; + $expectedq->graderinfo = array( + 'text' => 'Blackboard answer for essay questions will be imported as informations for graders.', + 'format' => FORMAT_HTML, + 'files' => array()); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } +} diff --git a/question/format/blackboard/tests/fixtures/sample_blackboard.dat b/question/format/blackboard/tests/fixtures/sample_blackboard.dat new file mode 100644 index 00000000000..88088c36fa2 --- /dev/null +++ b/question/format/blackboard/tests/fixtures/sample_blackboard.dat @@ -0,0 +1,142 @@ +<?xml version='1.0' encoding='utf-8'?> +<POOL> + <TITLE value='exam 3 2008-9'/> + <QUESTIONLIST> + <QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/> + <QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/> + <QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/> + <QUESTION id='q39-44' class='QUESTION_MATCH' points='1'/> + <QUESTION id='q9' class='QUESTION_ESSAY' points='1'/> + <QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/> + </QUESTIONLIST> + <QUESTION_TRUEFALSE id='q1'> + <BODY> + <TEXT><![CDATA[<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q1_a1'> + <TEXT>False</TEXT> + </ANSWER> + <ANSWER id='q1_a2'> + <TEXT>True</TEXT> + </ANSWER> + <GRADABLE> + <CORRECTANSWER answer_id='q1_a2'/> + <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT> + <FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT> + </GRADABLE> + </QUESTION_TRUEFALSE> + <QUESTION_MULTIPLECHOICE id='q7'> + <BODY> + <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q7_a1' position='1'> + <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT> + </ANSWER> + <ANSWER id='q7_a2' position='2'> + <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT> + </ANSWER> + <ANSWER id='q7_a3' position='3'> + <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT> + </ANSWER> + <GRADABLE> + <CORRECTANSWER answer_id='q7_a2'/> + <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT> + <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT> + </GRADABLE> + </QUESTION_MULTIPLECHOICE> + <QUESTION_MULTIPLEANSWER id='q8'> + <BODY> + <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q8_a1' position='1'> + <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT> + </ANSWER> + <ANSWER id='q8_a2' position='2'> + <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT> + </ANSWER> + <ANSWER id='q8_a3' position='3'> + <TEXT><![CDATA[<span style="font-size:12pt">off-beige</span>]]></TEXT> + </ANSWER> + <ANSWER id='q8_a4' position='4'> + <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT> + </ANSWER> + <GRADABLE> + <CORRECTANSWER answer_id='q8_a1'/> + <CORRECTANSWER answer_id='q8_a3'/> + <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT> + <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT> + </GRADABLE> + </QUESTION_MULTIPLEANSWER> + <QUESTION_MATCH id='q39-44'> + <BODY> + <TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q39-44_a1' position='1'> + <TEXT><![CDATA[frog]]></TEXT> + </ANSWER> + <ANSWER id='q39-44_a2' position='2'> + <TEXT><![CDATA[cat]]></TEXT> + </ANSWER> + <ANSWER id='q39-44_a3' position='3'> + <TEXT><![CDATA[newt]]></TEXT> + </ANSWER> + <CHOICE id='q39-44_c1' position='1'> + <TEXT><![CDATA[mammal]]></TEXT> + </CHOICE> + <CHOICE id='q39-44_c2' position='2'> + <TEXT><![CDATA[insect]]></TEXT> + </CHOICE> + <CHOICE id='q39-44_c3' position='3'> + <TEXT><![CDATA[amphibian]]></TEXT> + </CHOICE> + <GRADABLE> + <CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/> + <CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/> + <CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/> + </GRADABLE> + </QUESTION_MATCH> + <QUESTION_ESSAY id='q9'> + <BODY> + <TEXT><![CDATA[How are you?]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q9_a1'> + <TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT> + </ANSWER> + <GRADABLE> + </GRADABLE> + </QUESTION_ESSAY> + <QUESTION_FILLINBLANK id='q27'> + <BODY> + <TEXT><![CDATA[Name an amphibian: __________.]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q27_a1' position='1'> + <TEXT>frog</TEXT> + </ANSWER> + <GRADABLE> + </GRADABLE> + </QUESTION_FILLINBLANK> +</POOL> diff --git a/question/format/blackboard/version.php b/question/format/blackboard/version.php index f2334f31f78..53948abb2d1 100644 --- a/question/format/blackboard/version.php +++ b/question/format/blackboard/version.php @@ -17,8 +17,7 @@ /** * Version information for the calculated question type. * - * @package qformat - * @subpackage blackboard + * @package qformat_blackboard * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -- 2.43.0