. /** * Blackboard V5 and V6 question importer. * * @package qformat_blackboard_six * @copyright 2005 Michael Penney * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/xmlize.php'); /** * Blackboard 6.0 question importer. * * @copyright 2005 Michael Penney * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class qformat_blackboard_six_qti extends qformat_blackboard_six_base { /** * 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 of lines from the input file. * @param stdClass $context * @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(); // First step : we are only interested in the tags. $rawquestions = $this->getpath($xml, array('questestinterop', '#', 'assessment', 0, '#', 'section', 0, '#', 'item'), array(), false); // Each tag contains data related to a single question. foreach ($rawquestions as $quest) { // Second step : parse each question data into the intermediate // rawquestion structure array. // Warning : rawquestions are not Moodle questions. $question = $this->create_raw_question($quest); // Third step : convert a rawquestion into a Moodle question. switch($question->qtype) { case "Matching": $this->process_matching($question, $questions); break; case "Multiple Choice": $this->process_mc($question, $questions); break; case "Essay": $this->process_essay($question, $questions); break; case "Multiple Answer": $this->process_ma($question, $questions); break; case "True/False": $this->process_tf($question, $questions); break; case 'Fill in the Blank': $this->process_fblank($question, $questions); break; case 'Short Response': $this->process_essay($question, $questions); break; default: $this->error(get_string('unknownorunhandledtype', 'qformat_blackboard_six', $question->qtype)); break; } } return $questions; } /** * Creates a cleaner object to deal with for processing into Moodle. * The object returned is NOT a moodle question object. * @param array $quest XML question data * @return object rawquestion */ public function create_raw_question($quest) { $rawquestion = new stdClass(); $rawquestion->qtype = $this->getpath($quest, array('#', 'itemmetadata', 0, '#', 'bbmd_questiontype', 0, '#'), '', true); $rawquestion->id = $this->getpath($quest, array('#', 'itemmetadata', 0, '#', 'bbmd_asi_object_id', 0, '#'), '', true); $presentation = new stdClass(); $presentation->blocks = $this->getpath($quest, array('#', 'presentation', 0, '#', 'flow', 0, '#', 'flow'), array(), false); foreach ($presentation->blocks as $pblock) { $block = new stdClass(); $block->type = $this->getpath($pblock, array('@', 'class'), '', true); switch($block->type) { case 'QUESTION_BLOCK': $subblocks = $this->getpath($pblock, array('#', 'flow'), array(), false); foreach ($subblocks as $sblock) { $this->process_block($sblock, $block); } break; case 'RESPONSE_BLOCK': $choices = null; switch($rawquestion->qtype) { case 'Matching': $bbsubquestions = $this->getpath($pblock, array('#', 'flow'), array(), false); $sub_questions = array(); foreach ($bbsubquestions as $bbsubquestion) { $sub_question = new stdClass(); $sub_question->ident = $this->getpath($bbsubquestion, array('#', 'response_lid', 0, '@', 'ident'), '', true); $this->process_block($this->getpath($bbsubquestion, array('#', 'flow', 0), false, false), $sub_question); $bbchoices = $this->getpath($bbsubquestion, array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label', 0, '#', 'response_label'), array(), false); $choices = array(); $this->process_choices($bbchoices, $choices); $sub_question->choices = $choices; if (!isset($block->subquestions)) { $block->subquestions = array(); } $block->subquestions[] = $sub_question; } break; case 'Multiple Answer': $bbchoices = $this->getpath($pblock, array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'), array(), false); $choices = array(); $this->process_choices($bbchoices, $choices); $block->choices = $choices; break; case 'Essay': // Doesn't apply since the user responds with text input. break; case 'Multiple Choice': $mcchoices = $this->getpath($pblock, array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'), array(), false); foreach ($mcchoices as $mcchoice) { $choices = new stdClass(); $choices = $this->process_block($mcchoice, $choices); $block->choices[] = $choices; } break; case 'Short Response': // Do nothing? break; case 'Fill in the Blank': // Do nothing? break; default: $bbchoices = $this->getpath($pblock, array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label', 0, '#', 'response_label'), array(), false); $choices = array(); $this->process_choices($bbchoices, $choices); $block->choices = $choices; } break; case 'RIGHT_MATCH_BLOCK': $matchinganswerset = $this->getpath($pblock, array('#', 'flow'), false, false); $answerset = array(); foreach ($matchinganswerset as $answer) { $bbanswer = new stdClass; $bbanswer->text = $this->getpath($answer, array('#', 'flow', 0, '#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'), false, false); $answerset[] = $bbanswer; } $block->matchinganswerset = $answerset; break; default: $this->error(get_string('unhandledpresblock', 'qformat_blackboard_six')); break; } $rawquestion->{$block->type} = $block; } // Determine response processing. // There is a section called 'outcomes' that I don't know what to do with. $resprocessing = $this->getpath($quest, array('#', 'resprocessing'), array(), false); $respconditions = $this->getpath($resprocessing[0], array('#', 'respcondition'), array(), false); $responses = array(); if ($rawquestion->qtype == 'Matching') { $this->process_matching_responses($respconditions, $responses); } else { $this->process_responses($respconditions, $responses); } $rawquestion->responses = $responses; $feedbackset = $this->getpath($quest, array('#', 'itemfeedback'), array(), false); $feedbacks = array(); $this->process_feedback($feedbackset, $feedbacks); $rawquestion->feedback = $feedbacks; return $rawquestion; } /** * Helper function to process an XML block into an object. * Can call himself recursively if necessary to parse this branch of the XML tree. * @param array $curblock XML block to parse * @return object $block parsed */ public function process_block($curblock, $block) { $curtype = $this->getpath($curblock, array('@', 'class'), '', true); switch($curtype) { case 'FORMATTED_TEXT_BLOCK': $text = $this->getpath($curblock, array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'), '', true); $block->text = $this->strip_applet_tags_get_mathml($text); break; case 'FILE_BLOCK': $block->filename = $this->getpath($curblock, array('#', 'material', 0, '#'), '', true); if ($block->filename != '') { // TODO : determine what to do with the file's content. $this->error(get_string('filenothandled', 'qformat_blackboard_six', $block->filename)); } break; case 'Block': if ($this->getpath($curblock, array('#', 'material', 0, '#', 'mattext'), false, false)) { $block->text = $this->getpath($curblock, array('#', 'material', 0, '#', 'mattext', 0, '#'), '', true); } else if ($this->getpath($curblock, array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext'), false, false)) { $block->text = $this->getpath($curblock, array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'), '', true); } else if ($this->getpath($curblock, array('#', 'response_label'), false, false)) { // This is a response label block. $subblocks = $this->getpath($curblock, array('#', 'response_label', 0), array(), false); if (!isset($block->ident)) { if ($this->getpath($subblocks, array('@', 'ident'), '', true)) { $block->ident = $this->getpath($subblocks, array('@', 'ident'), '', true); } } foreach ($this->getpath($subblocks, array('#', 'flow_mat'), array(), false) as $subblock) { $this->process_block($subblock, $block); } } else { if ($this->getpath($curblock, array('#', 'flow_mat'), false, false) || $this->getpath($curblock, array('#', 'flow'), false, false)) { if ($this->getpath($curblock, array('#', 'flow_mat'), false, false)) { $subblocks = $this->getpath($curblock, array('#', 'flow_mat'), array(), false); } else if ($this->getpath($curblock, array('#', 'flow'), false, false)) { $subblocks = $this->getpath($curblock, array('#', 'flow'), array(), false); } foreach ($subblocks as $sblock) { // This will recursively grab the sub blocks which should be of one of the other types. $this->process_block($sblock, $block); } } } break; case 'LINK_BLOCK': // Not sure how this should be included? $link = $this->getpath($curblock, array('#', 'material', 0, '#', 'mattext', 0, '@', 'uri'), '', true); if (!empty($link)) { $block->link = $link; } else { $block->link = ''; } break; } return $block; } /** * Preprocess XML blocks containing data for questions' choices. * Called by {@link create_raw_question()} * for matching, multichoice and fill in the blank questions. * @param array $bbchoices XML block to parse * @param array $choices array of choices suitable for a rawquestion. */ protected function process_choices($bbchoices, &$choices) { foreach ($bbchoices as $choice) { if ($this->getpath($choice, array('@', 'ident'), '', true)) { $curchoice = $this->getpath($choice, array('@', 'ident'), '', true); } else { // For multiple answers. $curchoice = $this->getpath($choice, array('#', 'response_label', 0), array(), false); } if ($this->getpath($choice, array('#', 'flow_mat', 0), false, false)) { // For multiple answers. $curblock = $this->getpath($choice, array('#', 'flow_mat', 0), false, false); // Reset $curchoice to new stdClass because process_block is expecting an object // for the second argument and not a string, // which is what is was set as originally - CT 8/7/06. $curchoice = new stdClass(); $this->process_block($curblock, $curchoice); } else if ($this->getpath($choice, array('#', 'response_label'), false, false)) { // Reset $curchoice to new stdClass because process_block is expecting an object // for the second argument and not a string, // which is what is was set as originally - CT 8/7/06. $curchoice = new stdClass(); $this->process_block($choice, $curchoice); } $choices[] = $curchoice; } } /** * Preprocess XML blocks containing data for subanswers * Called by {@link create_raw_question()} * for matching questions only. * @param array $bbresponses XML block to parse * @param array $responses array of responses suitable for a matching rawquestion. */ protected function process_matching_responses($bbresponses, &$responses) { foreach ($bbresponses as $bbresponse) { $response = new stdClass; if ($this->getpath($bbresponse, array('#', 'conditionvar', 0, '#', 'varequal'), false, false)) { $response->correct = $this->getpath($bbresponse, array('#', 'conditionvar', 0, '#', 'varequal', 0, '#'), '', true); $response->ident = $this->getpath($bbresponse, array('#', 'conditionvar', 0, '#', 'varequal', 0, '@', 'respident'), '', true); } // Suppressed an else block because if the above if condition is false, // the question is not necessary a broken one, most of the time it's an tag. $response->feedback = $this->getpath($bbresponse, array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); $responses[] = $response; } } /** * Preprocess XML blocks containing data for responses processing. * Called by {@link create_raw_question()} * for all questions types. * @param array $bbresponses XML block to parse * @param array $responses array of responses suitable for a rawquestion. */ protected function process_responses($bbresponses, &$responses) { foreach ($bbresponses as $bbresponse) { $response = new stdClass(); if ($this->getpath($bbresponse, array('@', 'title'), '', true)) { $response->title = $this->getpath($bbresponse, array('@', 'title'), '', true); } else { $response->title = $this->getpath($bbresponse, array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); } $response->ident = array(); if ($this->getpath($bbresponse, array('#', 'conditionvar', 0, '#'), false, false)) { $response->ident[0] = $this->getpath($bbresponse, array('#', 'conditionvar', 0, '#'), array(), false); } else if ($this->getpath($bbresponse, array('#', 'conditionvar', 0, '#', 'other', 0, '#'), false, false)) { $response->ident[0] = $this->getpath($bbresponse, array('#', 'conditionvar', 0, '#', 'other', 0, '#'), array(), false); } if ($this->getpath($bbresponse, array('#', 'conditionvar', 0, '#', 'and'), false, false)) { $responseset = $this->getpath($bbresponse, array('#', 'conditionvar', 0, '#', 'and'), array(), false); foreach ($responseset as $rs) { $response->ident[] = $this->getpath($rs, array('#'), array(), false); if (!isset($response->feedback) and $this->getpath($rs, array('@'), false, false)) { $response->feedback = $this->getpath($rs, array('@', 'respident'), '', true); } } } else { $response->feedback = $this->getpath($bbresponse, array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); } // Determine what fraction to give response. if ($this->getpath($bbresponse, array('#', 'setvar'), false, false)) { switch ($this->getpath($bbresponse, array('#', 'setvar', 0, '#'), false, false)) { case "SCORE.max": $response->fraction = 1; break; default: // I have only seen this being 0 or unset. // There are probably fractional values of SCORE.max, but I'm not sure what they look like. $response->fraction = 0; break; } } else { // Just going to assume this is the case this is probably not correct. $response->fraction = 0; } $responses[] = $response; } } /** * Preprocess XML blocks containing data for responses feedbacks. * Called by {@link create_raw_question()} * for all questions types. * @param array $feedbackset XML block to parse * @param array $feedbacks array of feedbacks suitable for a rawquestion. */ public function process_feedback($feedbackset, &$feedbacks) { foreach ($feedbackset as $bb_feedback) { $feedback = new stdClass(); $feedback->ident = $this->getpath($bb_feedback, array('@', 'ident'), '', true); $feedback->text = ''; if ($this->getpath($bb_feedback, array('#', 'flow_mat', 0), false, false)) { $this->process_block($this->getpath($bb_feedback, array('#', 'flow_mat', 0), false, false), $feedback); } else if ($this->getpath($bb_feedback, array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false)) { $this->process_block($this->getpath($bb_feedback, array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false), $feedback); } $feedbacks[$feedback->ident] = $feedback; } } /** * Create common parts of question * @param object $quest rawquestion * @return object Moodle question. */ public function process_common($quest) { $question = $this->defaultquestion(); $text = $quest->QUESTION_BLOCK->text; $question->questiontext = $this->cleaned_text_field($text); $question->questiontextformat = FORMAT_HTML; // Needed because add_blank_combined_feedback uses it. $question->name = $this->create_default_question_name($question->questiontext['text'], get_string('defaultname', 'qformat_blackboard_six' , $quest->id)); $question->generalfeedback = ''; $question->generalfeedbackformat = FORMAT_HTML; $question->generalfeedbackfiles = array(); return $question; } /** * Process True / False Questions * Parse a truefalse rawquestion and add the result * to the array of questions already parsed. * @param object $quest rawquestion * @param $questions array of Moodle questions already done. */ protected function process_tf($quest, &$questions) { $question = $this->process_common($quest); $question->qtype = 'truefalse'; $question->single = 1; // Only one answer is allowed. $question->penalty = 1; // Penalty = 1 for truefalse questions. // 0th [response] is the correct answer. $responses = $quest->responses; $correctresponse = $this->getpath($responses[0]->ident[0], array('varequal', 0, '#'), '', true); if ($correctresponse != 'false') { $correct = true; } else { $correct = false; } $fback = new stdClass(); foreach ($quest->feedback as $fb) { $fback->{$fb->ident} = $fb->text; } if ($correct) { // True is correct. $question->answer = 1; $question->feedbacktrue = $this->cleaned_text_field($fback->correct); $question->feedbackfalse = $this->cleaned_text_field($fback->incorrect); } else { // False is correct. $question->answer = 0; $question->feedbacktrue = $this->cleaned_text_field($fback->incorrect); $question->feedbackfalse = $this->cleaned_text_field($fback->correct); } $question->correctanswer = $question->answer; $questions[] = $question; } /** * Process Fill in the Blank Questions * Parse a fillintheblank rawquestion and add the result * to the array of questions already parsed. * @param object $quest rawquestion * @param $questions array of Moodle questions already done. */ protected function process_fblank($quest, &$questions) { $question = $this->process_common($quest); $question->qtype = 'shortanswer'; $question->usecase = 0; // Ignore case. $answers = array(); $fractions = array(); $feedbacks = array(); // Extract the feedback. $feedback = array(); foreach ($quest->feedback as $fback) { if (isset($fback->ident)) { if ($fback->ident == 'correct' || $fback->ident == 'incorrect') { $feedback[$fback->ident] = $fback->text; } } } foreach ($quest->responses as $response) { if (isset($response->title)) { if ($this->getpath($response->ident[0], array('varequal', 0, '#'), false, false)) { // For BB Fill in the Blank, only interested in correct answers. if ($response->feedback = 'correct') { $answers[] = $this->getpath($response->ident[0], array('varequal', 0, '#'), '', true); $fractions[] = 1; if (isset($feedback['correct'])) { $feedbacks[] = $this->cleaned_text_field($feedback['correct']); } else { $feedbacks[] = $this->text_field(''); } } } } } // Adding catchall to so that students can see feedback for incorrect answers when they enter something, // the instructor did not enter. $answers[] = '*'; $fractions[] = 0; if (isset($feedback['incorrect'])) { $feedbacks[] = $this->cleaned_text_field($feedback['incorrect']); } else { $feedbacks[] = $this->text_field(''); } $question->answer = $answers; $question->fraction = $fractions; $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of. if (!empty($question)) { $questions[] = $question; } } /** * Process Multichoice Questions * Parse a multichoice single answer rawquestion and add the result * to the array of questions already parsed. * @param object $quest rawquestion * @param $questions array of Moodle questions already done. */ protected function process_mc($quest, &$questions) { $question = $this->process_common($quest); $question->qtype = 'multichoice'; $question = $this->add_blank_combined_feedback($question); $question->single = 1; $feedback = array(); foreach ($quest->feedback as $fback) { $feedback[$fback->ident] = $fback->text; } foreach ($quest->responses as $response) { if (isset($response->title)) { if ($response->title == 'correct') { // Only one answer possible for this qtype so first index is correct answer. $correct = $this->getpath($response->ident[0], array('varequal', 0, '#'), '', true); } } else { // Fallback method for when the title is not set. if ($response->feedback == 'correct') { // Only one answer possible for this qtype so first index is correct answer. $correct = $this->getpath($response->ident[0], array('varequal', 0, '#'), '', true); } } } $i = 0; foreach ($quest->RESPONSE_BLOCK->choices as $response) { $question->answer[$i] = $this->cleaned_text_field($response->text); if ($correct == $response->ident) { $question->fraction[$i] = 1; // This is a bit of a hack to catch the feedback... first we see if a 'specific' // feedback for this response exists, then if a 'correct' feedback exists. if (!empty($feedback[$response->ident]) ) { $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]); } else if (!empty($feedback['correct'])) { $question->feedback[$i] = $this->cleaned_text_field($feedback['correct']); } else if (!empty($feedback[$i])) { $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]); } else { $question->feedback[$i] = $this->cleaned_text_field(get_string('correct', 'question')); } } else { $question->fraction[$i] = 0; if (!empty($feedback[$response->ident]) ) { $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]); } else if (!empty($feedback['incorrect'])) { $question->feedback[$i] = $this->cleaned_text_field($feedback['incorrect']); } else if (!empty($feedback[$i])) { $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]); } else { $question->feedback[$i] = $this->cleaned_text_field(get_string('incorrect', 'question')); } } $i++; } if (!empty($question)) { $questions[] = $question; } } /** * Process Multiple Choice Questions With Multiple Answers. * Parse a multichoice multianswer rawquestion and add the result * to the array of questions already parsed. * @param object $quest rawquestion * @param $questions array of Moodle questions already done. */ public function process_ma($quest, &$questions) { $question = $this->process_common($quest); $question->qtype = 'multichoice'; $question = $this->add_blank_combined_feedback($question); $question->single = 0; // More than one answer allowed. $answers = $quest->responses; $correctanswers = array(); foreach ($answers as $answer) { if ($answer->title == 'correct') { $answerset = $this->getpath($answer->ident[0], array('and', 0, '#', 'varequal'), array(), false); foreach ($answerset as $ans) { $correctanswers[] = $ans['#']; } } } $feedback = new stdClass(); foreach ($quest->feedback as $fb) { $feedback->{$fb->ident} = trim($fb->text); } $correctanswercount = count($correctanswers); $fraction = 1/$correctanswercount; $choiceset = $quest->RESPONSE_BLOCK->choices; $i = 0; foreach ($choiceset as $choice) { $question->answer[$i] = $this->cleaned_text_field(trim($choice->text)); if (in_array($choice->ident, $correctanswers)) { // Correct answer. $question->fraction[$i] = $fraction; $question->feedback[$i] = $this->cleaned_text_field($feedback->correct); } else { // Wrong answer. $question->fraction[$i] = 0; $question->feedback[$i] = $this->cleaned_text_field($feedback->incorrect); } $i++; } $questions[] = $question; } /** * Process Essay Questions * Parse an essay rawquestion and add the result * to the array of questions already parsed. * @param object $quest rawquestion * @param $questions array of Moodle questions already done. */ public function process_essay($quest, &$questions) { $question = $this->process_common($quest); $question->qtype = 'essay'; $question->feedback = array(); // Not sure where to get the correct answer from? foreach ($quest->feedback as $feedback) { // Added this code to put the possible solution that the // instructor gives as the Moodle answer for an essay question. if ($feedback->ident == 'solution') { $question->graderinfo = $this->cleaned_text_field($feedback->text); } } // Added because essay/questiontype.php:save_question_option is expecting a // fraction property - CT 8/10/06. $question->fraction[] = 1; $question->defaultmark = 1; $question->responseformat = 'editor'; $question->responsefieldlines = 15; $question->attachments = 0; $questions[]=$question; } /** * Process Matching Questions * Parse a matching rawquestion and add the result * to the array of questions already parsed. * @param object $quest rawquestion * @param $questions array of Moodle questions already done. */ public function process_matching($quest, &$questions) { // Blackboard matching 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'); $question = $this->process_common($quest); $question = $this->add_blank_combined_feedback($question); $question->valid = true; if ($ddmatchisinstalled) { $question->qtype = 'ddmatch'; } else { $question->qtype = 'match'; } // Construction of the array holding mappings between subanswers and subquestions. foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { foreach ($quest->responses as $rid => $resp) { if (isset($resp->ident) && $resp->ident == $subq->ident) { $correct = $resp->correct; } } foreach ($subq->choices as $cid => $choice) { if ($choice == $correct) { $mappings[$subq->ident] = $cid; } } } foreach ($subq->choices as $choiceid => $choice) { $subanswertext = $quest->RIGHT_MATCH_BLOCK->matchinganswerset[$choiceid]->text; if ($ddmatchisinstalled) { $subanswer = $this->cleaned_text_field($subanswertext); } else { $subanswertext = html_to_text($this->cleaninput($subanswertext), 0); $subanswer = $subanswertext; } if ($subanswertext != '') { // Only import non empty subanswers. $subquestion = ''; $fiber = array_keys ($mappings, $choiceid); foreach ($fiber as $correctanswerid) { // We have found a correspondance for this subanswer so we need to take the associated subquestion. foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { $currentsubqid = $subq->ident; if (strcmp ($currentsubqid, $correctanswerid) == 0) { $subquestion = $subq->text; break; } } $question->subquestions[] = $this->cleaned_text_field($subquestion); $question->subanswers[] = $subanswer; } if ($subquestion == '') { // Then in this case, $choice is a distractor. $question->subquestions[] = $this->text_field(''); $question->subanswers[] = $subanswer; } } } // 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['text'])); } else { $questions[] = $question; } } /** * Strip the applet tag used by Blackboard to render mathml formulas, * keeping the mathml tag. * @param string $string * @return string */ public function strip_applet_tags_get_mathml($string) { if (stristr($string, '') === false) { return $string; } else { // Strip all applet tags keeping stuff before/after and inbetween (if mathml) them. while (stristr($string, '') !== false) { preg_match("/(.*)\.*\<\/math\>)\".*\<\/applet\>(.*)/i", $string, $mathmls); $string = $mathmls[1].$mathmls[2].$mathmls[3]; } return $string; } } }