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