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