Differences Between: [Versions 310 and 311] [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 * Validate the given file. 194 * 195 * For more expensive or detailed integrity checks. 196 * 197 * @param stored_file $file the file to check 198 * @return string the error message that occurred while validating the given file 199 */ 200 public function validate_file(stored_file $file): string { 201 return $this->validate_is_utf8_file($file); 202 } 203 204 /** 205 * Store an image file in a draft filearea 206 * @param array $text, if itemid element don't exists it will be created 207 * @param string tempdir path to root of image tree 208 * @param string filepathinsidetempdir path to image in the tree 209 * @param string filename image's name 210 * @return string new name of the image as it was stored 211 */ 212 protected function store_file_for_text_field(&$text, $tempdir, $filepathinsidetempdir, $filename) { 213 global $USER; 214 $fs = get_file_storage(); 215 if (empty($text['itemid'])) { 216 $text['itemid'] = file_get_unused_draft_itemid(); 217 } 218 // As question file areas don't support subdirs, 219 // convert path to filename. 220 // So that images with same name can be imported. 221 $newfilename = clean_param(str_replace('/', '__', $filepathinsidetempdir . '__' . $filename), PARAM_FILE); 222 $filerecord = array( 223 'contextid' => context_user::instance($USER->id)->id, 224 'component' => 'user', 225 'filearea' => 'draft', 226 'itemid' => $text['itemid'], 227 'filepath' => '/', 228 'filename' => $newfilename, 229 ); 230 $fs->create_file_from_pathname($filerecord, $tempdir . '/' . $filepathinsidetempdir . '/' . $filename); 231 return $newfilename; 232 } 233 234 /** 235 * Given an HTML text with references to images files, 236 * store all images in a draft filearea, 237 * and return an array with all urls in text recoded, 238 * format set to FORMAT_HTML, and itemid set to filearea itemid 239 * @param string text text to parse and recode 240 * @return array with keys text, format, itemid. 241 */ 242 public function text_field($text) { 243 $data = array(); 244 // Step one, find all file refs then add to array. 245 preg_match_all('|<img[^>]+src="([^"]*)"|i', $text, $out); // Find all src refs. 246 247 $filepaths = array(); 248 foreach ($out[1] as $path) { 249 $fullpath = $this->tempdir . '/' . $path; 250 if (is_readable($fullpath) && !in_array($path, $filepaths)) { 251 $dirpath = dirname($path); 252 $filename = basename($path); 253 $newfilename = $this->store_file_for_text_field($data, $this->tempdir, $dirpath, $filename); 254 $text = preg_replace("|{$path}|", "@@PLUGINFILE@@/" . $newfilename, $text); 255 $filepaths[] = $path; 256 } 257 258 } 259 $data['text'] = $text; 260 $data['format'] = FORMAT_HTML; 261 return $data; 262 } 263 264 /** 265 * Does any post-processing that may be desired 266 * Clean the temporary directory if a zip file was imported 267 * @return bool success 268 */ 269 public function importpostprocess() { 270 if (!empty($this->tempdir)) { 271 fulldelete($this->tempdir); 272 } 273 return true; 274 } 275 276 /** 277 * Return content of all files containing questions, 278 * as an array one element for each file found, 279 * For each file, the corresponding element is an array of lines. 280 * @param string filename name of file 281 * @return mixed contents array or false on failure 282 */ 283 public function readdata($filename) { 284 285 // Find if we are importing a .txt file. 286 if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'txt') { 287 if (!is_readable($filename)) { 288 $this->error(get_string('filenotreadable', 'error')); 289 return false; 290 } 291 return file($filename); 292 } 293 // We are importing a zip file. 294 // Create name for temporary directory. 295 $this->tempdir = make_request_directory(); 296 if (is_readable($filename)) { 297 if (!copy($filename, $this->tempdir . '/webct.zip')) { 298 $this->error(get_string('cannotcopybackup', 'question')); 299 fulldelete($this->tempdir); 300 return false; 301 } 302 $packer = get_file_packer('application/zip'); 303 if ($packer->extract_to_pathname($this->tempdir . '/webct.zip', $this->tempdir, null, null, true)) { 304 $dir = $this->tempdir; 305 if ((($handle = opendir($dir))) == false) { 306 // The directory could not be opened. 307 fulldelete($this->tempdir); 308 return false; 309 } 310 // Create arrays to store files and directories. 311 $dirfiles = array(); 312 $dirsubdirs = array(); 313 $slash = '/'; 314 315 // Loop through all directory entries, and construct two temporary arrays containing files and sub directories. 316 while (false !== ($entry = readdir($handle))) { 317 if (is_dir($dir. $slash .$entry) && $entry != '..' && $entry != '.') { 318 $dirsubdirs[] = $dir. $slash .$entry; 319 } else if ($entry != '..' && $entry != '.') { 320 $dirfiles[] = $dir. $slash .$entry; 321 } 322 } 323 if ((($handle = opendir($dirsubdirs[0]))) == false) { 324 // The directory could not be opened. 325 fulldelete($this->tempdir); 326 return false; 327 } 328 while (false !== ($entry = readdir($handle))) { 329 if (is_dir($dirsubdirs[0]. $slash .$entry) && $entry != '..' && $entry != '.') { 330 $dirsubdirs[] = $dirsubdirs[0]. $slash .$entry; 331 } else if ($entry != '..' && $entry != '.') { 332 $dirfiles[] = $dirsubdirs[0]. $slash .$entry; 333 } 334 } 335 return file($dirfiles[1]); 336 } else { 337 $this->error(get_string('cannotunzip', 'question')); 338 fulldelete($this->tempdir); 339 } 340 } else { 341 $this->error(get_string('cannotreaduploadfile', 'error')); 342 fulldelete($this->tempdir); 343 } 344 return false; 345 } 346 347 public function readquestions ($lines) { 348 $webctnumberregex = 349 '[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)((e|E|\\*10\\*\\*)([+-]?[0-9]+|\\([+-]?[0-9]+\\)))?'; 350 351 $questions = array(); 352 $warnings = array(); 353 $webctoptions = array(); 354 355 $ignorerestofquestion = false; 356 357 $nlinecounter = 0; 358 $nquestionstartline = 0; 359 $bishtmltext = false; 360 $lines[] = ":EOF:"; // For an easiest processing of the last line. 361 // We don't call defaultquestion() here, it will be called later. 362 363 foreach ($lines as $line) { 364 $nlinecounter++; 365 $line = core_text::convert($line, 'windows-1252', 'utf-8'); 366 // Processing multiples lines strings. 367 368 if (isset($questiontext) and is_string($questiontext)) { 369 if (preg_match("~^:~", $line)) { 370 $questiontext = $this->text_field(trim($questiontext)); 371 $question->questiontext = $questiontext['text']; 372 $question->questiontextformat = $questiontext['format']; 373 if (isset($questiontext['itemid'])) { 374 $question->questiontextitemid = $questiontext['itemid']; 375 } 376 unset($questiontext); 377 } else { 378 $questiontext .= str_replace('\:', ':', $line); 379 continue; 380 } 381 } 382 383 if (isset($answertext) and is_string($answertext)) { 384 if (preg_match("~^:~", $line)) { 385 $answertext = trim($answertext); 386 if ($question->qtype == 'multichoice' || $question->qtype == 'match' ) { 387 $question->answer[$currentchoice] = $this->text_field($answertext); 388 $question->subanswers[$currentchoice] = $question->answer[$currentchoice]; 389 390 } else { 391 $question->answer[$currentchoice] = $answertext; 392 $question->subanswers[$currentchoice] = $answertext; 393 } 394 unset($answertext); 395 } else { 396 $answertext .= str_replace('\:', ':', $line); 397 continue; 398 } 399 } 400 401 if (isset($responsetext) and is_string($responsetext)) { 402 if (preg_match("~^:~", $line)) { 403 $question->subquestions[$currentchoice] = trim($responsetext); 404 unset($responsetext); 405 } else { 406 $responsetext .= str_replace('\:', ':', $line); 407 continue; 408 } 409 } 410 411 if (isset($feedbacktext) and is_string($feedbacktext)) { 412 if (preg_match("~^:~", $line)) { 413 $question->feedback[$currentchoice] = $this->text_field(trim($feedbacktext)); 414 unset($feedbacktext); 415 } else { 416 $feedbacktext .= str_replace('\:', ':', $line); 417 continue; 418 } 419 } 420 421 if (isset($generalfeedbacktext) and is_string($generalfeedbacktext)) { 422 if (preg_match("~^:~", $line)) { 423 $question->tempgeneralfeedback = trim($generalfeedbacktext); 424 unset($generalfeedbacktext); 425 } else { 426 $generalfeedbacktext .= str_replace('\:', ':', $line); 427 continue; 428 } 429 } 430 431 if (isset($graderinfo) and is_string($graderinfo)) { 432 if (preg_match("~^:~", $line)) { 433 $question->graderinfo['text'] = trim($graderinfo); 434 $question->graderinfo['format'] = FORMAT_HTML; 435 unset($graderinfo); 436 } else { 437 $graderinfo .= str_replace('\:', ':', $line); 438 continue; 439 } 440 } 441 442 $line = trim($line); 443 444 if (preg_match("~^:(TYPE|EOF):~i", $line)) { 445 // New Question or End of File. 446 if (isset($question)) { // If previous question exists, complete, check and save it. 447 448 // Setup default value of missing fields. 449 if (!isset($question->name)) { 450 $question->name = $this->create_default_question_name( 451 $question->questiontext, get_string('questionname', 'question')); 452 } 453 if (!isset($question->defaultmark)) { 454 $question->defaultmark = 1; 455 } 456 if (!isset($question->image)) { 457 $question->image = ''; 458 } 459 460 // Perform sanity checks. 461 $questionok = true; 462 if (strlen($question->questiontext) == 0) { 463 $warnings[] = get_string('missingquestion', 'qformat_webct', $nquestionstartline); 464 $questionok = false; 465 } 466 if (count($question->answer) < 1) { // A question must have at least 1 answer. 467 $this->error(get_string('missinganswer', 'qformat_webct', $nquestionstartline), '', $question->name); 468 $questionok = false; 469 } else { 470 // Create empty feedback array. 471 foreach ($question->answer as $key => $dataanswer) { 472 if (!isset($question->feedback[$key])) { 473 $question->feedback[$key]['text'] = ''; 474 $question->feedback[$key]['format'] = FORMAT_HTML; 475 } 476 } 477 // This tempgeneralfeedback allows the code to work with versions from 1.6 to 1.9. 478 // When question->generalfeedback is undefined, the webct feedback is added to each answer feedback. 479 if (isset($question->tempgeneralfeedback)) { 480 if (isset($question->generalfeedback)) { 481 $generalfeedback = $this->text_field($question->tempgeneralfeedback); 482 $question->generalfeedback = $generalfeedback['text']; 483 $question->generalfeedbackformat = $generalfeedback['format']; 484 if (isset($generalfeedback['itemid'])) { 485 $question->genralfeedbackitemid = $generalfeedback['itemid']; 486 } 487 } else { 488 foreach ($question->answer as $key => $dataanswer) { 489 if ($question->tempgeneralfeedback != '') { 490 $question->feedback[$key]['text'] = $question->tempgeneralfeedback 491 .'<br/>'.$question->feedback[$key]['text']; 492 } 493 } 494 } 495 unset($question->tempgeneralfeedback); 496 } 497 $maxfraction = -1; 498 $totalfraction = 0; 499 foreach ($question->fraction as $fraction) { 500 if ($fraction > 0) { 501 $totalfraction += $fraction; 502 } 503 if ($fraction > $maxfraction) { 504 $maxfraction = $fraction; 505 } 506 } 507 switch ($question->qtype) { 508 case 'shortanswer': 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 break; 516 517 case 'multichoice': 518 $question = $this->add_blank_combined_feedback($question); 519 520 if ($question->single) { 521 if ($maxfraction != 1) { 522 $maxfraction = $maxfraction * 100; 523 $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter) 524 .' '.get_string('fractionsnomax', 'question', $maxfraction), '', $question->name); 525 $questionok = false; 526 } 527 } else { 528 $totalfraction = round($totalfraction, 2); 529 if ($totalfraction != 1) { 530 $totalfraction = $totalfraction * 100; 531 $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter) 532 .' '.get_string('fractionsaddwrong', 'qtype_multichoice', $totalfraction), 533 '', $question->name); 534 $questionok = false; 535 } 536 } 537 break; 538 539 case 'calculated': 540 foreach ($question->answer as $answer) { 541 if ($formulaerror = qtype_calculated_find_formula_errors($answer)) { 542 $warnings[] = "'{$question->name}': ". $formulaerror; 543 $questionok = false; 544 } 545 } 546 foreach ($question->dataset as $dataset) { 547 $dataset->itemcount = count($dataset->datasetitem); 548 } 549 $question->import_process = true; 550 break; 551 case 'match': 552 // MDL-10680: 553 // Switch subquestions and subanswers. 554 $question = $this->add_blank_combined_feedback($question); 555 foreach ($question->subquestions as $id => $subquestion) { 556 $temp = $question->subquestions[$id]; 557 $question->subquestions[$id] = $question->subanswers[$id]; 558 $question->subanswers[$id] = $temp; 559 } 560 if (count($question->answer) < 3) { 561 // Add a dummy missing question. 562 $question->name = 'Dummy question added '.$question->name; 563 $question->answer[] = 'dummy'; 564 $question->subanswers[] = 'dummy'; 565 $question->subquestions[] = 'dummy'; 566 $question->fraction[] = '0.0'; 567 $question->feedback[] = ''; 568 } 569 break; 570 default: 571 // No problemo. 572 } 573 } 574 575 if ($questionok) { 576 $questions[] = $question; // Store it. 577 unset($question); // And prepare a new one. 578 $question = $this->defaultquestion(); 579 } 580 } 581 $nquestionstartline = $nlinecounter; 582 } 583 584 // Processing Question Header. 585 586 if (preg_match("~^:TYPE:MC:1(.*)~i", $line, $webctoptions)) { 587 // Multiple Choice Question with only one good answer. 588 $question = $this->defaultquestion(); 589 $question->feedback = array(); 590 $question->qtype = 'multichoice'; 591 $question->single = 1; // Only one answer is allowed. 592 $ignorerestofquestion = false; 593 continue; 594 } 595 596 if (preg_match("~^:TYPE:MC:N(.*)~i", $line, $webctoptions)) { 597 // Multiple Choice Question with several good answers. 598 $question = $this->defaultquestion(); 599 $question->feedback = array(); 600 $question->qtype = 'multichoice'; 601 $question->single = 0; // Many answers allowed. 602 $ignorerestofquestion = false; 603 continue; 604 } 605 606 if (preg_match("~^:TYPE:S~i", $line)) { 607 // Short Answer Question. 608 $question = $this->defaultquestion(); 609 $question->feedback = array(); 610 $question->qtype = 'shortanswer'; 611 $question->usecase = 0; // Ignore case. 612 $ignorerestofquestion = false; 613 continue; 614 } 615 616 if (preg_match("~^:TYPE:C~i", $line)) { 617 // Calculated Question. 618 $question = $this->defaultquestion(); 619 $question->qtype = 'calculated'; 620 $question->answer = array(); // No problem as they go as :FORMULA: from webct. 621 $question->units = array(); 622 $question->dataset = array(); 623 $question->fraction = array('1.0'); 624 $question->feedback = array(); 625 626 $currentchoice = -1; 627 $ignorerestofquestion = false; 628 continue; 629 } 630 631 if (preg_match("~^:TYPE:M~i", $line)) { 632 // Match Question. 633 $question = $this->defaultquestion(); 634 $question->qtype = 'match'; 635 $question->feedback = array(); 636 $ignorerestofquestion = false; // Match question processing is not debugged. 637 continue; 638 } 639 640 if (preg_match("~^:TYPE:P~i", $line)) { 641 // Paragraph Question. 642 $question = $this->defaultquestion(); 643 $question->qtype = 'essay'; 644 $question->responseformat = 'editor'; 645 $question->responserequired = 1; 646 $question->responsefieldlines = 15; 647 $question->attachments = 0; 648 $question->attachmentsrequired = 0; 649 $question->graderinfo = array( 650 'text' => '', 651 'format' => FORMAT_HTML, 652 ); 653 $question->feedback = array(); 654 $question->generalfeedback = ''; 655 $question->generalfeedbackformat = FORMAT_HTML; 656 $question->generalfeedbackfiles = array(); 657 $question->responsetemplate = $this->text_field(''); 658 $question->questiontextformat = FORMAT_HTML; 659 $ignorerestofquestion = false; 660 // To make us pass the end-of-question sanity checks. 661 $question->answer = array('dummy'); 662 $question->fraction = array('1.0'); 663 continue; 664 } 665 666 if (preg_match("~^:TYPE:~i", $line)) { 667 // Unknow question type. 668 $warnings[] = get_string('unknowntype', 'qformat_webct', $nlinecounter); 669 unset($question); 670 $ignorerestofquestion = true; // Question Type not handled by Moodle. 671 continue; 672 } 673 674 if ($ignorerestofquestion) { 675 continue; 676 } 677 678 if (preg_match("~^:TITLE:(.*)~i", $line, $webctoptions)) { 679 $name = trim($webctoptions[1]); 680 $question->name = $this->clean_question_name($name); 681 continue; 682 } 683 684 if (preg_match("~^:IMAGE:(.*)~i", $line, $webctoptions)) { 685 $filename = trim($webctoptions[1]); 686 if (preg_match("~^http://~i", $filename)) { 687 $question->image = $filename; 688 } 689 continue; 690 } 691 692 // Need to put the parsing of calculated items here to avoid ambitiuosness: 693 // if question isn't defined yet there is nothing to do here (avoid notices). 694 if (!isset($question)) { 695 continue; 696 } 697 if (isset($question->qtype ) && 'calculated' == $question->qtype && preg_match( 698 "~^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?({$webctnumberregex})~", $line, $webctoptions)) { 699 $datasetname = preg_replace('/^::/', '', $webctoptions[1]); 700 $datasetvalue = qformat_webct_convert_formula($webctoptions[4]); 701 switch ($webctoptions[2]) { 702 case 'MIN': 703 $question->dataset[$datasetname]->min = $datasetvalue; 704 break; 705 case 'MAX': 706 $question->dataset[$datasetname]->max = $datasetvalue; 707 break; 708 case 'DEC': 709 $datasetvalue = floor($datasetvalue); // Int only! 710 $question->dataset[$datasetname]->length = max(0, $datasetvalue); 711 break; 712 default: 713 // The VAL case. 714 $question->dataset[$datasetname]->datasetitem[$webctoptions[3]] = new stdClass(); 715 $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->itemnumber = $webctoptions[3]; 716 $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->value = $datasetvalue; 717 break; 718 } 719 continue; 720 } 721 722 $bishtmltext = preg_match("~:H$~i", $line); // True if next lines are coded in HTML. 723 if (preg_match("~^:QUESTION~i", $line)) { 724 $questiontext = ''; // Start gathering next lines. 725 continue; 726 } 727 728 if (preg_match("~^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)~i", $line, $webctoptions)) { // Shortanswer. 729 $currentchoice = $webctoptions[1]; 730 $answertext = $webctoptions[2]; // Start gathering next lines. 731 $question->fraction[$currentchoice] = ($webctoptions[3]/100); 732 continue; 733 } 734 735 if (preg_match("~^:ANSWER([0-9]+):([0-9\.\-]+)~i", $line, $webctoptions)) { 736 $answertext = ''; // Start gathering next lines. 737 $currentchoice = $webctoptions[1]; 738 $question->fraction[$currentchoice] = ($webctoptions[2]/100); 739 continue; 740 } 741 742 if (preg_match('~^:ANSWER:~i', $line)) { // Essay. 743 $graderinfo = ''; // Start gathering next lines. 744 continue; 745 } 746 747 if (preg_match('~^:FORMULA:(.*)~i', $line, $webctoptions)) { 748 // Answer for a calculated question. 749 ++$currentchoice; 750 $question->answer[$currentchoice] = 751 qformat_webct_convert_formula($webctoptions[1]); 752 753 // Default settings. 754 $question->fraction[$currentchoice] = 1.0; 755 $question->tolerance[$currentchoice] = 0.0; 756 $question->tolerancetype[$currentchoice] = 2; // Nominal (units in webct). 757 $question->feedback[$currentchoice]['text'] = ''; 758 $question->feedback[$currentchoice]['format'] = FORMAT_HTML; 759 $question->correctanswerlength[$currentchoice] = 4; 760 761 $datasetnames = 762 question_bank::get_qtype('calculated')->find_dataset_names($webctoptions[1]); 763 foreach ($datasetnames as $datasetname) { 764 $question->dataset[$datasetname] = new stdClass(); 765 $question->dataset[$datasetname]->datasetitem = array(); 766 $question->dataset[$datasetname]->name = $datasetname; 767 $question->dataset[$datasetname]->distribution = 'uniform'; 768 $question->dataset[$datasetname]->status = 'private'; 769 } 770 continue; 771 } 772 773 if (preg_match("~^:L([0-9]+)~i", $line, $webctoptions)) { 774 $answertext = ''; // Start gathering next lines. 775 $currentchoice = $webctoptions[1]; 776 $question->fraction[$currentchoice] = 1; 777 continue; 778 } 779 780 if (preg_match("~^:R([0-9]+)~i", $line, $webctoptions)) { 781 $responsetext = ''; // Start gathering next lines. 782 $currentchoice = $webctoptions[1]; 783 continue; 784 } 785 786 if (preg_match("~^:REASON([0-9]+):?~i", $line, $webctoptions)) { 787 $feedbacktext = ''; // Start gathering next lines. 788 $currentchoice = $webctoptions[1]; 789 continue; 790 } 791 if (preg_match("~^:FEEDBACK([0-9]+):?~i", $line, $webctoptions)) { 792 $generalfeedbacktext = ''; // Start gathering next lines. 793 $currentchoice = $webctoptions[1]; 794 continue; 795 } 796 if (preg_match('~^:FEEDBACK:(.*)~i', $line, $webctoptions)) { 797 $generalfeedbacktext = ''; // Start gathering next lines. 798 continue; 799 } 800 if (preg_match('~^:LAYOUT:(.*)~i', $line, $webctoptions)) { 801 // Ignore since layout in question_multichoice is no more used in Moodle. 802 // $webctoptions[1] contains either vertical or horizontal. 803 continue; 804 } 805 806 if (isset($question->qtype ) && 'calculated' == $question->qtype 807 && preg_match('~^:ANS-DEC:([1-9][0-9]*)~i', $line, $webctoptions)) { 808 // We can but hope that this always appear before the ANSTYPE property. 809 $question->correctanswerlength[$currentchoice] = $webctoptions[1]; 810 continue; 811 } 812 813 if (isset($question->qtype )&& 'calculated' == $question->qtype 814 && preg_match("~^:TOL:({$webctnumberregex})~i", $line, $webctoptions)) { 815 // We can but hope that this always appear before the TOL property. 816 $question->tolerance[$currentchoice] = 817 qformat_webct_convert_formula($webctoptions[1]); 818 continue; 819 } 820 821 if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:TOLTYPE:percent~i', $line)) { 822 // Percentage case is handled as relative in Moodle. 823 $question->tolerance[$currentchoice] /= 100; 824 $question->tolerancetype[$currentchoice] = 1; // Relative. 825 continue; 826 } 827 828 if (preg_match('~^:UNITS:(.+)~i', $line, $webctoptions) 829 and $webctunits = trim($webctoptions[1])) { 830 // This is a guess - I really do not know how different webct units are separated... 831 $webctunits = explode(':', $webctunits); 832 $unitrec->multiplier = 1.0; // Webct does not seem to support this. 833 foreach ($webctunits as $webctunit) { 834 $unitrec->unit = trim($webctunit); 835 $question->units[] = $unitrec; 836 } 837 continue; 838 } 839 840 if (!empty($question->units) && preg_match('~^:UNITREQ:(.*)~i', $line, $webctoptions) 841 && !$webctoptions[1]) { 842 // There are units but units are not required so add the no unit alternative. 843 // We can but hope that the UNITS property always appear before this property. 844 $unitrec->unit = ''; 845 $unitrec->multiplier = 1.0; 846 $question->units[] = $unitrec; 847 continue; 848 } 849 850 if (!empty($question->units) && preg_match('~^:UNITCASE:~i', $line)) { 851 // This could be important but I was not able to figure out how 852 // it works so I ignore it for now. 853 continue; 854 } 855 856 if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:dec~i', $line)) { 857 $question->correctanswerformat[$currentchoice] = '1'; 858 continue; 859 } 860 if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:sig~i', $line)) { 861 $question->correctanswerformat[$currentchoice] = '2'; 862 continue; 863 } 864 } 865 866 if (count($warnings) > 0) { 867 echo '<p>'.get_string('warningsdetected', 'qformat_webct', count($warnings)).'</p><ul>'; 868 foreach ($warnings as $warning) { 869 echo "<li>{$warning}</li>"; 870 } 871 echo '</ul>'; 872 } 873 return $questions; 874 } 875 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body