1c713f2006421e493505122ae1721de5fd5a1557
[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         static $overlibdivoutput = false;
282         if (!$overlibdivoutput) {
283             echo '<div id="overDiv" style="position:absolute; visibility:hidden; z-index:1000;"></div>'; // for overlib
284             $overlibdivoutput = true;
285         }
287         $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
288         $disabled = empty($options->readonly) ? '' : 'disabled="disabled"';
289         $formatoptions = new stdClass;
290         $formatoptions->noclean = true;
291         $formatoptions->para = false;
292         $nameprefix = $question->name_prefix;
294         // adding an icon with alt to warn user this is a fill in the gap question
295         // MDL-7497
296         if (!empty($USER->screenreader)) {
297             echo "<img src=\"".$OUTPUT->pix_url('icon', 'qtype_'.$question->qtype)."\" ".
298                 "class=\"icon\" alt=\"".get_string('clozeaid','qtype_multichoice')."\" />  ";
299         }
301         echo '<div class="ablock clearfix">';
303         $qtextremaining = format_text($question->questiontext,
304                 $question->questiontextformat, $formatoptions, $cmoptions->course);
306         $strfeedback = get_string('feedback', 'quiz');
308         // The regex will recognize text snippets of type {#X}
309         // where the X can be any text not containg } or white-space characters.
310         while (preg_match('~\{#([^[:space:]}]*)}~', $qtextremaining, $regs)) {
311             $qtextsplits = explode($regs[0], $qtextremaining, 2);
312             echo $qtextsplits[0];
313             echo "<label>"; // MDL-7497
314             $qtextremaining = $qtextsplits[1];
316             $positionkey = $regs[1];
317             if (isset($question->options->questions[$positionkey]) && $question->options->questions[$positionkey] != ''){
318             $wrapped = &$question->options->questions[$positionkey];
319             $answers = &$wrapped->options->answers;
320            // $correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state);
322             $inputname = $nameprefix.$positionkey;
323             if (isset($state->responses[$positionkey])) {
324                 $response = $state->responses[$positionkey];
325             } else {
326                 $response = null;
327             }
328             //   echo "<p> multianswer positionkey $positionkey response $response state  <pre>";print_r($state);echo "</pre></p>";
330             // Determine feedback popup if any
331             $popup = '';
332             $style = '';
333             $feedbackimg = '';
334             $feedback = '' ;
335             $correctanswer = '';
336             $strfeedbackwrapped  = $strfeedback;
337                 $testedstate = clone($state);
338                 if ($correctanswers =  $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
339                     if ($options->readonly && $options->correct_responses) {
340                         $delimiter = '';
341                         if ($correctanswers) {
342                             foreach ($correctanswers as $ca) {
343                                 switch($wrapped->qtype){
344                                     case 'numerical':
345                                     case 'shortanswer':
346                                         $correctanswer .= $delimiter.$ca;
347                                         break ;
348                                     case 'multichoice':
349                                         if (isset($answers[$ca])){
350                                             $correctanswer .= $delimiter.$answers[$ca]->answer;
351                                         }
352                                         break ;
353                                 }
354                                 $delimiter = ', ';
355                             }
356                         }
357                     }
358                     if ($correctanswer != '' ) {
359                         $feedback = '<div class="correctness">';
360                         $feedback .= get_string('correctansweris', 'quiz', s($correctanswer));
361                         $feedback .= '</div>';
362                     }
363                 }
365             if ($options->feedback) {
366                 $chosenanswer = null;
367                 switch ($wrapped->qtype) {
368                     case 'numerical':
369                     case 'shortanswer':
370                         $testedstate = clone($state);
371                         $testedstate->responses[''] = $response;
372                         foreach ($answers as $answer) {
373                             if($QTYPES[$wrapped->qtype]
374                                     ->test_response($wrapped, $testedstate, $answer)) {
375                                 $chosenanswer = clone($answer);
376                                 break;
377                             }
378                         }
379                         break;
380                     case 'multichoice':
381                         if (isset($answers[$response])) {
382                             $chosenanswer = clone($answers[$response]);
383                         }
384                         break;
385                     default:
386                         break;
387                 }
389                 // Set up a default chosenanswer so that all non-empty wrong
390                 // answers are highlighted red
391                 if (empty($chosenanswer) && $response != '') {
392                     $chosenanswer = new stdClass;
393                     $chosenanswer->fraction = 0.0;
394                 }
396                 if (!empty($chosenanswer->feedback)) {
397                     $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback.$chosenanswer->feedback));
398                     if  ($options->readonly && $options->correct_responses) {
399                         $strfeedbackwrapped = get_string('correctanswerandfeedback', 'qtype_multianswer');
400                     }else {
401                         $strfeedbackwrapped = get_string('feedback', 'quiz');
402                     }
403                     $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
404                              " onmouseout=\"return nd();\" ";
405                 }
407                 /// Determine style
408                 if ($options->feedback && $response != '') {
409                     $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction).'"';
410                     $feedbackimg = question_get_feedback_image($chosenanswer->fraction);
411                 } else {
412                     $style = '';
413                     $feedbackimg = '';
414                 }
415             }
416             if ($feedback !='' && $popup == ''){
417                 $strfeedbackwrapped = get_string('correctanswer', 'qtype_multianswer');
418                     $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback));
419                     $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
420                              " onmouseout=\"return nd();\" ";
421             }
423             // Print the input control
424             switch ($wrapped->qtype) {
425                 case 'shortanswer':
426                 case 'numerical':
427                     $size = 1 ;
428                     foreach ($answers as $answer) {
429                         if (strlen(trim($answer->answer)) > $size ){
430                             $size = strlen(trim($answer->answer));
431                         }
432                     }
433                     if (strlen(trim($response))> $size ){
434                             $size = strlen(trim($response))+1;
435                     }
436                     $size = $size + rand(0,$size*0.15);
437                     $size > 60 ? $size = 60 : $size = $size;
438                     $styleinfo = "size=\"$size\"";
439                     /**
440                     * Uncomment the following lines if you want to limit for small sizes.
441                     * Results may vary with browsers see MDL-3274
442                     */
443                     /*
444                     if ($size < 2) {
445                         $styleinfo = 'style="width: 1.1em;"';
446                     }
447                     if ($size == 2) {
448                         $styleinfo = 'style="width: 1.9em;"';
449                     }
450                     if ($size == 3) {
451                         $styleinfo = 'style="width: 2.3em;"';
452                     }
453                     if ($size == 4) {
454                         $styleinfo = 'style="width: 2.8em;"';
455                     }
456                     */
458                     echo "<input $style $readonly $popup name=\"$inputname\"";
459                     echo "  type=\"text\" value=\"".s($response)."\" ".$styleinfo." /> ";
460                     if (!empty($feedback) && !empty($USER->screenreader)) {
461                         echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
462                     }
463                     echo $feedbackimg;
464                     break;
465                 case 'multichoice':
466                  if ($wrapped->options->layout == 0 ){
467                       $outputoptions = '<option></option>'; // Default empty option
468                       foreach ($answers as $mcanswer) {
469                         $selected = '';
470                         if ($response == $mcanswer->id) {
471                             $selected = ' selected="selected"';
472                         }
473                         $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
474                                 s($mcanswer->answer) . '</option>';
475                         }
476                         // In the next line, $readonly is invalid HTML, but it works in
477                         // all browsers. $disabled would be valid, but then the JS for
478                         // displaying the feedback does not work. Of course, we should
479                         // not be relying on JS (for accessibility reasons), but that is
480                         // a bigger problem.
481                         //
482                         // The span is used for safari, which does not allow styling of
483                         // selects.
484                         echo "<span $style><select $popup $readonly $style name=\"$inputname\">";
485                         echo $outputoptions;
486                         echo '</select></span>';
487                         if (!empty($feedback) && !empty($USER->screenreader)) {
488                             echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
489                         }
490                         echo $feedbackimg;
491                     }else if ($wrapped->options->layout == 1 || $wrapped->options->layout == 2){
492                         $ordernumber=0;
493                         $anss =  Array();
494                         foreach ($answers as $mcanswer) {
495                             $ordernumber++;
496                             $checked = '';
497                             $chosen = false;
498                             $type = 'type="radio"';
499                             $name   = "name=\"{$inputname}\"";
500                             if ($response == $mcanswer->id) {
501                                 $checked = 'checked="checked"';
502                                 $chosen = true;
503                             }
504                             $a = new stdClass;
505                             $a->id   = $question->name_prefix . $mcanswer->id;
506                             $a->class = '';
507                             $a->feedbackimg = '';
509                     // Print the control
510                     $a->control = "<input $readonly id=\"$a->id\" $name $checked $type value=\"$mcanswer->id\" />";
511                 if ($options->correct_responses && $mcanswer->fraction > 0) {
512                     $a->class = question_get_feedback_class(1);
513                 }
514                 if (($options->feedback && $chosen) || $options->correct_responses) {
515                     if ($type == ' type="checkbox" ') {
516                         $a->feedbackimg = question_get_feedback_image($mcanswer->fraction > 0 ? 1 : 0, $chosen && $options->feedback);
517                     } else {
518                         $a->feedbackimg = question_get_feedback_image($mcanswer->fraction, $chosen && $options->feedback);
519                     }
520                 }
522                 // Print the answer text: no automatic numbering
524                 $a->text = format_text($mcanswer->answer, $mcanswer->answerformat, $formatoptions, $cmoptions->course);
526                 // Print feedback if feedback is on
527                 if (($options->feedback || $options->correct_responses) && ($checked )) { //|| $options->readonly
528                     $a->feedback = format_text($mcanswer->feedback, $mcanswer->feedbackformat, $formatoptions, $cmoptions->course);
529                 } else {
530                     $a->feedback = '';
531                 }
533                     $anss[] = clone($a);
534                 }
535                 ?>
536             <?php    if ($wrapped->options->layout == 1 ){
537             ?>
538                   <table class="answer">
539                     <?php $row = 1; foreach ($anss as $answer) { ?>
540                       <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
541                         <td class="c0 control">
542                           <?php echo $answer->control; ?>
543                         </td>
544                         <td class="c1 text <?php echo $answer->class ?>">
545                           <label for="<?php echo $answer->id ?>">
546                             <?php echo $answer->text; ?>
547                             <?php echo $answer->feedbackimg; ?>
548                           </label>
549                         </td>
550                         <td class="c0 feedback">
551                           <?php echo $answer->feedback; ?>
552                         </td>
553                       </tr>
554                     <?php } ?>
555                   </table>
556                   <?php }else  if ($wrapped->options->layout == 2 ){
557                     ?>
559                   <table class="answer">
560                       <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
561                     <?php $row = 1; foreach ($anss as $answer) { ?>
562                         <td class="c0 control">
563                           <?php echo $answer->control; ?>
564                         </td>
565                         <td class="c1 text <?php echo $answer->class ?>">
566                           <label for="<?php echo $answer->id ?>">
567                             <?php echo $answer->text; ?>
568                             <?php echo $answer->feedbackimg; ?>
569                           </label>
570                         </td>
571                         <td class="c0 feedback">
572                           <?php echo $answer->feedback; ?>
573                         </td>
574                     <?php } ?>
575                       </tr>
576                   </table>
577                   <?php }
579                     }else {
580                         echo "no valid layout";
581                     }
583                     break;
584                 default:
585                     $a = new stdClass;
586                     $a->type = $wrapped->qtype ;
587                     $a->sub = $positionkey;
588                     print_error('unknownquestiontypeofsubquestion', 'qtype_multianswer','',$a);
589                     break;
590            }
591            echo "</label>"; // MDL-7497
592         }
593         else {
594             if(!  isset($question->options->questions[$positionkey])){
595                 echo $regs[0]."</label>";
596             }else {
597                 echo '</label><div class="error" >'.get_string('questionnotfound','qtype_multianswer',$positionkey).'</div>';
598             }
599        }
600     }
602         // Print the final piece of question text:
603         echo $qtextremaining;
604         $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
605         echo '</div>';
606     }
608     function grade_responses(&$question, &$state, $cmoptions) {
609         global $QTYPES;
610         $teststate = clone($state);
611         $state->raw_grade = 0;
612         foreach($question->options->questions as $key => $wrapped) {
613             if (!empty($wrapped)){
614                 if(isset($state->responses[$key])){
615                     $state->responses[$key] = $state->responses[$key];
616                 }else {
617                     $state->responses[$key] = '' ;
618                 }
619                 $teststate->responses = array('' => $state->responses[$key]);
620                 $teststate->raw_grade = 0;
621                 if (false === $QTYPES[$wrapped->qtype]
622                  ->grade_responses($wrapped, $teststate, $cmoptions)) {
623                     return false;
624                 }
625                 $state->raw_grade += $teststate->raw_grade;
626             }
627         }
628         $state->raw_grade /= $question->defaultgrade;
629         $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0)
630          * $question->maxgrade;
632         if (empty($state->raw_grade)) {
633             $state->raw_grade = 0.0;
634         }
635         $state->penalty = $question->penalty * $question->maxgrade;
637         // mark the state as graded
638         $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
640         return true;
641     }
643     function get_actual_response($question, $state) {
644         global $QTYPES;
645         $teststate = clone($state);
646         foreach($question->options->questions as $key => $wrapped) {
647             $state->responses[$key] = html_entity_decode($state->responses[$key]);
648             $teststate->responses = array('' => $state->responses[$key]);
649             $correct = $QTYPES[$wrapped->qtype]
650              ->get_actual_response($wrapped, $teststate);
651             $responses[$key] = implode(';', $correct);
652         }
653         return $responses;
654     }
656     /**
657      * @param object $question
658      * @return mixed either a integer score out of 1 that the average random
659      * guess by a student might give or an empty string which means will not
660      * calculate.
661      */
662     function get_random_guess_score($question) {
663         $totalfraction = 0;
664         foreach (array_keys($question->options->questions) as $key){
665             $totalfraction += question_get_random_guess_score($question->options->questions[$key]);
666         }
667         return $totalfraction / count($question->options->questions);
668     }
670     /**
671      * Runs all the code required to set up and save an essay question for testing purposes.
672      * Alternate DB table prefix may be used to facilitate data deletion.
673      */
674     function generate_test($name, $courseid = null) {
675         global $DB;
676         list($form, $question) = parent::generate_test($name, $courseid);
677         $question->category = $form->category;
678         $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}.
680 Note that addresses like www.moodle.org and smileys :-) all work as normal:
681  a) How good is this? {:MULTICHOICE:=Yes#Correct~No#We have a different opinion}
682  b) What grade would you give it? {3:NUMERICAL:=3:2}
684 Good luck!
685 ";
686         $form->feedback = "feedback";
687         $form->generalfeedback = "General feedback";
688         $form->fraction = 0;
689         $form->penalty = 0.1;
690         $form->versioning = 0;
692         if ($courseid) {
693             $course = $DB->get_record('course', array('id' => $courseid));
694         }
696         return $this->save_question($question, $form);
697     }
700 //// END OF CLASS ////
703 //////////////////////////////////////////////////////////////////////////
704 //// INITIATION - Without this line the question type is not in use... ///
705 //////////////////////////////////////////////////////////////////////////
706 question_register_questiontype(new embedded_cloze_qtype());
708 /////////////////////////////////////////////////////////////
709 //// ADDITIONAL FUNCTIONS
710 //// The functions below deal exclusivly with editing
711 //// of questions with question type 'multianswer'.
712 //// Therefore they are kept in this file.
713 //// They are not in the class as they are not
714 //// likely to be subject for overriding.
715 /////////////////////////////////////////////////////////////
717 // ANSWER_ALTERNATIVE regexes
718 define("ANSWER_ALTERNATIVE_FRACTION_REGEX",
719        '=|%(-?[0-9]+)%');
720 // for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
721 define("ANSWER_ALTERNATIVE_ANSWER_REGEX",
722         '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
723 define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX",
724         '.*?(?<!\\\\)(?=[~}]|$)');
725 define("ANSWER_ALTERNATIVE_REGEX",
726        '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
727        '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
728        '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
730 // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
731 define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2);
732 define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1);
733 define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3);
734 define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5);
736 // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
737 // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
738 define("NUMBER_REGEX",
739         '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
740 define("NUMERICAL_ALTERNATIVE_REGEX",
741         '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
743 // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
744 define("NUMERICAL_CORRECT_ANSWER", 1);
745 define("NUMERICAL_ABS_ERROR_MARGIN", 6);
747 // Remaining ANSWER regexes
748 define("ANSWER_TYPE_DEF_REGEX",
749        '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)');
750 define("ANSWER_START_REGEX",
751        '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
753 define("ANSWER_REGEX",
754         ANSWER_START_REGEX
755         . '(' . ANSWER_ALTERNATIVE_REGEX
756         . '(~'
757         . ANSWER_ALTERNATIVE_REGEX
758         . ')*)\}' );
760 // Parenthesis positions for singulars in ANSWER_REGEX
761 define("ANSWER_REGEX_NORM", 1);
762 define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3);
763 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4);
764 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR", 5);
765 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL", 6);
766 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 7);
767 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C", 8);
768 define("ANSWER_REGEX_ALTERNATIVES", 9);
770 function qtype_multianswer_extract_question($text) {
771     // $text is an array [text][format][itemid]
772     $question = new stdClass;
773     $question->qtype = 'multianswer';
774     $question->questiontext = $text;
775     $question->generalfeedback['text'] = '';
776     $question->generalfeedback['format'] = '1';
777     $question->generalfeedback['itemid'] = '';
778     
779     $question->options->questions = array();    
780     $question->defaultgrade = 0; // Will be increased for each answer norm
782     for ($positionkey=1; preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs); ++$positionkey ) {
783         $wrapped = new stdClass;
784         $wrapped->generalfeedback['text'] = '';
785         $wrapped->generalfeedback['format'] = '1';
786         $wrapped->generalfeedback['itemid'] = '';
787         if (isset($answerregs[ANSWER_REGEX_NORM])&& $answerregs[ANSWER_REGEX_NORM]!== ''){
788             $wrapped->defaultgrade = $answerregs[ANSWER_REGEX_NORM];
789         } else {
790             $wrapped->defaultgrade = '1';
791         }
792         if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
793             $wrapped->qtype = 'numerical';
794             $wrapped->multiplier = array();
795             $wrapped->units      = array();
796             $wrapped->instructions['text'] = '';
797             $wrapped->instructions['format'] = '1';
798             $wrapped->instructions['itemid'] = '';
799         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
800             $wrapped->qtype = 'shortanswer';
801             $wrapped->usecase = 0;
802         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
803             $wrapped->qtype = 'shortanswer';
804             $wrapped->usecase = 1;
805         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
806             $wrapped->qtype = 'multichoice';
807             $wrapped->single = 1;
808             $wrapped->answernumbering = 0;
809             $wrapped->correctfeedback['text'] = '';
810             $wrapped->correctfeedback['format'] = '1';
811             $wrapped->correctfeedback['itemid'] = '';
812             $wrapped->partiallycorrectfeedback['text'] = '';
813             $wrapped->partiallycorrectfeedback['format'] = '1';
814             $wrapped->partiallycorrectfeedback['itemid'] = '';
815             $wrapped->incorrectfeedback['text'] = '';
816             $wrapped->incorrectfeedback['format'] = '1';
817             $wrapped->incorrectfeedback['itemid'] = '';
818             $wrapped->layout = 0;
819         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
820             $wrapped->qtype = 'multichoice';
821             $wrapped->single = 1;
822             $wrapped->answernumbering = 0;
823             $wrapped->correctfeedback['text'] = '';
824             $wrapped->correctfeedback['format'] = '1';
825             $wrapped->correctfeedback['itemid'] = '';
826             $wrapped->partiallycorrectfeedback['text'] = '';
827             $wrapped->partiallycorrectfeedback['format'] = '1';
828             $wrapped->partiallycorrectfeedback['itemid'] = '';
829             $wrapped->incorrectfeedback['text'] = '';
830             $wrapped->incorrectfeedback['format'] = '1';
831             $wrapped->incorrectfeedback['itemid'] = '';
832             $wrapped->layout = 1;
833         } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
834             $wrapped->qtype = 'multichoice';
835             $wrapped->single = 1;
836             $wrapped->answernumbering = 0;
837             $wrapped->correctfeedback['text'] = '';
838             $wrapped->correctfeedback['format'] = '1';
839             $wrapped->correctfeedback['itemid'] = '';
840             $wrapped->partiallycorrectfeedback['text'] = '';
841             $wrapped->partiallycorrectfeedback['format'] = '1';
842             $wrapped->partiallycorrectfeedback['itemid'] = '';
843             $wrapped->incorrectfeedback['text'] = '';
844             $wrapped->incorrectfeedback['format'] = '1';
845             $wrapped->incorrectfeedback['itemid'] = '';
846             $wrapped->layout = 2;
847         } else {
848             print_error('unknownquestiontype', 'question', '', $answerregs[2]);
849             return false;
850         }
852         // Each $wrapped simulates a $form that can be processed by the
853         // respective save_question and save_question_options methods of the
854         // wrapped questiontypes
855         $wrapped->answer   = array();
856         $wrapped->fraction = array();
857         $wrapped->feedback = array();
858         $wrapped->shuffleanswers = 1;
859         $wrapped->questiontext['text'] = $answerregs[0];
860         $wrapped->questiontext['format'] = 0 ;
861         $wrapped->questiontext['itemid'] = '' ;
862         $answerindex = 0 ;
864         $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
865         while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
866             if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
867                 $wrapped->fraction["$answerindex"] = '1';
868             } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]){
869                 $wrapped->fraction["$answerindex"] = .01 * $percentile;
870             } else {
871                 $wrapped->fraction["$answerindex"] = '0';
872             }
873             if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
874                 $feedback = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
875                 $feedback = str_replace('\}', '}', $feedback);
876                 $wrapped->feedback["$answerindex"]['text'] = str_replace('\#', '#', $feedback);
877                 $wrapped->feedback["$answerindex"]['format'] = '1';
878                 $wrapped->feedback["$answerindex"]['itemid'] = '';
879             } else {
880                 $wrapped->feedback["$answerindex"]['text'] = '';
881                 $wrapped->feedback["$answerindex"]['format'] = '1';
882                 $wrapped->feedback["$answerindex"]['itemid'] = '1';
884             }
885             if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
886                     && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~', $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
887                 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
888                 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
889                     $wrapped->tolerance["$answerindex"] =
890                     $numregs[NUMERICAL_ABS_ERROR_MARGIN];
891                 } else {
892                     $wrapped->tolerance["$answerindex"] = 0;
893                 }
894             } else { // Tolerance can stay undefined for non numerical questions
895                 // Undo quoting done by the HTML editor.
896                 $answer = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
897                 $answer = str_replace('\}', '}', $answer);
898                 $wrapped->answer["$answerindex"] = str_replace('\#', '#', $answer);
899             }
900             $tmp = explode($altregs[0], $remainingalts, 2);
901             $remainingalts = $tmp[1];
902             $answerindex++ ;
903         }
905         $question->defaultgrade += $wrapped->defaultgrade;
906         $question->options->questions[$positionkey] = clone($wrapped);
907         $question->questiontext['text'] = implode("{#$positionkey}",
908                     explode($answerregs[0], $question->questiontext['text'], 2));
909 //    echo"<p>questiontext 2 <pre>";print_r($question->questiontext);echo"<pre></p>";
910     }
911 //    echo"<p>questiontext<pre>";print_r($question->questiontext);echo"<pre></p>";
912     $question->questiontext = $question->questiontext;
913 //    echo"<p>question<pre>";print_r($question);echo"<pre></p>";
914     return $question;