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