Automatic installer.php lang files by installer_builder (20070726)
[moodle-linuxchix.git] / question / type / multianswer / questiontype.php
blob77bc15572bb7493831b9a77f9b79313e9e538faa
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
20 class embedded_cloze_qtype extends default_questiontype {
22 function name() {
23 return 'multianswer';
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;
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})");
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?
52 return true;
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);
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;
78 $wrapped->name = $question->name;
79 $wrapped->parent = $question->id;
80 $wrapped->category = $question->category;
81 $wrapped = $QTYPES[$wrapped->qtype]->save_question($wrapped,
82 $wrapped, $question->course);
83 $sequence[] = $wrapped->id;
86 // Delete redundant wrapped questions
87 $oldwrappedids = implode(',', $oldwrappedids);
88 delete_records_select('question', "id IN ($oldwrappedids)");
90 if (!empty($sequence)) {
91 $multianswer = new stdClass;
92 $multianswer->question = $question->id;
93 $multianswer->sequence = implode(',', $sequence);
94 if ($oldid = get_field('question_multianswer', 'id', 'question', $question->id)) {
95 $multianswer->id = $oldid;
96 if (!update_record("question_multianswer", $multianswer)) {
97 $result->error = "Could not update cloze question options! " .
98 "(id=$multianswer->id)";
99 return $result;
101 } else {
102 if (!insert_record("question_multianswer", $multianswer)) {
103 $result->error = "Could not insert cloze question options!";
104 return $result;
110 function save_question($authorizedquestion, $form, $course) {
111 $question = qtype_multianswer_extract_question($form->questiontext);
112 if (isset($authorizedquestion->id)) {
113 $question->id = $authorizedquestion->id;
117 $question->category = $authorizedquestion->category;
118 $form->course = $course; // To pass the course object to
119 // save_question_options, where it is
120 // needed to call type specific
121 // save_question methods.
122 $form->defaultgrade = $question->defaultgrade;
123 $form->questiontext = $question->questiontext;
124 $form->questiontextformat = 0;
125 $form->options = clone($question->options);
126 unset($question->options);
127 return parent::save_question($question, $form, $course);
130 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
131 $state->responses = array();
132 foreach ($question->options->questions as $key => $wrapped) {
133 $state->responses[$key] = '';
135 return true;
138 function restore_session_and_responses(&$question, &$state) {
139 $responses = explode(',', $state->responses['']);
140 $state->responses = array();
141 foreach ($responses as $response) {
142 $tmp = explode("-", $response);
143 // restore encoded characters
144 $state->responses[$tmp[0]] = str_replace(array("&#0044;", "&#0045;"),
145 array(",", "-"), $tmp[1]);
147 return true;
150 function save_session_and_responses(&$question, &$state) {
151 $responses = $state->responses;
152 // encode - (hyphen) and , (comma) to &#0045; because they are used as
153 // delimiters
154 array_walk($responses, create_function('&$val, $key',
155 '$val = str_replace(array(",", "-"), array("&#0044;", "&#0045;"), $val);
156 $val = "$key-$val";'));
157 $responses = implode(',', $responses);
159 // Set the legacy answer field
160 if (!set_field('question_states', 'answer', $responses, 'id', $state->id)) {
161 return false;
163 return true;
167 * Deletes question from the question-type specific tables
169 * @return boolean Success/Failure
170 * @param object $question The question being deleted
172 function delete_question($questionid) {
173 delete_records("question_multianswer", "question", $questionid);
174 return true;
177 function get_correct_responses(&$question, &$state) {
178 global $QTYPES;
179 $responses = array();
180 foreach($question->options->questions as $key => $wrapped) {
181 if ($correct = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
182 $responses[$key] = $correct[''];
183 } else {
184 // if there is no correct answer to this subquestion then there
185 // can not be a correct answer to the whole question either, so
186 // we have to return null.
187 return null;
190 return $responses;
193 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
195 global $QTYPES, $CFG, $USER;
196 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
197 $disabled = empty($options->readonly) ? '' : 'disabled="disabled"';
198 $formatoptions = new stdClass;
199 $formatoptions->noclean = true;
200 $formatoptions->para = false;
201 $nameprefix = $question->name_prefix;
203 // adding an icon with alt to warn user this is a fill in the gap question
204 // MDL-7497
205 if (!empty($USER->screenreader)) {
206 echo "<img src=\"$CFG->wwwroot/question/type/$question->qtype/icon.gif\" ".
207 "class=\"icon\" alt=\"".get_string('clozeaid','qtype_multichoice')."\" /> ";
209 // For this question type, we better print the image on top:
210 if ($image = get_question_image($question, $cmoptions->course)) {
211 echo('<img class="qimage" src="' . $image . '" alt="" /><br />');
214 $qtextremaining = format_text($question->questiontext,
215 $question->questiontextformat, $formatoptions, $cmoptions->course);
217 $strfeedback = get_string('feedback', 'quiz');
219 // The regex will recognize text snippets of type {#X}
220 // where the X can be any text not containg } or white-space characters.
222 while (ereg('\{#([^[:space:]}]*)}', $qtextremaining, $regs)) {
223 $qtextsplits = explode($regs[0], $qtextremaining, 2);
224 echo "<label>"; // MDL-7497
225 echo $qtextsplits[0];
226 $qtextremaining = $qtextsplits[1];
228 $positionkey = $regs[1];
229 $wrapped = &$question->options->questions[$positionkey];
230 $answers = &$wrapped->options->answers;
231 $correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state);
233 $inputname = $nameprefix.$positionkey;
234 if (isset($state->responses[$positionkey])) {
235 $response = $state->responses[$positionkey];
236 } else {
237 $response = null;
240 // Determine feedback popup if any
241 $popup = '';
242 $style = '';
243 $feedbackimg = '';
244 if ($options->feedback) {
245 $chosenanswer = null;
246 switch ($wrapped->qtype) {
247 case 'numerical':
248 case 'shortanswer':
249 $testedstate = clone($state);
250 $testedstate->responses[''] = $response;
251 foreach ($answers as $answer) {
252 if($QTYPES[$wrapped->qtype]
253 ->test_response($wrapped, $testedstate, $answer)) {
254 $chosenanswer = clone($answer);
255 break;
258 break;
259 case 'multichoice':
260 if (isset($answers[$response])) {
261 $chosenanswer = clone($answers[$response]);
263 break;
264 default:
265 break;
268 // Set up a default chosenanswer so that all non-empty wrong
269 // answers are highlighted red
270 if (empty($chosenanswer) && !empty($response)) {
271 $chosenanswer = new stdClass;
272 $chosenanswer->fraction = 0.0;
275 if (!empty($chosenanswer->feedback)) {
276 $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $chosenanswer->feedback));
277 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedback', FGCOLOR, '#FFFFFF');\" ".
278 " onmouseout=\"return nd();\" ";
281 /// Determine style
282 if ($options->feedback && !empty($response)) {
283 $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction).'"';
284 $feedbackimg = question_get_feedback_image($chosenanswer->fraction);
285 } else {
286 $style = '';
287 $feedbackimg = '';
291 // Print the input control
292 switch ($wrapped->qtype) {
293 case 'shortanswer':
294 case 'numerical':
295 echo " <input $style $readonly $popup name=\"$inputname\"
296 type=\"text\" value=\"".s($response, true)."\" size=\"12\" /> ";
297 if (!empty($feedback) && !empty($USER->screenreader)) {
298 echo "<img src=\"$CFG->pixpath/i/feedback.gif\" alt=\"$feedback\" />";
300 echo $feedbackimg;
301 break;
302 case 'multichoice':
303 $outputoptions = '<option></option>'; // Default empty option
304 foreach ($answers as $mcanswer) {
305 $selected = '';
306 if ($response == $mcanswer->id) {
307 $selected = ' selected="selected"';
309 $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
310 s($mcanswer->answer, true) . '</option>';
312 // In the next line, $readonly is invalid HTML, but it works in
313 // all browsers. $disabled would be valid, but then the JS for
314 // displaying the feedback does not work. Of course, we should
315 // not be relying on JS (for accessibility reasons), but that is
316 // a bigger problem.
318 // The span is used for safari, which does not allow styling of
319 // selects.
320 echo "<span $style><select $popup $readonly $style name=\"$inputname\">";
321 echo $outputoptions;
322 echo '</select></span>';
323 if (!empty($feedback) && !empty($USER->screenreader)) {
324 echo "<img src=\"$CFG->pixpath/i/feedback.gif\" alt=\"$feedback\" />";
326 echo $feedbackimg;
327 break;
328 default:
329 error("Unable to recognize questiontype ($wrapped->qtype) of
330 question part $positionkey.");
331 break;
333 echo "</label>"; // MDL-7497
336 // Print the final piece of question text:
337 echo $qtextremaining;
338 $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
341 function grade_responses(&$question, &$state, $cmoptions) {
342 global $QTYPES;
343 $teststate = clone($state);
344 $state->raw_grade = 0;
345 foreach($question->options->questions as $key => $wrapped) {
346 $state->responses[$key] = $state->responses[$key];
347 $teststate->responses = array('' => $state->responses[$key]);
348 $teststate->raw_grade = 0;
349 if (false === $QTYPES[$wrapped->qtype]
350 ->grade_responses($wrapped, $teststate, $cmoptions)) {
351 return false;
353 $state->raw_grade += $teststate->raw_grade;
355 $state->raw_grade /= $question->defaultgrade;
356 $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0)
357 * $question->maxgrade;
359 if (empty($state->raw_grade)) {
360 $state->raw_grade = 0.0;
362 $state->penalty = $question->penalty * $question->maxgrade;
364 // mark the state as graded
365 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
367 return true;
370 function get_actual_response($question, $state) {
371 global $QTYPES;
372 $teststate = clone($state);
373 foreach($question->options->questions as $key => $wrapped) {
374 $state->responses[$key] = html_entity_decode($state->responses[$key]);
375 $teststate->responses = array('' => $state->responses[$key]);
376 $correct = $QTYPES[$wrapped->qtype]
377 ->get_actual_response($wrapped, $teststate);
378 // change separator here if you want
379 $responsesseparator = ',';
380 $responses[$key] = implode($responsesseparator, $correct);
382 return $responses;
385 /// BACKUP FUNCTIONS ////////////////////////////
388 * Backup the data in the question
390 * This is used in question/backuplib.php
392 function backup($bf,$preferences,$question,$level=6) {
394 $status = true;
396 $multianswers = get_records("question_multianswer","question",$question,"id");
397 //If there are multianswers
398 if ($multianswers) {
399 //Print multianswers header
400 $status = fwrite ($bf,start_tag("MULTIANSWERS",$level,true));
401 //Iterate over each multianswer
402 foreach ($multianswers as $multianswer) {
403 $status = fwrite ($bf,start_tag("MULTIANSWER",$level+1,true));
404 //Print multianswer contents
405 fwrite ($bf,full_tag("ID",$level+2,false,$multianswer->id));
406 fwrite ($bf,full_tag("QUESTION",$level+2,false,$multianswer->question));
407 fwrite ($bf,full_tag("SEQUENCE",$level+2,false,$multianswer->sequence));
408 $status = fwrite ($bf,end_tag("MULTIANSWER",$level+1,true));
410 //Print multianswers footer
411 $status = fwrite ($bf,end_tag("MULTIANSWERS",$level,true));
412 //Now print question_answers
413 $status = question_backup_answers($bf,$preferences,$question);
415 return $status;
418 /// RESTORE FUNCTIONS /////////////////
421 * Restores the data in the question
423 * This is used in question/restorelib.php
425 function restore($old_question_id,$new_question_id,$info,$restore) {
427 $status = true;
429 //Get the multianswers array
430 $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER'];
431 //Iterate over multianswers
432 for($i = 0; $i < sizeof($multianswers); $i++) {
433 $mul_info = $multianswers[$i];
435 //We need this later
436 $oldid = backup_todb($mul_info['#']['ID']['0']['#']);
438 //Now, build the question_multianswer record structure
439 $multianswer = new stdClass;
440 $multianswer->question = $new_question_id;
441 $multianswer->sequence = backup_todb($mul_info['#']['SEQUENCE']['0']['#']);
443 //We have to recode the sequence field (a list of question ids)
444 //Extracts question id from sequence
445 $sequence_field = "";
446 $in_first = true;
447 $tok = strtok($multianswer->sequence,",");
448 while ($tok) {
449 //Get the answer from backup_ids
450 $question = backup_getid($restore->backup_unique_code,"question",$tok);
451 if ($question) {
452 if ($in_first) {
453 $sequence_field .= $question->new_id;
454 $in_first = false;
455 } else {
456 $sequence_field .= ",".$question->new_id;
459 //check for next
460 $tok = strtok(",");
462 //We have the answers field recoded to its new ids
463 $multianswer->sequence = $sequence_field;
464 //The structure is equal to the db, so insert the question_multianswer
465 $newid = insert_record("question_multianswer", $multianswer);
467 //Save ids in backup_ids
468 if ($newid) {
469 backup_putid($restore->backup_unique_code,"question_multianswer",
470 $oldid, $newid);
473 //Do some output
474 if (($i+1) % 50 == 0) {
475 if (!defined('RESTORE_SILENTLY')) {
476 echo ".";
477 if (($i+1) % 1000 == 0) {
478 echo "<br />";
481 backup_flush(300);
485 return $status;
488 function restore_map($old_question_id,$new_question_id,$info,$restore) {
490 $status = true;
492 //Get the multianswers array
493 $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER'];
494 //Iterate over multianswers
495 for($i = 0; $i < sizeof($multianswers); $i++) {
496 $mul_info = $multianswers[$i];
498 //We need this later
499 $oldid = backup_todb($mul_info['#']['ID']['0']['#']);
501 //Now, build the question_multianswer record structure
502 $multianswer->question = $new_question_id;
503 $multianswer->answers = backup_todb($mul_info['#']['ANSWERS']['0']['#']);
504 $multianswer->positionkey = backup_todb($mul_info['#']['POSITIONKEY']['0']['#']);
505 $multianswer->answertype = backup_todb($mul_info['#']['ANSWERTYPE']['0']['#']);
506 $multianswer->norm = backup_todb($mul_info['#']['NORM']['0']['#']);
508 //If we are in this method is because the question exists in DB, so its
509 //multianswer must exist too.
510 //Now, we are going to look for that multianswer in DB and to create the
511 //mappings in backup_ids to use them later where restoring states (user level).
513 //Get the multianswer from DB (by question and positionkey)
514 $db_multianswer = get_record ("question_multianswer","question",$new_question_id,
515 "positionkey",$multianswer->positionkey);
516 //Do some output
517 if (($i+1) % 50 == 0) {
518 if (!defined('RESTORE_SILENTLY')) {
519 echo ".";
520 if (($i+1) % 1000 == 0) {
521 echo "<br />";
524 backup_flush(300);
527 //We have the database multianswer, so update backup_ids
528 if ($db_multianswer) {
529 //We have the newid, update backup_ids
530 backup_putid($restore->backup_unique_code,"question_multianswer",$oldid,
531 $db_multianswer->id);
532 } else {
533 $status = false;
537 return $status;
540 function restore_recode_answer($state, $restore) {
541 //The answer is a comma separated list of hypen separated sequence number and answers. We may have to recode the answers
542 $answer_field = "";
543 $in_first = true;
544 $tok = strtok($state->answer,",");
545 while ($tok) {
546 //Extract the multianswer_id and the answer
547 $exploded = explode("-",$tok);
548 $seqnum = $exploded[0];
549 $answer = $exploded[1];
550 // $sequence is an ordered array of the question ids.
551 if (!$sequence = get_field('question_multianswer', 'sequence', 'question', $state->question)) {
552 error("The cloze question $state->question is missing its options");
554 $sequence = explode(',', $sequence);
555 // The id of the current question.
556 $wrappedquestionid = $sequence[$seqnum-1];
557 // now we can find the question
558 if (!$wrappedquestion = get_record('question', 'id', $wrappedquestionid)) {
559 notify("Can't find the subquestion $wrappedquestionid that is used as part $seqnum in cloze question $state->question");
561 // For multichoice question we need to recode the answer
562 if ($answer and $wrappedquestion->qtype == 'multichoice') {
563 //The answer is an answer_id, look for it in backup_ids
564 if (!$ans = backup_getid($restore->backup_unique_code,"question_answers",$answer)) {
565 echo 'Could not recode cloze multichoice answer '.$answer.'<br />';
567 $answer = $ans->new_id;
569 //build the new answer field for each pair
570 if ($in_first) {
571 $answer_field .= $seqnum."-".$answer;
572 $in_first = false;
573 } else {
574 $answer_field .= ",".$seqnum."-".$answer;
576 //check for next
577 $tok = strtok(",");
579 return $answer_field;
584 //// END OF CLASS ////
587 //////////////////////////////////////////////////////////////////////////
588 //// INITIATION - Without this line the question type is not in use... ///
589 //////////////////////////////////////////////////////////////////////////
590 question_register_questiontype(new embedded_cloze_qtype());
592 /////////////////////////////////////////////////////////////
593 //// ADDITIONAL FUNCTIONS
594 //// The functions below deal exclusivly with editing
595 //// of questions with question type 'multianswer'.
596 //// Therefore they are kept in this file.
597 //// They are not in the class as they are not
598 //// likely to be subject for overriding.
599 /////////////////////////////////////////////////////////////
601 // ANSWER_ALTERNATIVE regexes
602 define("ANSWER_ALTERNATIVE_FRACTION_REGEX",
603 '=|%(-?[0-9]+)%');
604 // for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
605 define("ANSWER_ALTERNATIVE_ANSWER_REGEX",
606 '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
607 define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX",
608 '.*?(?<!\\\\)(?=[~}]|$)');
609 define("ANSWER_ALTERNATIVE_REGEX",
610 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
611 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
612 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
614 // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
615 define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2);
616 define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1);
617 define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3);
618 define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5);
620 // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
621 // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
622 define("NUMBER_REGEX",
623 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
624 define("NUMERICAL_ALTERNATIVE_REGEX",
625 '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
627 // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
628 define("NUMERICAL_CORRECT_ANSWER", 1);
629 define("NUMERICAL_ABS_ERROR_MARGIN", 6);
631 // Remaining ANSWER regexes
632 define("ANSWER_TYPE_DEF_REGEX",
633 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(SHORTANSWER|SA|MW)');
634 define("ANSWER_START_REGEX",
635 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
637 define("ANSWER_REGEX",
638 ANSWER_START_REGEX
639 . '(' . ANSWER_ALTERNATIVE_REGEX
640 . '(~'
641 . ANSWER_ALTERNATIVE_REGEX
642 . ')*)\}' );
644 // Parenthesis positions for singulars in ANSWER_REGEX
645 define("ANSWER_REGEX_NORM", 1);
646 define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3);
647 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4);
648 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 5);
649 define("ANSWER_REGEX_ALTERNATIVES", 6);
651 function qtype_multianswer_extract_question($text) {
652 $question = new stdClass;
653 $question->qtype = 'multianswer';
654 $question->questiontext = $text;
655 $question->options->questions = array();
656 $question->defaultgrade = 0; // Will be increased for each answer norm
658 for ($positionkey=1
659 ; preg_match('/'.ANSWER_REGEX.'/', $question->questiontext, $answerregs)
660 ; ++$positionkey ) {
661 $wrapped = new stdClass;
662 $wrapped->defaultgrade = $answerregs[ANSWER_REGEX_NORM]
663 or $wrapped->defaultgrade = '1';
664 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
665 $wrapped->qtype = 'numerical';
666 $wrapped->multiplier = array();
667 $wrapped->units = array();
668 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
669 $wrapped->qtype = 'shortanswer';
670 $wrapped->usecase = 0;
671 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
672 $wrapped->qtype = 'multichoice';
673 $wrapped->single = 1;
674 $wrapped->correctfeedback = '';
675 $wrapped->partiallycorrectfeedback = '';
676 $wrapped->incorrectfeedback = '';
677 } else {
678 error("Cannot identify qtype $answerregs[2]");
679 return false;
682 // Each $wrapped simulates a $form that can be processed by the
683 // respective save_question and save_question_options methods of the
684 // wrapped questiontypes
685 $wrapped->answer = array();
686 $wrapped->fraction = array();
687 $wrapped->feedback = array();
688 $wrapped->shuffleanswers = 1;
689 $wrapped->questiontext = $answerregs[0];
690 $wrapped->questiontextformat = 0;
692 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
693 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
694 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
695 $wrapped->fraction[] = '1';
696 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]){
697 $wrapped->fraction[] = .01 * $percentile;
698 } else {
699 $wrapped->fraction[] = '0';
701 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
702 $wrapped->feedback[] = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
703 } else {
704 $wrapped->feedback[] = '';
706 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
707 && ereg(NUMERICAL_ALTERNATIVE_REGEX, $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
708 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
709 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
710 $wrapped->tolerance[] =
711 $numregs[NUMERICAL_ABS_ERROR_MARGIN];
712 } else {
713 $wrapped->tolerance[] = 0;
715 } else { // Tolerance can stay undefined for non numerical questions
716 // Undo quoting done by the HTML editor.
717 $wrapped->answer[] = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
719 $tmp = explode($altregs[0], $remainingalts, 2);
720 $remainingalts = $tmp[1];
723 $question->defaultgrade += $wrapped->defaultgrade;
724 $question->options->questions[$positionkey] = clone($wrapped);
725 $question->questiontext = implode("{#$positionkey}",
726 explode($answerregs[0], $question->questiontext, 2));
728 $question->questiontext = $question->questiontext;
729 return $question;