/question/format/gift/format.php
PHP | 874 lines | 613 code | 128 blank | 133 comment | 158 complexity | 5d0fcad40da5f81cc3e4f674559afa3c MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
- <?php
- // This file is part of Moodle - http://moodle.org/
- //
- // Moodle is free software: you can redistribute it and/or modify
- // it under the terms of the GNU General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // Moodle is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU General Public License for more details.
- //
- // You should have received a copy of the GNU General Public License
- // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
- /**
- * GIFT format question importer/exporter.
- *
- * @package qformat_gift
- * @copyright 2003 Paul Tsuchido Shew
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
- defined('MOODLE_INTERNAL') || die();
- /**
- * The GIFT import filter was designed as an easy to use method
- * for teachers writing questions as a text file. It supports most
- * question types and the missing word format.
- *
- * Multiple Choice / Missing Word
- * Who's buried in Grant's tomb?{~Grant ~Jefferson =no one}
- * Grant is {~buried =entombed ~living} in Grant's tomb.
- * True-False:
- * Grant is buried in Grant's tomb.{FALSE}
- * Short-Answer.
- * Who's buried in Grant's tomb?{=no one =nobody}
- * Numerical
- * When was Ulysses S. Grant born?{#1822:5}
- * Matching
- * Match the following countries with their corresponding
- * capitals.{=Canada->Ottawa =Italy->Rome =Japan->Tokyo}
- *
- * Comment lines start with a double backslash (//).
- * Optional question names are enclosed in double colon(::).
- * Answer feedback is indicated with hash mark (#).
- * Percentage answer weights immediately follow the tilde (for
- * multiple choice) or equal sign (for short answer and numerical),
- * and are enclosed in percent signs (% %). See docs and examples.txt for more.
- *
- * This filter was written through the collaboration of numerous
- * members of the Moodle community. It was originally based on
- * the missingword format, which included code from Thomas Robb
- * and others. Paul Tsuchido Shew wrote this filter in December 2003.
- *
- * @copyright 2003 Paul Tsuchido Shew
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
- class qformat_gift extends qformat_default {
- public function provide_import() {
- return true;
- }
- public function provide_export() {
- return true;
- }
- public function export_file_extension() {
- return '.txt';
- }
- protected function answerweightparser(&$answer) {
- $answer = substr($answer, 1); // Removes initial %.
- $endposition = strpos($answer, "%");
- $answerweight = substr($answer, 0, $endposition); // Gets weight as integer.
- $answerweight = $answerweight/100; // Converts to percent.
- $answer = substr($answer, $endposition+1); // Removes comment from answer.
- return $answerweight;
- }
- protected function commentparser($answer, $defaultformat) {
- $bits = explode('#', $answer, 2);
- $ans = $this->parse_text_with_format(trim($bits[0]), $defaultformat);
- if (count($bits) > 1) {
- $feedback = $this->parse_text_with_format(trim($bits[1]), $defaultformat);
- } else {
- $feedback = array('text' => '', 'format' => $defaultformat, 'files' => array());
- }
- return array($ans, $feedback);
- }
- protected function split_truefalse_comment($answer, $defaultformat) {
- $bits = explode('#', $answer, 3);
- $ans = $this->parse_text_with_format(trim($bits[0]), $defaultformat);
- if (count($bits) > 1) {
- $wrongfeedback = $this->parse_text_with_format(trim($bits[1]), $defaultformat);
- } else {
- $wrongfeedback = array('text' => '', 'format' => $defaultformat, 'files' => array());
- }
- if (count($bits) > 2) {
- $rightfeedback = $this->parse_text_with_format(trim($bits[2]), $defaultformat);
- } else {
- $rightfeedback = array('text' => '', 'format' => $defaultformat, 'files' => array());
- }
- return array($ans, $wrongfeedback, $rightfeedback);
- }
- protected function escapedchar_pre($string) {
- // Replaces escaped control characters with a placeholder BEFORE processing.
- $escapedcharacters = array("\\:", "\\#", "\\=", "\\{", "\\}", "\\~", "\\n" );
- $placeholders = array("&&058;", "&&035;", "&&061;", "&&123;", "&&125;", "&&126;", "&&010");
- $string = str_replace("\\\\", "&&092;", $string);
- $string = str_replace($escapedcharacters, $placeholders, $string);
- $string = str_replace("&&092;", "\\", $string);
- return $string;
- }
- protected function escapedchar_post($string) {
- // Replaces placeholders with corresponding character AFTER processing is done.
- $placeholders = array("&&058;", "&&035;", "&&061;", "&&123;", "&&125;", "&&126;", "&&010");
- $characters = array(":", "#", "=", "{", "}", "~", "\n" );
- $string = str_replace($placeholders, $characters, $string);
- return $string;
- }
- protected function check_answer_count($min, $answers, $text) {
- $countanswers = count($answers);
- if ($countanswers < $min) {
- $this->error(get_string('importminerror', 'qformat_gift'), $text);
- return false;
- }
- return true;
- }
- protected function parse_text_with_format($text, $defaultformat = FORMAT_MOODLE) {
- $result = array(
- 'text' => $text,
- 'format' => $defaultformat,
- 'files' => array(),
- );
- if (strpos($text, '[') === 0) {
- $formatend = strpos($text, ']');
- $result['format'] = $this->format_name_to_const(substr($text, 1, $formatend - 1));
- if ($result['format'] == -1) {
- $result['format'] = $defaultformat;
- } else {
- $result['text'] = substr($text, $formatend + 1);
- }
- }
- $result['text'] = trim($this->escapedchar_post($result['text']));
- return $result;
- }
- public function readquestion($lines) {
- // Given an array of lines known to define a question in this format, this function
- // converts it into a question object suitable for processing and insertion into Moodle.
- $question = $this->defaultquestion();
- // Define replaced by simple assignment, stop redefine notices.
- $giftanswerweightregex = '/^%\-*([0-9]{1,2})\.?([0-9]*)%/';
- // Separate comments and implode.
- $comments = '';
- foreach ($lines as $key => $line) {
- $line = trim($line);
- if (substr($line, 0, 2) == '//') {
- $comments .= $line . "\n";
- $lines[$key] = ' ';
- }
- }
- $text = trim(implode("\n", $lines));
- if ($text == '') {
- return false;
- }
- // Substitute escaped control characters with placeholders.
- $text = $this->escapedchar_pre($text);
- // Look for category modifier.
- if (preg_match('~^\$CATEGORY:~', $text)) {
- $newcategory = trim(substr($text, 10));
- // Build fake question to contain category.
- $question->qtype = 'category';
- $question->category = $newcategory;
- return $question;
- }
- // Question name parser.
- if (substr($text, 0, 2) == '::') {
- $text = substr($text, 2);
- $namefinish = strpos($text, '::');
- if ($namefinish === false) {
- $question->name = false;
- // Name will be assigned after processing question text below.
- } else {
- $questionname = substr($text, 0, $namefinish);
- $question->name = $this->clean_question_name($this->escapedchar_post($questionname));
- $text = trim(substr($text, $namefinish+2)); // Remove name from text.
- }
- } else {
- $question->name = false;
- }
- // Find the answer section.
- $answerstart = strpos($text, '{');
- $answerfinish = strpos($text, '}');
- $description = false;
- if ($answerstart === false && $answerfinish === false) {
- // No answer means it's a description.
- $description = true;
- $answertext = '';
- $answerlength = 0;
- } else if ($answerstart === false || $answerfinish === false) {
- $this->error(get_string('braceerror', 'qformat_gift'), $text);
- return false;
- } else {
- $answerlength = $answerfinish - $answerstart;
- $answertext = trim(substr($text, $answerstart + 1, $answerlength - 1));
- }
- // Format the question text, without answer, inserting "_____" as necessary.
- if ($description) {
- $questiontext = $text;
- } else if (substr($text, -1) == "}") {
- // No blank line if answers follow question, outside of closing punctuation.
- $questiontext = substr_replace($text, "", $answerstart, $answerlength + 1);
- } else {
- // Inserts blank line for missing word format.
- $questiontext = substr_replace($text, "_____", $answerstart, $answerlength + 1);
- }
- // Look to see if there is any general feedback.
- $gfseparator = strrpos($answertext, '####');
- if ($gfseparator === false) {
- $generalfeedback = '';
- } else {
- $generalfeedback = substr($answertext, $gfseparator + 4);
- $answertext = trim(substr($answertext, 0, $gfseparator));
- }
- // Get questiontext format from questiontext.
- $text = $this->parse_text_with_format($questiontext);
- $question->questiontextformat = $text['format'];
- $question->questiontext = $text['text'];
- // Get generalfeedback format from questiontext.
- $text = $this->parse_text_with_format($generalfeedback, $question->questiontextformat);
- $question->generalfeedback = $text['text'];
- $question->generalfeedbackformat = $text['format'];
- // Set question name if not already set.
- if ($question->name === false) {
- $question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
- }
- // Determine question type.
- $question->qtype = null;
- // Give plugins first try.
- // Plugins must promise not to intercept standard qtypes
- // MDL-12346, this could be called from lesson mod which has its own base class =(.
- if (method_exists($this, 'try_importing_using_qtypes')
- && ($tryquestion = $this->try_importing_using_qtypes($lines, $question, $answertext))) {
- return $tryquestion;
- }
- if ($description) {
- $question->qtype = 'description';
- } else if ($answertext == '') {
- $question->qtype = 'essay';
- } else if ($answertext[0] == '#') {
- $question->qtype = 'numerical';
- } else if (strpos($answertext, '~') !== false) {
- // Only Multiplechoice questions contain tilde ~.
- $question->qtype = 'multichoice';
- } else if (strpos($answertext, '=') !== false
- && strpos($answertext, '->') !== false) {
- // Only Matching contains both = and ->.
- $question->qtype = 'match';
- } else { // Either truefalse or shortanswer.
- // Truefalse question check.
- $truefalsecheck = $answertext;
- if (strpos($answertext, '#') > 0) {
- // Strip comments to check for TrueFalse question.
- $truefalsecheck = trim(substr($answertext, 0, strpos($answertext, "#")));
- }
- $validtfanswers = array('T', 'TRUE', 'F', 'FALSE');
- if (in_array($truefalsecheck, $validtfanswers)) {
- $question->qtype = 'truefalse';
- } else { // Must be shortanswer.
- $question->qtype = 'shortanswer';
- }
- }
- // Extract any idnumber and tags from the comments.
- list($question->idnumber, $question->tags) =
- $this->extract_idnumber_and_tags_from_comment($comments);
- if (!isset($question->qtype)) {
- $giftqtypenotset = get_string('giftqtypenotset', 'qformat_gift');
- $this->error($giftqtypenotset, $text);
- return false;
- }
- switch ($question->qtype) {
- case 'description':
- $question->defaultmark = 0;
- $question->length = 0;
- return $question;
- case 'essay':
- $question->responseformat = 'editor';
- $question->responserequired = 1;
- $question->responsefieldlines = 15;
- $question->attachments = 0;
- $question->attachmentsrequired = 0;
- $question->graderinfo = array(
- 'text' => '', 'format' => FORMAT_HTML, 'files' => array());
- $question->responsetemplate = array(
- 'text' => '', 'format' => FORMAT_HTML);
- return $question;
- case 'multichoice':
- // "Temporary" solution to enable choice of answernumbering on GIFT import
- // by respecting default set for multichoice questions (MDL-59447)
- $question->answernumbering = get_config('qtype_multichoice', 'answernumbering');
- if (strpos($answertext, "=") === false) {
- $question->single = 0; // Multiple answers are enabled if no single answer is 100% correct.
- } else {
- $question->single = 1; // Only one answer allowed (the default).
- }
- $question = $this->add_blank_combined_feedback($question);
- $answertext = str_replace("=", "~=", $answertext);
- $answers = explode("~", $answertext);
- if (isset($answers[0])) {
- $answers[0] = trim($answers[0]);
- }
- if (empty($answers[0])) {
- array_shift($answers);
- }
- $countanswers = count($answers);
- if (!$this->check_answer_count(2, $answers, $text)) {
- return false;
- }
- foreach ($answers as $key => $answer) {
- $answer = trim($answer);
- // Determine answer weight.
- if ($answer[0] == '=') {
- $answerweight = 1;
- $answer = substr($answer, 1);
- } else if (preg_match($giftanswerweightregex, $answer)) { // Check for properly formatted answer weight.
- $answerweight = $this->answerweightparser($answer);
- } else { // Default, i.e., wrong anwer.
- $answerweight = 0;
- }
- list($question->answer[$key], $question->feedback[$key]) =
- $this->commentparser($answer, $question->questiontextformat);
- $question->fraction[$key] = $answerweight;
- } // End foreach answer.
- return $question;
- case 'match':
- $question = $this->add_blank_combined_feedback($question);
- $answers = explode('=', $answertext);
- if (isset($answers[0])) {
- $answers[0] = trim($answers[0]);
- }
- if (empty($answers[0])) {
- array_shift($answers);
- }
- if (!$this->check_answer_count(2, $answers, $text)) {
- return false;
- }
- foreach ($answers as $key => $answer) {
- $answer = trim($answer);
- if (strpos($answer, "->") === false) {
- $this->error(get_string('giftmatchingformat', 'qformat_gift'), $answer);
- return false;
- }
- $marker = strpos($answer, '->');
- $question->subquestions[$key] = $this->parse_text_with_format(
- substr($answer, 0, $marker), $question->questiontextformat);
- $question->subanswers[$key] = trim($this->escapedchar_post(
- substr($answer, $marker + 2)));
- }
- return $question;
- case 'truefalse':
- list($answer, $wrongfeedback, $rightfeedback) =
- $this->split_truefalse_comment($answertext, $question->questiontextformat);
- if ($answer['text'] == "T" || $answer['text'] == "TRUE") {
- $question->correctanswer = 1;
- $question->feedbacktrue = $rightfeedback;
- $question->feedbackfalse = $wrongfeedback;
- } else {
- $question->correctanswer = 0;
- $question->feedbacktrue = $wrongfeedback;
- $question->feedbackfalse = $rightfeedback;
- }
- $question->penalty = 1;
- return $question;
- case 'shortanswer':
- // Shortanswer question.
- $answers = explode("=", $answertext);
- if (isset($answers[0])) {
- $answers[0] = trim($answers[0]);
- }
- if (empty($answers[0])) {
- array_shift($answers);
- }
- if (!$this->check_answer_count(1, $answers, $text)) {
- return false;
- }
- foreach ($answers as $key => $answer) {
- $answer = trim($answer);
- // Answer weight.
- if (preg_match($giftanswerweightregex, $answer)) { // Check for properly formatted answer weight.
- $answerweight = $this->answerweightparser($answer);
- } else { // Default, i.e., full-credit anwer.
- $answerweight = 1;
- }
- list($answer, $question->feedback[$key]) = $this->commentparser(
- $answer, $question->questiontextformat);
- $question->answer[$key] = $answer['text'];
- $question->fraction[$key] = $answerweight;
- }
- return $question;
- case 'numerical':
- // Note similarities to ShortAnswer.
- $answertext = substr($answertext, 1); // Remove leading "#".
- // If there is feedback for a wrong answer, store it for now.
- if (($pos = strpos($answertext, '~')) !== false) {
- $wrongfeedback = substr($answertext, $pos);
- $answertext = substr($answertext, 0, $pos);
- } else {
- $wrongfeedback = '';
- }
- $answers = explode("=", $answertext);
- if (isset($answers[0])) {
- $answers[0] = trim($answers[0]);
- }
- if (empty($answers[0])) {
- array_shift($answers);
- }
- if (count($answers) == 0) {
- // Invalid question.
- $giftnonumericalanswers = get_string('giftnonumericalanswers', 'qformat_gift');
- $this->error($giftnonumericalanswers, $text);
- return false;
- }
- foreach ($answers as $key => $answer) {
- $answer = trim($answer);
- // Answer weight.
- if (preg_match($giftanswerweightregex, $answer)) { // Check for properly formatted answer weight.
- $answerweight = $this->answerweightparser($answer);
- } else { // Default, i.e., full-credit anwer.
- $answerweight = 1;
- }
- list($answer, $question->feedback[$key]) = $this->commentparser(
- $answer, $question->questiontextformat);
- $question->fraction[$key] = $answerweight;
- $answer = $answer['text'];
- // Calculate Answer and Min/Max values.
- if (strpos($answer, "..") > 0) { // Optional [min]..[max] format.
- $marker = strpos($answer, "..");
- $max = trim(substr($answer, $marker + 2));
- $min = trim(substr($answer, 0, $marker));
- $ans = ($max + $min)/2;
- $tol = $max - $ans;
- } else if (strpos($answer, ':') > 0) { // Standard [answer]:[errormargin] format.
- $marker = strpos($answer, ':');
- $tol = trim(substr($answer, $marker+1));
- $ans = trim(substr($answer, 0, $marker));
- } else { // Only one valid answer (zero errormargin).
- $tol = 0;
- $ans = trim($answer);
- }
- if (!(is_numeric($ans) || $ans = '*') || !is_numeric($tol)) {
- $errornotnumbers = get_string('errornotnumbers');
- $this->error($errornotnumbers, $text);
- return false;
- }
- // Store results.
- $question->answer[$key] = $ans;
- $question->tolerance[$key] = $tol;
- }
- if ($wrongfeedback) {
- $key += 1;
- $question->fraction[$key] = 0;
- list($notused, $question->feedback[$key]) = $this->commentparser(
- $wrongfeedback, $question->questiontextformat);
- $question->answer[$key] = '*';
- $question->tolerance[$key] = '';
- }
- return $question;
- default:
- $this->error(get_string('giftnovalidquestion', 'qformat_gift'), $text);
- return false;
- }
- }
- protected function repchar($text, $notused = 0) {
- // Escapes 'reserved' characters # = ~ {) :
- // Removes new lines.
- $reserved = array( '\\', '#', '=', '~', '{', '}', ':', "\n", "\r");
- $escaped = array('\\\\', '\#', '\=', '\~', '\{', '\}', '\:', '\n', '');
- $newtext = str_replace($reserved, $escaped, $text);
- return $newtext;
- }
- /**
- * @param int $format one of the FORMAT_ constants.
- * @return string the corresponding name.
- */
- protected function format_const_to_name($format) {
- if ($format == FORMAT_MOODLE) {
- return 'moodle';
- } else if ($format == FORMAT_HTML) {
- return 'html';
- } else if ($format == FORMAT_PLAIN) {
- return 'plain';
- } else if ($format == FORMAT_MARKDOWN) {
- return 'markdown';
- } else {
- return 'moodle';
- }
- }
- /**
- * @param int $format one of the FORMAT_ constants.
- * @return string the corresponding name.
- */
- protected function format_name_to_const($format) {
- if ($format == 'moodle') {
- return FORMAT_MOODLE;
- } else if ($format == 'html') {
- return FORMAT_HTML;
- } else if ($format == 'plain') {
- return FORMAT_PLAIN;
- } else if ($format == 'markdown') {
- return FORMAT_MARKDOWN;
- } else {
- return -1;
- }
- }
- /**
- * Extract any tags or idnumber declared in the question comment.
- *
- * @param string $comment E.g. "// Line 1.\n//Line 2.\n".
- * @return array with two elements. string $idnumber (or '') and string[] of tags.
- */
- public function extract_idnumber_and_tags_from_comment(string $comment): array {
- // Find the idnumber, if any. There should not be more than one, but if so, we just find the first.
- $idnumber = '';
- if (preg_match('~
- # Start of id token.
- \[id:
- # Any number of (non-control) characters, with any ] escaped.
- # This is the bit we want so capture it.
- (
- (?:\\\\]|[^][:cntrl:]])+
- )
- # End of id token.
- ]
- ~x', $comment, $match)) {
- $idnumber = str_replace('\]', ']', trim($match[1]));
- }
- // Find any tags.
- $tags = [];
- if (preg_match_all('~
- # Start of tag token.
- \[tag:
- # Any number of allowed characters (see PARAM_TAG), with any ] escaped.
- # This is the bit we want so capture it.
- (
- (?:\\\\]|[^]<>`[:cntrl:]]|)+
- )
- # End of tag token.
- ]
- ~x', $comment, $matches)) {
- foreach ($matches[1] as $rawtag) {
- $tags[] = str_replace('\]', ']', trim($rawtag));
- }
- }
- return [$idnumber, $tags];
- }
- public function write_name($name) {
- return '::' . $this->repchar($name) . '::';
- }
- public function write_questiontext($text, $format, $defaultformat = FORMAT_MOODLE) {
- $output = '';
- if ($text != '' && $format != $defaultformat) {
- $output .= '[' . $this->format_const_to_name($format) . ']';
- }
- $output .= $this->repchar($text, $format);
- return $output;
- }
- /**
- * Outputs the general feedback for the question, if any. This needs to be the
- * last thing before the }.
- * @param object $question the question data.
- * @param string $indent to put before the general feedback. Defaults to a tab.
- * If this is not blank, a newline is added after the line.
- */
- public function write_general_feedback($question, $indent = "\t") {
- $generalfeedback = $this->write_questiontext($question->generalfeedback,
- $question->generalfeedbackformat, $question->questiontextformat);
- if ($generalfeedback) {
- $generalfeedback = '####' . $generalfeedback;
- if ($indent) {
- $generalfeedback = $indent . $generalfeedback . "\n";
- }
- }
- return $generalfeedback;
- }
- public function writequestion($question) {
- // Start with a comment.
- $expout = "// question: {$question->id} name: {$question->name}\n";
- $expout .= $this->write_idnumber_and_tags($question);
- // Output depends on question type.
- switch($question->qtype) {
- case 'category':
- // Not a real question, used to insert category switch.
- $expout .= "\$CATEGORY: $question->category\n";
- break;
- case 'description':
- $expout .= $this->write_name($question->name);
- $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
- break;
- case 'essay':
- $expout .= $this->write_name($question->name);
- $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
- $expout .= "{";
- $expout .= $this->write_general_feedback($question, '');
- $expout .= "}\n";
- break;
- case 'truefalse':
- $trueanswer = $question->options->answers[$question->options->trueanswer];
- $falseanswer = $question->options->answers[$question->options->falseanswer];
- if ($trueanswer->fraction == 1) {
- $answertext = 'TRUE';
- $rightfeedback = $this->write_questiontext($trueanswer->feedback,
- $trueanswer->feedbackformat, $question->questiontextformat);
- $wrongfeedback = $this->write_questiontext($falseanswer->feedback,
- $falseanswer->feedbackformat, $question->questiontextformat);
- } else {
- $answertext = 'FALSE';
- $rightfeedback = $this->write_questiontext($falseanswer->feedback,
- $falseanswer->feedbackformat, $question->questiontextformat);
- $wrongfeedback = $this->write_questiontext($trueanswer->feedback,
- $trueanswer->feedbackformat, $question->questiontextformat);
- }
- $expout .= $this->write_name($question->name);
- $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
- $expout .= '{' . $this->repchar($answertext);
- if ($wrongfeedback) {
- $expout .= '#' . $wrongfeedback;
- } else if ($rightfeedback) {
- $expout .= '#';
- }
- if ($rightfeedback) {
- $expout .= '#' . $rightfeedback;
- }
- $expout .= $this->write_general_feedback($question, '');
- $expout .= "}\n";
- break;
- case 'multichoice':
- $expout .= $this->write_name($question->name);
- $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
- $expout .= "{\n";
- foreach ($question->options->answers as $answer) {
- if ($answer->fraction == 1 && $question->options->single) {
- $answertext = '=';
- } else if ($answer->fraction == 0) {
- $answertext = '~';
- } else {
- $weight = $answer->fraction * 100;
- $answertext = '~%' . $weight . '%';
- }
- $expout .= "\t" . $answertext . $this->write_questiontext($answer->answer,
- $answer->answerformat, $question->questiontextformat);
- if ($answer->feedback != '') {
- $expout .= '#' . $this->write_questiontext($answer->feedback,
- $answer->feedbackformat, $question->questiontextformat);
- }
- $expout .= "\n";
- }
- $expout .= $this->write_general_feedback($question);
- $expout .= "}\n";
- break;
- case 'shortanswer':
- $expout .= $this->write_name($question->name);
- $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
- $expout .= "{\n";
- foreach ($question->options->answers as $answer) {
- $weight = 100 * $answer->fraction;
- $expout .= "\t=%" . $weight . '%' . $this->repchar($answer->answer) .
- '#' . $this->write_questiontext($answer->feedback,
- $answer->feedbackformat, $question->questiontextformat) . "\n";
- }
- $expout .= $this->write_general_feedback($question);
- $expout .= "}\n";
- break;
- case 'numerical':
- $expout .= $this->write_name($question->name);
- $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
- $expout .= "{#\n";
- foreach ($question->options->answers as $answer) {
- if ($answer->answer != '' && $answer->answer != '*') {
- $weight = 100 * $answer->fraction;
- $expout .= "\t=%" . $weight . '%' . $answer->answer . ':' .
- (float)$answer->tolerance . '#' . $this->write_questiontext($answer->feedback,
- $answer->feedbackformat, $question->questiontextformat) . "\n";
- } else {
- $expout .= "\t~#" . $this->write_questiontext($answer->feedback,
- $answer->feedbackformat, $question->questiontextformat) . "\n";
- }
- }
- $expout .= $this->write_general_feedback($question);
- $expout .= "}\n";
- break;
- case 'match':
- $expout .= $this->write_name($question->name);
- $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
- $expout .= "{\n";
- foreach ($question->options->subquestions as $subquestion) {
- $expout .= "\t=" . $this->write_questiontext($subquestion->questiontext,
- $subquestion->questiontextformat, $question->questiontextformat) .
- ' -> ' . $this->repchar($subquestion->answertext) . "\n";
- }
- $expout .= $this->write_general_feedback($question);
- $expout .= "}\n";
- break;
- default:
- // Check for plugins.
- if ($out = $this->try_exporting_using_qtypes($question->qtype, $question)) {
- $expout .= $out;
- }
- }
- // Add empty line to delimit questions.
- $expout .= "\n";
- return $expout;
- }
- /**
- * Prepare any question idnumber or tags for export.
- *
- * @param stdClass $questiondata the question data we are exporting.
- * @return string a string that can be written as a line in the GIFT file,
- * e.g. "// [id:myid] [tag:some-tag]\n". Will be '' if none.
- */
- public function write_idnumber_and_tags(stdClass $questiondata): string {
- if ($questiondata->qtype == 'category') {
- return '';
- }
- $bits = [];
- if (isset($questiondata->idnumber) && $questiondata->idnumber !== '') {
- $bits[] = '[id:' . str_replace(']', '\]', $questiondata->idnumber) . ']';
- }
- // Write the question tags.
- if (core_tag_tag::is_enabled('core_question', 'question')) {
- $tagobjects = core_tag_tag::get_item_tags('core_question', 'question', $questiondata->id);
- if (!empty($tagobjects)) {
- $context = context::instance_by_id($questiondata->contextid);
- $sortedtagobjects = question_sort_tags($tagobjects, $context, [$this->course]);
- // Currently we ignore course tags. This should probably be fixed in future.
- if (!empty($sortedtagobjects->tags)) {
- foreach ($sortedtagobjects->tags as $tag) {
- $bits[] = '[tag:' . str_replace(']', '\]', $tag) . ']';
- }
- }
- }
- }
- if (!$bits) {
- return '';
- }
- return '// ' . implode(' ', $bits) . "\n";
- }
- }