checking that there remain $oldwrappedids before deleting them lines 87-..
[moodle.git] / question / type / multianswer / questiontype.php
1 <?php  // $Id$
3 ///////////////////
4 /// MULTIANSWER /// (Embedded - cloze)
5 ///////////////////
7 ///
8 /// The multianswer question type is special in that it
9 /// depends on a few other question types, i.e.
10 /// 'multichoice', 'shortanswer' and 'numerical'.
11 /// These question types have got a few special features that
12 /// makes them useable by the 'multianswer' question type
13 ///
15 /// QUESTION TYPE CLASS //////////////////
16 /**
17  * @package questionbank
18  * @subpackage questiontypes
19  */
20 class embedded_cloze_qtype extends default_questiontype {
22     function name() {
23         return 'multianswer';
24     }
26     function get_question_options(&$question) {
27         global $QTYPES;
29         // Get relevant data indexed by positionkey from the multianswers table
30         if (!$sequence = get_field('question_multianswer', 'sequence', 'question', $question->id)) {
31             notify('Error: Cloze question '.$question->id.' is missing question options!');
32             return false;
33         }
35         $wrappedquestions = get_records_list('question', 'id', $sequence, 'id ASC');
37         // We want an array with question ids as index and the positions as values
38         $sequence = array_flip(explode(',', $sequence));
39         array_walk($sequence, create_function('&$val', '$val++;'));
41         foreach ($wrappedquestions as $wrapped) {
42             if (!$QTYPES[$wrapped->qtype]->get_question_options($wrapped)) {
43                 notify("Unable to get options for questiontype {$wrapped->qtype} (id={$wrapped->id})");
44             }
45             // for wrapped questions the maxgrade is always equal to the defaultgrade,
46             // there is no entry in the question_instances table for them
47             $wrapped->maxgrade = $wrapped->defaultgrade;
49             $question->options->questions[$sequence[$wrapped->id]] = clone($wrapped); // ??? Why do we need a clone here?
50         }
52         return true;
53     }
55     function save_question_options($question) {
56         global $QTYPES;
57         $result = new stdClass;
59         // This function needs to be able to handle the case where the existing set of wrapped
60         // questions does not match the new set of wrapped questions so that some need to be
61         // created, some modified and some deleted
62         // Unfortunately the code currently simply overwrites existing ones in sequence. This
63         // will make re-marking after a re-ordering of wrapped questions impossible and
64         // will also create difficulties if questiontype specific tables reference the id.
66         // First we get all the existing wrapped questions
67         if (!$oldwrappedids = get_field('question_multianswer', 'sequence', 'question', $question->id)) {
68             $oldwrappedids = array();
69         } else {
70             $oldwrappedids = explode(',', $oldwrappedids);
71         }
72         $sequence = array();
73         foreach($question->options->questions as $wrapped) {
74             // if we still have some old wrapped question ids, reuse the next of them
75             if ($oldwrappedid = array_shift($oldwrappedids)) {
76                 $wrapped->id = $oldwrappedid;
77             }
78             $wrapped->name = $question->name;
79             $wrapped->parent = $question->id;
80             $wrapped->category = $question->category . ',1'; // save_question strips this extra bit off again.
81             $wrapped = $QTYPES[$wrapped->qtype]->save_question($wrapped,
82                     $wrapped, $question->course);
83             $sequence[] = $wrapped->id;
84         }
86         // Delete redundant wrapped questions
87         if(is_array($oldwrappedids) && count($oldwrappedids)){ 
88             $oldwrappedids = implode(',', $oldwrappedids);
89             delete_records_select('question', "id IN ($oldwrappedids)");
90         }
92         if (!empty($sequence)) {
93             $multianswer = new stdClass;
94             $multianswer->question = $question->id;
95             $multianswer->sequence = implode(',', $sequence);
96             if ($oldid = get_field('question_multianswer', 'id', 'question', $question->id)) {
97                 $multianswer->id = $oldid;
98                 if (!update_record("question_multianswer", $multianswer)) {
99                     $result->error = "Could not update cloze question options! " .
100                             "(id=$multianswer->id)";
101                     return $result;
102                 }
103             } else {
104                 if (!insert_record("question_multianswer", $multianswer)) {
105                     $result->error = "Could not insert cloze question options!";
106                     return $result;
107                 }
108             }
109         }
110     }
112     function save_question($authorizedquestion, $form, $course) {
113         $question = qtype_multianswer_extract_question($form->questiontext);
114         if (isset($authorizedquestion->id)) {
115             $question->id = $authorizedquestion->id;
116         }
119         $question->category = $authorizedquestion->category;
120         $form->course = $course; // To pass the course object to
121                                  // save_question_options, where it is
122                                  // needed to call type specific
123                                  // save_question methods.
124         $form->defaultgrade = $question->defaultgrade;
125         $form->questiontext = $question->questiontext;
126         $form->questiontextformat = 0;
127         $form->options = clone($question->options);
128         unset($question->options);
129         return parent::save_question($question, $form, $course);
130     }
132     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
133         $state->responses = array();
134         foreach ($question->options->questions as $key => $wrapped) {
135             $state->responses[$key] = '';
136         }
137         return true;
138     }
140     function restore_session_and_responses(&$question, &$state) {
141         $responses = explode(',', $state->responses['']);
142         $state->responses = array();
143         foreach ($responses as $response) {
144             $tmp = explode("-", $response);
145             // restore encoded characters
146             $state->responses[$tmp[0]] = str_replace(array("&#0044;", "&#0045;"),
147                     array(",", "-"), $tmp[1]);
148         }
149         return true;
150     }
152     function save_session_and_responses(&$question, &$state) {
153         $responses = $state->responses;
154         // encode - (hyphen) and , (comma) to &#0045; because they are used as
155         // delimiters
156         array_walk($responses, create_function('&$val, $key',
157                 '$val = str_replace(array(",", "-"), array("&#0044;", "&#0045;"), $val);
158                 $val = "$key-$val";'));
159         $responses = implode(',', $responses);
161         // Set the legacy answer field
162         if (!set_field('question_states', 'answer', $responses, 'id', $state->id)) {
163             return false;
164         }
165         return true;
166     }
168     /**
169     * Deletes question from the question-type specific tables
170     *
171     * @return boolean Success/Failure
172     * @param object $question  The question being deleted
173     */
174     function delete_question($questionid) {
175         delete_records("question_multianswer", "question", $questionid);
176         return true;
177     }
179     function get_correct_responses(&$question, &$state) {
180         global $QTYPES;
181         $responses = array();
182         foreach($question->options->questions as $key => $wrapped) {
183             if ($correct = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
184                 $responses[$key] = $correct[''];
185             } else {
186                 // if there is no correct answer to this subquestion then there
187                 // can not be a correct answer to the whole question either, so
188                 // we have to return null.
189                 return null;
190             }
191         }
192         return $responses;
193     }
195     function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
197         global $QTYPES, $CFG, $USER;
198         $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
199         $disabled = empty($options->readonly) ? '' : 'disabled="disabled"';
200         $formatoptions = new stdClass;
201         $formatoptions->noclean = true;
202         $formatoptions->para = false;
203         $nameprefix = $question->name_prefix;
205         // adding an icon with alt to warn user this is a fill in the gap question
206         // MDL-7497
207         if (!empty($USER->screenreader)) {
208             echo "<img src=\"$CFG->wwwroot/question/type/$question->qtype/icon.gif\" ".
209                 "class=\"icon\" alt=\"".get_string('clozeaid','qtype_multichoice')."\" />  ";
210         }
211         // For this question type, we better print the image on top:
212         if ($image = get_question_image($question)) {
213             echo('<img class="qimage" src="' . $image . '" alt="" /><br />');
214         }
216         $qtextremaining = format_text($question->questiontext,
217                 $question->questiontextformat, $formatoptions, $cmoptions->course);
219         $strfeedback = get_string('feedback', 'quiz');
221         // The regex will recognize text snippets of type {#X}
222         // where the X can be any text not containg } or white-space characters.
224         while (ereg('\{#([^[:space:]}]*)}', $qtextremaining, $regs)) {
225             $qtextsplits = explode($regs[0], $qtextremaining, 2);
226             echo "<label>"; // MDL-7497
227             echo $qtextsplits[0];
228             $qtextremaining = $qtextsplits[1];
230             $positionkey = $regs[1];
231             $wrapped = &$question->options->questions[$positionkey];
232             $answers = &$wrapped->options->answers;
233             $correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state);
235             $inputname = $nameprefix.$positionkey;
236             if (isset($state->responses[$positionkey])) {
237                 $response = $state->responses[$positionkey];
238             } else {
239                 $response = null;
240             }
242             // Determine feedback popup if any
243             $popup = '';
244             $style = '';
245             $feedbackimg = '';
246             if ($options->feedback) {
247                 $chosenanswer = null;
248                 switch ($wrapped->qtype) {
249                     case 'numerical':
250                     case 'shortanswer':
251                         $testedstate = clone($state);
252                         $testedstate->responses[''] = $response;
253                         foreach ($answers as $answer) {
254                             if($QTYPES[$wrapped->qtype]
255                                     ->test_response($wrapped, $testedstate, $answer)) {
256                                 $chosenanswer = clone($answer);
257                                 break;
258                             }
259                         }
260                         break;
261                     case 'multichoice':
262                         if (isset($answers[$response])) {
263                             $chosenanswer = clone($answers[$response]);
264                         }
265                         break;
266                     default:
267                         break;
268                 }
270                 // Set up a default chosenanswer so that all non-empty wrong
271                 // answers are highlighted red
272                 if (empty($chosenanswer) && !empty($response)) {
273                     $chosenanswer = new stdClass;
274                     $chosenanswer->fraction = 0.0;
275                 }
277                 if (!empty($chosenanswer->feedback)) {
278                     $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $chosenanswer->feedback));
279                     $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedback', FGCOLOR, '#FFFFFF');\" ".
280                              " onmouseout=\"return nd();\" ";
281                 }
283                 /// Determine style
284                 if ($options->feedback && $response != '') {
285                     $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction).'"';
286                     $feedbackimg = question_get_feedback_image($chosenanswer->fraction);
287                 } else {
288                     $style = '';
289                     $feedbackimg = '';
290                 }
291             }
293             // Print the input control
294             switch ($wrapped->qtype) {
295                 case 'shortanswer':
296                 case 'numerical':
297                     echo " <input $style $readonly $popup name=\"$inputname\"
298                             type=\"text\" value=\"".s($response, true)."\" size=\"12\" /> ";
299                     if (!empty($feedback) && !empty($USER->screenreader)) {
300                         echo "<img src=\"$CFG->pixpath/i/feedback.gif\" alt=\"$feedback\" />";
301                     }
302                     echo $feedbackimg;
303                     break;
304                 case 'multichoice':
305                     $outputoptions = '<option></option>'; // Default empty option
306                     foreach ($answers as $mcanswer) {
307                         $selected = '';
308                         if ($response == $mcanswer->id) {
309                             $selected = ' selected="selected"';
310                         }
311                         $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
312                                 s($mcanswer->answer, true) . '</option>';
313                     }
314                     // In the next line, $readonly is invalid HTML, but it works in
315                     // all browsers. $disabled would be valid, but then the JS for
316                     // displaying the feedback does not work. Of course, we should
317                     // not be relying on JS (for accessibility reasons), but that is
318                     // a bigger problem.
319                     //
320                     // The span is used for safari, which does not allow styling of
321                     // selects.
322                     echo "<span $style><select $popup $readonly $style name=\"$inputname\">";
323                     echo $outputoptions;
324                     echo '</select></span>';
325                     if (!empty($feedback) && !empty($USER->screenreader)) {
326                         echo "<img src=\"$CFG->pixpath/i/feedback.gif\" alt=\"$feedback\" />";
327                     }
328                     echo $feedbackimg;
329                     break;
330                 default:
331                     error("Unable to recognize questiontype ($wrapped->qtype) of
332                            question part $positionkey.");
333                     break;
334            }
335            echo "</label>"; // MDL-7497
336         }
338         // Print the final piece of question text:
339         echo $qtextremaining;
340         $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
341     }
343     function grade_responses(&$question, &$state, $cmoptions) {
344         global $QTYPES;
345         $teststate = clone($state);
346         $state->raw_grade = 0;
347         foreach($question->options->questions as $key => $wrapped) {
348             $state->responses[$key] = $state->responses[$key];
349             $teststate->responses = array('' => $state->responses[$key]);
350             $teststate->raw_grade = 0;
351             if (false === $QTYPES[$wrapped->qtype]
352              ->grade_responses($wrapped, $teststate, $cmoptions)) {
353                 return false;
354             }
355             $state->raw_grade += $teststate->raw_grade;
356         }
357         $state->raw_grade /= $question->defaultgrade;
358         $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0)
359          * $question->maxgrade;
361         if (empty($state->raw_grade)) {
362             $state->raw_grade = 0.0;
363         }
364         $state->penalty = $question->penalty * $question->maxgrade;
366         // mark the state as graded
367         $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
369         return true;
370     }
372     function get_actual_response($question, $state) {
373         global $QTYPES;
374         $teststate = clone($state);
375         foreach($question->options->questions as $key => $wrapped) {
376             $state->responses[$key] = html_entity_decode($state->responses[$key]);
377             $teststate->responses = array('' => $state->responses[$key]);
378             $correct = $QTYPES[$wrapped->qtype]
379              ->get_actual_response($wrapped, $teststate);
380             // change separator here if you want
381             $responsesseparator = ',';
382             $responses[$key] = implode($responsesseparator, $correct);
383         }
384         return $responses;
385     }
387 /// BACKUP FUNCTIONS ////////////////////////////
389     /*
390      * Backup the data in the question
391      *
392      * This is used in question/backuplib.php
393      */
394     function backup($bf,$preferences,$question,$level=6) {
396         $status = true;
398         $multianswers = get_records("question_multianswer","question",$question,"id");
399         //If there are multianswers
400         if ($multianswers) {
401             //Print multianswers header
402             $status = fwrite ($bf,start_tag("MULTIANSWERS",$level,true));
403             //Iterate over each multianswer
404             foreach ($multianswers as $multianswer) {
405                 $status = fwrite ($bf,start_tag("MULTIANSWER",$level+1,true));
406                 //Print multianswer contents
407                 fwrite ($bf,full_tag("ID",$level+2,false,$multianswer->id));
408                 fwrite ($bf,full_tag("QUESTION",$level+2,false,$multianswer->question));
409                 fwrite ($bf,full_tag("SEQUENCE",$level+2,false,$multianswer->sequence));
410                 $status = fwrite ($bf,end_tag("MULTIANSWER",$level+1,true));
411             }
412             //Print multianswers footer
413             $status = fwrite ($bf,end_tag("MULTIANSWERS",$level,true));
414             //Now print question_answers
415             $status = question_backup_answers($bf,$preferences,$question);
416         }
417         return $status;
418     }
420 /// RESTORE FUNCTIONS /////////////////
422     /*
423      * Restores the data in the question
424      *
425      * This is used in question/restorelib.php
426      */
427     function restore($old_question_id,$new_question_id,$info,$restore) {
429         $status = true;
431         //Get the multianswers array
432         $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER'];
433         //Iterate over multianswers
434         for($i = 0; $i < sizeof($multianswers); $i++) {
435             $mul_info = $multianswers[$i];
437             //We need this later
438             $oldid = backup_todb($mul_info['#']['ID']['0']['#']);
440             //Now, build the question_multianswer record structure
441             $multianswer = new stdClass;
442             $multianswer->question = $new_question_id;
443             $multianswer->sequence = backup_todb($mul_info['#']['SEQUENCE']['0']['#']);
445             //We have to recode the sequence field (a list of question ids)
446             //Extracts question id from sequence
447             $sequence_field = "";
448             $in_first = true;
449             $tok = strtok($multianswer->sequence,",");
450             while ($tok) {
451                 //Get the answer from backup_ids
452                 $question = backup_getid($restore->backup_unique_code,"question",$tok);
453                 if ($question) {
454                     if ($in_first) {
455                         $sequence_field .= $question->new_id;
456                         $in_first = false;
457                     } else {
458                         $sequence_field .= ",".$question->new_id;
459                     }
460                 }
461                 //check for next
462                 $tok = strtok(",");
463             }
464             //We have the answers field recoded to its new ids
465             $multianswer->sequence = $sequence_field;
466             //The structure is equal to the db, so insert the question_multianswer
467             $newid = insert_record("question_multianswer", $multianswer);
469             //Save ids in backup_ids
470             if ($newid) {
471                 backup_putid($restore->backup_unique_code,"question_multianswer",
472                              $oldid, $newid);
473             }
475             //Do some output
476             if (($i+1) % 50 == 0) {
477                 if (!defined('RESTORE_SILENTLY')) {
478                     echo ".";
479                     if (($i+1) % 1000 == 0) {
480                         echo "<br />";
481                     }
482                 }
483                 backup_flush(300);
484             }
485         }
487         return $status;
488     }
490     function restore_map($old_question_id,$new_question_id,$info,$restore) {
492         $status = true;
494         //Get the multianswers array
495         $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER'];
496         //Iterate over multianswers
497         for($i = 0; $i < sizeof($multianswers); $i++) {
498             $mul_info = $multianswers[$i];
500             //We need this later
501             $oldid = backup_todb($mul_info['#']['ID']['0']['#']);
503             //Now, build the question_multianswer record structure
504             $multianswer->question = $new_question_id;
505             $multianswer->answers = backup_todb($mul_info['#']['ANSWERS']['0']['#']);
506             $multianswer->positionkey = backup_todb($mul_info['#']['POSITIONKEY']['0']['#']);
507             $multianswer->answertype = backup_todb($mul_info['#']['ANSWERTYPE']['0']['#']);
508             $multianswer->norm = backup_todb($mul_info['#']['NORM']['0']['#']);
510             //If we are in this method is because the question exists in DB, so its
511             //multianswer must exist too.
512             //Now, we are going to look for that multianswer in DB and to create the
513             //mappings in backup_ids to use them later where restoring states (user level).
515             //Get the multianswer from DB (by question and positionkey)
516             $db_multianswer = get_record ("question_multianswer","question",$new_question_id,
517                                                       "positionkey",$multianswer->positionkey);
518             //Do some output
519             if (($i+1) % 50 == 0) {
520                 if (!defined('RESTORE_SILENTLY')) {
521                     echo ".";
522                     if (($i+1) % 1000 == 0) {
523                         echo "<br />";
524                     }
525                 }
526                 backup_flush(300);
527             }
529             //We have the database multianswer, so update backup_ids
530             if ($db_multianswer) {
531                 //We have the newid, update backup_ids
532                 backup_putid($restore->backup_unique_code,"question_multianswer",$oldid,
533                              $db_multianswer->id);
534             } else {
535                 $status = false;
536             }
537         }
539         return $status;
540     }
542     function restore_recode_answer($state, $restore) {
543         //The answer is a comma separated list of hypen separated sequence number and answers. We may have to recode the answers
544         $answer_field = "";
545         $in_first = true;
546         $tok = strtok($state->answer,",");
547         while ($tok) {
548             //Extract the multianswer_id and the answer
549             $exploded = explode("-",$tok);
550             $seqnum = $exploded[0];
551             $answer = $exploded[1];
552             // $sequence is an ordered array of the question ids.
553             if (!$sequence = get_field('question_multianswer', 'sequence', 'question', $state->question)) {
554                 error("The cloze question $state->question is missing its options");
555             }
556             $sequence = explode(',', $sequence);
557             // The id of the current question.
558             $wrappedquestionid = $sequence[$seqnum-1];
559             // now we can find the question
560             if (!$wrappedquestion = get_record('question', 'id', $wrappedquestionid)) {
561                 notify("Can't find the subquestion $wrappedquestionid that is used as part $seqnum in cloze question $state->question");
562             }
563             // For multichoice question we need to recode the answer
564             if ($answer and $wrappedquestion->qtype == 'multichoice') {
565                 //The answer is an answer_id, look for it in backup_ids
566                 if (!$ans = backup_getid($restore->backup_unique_code,"question_answers",$answer)) {
567                     echo 'Could not recode cloze multichoice answer '.$answer.'<br />';
568                 }
569                 $answer = $ans->new_id;
570             }
571             //build the new answer field for each pair
572             if ($in_first) {
573                 $answer_field .= $seqnum."-".$answer;
574                 $in_first = false;
575             } else {
576                 $answer_field .= ",".$seqnum."-".$answer;
577             }
578             //check for next
579             $tok = strtok(",");
580         }
581         return $answer_field;
582     }
586 //// END OF CLASS ////
589 //////////////////////////////////////////////////////////////////////////
590 //// INITIATION - Without this line the question type is not in use... ///
591 //////////////////////////////////////////////////////////////////////////
592 question_register_questiontype(new embedded_cloze_qtype());
594 /////////////////////////////////////////////////////////////
595 //// ADDITIONAL FUNCTIONS
596 //// The functions below deal exclusivly with editing
597 //// of questions with question type 'multianswer'.
598 //// Therefore they are kept in this file.
599 //// They are not in the class as they are not
600 //// likely to be subject for overriding.
601 /////////////////////////////////////////////////////////////
603 // ANSWER_ALTERNATIVE regexes
604 define("ANSWER_ALTERNATIVE_FRACTION_REGEX",
605        '=|%(-?[0-9]+)%');
606 // for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
607 define("ANSWER_ALTERNATIVE_ANSWER_REGEX",
608         '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
609 define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX",
610         '.*?(?<!\\\\)(?=[~}]|$)');
611 define("ANSWER_ALTERNATIVE_REGEX",
612        '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
613        '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
614        '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
616 // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
617 define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2);
618 define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1);
619 define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3);
620 define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5);
622 // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
623 // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
624 define("NUMBER_REGEX",
625         '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
626 define("NUMERICAL_ALTERNATIVE_REGEX",
627         '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
629 // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
630 define("NUMERICAL_CORRECT_ANSWER", 1);
631 define("NUMERICAL_ABS_ERROR_MARGIN", 6);
633 // Remaining ANSWER regexes
634 define("ANSWER_TYPE_DEF_REGEX",
635        '(NUMERICAL|NM)|(MULTICHOICE|MC)|(SHORTANSWER|SA|MW)');
636 define("ANSWER_START_REGEX",
637        '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
639 define("ANSWER_REGEX",
640         ANSWER_START_REGEX
641         . '(' . ANSWER_ALTERNATIVE_REGEX
642         . '(~'
643         . ANSWER_ALTERNATIVE_REGEX
644         . ')*)\}' );
646 // Parenthesis positions for singulars in ANSWER_REGEX
647 define("ANSWER_REGEX_NORM", 1);
648 define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3);
649 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4);
650 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 5);
651 define("ANSWER_REGEX_ALTERNATIVES", 6);
653 function qtype_multianswer_extract_question($text) {
654     $question = new stdClass;
655     $question->qtype = 'multianswer';
656     $question->questiontext = $text;
657     $question->options->questions = array();
658     $question->defaultgrade = 0; // Will be increased for each answer norm
660     for ($positionkey=1
661         ; preg_match('/'.ANSWER_REGEX.'/', $question->questiontext, $answerregs)
662         ; ++$positionkey ) {
663         $wrapped = new stdClass;
664         $wrapped->defaultgrade = $answerregs[ANSWER_REGEX_NORM]
665             or $wrapped->defaultgrade = '1';
666         if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
667             $wrapped->qtype = 'numerical';
668             $wrapped->multiplier = array();
669             $wrapped->units      = array();
670         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
671             $wrapped->qtype = 'shortanswer';
672             $wrapped->usecase = 0;
673         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
674             $wrapped->qtype = 'multichoice';
675             $wrapped->single = 1;
676             $wrapped->answernumbering = 0;
677             $wrapped->correctfeedback = '';
678             $wrapped->partiallycorrectfeedback = '';
679             $wrapped->incorrectfeedback = '';
680         } else {
681             error("Cannot identify qtype $answerregs[2]");
682             return false;
683         }
685         // Each $wrapped simulates a $form that can be processed by the
686         // respective save_question and save_question_options methods of the
687         // wrapped questiontypes
688         $wrapped->answer   = array();
689         $wrapped->fraction = array();
690         $wrapped->feedback = array();
691         $wrapped->shuffleanswers = 1;
692         $wrapped->questiontext = $answerregs[0];
693         $wrapped->questiontextformat = 0;
695         $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
696         while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
697             if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
698                 $wrapped->fraction[] = '1';
699             } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]){
700                 $wrapped->fraction[] = .01 * $percentile;
701             } else {
702                 $wrapped->fraction[] = '0';
703             }
704             if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
705                 $feedback = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
706                 $feedback = str_replace('\}', '}', $feedback);
707                 $wrapped->feedback[] = str_replace('\#', '#', $feedback);
708             } else {
709                 $wrapped->feedback[] = '';
710             }
711             if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
712                     && ereg(NUMERICAL_ALTERNATIVE_REGEX, $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
713                 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
714                 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
715                     $wrapped->tolerance[] =
716                     $numregs[NUMERICAL_ABS_ERROR_MARGIN];
717                 } else {
718                     $wrapped->tolerance[] = 0;
719                 }
720             } else { // Tolerance can stay undefined for non numerical questions
721                 // Undo quoting done by the HTML editor.
722                 $answer = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
723                 $answer = str_replace('\}', '}', $answer);
724                 $wrapped->answer[] = str_replace('\#', '#', $answer);
725             }
726             $tmp = explode($altregs[0], $remainingalts, 2);
727             $remainingalts = $tmp[1];
728         }
730         $question->defaultgrade += $wrapped->defaultgrade;
731         $question->options->questions[$positionkey] = clone($wrapped);
732         $question->questiontext = implode("{#$positionkey}",
733                     explode($answerregs[0], $question->questiontext, 2));
734     }
735     $question->questiontext = $question->questiontext;
736     return $question;
738 ?>