MDL-20636 Split question-type specific styles into the separate plugins.
[moodle.git] / question / type / multianswer / questiontype.php
CommitLineData
aeb15530 1<?php
516cf3eb 2
d3603157
TH
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/>.
17
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 */
26
27
a17b297d
TH
28defined('MOODLE_INTERNAL') || die();
29
30
1976496e 31/**
d3603157
TH
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
7375c542 36 */
d649fb02 37class embedded_cloze_qtype extends question_type {
516cf3eb 38
39 function name() {
40 return 'multianswer';
41 }
aeb15530 42
869309b8 43 function has_wildcards_in_responses($question, $subqid) {
fef8f84e 44 global $QTYPES, $OUTPUT;
869309b8 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 }
fef8f84e 50 echo $OUTPUT->notification('Could not find sub question!');
869309b8 51 return true;
52 }
53
54 function requires_qtypes() {
55 return array('shortanswer', 'numerical', 'multichoice');
56 }
516cf3eb 57
58 function get_question_options(&$question) {
fef8f84e 59 global $QTYPES, $DB, $OUTPUT;
516cf3eb 60
61 // Get relevant data indexed by positionkey from the multianswers table
f34488b2 62 if (!$sequence = $DB->get_field('question_multianswer', 'sequence', array('question' => $question->id))) {
fef8f84e 63 echo $OUTPUT->notification(get_string('noquestions','qtype_multianswer',$question->name));
857caf3b 64 $question->options->questions['1']= '';
65 return true ;
516cf3eb 66 }
67
44e1b7d7 68 $wrappedquestions = $DB->get_records_list('question', 'id', explode(',', $sequence), 'id ASC');
516cf3eb 69
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++;'));
857caf3b 73 //If a question is lost, the corresponding index is null
aeb15530 74 // so this null convention is used to test $question->options->questions
857caf3b 75 // before using the values.
aeb15530 76 // first all possible questions from sequence are nulled
857caf3b 77 // then filled with the data if available in $wrappedquestions
78 $nbvaliquestion = 0 ;
df79079f 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)) {
fef8f84e 85 echo $OUTPUT->notification("Unable to get options for questiontype {$wrapped->qtype} (id={$wrapped->id})");
857caf3b 86 }else {
df79079f 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;
857caf3b 90 $nbvaliquestion++ ;
df79079f 91 $question->options->questions[$sequence[$wrapped->id]] = clone($wrapped); // ??? Why do we need a clone here?
516cf3eb 92 }
516cf3eb 93 }
857caf3b 94 }
95 if ($nbvaliquestion == 0 ) {
fef8f84e 96 echo $OUTPUT->notification(get_string('noquestions','qtype_multianswer',$question->name));
857caf3b 97 }
516cf3eb 98
99 return true;
100 }
101
102 function save_question_options($question) {
44e1b7d7 103 global $QTYPES, $DB;
0ff4bd08 104 $result = new stdClass();
9fc3100f 105
516cf3eb 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
9fc3100f 110 // will make re-marking after a re-ordering of wrapped questions impossible and
516cf3eb 111 // will also create difficulties if questiontype specific tables reference the id.
9fc3100f 112
516cf3eb 113 // First we get all the existing wrapped questions
f34488b2 114 if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence', array('question' => $question->id))) {
857caf3b 115 $oldwrappedquestions = array();
0a5b58af 116 } else {
857caf3b 117 $oldwrappedquestions = $DB->get_records_list('question', 'id', explode(',', $oldwrappedids), 'id ASC');
516cf3eb 118 }
516cf3eb 119 $sequence = array();
120 foreach($question->options->questions as $wrapped) {
103a800d 121 if (!empty($wrapped)){
df79079f 122 // if we still have some old wrapped question ids, reuse the next of them
f34488b2 123
857caf3b 124 if (is_array($oldwrappedquestions) && $oldwrappedquestion = array_shift($oldwrappedquestions)) {
125 $wrapped->id = $oldwrappedquestion->id;
126 if($oldwrappedquestion->qtype != $wrapped->qtype ) {
127 switch ($oldwrappedquestion->qtype) {
df79079f 128 case 'multichoice':
857caf3b 129 $DB->delete_records('question_multichoice', array('question' => $oldwrappedquestion->id));
df79079f 130 break;
131 case 'shortanswer':
857caf3b 132 $DB->delete_records('question_shortanswer', array('question' => $oldwrappedquestion->id));
df79079f 133 break;
134 case 'numerical':
857caf3b 135 $DB->delete_records('question_numerical', array('question' => $oldwrappedquestion->id));
df79079f 136 break;
137 default:
857caf3b 138 print_error('qtypenotrecognized', 'qtype_multianswer','',$oldwrappedquestion->qtype);
df79079f 139 $wrapped->id = 0 ;
df79079f 140 }
e9028ffc 141 }
df79079f 142 }else {
143 $wrapped->id = 0 ;
e9028ffc 144 }
516cf3eb 145 }
77fa3a0d 146 $wrapped->name = $question->name;
147 $wrapped->parent = $question->id;
26053641 148 $previousid = $wrapped->id ;
80fdc53e 149 $wrapped->category = $question->category . ',1'; // save_question strips this extra bit off again.
94dbfb3a 150 $wrapped = $QTYPES[$wrapped->qtype]->save_question($wrapped, clone($wrapped));
516cf3eb 151 $sequence[] = $wrapped->id;
aeb15530 152 if ($previousid != 0 && $previousid != $wrapped->id ) {
26053641 153 // for some reasons a new question has been created
154 // so delete the old one
155 delete_question($previousid) ;
156 }
516cf3eb 157 }
158
159 // Delete redundant wrapped questions
26053641 160 if(is_array($oldwrappedquestions) && count($oldwrappedquestions)){
161 foreach ($oldwrappedquestions as $oldwrappedquestion) {
162 delete_question($oldwrappedquestion->id) ;
e9028ffc 163 }
4bc4ca50 164 }
516cf3eb 165
166 if (!empty($sequence)) {
0ff4bd08 167 $multianswer = new stdClass();
516cf3eb 168 $multianswer->question = $question->id;
169 $multianswer->sequence = implode(',', $sequence);
f34488b2 170 if ($oldid = $DB->get_field('question_multianswer', 'id', array('question' => $question->id))) {
516cf3eb 171 $multianswer->id = $oldid;
bb4b6010 172 $DB->update_record("question_multianswer", $multianswer);
516cf3eb 173 } else {
bb4b6010 174 $DB->insert_record("question_multianswer", $multianswer);
516cf3eb 175 }
176 }
177 }
178
94dbfb3a 179 function save_question($authorizedquestion, $form) {
e51efd7e 180 $question = qtype_multianswer_extract_question($form->questiontext);
516cf3eb 181 if (isset($authorizedquestion->id)) {
182 $question->id = $authorizedquestion->id;
516cf3eb 183 }
184
516cf3eb 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;
77fa3a0d 193 $form->options = clone($question->options);
516cf3eb 194 unset($question->options);
94dbfb3a 195 return parent::save_question($question, $form);
516cf3eb 196 }
197
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 }
205
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
77fa3a0d 212 $state->responses[$tmp[0]] = str_replace(array("&#0044;", "&#0045;"),
213 array(",", "-"), $tmp[1]);
516cf3eb 214 }
215 return true;
216 }
217
218 function save_session_and_responses(&$question, &$state) {
f34488b2 219 global $DB;
516cf3eb 220 $responses = $state->responses;
77fa3a0d 221 // encode - (hyphen) and , (comma) to &#0045; because they are used as
222 // delimiters
516cf3eb 223 array_walk($responses, create_function('&$val, $key',
77fa3a0d 224 '$val = str_replace(array(",", "-"), array("&#0044;", "&#0045;"), $val);
225 $val = "$key-$val";'));
516cf3eb 226 $responses = implode(',', $responses);
227
228 // Set the legacy answer field
f685e830 229 $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id));
516cf3eb 230 return true;
231 }
232
9203b705 233 function delete_question($questionid, $contextid) {
f34488b2 234 global $DB;
235 $DB->delete_records("question_multianswer", array("question" => $questionid));
9203b705
TH
236
237 parent::delete_question($questionid, $contextid);
516cf3eb 238 }
239
240 function get_correct_responses(&$question, &$state) {
f02c6f01 241 global $QTYPES;
516cf3eb 242 $responses = array();
243 foreach($question->options->questions as $key => $wrapped) {
103a800d 244 if (!empty($wrapped)){
8795a5ae 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 }
516cf3eb 253 }
254 }
255 return $responses;
256 }
257
869309b8 258 function get_possible_responses(&$question) {
259 global $QTYPES;
260 $responses = array();
261 foreach($question->options->questions as $key => $wrapped) {
103a800d 262 if (!empty($wrapped)){
869309b8 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) {
103a800d 279 if (!empty($wrapped)){
869309b8 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 }
287
45c4a5c7
TH
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 }
294
516cf3eb 295 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
458eb0d1 296 global $QTYPES, $CFG, $USER, $OUTPUT, $PAGE;
9fc3100f 297
516cf3eb 298 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
6463e8a6 299 $disabled = empty($options->readonly) ? '' : 'disabled="disabled"';
0ff4bd08 300 $formatoptions = new stdClass();
7347c60b 301 $formatoptions->noclean = true;
302 $formatoptions->para = false;
516cf3eb 303 $nameprefix = $question->name_prefix;
9fc3100f 304
73ca1421 305 // adding an icon with alt to warn user this is a fill in the gap question
306 // MDL-7497
ccffd412 307 if (!empty($USER->screenreader)) {
0c3c5493 308 echo "<img src=\"".$OUTPUT->pix_url('icon', 'qtype_'.$question->qtype)."\" ".
0d905d9f 309 "class=\"icon\" alt=\"".get_string('clozeaid','qtype_multichoice')."\" /> ";
73ca1421 310 }
06e2b0de 311
312 echo '<div class="ablock clearfix">';
9fc3100f 313
516cf3eb 314 $qtextremaining = format_text($question->questiontext,
77fa3a0d 315 $question->questiontextformat, $formatoptions, $cmoptions->course);
516cf3eb 316
5e8a85aa 317 $strfeedback = get_string('feedback', 'question');
516cf3eb 318
319 // The regex will recognize text snippets of type {#X}
320 // where the X can be any text not containg } or white-space characters.
6dbcacee 321 while (preg_match('~\{#([^[:space:]}]*)}~', $qtextremaining, $regs)) {
516cf3eb 322 $qtextsplits = explode($regs[0], $qtextremaining, 2);
323 echo $qtextsplits[0];
0bddf4b6 324 echo "<label>"; // MDL-7497
516cf3eb 325 $qtextremaining = $qtextsplits[1];
326
327 $positionkey = $regs[1];
df79079f 328 if (isset($question->options->questions[$positionkey]) && $question->options->questions[$positionkey] != ''){
516cf3eb 329 $wrapped = &$question->options->questions[$positionkey];
330 $answers = &$wrapped->options->answers;
907a3759 331 // $correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state);
516cf3eb 332
333 $inputname = $nameprefix.$positionkey;
e51efd7e 334 if (isset($state->responses[$positionkey])) {
1f8db780 335 $response = $state->responses[$positionkey];
e51efd7e 336 } else {
337 $response = null;
338 }
f26c5297 339 // echo "<p> multianswer positionkey $positionkey response $response state <pre>";print_r($state);echo "</pre></p>";
516cf3eb 340
341 // Determine feedback popup if any
342 $popup = '';
343 $style = '';
2b087056 344 $feedbackimg = '';
907a3759 345 $feedback = '' ;
346 $correctanswer = '';
f34488b2 347 $strfeedbackwrapped = $strfeedback;
f34488b2 348 $testedstate = clone($state);
f26c5297 349 if ($correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
907a3759 350 if ($options->readonly && $options->correct_responses) {
351 $delimiter = '';
352 if ($correctanswers) {
353 foreach ($correctanswers as $ca) {
354 switch($wrapped->qtype){
355 case 'numerical':
f34488b2 356 case 'shortanswer':
907a3759 357 $correctanswer .= $delimiter.$ca;
358 break ;
359 case 'multichoice':
360 if (isset($answers[$ca])){
361 $correctanswer .= $delimiter.$answers[$ca]->answer;
362 }
363 break ;
f34488b2 364 }
907a3759 365 $delimiter = ', ';
366 }
367 }
368 }
ba954145 369 if ($correctanswer != '' ) {
907a3759 370 $feedback = '<div class="correctness">';
5e8a85aa 371 $feedback .= get_string('correctansweris', 'question', s($correctanswer));
907a3759 372 $feedback .= '</div>';
907a3759 373 }
374 }
458eb0d1 375
516cf3eb 376 if ($options->feedback) {
377 $chosenanswer = null;
378 switch ($wrapped->qtype) {
dfa47f96 379 case 'numerical':
dfa47f96 380 case 'shortanswer':
516cf3eb 381 $testedstate = clone($state);
382 $testedstate->responses[''] = $response;
516cf3eb 383 foreach ($answers as $answer) {
f02c6f01 384 if($QTYPES[$wrapped->qtype]
bc2defd5 385 ->test_response($wrapped, $testedstate, $answer)) {
386 $chosenanswer = clone($answer);
387 break;
516cf3eb 388 }
389 }
390 break;
dfa47f96 391 case 'multichoice':
516cf3eb 392 if (isset($answers[$response])) {
393 $chosenanswer = clone($answers[$response]);
394 }
395 break;
396 default:
397 break;
398 }
399
400 // Set up a default chosenanswer so that all non-empty wrong
401 // answers are highlighted red
b7c81cf0 402 if (empty($chosenanswer) && $response != '') {
0ff4bd08 403 $chosenanswer = new stdClass();
516cf3eb 404 $chosenanswer->fraction = 0.0;
405 }
406
407 if (!empty($chosenanswer->feedback)) {
907a3759 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 {
5e8a85aa 412 $strfeedbackwrapped = get_string('feedback', 'question');
907a3759 413 }
414 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
516cf3eb 415 " onmouseout=\"return nd();\" ";
416 }
417
418 /// Determine style
d9935f29 419 if ($options->feedback && $response != '') {
2b087056 420 $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction).'"';
421 $feedbackimg = question_get_feedback_image($chosenanswer->fraction);
516cf3eb 422 } else {
423 $style = '';
2b087056 424 $feedbackimg = '';
516cf3eb 425 }
426 }
907a3759 427 if ($feedback !='' && $popup == ''){
f34488b2 428 $strfeedbackwrapped = get_string('correctanswer', 'qtype_multianswer');
907a3759 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 }
516cf3eb 433
434 // Print the input control
435 switch ($wrapped->qtype) {
dfa47f96 436 case 'shortanswer':
437 case 'numerical':
eba53585 438 $size = 1 ;
439 foreach ($answers as $answer) {
440 if (strlen(trim($answer->answer)) > $size ){
441 $size = strlen(trim($answer->answer));
442 }
f34488b2 443 }
eba53585 444 if (strlen(trim($response))> $size ){
445 $size = strlen(trim($response))+1;
446 }
447 $size = $size + rand(0,$size*0.15);
f34488b2 448 $size > 60 ? $size = 60 : $size = $size;
b119a40f 449 $styleinfo = "size=\"$size\"";
eba53585 450 /**
451 * Uncomment the following lines if you want to limit for small sizes.
f34488b2 452 * Results may vary with browsers see MDL-3274
eba53585 453 */
f34488b2 454 /*
eba53585 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 */
f34488b2 468
814a96fa 469 echo "<input $style $readonly $popup name=\"$inputname\"";
cdfaa838 470 echo " type=\"text\" value=\"".s($response)."\" ".$styleinfo." /> ";
ccffd412 471 if (!empty($feedback) && !empty($USER->screenreader)) {
b5d0cafc 472 echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
04d6ac46 473 }
2b087056 474 echo $feedbackimg;
516cf3eb 475 break;
dfa47f96 476 case 'multichoice':
e5ebbd53 477 if ($wrapped->options->layout == 0 ){
478 $outputoptions = '<option></option>'; // Default empty option
479 foreach ($answers as $mcanswer) {
77fa3a0d 480 $selected = '';
481 if ($response == $mcanswer->id) {
482 $selected = ' selected="selected"';
483 }
484 $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
cdfaa838 485 s($mcanswer->answer) . '</option>';
e5ebbd53 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)) {
b5d0cafc 499 echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
e5ebbd53 500 }
501 echo $feedbackimg;
502 }else if ($wrapped->options->layout == 1 || $wrapped->options->layout == 2){
e5ebbd53 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 }
0ff4bd08 515 $a = new stdClass();
e5ebbd53 516 $a->id = $question->name_prefix . $mcanswer->id;
e5ebbd53 517 $a->class = '';
518 $a->feedbackimg = '';
aeb15530 519
e5ebbd53 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);
516cf3eb 530 }
e5ebbd53 531 }
aeb15530 532
0bddf4b6 533 // Print the answer text: no automatic numbering
534
a9efae50 535 $a->text = format_text($mcanswer->answer, $mcanswer->answerformat, $formatoptions, $cmoptions->course);
aeb15530 536
e5ebbd53 537 // Print feedback if feedback is on
538 if (($options->feedback || $options->correct_responses) && ($checked )) { //|| $options->readonly
a9efae50 539 $a->feedback = format_text($mcanswer->feedback, $mcanswer->feedbackformat, $formatoptions, $cmoptions->course);
e5ebbd53 540 } else {
541 $a->feedback = '';
542 }
aeb15530 543
e5ebbd53 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 ?>
aeb15530 569
e5ebbd53 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>
aeb15530
PS
588 <?php }
589
e5ebbd53 590 }else {
591 echo "no valid layout";
e0c25647 592 }
aeb15530 593
e0c25647 594 break;
595 default:
0ff4bd08 596 $a = new stdClass();
aeb15530 597 $a->type = $wrapped->qtype ;
857caf3b 598 $a->sub = $positionkey;
599 print_error('unknownquestiontypeofsubquestion', 'qtype_multianswer','',$a);
e0c25647 600 break;
516cf3eb 601 }
73ca1421 602 echo "</label>"; // MDL-7497
516cf3eb 603 }
df79079f 604 else {
605 if(! isset($question->options->questions[$positionkey])){
ea632e88 606 echo $regs[0]."</label>";
df79079f 607 }else {
ea632e88 608 echo '</label><div class="error" >'.get_string('questionnotfound','qtype_multianswer',$positionkey).'</div>';
df79079f 609 }
610 }
611 }
516cf3eb 612
613 // Print the final piece of question text:
614 echo $qtextremaining;
f4b72cdb 615 $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
f34488b2 616 echo '</div>';
516cf3eb 617 }
618
619 function grade_responses(&$question, &$state, $cmoptions) {
f02c6f01 620 global $QTYPES;
516cf3eb 621 $teststate = clone($state);
622 $state->raw_grade = 0;
623 foreach($question->options->questions as $key => $wrapped) {
103a800d 624 if (!empty($wrapped)){
0bddf4b6 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;
516cf3eb 637 }
df79079f 638 }
516cf3eb 639 $state->raw_grade /= $question->defaultgrade;
640 $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0)
641 * $question->maxgrade;
642
643 if (empty($state->raw_grade)) {
644 $state->raw_grade = 0.0;
645 }
646 $state->penalty = $question->penalty * $question->maxgrade;
647
f30bbcaf 648 // mark the state as graded
649 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
650
516cf3eb 651 return true;
652 }
653
654 function get_actual_response($question, $state) {
f02c6f01 655 global $QTYPES;
516cf3eb 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]);
f02c6f01 660 $correct = $QTYPES[$wrapped->qtype]
516cf3eb 661 ->get_actual_response($wrapped, $teststate);
2280e147 662 $responses[$key] = implode(';', $correct);
516cf3eb 663 }
664 return $responses;
665 }
aeb15530 666
6f51ed72 667 /**
668 * @param object $question
455c3efa 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.
6f51ed72 672 */
673 function get_random_guess_score($question) {
674 $totalfraction = 0;
675 foreach (array_keys($question->options->questions) as $key){
455c3efa 676 $totalfraction += question_get_random_guess_score($question->options->questions[$key]);
6f51ed72 677 }
678 return $totalfraction / count($question->options->questions);
679 }
9fc3100f 680
b9bd6da4 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}.
690
691Note 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}
694
695Good luck!
696";
697 $form->feedback = "feedback";
698 $form->generalfeedback = "General feedback";
699 $form->fraction = 0;
700 $form->penalty = 0.1;
701 $form->versioning = 0;
702
703 if ($courseid) {
704 $course = $DB->get_record('course', array('id' => $courseid));
705 }
706
94dbfb3a 707 return $this->save_question($question, $form);
b9bd6da4 708 }
315559d3 709
516cf3eb 710}
711//// END OF CLASS ////
712
516cf3eb 713/////////////////////////////////////////////////////////////
714//// ADDITIONAL FUNCTIONS
715//// The functions below deal exclusivly with editing
dfa47f96 716//// of questions with question type 'multianswer'.
516cf3eb 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/////////////////////////////////////////////////////////////
721
0b346164 722// ANSWER_ALTERNATIVE regexes
723define("ANSWER_ALTERNATIVE_FRACTION_REGEX",
724 '=|%(-?[0-9]+)%');
725// for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
726define("ANSWER_ALTERNATIVE_ANSWER_REGEX",
e51efd7e 727 '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
0b346164 728define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX",
729 '.*?(?<!\\\\)(?=[~}]|$)');
0b346164 730define("ANSWER_ALTERNATIVE_REGEX",
e51efd7e 731 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
732 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
733 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
0b346164 734
735// Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
736define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2);
737define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1);
738define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3);
739define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5);
740
741// NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
742// for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
743define("NUMBER_REGEX",
744 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
745define("NUMERICAL_ALTERNATIVE_REGEX",
746 '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
747
748// Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
749define("NUMERICAL_CORRECT_ANSWER", 1);
750define("NUMERICAL_ABS_ERROR_MARGIN", 6);
751
752// Remaining ANSWER regexes
753define("ANSWER_TYPE_DEF_REGEX",
fd97082c 754 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)');
0b346164 755define("ANSWER_START_REGEX",
756 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
757
758define("ANSWER_REGEX",
759 ANSWER_START_REGEX
760 . '(' . ANSWER_ALTERNATIVE_REGEX
761 . '(~'
762 . ANSWER_ALTERNATIVE_REGEX
763 . ')*)\}' );
764
765// Parenthesis positions for singulars in ANSWER_REGEX
766define("ANSWER_REGEX_NORM", 1);
767define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3);
768define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4);
e5ebbd53 769define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR", 5);
770define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL", 6);
771define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 7);
fd97082c 772define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C", 8);
773define("ANSWER_REGEX_ALTERNATIVES", 9);
516cf3eb 774
7518b645 775function qtype_multianswer_extract_question($text) {
61dfe97e 776 // $text is an array [text][format][itemid]
0ff4bd08 777 $question = new stdClass();
dfa47f96 778 $question->qtype = 'multianswer';
516cf3eb 779 $question->questiontext = $text;
61dfe97e
PP
780 $question->generalfeedback['text'] = '';
781 $question->generalfeedback['format'] = '1';
782 $question->generalfeedback['itemid'] = '';
783
784 $question->options->questions = array();
516cf3eb 785 $question->defaultgrade = 0; // Will be increased for each answer norm
786
fe6ce234 787 for ($positionkey=1; preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs); ++$positionkey ) {
0ff4bd08 788 $wrapped = new stdClass();
61dfe97e
PP
789 $wrapped->generalfeedback['text'] = '';
790 $wrapped->generalfeedback['format'] = '1';
791 $wrapped->generalfeedback['itemid'] = '';
8795a5ae 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 }
516cf3eb 797 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
dfa47f96 798 $wrapped->qtype = 'numerical';
516cf3eb 799 $wrapped->multiplier = array();
800 $wrapped->units = array();
61dfe97e
PP
801 $wrapped->instructions['text'] = '';
802 $wrapped->instructions['format'] = '1';
803 $wrapped->instructions['itemid'] = '';
516cf3eb 804 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
dfa47f96 805 $wrapped->qtype = 'shortanswer';
516cf3eb 806 $wrapped->usecase = 0;
fd97082c 807 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
808 $wrapped->qtype = 'shortanswer';
809 $wrapped->usecase = 1;
516cf3eb 810 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
dfa47f96 811 $wrapped->qtype = 'multichoice';
516cf3eb 812 $wrapped->single = 1;
271e6dec 813 $wrapped->answernumbering = 0;
61dfe97e
PP
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'] = '';
e5ebbd53 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;
61dfe97e
PP
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'] = '';
e5ebbd53 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;
61dfe97e
PP
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'] = '';
e5ebbd53 851 $wrapped->layout = 2;
516cf3eb 852 } else {
2471ef86 853 print_error('unknownquestiontype', 'question', '', $answerregs[2]);
516cf3eb 854 return false;
855 }
856
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;
61dfe97e
PP
864 $wrapped->questiontext['text'] = $answerregs[0];
865 $wrapped->questiontext['format'] = 0 ;
866 $wrapped->questiontext['itemid'] = '' ;
867 $answerindex = 0 ;
516cf3eb 868
869 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
870 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
871 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
61dfe97e 872 $wrapped->fraction["$answerindex"] = '1';
e51efd7e 873 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]){
61dfe97e 874 $wrapped->fraction["$answerindex"] = .01 * $percentile;
516cf3eb 875 } else {
61dfe97e 876 $wrapped->fraction["$answerindex"] = '0';
516cf3eb 877 }
e51efd7e 878 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
83d22f70 879 $feedback = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
095b599a 880 $feedback = str_replace('\}', '}', $feedback);
61dfe97e
PP
881 $wrapped->feedback["$answerindex"]['text'] = str_replace('\#', '#', $feedback);
882 $wrapped->feedback["$answerindex"]['format'] = '1';
883 $wrapped->feedback["$answerindex"]['itemid'] = '';
e51efd7e 884 } else {
61dfe97e
PP
885 $wrapped->feedback["$answerindex"]['text'] = '';
886 $wrapped->feedback["$answerindex"]['format'] = '1';
887 $wrapped->feedback["$answerindex"]['itemid'] = '1';
888
e51efd7e 889 }
516cf3eb 890 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
6dbcacee 891 && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~', $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
e51efd7e 892 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
516cf3eb 893 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
61dfe97e 894 $wrapped->tolerance["$answerindex"] =
e51efd7e 895 $numregs[NUMERICAL_ABS_ERROR_MARGIN];
516cf3eb 896 } else {
61dfe97e 897 $wrapped->tolerance["$answerindex"] = 0;
516cf3eb 898 }
899 } else { // Tolerance can stay undefined for non numerical questions
1f8db780 900 // Undo quoting done by the HTML editor.
9c61c44f 901 $answer = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
095b599a 902 $answer = str_replace('\}', '}', $answer);
61dfe97e 903 $wrapped->answer["$answerindex"] = str_replace('\#', '#', $answer);
516cf3eb 904 }
905 $tmp = explode($altregs[0], $remainingalts, 2);
906 $remainingalts = $tmp[1];
61dfe97e 907 $answerindex++ ;
516cf3eb 908 }
909
910 $question->defaultgrade += $wrapped->defaultgrade;
911 $question->options->questions[$positionkey] = clone($wrapped);
61dfe97e
PP
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>";
516cf3eb 915 }
61dfe97e 916// echo"<p>questiontext<pre>";print_r($question->questiontext);echo"<pre></p>";
e51efd7e 917 $question->questiontext = $question->questiontext;
61dfe97e 918// echo"<p>question<pre>";print_r($question);echo"<pre></p>";
516cf3eb 919 return $question;
920}