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