MDL-20636 Merge branch 'master' into qe2_wip
[moodle.git] / question / format / gift / format.php
1 <?php
2 //
3 ///////////////////////////////////////////////////////////////
4 // The GIFT import filter was designed as an easy to use method
5 // for teachers writing questions as a text file. It supports most
6 // question types and the missing word format.
7 //
8 // Multiple Choice / Missing Word
9 //     Who's buried in Grant's tomb?{~Grant ~Jefferson =no one}
10 //     Grant is {~buried =entombed ~living} in Grant's tomb.
11 // True-False:
12 //     Grant is buried in Grant's tomb.{FALSE}
13 // Short-Answer.
14 //     Who's buried in Grant's tomb?{=no one =nobody}
15 // Numerical
16 //     When was Ulysses S. Grant born?{#1822:5}
17 // Matching
18 //     Match the following countries with their corresponding
19 //     capitals.{=Canada->Ottawa =Italy->Rome =Japan->Tokyo}
20 //
21 // Comment lines start with a double backslash (//).
22 // Optional question names are enclosed in double colon(::).
23 // Answer feedback is indicated with hash mark (#).
24 // Percentage answer weights immediately follow the tilde (for
25 // multiple choice) or equal sign (for short answer and numerical),
26 // and are enclosed in percent signs (% %). See docs and examples.txt for more.
27 //
28 // This filter was written through the collaboration of numerous
29 // members of the Moodle community. It was originally based on
30 // the missingword format, which included code from Thomas Robb
31 // and others. Paul Tsuchido Shew wrote this filter in December 2003.
32 //////////////////////////////////////////////////////////////////////////
33 // Based on default.php, included by ../import.php
34 /**
35  * @package questionbank
36  * @subpackage importexport
37  */
38 class qformat_gift extends qformat_default {
40     function provide_import() {
41         return true;
42     }
44     function provide_export() {
45         return true;
46     }
48     function export_file_extension() {
49         return '.txt';
50     }
52     function answerweightparser(&$answer) {
53         $answer = substr($answer, 1);                        // removes initial %
54         $end_position  = strpos($answer, "%");
55         $answer_weight = substr($answer, 0, $end_position);  // gets weight as integer
56         $answer_weight = $answer_weight/100;                 // converts to percent
57         $answer = substr($answer, $end_position+1);          // removes comment from answer
58         return $answer_weight;
59     }
61     function commentparser($answer, $defaultformat) {
62         $bits = explode('#', $answer, 2);
63         $ans = $this->parse_text_with_format(trim($bits[0]), $defaultformat);
64         if (count($bits) > 1) {
65             $feedback = $this->parse_text_with_format(trim($bits[1]), $defaultformat);
66         } else {
67             $feedback = array('text' => '', 'format' => $defaultformat, 'files' => array());
68         }
69         return array($ans, $feedback);
70     }
72     function split_truefalse_comment($answer, $defaultformat) {
73         $bits = explode('#', $answer, 3);
74         $ans = $this->parse_text_with_format(trim($bits[0]), $defaultformat);
75         if (count($bits) > 1) {
76             $wrongfeedback = $this->parse_text_with_format(trim($bits[1]), $defaultformat);
77         } else {
78             $wrongfeedback = array('text' => '', 'format' => $defaultformat, 'files' => array());
79         }
80         if (count($bits) > 2) {
81             $rightfeedback = $this->parse_text_with_format(trim($bits[2]), $defaultformat);
82         } else {
83             $rightfeedback = array('text' => '', 'format' => $defaultformat, 'files' => array());
84         }
85         return array($ans, $wrongfeedback, $rightfeedback);
86     }
88     function escapedchar_pre($string) {
89         //Replaces escaped control characters with a placeholder BEFORE processing
91         $escapedcharacters = array("\\:",    "\\#",    "\\=",    "\\{",    "\\}",    "\\~",    "\\n"  );  //dlnsk
92         $placeholders      = array("&&058;", "&&035;", "&&061;", "&&123;", "&&125;", "&&126;", "&&010");  //dlnsk
94         $string = str_replace("\\\\", "&&092;", $string);
95         $string = str_replace($escapedcharacters, $placeholders, $string);
96         $string = str_replace("&&092;", "\\", $string);
97         return $string;
98     }
100     function escapedchar_post($string) {
101         //Replaces placeholders with corresponding character AFTER processing is done
102         $placeholders = array("&&058;", "&&035;", "&&061;", "&&123;", "&&125;", "&&126;", "&&010"); //dlnsk
103         $characters   = array(":",     "#",      "=",      "{",      "}",      "~",      "\n"  ); //dlnsk
104         $string = str_replace($placeholders, $characters, $string);
105         return $string;
106     }
108     function check_answer_count($min, $answers, $text) {
109         $countanswers = count($answers);
110         if ($countanswers < $min) {
111             $importminerror = get_string('importminerror', 'quiz');
112             $this->error($importminerror, $text);
113             return false;
114         }
116         return true;
117     }
119     protected function parse_text_with_format($text, $defaultformat = FORMAT_MOODLE) {
120         $result = array(
121             'text' => $text,
122             'format' => $defaultformat,
123             'files' => array(),
124         );
125         if (strpos($text, '[') === 0) {
126             $formatend = strpos($text, ']');
127             $result['format'] = $this->format_name_to_const(substr($text, 1, $formatend - 1));
128             if ($result['format'] == -1) {
129                 $result['format'] = $defaultformat;
130             } else {
131                 $result['text'] = substr($text, $formatend + 1);
132             }
133         }
134         $result['text'] = trim($this->escapedchar_post($result['text']));
135         return $result;
136     }
138     function readquestion($lines) {
139     // Given an array of lines known to define a question in this format, this function
140     // converts it into a question object suitable for processing and insertion into Moodle.
142         $question = $this->defaultquestion();
143         $comment = NULL;
144         // define replaced by simple assignment, stop redefine notices
145         $gift_answerweight_regex = '/^%\-*([0-9]{1,2})\.?([0-9]*)%/';
147         // REMOVED COMMENTED LINES and IMPLODE
148         foreach ($lines as $key => $line) {
149             $line = trim($line);
150             if (substr($line, 0, 2) == '//') {
151                 $lines[$key] = ' ';
152             }
153         }
155         $text = trim(implode(' ', $lines));
157         if ($text == '') {
158             return false;
159         }
161         // Substitute escaped control characters with placeholders
162         $text = $this->escapedchar_pre($text);
164         // Look for category modifier
165         if (preg_match('~^\$CATEGORY:~', $text)) {
166             // $newcategory = $matches[1];
167             $newcategory = trim(substr($text, 10));
169             // build fake question to contain category
170             $question->qtype = 'category';
171             $question->category = $newcategory;
172             return $question;
173         }
175         // QUESTION NAME parser
176         if (substr($text, 0, 2) == '::') {
177             $text = substr($text, 2);
179             $namefinish = strpos($text, '::');
180             if ($namefinish === false) {
181                 $question->name = false;
182                 // name will be assigned after processing question text below
183             } else {
184                 $questionname = substr($text, 0, $namefinish);
185                 $question->name = trim($this->escapedchar_post($questionname));
186                 $text = trim(substr($text, $namefinish+2)); // Remove name from text
187             }
188         } else {
189             $question->name = false;
190         }
193         // FIND ANSWER section
194         // no answer means its a description
195         $answerstart = strpos($text, '{');
196         $answerfinish = strpos($text, '}');
198         $description = false;
199         if (($answerstart === false) and ($answerfinish === false)) {
200             $description = true;
201             $answertext = '';
202             $answerlength = 0;
203         } else if (!(($answerstart !== false) and ($answerfinish !== false))) {
204             $this->error(get_string('braceerror', 'quiz'), $text);
205             return false;
206         } else {
207             $answerlength = $answerfinish - $answerstart;
208             $answertext = trim(substr($text, $answerstart + 1, $answerlength - 1));
209         }
211         // Format QUESTION TEXT without answer, inserting "_____" as necessary
212         if ($description) {
213             $questiontext = $text;
214         } else if (substr($text, -1) == "}") {
215             // no blank line if answers follow question, outside of closing punctuation
216             $questiontext = substr_replace($text, "", $answerstart, $answerlength+1);
217         } else {
218             // inserts blank line for missing word format
219             $questiontext = substr_replace($text, "_____", $answerstart, $answerlength+1);
220         }
222         // Get questiontext format from questiontext
223         $text = $this->parse_text_with_format($questiontext);
224         $question->questiontextformat = $text['format'];
225         $question->generalfeedbackformat = $text['format'];
226         $question->questiontext = $text['text'];
228         // set question name if not already set
229         if ($question->name === false) {
230             $question->name = $question->questiontext;
231         }
233         // ensure name is not longer than 250 characters
234         $question->name = shorten_text($question->name, 200);
235         $question->name = strip_tags(substr($question->name, 0, 250));
237         // determine QUESTION TYPE
238         $question->qtype = NULL;
240         // give plugins first try
241         // plugins must promise not to intercept standard qtypes
242         // MDL-12346, this could be called from lesson mod which has its own base class =(
243         if (method_exists($this, 'try_importing_using_qtypes') && ($try_question = $this->try_importing_using_qtypes($lines, $question, $answertext))) {
244             return $try_question;
245         }
247         if ($description) {
248             $question->qtype = DESCRIPTION;
250         } else if ($answertext == '') {
251             $question->qtype = ESSAY;
253         } else if ($answertext{0} == '#') {
254             $question->qtype = NUMERICAL;
256         } else if (strpos($answertext, '~') !== false)  {
257             // only Multiplechoice questions contain tilde ~
258             $question->qtype = MULTICHOICE;
260         } else if (strpos($answertext, '=')  !== false
261                 && strpos($answertext, '->') !== false) {
262             // only Matching contains both = and ->
263             $question->qtype = MATCH;
265         } else { // either TRUEFALSE or SHORTANSWER
267             // TRUEFALSE question check
268             $truefalse_check = $answertext;
269             if (strpos($answertext, '#') > 0) {
270                 // strip comments to check for TrueFalse question
271                 $truefalse_check = trim(substr($answertext, 0, strpos($answertext,"#")));
272             }
274             $valid_tf_answers = array('T', 'TRUE', 'F', 'FALSE');
275             if (in_array($truefalse_check, $valid_tf_answers)) {
276                 $question->qtype = TRUEFALSE;
278             } else { // Must be SHORTANSWER
279                 $question->qtype = SHORTANSWER;
280             }
281         }
283         if (!isset($question->qtype)) {
284             $giftqtypenotset = get_string('giftqtypenotset', 'quiz');
285             $this->error($giftqtypenotset, $text);
286             return false;
287         }
289         switch ($question->qtype) {
290             case DESCRIPTION:
291                 $question->defaultmark = 0;
292                 $question->length = 0;
293                 return $question;
294                 break;
295             case ESSAY:
296                 $question->fraction = 0;
297                 $question->feedback['text'] = '';
298                 $question->feedback['format'] = $question->questiontextformat;
299                 $question->feedback['files'] = array();
300                 return $question;
301                 break;
302             case MULTICHOICE:
303                 if (strpos($answertext,"=") === false) {
304                     $question->single = 0; // multiple answers are enabled if no single answer is 100% correct
305                 } else {
306                     $question->single = 1; // only one answer allowed (the default)
307                 }
308                 $question->correctfeedback['text'] = '';
309                 $question->correctfeedback['format'] = $question->questiontextformat;
310                 $question->correctfeedback['files'] = array();
311                 $question->partiallycorrectfeedback['text'] = '';
312                 $question->partiallycorrectfeedback['format'] = $question->questiontextformat;
313                 $question->partiallycorrectfeedback['files'] = array();
314                 $question->incorrectfeedback['text'] = '';
315                 $question->incorrectfeedback['format'] = $question->questiontextformat;
316                 $question->incorrectfeedback['files'] = array();
318                 $answertext = str_replace("=", "~=", $answertext);
319                 $answers = explode("~", $answertext);
320                 if (isset($answers[0])) {
321                     $answers[0] = trim($answers[0]);
322                 }
323                 if (empty($answers[0])) {
324                     array_shift($answers);
325                 }
327                 $countanswers = count($answers);
329                 if (!$this->check_answer_count(2, $answers, $text)) {
330                     return false;
331                     break;
332                 }
334                 foreach ($answers as $key => $answer) {
335                     $answer = trim($answer);
337                     // determine answer weight
338                     if ($answer[0] == '=') {
339                         $answer_weight = 1;
340                         $answer = substr($answer, 1);
342                     } else if (preg_match($gift_answerweight_regex, $answer)) {    // check for properly formatted answer weight
343                         $answer_weight = $this->answerweightparser($answer);
345                     } else {     //default, i.e., wrong anwer
346                         $answer_weight = 0;
347                     }
348                     list($question->answer[$key], $question->feedback[$key]) =
349                             $this->commentparser($answer, $question->questiontextformat);
350                     $question->fraction[$key] = $answer_weight;
351                 }  // end foreach answer
353                 //$question->defaultmark = 1;
354                 //$question->image = "";   // No images with this format
355                 return $question;
356                 break;
358             case MATCH:
359                 $answers = explode('=', $answertext);
360                 if (isset($answers[0])) {
361                     $answers[0] = trim($answers[0]);
362                 }
363                 if (empty($answers[0])) {
364                     array_shift($answers);
365                 }
367                 if (!$this->check_answer_count(2,$answers,$text)) {
368                     return false;
369                     break;
370                 }
372                 foreach ($answers as $key => $answer) {
373                     $answer = trim($answer);
374                     if (strpos($answer, "->") === false) {
375                         $giftmatchingformat = get_string('giftmatchingformat','quiz');
376                         $this->error($giftmatchingformat, $answer);
377                         return false;
378                         break 2;
379                     }
381                     $marker = strpos($answer, '->');
382                     $question->subquestions[$key] = $this->parse_text_with_format(
383                             substr($answer, 0, $marker), $question->questiontextformat);
384                     $question->subanswers[$key] = trim($this->escapedchar_post(
385                             substr($answer, $marker + 2)));
386                 }
388                 return $question;
389                 break;
391             case TRUEFALSE:
392                 list($answer, $wrongfeedback, $rightfeedback) =
393                         $this->split_truefalse_comment($answertext, $question->questiontextformat);
395                 if ($answer['text'] == "T" OR $answer['text'] == "TRUE") {
396                     $question->correctanswer = 1;
397                     $question->feedbacktrue = $rightfeedback;
398                     $question->feedbackfalse = $wrongfeedback;
399                 } else {
400                     $question->correctanswer = 0;
401                     $question->feedbacktrue = $wrongfeedback;
402                     $question->feedbackfalse = $rightfeedback;
403                 }
405                 $question->penalty = 1;
407                 return $question;
408                 break;
410             case SHORTANSWER:
411                 // SHORTANSWER Question
412                 $answers = explode("=", $answertext);
413                 if (isset($answers[0])) {
414                     $answers[0] = trim($answers[0]);
415                 }
416                 if (empty($answers[0])) {
417                     array_shift($answers);
418                 }
420                 if (!$this->check_answer_count(1, $answers, $text)) {
421                     return false;
422                     break;
423                 }
425                 foreach ($answers as $key => $answer) {
426                     $answer = trim($answer);
428                     // Answer weight
429                     if (preg_match($gift_answerweight_regex, $answer)) {    // check for properly formatted answer weight
430                         $answer_weight = $this->answerweightparser($answer);
431                     } else {     //default, i.e., full-credit anwer
432                         $answer_weight = 1;
433                     }
435                     list($answer, $question->feedback[$key]) = $this->commentparser(
436                             $answer, $question->questiontextformat);
438                     $question->answer[$key] = $answer['text'];
439                     $question->fraction[$key] = $answer_weight;
440                 }
442                 return $question;
443                 break;
445             case NUMERICAL:
446                 // Note similarities to ShortAnswer
447                 $answertext = substr($answertext, 1); // remove leading "#"
449                 // If there is feedback for a wrong answer, store it for now.
450                 if (($pos = strpos($answertext, '~')) !== false) {
451                     $wrongfeedback = substr($answertext, $pos);
452                     $answertext = substr($answertext, 0, $pos);
453                 } else {
454                     $wrongfeedback = '';
455                 }
457                 $answers = explode("=", $answertext);
458                 if (isset($answers[0])) {
459                     $answers[0] = trim($answers[0]);
460                 }
461                 if (empty($answers[0])) {
462                     array_shift($answers);
463                 }
465                 if (count($answers) == 0) {
466                     // invalid question
467                     $giftnonumericalanswers = get_string('giftnonumericalanswers','quiz');
468                     $this->error($giftnonumericalanswers, $text);
469                     return false;
470                     break;
471                 }
473                 foreach ($answers as $key => $answer) {
474                     $answer = trim($answer);
476                     // Answer weight
477                     if (preg_match($gift_answerweight_regex, $answer)) {    // check for properly formatted answer weight
478                         $answer_weight = $this->answerweightparser($answer);
479                     } else {     //default, i.e., full-credit anwer
480                         $answer_weight = 1;
481                     }
483                     list($answer, $question->feedback[$key]) = $this->commentparser(
484                             $answer, $question->questiontextformat);
485                     $question->fraction[$key] = $answer_weight;
486                     $answer = $answer['text'];
488                     //Calculate Answer and Min/Max values
489                     if (strpos($answer,"..") > 0) { // optional [min]..[max] format
490                         $marker = strpos($answer,"..");
491                         $max = trim(substr($answer, $marker+2));
492                         $min = trim(substr($answer, 0, $marker));
493                         $ans = ($max + $min)/2;
494                         $tol = $max - $ans;
495                     } else if (strpos($answer, ':') > 0) { // standard [answer]:[errormargin] format
496                         $marker = strpos($answer, ':');
497                         $tol = trim(substr($answer, $marker+1));
498                         $ans = trim(substr($answer, 0, $marker));
499                     } else { // only one valid answer (zero errormargin)
500                         $tol = 0;
501                         $ans = trim($answer);
502                     }
504                     if (!(is_numeric($ans) || $ans = '*') || !is_numeric($tol)) {
505                             $errornotnumbers = get_string('errornotnumbers');
506                             $this->error($errornotnumbers, $text);
507                         return false;
508                         break;
509                     }
511                     // store results
512                     $question->answer[$key] = $ans;
513                     $question->tolerance[$key] = $tol;
514                 }
516                 if ($wrongfeedback) {
517                     $key += 1;
518                     $question->fraction[$key] = 0;
519                     list($notused, $question->feedback[$key]) = $this->commentparser(
520                             $wrongfeedback, $question->questiontextformat);
521                     $question->answer[$key] = '*';
522                     $question->tolerance[$key] = '';
523                 }
525                 return $question;
526                 break;
528                 default:
529                     $this->error(get_string('giftnovalidquestion', 'quiz'), $text);
530                 return fale;
531                 break;
533         }
535     }
537     function repchar($text, $notused = 0) {
538         // Escapes 'reserved' characters # = ~ {) :
539         // Removes new lines
540         $reserved = array( '#', '=', '~', '{', '}', ':', "\n", "\r");
541         $escaped =  array('\#','\=','\~','\{','\}','\:', '\n', '' );
543         $newtext = str_replace($reserved, $escaped, $text);
544         return $newtext;
545     }
547     /**
548      * @param integer $format one of the FORMAT_ constants.
549      * @return string the corresponding name.
550      */
551     function format_const_to_name($format) {
552         if ($format == FORMAT_MOODLE) {
553             return 'moodle';
554         } else if ($format == FORMAT_HTML) {
555             return 'html';
556         } else if ($format == FORMAT_PLAIN) {
557             return 'plain';
558         } else if ($format == FORMAT_MARKDOWN) {
559             return 'markdown';
560         } else {
561             return 'moodle';
562         }
563     }
565     /**
566      * @param integer $format one of the FORMAT_ constants.
567      * @return string the corresponding name.
568      */
569     function format_name_to_const($format) {
570         if ($format == 'moodle') {
571             return FORMAT_MOODLE;
572         } else if ($format == 'html') {
573             return FORMAT_HTML;
574         } else if ($format == 'plain') {
575             return FORMAT_PLAIN;
576         } else if ($format == 'markdown') {
577             return FORMAT_MARKDOWN;
578         } else {
579             return -1;
580         }
581     }
583     public function write_name($name) {
584         return '::' . $this->repchar($name) . '::';
585     }
587     public function write_questiontext($text, $format, $defaultformat = FORMAT_MOODLE) {
588         $output = '';
589         if ($text != '' && $format != $defaultformat) {
590             $output .= '[' . $this->format_const_to_name($format) . ']';
591         }
592         $output .= $this->repchar($text, $format);
593         return $output;
594     }
596     function writequestion($question) {
597         global $QTYPES, $OUTPUT;
599         // Start with a comment
600         $expout = "// question: $question->id  name: $question->name\n";
602         // output depends on question type
603         switch($question->qtype) {
605         case 'category':
606             // not a real question, used to insert category switch
607             $expout .= "\$CATEGORY: $question->category\n";
608             break;
610         case DESCRIPTION:
611             $expout .= $this->write_name($question->name);
612             $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
613             break;
615         case ESSAY:
616             $expout .= $this->write_name($question->name);
617             $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
618             $expout .= "{}\n";
619             break;
621         case TRUEFALSE:
622             $trueanswer = $question->options->answers[$question->options->trueanswer];
623             $falseanswer = $question->options->answers[$question->options->falseanswer];
624             if ($trueanswer->fraction == 1) {
625                 $answertext = 'TRUE';
626                 $rightfeedback = $this->write_questiontext($trueanswer->feedback,
627                         $trueanswer->feedbackformat, $question->questiontextformat);
628                 $wrongfeedback = $this->write_questiontext($falseanswer->feedback,
629                         $falseanswer->feedbackformat, $question->questiontextformat);
630             } else {
631                 $answertext = 'FALSE';
632                 $rightfeedback = $this->write_questiontext($falseanswer->feedback,
633                         $falseanswer->feedbackformat, $question->questiontextformat);
634                 $wrongfeedback = $this->write_questiontext($trueanswer->feedback,
635                         $trueanswer->feedbackformat, $question->questiontextformat);
636             }
638             $expout .= $this->write_name($question->name);
639             $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
640             $expout .= '{' . $this->repchar($answertext);
641             if ($wrongfeedback) {
642                 $expout .= '#' . $wrongfeedback;
643             } else if ($rightfeedback) {
644                 $expout .= '#';
645             }
646             if ($rightfeedback) {
647                 $expout .= '#' . $rightfeedback;
648             }
649             $expout .= "}\n";
650             break;
652         case MULTICHOICE:
653             $expout .= $this->write_name($question->name);
654             $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
655             $expout .= "{\n";
656             foreach($question->options->answers as $answer) {
657                 if ($answer->fraction == 1) {
658                     $answertext = '=';
659                 } else if ($answer->fraction == 0) {
660                     $answertext = '~';
661                 } else {
662                     $weight = $answer->fraction * 100;
663                     $answertext = '~%' . $weight . '%';
664                 }
665                 $expout .= "\t" . $answertext . $this->write_questiontext($answer->answer,
666                             $answer->answerformat, $question->questiontextformat);
667                 if ($answer->feedback != '') {
668                     $expout .= '#' . $this->write_questiontext($answer->feedback,
669                             $answer->feedbackformat, $question->questiontextformat);
670                 }
671                 $expout .= "\n";
672             }
673             $expout .= "}\n";
674             break;
676         case SHORTANSWER:
677             $expout .= $this->write_name($question->name);
678             $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
679             $expout .= "{\n";
680             foreach($question->options->answers as $answer) {
681                 $weight = 100 * $answer->fraction;
682                 $expout .= "\t=%" . $weight . '%' . $this->repchar($answer->answer) .
683                         '#' . $this->write_questiontext($answer->feedback,
684                             $answer->feedbackformat, $question->questiontextformat) . "\n";
685             }
686             $expout .= "}\n";
687             break;
689         case NUMERICAL:
690             $expout .= $this->write_name($question->name);
691             $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
692             $expout .= "{#\n";
693             foreach ($question->options->answers as $answer) {
694                 if ($answer->answer != '' && $answer->answer != '*') {
695                     $weight = 100 * $answer->fraction;
696                     $expout .= "\t=%" . $weight . '%' . $answer->answer . ':' .
697                             (float)$answer->tolerance . '#' . $this->write_questiontext($answer->feedback,
698                             $answer->feedbackformat, $question->questiontextformat) . "\n";
699                 } else {
700                     $expout .= "\t~#" . $this->write_questiontext($answer->feedback,
701                             $answer->feedbackformat, $question->questiontextformat) . "\n";
702                 }
703             }
704             $expout .= "}\n";
705             break;
707         case MATCH:
708             $expout .= $this->write_name($question->name);
709             $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
710             $expout .= "{\n";
711             foreach($question->options->subquestions as $subquestion) {
712                 $expout .= "\t=" . $this->repchar($this->write_questiontext($subquestion->questiontext, $subquestion->questiontextformat, $question->questiontextformat)) .
713                         ' -> ' . $this->repchar($subquestion->answertext) . "\n";
714             }
715             $expout .= "}\n";
716             break;
718         default:
719             // Check for plugins
720             if ($out = $this->try_exporting_using_qtypes($question->qtype, $question)) {
721                 $expout .= $out;
722             } else {
723                 $expout .= "Question type $question->qtype is not supported\n";
724                 echo $OUTPUT->notification(get_string('nohandler', 'qformat_gift',
725                         $QTYPES[$question->qtype]->local_name()));
726             }
727         }
729         // Add empty line to delimit questions
730         $expout .= "\n";
731         return $expout;
732     }