. /** * Blackboard V5 and V6 question importer. * * @package qformat_blackboard_six * @copyright 2003 Scott Elliott * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/xmlize.php'); /** * Blackboard pool question importer class. * * @package qformat_blackboard_six * @copyright 2003 Scott Elliott * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class qformat_blackboard_six_pool extends qformat_blackboard_six_base { /** * @var bool Is the current question's question text escaped HTML * (true for most if not all Blackboard files). */ public $ishtml = true; /** * Parse the xml document into an array of questions * * This *could* burn memory - but it won't happen that much * so fingers crossed! * * @param array $text array of lines from the input file. * @return array (of objects) questions objects. */ protected function readquestions($text) { // 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(); $this->process_category($xml, $questions); $this->process_tf($xml, $questions); $this->process_mc($xml, $questions); $this->process_ma($xml, $questions); $this->process_fib($xml, $questions); $this->process_matching($xml, $questions); $this->process_essay($xml, $questions); return $questions; } /** * 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) { // 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_six')); $questiontext = $this->cleaned_text_field($text); $question->questiontext = $questiontext['text']; $question->questiontextformat = $questiontext['format']; // Needed because add_blank_combined_feedback uses it. if (isset($questiontext['itemid'])) { $question->questiontextitemid = $questiontext['itemid']; } // Put name in question object. We must ensure it is not empty and it is less than 250 chars. $id = $this->getpath($questiondata, array('@', 'id'), '', true); $question->name = $this->create_default_question_name($question->questiontext, get_string('defaultname', 'qformat_blackboard_six' , $id)); $question->generalfeedback = ''; $question->generalfeedbackformat = FORMAT_HTML; $question->generalfeedbackfiles = array(); // TODO : read the mark from the POOL TITLE QUESTIONLIST section. $question->defaultmark = 1; return $question; } /** * Add a category question entry based on the pool file title * @param array $xml the xml tree * @param array $questions the questions already parsed */ public function process_category($xml, &$questions) { $title = $this->getpath($xml, array('POOL', '#', 'TITLE', 0, '@', 'value'), '', true); $dummyquestion = new stdClass(); $dummyquestion->qtype = 'category'; $dummyquestion->category = $this->cleaninput($this->clean_question_name($title)); $questions[] = $dummyquestion; } /** * 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; } foreach ($essayquestions as $thisquestion) { $question = $this->process_common($thisquestion); $question->qtype = 'essay'; $question->answer = ''; $answer = $this->getpath($thisquestion, array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true); $question->graderinfo = $this->cleaned_text_field($answer); $question->responsetemplate = $this->text_field(''); $question->feedback = ''; $question->responseformat = 'editor'; $question->responserequired = 1; $question->responsefieldlines = 15; $question->attachments = 0; $question->attachmentsrequired = 0; $question->fraction = 0; $questions[] = $question; } } /** * 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; } foreach ($tfquestions as $thisquestion) { $question = $this->process_common($thisquestion); $question->qtype = 'truefalse'; $question->single = 1; // Only one answer is allowed. $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false); $correctanswer = $this->getpath($thisquestion, array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'), '', true); // 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, $correctanswer) == 0) { // True is correct. $question->answer = 1; $question->feedbacktrue = $this->cleaned_text_field($correctfeedback); $question->feedbackfalse = $this->cleaned_text_field($incorrectfeedback); } else { // False is correct. $question->answer = 0; $question->feedbacktrue = $this->cleaned_text_field($incorrectfeedback); $question->feedbackfalse = $this->cleaned_text_field($correctfeedback); } $question->correctanswer = $question->answer; $questions[] = $question; } } /** * 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; } 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->cleaned_text_field($correctfeedback); $question->partiallycorrectfeedback = $this->text_field(''); $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback); $question->qtype = 'multichoice'; $question->single = 1; // Only one answer is allowed. $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); $correctanswerid = $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->cleaned_text_field($choicetext); $choiceid = $this->getpath($choice, array('@', 'id'), '', true); // If choice is the right answer, give 100% mark, otherwise give 0%. if (strcmp ($choiceid, $correctanswerid) == 0) { $question->fraction[] = 1; } else { $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 * @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; } 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->cleaned_text_field($correctfeedback); // As there is no partially correct feedback we use incorrect one. $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback); $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback); $question->qtype = 'multichoice'; $question->defaultmark = 1; $question->single = 0; // More than one answers allowed. $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); $correctanswerids = array(); foreach ($this->getpath($thisquestion, array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) { if ($correctanswer) { $correctanswerids[] = $this->getpath($correctanswer, array('@', 'answer_id'), '', true); } } $fraction = 1 / count($correctanswerids); foreach ($choices as $choice) { $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true); // Put this choice in the question object. $question->answer[] = $this->cleaned_text_field($choicetext); $choiceid = $this->getpath($choice, array('@', 'id'), '', true); $iscorrect = in_array($choiceid, $correctanswerids); if ($iscorrect) { $question->fraction[] = $fraction; } else { $question->fraction[] = 0; } // There is never feedback specific to each choice. $question->feedback[] = $this->text_field(''); } $questions[] = $question; } } /** * 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; } 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->cleaned_text_field($correctfeedback); } $question->answer[] = '*'; $question->fraction[] = 0; $question->feedback[] = $this->cleaned_text_field($incorrectfeedback); $questions[] = $question; } } /** * 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; } // 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. $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch'); foreach ($matchquestions as $thisquestion) { $question = $this->process_common($thisquestion); if ($ddmatchisinstalled) { $question->qtype = 'ddmatch'; } else { $question->qtype = 'match'; } $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->cleaned_text_field($correctfeedback); // As there is no partially correct feedback we use incorrect one. $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback); $question->incorrectfeedback = $this->cleaned_text_field($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) { $correctchoiceid = $this->getpath($correctanswer, array('@', 'choice_id'), '', true); $correctanswerid = $this->getpath($correctanswer, array('@', 'answer_id'), '', true); $mappings[$correctanswerid] = $correctchoiceid; } } foreach ($choices as $choice) { if ($ddmatchisinstalled) { $choicetext = $this->cleaned_text_field($this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true)); } else { $choicetext = trim(strip_tags($this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true))); } if ($choicetext != '') { // Only import non empty subanswers. $subquestion = ''; $choiceid = $this->getpath($choice, array('@', 'id'), '', true); $fiber = array_search($choiceid, $mappings); $fiber = array_keys ($mappings, $choiceid); foreach ($fiber as $correctanswerid) { // We have found a correspondance for this choice so we need to take the associated answer. foreach ($answers as $answer) { $currentanswerid = $this->getpath($answer, array('@', 'id'), '', true); if (strcmp ($currentanswerid, $correctanswerid) == 0) { $subquestion = $this->getpath($answer, array('#', 'TEXT', 0, '#'), '', true); break; } } $question->subquestions[] = $this->cleaned_text_field($subquestion); $question->subanswers[] = $choicetext; } if ($subquestion == '') { // Then in this case, $choice is a distractor. $question->subquestions[] = $this->text_field(''); $question->subanswers[] = $choicetext; } } } // 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_six', $question->questiontext)); } else { $questions[] = $question; } } } }