MDL-29691 Improve numerical format in multianswer
[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) {
7ac7977c 70
42a5b055
TH
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 */
7ac7977c
TH
102abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
103
104 abstract public function subquestion(question_attempt $qa,
105 question_display_options $options, $index,
106 question_graded_automatically $subq);
107
108 /**
109 * Render the feedback pop-up contents.
36e91f1d 110 *
7ac7977c
TH
111 * @param question_graded_automatically $subq the subquestion.
112 * @param float $fraction the mark the student got. null if this subq was not answered.
113 * @param string $feedbacktext the feedback text, already processed with format_text etc.
114 * @param string $rightanswer the right answer, already processed with format_text etc.
115 * @param question_display_options $options the display options.
116 * @return string the HTML for the feedback popup.
117 */
118 protected function feedback_popup(question_graded_automatically $subq,
119 $fraction, $feedbacktext, $rightanswer, question_display_options $options) {
120
7ac7977c
TH
121 $feedback = array();
122 if ($options->correctness) {
123 if (is_null($fraction)) {
124 $state = question_state::$gaveup;
125 } else {
126 $state = question_state::graded_state_for_fraction($fraction);
127 }
128 $feedback[] = $state->default_string(true);
129 }
130
5d0f5f86 131 if ($options->feedback && $feedbacktext) {
bca80658
TH
132 $feedback[] = $feedbacktext;
133 }
134
7ac7977c
TH
135 if ($options->rightanswer) {
136 $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer);
137 }
138
139 $subfraction = '';
5d0f5f86
TH
140 if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->maxmark > 0
141 && (!is_null($fraction) || $feedback)) {
7ac7977c
TH
142 $a = new stdClass();
143 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
144 $a->max = format_float($subq->maxmark, $options->markdp);
145 $feedback[] = get_string('markoutofmax', 'question', $a);
146 }
147
bca80658
TH
148 if (!$feedback) {
149 return '';
150 }
151
7ac7977c
TH
152 return html_writer::tag('span', implode('<br />', $feedback),
153 array('class' => 'feedbackspan accesshide'));
154 }
155}
156
157
158/**
159 * Subclass for generating the bits of output specific to shortanswer
160 * subquestions.
161 *
162 * @copyright 2011 The Open University
163 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
164 */
165class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_renderer_base {
ab50232b 166
42a5b055
TH
167 public function subquestion(question_attempt $qa, question_display_options $options,
168 $index, question_graded_automatically $subq) {
169
170 $fieldprefix = 'sub' . $index . '_';
171 $fieldname = $fieldprefix . 'answer';
7ac7977c 172
42a5b055 173 $response = $qa->get_last_qt_var($fieldname);
7ac7977c
TH
174 if ($subq->qtype->name() == 'shortanswer') {
175 $matchinganswer = $subq->get_matching_answer(array('answer' => $response));
3a6eb8ef 176 } else if ($subq->qtype->name() == 'numerical') {
ad1a52e0
TH
177 list($value, $unit, $multiplier) = $subq->ap->apply_units($response, '');
178 $matchinganswer = $subq->get_matching_answer($value, 1);
7ac7977c
TH
179 } else {
180 $matchinganswer = $subq->get_matching_answer($response);
181 }
182
42a5b055 183 if (!$matchinganswer) {
ad1a52e0
TH
184 if (is_null($response) || $response === '') {
185 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
186 } else {
187 $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML);
188 }
ab50232b 189 }
42a5b055
TH
190
191 // Work out a good input field size.
192 $size = max(1, strlen(trim($response)) + 1);
193 foreach ($subq->answers as $ans) {
194 $size = max($size, strlen(trim($ans->answer)));
ab50232b 195 }
42a5b055 196 $size = min(60, round($size + rand(0, $size*0.15)));
b05659dd 197 // The rand bit is to make guessing harder.
42a5b055 198
ab50232b
TH
199 $inputattributes = array(
200 'type' => 'text',
42a5b055 201 'name' => $qa->get_qt_field_name($fieldname),
ab50232b 202 'value' => $response,
42a5b055 203 'id' => $qa->get_qt_field_name($fieldname),
ab50232b
TH
204 'size' => $size,
205 );
ab50232b
TH
206 if ($options->readonly) {
207 $inputattributes['readonly'] = 'readonly';
ab50232b 208 }
ab50232b 209
42a5b055
TH
210 $feedbackimg = '';
211 if ($options->correctness) {
7ac7977c
TH
212 $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
213 $feedbackimg = $this->feedback_image($matchinganswer->fraction);
ab50232b 214 }
42a5b055 215
7ac7977c
TH
216 if ($subq->qtype->name() == 'shortanswer') {
217 $correctanswer = $subq->get_matching_answer($subq->get_correct_response());
218 } else {
219 $correctanswer = $subq->get_correct_answer();
220 }
42a5b055 221
7ac7977c
TH
222 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
223 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
224 $qa, 'question', 'answerfeedback', $matchinganswer->id),
225 s($correctanswer->answer), $options);
226
ad87b3a2 227 $output = html_writer::start_tag('span', array('class' => 'subquestion'));
d3187c29
FM
228 $output .= html_writer::tag('label', get_string('answer'),
229 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
7ac7977c
TH
230 $output .= html_writer::empty_tag('input', $inputattributes);
231 $output .= $feedbackimg;
232 $output .= $feedbackpopup;
ad87b3a2 233 $output .= html_writer::end_tag('span');
42a5b055 234
7ac7977c
TH
235 return $output;
236 }
237}
238
239
240/**
241 * Render an embedded multiple-choice question that is displayed as a select menu.
242 *
dcedbb0e 243 * @copyright 2011 The Open University
7ac7977c
TH
244 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
245 */
246class qtype_multianswer_multichoice_inline_renderer
247 extends qtype_multianswer_subq_renderer_base {
248
249 public function subquestion(question_attempt $qa, question_display_options $options,
250 $index, question_graded_automatically $subq) {
251
252 $fieldprefix = 'sub' . $index . '_';
253 $fieldname = $fieldprefix . 'answer';
254
255 $response = $qa->get_last_qt_var($fieldname);
256 $choices = array();
257 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
258 $rightanswer = null;
259 foreach ($subq->get_order($qa) as $value => $ansid) {
260 $ans = $subq->answers[$ansid];
261 $choices[$value] = $subq->format_text($ans->answer, $ans->answerformat,
262 $qa, 'question', 'answer', $ansid);
263 if ($subq->is_choice_selected($response, $value)) {
264 $matchinganswer = $ans;
ab50232b 265 }
7ac7977c 266 }
42a5b055 267
7ac7977c
TH
268 $inputattributes = array(
269 'id' => $qa->get_qt_field_name($fieldname),
270 );
271 if ($options->readonly) {
272 $inputattributes['disabled'] = 'disabled';
ab50232b 273 }
ab50232b 274
7ac7977c
TH
275 $feedbackimg = '';
276 if ($options->correctness) {
277 $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
278 $feedbackimg = $this->feedback_image($matchinganswer->fraction);
279 }
3211569a 280 $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname),
7ac7977c
TH
281 $response, array('' => ''), $inputattributes);
282
283 $order = $subq->get_order($qa);
b8447700
TH
284 $correctresponses = $subq->get_correct_response();
285 $rightanswer = $subq->answers[$order[reset($correctresponses)]];
ad1a52e0
TH
286 if (!$matchinganswer) {
287 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
288 }
7ac7977c
TH
289 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
290 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
291 $qa, 'question', 'answerfeedback', $matchinganswer->id),
292 $subq->format_text($rightanswer->answer, $rightanswer->answerformat,
293 $qa, 'question', 'answer', $rightanswer->id), $options);
294
ad87b3a2 295 $output = html_writer::start_tag('span', array('class' => 'subquestion'));
d3187c29
FM
296 $output .= html_writer::tag('label', get_string('answer'),
297 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
7ac7977c 298 $output .= $select;
42a5b055
TH
299 $output .= $feedbackimg;
300 $output .= $feedbackpopup;
ad87b3a2 301 $output .= html_writer::end_tag('span');
ab50232b 302
42a5b055
TH
303 return $output;
304 }
ab50232b
TH
305}
306
12c6e008 307
ab50232b 308/**
dcedbb0e
TH
309 * Render an embedded multiple-choice question vertically, like for a normal
310 * multiple-choice question.
12c6e008
TH
311 *
312 * @copyright 2010 Pierre Pichet
313 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
ab50232b 314 */
dcedbb0e 315class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_subq_renderer_base {
ab50232b 316
dcedbb0e
TH
317 public function subquestion(question_attempt $qa, question_display_options $options,
318 $index, question_graded_automatically $subq) {
12c6e008 319
dcedbb0e
TH
320 $fieldprefix = 'sub' . $index . '_';
321 $fieldname = $fieldprefix . 'answer';
322 $response = $qa->get_last_qt_var($fieldname);
12c6e008 323
ab50232b 324 $inputattributes = array(
dcedbb0e
TH
325 'type' => 'radio',
326 'name' => $qa->get_qt_field_name($fieldname),
12c6e008 327 );
ab50232b
TH
328 if ($options->readonly) {
329 $inputattributes['disabled'] = 'disabled';
330 }
dcedbb0e
TH
331
332 $result = $this->all_choices_wrapper_start();
333 $fraction = null;
334 foreach ($subq->get_order($qa) as $value => $ansid) {
335 $ans = $subq->answers[$ansid];
336
337 $inputattributes['value'] = $value;
338 $inputattributes['id'] = $inputattributes['name'] . $value;
339
340 $isselected = $subq->is_choice_selected($response, $value);
ab50232b
TH
341 if ($isselected) {
342 $inputattributes['checked'] = 'checked';
dcedbb0e 343 $fraction = $ans->fraction;
ab50232b
TH
344 } else {
345 unset($inputattributes['checked']);
346 }
ab50232b 347
ab50232b 348 $class = 'r' . ($value % 2);
dcedbb0e
TH
349 if ($options->correctness && $isselected) {
350 $feedbackimg = $this->feedback_image($ans->fraction);
351 $class .= ' ' . $this->feedback_class($ans->fraction);
352 } else {
353 $feedbackimg = '';
ab50232b 354 }
12c6e008 355
dcedbb0e
TH
356 $result .= $this->choice_wrapper_start($class);
357 $result .= html_writer::empty_tag('input', $inputattributes);
358 $result .= html_writer::tag('label', $subq->format_text($ans->answer,
359 $ans->answerformat, $qa, 'question', 'answer', $ansid),
360 array('for' => $inputattributes['id']));
361 $result .= $feedbackimg;
362
363 if ($options->feedback && $isselected && trim($ans->feedback)) {
364 $result .= html_writer::tag('div',
365 $subq->format_text($ans->feedback, $ans->feedbackformat,
366 $qa, 'question', 'answerfeedback', $ansid),
367 array('class' => 'specificfeedback'));
ab50232b 368 }
12c6e008 369
dcedbb0e 370 $result .= $this->choice_wrapper_end();
12c6e008
TH
371 }
372
dcedbb0e 373 $result .= $this->all_choices_wrapper_end();
12c6e008 374
ad1a52e0 375 $feedback = array();
dcedbb0e
TH
376 if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
377 $subq->maxmark > 0) {
378 $a = new stdClass();
379 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
380 $a->max = format_float($subq->maxmark, $options->markdp);
ab50232b 381
ad1a52e0
TH
382 $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
383 }
384
385 if ($options->rightanswer) {
386 foreach ($subq->answers as $ans) {
387 if (question_state::graded_state_for_fraction($ans->fraction) ==
388 question_state::$gradedright) {
389 $feedback[] = get_string('correctansweris', 'qtype_multichoice',
390 $subq->format_text($ans->answer, $ans->answerformat,
391 $qa, 'question', 'answer', $ansid));
392 break;
393 }
394 }
12c6e008 395 }
ab50232b 396
ad1a52e0
TH
397 $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
398
dcedbb0e 399 return $result;
12c6e008
TH
400 }
401
dcedbb0e
TH
402 /**
403 * @param string $class class attribute value.
404 * @return string HTML to go before each choice.
405 */
406 protected function choice_wrapper_start($class) {
407 return html_writer::start_tag('div', array('class' => $class));
12c6e008
TH
408 }
409
dcedbb0e
TH
410 /**
411 * @return string HTML to go after each choice.
412 */
413 protected function choice_wrapper_end() {
414 return html_writer::end_tag('div');
12c6e008
TH
415 }
416
dcedbb0e
TH
417 /**
418 * @return string HTML to go before all the choices.
419 */
420 protected function all_choices_wrapper_start() {
421 return html_writer::start_tag('div', array('class' => 'answer'));
12c6e008
TH
422 }
423
dcedbb0e
TH
424 /**
425 * @return string HTML to go after all the choices.
426 */
427 protected function all_choices_wrapper_end() {
428 return html_writer::end_tag('div');
12c6e008 429 }
dcedbb0e 430}
12c6e008 431
ab50232b 432
dcedbb0e
TH
433/**
434 * Render an embedded multiple-choice question vertically, like for a normal
435 * multiple-choice question.
436 *
437 * @copyright 2010 Pierre Pichet
438 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
439 */
440class qtype_multianswer_multichoice_horizontal_renderer
441 extends qtype_multianswer_multichoice_vertical_renderer {
12c6e008 442
dcedbb0e
TH
443 protected function choice_wrapper_start($class) {
444 return html_writer::start_tag('td', array('class' => $class));
ab50232b 445 }
12c6e008 446
dcedbb0e
TH
447 protected function choice_wrapper_end() {
448 return html_writer::end_tag('td');
449 }
12c6e008 450
dcedbb0e
TH
451 protected function all_choices_wrapper_start() {
452 return html_writer::start_tag('table', array('class' => 'answer')) .
453 html_writer::start_tag('tbody') . html_writer::start_tag('tr');
454 }
ab50232b 455
dcedbb0e
TH
456 protected function all_choices_wrapper_end() {
457 return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
458 html_writer::end_tag('table');
ab50232b 459 }
ab50232b 460}