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