See Release Notes
Long Term Support Release
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Blackboard V5 and V6 question importer. 19 * 20 * @package qformat_blackboard_six 21 * @copyright 2005 Michael Penney 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 require_once($CFG->libdir . '/xmlize.php'); 28 29 /** 30 * Blackboard 6.0 question importer. 31 * 32 * @copyright 2005 Michael Penney 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 */ 35 class qformat_blackboard_six_qti extends qformat_blackboard_six_base { 36 /** 37 * Parse the xml document into an array of questions 38 * this *could* burn memory - but it won't happen that much 39 * so fingers crossed! 40 * @param array $text array of lines from the input file. 41 * @return array (of objects) questions objects. 42 */ 43 protected function readquestions($text) { 44 45 // This converts xml to big nasty data structure, 46 // the 0 means keep white space as it is. 47 try { 48 $xml = xmlize($text, 0, 'UTF-8', true); 49 } catch (xml_format_exception $e) { 50 $this->error($e->getMessage(), ''); 51 return false; 52 } 53 54 $questions = array(); 55 56 // Treat the assessment title as a category title. 57 $this->process_category($xml, $questions); 58 59 // First step : we are only interested in the <item> tags. 60 $rawquestions = $this->getpath($xml, 61 array('questestinterop', '#', 'assessment', 0, '#', 'section', 0, '#', 'item'), 62 array(), false); 63 // Each <item> tag contains data related to a single question. 64 foreach ($rawquestions as $quest) { 65 // Second step : parse each question data into the intermediate 66 // rawquestion structure array. 67 // Warning : rawquestions are not Moodle questions. 68 $question = $this->create_raw_question($quest); 69 // Third step : convert a rawquestion into a Moodle question. 70 switch($question->qtype) { 71 case "Matching": 72 $this->process_matching($question, $questions); 73 break; 74 case "Multiple Choice": 75 $this->process_mc($question, $questions); 76 break; 77 case "Essay": 78 $this->process_essay($question, $questions); 79 break; 80 case "Multiple Answer": 81 $this->process_ma($question, $questions); 82 break; 83 case "True/False": 84 $this->process_tf($question, $questions); 85 break; 86 case 'Fill in the Blank': 87 $this->process_fblank($question, $questions); 88 break; 89 case 'Short Response': 90 $this->process_essay($question, $questions); 91 break; 92 default: 93 $this->error(get_string('unknownorunhandledtype', 'question', $question->qtype)); 94 break; 95 } 96 } 97 return $questions; 98 } 99 100 /** 101 * Creates a cleaner object to deal with for processing into Moodle. 102 * The object returned is NOT a moodle question object. 103 * @param array $quest XML <item> question data 104 * @return object rawquestion 105 */ 106 public function create_raw_question($quest) { 107 108 $rawquestion = new stdClass(); 109 $rawquestion->qtype = $this->getpath($quest, 110 array('#', 'itemmetadata', 0, '#', 'bbmd_questiontype', 0, '#'), 111 '', true); 112 $rawquestion->id = $this->getpath($quest, 113 array('#', 'itemmetadata', 0, '#', 'bbmd_asi_object_id', 0, '#'), 114 '', true); 115 $presentation = new stdClass(); 116 $presentation->blocks = $this->getpath($quest, 117 array('#', 'presentation', 0, '#', 'flow', 0, '#', 'flow'), 118 array(), false); 119 120 foreach ($presentation->blocks as $pblock) { 121 $block = new stdClass(); 122 $block->type = $this->getpath($pblock, 123 array('@', 'class'), 124 '', true); 125 126 switch($block->type) { 127 case 'QUESTION_BLOCK': 128 $subblocks = $this->getpath($pblock, 129 array('#', 'flow'), 130 array(), false); 131 foreach ($subblocks as $sblock) { 132 $this->process_block($sblock, $block); 133 } 134 break; 135 136 case 'RESPONSE_BLOCK': 137 $choices = null; 138 switch($rawquestion->qtype) { 139 case 'Matching': 140 $bbsubquestions = $this->getpath($pblock, 141 array('#', 'flow'), 142 array(), false); 143 foreach ($bbsubquestions as $bbsubquestion) { 144 $subquestion = new stdClass(); 145 $subquestion->ident = $this->getpath($bbsubquestion, 146 array('#', 'response_lid', 0, '@', 'ident'), 147 '', true); 148 $this->process_block($this->getpath($bbsubquestion, 149 array('#', 'flow', 0), 150 false, false), $subquestion); 151 $bbchoices = $this->getpath($bbsubquestion, 152 array('#', 'response_lid', 0, '#', 'render_choice', 0, 153 '#', 'flow_label', 0, '#', 'response_label'), 154 array(), false); 155 $choices = array(); 156 $this->process_choices($bbchoices, $choices); 157 $subquestion->choices = $choices; 158 if (!isset($block->subquestions)) { 159 $block->subquestions = array(); 160 } 161 $block->subquestions[] = $subquestion; 162 } 163 break; 164 case 'Multiple Answer': 165 $bbchoices = $this->getpath($pblock, 166 array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'), 167 array(), false); 168 $choices = array(); 169 $this->process_choices($bbchoices, $choices); 170 $block->choices = $choices; 171 break; 172 case 'Essay': 173 // Doesn't apply since the user responds with text input. 174 break; 175 case 'Multiple Choice': 176 $mcchoices = $this->getpath($pblock, 177 array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'), 178 array(), false); 179 foreach ($mcchoices as $mcchoice) { 180 $choices = new stdClass(); 181 $choices = $this->process_block($mcchoice, $choices); 182 $block->choices[] = $choices; 183 } 184 break; 185 case 'Short Response': 186 // Do nothing? 187 break; 188 case 'Fill in the Blank': 189 // Do nothing? 190 break; 191 default: 192 $bbchoices = $this->getpath($pblock, 193 array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 194 'flow_label', 0, '#', 'response_label'), 195 array(), false); 196 $choices = array(); 197 $this->process_choices($bbchoices, $choices); 198 $block->choices = $choices; 199 } 200 break; 201 case 'RIGHT_MATCH_BLOCK': 202 $matchinganswerset = $this->getpath($pblock, 203 array('#', 'flow'), 204 false, false); 205 206 $answerset = array(); 207 foreach ($matchinganswerset as $answer) { 208 $bbanswer = new stdClass; 209 $bbanswer->text = $this->getpath($answer, 210 array('#', 'flow', 0, '#', 'material', 0, '#', 'mat_extension', 211 0, '#', 'mat_formattedtext', 0, '#'), 212 false, false); 213 $answerset[] = $bbanswer; 214 } 215 $block->matchinganswerset = $answerset; 216 break; 217 default: 218 $this->error(get_string('unhandledpresblock', 'qformat_blackboard_six')); 219 break; 220 } 221 $rawquestion->{$block->type} = $block; 222 } 223 224 // Determine response processing. 225 // There is a section called 'outcomes' that I don't know what to do with. 226 $resprocessing = $this->getpath($quest, 227 array('#', 'resprocessing'), 228 array(), false); 229 230 $respconditions = $this->getpath($resprocessing[0], 231 array('#', 'respcondition'), 232 array(), false); 233 $responses = array(); 234 if ($rawquestion->qtype == 'Matching') { 235 $this->process_matching_responses($respconditions, $responses); 236 } else { 237 $this->process_responses($respconditions, $responses); 238 } 239 $rawquestion->responses = $responses; 240 $feedbackset = $this->getpath($quest, 241 array('#', 'itemfeedback'), 242 array(), false); 243 244 $feedbacks = array(); 245 $this->process_feedback($feedbackset, $feedbacks); 246 $rawquestion->feedback = $feedbacks; 247 return $rawquestion; 248 } 249 250 /** 251 * Helper function to process an XML block into an object. 252 * Can call himself recursively if necessary to parse this branch of the XML tree. 253 * @param array $curblock XML block to parse 254 * @param object $block block already parsed so far 255 * @return object $block parsed 256 */ 257 public function process_block($curblock, $block) { 258 259 $curtype = $this->getpath($curblock, 260 array('@', 'class'), 261 '', true); 262 263 switch($curtype) { 264 case 'FORMATTED_TEXT_BLOCK': 265 $text = $this->getpath($curblock, 266 array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'), 267 '', true); 268 $block->text = $this->strip_applet_tags_get_mathml($text); 269 break; 270 case 'FILE_BLOCK': 271 $block->filename = $this->getpath($curblock, 272 array('#', 'material', 0, '#'), 273 '', true); 274 if ($block->filename != '') { 275 // TODO : determine what to do with the file's content. 276 $this->error(get_string('filenothandled', 'qformat_blackboard_six', $block->filename)); 277 } 278 break; 279 case 'Block': 280 if ($this->getpath($curblock, 281 array('#', 'material', 0, '#', 'mattext'), 282 false, false)) { 283 $block->text = $this->getpath($curblock, 284 array('#', 'material', 0, '#', 'mattext', 0, '#'), 285 '', true); 286 } else if ($this->getpath($curblock, 287 array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext'), 288 false, false)) { 289 $block->text = $this->getpath($curblock, 290 array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'), 291 '', true); 292 } else if ($this->getpath($curblock, 293 array('#', 'response_label'), 294 false, false)) { 295 // This is a response label block. 296 $subblocks = $this->getpath($curblock, 297 array('#', 'response_label', 0), 298 array(), false); 299 if (!isset($block->ident)) { 300 301 if ($this->getpath($subblocks, 302 array('@', 'ident'), '', true)) { 303 $block->ident = $this->getpath($subblocks, 304 array('@', 'ident'), '', true); 305 } 306 } 307 foreach ($this->getpath($subblocks, 308 array('#', 'flow_mat'), array(), false) as $subblock) { 309 $this->process_block($subblock, $block); 310 } 311 } else { 312 if ($this->getpath($curblock, 313 array('#', 'flow_mat'), false, false) 314 || $this->getpath($curblock, 315 array('#', 'flow'), false, false)) { 316 if ($this->getpath($curblock, 317 array('#', 'flow_mat'), false, false)) { 318 $subblocks = $this->getpath($curblock, 319 array('#', 'flow_mat'), array(), false); 320 } else if ($this->getpath($curblock, 321 array('#', 'flow'), false, false)) { 322 $subblocks = $this->getpath($curblock, 323 array('#', 'flow'), array(), false); 324 } 325 foreach ($subblocks as $sblock) { 326 // This will recursively grab the sub blocks which should be of one of the other types. 327 $this->process_block($sblock, $block); 328 } 329 } 330 } 331 break; 332 case 'LINK_BLOCK': 333 // Not sure how this should be included? 334 $link = $this->getpath($curblock, 335 array('#', 'material', 0, '#', 'mattext', 0, '@', 'uri'), '', true); 336 if (!empty($link)) { 337 $block->link = $link; 338 } else { 339 $block->link = ''; 340 } 341 break; 342 } 343 return $block; 344 } 345 346 /** 347 * Preprocess XML blocks containing data for questions' choices. 348 * Called by {@link create_raw_question()} 349 * for matching, multichoice and fill in the blank questions. 350 * @param array $bbchoices XML block to parse 351 * @param array $choices array of choices suitable for a rawquestion. 352 */ 353 protected function process_choices($bbchoices, &$choices) { 354 foreach ($bbchoices as $choice) { 355 if ($this->getpath($choice, 356 array('@', 'ident'), '', true)) { 357 $curchoice = $this->getpath($choice, 358 array('@', 'ident'), '', true); 359 } else { // For multiple answers. 360 $curchoice = $this->getpath($choice, 361 array('#', 'response_label', 0), array(), false); 362 } 363 if ($this->getpath($choice, 364 array('#', 'flow_mat', 0), false, false)) { // For multiple answers. 365 $curblock = $this->getpath($choice, 366 array('#', 'flow_mat', 0), false, false); 367 // Reset $curchoice to new stdClass because process_block is expecting an object 368 // for the second argument and not a string, 369 // which is what is was set as originally - CT 8/7/06. 370 $curchoice = new stdClass(); 371 $this->process_block($curblock, $curchoice); 372 } else if ($this->getpath($choice, 373 array('#', 'response_label'), false, false)) { 374 // Reset $curchoice to new stdClass because process_block is expecting an object 375 // for the second argument and not a string, 376 // which is what is was set as originally - CT 8/7/06. 377 $curchoice = new stdClass(); 378 $this->process_block($choice, $curchoice); 379 } 380 $choices[] = $curchoice; 381 } 382 } 383 384 /** 385 * Preprocess XML blocks containing data for subanswers 386 * Called by {@link create_raw_question()} 387 * for matching questions only. 388 * @param array $bbresponses XML block to parse 389 * @param array $responses array of responses suitable for a matching rawquestion. 390 */ 391 protected function process_matching_responses($bbresponses, &$responses) { 392 foreach ($bbresponses as $bbresponse) { 393 $response = new stdClass; 394 if ($this->getpath($bbresponse, 395 array('#', 'conditionvar', 0, '#', 'varequal'), false, false)) { 396 $response->correct = $this->getpath($bbresponse, 397 array('#', 'conditionvar', 0, '#', 'varequal', 0, '#'), '', true); 398 $response->ident = $this->getpath($bbresponse, 399 array('#', 'conditionvar', 0, '#', 'varequal', 0, '@', 'respident'), '', true); 400 } 401 // Suppressed an else block because if the above if condition is false, 402 // the question is not necessary a broken one, most of the time it's an <other> tag. 403 404 $response->feedback = $this->getpath($bbresponse, 405 array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); 406 $responses[] = $response; 407 } 408 } 409 410 /** 411 * Preprocess XML blocks containing data for responses processing. 412 * Called by {@link create_raw_question()} 413 * for all questions types. 414 * @param array $bbresponses XML block to parse 415 * @param array $responses array of responses suitable for a rawquestion. 416 */ 417 protected function process_responses($bbresponses, &$responses) { 418 foreach ($bbresponses as $bbresponse) { 419 $response = new stdClass(); 420 if ($this->getpath($bbresponse, 421 array('@', 'title'), '', true)) { 422 $response->title = $this->getpath($bbresponse, 423 array('@', 'title'), '', true); 424 } else { 425 $response->title = $this->getpath($bbresponse, 426 array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); 427 } 428 $response->ident = array(); 429 if ($this->getpath($bbresponse, 430 array('#', 'conditionvar', 0, '#'), false, false)) { 431 $response->ident[0] = $this->getpath($bbresponse, 432 array('#', 'conditionvar', 0, '#'), array(), false); 433 } else if ($this->getpath($bbresponse, 434 array('#', 'conditionvar', 0, '#', 'other', 0, '#'), false, false)) { 435 $response->ident[0] = $this->getpath($bbresponse, 436 array('#', 'conditionvar', 0, '#', 'other', 0, '#'), array(), false); 437 } 438 if ($this->getpath($bbresponse, 439 array('#', 'conditionvar', 0, '#', 'and'), false, false)) { 440 $responseset = $this->getpath($bbresponse, 441 array('#', 'conditionvar', 0, '#', 'and'), array(), false); 442 foreach ($responseset as $rs) { 443 $response->ident[] = $this->getpath($rs, array('#'), array(), false); 444 if (!isset($response->feedback) and $this->getpath($rs, array('@'), false, false)) { 445 $response->feedback = $this->getpath($rs, 446 array('@', 'respident'), '', true); 447 } 448 } 449 } else { 450 $response->feedback = $this->getpath($bbresponse, 451 array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); 452 } 453 454 // Determine what fraction to give response. 455 if ($this->getpath($bbresponse, 456 array('#', 'setvar'), false, false)) { 457 switch ($this->getpath($bbresponse, 458 array('#', 'setvar', 0, '#'), false, false)) { 459 case "SCORE.max": 460 $response->fraction = 1; 461 break; 462 default: 463 // I have only seen this being 0 or unset. 464 // There are probably fractional values of SCORE.max, but I'm not sure what they look like. 465 $response->fraction = 0; 466 break; 467 } 468 } else { 469 // Just going to assume this is the case this is probably not correct. 470 $response->fraction = 0; 471 } 472 473 $responses[] = $response; 474 } 475 } 476 477 /** 478 * Preprocess XML blocks containing data for responses feedbacks. 479 * Called by {@link create_raw_question()} 480 * for all questions types. 481 * @param array $feedbackset XML block to parse 482 * @param array $feedbacks array of feedbacks suitable for a rawquestion. 483 */ 484 public function process_feedback($feedbackset, &$feedbacks) { 485 foreach ($feedbackset as $bbfeedback) { 486 $feedback = new stdClass(); 487 $feedback->ident = $this->getpath($bbfeedback, 488 array('@', 'ident'), '', true); 489 $feedback->text = ''; 490 if ($this->getpath($bbfeedback, 491 array('#', 'flow_mat', 0), false, false)) { 492 $this->process_block($this->getpath($bbfeedback, 493 array('#', 'flow_mat', 0), false, false), $feedback); 494 } else if ($this->getpath($bbfeedback, 495 array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false)) { 496 $this->process_block($this->getpath($bbfeedback, 497 array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false), $feedback); 498 } 499 500 $feedbacks[$feedback->ident] = $feedback; 501 } 502 } 503 504 /** 505 * Create common parts of question 506 * @param object $quest rawquestion 507 * @return object Moodle question. 508 */ 509 public function process_common($quest) { 510 $question = $this->defaultquestion(); 511 $text = $quest->QUESTION_BLOCK->text; 512 $questiontext = $this->cleaned_text_field($text); 513 $question->questiontext = $questiontext['text']; 514 $question->questiontextformat = $questiontext['format']; // Needed because add_blank_combined_feedback uses it. 515 if (isset($questiontext['itemid'])) { 516 $question->questiontextitemid = $questiontext['itemid']; 517 } 518 $question->name = $this->create_default_question_name($question->questiontext, 519 get_string('defaultname', 'qformat_blackboard_six' , $quest->id)); 520 $question->generalfeedback = ''; 521 $question->generalfeedbackformat = FORMAT_HTML; 522 $question->generalfeedbackfiles = array(); 523 524 return $question; 525 } 526 527 /** 528 * Process True / False Questions 529 * Parse a truefalse rawquestion and add the result 530 * to the array of questions already parsed. 531 * @param object $quest rawquestion 532 * @param array $questions array of Moodle questions already done 533 */ 534 protected function process_tf($quest, &$questions) { 535 $question = $this->process_common($quest); 536 537 $question->qtype = 'truefalse'; 538 $question->single = 1; // Only one answer is allowed. 539 $question->penalty = 1; // Penalty = 1 for truefalse questions. 540 // 0th [response] is the correct answer. 541 $responses = $quest->responses; 542 $correctresponse = $this->getpath($responses[0]->ident[0], 543 array('varequal', 0, '#'), '', true); 544 if ($correctresponse != 'false') { 545 $correct = true; 546 } else { 547 $correct = false; 548 } 549 $fback = new stdClass(); 550 551 foreach ($quest->feedback as $fb) { 552 $fback->{$fb->ident} = $fb->text; 553 } 554 555 if ($correct) { // True is correct. 556 $question->answer = 1; 557 $question->feedbacktrue = $this->cleaned_text_field($fback->correct); 558 $question->feedbackfalse = $this->cleaned_text_field($fback->incorrect); 559 } else { // False is correct. 560 $question->answer = 0; 561 $question->feedbacktrue = $this->cleaned_text_field($fback->incorrect); 562 $question->feedbackfalse = $this->cleaned_text_field($fback->correct); 563 } 564 $question->correctanswer = $question->answer; 565 $questions[] = $question; 566 } 567 568 /** 569 * Process Fill in the Blank Questions 570 * Parse a fillintheblank rawquestion and add the result 571 * to the array of questions already parsed. 572 * @param object $quest rawquestion 573 * @param array $questions array of Moodle questions already done. 574 */ 575 protected function process_fblank($quest, &$questions) { 576 $question = $this->process_common($quest); 577 $question->qtype = 'shortanswer'; 578 $question->usecase = 0; // Ignore case. 579 580 $answers = array(); 581 $fractions = array(); 582 $feedbacks = array(); 583 584 // Extract the feedback. 585 $feedback = array(); 586 foreach ($quest->feedback as $fback) { 587 if (isset($fback->ident)) { 588 if ($fback->ident == 'correct' || $fback->ident == 'incorrect') { 589 $feedback[$fback->ident] = $fback->text; 590 } 591 } 592 } 593 594 foreach ($quest->responses as $response) { 595 if (isset($response->title)) { 596 if ($this->getpath($response->ident[0], 597 array('varequal', 0, '#'), false, false)) { 598 // For BB Fill in the Blank, only interested in correct answers. 599 if ($response->feedback = 'correct') { 600 $answers[] = $this->getpath($response->ident[0], 601 array('varequal', 0, '#'), '', true); 602 $fractions[] = 1; 603 if (isset($feedback['correct'])) { 604 $feedbacks[] = $this->cleaned_text_field($feedback['correct']); 605 } else { 606 $feedbacks[] = $this->text_field(''); 607 } 608 } 609 } 610 611 } 612 } 613 614 // Adding catchall to so that students can see feedback for incorrect answers when they enter something, 615 // the instructor did not enter. 616 $answers[] = '*'; 617 $fractions[] = 0; 618 if (isset($feedback['incorrect'])) { 619 $feedbacks[] = $this->cleaned_text_field($feedback['incorrect']); 620 } else { 621 $feedbacks[] = $this->text_field(''); 622 } 623 624 $question->answer = $answers; 625 $question->fraction = $fractions; 626 $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of. 627 628 if (!empty($question)) { 629 $questions[] = $question; 630 } 631 632 } 633 634 /** 635 * Process Multichoice Questions 636 * Parse a multichoice single answer rawquestion and add the result 637 * to the array of questions already parsed. 638 * @param object $quest rawquestion 639 * @param array $questions array of Moodle questions already done. 640 */ 641 protected function process_mc($quest, &$questions) { 642 $question = $this->process_common($quest); 643 $question->qtype = 'multichoice'; 644 $question = $this->add_blank_combined_feedback($question); 645 $question->single = 1; 646 $feedback = array(); 647 foreach ($quest->feedback as $fback) { 648 $feedback[$fback->ident] = $fback->text; 649 } 650 651 foreach ($quest->responses as $response) { 652 if (isset($response->title)) { 653 if ($response->title == 'correct') { 654 // Only one answer possible for this qtype so first index is correct answer. 655 $correct = $this->getpath($response->ident[0], 656 array('varequal', 0, '#'), '', true); 657 } 658 } else { 659 // Fallback method for when the title is not set. 660 if ($response->feedback == 'correct') { 661 // Only one answer possible for this qtype so first index is correct answer. 662 $correct = $this->getpath($response->ident[0], 663 array('varequal', 0, '#'), '', true); 664 } 665 } 666 } 667 668 $i = 0; 669 foreach ($quest->RESPONSE_BLOCK->choices as $response) { 670 $question->answer[$i] = $this->cleaned_text_field($response->text); 671 if ($correct == $response->ident) { 672 $question->fraction[$i] = 1; 673 // This is a bit of a hack to catch the feedback... first we see if a 'specific' 674 // feedback for this response exists, then if a 'correct' feedback exists. 675 676 if (!empty($feedback[$response->ident]) ) { 677 $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]); 678 } else if (!empty($feedback['correct'])) { 679 $question->feedback[$i] = $this->cleaned_text_field($feedback['correct']); 680 } else if (!empty($feedback[$i])) { 681 $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]); 682 } else { 683 $question->feedback[$i] = $this->cleaned_text_field(get_string('correct', 'question')); 684 } 685 } else { 686 $question->fraction[$i] = 0; 687 if (!empty($feedback[$response->ident]) ) { 688 $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]); 689 } else if (!empty($feedback['incorrect'])) { 690 $question->feedback[$i] = $this->cleaned_text_field($feedback['incorrect']); 691 } else if (!empty($feedback[$i])) { 692 $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]); 693 } else { 694 $question->feedback[$i] = $this->cleaned_text_field(get_string('incorrect', 'question')); 695 } 696 } 697 $i++; 698 } 699 700 if (!empty($question)) { 701 $questions[] = $question; 702 } 703 } 704 705 /** 706 * Process Multiple Choice Questions With Multiple Answers. 707 * Parse a multichoice multianswer rawquestion and add the result 708 * to the array of questions already parsed. 709 * @param object $quest rawquestion 710 * @param array $questions array of Moodle questions already done. 711 */ 712 public function process_ma($quest, &$questions) { 713 $question = $this->process_common($quest); 714 $question->qtype = 'multichoice'; 715 $question = $this->add_blank_combined_feedback($question); 716 $question->single = 0; // More than one answer allowed. 717 718 $answers = $quest->responses; 719 $correctanswers = array(); 720 foreach ($answers as $answer) { 721 if ($answer->title == 'correct') { 722 $answerset = $this->getpath($answer->ident[0], 723 array('and', 0, '#', 'varequal'), array(), false); 724 foreach ($answerset as $ans) { 725 $correctanswers[] = $ans['#']; 726 } 727 } 728 } 729 $feedback = new stdClass(); 730 foreach ($quest->feedback as $fb) { 731 $feedback->{$fb->ident} = trim($fb->text); 732 } 733 734 $correctanswercount = count($correctanswers); 735 $fraction = 1 / $correctanswercount; 736 $choiceset = $quest->RESPONSE_BLOCK->choices; 737 $i = 0; 738 foreach ($choiceset as $choice) { 739 $question->answer[$i] = $this->cleaned_text_field(trim($choice->text)); 740 if (in_array($choice->ident, $correctanswers)) { 741 // Correct answer. 742 $question->fraction[$i] = $fraction; 743 $question->feedback[$i] = $this->cleaned_text_field($feedback->correct); 744 } else { 745 // Wrong answer. 746 $question->fraction[$i] = 0; 747 $question->feedback[$i] = $this->cleaned_text_field($feedback->incorrect); 748 } 749 $i++; 750 } 751 752 $questions[] = $question; 753 } 754 755 /** 756 * Process Essay Questions 757 * Parse an essay rawquestion and add the result 758 * to the array of questions already parsed. 759 * @param object $quest rawquestion 760 * @param array $questions array of Moodle questions already done. 761 */ 762 public function process_essay($quest, &$questions) { 763 764 $question = $this->process_common($quest); 765 $question->qtype = 'essay'; 766 767 $question->feedback = array(); 768 // Not sure where to get the correct answer from? 769 foreach ($quest->feedback as $feedback) { 770 // Added this code to put the possible solution that the 771 // instructor gives as the Moodle answer for an essay question. 772 if ($feedback->ident == 'solution') { 773 $question->graderinfo = $this->cleaned_text_field($feedback->text); 774 } 775 } 776 // Added because essay/questiontype.php:save_question_option is expecting a 777 // fraction property - CT 8/10/06. 778 $question->fraction[] = 1; 779 $question->defaultmark = 1; 780 $question->responseformat = 'editor'; 781 $question->responserequired = 1; 782 $question->responsefieldlines = 15; 783 $question->attachments = 0; 784 $question->attachmentsrequired = 0; 785 $question->responsetemplate = $this->text_field(''); 786 787 $questions[] = $question; 788 } 789 790 /** 791 * Process Matching Questions 792 * Parse a matching rawquestion and add the result 793 * to the array of questions already parsed. 794 * @param object $quest rawquestion 795 * @param array $questions array of Moodle questions already done. 796 */ 797 public function process_matching($quest, &$questions) { 798 799 // Blackboard matching questions can't be imported in core Moodle without a loss in data, 800 // as core match question don't allow HTML in subanswers. The contributed ddmatch 801 // question type support HTML in subanswers. 802 // The ddmatch question type is not part of core, so we need to check if it is defined. 803 $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch'); 804 805 $question = $this->process_common($quest); 806 $question = $this->add_blank_combined_feedback($question); 807 $question->valid = true; 808 if ($ddmatchisinstalled) { 809 $question->qtype = 'ddmatch'; 810 } else { 811 $question->qtype = 'match'; 812 } 813 // Construction of the array holding mappings between subanswers and subquestions. 814 foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { 815 foreach ($quest->responses as $rid => $resp) { 816 if (isset($resp->ident) && $resp->ident == $subq->ident) { 817 $correct = $resp->correct; 818 } 819 } 820 821 foreach ($subq->choices as $cid => $choice) { 822 if ($choice == $correct) { 823 $mappings[$subq->ident] = $cid; 824 } 825 } 826 } 827 828 foreach ($subq->choices as $choiceid => $choice) { 829 $subanswertext = $quest->RIGHT_MATCH_BLOCK->matchinganswerset[$choiceid]->text; 830 if ($ddmatchisinstalled) { 831 $subanswer = $this->cleaned_text_field($subanswertext); 832 } else { 833 $subanswertext = html_to_text($this->cleaninput($subanswertext), 0); 834 $subanswer = $subanswertext; 835 } 836 837 if ($subanswertext != '') { // Only import non empty subanswers. 838 $subquestion = ''; 839 840 $fiber = array_keys ($mappings, $choiceid); 841 foreach ($fiber as $correctanswerid) { 842 // We have found a correspondance for this subanswer so we need to take the associated subquestion. 843 foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { 844 $currentsubqid = $subq->ident; 845 if (strcmp ($currentsubqid, $correctanswerid) == 0) { 846 $subquestion = $subq->text; 847 break; 848 } 849 } 850 $question->subquestions[] = $this->cleaned_text_field($subquestion); 851 $question->subanswers[] = $subanswer; 852 } 853 854 if ($subquestion == '') { // Then in this case, $choice is a distractor. 855 $question->subquestions[] = $this->text_field(''); 856 $question->subanswers[] = $subanswer; 857 } 858 } 859 } 860 861 // Verify that this matching question has enough subquestions and subanswers. 862 $subquestioncount = 0; 863 $subanswercount = 0; 864 $subanswers = $question->subanswers; 865 foreach ($question->subquestions as $key => $subquestion) { 866 $subquestion = $subquestion['text']; 867 $subanswer = $subanswers[$key]; 868 if ($subquestion != '') { 869 $subquestioncount++; 870 } 871 $subanswercount++; 872 } 873 if ($subquestioncount < 2 || $subanswercount < 3) { 874 $this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext)); 875 } else { 876 $questions[] = $question; 877 } 878 } 879 880 /** 881 * Add a category question entry based on the assessment title 882 * @param array $xml the xml tree 883 * @param array $questions the questions already parsed 884 */ 885 public function process_category($xml, &$questions) { 886 $title = $this->getpath($xml, array('questestinterop', '#', 'assessment', 0, '@', 'title'), '', true); 887 888 $dummyquestion = new stdClass(); 889 $dummyquestion->qtype = 'category'; 890 $dummyquestion->category = $this->cleaninput($this->clean_question_name($title)); 891 892 $questions[] = $dummyquestion; 893 } 894 895 /** 896 * Strip the applet tag used by Blackboard to render mathml formulas, 897 * keeping the mathml tag. 898 * @param string $string 899 * @return string 900 */ 901 public function strip_applet_tags_get_mathml($string) { 902 if (stristr($string, '</APPLET>') === false) { 903 return $string; 904 } else { 905 // Strip all applet tags keeping stuff before/after and inbetween (if mathml) them. 906 while (stristr($string, '</APPLET>') !== false) { 907 preg_match("/(.*)\<applet.*value=\"(\<math\>.*\<\/math\>)\".*\<\/applet\>(.*)/i", $string, $mathmls); 908 $string = $mathmls[1].$mathmls[2].$mathmls[3]; 909 } 910 return $string; 911 } 912 } 913 914 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body