qtype_multianswer MDL-25841 feedback pop-up was appearing in the wrong place.
[moodle.git] / question / type / multianswer / questiontype.php
1 <?php
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 has_wildcards_in_responses($question, $subqid) {
27         global $QTYPES, $OUTPUT;
28         foreach ($question->options->questions as $subq){
29             if ($subq->id == $subqid){
30                 return $QTYPES[$subq->qtype]->has_wildcards_in_responses($subq, $subqid);
31             }
32         }
33         echo $OUTPUT->notification('Could not find sub question!');
34         return true;
35     }
37     function requires_qtypes() {
38         return array('shortanswer', 'numerical', 'multichoice');
39     }
41     function get_question_options(&$question) {
42         global $QTYPES, $DB, $OUTPUT;
44         // Get relevant data indexed by positionkey from the multianswers table
45         if (!$sequence = $DB->get_field('question_multianswer', 'sequence', array('question' => $question->id))) {
46             echo $OUTPUT->notification(get_string('noquestions','qtype_multianswer',$question->name));
47             $question->options->questions['1']= '';
48             return true ;
49         }
51         $wrappedquestions = $DB->get_records_list('question', 'id', explode(',', $sequence), 'id ASC');
53         // We want an array with question ids as index and the positions as values
54         $sequence = array_flip(explode(',', $sequence));
55         array_walk($sequence, create_function('&$val', '$val++;'));
56         //If a question is lost, the corresponding index is null
57         // so this null convention is used to test $question->options->questions
58         // before using the values.
59         // first all possible questions from sequence are nulled
60         // then filled with the data if available in  $wrappedquestions
61         $nbvaliquestion = 0 ;
62         foreach($sequence as $seq){
63             $question->options->questions[$seq]= '';
64         }
65         if (isset($wrappedquestions) && is_array($wrappedquestions)){
66             foreach ($wrappedquestions as $wrapped) {
67                 if (!$QTYPES[$wrapped->qtype]->get_question_options($wrapped)) {
68                     echo $OUTPUT->notification("Unable to get options for questiontype {$wrapped->qtype} (id={$wrapped->id})");
69                 }else {
70                 // for wrapped questions the maxgrade is always equal to the defaultgrade,
71                 // there is no entry in the question_instances table for them
72                 $wrapped->maxgrade = $wrapped->defaultgrade;
73                     $nbvaliquestion++ ;
74                 $question->options->questions[$sequence[$wrapped->id]] = clone($wrapped); // ??? Why do we need a clone here?
75             }
76         }
77         }
78         if ($nbvaliquestion == 0 ) {
79             echo $OUTPUT->notification(get_string('noquestions','qtype_multianswer',$question->name));
80         }
82         return true;
83     }
85     function save_question_options($question) {
86         global $QTYPES, $DB;
87         $result = new stdClass;
89         // This function needs to be able to handle the case where the existing set of wrapped
90         // questions does not match the new set of wrapped questions so that some need to be
91         // created, some modified and some deleted
92         // Unfortunately the code currently simply overwrites existing ones in sequence. This
93         // will make re-marking after a re-ordering of wrapped questions impossible and
94         // will also create difficulties if questiontype specific tables reference the id.
96         // First we get all the existing wrapped questions
97         if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence', array('question' => $question->id))) {
98             $oldwrappedquestions = array();
99         } else {
100             $oldwrappedquestions = $DB->get_records_list('question', 'id', explode(',', $oldwrappedids), 'id ASC');
101         }
102         $sequence = array();
103         foreach($question->options->questions as $wrapped) {
104             if (!empty($wrapped)){
105                 // if we still have some old wrapped question ids, reuse the next of them
107                 if (is_array($oldwrappedquestions) && $oldwrappedquestion = array_shift($oldwrappedquestions)) {
108                     $wrapped->id = $oldwrappedquestion->id;
109                     if($oldwrappedquestion->qtype != $wrapped->qtype ) {
110                         switch ($oldwrappedquestion->qtype) {
111                                 case 'multichoice':
112                                  $DB->delete_records('question_multichoice', array('question' => $oldwrappedquestion->id));
113                                     break;
114                                 case 'shortanswer':
115                                  $DB->delete_records('question_shortanswer', array('question' => $oldwrappedquestion->id));
116                                     break;
117                                 case 'numerical':
118                                  $DB->delete_records('question_numerical', array('question' => $oldwrappedquestion->id));
119                                     break;
120                                 default:
121                                 print_error('qtypenotrecognized', 'qtype_multianswer','',$oldwrappedquestion->qtype);
122                                         $wrapped->id = 0 ;
123                         }
124                     }
125                 }else {
126                     $wrapped->id = 0 ;
127                 }
128             }
129             $wrapped->name = $question->name;
130             $wrapped->parent = $question->id;
131             $previousid = $wrapped->id ;
132             $wrapped->category = $question->category . ',1'; // save_question strips this extra bit off again.
133             $wrapped = $QTYPES[$wrapped->qtype]->save_question($wrapped, clone($wrapped));
134             $sequence[] = $wrapped->id;
135             if ($previousid != 0 && $previousid != $wrapped->id ) {
136                 // for some reasons a new question has been created
137                 // so delete the old one
138                 delete_question($previousid) ;
139             }
140         }
142         // Delete redundant wrapped questions
143         if(is_array($oldwrappedquestions) && count($oldwrappedquestions)){
144             foreach ($oldwrappedquestions as $oldwrappedquestion) {
145                 delete_question($oldwrappedquestion->id) ;
146             }
147         }
149         if (!empty($sequence)) {
150             $multianswer = new stdClass;
151             $multianswer->question = $question->id;
152             $multianswer->sequence = implode(',', $sequence);
153             if ($oldid = $DB->get_field('question_multianswer', 'id', array('question' => $question->id))) {
154                 $multianswer->id = $oldid;
155                 $DB->update_record("question_multianswer", $multianswer);
156             } else {
157                 $DB->insert_record("question_multianswer", $multianswer);
158             }
159         }
160     }
162     function save_question($authorizedquestion, $form) {
163         $question = qtype_multianswer_extract_question($form->questiontext);
164         if (isset($authorizedquestion->id)) {
165             $question->id = $authorizedquestion->id;
166         }
168         $question->category = $authorizedquestion->category;
169         $form->course = $course; // To pass the course object to
170                                  // save_question_options, where it is
171                                  // needed to call type specific
172                                  // save_question methods.
173         $form->defaultgrade = $question->defaultgrade;
174         $form->questiontext = $question->questiontext;
175         $form->questiontextformat = 0;
176         $form->options = clone($question->options);
177         unset($question->options);
178         return parent::save_question($question, $form);
179     }
181     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
182         $state->responses = array();
183         foreach ($question->options->questions as $key => $wrapped) {
184             $state->responses[$key] = '';
185         }
186         return true;
187     }
189     function restore_session_and_responses(&$question, &$state) {
190         $responses = explode(',', $state->responses['']);
191         $state->responses = array();
192         foreach ($responses as $response) {
193             $tmp = explode("-", $response);
194             // restore encoded characters
195             $state->responses[$tmp[0]] = str_replace(array("&#0044;", "&#0045;"),
196                     array(",", "-"), $tmp[1]);
197         }
198         return true;
199     }
201     function save_session_and_responses(&$question, &$state) {
202         global $DB;
203         $responses = $state->responses;
204         // encode - (hyphen) and , (comma) to &#0045; because they are used as
205         // delimiters
206         array_walk($responses, create_function('&$val, $key',
207                 '$val = str_replace(array(",", "-"), array("&#0044;", "&#0045;"), $val);
208                 $val = "$key-$val";'));
209         $responses = implode(',', $responses);
211         // Set the legacy answer field
212         $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id));
213         return true;
214     }
216     function delete_question($questionid, $contextid) {
217         global $DB;
218         $DB->delete_records("question_multianswer", array("question" => $questionid));
220         parent::delete_question($questionid, $contextid);
221     }
223     function get_correct_responses(&$question, &$state) {
224         global $QTYPES;
225         $responses = array();
226         foreach($question->options->questions as $key => $wrapped) {
227             if (!empty($wrapped)){
228                 if ($correct = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
229                     $responses[$key] = $correct[''];
230                 } else {
231                     // if there is no correct answer to this subquestion then there
232                     // can not be a correct answer to the whole question either, so
233                     // we have to return null.
234                     return null;
235                 }
236             }
237         }
238         return $responses;
239     }
241     function get_possible_responses(&$question) {
242         global $QTYPES;
243         $responses = array();
244         foreach($question->options->questions as $key => $wrapped) {
245             if (!empty($wrapped)){
246                 if ($correct = $QTYPES[$wrapped->qtype]->get_possible_responses($wrapped)) {
247                     $responses += $correct;
248                 } else {
249                     // if there is no correct answer to this subquestion then there
250                     // can not be a correct answer to the whole question either, so
251                     // we have to return null.
252                     return null;
253                 }
254             }
255         }
256         return $responses;
257     }
258     function get_actual_response_details($question, $state){
259         global $QTYPES;
260         $details = array();
261         foreach($question->options->questions as $key => $wrapped) {
262             if (!empty($wrapped)){
263                 $stateforquestion = clone($state);
264                 $stateforquestion->responses[''] = $state->responses[$key];
265                 $details = array_merge($details, $QTYPES[$wrapped->qtype]->get_actual_response_details($wrapped, $stateforquestion));
266             }
267         }
268         return $details;
269     }
271     function get_html_head_contributions(&$question, &$state) {
272         global $PAGE;
273         parent::get_html_head_contributions($question, $state);
274         $PAGE->requires->js('/lib/overlib/overlib.js', true);
275         $PAGE->requires->js('/lib/overlib/overlib_cssstyle.js', true);
276     }
278     function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
279         global $QTYPES, $CFG, $USER, $OUTPUT, $PAGE;
281         $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
282         $disabled = empty($options->readonly) ? '' : 'disabled="disabled"';
283         $formatoptions = new stdClass;
284         $formatoptions->noclean = true;
285         $formatoptions->para = false;
286         $nameprefix = $question->name_prefix;
288         // adding an icon with alt to warn user this is a fill in the gap question
289         // MDL-7497
290         if (!empty($USER->screenreader)) {
291             echo "<img src=\"".$OUTPUT->pix_url('icon', 'qtype_'.$question->qtype)."\" ".
292                 "class=\"icon\" alt=\"".get_string('clozeaid','qtype_multichoice')."\" />  ";
293         }
295         echo '<div class="ablock clearfix">';
297         $qtextremaining = format_text($question->questiontext,
298                 $question->questiontextformat, $formatoptions, $cmoptions->course);
300         $strfeedback = get_string('feedback', 'quiz');
302         // The regex will recognize text snippets of type {#X}
303         // where the X can be any text not containg } or white-space characters.
304         while (preg_match('~\{#([^[:space:]}]*)}~', $qtextremaining, $regs)) {
305             $qtextsplits = explode($regs[0], $qtextremaining, 2);
306             echo $qtextsplits[0];
307             echo "<label>"; // MDL-7497
308             $qtextremaining = $qtextsplits[1];
310             $positionkey = $regs[1];
311             if (isset($question->options->questions[$positionkey]) && $question->options->questions[$positionkey] != ''){
312             $wrapped = &$question->options->questions[$positionkey];
313             $answers = &$wrapped->options->answers;
314            // $correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state);
316             $inputname = $nameprefix.$positionkey;
317             if (isset($state->responses[$positionkey])) {
318                 $response = $state->responses[$positionkey];
319             } else {
320                 $response = null;
321             }
322             //   echo "<p> multianswer positionkey $positionkey response $response state  <pre>";print_r($state);echo "</pre></p>";
324             // Determine feedback popup if any
325             $popup = '';
326             $style = '';
327             $feedbackimg = '';
328             $feedback = '' ;
329             $correctanswer = '';
330             $strfeedbackwrapped  = $strfeedback;
331                 $testedstate = clone($state);
332                 if ($correctanswers =  $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
333                     if ($options->readonly && $options->correct_responses) {
334                         $delimiter = '';
335                         if ($correctanswers) {
336                             foreach ($correctanswers as $ca) {
337                                 switch($wrapped->qtype){
338                                     case 'numerical':
339                                     case 'shortanswer':
340                                         $correctanswer .= $delimiter.$ca;
341                                         break ;
342                                     case 'multichoice':
343                                         if (isset($answers[$ca])){
344                                             $correctanswer .= $delimiter.$answers[$ca]->answer;
345                                         }
346                                         break ;
347                                 }
348                                 $delimiter = ', ';
349                             }
350                         }
351                     }
352                     if ($correctanswer != '' ) {
353                         $feedback = '<div class="correctness">';
354                         $feedback .= get_string('correctansweris', 'quiz', s($correctanswer));
355                         $feedback .= '</div>';
356                     }
357                 }
359             if ($options->feedback) {
360                 $chosenanswer = null;
361                 switch ($wrapped->qtype) {
362                     case 'numerical':
363                     case 'shortanswer':
364                         $testedstate = clone($state);
365                         $testedstate->responses[''] = $response;
366                         foreach ($answers as $answer) {
367                             if($QTYPES[$wrapped->qtype]
368                                     ->test_response($wrapped, $testedstate, $answer)) {
369                                 $chosenanswer = clone($answer);
370                                 break;
371                             }
372                         }
373                         break;
374                     case 'multichoice':
375                         if (isset($answers[$response])) {
376                             $chosenanswer = clone($answers[$response]);
377                         }
378                         break;
379                     default:
380                         break;
381                 }
383                 // Set up a default chosenanswer so that all non-empty wrong
384                 // answers are highlighted red
385                 if (empty($chosenanswer) && $response != '') {
386                     $chosenanswer = new stdClass;
387                     $chosenanswer->fraction = 0.0;
388                 }
390                 if (!empty($chosenanswer->feedback)) {
391                     $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback.$chosenanswer->feedback));
392                     if  ($options->readonly && $options->correct_responses) {
393                         $strfeedbackwrapped = get_string('correctanswerandfeedback', 'qtype_multianswer');
394                     }else {
395                         $strfeedbackwrapped = get_string('feedback', 'quiz');
396                     }
397                     $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
398                              " onmouseout=\"return nd();\" ";
399                 }
401                 /// Determine style
402                 if ($options->feedback && $response != '') {
403                     $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction).'"';
404                     $feedbackimg = question_get_feedback_image($chosenanswer->fraction);
405                 } else {
406                     $style = '';
407                     $feedbackimg = '';
408                 }
409             }
410             if ($feedback !='' && $popup == ''){
411                 $strfeedbackwrapped = get_string('correctanswer', 'qtype_multianswer');
412                     $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback));
413                     $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
414                              " onmouseout=\"return nd();\" ";
415             }
417             // Print the input control
418             switch ($wrapped->qtype) {
419                 case 'shortanswer':
420                 case 'numerical':
421                     $size = 1 ;
422                     foreach ($answers as $answer) {
423                         if (strlen(trim($answer->answer)) > $size ){
424                             $size = strlen(trim($answer->answer));
425                         }
426                     }
427                     if (strlen(trim($response))> $size ){
428                             $size = strlen(trim($response))+1;
429                     }
430                     $size = $size + rand(0,$size*0.15);
431                     $size > 60 ? $size = 60 : $size = $size;
432                     $styleinfo = "size=\"$size\"";
433                     /**
434                     * Uncomment the following lines if you want to limit for small sizes.
435                     * Results may vary with browsers see MDL-3274
436                     */
437                     /*
438                     if ($size < 2) {
439                         $styleinfo = 'style="width: 1.1em;"';
440                     }
441                     if ($size == 2) {
442                         $styleinfo = 'style="width: 1.9em;"';
443                     }
444                     if ($size == 3) {
445                         $styleinfo = 'style="width: 2.3em;"';
446                     }
447                     if ($size == 4) {
448                         $styleinfo = 'style="width: 2.8em;"';
449                     }
450                     */
452                     echo "<input $style $readonly $popup name=\"$inputname\"";
453                     echo "  type=\"text\" value=\"".s($response)."\" ".$styleinfo." /> ";
454                     if (!empty($feedback) && !empty($USER->screenreader)) {
455                         echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
456                     }
457                     echo $feedbackimg;
458                     break;
459                 case 'multichoice':
460                  if ($wrapped->options->layout == 0 ){
461                       $outputoptions = '<option></option>'; // Default empty option
462                       foreach ($answers as $mcanswer) {
463                         $selected = '';
464                         if ($response == $mcanswer->id) {
465                             $selected = ' selected="selected"';
466                         }
467                         $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
468                                 s($mcanswer->answer) . '</option>';
469                         }
470                         // In the next line, $readonly is invalid HTML, but it works in
471                         // all browsers. $disabled would be valid, but then the JS for
472                         // displaying the feedback does not work. Of course, we should
473                         // not be relying on JS (for accessibility reasons), but that is
474                         // a bigger problem.
475                         //
476                         // The span is used for safari, which does not allow styling of
477                         // selects.
478                         echo "<span $style><select $popup $readonly $style name=\"$inputname\">";
479                         echo $outputoptions;
480                         echo '</select></span>';
481                         if (!empty($feedback) && !empty($USER->screenreader)) {
482                             echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
483                         }
484                         echo $feedbackimg;
485                     }else if ($wrapped->options->layout == 1 || $wrapped->options->layout == 2){
486                         $ordernumber=0;
487                         $anss =  Array();
488                         foreach ($answers as $mcanswer) {
489                             $ordernumber++;
490                             $checked = '';
491                             $chosen = false;
492                             $type = 'type="radio"';
493                             $name   = "name=\"{$inputname}\"";
494                             if ($response == $mcanswer->id) {
495                                 $checked = 'checked="checked"';
496                                 $chosen = true;
497                             }
498                             $a = new stdClass;
499                             $a->id   = $question->name_prefix . $mcanswer->id;
500                             $a->class = '';
501                             $a->feedbackimg = '';
503                     // Print the control
504                     $a->control = "<input $readonly id=\"$a->id\" $name $checked $type value=\"$mcanswer->id\" />";
505                 if ($options->correct_responses && $mcanswer->fraction > 0) {
506                     $a->class = question_get_feedback_class(1);
507                 }
508                 if (($options->feedback && $chosen) || $options->correct_responses) {
509                     if ($type == ' type="checkbox" ') {
510                         $a->feedbackimg = question_get_feedback_image($mcanswer->fraction > 0 ? 1 : 0, $chosen && $options->feedback);
511                     } else {
512                         $a->feedbackimg = question_get_feedback_image($mcanswer->fraction, $chosen && $options->feedback);
513                     }
514                 }
516                 // Print the answer text: no automatic numbering
518                 $a->text = format_text($mcanswer->answer, $mcanswer->answerformat, $formatoptions, $cmoptions->course);
520                 // Print feedback if feedback is on
521                 if (($options->feedback || $options->correct_responses) && ($checked )) { //|| $options->readonly
522                     $a->feedback = format_text($mcanswer->feedback, $mcanswer->feedbackformat, $formatoptions, $cmoptions->course);
523                 } else {
524                     $a->feedback = '';
525                 }
527                     $anss[] = clone($a);
528                 }
529                 ?>
530             <?php    if ($wrapped->options->layout == 1 ){
531             ?>
532                   <table class="answer">
533                     <?php $row = 1; foreach ($anss as $answer) { ?>
534                       <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
535                         <td class="c0 control">
536                           <?php echo $answer->control; ?>
537                         </td>
538                         <td class="c1 text <?php echo $answer->class ?>">
539                           <label for="<?php echo $answer->id ?>">
540                             <?php echo $answer->text; ?>
541                             <?php echo $answer->feedbackimg; ?>
542                           </label>
543                         </td>
544                         <td class="c0 feedback">
545                           <?php echo $answer->feedback; ?>
546                         </td>
547                       </tr>
548                     <?php } ?>
549                   </table>
550                   <?php }else  if ($wrapped->options->layout == 2 ){
551                     ?>
553                   <table class="answer">
554                       <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
555                     <?php $row = 1; foreach ($anss as $answer) { ?>
556                         <td class="c0 control">
557                           <?php echo $answer->control; ?>
558                         </td>
559                         <td class="c1 text <?php echo $answer->class ?>">
560                           <label for="<?php echo $answer->id ?>">
561                             <?php echo $answer->text; ?>
562                             <?php echo $answer->feedbackimg; ?>
563                           </label>
564                         </td>
565                         <td class="c0 feedback">
566                           <?php echo $answer->feedback; ?>
567                         </td>
568                     <?php } ?>
569                       </tr>
570                   </table>
571                   <?php }
573                     }else {
574                         echo "no valid layout";
575                     }
577                     break;
578                 default:
579                     $a = new stdClass;
580                     $a->type = $wrapped->qtype ;
581                     $a->sub = $positionkey;
582                     print_error('unknownquestiontypeofsubquestion', 'qtype_multianswer','',$a);
583                     break;
584            }
585            echo "</label>"; // MDL-7497
586         }
587         else {
588             if(!  isset($question->options->questions[$positionkey])){
589                 echo $regs[0]."</label>";
590             }else {
591                 echo '</label><div class="error" >'.get_string('questionnotfound','qtype_multianswer',$positionkey).'</div>';
592             }
593        }
594     }
596         // Print the final piece of question text:
597         echo $qtextremaining;
598         $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
599         echo '</div>';
600     }
602     function grade_responses(&$question, &$state, $cmoptions) {
603         global $QTYPES;
604         $teststate = clone($state);
605         $state->raw_grade = 0;
606         foreach($question->options->questions as $key => $wrapped) {
607             if (!empty($wrapped)){
608                 if(isset($state->responses[$key])){
609                     $state->responses[$key] = $state->responses[$key];
610                 }else {
611                     $state->responses[$key] = '' ;
612                 }
613                 $teststate->responses = array('' => $state->responses[$key]);
614                 $teststate->raw_grade = 0;
615                 if (false === $QTYPES[$wrapped->qtype]
616                  ->grade_responses($wrapped, $teststate, $cmoptions)) {
617                     return false;
618                 }
619                 $state->raw_grade += $teststate->raw_grade;
620             }
621         }
622         $state->raw_grade /= $question->defaultgrade;
623         $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0)
624          * $question->maxgrade;
626         if (empty($state->raw_grade)) {
627             $state->raw_grade = 0.0;
628         }
629         $state->penalty = $question->penalty * $question->maxgrade;
631         // mark the state as graded
632         $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
634         return true;
635     }
637     function get_actual_response($question, $state) {
638         global $QTYPES;
639         $teststate = clone($state);
640         foreach($question->options->questions as $key => $wrapped) {
641             $state->responses[$key] = html_entity_decode($state->responses[$key]);
642             $teststate->responses = array('' => $state->responses[$key]);
643             $correct = $QTYPES[$wrapped->qtype]
644              ->get_actual_response($wrapped, $teststate);
645             $responses[$key] = implode(';', $correct);
646         }
647         return $responses;
648     }
650     /**
651      * @param object $question
652      * @return mixed either a integer score out of 1 that the average random
653      * guess by a student might give or an empty string which means will not
654      * calculate.
655      */
656     function get_random_guess_score($question) {
657         $totalfraction = 0;
658         foreach (array_keys($question->options->questions) as $key){
659             $totalfraction += question_get_random_guess_score($question->options->questions[$key]);
660         }
661         return $totalfraction / count($question->options->questions);
662     }
664     /**
665      * Runs all the code required to set up and save an essay question for testing purposes.
666      * Alternate DB table prefix may be used to facilitate data deletion.
667      */
668     function generate_test($name, $courseid = null) {
669         global $DB;
670         list($form, $question) = parent::generate_test($name, $courseid);
671         $question->category = $form->category;
672         $form->questiontext = "This question consists of some text with an answer embedded right here {1:MULTICHOICE:Wrong answer#Feedback for this wrong answer~Another wrong answer#Feedback for the other wrong answer~=Correct answer#Feedback for correct answer~%50%Answer that gives half the credit#Feedback for half credit answer} and right after that you will have to deal with this short answer {1:SHORTANSWER:Wrong answer#Feedback for this wrong answer~=Correct answer#Feedback for correct answer~%50%Answer that gives half the credit#Feedback for half credit answer} and finally we have a floating point number {2:NUMERICAL:=23.8:0.1#Feedback for correct answer 23.8~%50%23.8:2#Feedback for half credit answer in the nearby region of the correct answer}.
674 Note that addresses like www.moodle.org and smileys :-) all work as normal:
675  a) How good is this? {:MULTICHOICE:=Yes#Correct~No#We have a different opinion}
676  b) What grade would you give it? {3:NUMERICAL:=3:2}
678 Good luck!
679 ";
680         $form->feedback = "feedback";
681         $form->generalfeedback = "General feedback";
682         $form->fraction = 0;
683         $form->penalty = 0.1;
684         $form->versioning = 0;
686         if ($courseid) {
687             $course = $DB->get_record('course', array('id' => $courseid));
688         }
690         return $this->save_question($question, $form);
691     }
694 //// END OF CLASS ////
697 //////////////////////////////////////////////////////////////////////////
698 //// INITIATION - Without this line the question type is not in use... ///
699 //////////////////////////////////////////////////////////////////////////
700 question_register_questiontype(new embedded_cloze_qtype());
702 /////////////////////////////////////////////////////////////
703 //// ADDITIONAL FUNCTIONS
704 //// The functions below deal exclusivly with editing
705 //// of questions with question type 'multianswer'.
706 //// Therefore they are kept in this file.
707 //// They are not in the class as they are not
708 //// likely to be subject for overriding.
709 /////////////////////////////////////////////////////////////
711 // ANSWER_ALTERNATIVE regexes
712 define("ANSWER_ALTERNATIVE_FRACTION_REGEX",
713        '=|%(-?[0-9]+)%');
714 // for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
715 define("ANSWER_ALTERNATIVE_ANSWER_REGEX",
716         '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
717 define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX",
718         '.*?(?<!\\\\)(?=[~}]|$)');
719 define("ANSWER_ALTERNATIVE_REGEX",
720        '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
721        '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
722        '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
724 // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
725 define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2);
726 define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1);
727 define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3);
728 define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5);
730 // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
731 // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
732 define("NUMBER_REGEX",
733         '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
734 define("NUMERICAL_ALTERNATIVE_REGEX",
735         '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
737 // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
738 define("NUMERICAL_CORRECT_ANSWER", 1);
739 define("NUMERICAL_ABS_ERROR_MARGIN", 6);
741 // Remaining ANSWER regexes
742 define("ANSWER_TYPE_DEF_REGEX",
743        '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)');
744 define("ANSWER_START_REGEX",
745        '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
747 define("ANSWER_REGEX",
748         ANSWER_START_REGEX
749         . '(' . ANSWER_ALTERNATIVE_REGEX
750         . '(~'
751         . ANSWER_ALTERNATIVE_REGEX
752         . ')*)\}' );
754 // Parenthesis positions for singulars in ANSWER_REGEX
755 define("ANSWER_REGEX_NORM", 1);
756 define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3);
757 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4);
758 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR", 5);
759 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL", 6);
760 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 7);
761 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C", 8);
762 define("ANSWER_REGEX_ALTERNATIVES", 9);
764 function qtype_multianswer_extract_question($text) {
765     // $text is an array [text][format][itemid]
766     $question = new stdClass;
767     $question->qtype = 'multianswer';
768     $question->questiontext = $text;
769     $question->generalfeedback['text'] = '';
770     $question->generalfeedback['format'] = '1';
771     $question->generalfeedback['itemid'] = '';
772     
773     $question->options->questions = array();    
774     $question->defaultgrade = 0; // Will be increased for each answer norm
776     for ($positionkey=1; preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs); ++$positionkey ) {
777         $wrapped = new stdClass;
778         $wrapped->generalfeedback['text'] = '';
779         $wrapped->generalfeedback['format'] = '1';
780         $wrapped->generalfeedback['itemid'] = '';
781         if (isset($answerregs[ANSWER_REGEX_NORM])&& $answerregs[ANSWER_REGEX_NORM]!== ''){
782             $wrapped->defaultgrade = $answerregs[ANSWER_REGEX_NORM];
783         } else {
784             $wrapped->defaultgrade = '1';
785         }
786         if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
787             $wrapped->qtype = 'numerical';
788             $wrapped->multiplier = array();
789             $wrapped->units      = array();
790             $wrapped->instructions['text'] = '';
791             $wrapped->instructions['format'] = '1';
792             $wrapped->instructions['itemid'] = '';
793         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
794             $wrapped->qtype = 'shortanswer';
795             $wrapped->usecase = 0;
796         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
797             $wrapped->qtype = 'shortanswer';
798             $wrapped->usecase = 1;
799         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
800             $wrapped->qtype = 'multichoice';
801             $wrapped->single = 1;
802             $wrapped->answernumbering = 0;
803             $wrapped->correctfeedback['text'] = '';
804             $wrapped->correctfeedback['format'] = '1';
805             $wrapped->correctfeedback['itemid'] = '';
806             $wrapped->partiallycorrectfeedback['text'] = '';
807             $wrapped->partiallycorrectfeedback['format'] = '1';
808             $wrapped->partiallycorrectfeedback['itemid'] = '';
809             $wrapped->incorrectfeedback['text'] = '';
810             $wrapped->incorrectfeedback['format'] = '1';
811             $wrapped->incorrectfeedback['itemid'] = '';
812             $wrapped->layout = 0;
813         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
814             $wrapped->qtype = 'multichoice';
815             $wrapped->single = 1;
816             $wrapped->answernumbering = 0;
817             $wrapped->correctfeedback['text'] = '';
818             $wrapped->correctfeedback['format'] = '1';
819             $wrapped->correctfeedback['itemid'] = '';
820             $wrapped->partiallycorrectfeedback['text'] = '';
821             $wrapped->partiallycorrectfeedback['format'] = '1';
822             $wrapped->partiallycorrectfeedback['itemid'] = '';
823             $wrapped->incorrectfeedback['text'] = '';
824             $wrapped->incorrectfeedback['format'] = '1';
825             $wrapped->incorrectfeedback['itemid'] = '';
826             $wrapped->layout = 1;
827         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
828             $wrapped->qtype = 'multichoice';
829             $wrapped->single = 1;
830             $wrapped->answernumbering = 0;
831             $wrapped->correctfeedback['text'] = '';
832             $wrapped->correctfeedback['format'] = '1';
833             $wrapped->correctfeedback['itemid'] = '';
834             $wrapped->partiallycorrectfeedback['text'] = '';
835             $wrapped->partiallycorrectfeedback['format'] = '1';
836             $wrapped->partiallycorrectfeedback['itemid'] = '';
837             $wrapped->incorrectfeedback['text'] = '';
838             $wrapped->incorrectfeedback['format'] = '1';
839             $wrapped->incorrectfeedback['itemid'] = '';
840             $wrapped->layout = 2;
841         } else {
842             print_error('unknownquestiontype', 'question', '', $answerregs[2]);
843             return false;
844         }
846         // Each $wrapped simulates a $form that can be processed by the
847         // respective save_question and save_question_options methods of the
848         // wrapped questiontypes
849         $wrapped->answer   = array();
850         $wrapped->fraction = array();
851         $wrapped->feedback = array();
852         $wrapped->shuffleanswers = 1;
853         $wrapped->questiontext['text'] = $answerregs[0];
854         $wrapped->questiontext['format'] = 0 ;
855         $wrapped->questiontext['itemid'] = '' ;
856         $answerindex = 0 ;
858         $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
859         while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
860             if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
861                 $wrapped->fraction["$answerindex"] = '1';
862             } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]){
863                 $wrapped->fraction["$answerindex"] = .01 * $percentile;
864             } else {
865                 $wrapped->fraction["$answerindex"] = '0';
866             }
867             if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
868                 $feedback = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
869                 $feedback = str_replace('\}', '}', $feedback);
870                 $wrapped->feedback["$answerindex"]['text'] = str_replace('\#', '#', $feedback);
871                 $wrapped->feedback["$answerindex"]['format'] = '1';
872                 $wrapped->feedback["$answerindex"]['itemid'] = '';
873             } else {
874                 $wrapped->feedback["$answerindex"]['text'] = '';
875                 $wrapped->feedback["$answerindex"]['format'] = '1';
876                 $wrapped->feedback["$answerindex"]['itemid'] = '1';
878             }
879             if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
880                     && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~', $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
881                 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
882                 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
883                     $wrapped->tolerance["$answerindex"] =
884                     $numregs[NUMERICAL_ABS_ERROR_MARGIN];
885                 } else {
886                     $wrapped->tolerance["$answerindex"] = 0;
887                 }
888             } else { // Tolerance can stay undefined for non numerical questions
889                 // Undo quoting done by the HTML editor.
890                 $answer = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
891                 $answer = str_replace('\}', '}', $answer);
892                 $wrapped->answer["$answerindex"] = str_replace('\#', '#', $answer);
893             }
894             $tmp = explode($altregs[0], $remainingalts, 2);
895             $remainingalts = $tmp[1];
896             $answerindex++ ;
897         }
899         $question->defaultgrade += $wrapped->defaultgrade;
900         $question->options->questions[$positionkey] = clone($wrapped);
901         $question->questiontext['text'] = implode("{#$positionkey}",
902                     explode($answerregs[0], $question->questiontext['text'], 2));
903 //    echo"<p>questiontext 2 <pre>";print_r($question->questiontext);echo"<pre></p>";
904     }
905 //    echo"<p>questiontext<pre>";print_r($question->questiontext);echo"<pre></p>";
906     $question->questiontext = $question->questiontext;
907 //    echo"<p>question<pre>";print_r($question);echo"<pre></p>";
908     return $question;