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 2003 Scott Elliott 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 pool question importer class. 31 * 32 * @package qformat_blackboard_six 33 * @copyright 2003 Scott Elliott 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class qformat_blackboard_six_pool extends qformat_blackboard_six_base { 37 /** 38 * @var bool Is the current question's question text escaped HTML 39 * (true for most if not all Blackboard files). 40 */ 41 public $ishtml = true; 42 43 /** 44 * Parse the xml document into an array of questions 45 * 46 * This *could* burn memory - but it won't happen that much 47 * so fingers crossed! 48 * 49 * @param array $text array of lines from the input file. 50 * @return array (of objects) questions objects. 51 */ 52 protected function readquestions($text) { 53 54 // This converts xml to big nasty data structure, 55 // the 0 means keep white space as it is. 56 try { 57 $xml = xmlize($text, 0, 'UTF-8', true); 58 } catch (xml_format_exception $e) { 59 $this->error($e->getMessage(), ''); 60 return false; 61 } 62 63 $questions = array(); 64 65 $this->process_category($xml, $questions); 66 67 $this->process_tf($xml, $questions); 68 $this->process_mc($xml, $questions); 69 $this->process_ma($xml, $questions); 70 $this->process_fib($xml, $questions); 71 $this->process_matching($xml, $questions); 72 $this->process_essay($xml, $questions); 73 74 return $questions; 75 } 76 77 /** 78 * Do question import processing common to every qtype. 79 * 80 * @param array $questiondata the xml tree related to the current question 81 * @return object initialized question object. 82 */ 83 public function process_common($questiondata) { 84 85 // This routine initialises the question object. 86 $question = $this->defaultquestion(); 87 88 // Determine if the question is already escaped html. 89 $this->ishtml = $this->getpath($questiondata, 90 array('#', 'BODY', 0, '#', 'FLAGS', 0, '#', 'ISHTML', 0, '@', 'value'), 91 false, false); 92 93 // Put questiontext in question object. 94 $text = $this->getpath($questiondata, 95 array('#', 'BODY', 0, '#', 'TEXT', 0, '#'), 96 '', true, get_string('importnotext', 'qformat_blackboard_six')); 97 98 $questiontext = $this->cleaned_text_field($text); 99 $question->questiontext = $questiontext['text']; 100 $question->questiontextformat = $questiontext['format']; // Needed because add_blank_combined_feedback uses it. 101 if (isset($questiontext['itemid'])) { 102 $question->questiontextitemid = $questiontext['itemid']; 103 } 104 105 // Put name in question object. We must ensure it is not empty and it is less than 250 chars. 106 $id = $this->getpath($questiondata, array('@', 'id'), '', true); 107 $question->name = $this->create_default_question_name($question->questiontext, 108 get_string('defaultname', 'qformat_blackboard_six' , $id)); 109 110 $question->generalfeedback = ''; 111 $question->generalfeedbackformat = FORMAT_HTML; 112 $question->generalfeedbackfiles = array(); 113 114 // TODO : read the mark from the POOL TITLE QUESTIONLIST section. 115 $question->defaultmark = 1; 116 return $question; 117 } 118 119 /** 120 * Add a category question entry based on the pool file title 121 * @param array $xml the xml tree 122 * @param array $questions the questions already parsed 123 */ 124 public function process_category($xml, &$questions) { 125 $title = $this->getpath($xml, array('POOL', '#', 'TITLE', 0, '@', 'value'), '', true); 126 127 $dummyquestion = new stdClass(); 128 $dummyquestion->qtype = 'category'; 129 $dummyquestion->category = $this->cleaninput($this->clean_question_name($title)); 130 131 $questions[] = $dummyquestion; 132 } 133 134 /** 135 * Process Essay Questions 136 * @param array $xml the xml tree 137 * @param array $questions the questions already parsed 138 */ 139 public function process_essay($xml, &$questions) { 140 141 if ($this->getpath($xml, array('POOL', '#', 'QUESTION_ESSAY'), false, false)) { 142 $essayquestions = $this->getpath($xml, 143 array('POOL', '#', 'QUESTION_ESSAY'), false, false); 144 } else { 145 return; 146 } 147 148 foreach ($essayquestions as $thisquestion) { 149 150 $question = $this->process_common($thisquestion); 151 152 $question->qtype = 'essay'; 153 154 $question->answer = ''; 155 $answer = $this->getpath($thisquestion, 156 array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true); 157 $question->graderinfo = $this->cleaned_text_field($answer); 158 $question->responsetemplate = $this->text_field(''); 159 $question->feedback = ''; 160 $question->responseformat = 'editor'; 161 $question->responserequired = 1; 162 $question->responsefieldlines = 15; 163 $question->attachments = 0; 164 $question->attachmentsrequired = 0; 165 $question->fraction = 0; 166 167 $questions[] = $question; 168 } 169 } 170 171 /** 172 * Process True / False Questions 173 * @param array $xml the xml tree 174 * @param array $questions the questions already parsed 175 */ 176 public function process_tf($xml, &$questions) { 177 178 if ($this->getpath($xml, array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false)) { 179 $tfquestions = $this->getpath($xml, 180 array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false); 181 } else { 182 return; 183 } 184 185 foreach ($tfquestions as $thisquestion) { 186 187 $question = $this->process_common($thisquestion); 188 189 $question->qtype = 'truefalse'; 190 $question->single = 1; // Only one answer is allowed. 191 192 $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false); 193 194 $correctanswer = $this->getpath($thisquestion, 195 array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'), 196 '', true); 197 198 // First choice is true, second is false. 199 $id = $this->getpath($choices[0], array('@', 'id'), '', true); 200 $correctfeedback = $this->getpath($thisquestion, 201 array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), 202 '', true); 203 $incorrectfeedback = $this->getpath($thisquestion, 204 array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), 205 '', true); 206 if (strcmp($id, $correctanswer) == 0) { // True is correct. 207 $question->answer = 1; 208 $question->feedbacktrue = $this->cleaned_text_field($correctfeedback); 209 $question->feedbackfalse = $this->cleaned_text_field($incorrectfeedback); 210 } else { // False is correct. 211 $question->answer = 0; 212 $question->feedbacktrue = $this->cleaned_text_field($incorrectfeedback); 213 $question->feedbackfalse = $this->cleaned_text_field($correctfeedback); 214 } 215 $question->correctanswer = $question->answer; 216 $questions[] = $question; 217 } 218 } 219 220 /** 221 * Process Multiple Choice Questions with single answer 222 * @param array $xml the xml tree 223 * @param array $questions the questions already parsed 224 */ 225 public function process_mc($xml, &$questions) { 226 227 if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false)) { 228 $mcquestions = $this->getpath($xml, 229 array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false); 230 } else { 231 return; 232 } 233 234 foreach ($mcquestions as $thisquestion) { 235 236 $question = $this->process_common($thisquestion); 237 238 $correctfeedback = $this->getpath($thisquestion, 239 array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), 240 '', true); 241 $incorrectfeedback = $this->getpath($thisquestion, 242 array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), 243 '', true); 244 $question->correctfeedback = $this->cleaned_text_field($correctfeedback); 245 $question->partiallycorrectfeedback = $this->text_field(''); 246 $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback); 247 248 $question->qtype = 'multichoice'; 249 $question->single = 1; // Only one answer is allowed. 250 251 $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); 252 $correctanswerid = $this->getpath($thisquestion, 253 array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'), 254 '', true); 255 foreach ($choices as $choice) { 256 $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true); 257 // Put this choice in the question object. 258 $question->answer[] = $this->cleaned_text_field($choicetext); 259 260 $choiceid = $this->getpath($choice, array('@', 'id'), '', true); 261 // If choice is the right answer, give 100% mark, otherwise give 0%. 262 if (strcmp ($choiceid, $correctanswerid) == 0) { 263 $question->fraction[] = 1; 264 } else { 265 $question->fraction[] = 0; 266 } 267 // There is never feedback specific to each choice. 268 $question->feedback[] = $this->text_field(''); 269 } 270 $questions[] = $question; 271 } 272 } 273 274 /** 275 * Process Multiple Choice Questions With Multiple Answers 276 * @param array $xml the xml tree 277 * @param array $questions the questions already parsed 278 */ 279 public function process_ma($xml, &$questions) { 280 if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false)) { 281 $maquestions = $this->getpath($xml, 282 array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false); 283 } else { 284 return; 285 } 286 287 foreach ($maquestions as $thisquestion) { 288 $question = $this->process_common($thisquestion); 289 290 $correctfeedback = $this->getpath($thisquestion, 291 array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), 292 '', true); 293 $incorrectfeedback = $this->getpath($thisquestion, 294 array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), 295 '', true); 296 $question->correctfeedback = $this->cleaned_text_field($correctfeedback); 297 // As there is no partially correct feedback we use incorrect one. 298 $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback); 299 $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback); 300 301 $question->qtype = 'multichoice'; 302 $question->defaultmark = 1; 303 $question->single = 0; // More than one answers allowed. 304 305 $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); 306 $correctanswerids = array(); 307 foreach ($this->getpath($thisquestion, 308 array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) { 309 if ($correctanswer) { 310 $correctanswerids[] = $this->getpath($correctanswer, 311 array('@', 'answer_id'), 312 '', true); 313 } 314 } 315 $fraction = 1 / count($correctanswerids); 316 317 foreach ($choices as $choice) { 318 $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true); 319 // Put this choice in the question object. 320 $question->answer[] = $this->cleaned_text_field($choicetext); 321 322 $choiceid = $this->getpath($choice, array('@', 'id'), '', true); 323 324 $iscorrect = in_array($choiceid, $correctanswerids); 325 326 if ($iscorrect) { 327 $question->fraction[] = $fraction; 328 } else { 329 $question->fraction[] = 0; 330 } 331 // There is never feedback specific to each choice. 332 $question->feedback[] = $this->text_field(''); 333 } 334 $questions[] = $question; 335 } 336 } 337 338 /** 339 * Process Fill in the Blank Questions 340 * @param array $xml the xml tree 341 * @param array $questions the questions already parsed 342 */ 343 public function process_fib($xml, &$questions) { 344 if ($this->getpath($xml, array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false)) { 345 $fibquestions = $this->getpath($xml, 346 array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false); 347 } else { 348 return; 349 } 350 351 foreach ($fibquestions as $thisquestion) { 352 353 $question = $this->process_common($thisquestion); 354 355 $question->qtype = 'shortanswer'; 356 $question->usecase = 0; // Ignore case. 357 358 $correctfeedback = $this->getpath($thisquestion, 359 array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), 360 '', true); 361 $incorrectfeedback = $this->getpath($thisquestion, 362 array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), 363 '', true); 364 $answers = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); 365 foreach ($answers as $answer) { 366 $question->answer[] = $this->getpath($answer, 367 array('#', 'TEXT', 0, '#'), '', true); 368 $question->fraction[] = 1; 369 $question->feedback[] = $this->cleaned_text_field($correctfeedback); 370 } 371 $question->answer[] = '*'; 372 $question->fraction[] = 0; 373 $question->feedback[] = $this->cleaned_text_field($incorrectfeedback); 374 375 $questions[] = $question; 376 } 377 } 378 379 /** 380 * Process Matching Questions 381 * @param array $xml the xml tree 382 * @param array $questions the questions already parsed 383 */ 384 public function process_matching($xml, &$questions) { 385 if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MATCH'), false, false)) { 386 $matchquestions = $this->getpath($xml, 387 array('POOL', '#', 'QUESTION_MATCH'), false, false); 388 } else { 389 return; 390 } 391 // Blackboard questions can't be imported in core Moodle without a loss in data, 392 // as core match question don't allow HTML in subanswers. The contributed ddmatch 393 // question type support HTML in subanswers. 394 // The ddmatch question type is not part of core, so we need to check if it is defined. 395 $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch'); 396 397 foreach ($matchquestions as $thisquestion) { 398 399 $question = $this->process_common($thisquestion); 400 if ($ddmatchisinstalled) { 401 $question->qtype = 'ddmatch'; 402 } else { 403 $question->qtype = 'match'; 404 } 405 406 $correctfeedback = $this->getpath($thisquestion, 407 array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), 408 '', true); 409 $incorrectfeedback = $this->getpath($thisquestion, 410 array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), 411 '', true); 412 $question->correctfeedback = $this->cleaned_text_field($correctfeedback); 413 // As there is no partially correct feedback we use incorrect one. 414 $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback); 415 $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback); 416 417 $choices = $this->getpath($thisquestion, 418 array('#', 'CHOICE'), false, false); // Blackboard "choices" are Moodle subanswers. 419 $answers = $this->getpath($thisquestion, 420 array('#', 'ANSWER'), false, false); // Blackboard "answers" are Moodle subquestions. 421 $correctanswers = $this->getpath($thisquestion, 422 array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false); // Mapping between choices and answers. 423 $mappings = array(); 424 foreach ($correctanswers as $correctanswer) { 425 if ($correctanswer) { 426 $correctchoiceid = $this->getpath($correctanswer, 427 array('@', 'choice_id'), '', true); 428 $correctanswerid = $this->getpath($correctanswer, 429 array('@', 'answer_id'), 430 '', true); 431 $mappings[$correctanswerid] = $correctchoiceid; 432 } 433 } 434 435 foreach ($choices as $choice) { 436 if ($ddmatchisinstalled) { 437 $choicetext = $this->cleaned_text_field($this->getpath($choice, 438 array('#', 'TEXT', 0, '#'), '', true)); 439 } else { 440 $choicetext = trim(strip_tags($this->getpath($choice, 441 array('#', 'TEXT', 0, '#'), '', true))); 442 } 443 444 if ($choicetext != '') { // Only import non empty subanswers. 445 $subquestion = ''; 446 $choiceid = $this->getpath($choice, 447 array('@', 'id'), '', true); 448 $fiber = array_search($choiceid, $mappings); 449 $fiber = array_keys ($mappings, $choiceid); 450 foreach ($fiber as $correctanswerid) { 451 // We have found a correspondance for this choice so we need to take the associated answer. 452 foreach ($answers as $answer) { 453 $currentanswerid = $this->getpath($answer, 454 array('@', 'id'), '', true); 455 if (strcmp ($currentanswerid, $correctanswerid) == 0) { 456 $subquestion = $this->getpath($answer, 457 array('#', 'TEXT', 0, '#'), '', true); 458 break; 459 } 460 } 461 $question->subquestions[] = $this->cleaned_text_field($subquestion); 462 $question->subanswers[] = $choicetext; 463 } 464 465 if ($subquestion == '') { // Then in this case, $choice is a distractor. 466 $question->subquestions[] = $this->text_field(''); 467 $question->subanswers[] = $choicetext; 468 } 469 } 470 } 471 472 // Verify that this matching question has enough subquestions and subanswers. 473 $subquestioncount = 0; 474 $subanswercount = 0; 475 $subanswers = $question->subanswers; 476 foreach ($question->subquestions as $key => $subquestion) { 477 $subquestion = $subquestion['text']; 478 $subanswer = $subanswers[$key]; 479 if ($subquestion != '') { 480 $subquestioncount++; 481 } 482 $subanswercount++; 483 } 484 if ($subquestioncount < 2 || $subanswercount < 3) { 485 $this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext)); 486 } else { 487 $questions[] = $question; 488 } 489 490 } 491 } 492 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body