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