MDL-20636 Fix potential bug with qtype_shortanswer correct answer display.
[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 */
ab50232b 36class qtype_multianswer extends question_type {
59a3fcd3 37 public function requires_qtypes() {
869309b8 38 return array('shortanswer', 'numerical', 'multichoice');
39 }
516cf3eb 40
ab50232b
TH
41 public function can_analyse_responses() {
42 return false;
43 }
44
59a3fcd3
TH
45 public function get_question_options($question) {
46 global $DB, $OUTPUT;
516cf3eb 47
48 // Get relevant data indexed by positionkey from the multianswers table
ab50232b
TH
49 $sequence = $DB->get_field('question_multianswer', 'sequence',
50 array('question' => $question->id), '*', MUST_EXIST);
516cf3eb 51
ab50232b
TH
52 $wrappedquestions = $DB->get_records_list('question', 'id',
53 explode(',', $sequence), 'id ASC');
516cf3eb 54
55 // We want an array with question ids as index and the positions as values
56 $sequence = array_flip(explode(',', $sequence));
57 array_walk($sequence, create_function('&$val', '$val++;'));
ab50232b
TH
58
59 // If a question is lost, the corresponding index is null
aeb15530 60 // so this null convention is used to test $question->options->questions
857caf3b 61 // before using the values.
aeb15530 62 // first all possible questions from sequence are nulled
857caf3b 63 // then filled with the data if available in $wrappedquestions
59a3fcd3
TH
64 foreach ($sequence as $seq) {
65 $question->options->questions[$seq] = '';
df79079f 66 }
ab50232b
TH
67
68 foreach ($wrappedquestions as $wrapped) {
69 question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped);
70 // for wrapped questions the maxgrade is always equal to the defaultmark,
71 // there is no entry in the question_instances table for them
72 $wrapped->maxmark = $wrapped->defaultmark;
73 $question->options->questions[$sequence[$wrapped->id]] = $wrapped;
857caf3b 74 }
516cf3eb 75
76 return true;
77 }
78
59a3fcd3
TH
79 public function save_question_options($question) {
80 global $DB;
0ff4bd08 81 $result = new stdClass();
9fc3100f 82
516cf3eb 83 // This function needs to be able to handle the case where the existing set of wrapped
84 // questions does not match the new set of wrapped questions so that some need to be
85 // created, some modified and some deleted
86 // Unfortunately the code currently simply overwrites existing ones in sequence. This
9fc3100f 87 // will make re-marking after a re-ordering of wrapped questions impossible and
516cf3eb 88 // will also create difficulties if questiontype specific tables reference the id.
9fc3100f 89
516cf3eb 90 // First we get all the existing wrapped questions
ab50232b
TH
91 if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence',
92 array('question' => $question->id))) {
857caf3b 93 $oldwrappedquestions = array();
0a5b58af 94 } else {
857caf3b 95 $oldwrappedquestions = $DB->get_records_list('question', 'id', explode(',', $oldwrappedids), 'id ASC');
516cf3eb 96 }
ab50232b 97
516cf3eb 98 $sequence = array();
59a3fcd3
TH
99 foreach ($question->options->questions as $wrapped) {
100 if (!empty($wrapped)) {
df79079f 101 // if we still have some old wrapped question ids, reuse the next of them
f34488b2 102
857caf3b 103 if (is_array($oldwrappedquestions) && $oldwrappedquestion = array_shift($oldwrappedquestions)) {
104 $wrapped->id = $oldwrappedquestion->id;
59a3fcd3 105 if ($oldwrappedquestion->qtype != $wrapped->qtype) {
857caf3b 106 switch ($oldwrappedquestion->qtype) {
59a3fcd3 107 case 'multichoice':
ab50232b
TH
108 $DB->delete_records('question_multichoice',
109 array('question' => $oldwrappedquestion->id));
59a3fcd3
TH
110 break;
111 case 'shortanswer':
ab50232b
TH
112 $DB->delete_records('question_shortanswer',
113 array('question' => $oldwrappedquestion->id));
59a3fcd3
TH
114 break;
115 case 'numerical':
ab50232b
TH
116 $DB->delete_records('question_numerical',
117 array('question' => $oldwrappedquestion->id));
59a3fcd3
TH
118 break;
119 default:
ab50232b
TH
120 throw new moodle_exception('qtypenotrecognized',
121 'qtype_multianswer', '', $oldwrappedquestion->qtype);
59a3fcd3 122 $wrapped->id = 0;
df79079f 123 }
e9028ffc 124 }
59a3fcd3
TH
125 } else {
126 $wrapped->id = 0;
e9028ffc 127 }
516cf3eb 128 }
77fa3a0d 129 $wrapped->name = $question->name;
130 $wrapped->parent = $question->id;
59a3fcd3 131 $previousid = $wrapped->id;
ab50232b
TH
132 // save_question strips this extra bit off the category again.
133 $wrapped->category = $question->category . ',1';
134 $wrapped = question_bank::get_qtype($wrapped->qtype)->save_question(
135 $wrapped, clone($wrapped));
516cf3eb 136 $sequence[] = $wrapped->id;
59a3fcd3 137 if ($previousid != 0 && $previousid != $wrapped->id) {
26053641 138 // for some reasons a new question has been created
139 // so delete the old one
59a3fcd3 140 delete_question($previousid);
26053641 141 }
516cf3eb 142 }
143
144 // Delete redundant wrapped questions
59a3fcd3 145 if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) {
26053641 146 foreach ($oldwrappedquestions as $oldwrappedquestion) {
59a3fcd3 147 delete_question($oldwrappedquestion->id);
e9028ffc 148 }
4bc4ca50 149 }
516cf3eb 150
151 if (!empty($sequence)) {
0ff4bd08 152 $multianswer = new stdClass();
516cf3eb 153 $multianswer->question = $question->id;
154 $multianswer->sequence = implode(',', $sequence);
f34488b2 155 if ($oldid = $DB->get_field('question_multianswer', 'id', array('question' => $question->id))) {
516cf3eb 156 $multianswer->id = $oldid;
ab50232b 157 $DB->update_record('question_multianswer', $multianswer);
516cf3eb 158 } else {
ab50232b 159 $DB->insert_record('question_multianswer', $multianswer);
516cf3eb 160 }
161 }
162 }
163
59a3fcd3 164 public function save_question($authorizedquestion, $form) {
e51efd7e 165 $question = qtype_multianswer_extract_question($form->questiontext);
516cf3eb 166 if (isset($authorizedquestion->id)) {
167 $question->id = $authorizedquestion->id;
516cf3eb 168 }
169
516cf3eb 170 $question->category = $authorizedquestion->category;
ab50232b 171 $form->defaultmark = $question->defaultmark;
516cf3eb 172 $form->questiontext = $question->questiontext;
173 $form->questiontextformat = 0;
77fa3a0d 174 $form->options = clone($question->options);
516cf3eb 175 unset($question->options);
94dbfb3a 176 return parent::save_question($question, $form);
516cf3eb 177 }
178
59a3fcd3 179 public function delete_question($questionid, $contextid) {
f34488b2 180 global $DB;
ab50232b 181 $DB->delete_records('question_multianswer', array('question' => $questionid));
9203b705
TH
182
183 parent::delete_question($questionid, $contextid);
516cf3eb 184 }
185
ab50232b
TH
186 protected function initialise_question_instance($question, $questiondata) {
187 parent::initialise_question_instance($question, $questiondata);
188 foreach ($questiondata->options->questions as $key => $subqdata) {
189 $subqdata->contextid = $questiondata->contextid;
190 $question->subquestions[$key] = question_bank::make_question($subqdata);
fa6c8620 191 $question->subquestions[$key]->maxmark = $subqdata->defaultmark;
869309b8 192 }
869309b8 193 }
869309b8 194
59a3fcd3 195 public function get_html_head_contributions(&$question, &$state) {
45c4a5c7
TH
196 global $PAGE;
197 parent::get_html_head_contributions($question, $state);
198 $PAGE->requires->js('/lib/overlib/overlib.js', true);
199 $PAGE->requires->js('/lib/overlib/overlib_cssstyle.js', true);
200 }
201
59a3fcd3
TH
202 public function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
203 global $CFG, $USER, $OUTPUT, $PAGE;
9fc3100f 204
516cf3eb 205 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
6463e8a6 206 $disabled = empty($options->readonly) ? '' : 'disabled="disabled"';
0ff4bd08 207 $formatoptions = new stdClass();
7347c60b 208 $formatoptions->noclean = true;
209 $formatoptions->para = false;
516cf3eb 210 $nameprefix = $question->name_prefix;
9fc3100f 211
73ca1421 212 // adding an icon with alt to warn user this is a fill in the gap question
213 // MDL-7497
ccffd412 214 if (!empty($USER->screenreader)) {
0c3c5493 215 echo "<img src=\"".$OUTPUT->pix_url('icon', 'qtype_'.$question->qtype)."\" ".
59a3fcd3 216 "class=\"icon\" alt=\"".get_string('clozeaid', 'qtype_multichoice')."\" /> ";
73ca1421 217 }
06e2b0de 218
219 echo '<div class="ablock clearfix">';
9fc3100f 220
516cf3eb 221 $qtextremaining = format_text($question->questiontext,
77fa3a0d 222 $question->questiontextformat, $formatoptions, $cmoptions->course);
516cf3eb 223
5e8a85aa 224 $strfeedback = get_string('feedback', 'question');
516cf3eb 225
226 // The regex will recognize text snippets of type {#X}
227 // where the X can be any text not containg } or white-space characters.
6dbcacee 228 while (preg_match('~\{#([^[:space:]}]*)}~', $qtextremaining, $regs)) {
516cf3eb 229 $qtextsplits = explode($regs[0], $qtextremaining, 2);
230 echo $qtextsplits[0];
0bddf4b6 231 echo "<label>"; // MDL-7497
516cf3eb 232 $qtextremaining = $qtextsplits[1];
233
234 $positionkey = $regs[1];
59a3fcd3
TH
235 if (isset($question->options->questions[$positionkey]) && $question->options->questions[$positionkey] != '') {
236 $wrapped = &$question->options->questions[$positionkey];
237 $answers = &$wrapped->options->answers;
238
239 $inputname = $nameprefix.$positionkey;
240 if (isset($state->responses[$positionkey])) {
241 $response = $state->responses[$positionkey];
242 } else {
243 $response = null;
244 }
245
246 // Determine feedback popup if any
247 $popup = '';
248 $style = '';
249 $feedbackimg = '';
250 $feedback = '';
251 $correctanswer = '';
252 $strfeedbackwrapped = $strfeedback;
f34488b2 253 $testedstate = clone($state);
ab50232b 254 if ($correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
907a3759 255 if ($options->readonly && $options->correct_responses) {
256 $delimiter = '';
257 if ($correctanswers) {
258 foreach ($correctanswers as $ca) {
59a3fcd3 259 switch($wrapped->qtype) {
907a3759 260 case 'numerical':
f34488b2 261 case 'shortanswer':
907a3759 262 $correctanswer .= $delimiter.$ca;
59a3fcd3 263 break;
907a3759 264 case 'multichoice':
59a3fcd3 265 if (isset($answers[$ca])) {
907a3759 266 $correctanswer .= $delimiter.$answers[$ca]->answer;
267 }
59a3fcd3 268 break;
f34488b2 269 }
907a3759 270 $delimiter = ', ';
271 }
272 }
273 }
59a3fcd3 274 if ($correctanswer != '') {
907a3759 275 $feedback = '<div class="correctness">';
5e8a85aa 276 $feedback .= get_string('correctansweris', 'question', s($correctanswer));
907a3759 277 $feedback .= '</div>';
907a3759 278 }
279 }
458eb0d1 280
59a3fcd3
TH
281 if ($options->feedback) {
282 $chosenanswer = null;
283 switch ($wrapped->qtype) {
284 case 'numerical':
285 case 'shortanswer':
286 $testedstate = clone($state);
287 $testedstate->responses[''] = $response;
288 foreach ($answers as $answer) {
289 if ($QTYPES[$wrapped->qtype]->test_response($wrapped, $testedstate, $answer)) {
290 $chosenanswer = clone($answer);
291 break;
292 }
516cf3eb 293 }
59a3fcd3
TH
294 break;
295 case 'multichoice':
296 if (isset($answers[$response])) {
297 $chosenanswer = clone($answers[$response]);
298 }
299 break;
300 default:
301 break;
907a3759 302 }
516cf3eb 303
59a3fcd3
TH
304 // Set up a default chosenanswer so that all non-empty wrong
305 // answers are highlighted red
306 if (empty($chosenanswer) && $response != '') {
307 $chosenanswer = new stdClass();
308 $chosenanswer->fraction = 0.0;
309 }
516cf3eb 310
59a3fcd3
TH
311 if (!empty($chosenanswer->feedback)) {
312 $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback.$chosenanswer->feedback));
313 if ($options->readonly && $options->correct_responses) {
314 $strfeedbackwrapped = get_string('correctanswerandfeedback', 'qtype_multianswer');
315 } else {
316 $strfeedbackwrapped = get_string('feedback', 'question');
eba53585 317 }
59a3fcd3
TH
318 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
319 " onmouseout=\"return nd();\" ";
f34488b2 320 }
f34488b2 321
59a3fcd3
TH
322 /// Determine style
323 if ($options->feedback && $response != '') {
324 $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction).'"';
325 $feedbackimg = question_get_feedback_image($chosenanswer->fraction);
326 } else {
327 $style = '';
328 $feedbackimg = '';
04d6ac46 329 }
59a3fcd3
TH
330 }
331 if ($feedback != '' && $popup == '') {
332 $strfeedbackwrapped = get_string('correctanswer', 'qtype_multianswer');
333 $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback));
334 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
335 " onmouseout=\"return nd();\" ";
336 }
337
338 // Print the input control
339 switch ($wrapped->qtype) {
340 case 'shortanswer':
341 case 'numerical':
342 $size = 1;
343 foreach ($answers as $answer) {
344 if (strlen(trim($answer->answer)) > $size) {
345 $size = strlen(trim($answer->answer));
346 }
77fa3a0d 347 }
59a3fcd3
TH
348 if (strlen(trim($response))> $size) {
349 $size = strlen(trim($response))+1;
e5ebbd53 350 }
59a3fcd3
TH
351 $size = $size + rand(0, $size*0.15);
352 $size > 60 ? $size = 60 : $size = $size;
353 $styleinfo = "size=\"$size\"";
354
355 echo "<input $style $readonly $popup name=\"$inputname\"";
356 echo " type=\"text\" value=\"".s($response)."\" ".$styleinfo." /> ";
e5ebbd53 357 if (!empty($feedback) && !empty($USER->screenreader)) {
b5d0cafc 358 echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
e5ebbd53 359 }
360 echo $feedbackimg;
59a3fcd3
TH
361 break;
362 case 'multichoice':
363 if ($wrapped->options->layout == 0) {
364 $outputoptions = '<option></option>'; // Default empty option
365 foreach ($answers as $mcanswer) {
366 $selected = '';
367 if ($response == $mcanswer->id) {
368 $selected = ' selected="selected"';
369 }
370 $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
371 s($mcanswer->answer) . '</option>';
e5ebbd53 372 }
59a3fcd3
TH
373 // In the next line, $readonly is invalid HTML, but it works in
374 // all browsers. $disabled would be valid, but then the JS for
375 // displaying the feedback does not work. Of course, we should
376 // not be relying on JS (for accessibility reasons), but that is
377 // a bigger problem.
378 //
379 // The span is used for safari, which does not allow styling of
380 // selects.
381 echo "<span $style><select $popup $readonly $style name=\"$inputname\">";
382 echo $outputoptions;
383 echo '</select></span>';
384 if (!empty($feedback) && !empty($USER->screenreader)) {
385 echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
386 }
387 echo $feedbackimg;
388 } else if ($wrapped->options->layout == 1 || $wrapped->options->layout == 2) {
389 $ordernumber = 0;
ab50232b 390 $anss = array();
59a3fcd3
TH
391 foreach ($answers as $mcanswer) {
392 $ordernumber++;
393 $checked = '';
394 $chosen = false;
395 $type = 'type="radio"';
396 $name = "name=\"{$inputname}\"";
397 if ($response == $mcanswer->id) {
398 $checked = 'checked="checked"';
399 $chosen = true;
400 }
401 $a = new stdClass();
402 $a->id = $question->name_prefix . $mcanswer->id;
403 $a->class = '';
404 $a->feedbackimg = '';
405
406 // Print the control
407 $a->control = "<input $readonly id=\"$a->id\" $name $checked $type value=\"$mcanswer->id\" />";
408 if ($options->correct_responses && $mcanswer->fraction > 0) {
409 $a->class = question_get_feedback_class(1);
410 }
411 if (($options->feedback && $chosen) || $options->correct_responses) {
412 if ($type == ' type="checkbox" ') {
413 $a->feedbackimg = question_get_feedback_image($mcanswer->fraction > 0 ? 1 : 0, $chosen && $options->feedback);
414 } else {
415 $a->feedbackimg = question_get_feedback_image($mcanswer->fraction, $chosen && $options->feedback);
416 }
417 }
aeb15530 418
59a3fcd3
TH
419 // Print the answer text: no automatic numbering
420 $a->text = format_text($mcanswer->answer, $mcanswer->answerformat, $formatoptions, $cmoptions->course);
0bddf4b6 421
59a3fcd3
TH
422 // Print feedback if feedback is on
423 if (($options->feedback || $options->correct_responses) && ($checked)) { //|| $options->readonly
424 $a->feedback = format_text($mcanswer->feedback, $mcanswer->feedbackformat, $formatoptions, $cmoptions->course);
425 } else {
426 $a->feedback = '';
427 }
aeb15530 428
59a3fcd3
TH
429 $anss[] = clone($a);
430 }
431 if ($wrapped->options->layout == 1) {
432 ?><table class="answer"><?php
433 $row = 1;
434 foreach ($anss as $answer) {
435 ?><tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
436 <td class="c0 control">
437 <?php echo $answer->control; ?>
438 </td>
439 <td class="c1 text <?php echo $answer->class ?>">
440 <label for="<?php echo $answer->id ?>">
441 <?php echo $answer->text; ?>
442 <?php echo $answer->feedbackimg; ?>
443 </label>
444 </td>
445 <td class="c0 feedback">
446 <?php echo $answer->feedback; ?>
447 </td>
448 </tr><?php
449 }
450 ?></table><?php
451 } else if ($wrapped->options->layout == 2) {
452 ?><table class="answer">
453 <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
454 <?php $row = 1;
455 foreach ($anss as $answer) { ?>
456 <td class="c0 control">
457 <?php echo $answer->control; ?>
458 </td>
459 <td class="c1 text <?php echo $answer->class ?>">
460 <label for="<?php echo $answer->id ?>">
461 <?php echo $answer->text; ?>
462 <?php echo $answer->feedbackimg; ?>
463 </label>
464 </td>
465 <td class="c0 feedback">
466 <?php echo $answer->feedback; ?>
467 </td><?php
468 }
469 ?>
470 </tr>
471 </table><?php
472 }
aeb15530 473
59a3fcd3
TH
474 } else {
475 echo "no valid layout";
476 }
aeb15530 477
59a3fcd3
TH
478 break;
479 default:
480 $a = new stdClass();
481 $a->type = $wrapped->qtype;
482 $a->sub = $positionkey;
483 print_error('unknownquestiontypeofsubquestion', 'qtype_multianswer', '', $a);
484 break;
485 }
486 echo "</label>"; // MDL-7497
487 } else {
488 if (! isset($question->options->questions[$positionkey])) {
489 echo $regs[0]."</label>";
490 } else {
491 echo '</label><div class="error" >'.get_string('questionnotfound', 'qtype_multianswer', $positionkey).'</div>';
492 }
df79079f 493 }
59a3fcd3 494 }
516cf3eb 495
496 // Print the final piece of question text:
497 echo $qtextremaining;
f4b72cdb 498 $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
f34488b2 499 echo '</div>';
516cf3eb 500 }
501
ab50232b
TH
502 public function get_random_guess_score($questiondata) {
503 $fractionsum = 0;
504 $fractionmax = 0;
505 foreach ($questiondata->options->questions as $key => $subqdata) {
fa6c8620 506 $fractionmax += $subqdata->defaultmark;
ab50232b
TH
507 $fractionsum += question_bank::get_qtype(
508 $subqdata->qtype)->get_random_guess_score($subqdata);
516cf3eb 509 }
ab50232b 510 return $fractionsum / $fractionmax;
516cf3eb 511 }
516cf3eb 512}
516cf3eb 513
ab50232b 514
0b346164 515// ANSWER_ALTERNATIVE regexes
ab50232b 516define('ANSWER_ALTERNATIVE_FRACTION_REGEX',
0b346164 517 '=|%(-?[0-9]+)%');
518// for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
ab50232b 519define('ANSWER_ALTERNATIVE_ANSWER_REGEX',
e51efd7e 520 '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
ab50232b 521define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX',
0b346164 522 '.*?(?<!\\\\)(?=[~}]|$)');
ab50232b 523define('ANSWER_ALTERNATIVE_REGEX',
e51efd7e 524 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
525 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
526 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
0b346164 527
528// Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
ab50232b
TH
529define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2);
530define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1);
531define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3);
532define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5);
0b346164 533
534// NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
535// for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
ab50232b 536define('NUMBER_REGEX',
0b346164 537 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
ab50232b 538define('NUMERICAL_ALTERNATIVE_REGEX',
0b346164 539 '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
540
541// Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
ab50232b
TH
542define('NUMERICAL_CORRECT_ANSWER', 1);
543define('NUMERICAL_ABS_ERROR_MARGIN', 6);
0b346164 544
545// Remaining ANSWER regexes
ab50232b 546define('ANSWER_TYPE_DEF_REGEX',
fd97082c 547 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)');
ab50232b 548define('ANSWER_START_REGEX',
0b346164 549 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
550
ab50232b 551define('ANSWER_REGEX',
0b346164 552 ANSWER_START_REGEX
553 . '(' . ANSWER_ALTERNATIVE_REGEX
554 . '(~'
555 . ANSWER_ALTERNATIVE_REGEX
59a3fcd3 556 . ')*)\}');
0b346164 557
558// Parenthesis positions for singulars in ANSWER_REGEX
ab50232b
TH
559define('ANSWER_REGEX_NORM', 1);
560define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3);
561define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4);
562define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5);
563define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6);
564define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7);
565define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
566define('ANSWER_REGEX_ALTERNATIVES', 9);
516cf3eb 567
7518b645 568function qtype_multianswer_extract_question($text) {
61dfe97e 569 // $text is an array [text][format][itemid]
0ff4bd08 570 $question = new stdClass();
dfa47f96 571 $question->qtype = 'multianswer';
516cf3eb 572 $question->questiontext = $text;
61dfe97e
PP
573 $question->generalfeedback['text'] = '';
574 $question->generalfeedback['format'] = '1';
575 $question->generalfeedback['itemid'] = '';
59a3fcd3
TH
576
577 $question->options->questions = array();
ab50232b 578 $question->defaultmark = 0; // Will be increased for each answer norm
516cf3eb 579
ab50232b
TH
580 for ($positionkey = 1;
581 preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs);
582 ++$positionkey) {
0ff4bd08 583 $wrapped = new stdClass();
61dfe97e
PP
584 $wrapped->generalfeedback['text'] = '';
585 $wrapped->generalfeedback['format'] = '1';
586 $wrapped->generalfeedback['itemid'] = '';
59a3fcd3 587 if (isset($answerregs[ANSWER_REGEX_NORM])&& $answerregs[ANSWER_REGEX_NORM]!== '') {
ab50232b 588 $wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM];
8795a5ae 589 } else {
ab50232b 590 $wrapped->defaultmark = '1';
8795a5ae 591 }
516cf3eb 592 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
dfa47f96 593 $wrapped->qtype = 'numerical';
516cf3eb 594 $wrapped->multiplier = array();
595 $wrapped->units = array();
61dfe97e
PP
596 $wrapped->instructions['text'] = '';
597 $wrapped->instructions['format'] = '1';
598 $wrapped->instructions['itemid'] = '';
59a3fcd3 599 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
dfa47f96 600 $wrapped->qtype = 'shortanswer';
516cf3eb 601 $wrapped->usecase = 0;
59a3fcd3 602 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
fd97082c 603 $wrapped->qtype = 'shortanswer';
604 $wrapped->usecase = 1;
59a3fcd3 605 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
dfa47f96 606 $wrapped->qtype = 'multichoice';
516cf3eb 607 $wrapped->single = 1;
271e6dec 608 $wrapped->answernumbering = 0;
61dfe97e
PP
609 $wrapped->correctfeedback['text'] = '';
610 $wrapped->correctfeedback['format'] = '1';
611 $wrapped->correctfeedback['itemid'] = '';
612 $wrapped->partiallycorrectfeedback['text'] = '';
613 $wrapped->partiallycorrectfeedback['format'] = '1';
614 $wrapped->partiallycorrectfeedback['itemid'] = '';
615 $wrapped->incorrectfeedback['text'] = '';
616 $wrapped->incorrectfeedback['format'] = '1';
617 $wrapped->incorrectfeedback['itemid'] = '';
e5ebbd53 618 $wrapped->layout = 0;
59a3fcd3 619 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
e5ebbd53 620 $wrapped->qtype = 'multichoice';
621 $wrapped->single = 1;
622 $wrapped->answernumbering = 0;
61dfe97e
PP
623 $wrapped->correctfeedback['text'] = '';
624 $wrapped->correctfeedback['format'] = '1';
625 $wrapped->correctfeedback['itemid'] = '';
626 $wrapped->partiallycorrectfeedback['text'] = '';
627 $wrapped->partiallycorrectfeedback['format'] = '1';
628 $wrapped->partiallycorrectfeedback['itemid'] = '';
629 $wrapped->incorrectfeedback['text'] = '';
630 $wrapped->incorrectfeedback['format'] = '1';
631 $wrapped->incorrectfeedback['itemid'] = '';
e5ebbd53 632 $wrapped->layout = 1;
59a3fcd3 633 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
e5ebbd53 634 $wrapped->qtype = 'multichoice';
635 $wrapped->single = 1;
636 $wrapped->answernumbering = 0;
61dfe97e
PP
637 $wrapped->correctfeedback['text'] = '';
638 $wrapped->correctfeedback['format'] = '1';
639 $wrapped->correctfeedback['itemid'] = '';
640 $wrapped->partiallycorrectfeedback['text'] = '';
641 $wrapped->partiallycorrectfeedback['format'] = '1';
642 $wrapped->partiallycorrectfeedback['itemid'] = '';
643 $wrapped->incorrectfeedback['text'] = '';
644 $wrapped->incorrectfeedback['format'] = '1';
645 $wrapped->incorrectfeedback['itemid'] = '';
e5ebbd53 646 $wrapped->layout = 2;
516cf3eb 647 } else {
2471ef86 648 print_error('unknownquestiontype', 'question', '', $answerregs[2]);
516cf3eb 649 return false;
650 }
651
652 // Each $wrapped simulates a $form that can be processed by the
653 // respective save_question and save_question_options methods of the
654 // wrapped questiontypes
655 $wrapped->answer = array();
656 $wrapped->fraction = array();
657 $wrapped->feedback = array();
658 $wrapped->shuffleanswers = 1;
61dfe97e 659 $wrapped->questiontext['text'] = $answerregs[0];
59a3fcd3
TH
660 $wrapped->questiontext['format'] = 0;
661 $wrapped->questiontext['itemid'] = '';
662 $answerindex = 0;
516cf3eb 663
664 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
665 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
666 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
61dfe97e 667 $wrapped->fraction["$answerindex"] = '1';
59a3fcd3 668 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
61dfe97e 669 $wrapped->fraction["$answerindex"] = .01 * $percentile;
516cf3eb 670 } else {
61dfe97e 671 $wrapped->fraction["$answerindex"] = '0';
516cf3eb 672 }
e51efd7e 673 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
ab50232b
TH
674 $feedback = html_entity_decode(
675 $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
095b599a 676 $feedback = str_replace('\}', '}', $feedback);
61dfe97e
PP
677 $wrapped->feedback["$answerindex"]['text'] = str_replace('\#', '#', $feedback);
678 $wrapped->feedback["$answerindex"]['format'] = '1';
679 $wrapped->feedback["$answerindex"]['itemid'] = '';
e51efd7e 680 } else {
61dfe97e
PP
681 $wrapped->feedback["$answerindex"]['text'] = '';
682 $wrapped->feedback["$answerindex"]['format'] = '1';
683 $wrapped->feedback["$answerindex"]['itemid'] = '1';
684
e51efd7e 685 }
516cf3eb 686 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
ab50232b
TH
687 && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~',
688 $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
e51efd7e 689 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
516cf3eb 690 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
61dfe97e 691 $wrapped->tolerance["$answerindex"] =
e51efd7e 692 $numregs[NUMERICAL_ABS_ERROR_MARGIN];
516cf3eb 693 } else {
61dfe97e 694 $wrapped->tolerance["$answerindex"] = 0;
516cf3eb 695 }
696 } else { // Tolerance can stay undefined for non numerical questions
1f8db780 697 // Undo quoting done by the HTML editor.
ab50232b
TH
698 $answer = html_entity_decode(
699 $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
095b599a 700 $answer = str_replace('\}', '}', $answer);
61dfe97e 701 $wrapped->answer["$answerindex"] = str_replace('\#', '#', $answer);
516cf3eb 702 }
703 $tmp = explode($altregs[0], $remainingalts, 2);
704 $remainingalts = $tmp[1];
59a3fcd3 705 $answerindex++;
516cf3eb 706 }
707
ab50232b 708 $question->defaultmark += $wrapped->defaultmark;
516cf3eb 709 $question->options->questions[$positionkey] = clone($wrapped);
61dfe97e
PP
710 $question->questiontext['text'] = implode("{#$positionkey}",
711 explode($answerregs[0], $question->questiontext['text'], 2));
516cf3eb 712 }
e51efd7e 713 $question->questiontext = $question->questiontext;
516cf3eb 714 return $question;
715}