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