See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 311]
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 * Web CT question importer. 19 * 20 * @package qformat_webct 21 * @copyright 2004 ASP Consulting http://www.asp-consulting.net 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 /** 29 * Manipulate HTML editites in a string. Used by WebCT import. 30 * @param string $string 31 * @return string 32 */ 33 function unhtmlentities($string) { 34 $search = array ("'<script[?>]*?>.*?</script>'si", // Remove javascript. 35 "'<[\/\!]*?[^<?>]*?>'si", // Remove HTML tags. 36 "'([\r\n])[\s]+'", // Remove spaces. 37 "'&(quot|#34);'i", // Remove HTML entites. 38 "'&(amp|#38);'i", 39 "'&(lt|#60);'i", 40 "'&(gt|#62);'i", 41 "'&(nbsp|#160);'i", 42 "'&(iexcl|#161);'i", 43 "'&(cent|#162);'i", 44 "'&(pound|#163);'i", 45 "'&(copy|#169);'i", 46 "'&#(\d+);'e"); // Evaluate like PHP. 47 $replace = array ("", 48 "", 49 "\\1", 50 "\"", 51 "&", 52 "<", 53 "?>", 54 " ", 55 chr(161), 56 chr(162), 57 chr(163), 58 chr(169), 59 "chr(\\1)"); 60 return preg_replace ($search, $replace, $string); 61 } 62 63 /** 64 * Helper function for WebCT import. 65 * @param unknown_type $formula 66 */ 67 function qformat_webct_convert_formula($formula) { 68 69 // Remove empty space, as it would cause problems otherwise. 70 $formula = str_replace(' ', '', $formula); 71 72 // Remove paranthesis after e,E and *10**. 73 while (preg_match('~[0-9.](e|E|\\*10\\*\\*)\\([+-]?[0-9]+\\)~', $formula, $regs)) { 74 $formula = str_replace( 75 $regs[0], preg_replace('/[)(]/', '', $regs[0]), $formula); 76 } 77 78 // Replace *10** with e where possible. 79 while (preg_match('~(^[+-]?|[^eE][+-]|[^0-9eE+-])[0-9.]+\\*10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)~', 80 $formula, $regs)) { 81 $formula = str_replace( 82 $regs[0], str_replace('*10**', 'e', $regs[0]), $formula); 83 } 84 85 // Replace other 10** with 1e where possible. 86 while (preg_match('~(^|[^0-9.eE])10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)~', $formula, $regs)) { 87 $formula = str_replace( 88 $regs[0], str_replace('10**', '1e', $regs[0]), $formula); 89 } 90 91 // Replace all other base**exp with the PHP equivalent function pow(base,exp) 92 // (Pretty tricky to exchange an operator with a function). 93 while (2 == count($splits = explode('**', $formula, 2))) { 94 95 // Find $base. 96 if (preg_match('~^(.*[^0-9.eE])?(([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?|\\{[^}]*\\})$~', 97 $splits[0], $regs)) { 98 // The simple cases. 99 $base = $regs[2]; 100 $splits[0] = $regs[1]; 101 102 } else if (preg_match('~\\)$~', $splits[0])) { 103 // Find the start of this parenthesis. 104 $deep = 1; 105 for ($i = 1; $deep; ++$i) { 106 if (!preg_match('~^(.*[^[:alnum:]_])?([[:alnum:]_]*([)(])([^)(]*[)(]){'.$i.'})$~', 107 $splits[0], $regs)) { 108 print_error('parenthesisinproperstart', 'question', '', $splits[0]); 109 } 110 if ('(' == $regs[3]) { 111 --$deep; 112 } else if (')' == $regs[3]) { 113 ++$deep; 114 } else { 115 print_error('impossiblechar', 'question', '', $regs[3]); 116 } 117 } 118 $base = $regs[2]; 119 $splits[0] = $regs[1]; 120 121 } else { 122 print_error('badbase', 'question', '', $splits[0]); 123 } 124 125 // Find $exp (similar to above but a little easier). 126 if (preg_match('~^([+-]?(\\{[^}]\\}|([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?))(.*)~', 127 $splits[1], $regs)) { 128 // The simple case. 129 $exp = $regs[1]; 130 $splits[1] = $regs[6]; 131 132 } else if (preg_match('~^[+-]?[[:alnum:]_]*\\(~', $splits[1])) { 133 // Find the end of the parenthesis. 134 $deep = 1; 135 for ($i = 1; $deep; ++$i) { 136 if (!preg_match('~^([+-]?[[:alnum:]_]*([)(][^)(]*){'.$i.'}([)(]))(.*)~', 137 $splits[1], $regs)) { 138 print_error('parenthesisinproperclose', 'question', '', $splits[1]); 139 } 140 if (')' == $regs[3]) { 141 --$deep; 142 } else if ('(' == $regs[3]) { 143 ++$deep; 144 } else { 145 print_error('impossiblechar', 'question'); 146 } 147 } 148 $exp = $regs[1]; 149 $splits[1] = $regs[4]; 150 } 151 152 // Replace it! 153 $formula = "{$splits[0]}pow({$base},{$exp}){$splits[1]}"; 154 } 155 156 // Nothing more is known to need to be converted. 157 158 return $formula; 159 } 160 161 162 /** 163 * Web CT question importer. 164 * 165 * @copyright 2004 ASP Consulting http://www.asp-consulting.net 166 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 167 */ 168 class qformat_webct extends qformat_default { 169 /** @var string path to the temporary directory. */ 170 public $tempdir = ''; 171 172 /** 173 * This plugin provide import 174 * @return bool true 175 */ 176 public function provide_import() { 177 return true; 178 } 179 180 public function can_import_file($file) { 181 $mimetypes = array( 182 mimeinfo('type', '.txt'), 183 mimeinfo('type', '.zip') 184 ); 185 return in_array($file->get_mimetype(), $mimetypes); 186 } 187 188 public function mime_type() { 189 return mimeinfo('type', '.zip'); 190 } 191 192 /** 193 * Store an image file in a draft filearea 194 * @param array $text, if itemid element don't exists it will be created 195 * @param string tempdir path to root of image tree 196 * @param string filepathinsidetempdir path to image in the tree 197 * @param string filename image's name 198 * @return string new name of the image as it was stored 199 */ 200 protected function store_file_for_text_field(&$text, $tempdir, $filepathinsidetempdir, $filename) { 201 global $USER; 202 $fs = get_file_storage(); 203 if (empty($text['itemid'])) { 204 $text['itemid'] = file_get_unused_draft_itemid(); 205 } 206 // As question file areas don't support subdirs, 207 // convert path to filename. 208 // So that images with same name can be imported. 209 $newfilename = clean_param(str_replace('/', '__', $filepathinsidetempdir . '__' . $filename), PARAM_FILE); 210 $filerecord = array( 211 'contextid' => context_user::instance($USER->id)->id, 212 'component' => 'user', 213 'filearea' => 'draft', 214 'itemid' => $text['itemid'], 215 'filepath' => '/', 216 'filename' => $newfilename, 217 ); 218 $fs->create_file_from_pathname($filerecord, $tempdir . '/' . $filepathinsidetempdir . '/' . $filename); 219 return $newfilename; 220 } 221 222 /** 223 * Given an HTML text with references to images files, 224 * store all images in a draft filearea, 225 * and return an array with all urls in text recoded, 226 * format set to FORMAT_HTML, and itemid set to filearea itemid 227 * @param string text text to parse and recode 228 * @return array with keys text, format, itemid. 229 */ 230 public function text_field($text) { 231 $data = array(); 232 // Step one, find all file refs then add to array. 233 preg_match_all('|<img[^>]+src="([^"]*)"|i', $text, $out); // Find all src refs. 234 235 $filepaths = array(); 236 foreach ($out[1] as $path) { 237 $fullpath = $this->tempdir . '/' . $path; 238 if (is_readable($fullpath) && !in_array($path, $filepaths)) { 239 $dirpath = dirname($path); 240 $filename = basename($path); 241 $newfilename = $this->store_file_for_text_field($data, $this->tempdir, $dirpath, $filename); 242 $text = preg_replace("|{$path}|", "@@PLUGINFILE@@/" . $newfilename, $text); 243 $filepaths[] = $path; 244 } 245 246 } 247 $data['text'] = $text; 248 $data['format'] = FORMAT_HTML; 249 return $data; 250 } 251 252 /** 253 * Does any post-processing that may be desired 254 * Clean the temporary directory if a zip file was imported 255 * @return bool success 256 */ 257 public function importpostprocess() { 258 if (!empty($this->tempdir)) { 259 fulldelete($this->tempdir); 260 } 261 return true; 262 } 263 264 /** 265 * Return content of all files containing questions, 266 * as an array one element for each file found, 267 * For each file, the corresponding element is an array of lines. 268 * @param string filename name of file 269 * @return mixed contents array or false on failure 270 */ 271 public function readdata($filename) { 272 273 // Find if we are importing a .txt file. 274 if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'txt') { 275 if (!is_readable($filename)) { 276 $this->error(get_string('filenotreadable', 'error')); 277 return false; 278 } 279 return file($filename); 280 } 281 // We are importing a zip file. 282 // Create name for temporary directory. 283 $this->tempdir = make_request_directory(); 284 if (is_readable($filename)) { 285 if (!copy($filename, $this->tempdir . '/webct.zip')) { 286 $this->error(get_string('cannotcopybackup', 'question')); 287 fulldelete($this->tempdir); 288 return false; 289 } 290 $packer = get_file_packer('application/zip'); 291 if ($packer->extract_to_pathname($this->tempdir . '/webct.zip', $this->tempdir, null, null, true)) { 292 $dir = $this->tempdir; 293 if ((($handle = opendir($dir))) == false) { 294 // The directory could not be opened. 295 fulldelete($this->tempdir); 296 return false; 297 } 298 // Create arrays to store files and directories. 299 $dirfiles = array(); 300 $dirsubdirs = array(); 301 $slash = '/'; 302 303 // Loop through all directory entries, and construct two temporary arrays containing files and sub directories. 304 while (false !== ($entry = readdir($handle))) { 305 if (is_dir($dir. $slash .$entry) && $entry != '..' && $entry != '.') { 306 $dirsubdirs[] = $dir. $slash .$entry; 307 } else if ($entry != '..' && $entry != '.') { 308 $dirfiles[] = $dir. $slash .$entry; 309 } 310 } 311 if ((($handle = opendir($dirsubdirs[0]))) == false) { 312 // The directory could not be opened. 313 fulldelete($this->tempdir); 314 return false; 315 } 316 while (false !== ($entry = readdir($handle))) { 317 if (is_dir($dirsubdirs[0]. $slash .$entry) && $entry != '..' && $entry != '.') { 318 $dirsubdirs[] = $dirsubdirs[0]. $slash .$entry; 319 } else if ($entry != '..' && $entry != '.') { 320 $dirfiles[] = $dirsubdirs[0]. $slash .$entry; 321 } 322 } 323 return file($dirfiles[1]); 324 } else { 325 $this->error(get_string('cannotunzip', 'question')); 326 fulldelete($this->tempdir); 327 } 328 } else { 329 $this->error(get_string('cannotreaduploadfile', 'error')); 330 fulldelete($this->tempdir); 331 } 332 return false; 333 } 334 335 public function readquestions ($lines) { 336 $webctnumberregex = 337 '[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)((e|E|\\*10\\*\\*)([+-]?[0-9]+|\\([+-]?[0-9]+\\)))?'; 338 339 $questions = array(); 340 $warnings = array(); 341 $webctoptions = array(); 342 343 $ignorerestofquestion = false; 344 345 $nlinecounter = 0; 346 $nquestionstartline = 0; 347 $bishtmltext = false; 348 $lines[] = ":EOF:"; // For an easiest processing of the last line. 349 // We don't call defaultquestion() here, it will be called later. 350 351 foreach ($lines as $line) { 352 $nlinecounter++; 353 $line = core_text::convert($line, 'windows-1252', 'utf-8'); 354 // Processing multiples lines strings. 355 356 if (isset($questiontext) and is_string($questiontext)) { 357 if (preg_match("~^:~", $line)) { 358 $questiontext = $this->text_field(trim($questiontext)); 359 $question->questiontext = $questiontext['text']; 360 $question->questiontextformat = $questiontext['format']; 361 if (isset($questiontext['itemid'])) { 362 $question->questiontextitemid = $questiontext['itemid']; 363 } 364 unset($questiontext); 365 } else { 366 $questiontext .= str_replace('\:', ':', $line); 367 continue; 368 } 369 } 370 371 if (isset($answertext) and is_string($answertext)) { 372 if (preg_match("~^:~", $line)) { 373 $answertext = trim($answertext); 374 if ($question->qtype == 'multichoice' || $question->qtype == 'match' ) { 375 $question->answer[$currentchoice] = $this->text_field($answertext); 376 $question->subanswers[$currentchoice] = $question->answer[$currentchoice]; 377 378 } else { 379 $question->answer[$currentchoice] = $answertext; 380 $question->subanswers[$currentchoice] = $answertext; 381 } 382 unset($answertext); 383 } else { 384 $answertext .= str_replace('\:', ':', $line); 385 continue; 386 } 387 } 388 389 if (isset($responsetext) and is_string($responsetext)) { 390 if (preg_match("~^:~", $line)) { 391 $question->subquestions[$currentchoice] = trim($responsetext); 392 unset($responsetext); 393 } else { 394 $responsetext .= str_replace('\:', ':', $line); 395 continue; 396 } 397 } 398 399 if (isset($feedbacktext) and is_string($feedbacktext)) { 400 if (preg_match("~^:~", $line)) { 401 $question->feedback[$currentchoice] = $this->text_field(trim($feedbacktext)); 402 unset($feedbacktext); 403 } else { 404 $feedbacktext .= str_replace('\:', ':', $line); 405 continue; 406 } 407 } 408 409 if (isset($generalfeedbacktext) and is_string($generalfeedbacktext)) { 410 if (preg_match("~^:~", $line)) { 411 $question->tempgeneralfeedback = trim($generalfeedbacktext); 412 unset($generalfeedbacktext); 413 } else { 414 $generalfeedbacktext .= str_replace('\:', ':', $line); 415 continue; 416 } 417 } 418 419 if (isset($graderinfo) and is_string($graderinfo)) { 420 if (preg_match("~^:~", $line)) { 421 $question->graderinfo['text'] = trim($graderinfo); 422 $question->graderinfo['format'] = FORMAT_HTML; 423 unset($graderinfo); 424 } else { 425 $graderinfo .= str_replace('\:', ':', $line); 426 continue; 427 } 428 } 429 430 $line = trim($line); 431 432 if (preg_match("~^:(TYPE|EOF):~i", $line)) { 433 // New Question or End of File. 434 if (isset($question)) { // If previous question exists, complete, check and save it. 435 436 // Setup default value of missing fields. 437 if (!isset($question->name)) { 438 $question->name = $this->create_default_question_name( 439 $question->questiontext, get_string('questionname', 'question')); 440 } 441 if (!isset($question->defaultmark)) { 442 $question->defaultmark = 1; 443 } 444 if (!isset($question->image)) { 445 $question->image = ''; 446 } 447 448 // Perform sanity checks. 449 $questionok = true; 450 if (strlen($question->questiontext) == 0) { 451 $warnings[] = get_string('missingquestion', 'qformat_webct', $nquestionstartline); 452 $questionok = false; 453 } 454 if (count($question->answer) < 1) { // A question must have at least 1 answer. 455 $this->error(get_string('missinganswer', 'qformat_webct', $nquestionstartline), '', $question->name); 456 $questionok = false; 457 } else { 458 // Create empty feedback array. 459 foreach ($question->answer as $key => $dataanswer) { 460 if (!isset($question->feedback[$key])) { 461 $question->feedback[$key]['text'] = ''; 462 $question->feedback[$key]['format'] = FORMAT_HTML; 463 } 464 } 465 // This tempgeneralfeedback allows the code to work with versions from 1.6 to 1.9. 466 // When question->generalfeedback is undefined, the webct feedback is added to each answer feedback. 467 if (isset($question->tempgeneralfeedback)) { 468 if (isset($question->generalfeedback)) { 469 $generalfeedback = $this->text_field($question->tempgeneralfeedback); 470 $question->generalfeedback = $generalfeedback['text']; 471 $question->generalfeedbackformat = $generalfeedback['format']; 472 if (isset($generalfeedback['itemid'])) { 473 $question->genralfeedbackitemid = $generalfeedback['itemid']; 474 } 475 } else { 476 foreach ($question->answer as $key => $dataanswer) { 477 if ($question->tempgeneralfeedback != '') { 478 $question->feedback[$key]['text'] = $question->tempgeneralfeedback 479 .'<br/>'.$question->feedback[$key]['text']; 480 } 481 } 482 } 483 unset($question->tempgeneralfeedback); 484 } 485 $maxfraction = -1; 486 $totalfraction = 0; 487 foreach ($question->fraction as $fraction) { 488 if ($fraction > 0) { 489 $totalfraction += $fraction; 490 } 491 if ($fraction > $maxfraction) { 492 $maxfraction = $fraction; 493 } 494 } 495 switch ($question->qtype) { 496 case 'shortanswer': 497 if ($maxfraction != 1) { 498 $maxfraction = $maxfraction * 100; 499 $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter) 500 .' '.get_string('fractionsnomax', 'question', $maxfraction), '', $question->name);; 501 $questionok = false; 502 } 503 break; 504 505 case 'multichoice': 506 $question = $this->add_blank_combined_feedback($question); 507 508 if ($question->single) { 509 if ($maxfraction != 1) { 510 $maxfraction = $maxfraction * 100; 511 $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter) 512 .' '.get_string('fractionsnomax', 'question', $maxfraction), '', $question->name); 513 $questionok = false; 514 } 515 } else { 516 $totalfraction = round($totalfraction, 2); 517 if ($totalfraction != 1) { 518 $totalfraction = $totalfraction * 100; 519 $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter) 520 .' '.get_string('fractionsaddwrong', 'qtype_multichoice', $totalfraction), 521 '', $question->name); 522 $questionok = false; 523 } 524 } 525 break; 526 527 case 'calculated': 528 foreach ($question->answer as $answer) { 529 if ($formulaerror = qtype_calculated_find_formula_errors($answer)) { 530 $warnings[] = "'{$question->name}': ". $formulaerror; 531 $questionok = false; 532 } 533 } 534 foreach ($question->dataset as $dataset) { 535 $dataset->itemcount = count($dataset->datasetitem); 536 } 537 $question->import_process = true; 538 break; 539 case 'match': 540 // MDL-10680: 541 // Switch subquestions and subanswers. 542 $question = $this->add_blank_combined_feedback($question); 543 foreach ($question->subquestions as $id => $subquestion) { 544 $temp = $question->subquestions[$id]; 545 $question->subquestions[$id] = $question->subanswers[$id]; 546 $question->subanswers[$id] = $temp; 547 } 548 if (count($question->answer) < 3) { 549 // Add a dummy missing question. 550 $question->name = 'Dummy question added '.$question->name; 551 $question->answer[] = 'dummy'; 552 $question->subanswers[] = 'dummy'; 553 $question->subquestions[] = 'dummy'; 554 $question->fraction[] = '0.0'; 555 $question->feedback[] = ''; 556 } 557 break; 558 default: 559 // No problemo. 560 } 561 } 562 563 if ($questionok) { 564 $questions[] = $question; // Store it. 565 unset($question); // And prepare a new one. 566 $question = $this->defaultquestion(); 567 } 568 } 569 $nquestionstartline = $nlinecounter; 570 } 571 572 // Processing Question Header. 573 574 if (preg_match("~^:TYPE:MC:1(.*)~i", $line, $webctoptions)) { 575 // Multiple Choice Question with only one good answer. 576 $question = $this->defaultquestion(); 577 $question->feedback = array(); 578 $question->qtype = 'multichoice'; 579 $question->single = 1; // Only one answer is allowed. 580 $ignorerestofquestion = false; 581 continue; 582 } 583 584 if (preg_match("~^:TYPE:MC:N(.*)~i", $line, $webctoptions)) { 585 // Multiple Choice Question with several good answers. 586 $question = $this->defaultquestion(); 587 $question->feedback = array(); 588 $question->qtype = 'multichoice'; 589 $question->single = 0; // Many answers allowed. 590 $ignorerestofquestion = false; 591 continue; 592 } 593 594 if (preg_match("~^:TYPE:S~i", $line)) { 595 // Short Answer Question. 596 $question = $this->defaultquestion(); 597 $question->feedback = array(); 598 $question->qtype = 'shortanswer'; 599 $question->usecase = 0; // Ignore case. 600 $ignorerestofquestion = false; 601 continue; 602 } 603 604 if (preg_match("~^:TYPE:C~i", $line)) { 605 // Calculated Question. 606 $question = $this->defaultquestion(); 607 $question->qtype = 'calculated'; 608 $question->answer = array(); // No problem as they go as :FORMULA: from webct. 609 $question->units = array(); 610 $question->dataset = array(); 611 $question->fraction = array('1.0'); 612 $question->feedback = array(); 613 614 $currentchoice = -1; 615 $ignorerestofquestion = false; 616 continue; 617 } 618 619 if (preg_match("~^:TYPE:M~i", $line)) { 620 // Match Question. 621 $question = $this->defaultquestion(); 622 $question->qtype = 'match'; 623 $question->feedback = array(); 624 $ignorerestofquestion = false; // Match question processing is not debugged. 625 continue; 626 } 627 628 if (preg_match("~^:TYPE:P~i", $line)) { 629 // Paragraph Question. 630 $question = $this->defaultquestion(); 631 $question->qtype = 'essay'; 632 $question->responseformat = 'editor'; 633 $question->responserequired = 1; 634 $question->responsefieldlines = 15; 635 $question->attachments = 0; 636 $question->attachmentsrequired = 0; 637 $question->graderinfo = array( 638 'text' => '', 639 'format' => FORMAT_HTML, 640 ); 641 $question->feedback = array(); 642 $question->generalfeedback = ''; 643 $question->generalfeedbackformat = FORMAT_HTML; 644 $question->generalfeedbackfiles = array(); 645 $question->responsetemplate = $this->text_field(''); 646 $question->questiontextformat = FORMAT_HTML; 647 $ignorerestofquestion = false; 648 // To make us pass the end-of-question sanity checks. 649 $question->answer = array('dummy'); 650 $question->fraction = array('1.0'); 651 continue; 652 } 653 654 if (preg_match("~^:TYPE:~i", $line)) { 655 // Unknow question type. 656 $warnings[] = get_string('unknowntype', 'qformat_webct', $nlinecounter); 657 unset($question); 658 $ignorerestofquestion = true; // Question Type not handled by Moodle. 659 continue; 660 } 661 662 if ($ignorerestofquestion) { 663 continue; 664 } 665 666 if (preg_match("~^:TITLE:(.*)~i", $line, $webctoptions)) { 667 $name = trim($webctoptions[1]); 668 $question->name = $this->clean_question_name($name); 669 continue; 670 } 671 672 if (preg_match("~^:IMAGE:(.*)~i", $line, $webctoptions)) { 673 $filename = trim($webctoptions[1]); 674 if (preg_match("~^http://~i", $filename)) { 675 $question->image = $filename; 676 } 677 continue; 678 } 679 680 // Need to put the parsing of calculated items here to avoid ambitiuosness: 681 // if question isn't defined yet there is nothing to do here (avoid notices). 682 if (!isset($question)) { 683 continue; 684 } 685 if (isset($question->qtype ) && 'calculated' == $question->qtype && preg_match( 686 "~^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?({$webctnumberregex})~", $line, $webctoptions)) { 687 $datasetname = preg_replace('/^::/', '', $webctoptions[1]); 688 $datasetvalue = qformat_webct_convert_formula($webctoptions[4]); 689 switch ($webctoptions[2]) { 690 case 'MIN': 691 $question->dataset[$datasetname]->min = $datasetvalue; 692 break; 693 case 'MAX': 694 $question->dataset[$datasetname]->max = $datasetvalue; 695 break; 696 case 'DEC': 697 $datasetvalue = floor($datasetvalue); // Int only! 698 $question->dataset[$datasetname]->length = max(0, $datasetvalue); 699 break; 700 default: 701 // The VAL case. 702 $question->dataset[$datasetname]->datasetitem[$webctoptions[3]] = new stdClass(); 703 $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->itemnumber = $webctoptions[3]; 704 $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->value = $datasetvalue; 705 break; 706 } 707 continue; 708 } 709 710 $bishtmltext = preg_match("~:H$~i", $line); // True if next lines are coded in HTML. 711 if (preg_match("~^:QUESTION~i", $line)) { 712 $questiontext = ''; // Start gathering next lines. 713 continue; 714 } 715 716 if (preg_match("~^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)~i", $line, $webctoptions)) { // Shortanswer. 717 $currentchoice = $webctoptions[1]; 718 $answertext = $webctoptions[2]; // Start gathering next lines. 719 $question->fraction[$currentchoice] = ($webctoptions[3]/100); 720 continue; 721 } 722 723 if (preg_match("~^:ANSWER([0-9]+):([0-9\.\-]+)~i", $line, $webctoptions)) { 724 $answertext = ''; // Start gathering next lines. 725 $currentchoice = $webctoptions[1]; 726 $question->fraction[$currentchoice] = ($webctoptions[2]/100); 727 continue; 728 } 729 730 if (preg_match('~^:ANSWER:~i', $line)) { // Essay. 731 $graderinfo = ''; // Start gathering next lines. 732 continue; 733 } 734 735 if (preg_match('~^:FORMULA:(.*)~i', $line, $webctoptions)) { 736 // Answer for a calculated question. 737 ++$currentchoice; 738 $question->answer[$currentchoice] = 739 qformat_webct_convert_formula($webctoptions[1]); 740 741 // Default settings. 742 $question->fraction[$currentchoice] = 1.0; 743 $question->tolerance[$currentchoice] = 0.0; 744 $question->tolerancetype[$currentchoice] = 2; // Nominal (units in webct). 745 $question->feedback[$currentchoice]['text'] = ''; 746 $question->feedback[$currentchoice]['format'] = FORMAT_HTML; 747 $question->correctanswerlength[$currentchoice] = 4; 748 749 $datasetnames = 750 question_bank::get_qtype('calculated')->find_dataset_names($webctoptions[1]); 751 foreach ($datasetnames as $datasetname) { 752 $question->dataset[$datasetname] = new stdClass(); 753 $question->dataset[$datasetname]->datasetitem = array(); 754 $question->dataset[$datasetname]->name = $datasetname; 755 $question->dataset[$datasetname]->distribution = 'uniform'; 756 $question->dataset[$datasetname]->status = 'private'; 757 } 758 continue; 759 } 760 761 if (preg_match("~^:L([0-9]+)~i", $line, $webctoptions)) { 762 $answertext = ''; // Start gathering next lines. 763 $currentchoice = $webctoptions[1]; 764 $question->fraction[$currentchoice] = 1; 765 continue; 766 } 767 768 if (preg_match("~^:R([0-9]+)~i", $line, $webctoptions)) { 769 $responsetext = ''; // Start gathering next lines. 770 $currentchoice = $webctoptions[1]; 771 continue; 772 } 773 774 if (preg_match("~^:REASON([0-9]+):?~i", $line, $webctoptions)) { 775 $feedbacktext = ''; // Start gathering next lines. 776 $currentchoice = $webctoptions[1]; 777 continue; 778 } 779 if (preg_match("~^:FEEDBACK([0-9]+):?~i", $line, $webctoptions)) { 780 $generalfeedbacktext = ''; // Start gathering next lines. 781 $currentchoice = $webctoptions[1]; 782 continue; 783 } 784 if (preg_match('~^:FEEDBACK:(.*)~i', $line, $webctoptions)) { 785 $generalfeedbacktext = ''; // Start gathering next lines. 786 continue; 787 } 788 if (preg_match('~^:LAYOUT:(.*)~i', $line, $webctoptions)) { 789 // Ignore since layout in question_multichoice is no more used in Moodle. 790 // $webctoptions[1] contains either vertical or horizontal. 791 continue; 792 } 793 794 if (isset($question->qtype ) && 'calculated' == $question->qtype 795 && preg_match('~^:ANS-DEC:([1-9][0-9]*)~i', $line, $webctoptions)) { 796 // We can but hope that this always appear before the ANSTYPE property. 797 $question->correctanswerlength[$currentchoice] = $webctoptions[1]; 798 continue; 799 } 800 801 if (isset($question->qtype )&& 'calculated' == $question->qtype 802 && preg_match("~^:TOL:({$webctnumberregex})~i", $line, $webctoptions)) { 803 // We can but hope that this always appear before the TOL property. 804 $question->tolerance[$currentchoice] = 805 qformat_webct_convert_formula($webctoptions[1]); 806 continue; 807 } 808 809 if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:TOLTYPE:percent~i', $line)) { 810 // Percentage case is handled as relative in Moodle. 811 $question->tolerance[$currentchoice] /= 100; 812 $question->tolerancetype[$currentchoice] = 1; // Relative. 813 continue; 814 } 815 816 if (preg_match('~^:UNITS:(.+)~i', $line, $webctoptions) 817 and $webctunits = trim($webctoptions[1])) { 818 // This is a guess - I really do not know how different webct units are separated... 819 $webctunits = explode(':', $webctunits); 820 $unitrec->multiplier = 1.0; // Webct does not seem to support this. 821 foreach ($webctunits as $webctunit) { 822 $unitrec->unit = trim($webctunit); 823 $question->units[] = $unitrec; 824 } 825 continue; 826 } 827 828 if (!empty($question->units) && preg_match('~^:UNITREQ:(.*)~i', $line, $webctoptions) 829 && !$webctoptions[1]) { 830 // There are units but units are not required so add the no unit alternative. 831 // We can but hope that the UNITS property always appear before this property. 832 $unitrec->unit = ''; 833 $unitrec->multiplier = 1.0; 834 $question->units[] = $unitrec; 835 continue; 836 } 837 838 if (!empty($question->units) && preg_match('~^:UNITCASE:~i', $line)) { 839 // This could be important but I was not able to figure out how 840 // it works so I ignore it for now. 841 continue; 842 } 843 844 if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:dec~i', $line)) { 845 $question->correctanswerformat[$currentchoice] = '1'; 846 continue; 847 } 848 if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:sig~i', $line)) { 849 $question->correctanswerformat[$currentchoice] = '2'; 850 continue; 851 } 852 } 853 854 if (count($warnings) > 0) { 855 echo '<p>'.get_string('warningsdetected', 'qformat_webct', count($warnings)).'</p><ul>'; 856 foreach ($warnings as $warning) { 857 echo "<li>{$warning}</li>"; 858 } 859 echo '</ul>'; 860 } 861 return $questions; 862 } 863 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body