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