See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
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 * GIFT format question importer/exporter. 19 * 20 * @package qformat_gift 21 * @copyright 2003 Paul Tsuchido Shew 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 /** 30 * The GIFT import filter was designed as an easy to use method 31 * for teachers writing questions as a text file. It supports most 32 * question types and the missing word format. 33 * 34 * Multiple Choice / Missing Word 35 * Who's buried in Grant's tomb?{~Grant ~Jefferson =no one} 36 * Grant is {~buried =entombed ~living} in Grant's tomb. 37 * True-False: 38 * Grant is buried in Grant's tomb.{FALSE} 39 * Short-Answer. 40 * Who's buried in Grant's tomb?{=no one =nobody} 41 * Numerical 42 * When was Ulysses S. Grant born?{#1822:5} 43 * Matching 44 * Match the following countries with their corresponding 45 * capitals.{=Canada->Ottawa =Italy->Rome =Japan->Tokyo} 46 * 47 * Comment lines start with a double backslash (//). 48 * Optional question names are enclosed in double colon(::). 49 * Answer feedback is indicated with hash mark (#). 50 * Percentage answer weights immediately follow the tilde (for 51 * multiple choice) or equal sign (for short answer and numerical), 52 * and are enclosed in percent signs (% %). See docs and examples.txt for more. 53 * 54 * This filter was written through the collaboration of numerous 55 * members of the Moodle community. It was originally based on 56 * the missingword format, which included code from Thomas Robb 57 * and others. Paul Tsuchido Shew wrote this filter in December 2003. 58 * 59 * @copyright 2003 Paul Tsuchido Shew 60 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 61 */ 62 class qformat_gift extends qformat_default { 63 64 public function provide_import() { 65 return true; 66 } 67 68 public function provide_export() { 69 return true; 70 } 71 72 public function export_file_extension() { 73 return '.txt'; 74 } 75 76 protected function answerweightparser(&$answer) { 77 $answer = substr($answer, 1); // Removes initial %. 78 $endposition = strpos($answer, "%"); 79 $answerweight = substr($answer, 0, $endposition); // Gets weight as integer. 80 $answerweight = $answerweight/100; // Converts to percent. 81 $answer = substr($answer, $endposition+1); // Removes comment from answer. 82 return $answerweight; 83 } 84 85 protected function commentparser($answer, $defaultformat) { 86 $bits = explode('#', $answer, 2); 87 $ans = $this->parse_text_with_format(trim($bits[0]), $defaultformat); 88 if (count($bits) > 1) { 89 $feedback = $this->parse_text_with_format(trim($bits[1]), $defaultformat); 90 } else { 91 $feedback = array('text' => '', 'format' => $defaultformat, 'files' => array()); 92 } 93 return array($ans, $feedback); 94 } 95 96 protected function split_truefalse_comment($answer, $defaultformat) { 97 $bits = explode('#', $answer, 3); 98 $ans = $this->parse_text_with_format(trim($bits[0]), $defaultformat); 99 if (count($bits) > 1) { 100 $wrongfeedback = $this->parse_text_with_format(trim($bits[1]), $defaultformat); 101 } else { 102 $wrongfeedback = array('text' => '', 'format' => $defaultformat, 'files' => array()); 103 } 104 if (count($bits) > 2) { 105 $rightfeedback = $this->parse_text_with_format(trim($bits[2]), $defaultformat); 106 } else { 107 $rightfeedback = array('text' => '', 'format' => $defaultformat, 'files' => array()); 108 } 109 return array($ans, $wrongfeedback, $rightfeedback); 110 } 111 112 protected function escapedchar_pre($string) { 113 // Replaces escaped control characters with a placeholder BEFORE processing. 114 115 $escapedcharacters = array("\\:", "\\#", "\\=", "\\{", "\\}", "\\~", "\\n" ); 116 $placeholders = array("&&058;", "&&035;", "&&061;", "&&123;", "&&125;", "&&126;", "&&010"); 117 118 $string = str_replace("\\\\", "&&092;", $string); 119 $string = str_replace($escapedcharacters, $placeholders, $string); 120 $string = str_replace("&&092;", "\\", $string); 121 return $string; 122 } 123 124 protected function escapedchar_post($string) { 125 // Replaces placeholders with corresponding character AFTER processing is done. 126 $placeholders = array("&&058;", "&&035;", "&&061;", "&&123;", "&&125;", "&&126;", "&&010"); 127 $characters = array(":", "#", "=", "{", "}", "~", "\n" ); 128 $string = str_replace($placeholders, $characters, $string); 129 return $string; 130 } 131 132 protected function check_answer_count($min, $answers, $text) { 133 $countanswers = count($answers); 134 if ($countanswers < $min) { 135 $this->error(get_string('importminerror', 'qformat_gift'), $text); 136 return false; 137 } 138 139 return true; 140 } 141 142 protected function parse_text_with_format($text, $defaultformat = FORMAT_MOODLE) { 143 $result = array( 144 'text' => $text, 145 'format' => $defaultformat, 146 'files' => array(), 147 ); 148 if (strpos($text, '[') === 0) { 149 $formatend = strpos($text, ']'); 150 $result['format'] = $this->format_name_to_const(substr($text, 1, $formatend - 1)); 151 if ($result['format'] == -1) { 152 $result['format'] = $defaultformat; 153 } else { 154 $result['text'] = substr($text, $formatend + 1); 155 } 156 } 157 $result['text'] = trim($this->escapedchar_post($result['text'])); 158 return $result; 159 } 160 161 public function readquestion($lines) { 162 // Given an array of lines known to define a question in this format, this function 163 // converts it into a question object suitable for processing and insertion into Moodle. 164 165 $question = $this->defaultquestion(); 166 // Define replaced by simple assignment, stop redefine notices. 167 $giftanswerweightregex = '/^%\-*([0-9]{1,2})\.?([0-9]*)%/'; 168 169 // Separate comments and implode. 170 $comments = ''; 171 foreach ($lines as $key => $line) { 172 $line = trim($line); 173 if (substr($line, 0, 2) == '//') { 174 $comments .= $line . "\n"; 175 $lines[$key] = ' '; 176 } 177 } 178 $text = trim(implode("\n", $lines)); 179 180 if ($text == '') { 181 return false; 182 } 183 184 // Substitute escaped control characters with placeholders. 185 $text = $this->escapedchar_pre($text); 186 187 // Look for category modifier. 188 if (preg_match('~^\$CATEGORY:~', $text)) { 189 $newcategory = trim(substr($text, 10)); 190 191 // Build fake question to contain category. 192 $question->qtype = 'category'; 193 $question->category = $newcategory; 194 return $question; 195 } 196 197 // Question name parser. 198 if (substr($text, 0, 2) == '::') { 199 $text = substr($text, 2); 200 201 $namefinish = strpos($text, '::'); 202 if ($namefinish === false) { 203 $question->name = false; 204 // Name will be assigned after processing question text below. 205 } else { 206 $questionname = substr($text, 0, $namefinish); 207 $question->name = $this->clean_question_name($this->escapedchar_post($questionname)); 208 $text = trim(substr($text, $namefinish+2)); // Remove name from text. 209 } 210 } else { 211 $question->name = false; 212 } 213 214 // Find the answer section. 215 $answerstart = strpos($text, '{'); 216 $answerfinish = strpos($text, '}'); 217 218 $description = false; 219 if ($answerstart === false && $answerfinish === false) { 220 // No answer means it's a description. 221 $description = true; 222 $answertext = ''; 223 $answerlength = 0; 224 225 } else if ($answerstart === false || $answerfinish === false) { 226 $this->error(get_string('braceerror', 'qformat_gift'), $text); 227 return false; 228 229 } else { 230 $answerlength = $answerfinish - $answerstart; 231 $answertext = trim(substr($text, $answerstart + 1, $answerlength - 1)); 232 } 233 234 // Format the question text, without answer, inserting "_____" as necessary. 235 if ($description) { 236 $questiontext = $text; 237 } else if (substr($text, -1) == "}") { 238 // No blank line if answers follow question, outside of closing punctuation. 239 $questiontext = substr_replace($text, "", $answerstart, $answerlength + 1); 240 } else { 241 // Inserts blank line for missing word format. 242 $questiontext = substr_replace($text, "_____", $answerstart, $answerlength + 1); 243 } 244 245 // Look to see if there is any general feedback. 246 $gfseparator = strrpos($answertext, '####'); 247 if ($gfseparator === false) { 248 $generalfeedback = ''; 249 } else { 250 $generalfeedback = substr($answertext, $gfseparator + 4); 251 $answertext = trim(substr($answertext, 0, $gfseparator)); 252 } 253 254 // Get questiontext format from questiontext. 255 $text = $this->parse_text_with_format($questiontext); 256 $question->questiontextformat = $text['format']; 257 $question->questiontext = $text['text']; 258 259 // Get generalfeedback format from questiontext. 260 $text = $this->parse_text_with_format($generalfeedback, $question->questiontextformat); 261 $question->generalfeedback = $text['text']; 262 $question->generalfeedbackformat = $text['format']; 263 264 // Set question name if not already set. 265 if ($question->name === false) { 266 $question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question')); 267 } 268 269 // Determine question type. 270 $question->qtype = null; 271 272 // Give plugins first try. 273 // Plugins must promise not to intercept standard qtypes 274 // MDL-12346, this could be called from lesson mod which has its own base class =(. 275 if (method_exists($this, 'try_importing_using_qtypes') 276 && ($tryquestion = $this->try_importing_using_qtypes($lines, $question, $answertext))) { 277 return $tryquestion; 278 } 279 280 if ($description) { 281 $question->qtype = 'description'; 282 283 } else if ($answertext == '') { 284 $question->qtype = 'essay'; 285 286 } else if ($answertext[0] == '#') { 287 $question->qtype = 'numerical'; 288 289 } else if (strpos($answertext, '~') !== false) { 290 // Only Multiplechoice questions contain tilde ~. 291 $question->qtype = 'multichoice'; 292 293 } else if (strpos($answertext, '=') !== false 294 && strpos($answertext, '->') !== false) { 295 // Only Matching contains both = and ->. 296 $question->qtype = 'match'; 297 298 } else { // Either truefalse or shortanswer. 299 300 // Truefalse question check. 301 $truefalsecheck = $answertext; 302 if (strpos($answertext, '#') > 0) { 303 // Strip comments to check for TrueFalse question. 304 $truefalsecheck = trim(substr($answertext, 0, strpos($answertext, "#"))); 305 } 306 307 $validtfanswers = array('T', 'TRUE', 'F', 'FALSE'); 308 if (in_array($truefalsecheck, $validtfanswers)) { 309 $question->qtype = 'truefalse'; 310 311 } else { // Must be shortanswer. 312 $question->qtype = 'shortanswer'; 313 } 314 } 315 316 // Extract any idnumber and tags from the comments. 317 list($question->idnumber, $question->tags) = 318 $this->extract_idnumber_and_tags_from_comment($comments); 319 320 if (!isset($question->qtype)) { 321 $giftqtypenotset = get_string('giftqtypenotset', 'qformat_gift'); 322 $this->error($giftqtypenotset, $text); 323 return false; 324 } 325 326 switch ($question->qtype) { 327 case 'description': 328 $question->defaultmark = 0; 329 $question->length = 0; 330 return $question; 331 332 case 'essay': 333 $question->responseformat = 'editor'; 334 $question->responserequired = 1; 335 $question->responsefieldlines = 15; 336 $question->attachments = 0; 337 $question->attachmentsrequired = 0; 338 $question->graderinfo = array( 339 'text' => '', 'format' => FORMAT_HTML, 'files' => array()); 340 $question->responsetemplate = array( 341 'text' => '', 'format' => FORMAT_HTML); 342 return $question; 343 344 case 'multichoice': 345 // "Temporary" solution to enable choice of answernumbering on GIFT import 346 // by respecting default set for multichoice questions (MDL-59447) 347 $question->answernumbering = get_config('qtype_multichoice', 'answernumbering'); 348 349 if (strpos($answertext, "=") === false) { 350 $question->single = 0; // Multiple answers are enabled if no single answer is 100% correct. 351 } else { 352 $question->single = 1; // Only one answer allowed (the default). 353 } 354 $question = $this->add_blank_combined_feedback($question); 355 356 $answertext = str_replace("=", "~=", $answertext); 357 $answers = explode("~", $answertext); 358 if (isset($answers[0])) { 359 $answers[0] = trim($answers[0]); 360 } 361 if (empty($answers[0])) { 362 array_shift($answers); 363 } 364 365 $countanswers = count($answers); 366 367 if (!$this->check_answer_count(2, $answers, $text)) { 368 return false; 369 } 370 371 foreach ($answers as $key => $answer) { 372 $answer = trim($answer); 373 374 // Determine answer weight. 375 if ($answer[0] == '=') { 376 $answerweight = 1; 377 $answer = substr($answer, 1); 378 379 } else if (preg_match($giftanswerweightregex, $answer)) { // Check for properly formatted answer weight. 380 $answerweight = $this->answerweightparser($answer); 381 382 } else { // Default, i.e., wrong anwer. 383 $answerweight = 0; 384 } 385 list($question->answer[$key], $question->feedback[$key]) = 386 $this->commentparser($answer, $question->questiontextformat); 387 $question->fraction[$key] = $answerweight; 388 } // End foreach answer. 389 390 return $question; 391 392 case 'match': 393 $question = $this->add_blank_combined_feedback($question); 394 395 $answers = explode('=', $answertext); 396 if (isset($answers[0])) { 397 $answers[0] = trim($answers[0]); 398 } 399 if (empty($answers[0])) { 400 array_shift($answers); 401 } 402 403 if (!$this->check_answer_count(2, $answers, $text)) { 404 return false; 405 } 406 407 foreach ($answers as $key => $answer) { 408 $answer = trim($answer); 409 if (strpos($answer, "->") === false) { 410 $this->error(get_string('giftmatchingformat', 'qformat_gift'), $answer); 411 return false; 412 } 413 414 $marker = strpos($answer, '->'); 415 $question->subquestions[$key] = $this->parse_text_with_format( 416 substr($answer, 0, $marker), $question->questiontextformat); 417 $question->subanswers[$key] = trim($this->escapedchar_post( 418 substr($answer, $marker + 2))); 419 } 420 421 return $question; 422 423 case 'truefalse': 424 list($answer, $wrongfeedback, $rightfeedback) = 425 $this->split_truefalse_comment($answertext, $question->questiontextformat); 426 427 if ($answer['text'] == "T" || $answer['text'] == "TRUE") { 428 $question->correctanswer = 1; 429 $question->feedbacktrue = $rightfeedback; 430 $question->feedbackfalse = $wrongfeedback; 431 } else { 432 $question->correctanswer = 0; 433 $question->feedbacktrue = $wrongfeedback; 434 $question->feedbackfalse = $rightfeedback; 435 } 436 437 $question->penalty = 1; 438 439 return $question; 440 441 case 'shortanswer': 442 // Shortanswer question. 443 $answers = explode("=", $answertext); 444 if (isset($answers[0])) { 445 $answers[0] = trim($answers[0]); 446 } 447 if (empty($answers[0])) { 448 array_shift($answers); 449 } 450 451 if (!$this->check_answer_count(1, $answers, $text)) { 452 return false; 453 } 454 455 foreach ($answers as $key => $answer) { 456 $answer = trim($answer); 457 458 // Answer weight. 459 if (preg_match($giftanswerweightregex, $answer)) { // Check for properly formatted answer weight. 460 $answerweight = $this->answerweightparser($answer); 461 } else { // Default, i.e., full-credit anwer. 462 $answerweight = 1; 463 } 464 465 list($answer, $question->feedback[$key]) = $this->commentparser( 466 $answer, $question->questiontextformat); 467 468 $question->answer[$key] = $answer['text']; 469 $question->fraction[$key] = $answerweight; 470 } 471 472 return $question; 473 474 case 'numerical': 475 // Note similarities to ShortAnswer. 476 $answertext = substr($answertext, 1); // Remove leading "#". 477 478 // If there is feedback for a wrong answer, store it for now. 479 if (($pos = strpos($answertext, '~')) !== false) { 480 $wrongfeedback = substr($answertext, $pos); 481 $answertext = substr($answertext, 0, $pos); 482 } else { 483 $wrongfeedback = ''; 484 } 485 486 $answers = explode("=", $answertext); 487 if (isset($answers[0])) { 488 $answers[0] = trim($answers[0]); 489 } 490 if (empty($answers[0])) { 491 array_shift($answers); 492 } 493 494 if (count($answers) == 0) { 495 // Invalid question. 496 $giftnonumericalanswers = get_string('giftnonumericalanswers', 'qformat_gift'); 497 $this->error($giftnonumericalanswers, $text); 498 return false; 499 } 500 501 foreach ($answers as $key => $answer) { 502 $answer = trim($answer); 503 504 // Answer weight. 505 if (preg_match($giftanswerweightregex, $answer)) { // Check for properly formatted answer weight. 506 $answerweight = $this->answerweightparser($answer); 507 } else { // Default, i.e., full-credit anwer. 508 $answerweight = 1; 509 } 510 511 list($answer, $question->feedback[$key]) = $this->commentparser( 512 $answer, $question->questiontextformat); 513 $question->fraction[$key] = $answerweight; 514 $answer = $answer['text']; 515 516 // Calculate Answer and Min/Max values. 517 if (strpos($answer, "..") > 0) { // Optional [min]..[max] format. 518 $marker = strpos($answer, ".."); 519 $max = trim(substr($answer, $marker + 2)); 520 $min = trim(substr($answer, 0, $marker)); 521 $ans = ($max + $min)/2; 522 $tol = $max - $ans; 523 } else if (strpos($answer, ':') > 0) { // Standard [answer]:[errormargin] format. 524 $marker = strpos($answer, ':'); 525 $tol = trim(substr($answer, $marker+1)); 526 $ans = trim(substr($answer, 0, $marker)); 527 } else { // Only one valid answer (zero errormargin). 528 $tol = 0; 529 $ans = trim($answer); 530 } 531 532 if (!(is_numeric($ans) || $ans = '*') || !is_numeric($tol)) { 533 $errornotnumbers = get_string('errornotnumbers'); 534 $this->error($errornotnumbers, $text); 535 return false; 536 } 537 538 // Store results. 539 $question->answer[$key] = $ans; 540 $question->tolerance[$key] = $tol; 541 } 542 543 if ($wrongfeedback) { 544 $key += 1; 545 $question->fraction[$key] = 0; 546 list($notused, $question->feedback[$key]) = $this->commentparser( 547 $wrongfeedback, $question->questiontextformat); 548 $question->answer[$key] = '*'; 549 $question->tolerance[$key] = ''; 550 } 551 552 return $question; 553 554 default: 555 $this->error(get_string('giftnovalidquestion', 'qformat_gift'), $text); 556 return false; 557 558 } 559 } 560 561 protected function repchar($text, $notused = 0) { 562 // Escapes 'reserved' characters # = ~ {) : 563 // Removes new lines. 564 $reserved = array( '\\', '#', '=', '~', '{', '}', ':', "\n", "\r"); 565 $escaped = array('\\\\', '\#', '\=', '\~', '\{', '\}', '\:', '\n', ''); 566 567 $newtext = str_replace($reserved, $escaped, $text); 568 return $newtext; 569 } 570 571 /** 572 * @param int $format one of the FORMAT_ constants. 573 * @return string the corresponding name. 574 */ 575 protected function format_const_to_name($format) { 576 if ($format == FORMAT_MOODLE) { 577 return 'moodle'; 578 } else if ($format == FORMAT_HTML) { 579 return 'html'; 580 } else if ($format == FORMAT_PLAIN) { 581 return 'plain'; 582 } else if ($format == FORMAT_MARKDOWN) { 583 return 'markdown'; 584 } else { 585 return 'moodle'; 586 } 587 } 588 589 /** 590 * @param int $format one of the FORMAT_ constants. 591 * @return string the corresponding name. 592 */ 593 protected function format_name_to_const($format) { 594 if ($format == 'moodle') { 595 return FORMAT_MOODLE; 596 } else if ($format == 'html') { 597 return FORMAT_HTML; 598 } else if ($format == 'plain') { 599 return FORMAT_PLAIN; 600 } else if ($format == 'markdown') { 601 return FORMAT_MARKDOWN; 602 } else { 603 return -1; 604 } 605 } 606 607 /** 608 * Extract any tags or idnumber declared in the question comment. 609 * 610 * @param string $comment E.g. "// Line 1.\n//Line 2.\n". 611 * @return array with two elements. string $idnumber (or '') and string[] of tags. 612 */ 613 public function extract_idnumber_and_tags_from_comment(string $comment): array { 614 615 // Find the idnumber, if any. There should not be more than one, but if so, we just find the first. 616 $idnumber = ''; 617 if (preg_match('~ 618 # Start of id token. 619 \[id: 620 621 # Any number of (non-control) characters, with any ] escaped. 622 # This is the bit we want so capture it. 623 ( 624 (?:\\\\]|[^][:cntrl:]])+ 625 ) 626 627 # End of id token. 628 ] 629 ~x', $comment, $match)) { 630 $idnumber = str_replace('\]', ']', trim($match[1])); 631 } 632 633 // Find any tags. 634 $tags = []; 635 if (preg_match_all('~ 636 # Start of tag token. 637 \[tag: 638 639 # Any number of allowed characters (see PARAM_TAG), with any ] escaped. 640 # This is the bit we want so capture it. 641 ( 642 (?:\\\\]|[^]<>`[:cntrl:]]|)+ 643 ) 644 645 # End of tag token. 646 ] 647 ~x', $comment, $matches)) { 648 foreach ($matches[1] as $rawtag) { 649 $tags[] = str_replace('\]', ']', trim($rawtag)); 650 } 651 } 652 653 return [$idnumber, $tags]; 654 } 655 656 public function write_name($name) { 657 return '::' . $this->repchar($name) . '::'; 658 } 659 660 public function write_questiontext($text, $format, $defaultformat = FORMAT_MOODLE) { 661 $output = ''; 662 if ($text != '' && $format != $defaultformat) { 663 $output .= '[' . $this->format_const_to_name($format) . ']'; 664 } 665 $output .= $this->repchar($text, $format); 666 return $output; 667 } 668 669 /** 670 * Outputs the general feedback for the question, if any. This needs to be the 671 * last thing before the }. 672 * @param object $question the question data. 673 * @param string $indent to put before the general feedback. Defaults to a tab. 674 * If this is not blank, a newline is added after the line. 675 */ 676 public function write_general_feedback($question, $indent = "\t") { 677 $generalfeedback = $this->write_questiontext($question->generalfeedback, 678 $question->generalfeedbackformat, $question->questiontextformat); 679 680 if ($generalfeedback) { 681 $generalfeedback = '####' . $generalfeedback; 682 if ($indent) { 683 $generalfeedback = $indent . $generalfeedback . "\n"; 684 } 685 } 686 687 return $generalfeedback; 688 } 689 690 public function writequestion($question) { 691 692 // Start with a comment. 693 $expout = "// question: {$question->id} name: {$question->name}\n"; 694 $expout .= $this->write_idnumber_and_tags($question); 695 696 // Output depends on question type. 697 switch($question->qtype) { 698 699 case 'category': 700 // Not a real question, used to insert category switch. 701 $expout .= "\$CATEGORY: $question->category\n"; 702 break; 703 704 case 'description': 705 $expout .= $this->write_name($question->name); 706 $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); 707 break; 708 709 case 'essay': 710 $expout .= $this->write_name($question->name); 711 $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); 712 $expout .= "{"; 713 $expout .= $this->write_general_feedback($question, ''); 714 $expout .= "}\n"; 715 break; 716 717 case 'truefalse': 718 $trueanswer = $question->options->answers[$question->options->trueanswer]; 719 $falseanswer = $question->options->answers[$question->options->falseanswer]; 720 if ($trueanswer->fraction == 1) { 721 $answertext = 'TRUE'; 722 $rightfeedback = $this->write_questiontext($trueanswer->feedback, 723 $trueanswer->feedbackformat, $question->questiontextformat); 724 $wrongfeedback = $this->write_questiontext($falseanswer->feedback, 725 $falseanswer->feedbackformat, $question->questiontextformat); 726 } else { 727 $answertext = 'FALSE'; 728 $rightfeedback = $this->write_questiontext($falseanswer->feedback, 729 $falseanswer->feedbackformat, $question->questiontextformat); 730 $wrongfeedback = $this->write_questiontext($trueanswer->feedback, 731 $trueanswer->feedbackformat, $question->questiontextformat); 732 } 733 734 $expout .= $this->write_name($question->name); 735 $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); 736 $expout .= '{' . $this->repchar($answertext); 737 if ($wrongfeedback) { 738 $expout .= '#' . $wrongfeedback; 739 } else if ($rightfeedback) { 740 $expout .= '#'; 741 } 742 if ($rightfeedback) { 743 $expout .= '#' . $rightfeedback; 744 } 745 $expout .= $this->write_general_feedback($question, ''); 746 $expout .= "}\n"; 747 break; 748 749 case 'multichoice': 750 $expout .= $this->write_name($question->name); 751 $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); 752 $expout .= "{\n"; 753 foreach ($question->options->answers as $answer) { 754 if ($answer->fraction == 1 && $question->options->single) { 755 $answertext = '='; 756 } else if ($answer->fraction == 0) { 757 $answertext = '~'; 758 } else { 759 $weight = $answer->fraction * 100; 760 $answertext = '~%' . $weight . '%'; 761 } 762 $expout .= "\t" . $answertext . $this->write_questiontext($answer->answer, 763 $answer->answerformat, $question->questiontextformat); 764 if ($answer->feedback != '') { 765 $expout .= '#' . $this->write_questiontext($answer->feedback, 766 $answer->feedbackformat, $question->questiontextformat); 767 } 768 $expout .= "\n"; 769 } 770 $expout .= $this->write_general_feedback($question); 771 $expout .= "}\n"; 772 break; 773 774 case 'shortanswer': 775 $expout .= $this->write_name($question->name); 776 $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); 777 $expout .= "{\n"; 778 foreach ($question->options->answers as $answer) { 779 $weight = 100 * $answer->fraction; 780 $expout .= "\t=%" . $weight . '%' . $this->repchar($answer->answer) . 781 '#' . $this->write_questiontext($answer->feedback, 782 $answer->feedbackformat, $question->questiontextformat) . "\n"; 783 } 784 $expout .= $this->write_general_feedback($question); 785 $expout .= "}\n"; 786 break; 787 788 case 'numerical': 789 $expout .= $this->write_name($question->name); 790 $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); 791 $expout .= "{#\n"; 792 foreach ($question->options->answers as $answer) { 793 if ($answer->answer != '' && $answer->answer != '*') { 794 $weight = 100 * $answer->fraction; 795 $expout .= "\t=%" . $weight . '%' . $answer->answer . ':' . 796 (float)$answer->tolerance . '#' . $this->write_questiontext($answer->feedback, 797 $answer->feedbackformat, $question->questiontextformat) . "\n"; 798 } else { 799 $expout .= "\t~#" . $this->write_questiontext($answer->feedback, 800 $answer->feedbackformat, $question->questiontextformat) . "\n"; 801 } 802 } 803 $expout .= $this->write_general_feedback($question); 804 $expout .= "}\n"; 805 break; 806 807 case 'match': 808 $expout .= $this->write_name($question->name); 809 $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); 810 $expout .= "{\n"; 811 foreach ($question->options->subquestions as $subquestion) { 812 $expout .= "\t=" . $this->write_questiontext($subquestion->questiontext, 813 $subquestion->questiontextformat, $question->questiontextformat) . 814 ' -> ' . $this->repchar($subquestion->answertext) . "\n"; 815 } 816 $expout .= $this->write_general_feedback($question); 817 $expout .= "}\n"; 818 break; 819 820 default: 821 // Check for plugins. 822 if ($out = $this->try_exporting_using_qtypes($question->qtype, $question)) { 823 $expout .= $out; 824 } 825 } 826 827 // Add empty line to delimit questions. 828 $expout .= "\n"; 829 return $expout; 830 } 831 832 /** 833 * Prepare any question idnumber or tags for export. 834 * 835 * @param stdClass $questiondata the question data we are exporting. 836 * @return string a string that can be written as a line in the GIFT file, 837 * e.g. "// [id:myid] [tag:some-tag]\n". Will be '' if none. 838 */ 839 public function write_idnumber_and_tags(stdClass $questiondata): string { 840 if ($questiondata->qtype == 'category') { 841 return ''; 842 } 843 844 $bits = []; 845 846 if (isset($questiondata->idnumber) && $questiondata->idnumber !== '') { 847 $bits[] = '[id:' . str_replace(']', '\]', $questiondata->idnumber) . ']'; 848 } 849 850 // Write the question tags. 851 if (core_tag_tag::is_enabled('core_question', 'question')) { 852 $tagobjects = core_tag_tag::get_item_tags('core_question', 'question', $questiondata->id); 853 854 if (!empty($tagobjects)) { 855 $context = context::instance_by_id($questiondata->contextid); 856 $sortedtagobjects = question_sort_tags($tagobjects, $context, [$this->course]); 857 858 // Currently we ignore course tags. This should probably be fixed in future. 859 860 if (!empty($sortedtagobjects->tags)) { 861 foreach ($sortedtagobjects->tags as $tag) { 862 $bits[] = '[tag:' . str_replace(']', '\]', $tag) . ']'; 863 } 864 } 865 } 866 } 867 868 if (!$bits) { 869 return ''; 870 } 871 872 return '// ' . implode(' ', $bits) . "\n"; 873 } 874 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body