MDL-20636 Fix potential bug with qtype_shortanswer correct answer display.
[moodle.git] / question / type / multianswer / renderer.php
CommitLineData
ab50232b
TH
1 <?php
2
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/**
20 * Multianswer question renderer classes.
21 * Handle shortanswer, numerical and various multichoice subquestions
22 *
23 * @package qtype_multianswer
24 * @copyright 2009 The Open University
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 */
27
28/**
29 * Base class for generating the bits of output common to multianswer
30 * (Cloze) questions.
31 * This render the main question text and transfer to the subquestions
32 * the task of display their input elements and status
33 * feedback, grade, correct answer(s)
34 *
fa6c8620 35 * @copyright 2010 Pierre Pichet
ab50232b
TH
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
38 class qtype_multianswer_renderer extends qtype_renderer {
39
40 public function formulation_and_controls(question_attempt $qa,
41 question_display_options $options) {
ab50232b
TH
42 $question = $qa->get_question();
43
44 $result = '';
45
46 $qtextremaining = $question->format_questiontext();
47
48 $strfeedback = get_string('feedback', 'quiz');
49
50 // The regex will recognize text snippets of type {#X}
51 // where the X can be any text not containg } or white-space characters.
52
53 while (ereg('\{#([^[:space:]}]*)}', $qtextremaining, $regs)) {
54 $qtextsplits = explode($regs[0], $qtextremaining, 2);
55 $result .= $qtextsplits[0];
56 // $result .= "<label>"; // MDL-7497
57 $qtextremaining = $qtextsplits[1];
58
59 $positionkey = $regs[1];
60 // transfer to the specific subquestion renderer
61 if (isset($question->subquestions[$positionkey]) && $question->subquestions[$positionkey] != ''){
62 $subquestion = &$question->subquestions[$positionkey];
63 $qout = $subquestion->get_renderer();
64 $qa->subquestionindex = $positionkey ;
65 $result .= $qout->formulation_and_controls($qa,$options); //
66
67 // $result .= "</label>"; // MDL-7497
68
69 } else {
70 if(! isset($question->subquestions[$positionkey])){
71 $result .= $regs[0]; //."</label>";
72 }else { //</label>
73 $result .= '<div class="error" >'.get_string('questionnotfound','qtype_multianswer',$positionkey).'</div>';
74 }
75 }
76 } // end while
77
78 // Print the final piece of question text:
79 $result .= $qtextremaining;
80
81 return $result;
82 }
83
84
85 public function correct_response(question_attempt $qa) {
fa6c8620 86 return '';
ab50232b
TH
87 }
88
89}
90
91
92/**
93 * Subclass for generating the bits of output specific to shortanswer
94 * subquestions.
95 *
fa6c8620 96 * @copyright 2009 The Open University
ab50232b
TH
97 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
98 */
99 require_once($CFG->dirroot . '/question/type/shortanswer/renderer.php');
100 class qtype_multianswer_shortanswer_renderer extends qtype_shortanswer_renderer {
101 /**
102 * function normally part of core_question_renderer
103 * that is copy here
104 */
105
106 public function correct_response(question_attempt $qa) {
107 $questiontot = $qa->get_question();
108 $subquestion = $questiontot->subquestions[$qa->subquestionindex];
109 $answer = reset($subquestion->get_answers());
110 if (!$answer) {
111 return '';
112 }
113 return get_string('correctansweris', 'qtype_multianswer', s($answer->answer));
114 }
115
116 public function formulation_and_controls(question_attempt $qa,
117 question_display_options $options) {
118 $questiontot = $qa->get_question();
119 $subquestion = $questiontot->subquestions[$qa->subquestionindex];
120 $answername = $subquestion->fieldid.'answer' ;
121 $response = $qa->get_last_qt_var($answername);
122 $inputname = $qa->get_qt_field_name($answername);
123 $size = 1 ;
124 foreach ($subquestion->answers as $answer) {
125 if (strlen(trim($answer->answer)) > $size ){
126 $size = strlen(trim($answer->answer));
127 }
128
129 }
130 if (strlen(trim($response))> $size ){
131 $size = strlen(trim($response))+1;
132 }
133 $size = round($size + rand(0,$size*0.15));
134 $size > 60 ? $size = 60 : $size = $size;
135 $inputattributes = array(
136 'type' => 'text',
137 'name' => $inputname,
138 'value' => $response,
139 'id' => $inputname,
140 'size' => $size,
141 );
142 // readonly cannot by put in input
143 if ($options->readonly) {
144 $inputattributes['readonly'] = 'readonly';
145
146 }
147 $class = '';
148 $feedbackimg = '';
149 // Determine feedback popup if any
150 $popup = '';
151 $feedback = '' ;
152 $fraction = 0 ;
153
154 if ($options->feedback) {
155 $answer = $subquestion->get_matching_answer(array('answer' => $response));
156 if ($answer) {
157 $inputattributes['class'] = question_get_feedback_class($answer->fraction);
158 $feedbackimg = question_get_feedback_image($answer->fraction);
159 $fraction = $answer->fraction ;
160 if ($answer->feedback) {
161 // $feedback .= $subquestion->format_text(htmlspecialchars($answer->feedback, ENT_QUOTES ));
162 $feedback .= $subquestion->format_text($answer->feedback );
163 }
164 } else {
165 $inputattributes['class'] = question_get_feedback_class(0);
166 $feedbackimg = question_get_feedback_image(0);
167 }
168 }
169 $readonly ='';
170 if ($options->readonly) {
171 $inputattributes['readonly'] = 'readonly';
172 $readonly = 'readonly="readonly"';
173 }
174 // determine popup
175 // answer feedback (specific)i.e if options->feedback already set
176 // subquestion status correctness or Finished validator if correctness
177 // Correct response
178 // marks
179 if ($options->feedback) {
180 $strfeedbackwrapped = 'Response Status';
181 $subfraction = '' ;
182 if ($options->correctness ) {
183 if ( ! $answer ){
184 $state = $qa->get_state();
185 $state = question_state::$invalid;
186 $strfeedbackwrapped .= ":<font color=red >".$state->default_string()."</font>" ;
187 $feedback = "<font color=red >".$subquestion->get_validation_error(array('answer' => $response)) ."</font>";
188 }else {
189 $state = $qa->get_state();
190 $state = question_state::graded_state_for_fraction($fraction);
191 $strfeedbackwrapped .= ":".$state->default_string();
192 }
193 }
194
195 if ($options->correctresponse ) {
196 $feedback .= "<br />".$this->correct_response( $qa);//
197 }
198 if ($options->marks ) {
199 $res = $subquestion->grade_response(array('answer'=>$response)); // fraction=>state
200 $subfraction = $res[0];
201 $subgrade= $subfraction * $subquestion->defaultmark ;
202 $feedback .= "<br />".$questiontot->mark_summary($options, $subquestion->defaultmark , $subgrade );
203
204 }
205 $feedback = str_replace("'","\'",$feedback);
206 $feedback = str_replace('"',"\'",$feedback);
207 $strfeedbackwrapped = str_replace("'"," ",$strfeedbackwrapped);
208 $strfeedbackwrapped = str_replace('"',"\'",$strfeedbackwrapped);
209 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
210 " onmouseout=\"return nd();\" ";
211 } //if feedback
212
213 $result = '';
214 $result .= "<label>"; // MDL-7497
215 $classes = 'control';
216 $result .="<span $popup >" ;
217 $input = html_writer::empty_tag('input', $inputattributes) ;
218 $result .= $input;
219 if (!empty($feedback) && !empty($USER->screenreader)) {
220 $result .= "<img src=\"$CFG->pixpath/i/feedback.gif\" alt=\"$feedback\" />";
221 }
222 $result .= $feedbackimg."</span>";
223 $result .= "</label>"; // MDL-7497
224
225 return $result;
226 }
227
228}
229
230/**
231 * As multianswer have specific display requirements for multichoice display
232 * a new class was defined although largely following the multichoice one
233 */
234
235abstract class qtype_multianswer_multichoice_renderer_base extends qtype_renderer {
236 abstract protected function get_input_type();
237
238 abstract protected function get_input_name(question_attempt $qa, $value);
239
240 abstract protected function get_input_value($value);
241
242 abstract protected function get_input_id(question_attempt $qa, $value);
243
244 abstract protected function is_choice_selected($response, $value);
245
246 abstract protected function is_right(question_answer $ans);
247
248 abstract protected function get_response(question_attempt $qa);
249
250
251
252 public function specific_feedback(question_attempt $qa) {
253 return '';
254 }
255
256 public function formulation_and_controls(question_attempt $qa,
257 question_display_options $options) {
258
259 $questiontot = $qa->get_question();
260 $subquestion = $questiontot->subquestions[$qa->subquestionindex];
261 $order = $subquestion->get_order($qa); //array_keys($question->answers); //
262 $response = $this->get_response($qa);
263 $inputattributes = array(
264 'type' => $this->get_input_type(),
265 );
266
267 if ($options->readonly) {
268 $inputattributes['disabled'] = 'disabled';
269 }
270 $radiobuttons = array();
271 $feedbackimg = array();
272 $feedback = array();
273 $classes = array();
274 $totfraction = 0 ;
275 $nullresponse = true ;
276 foreach ($order as $value => $ansid) {
277 $ans = $subquestion->answers[$ansid];
278 $inputattributes['name'] = $this->get_input_name($qa, $value);
279 // echo "<p>name $value name".$inputattributes['name']." </p>";
280 $inputattributes['value'] = $this->get_input_value($value);
281 $inputattributes['id'] = $this->get_input_id($qa, $value);
282 if ($subquestion->single) {
283 $isselected = $this->is_choice_selected($response, $value);
284 } else {
285 $isselected = $this->is_choice_selected($response,$value) ; //$subquestion->field( $value));
286 }
287 if ($isselected) {
288 $inputattributes['checked'] = 'checked';
289 $totfraction += $ans->fraction ;
290 $nullresponse = false ;
291 } else {
292 unset($inputattributes['checked']);
293 }
294 $radiobuttons[] = html_writer::empty_tag('input', $inputattributes) .
295 html_writer::tag('label', $subquestion->format_text($ans->answer), array('for' => $inputattributes['id']));
296
297 if (($options->feedback || $options->correctresponse) && $response !== -1) {
298 $feedbackimg[] = question_get_feedback_image($this->is_right($ans), $isselected && $options->feedback);
299 } else {
300 $feedbackimg[] = '';
301 }
302 if (($options->feedback || $options->correctresponse) && $isselected) {
303 $feedback[] = $subquestion->format_text($ans->feedback);
304 } else {
305 $feedback[] = '';
306 }
307 $class = 'r' . ($value % 2);
308 if ($options->correctresponse && $ans->fraction > 0) {
309 $class .= ' ' . question_get_feedback_class($ans->fraction);
310 }
311 $classes[] = $class;
312 }
313
314 $result = '' ;
315
316 $answername = 'answer' ;
317 if ($subquestion->layout == 1 ){
318 $result .= html_writer::start_tag('div', array('class' => 'ablock'));
319
320 $result .= html_writer::start_tag('table', array('class' => $answername));
321 foreach ($radiobuttons as $key => $radio) {
322 $result .= html_writer::start_tag('tr', array('class' => $answername));
323 $result .= html_writer::start_tag('td', array('class' => $answername));
324 $result .= html_writer::tag('span',$radio . $feedbackimg[$key] . $feedback[$key], array('class' => $classes[$key])) . "\n";
325 $result .= html_writer::end_tag('td');
326 $result .= html_writer::end_tag('tr');
327 }
328 $result .= html_writer::end_tag('table'); // answer
329
330 $result .= html_writer::end_tag('div'); // ablock
331 }
332 if ($subquestion->layout == 2 ){
333 $result .= html_writer::start_tag('div', array('class' => 'ablock'));
334 $result .= html_writer::start_tag('table', array('class' => $answername));
335 $result .= html_writer::start_tag('tr', array('class' => $answername));
336 foreach ($radiobuttons as $key => $radio) {
337 $result .= html_writer::start_tag('td', array('class' => $answername));
338 $result .= html_writer::tag('span',$radio . $feedbackimg[$key] . $feedback[$key]
339 , array('class' => $classes[$key])) . "\n";
340 $result .= html_writer::end_tag('td');
341 }
342 $result .= html_writer::end_tag('tr');
343 $result .= html_writer::end_tag('table'); // answer
344
345 $result .= html_writer::end_tag('div'); // ablock
346
347 }
348 if ($options->feedback ) {
349 $result .= html_writer::start_tag('div', array('class' => 'outcome'));
350
351 if ($options->correctness ) {
352 if ( $nullresponse ){
353 $state = $qa->get_state();
354 $state = question_state::$invalid;
355 $result1 = $state->default_string();
356 $result .= html_writer::nonempty_tag('div',$result1,
357 array('class' => 'validationerror'));
358 $result1 = ($subquestion->single) ? get_string('singleanswer', 'quiz') : get_string('multipleanswers', 'quiz');
359 $result .= html_writer::nonempty_tag('div', $result1,
360 array('class' => 'validationerror'))
361 ;
362 }else {
363 $state = $qa->get_state();
364 $state = question_state::graded_state_for_fraction($totfraction);
365 $result1 = $state->default_string();
366 $result .= html_writer::nonempty_tag('div', $result1,
367 array('class' => 'outcome'));
368 }
369 }
370
371
372 if ($options->correctresponse ) {
373 $result1 = $this->correct_response($qa);
374 $result .= html_writer::nonempty_tag('div',$result1, array('class' => 'outcome'))
375 ;
376 }
377 if ($options->marks ) {
378 $subgrade= $totfraction * $subquestion->defaultmark ;
379 $result .= $questiontot->mark_summary($options, $subquestion->defaultmark , $subgrade );
380 }
381
382 if ($qa->get_state() == question_state::$invalid) {
383 $result .= html_writer::nonempty_tag('div', array('class' => 'validationerror'),
384 $subquestion->get_validation_error($qa->get_last_qt_data()));
385 }
386 $result .= html_writer::end_tag('div');
387
388 }
389 return $result;
390 }
391
392
393}
394
395
396class qtype_multianswer_multichoice_single_renderer extends qtype_multianswer_multichoice_renderer_base {
397 protected function get_input_type() {
398 return 'radio';
399 }
400
401 protected function is_choice_selected($response, $value) {
402 return $response == $value ;
403 }
404 protected function is_right(question_answer $ans) {
405 return $ans->fraction > 0.9999999;
406 }
407 protected function get_input_name(question_attempt $qa, $value) {
408 $questiontot = $qa->get_question();
409 $subquestion = $questiontot->subquestions[$qa->subquestionindex];
410 $answername = $subquestion->fieldid.'answer';
411 return $qa->get_qt_field_name($answername);
412 }
413 protected function get_input_value($value) {
414 return $value;
415 }
416
417 protected function get_input_id(question_attempt $qa, $value) {
418 $questiontot = $qa->get_question();
419 $subquestion = $questiontot->subquestions[$qa->subquestionindex];
420 $answername = $subquestion->fieldid.'answer';
421 return $qa->get_qt_field_name($answername);
422 }
423
424 protected function get_response(question_attempt $qa) {
425 $questiontot = $qa->get_question();
426 $subquestion = $questiontot->subquestions[$qa->subquestionindex];
427 return $qa->get_last_qt_var($subquestion->fieldid.'answer', -1);
428
429 }
430 public function correct_response(question_attempt $qa) {
431 $questiontot = $qa->get_question();
432 $subquestion = $questiontot->subquestions[$qa->subquestionindex];
433
434 foreach ($subquestion->answers as $ans) {
435 if ($ans->fraction > 0.9999999) {
436 return get_string('correctansweris', 'qtype_multichoice',
437 $subquestion->format_text($ans->answer));
438 }
439 }
440
441 return '';
442 }
443
444}
445class qtype_multianswer_multichoice_single_inline_renderer extends qtype_multianswer_multichoice_single_renderer {
446 protected function get_input_type() {
447 return 'select';
448 }
449
450 public function formulation_and_controls(question_attempt $qa,
451 question_display_options $options) {
452 $questiontot = $qa->get_question();
453 $subquestion = $questiontot->subquestions[$qa->subquestionindex];
454 $answers = $subquestion->answers;
455 $correctanswers = $subquestion->get_correct_response();
456 foreach($correctanswers as $key=> $value){
457 $correct = $value ;
458 }
459 $order = $subquestion->get_order($qa);
460 $response = $this->get_response($qa);
461 $currentanswer = $response ;
462 $answername = $subquestion->fieldid.'answer';
463 $inputname = $qa->get_qt_field_name($answername);
464 $inputattributes = array(
465 'type' => $this->get_input_type(),
466 'name' => $inputname,
467 );
468
469 if ($options->readonly) {
470 $inputattributes['disabled'] = 'disabled';
471 $readonly = 'disabled ="disabled"';
472 }
473 $choices = array();
474 $popup = '';
475 $feedback = '' ;
476 $answer = '' ;
477 $classes = 'control';
478 $feedbackimage = '';
479 $fraction = 0 ;
480 $chosen = 0 ;
481
482 foreach ($order as $value => $ansid) {
483 $mcanswer = $subquestion->answers[$ansid];
484 $choices[$value] = strip_tags($mcanswer->answer);
485 $selected = '';
486 $isselected = false ;
487 if( $response != ''){
488 $isselected = $this->is_choice_selected($response, $value);
489 }
490 if ($isselected) {
491 $chosen = $value ;
492 $answer = $mcanswer ;
493 $fraction = $mcanswer->fraction ;
494 $selected = ' selected="selected"';
495 }
496 }
497 if ($options->feedback) {
498 if ($answer) {
499 $classes .= ' ' . question_get_feedback_class($fraction);
500 $feedbackimage = question_get_feedback_image($answer->fraction);
501 if ($answer->feedback) {
502 $feedback .= $subquestion->format_text($answer->feedback);
503 }
504 } else {
505 $classes .= ' ' . question_get_feedback_class(0);
506 $feedbackimage = question_get_feedback_image(0);
507 }
508 }
509 // determine popup
510 // answer feedback (specific)i.e if options->feedback already set
511 // subquestion status correctness or Finished validator if correctness
512 // Correct response
513 // marks
514 $strfeedbackwrapped = 'Response Status';
515 if ($options->feedback ) {
516 $feedback = get_string('feedback', 'quiz').":".$feedback."<br />";
517
518 if ($options->correctness ) {
519 if ( ! $answer ){
520 $state = $qa->get_state();
521 $state = question_state::$invalid;
522 $strfeedbackwrapped .= ":<font color=red >".$state->default_string()."</font>" ;
523 $feedback = "<font color=red >".get_string('singleanswer', 'quiz') ."</font><br />";
524 }else {
525 $state = $qa->get_state();
526 $state = question_state::graded_state_for_fraction($fraction);
527 $strfeedbackwrapped .= ":".$state->default_string();
528 }
529 }
530
531
532 if ($options->correctresponse ) {
533 $feedback .= $this->correct_response($qa)."<br />";
534 }
535 if ($options->marks ) {
536 $subgrade= $fraction * $subquestion->defaultmark ;
537 $feedback .= $questiontot->mark_summary($options, $subquestion->defaultmark , $subgrade );
538 }
539
540 $feedback .= '</div>';
541 }
542
543 if ($options->feedback ) {
544 // need to replace ' and " as they could break the popup string
545 // as the text comes from database, slashes have been removed
546 // addslashes will not work as it keeps the "
547 // HTML &#039; for ' does not work
548 $feedback = str_replace("'","\'",$feedback);
549 $feedback = str_replace('"',"\'",$feedback);
550 $strfeedbackwrapped = str_replace("'","\'",$strfeedbackwrapped);
551 $strfeedbackwrapped = str_replace('"',"\'",$strfeedbackwrapped);
552
553 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
554 " onmouseout=\"return nd();\" ";
555 }
556 $result = '';
557
558 $result .= "<span $popup >";
559 $result .= html_writer::start_tag('span', array('class' => $classes), '');
560
561 $result .=
562 choose_from_menu($choices, $inputname, $chosen,
563 ' ', '', '', true, $options->readonly) . $feedbackimage ;
564 $result .= html_writer::end_tag('span');
565 $result .= html_writer::end_tag('span');
566
567
568 return $result;
569 }
570
571 protected function format_choices($question) {
572 $choices = array();
573 foreach ($question->get_choice_order() as $key => $choiceid) {
574 $choices[$key] = strip_tags($question->format_text($question->choices[$choiceid]));
575 }
576 return $choices;
577 }
578
579
580}
581class qtype_multianswer_multichoice_multi_renderer extends qtype_multianswer_multichoice_renderer_base {
582 protected function get_input_type() {
583 return 'checkbox';
584 }
585
586 protected function get_input_name(question_attempt $qa, $value) {
587 $questiontot = $qa->get_question();
588 $subquestion = $questiontot->subquestions[$qa->subquestionindex];
589 return $qa->get_qt_field_name($subquestion->fieldid.'choice'. $value);
590 }
591
592 protected function get_input_value($value) {
593 return 1;
594 }
595
596 protected function get_input_id(question_attempt $qa, $value) {
597 return $this->get_input_name($qa, $value);
598 }
599
600 protected function get_response(question_attempt $qa) {
601 $responses = $qa->get_last_qt_data();
602 $questiontot = $qa->get_question();
603 $subresponses =$questiontot->decode_subquestion_responses($responses);
604 if( isset($subresponses[$qa->subquestionindex])) {
605 return $subresponses[$qa->subquestionindex] ;
606 }else{
607 return '';
608 }
609 }
610
611 protected function is_choice_selected($response, $value) {
612 return isset($response['choice'.$value]);
613 }
614
615 protected function is_right(question_answer $ans) {
616 return $ans->fraction > 0;
617 }
618
619 public function correct_response(question_attempt $qa) {
620 $questiontot = $qa->get_question();
621 $subquestion = $questiontot->subquestions[$qa->subquestionindex];
622
623 $right = array();
624 foreach ($subquestion->answers as $ans) {
625 if ($ans->fraction > 0) {
626 $right[] = $subquestion->format_text($ans->answer);
627 }
628 }
629
630 if (!empty($right)) {
631 return get_string('correctansweris', 'qtype_multichoice',
632 implode(', ', $right));
633
634 }
635 return '';
636 }
637
638
639
640}