MDL-68761 question_multichoice: show question content inline
[moodle.git] / question / type / multichoice / renderer.php
CommitLineData
c9c989a0 1<?php
c9c989a0
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
c9c989a0
TH
17/**
18 * Multiple choice question renderer classes.
19 *
7764183a
TH
20 * @package qtype
21 * @subpackage multichoice
22 * @copyright 2009 The Open University
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
c9c989a0
TH
24 */
25
26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
29
c9c989a0
TH
30/**
31 * Base class for generating the bits of output common to multiple choice
32 * single and multiple questions.
33 *
7764183a
TH
34 * @copyright 2009 The Open University
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
c9c989a0
TH
36 */
37abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedback_renderer {
f6a77849
SL
38
39 /**
40 * Method to generating the bits of output after question choices.
41 *
42 * @param question_attempt $qa The question attempt object.
43 * @param question_display_options $options controls what should and should not be displayed.
44 *
45 * @return string HTML output.
46 */
47 protected abstract function after_choices(question_attempt $qa, question_display_options $options);
48
c7df5006 49 protected abstract function get_input_type();
c9c989a0 50
c7df5006 51 protected abstract function get_input_name(question_attempt $qa, $value);
c9c989a0 52
c7df5006 53 protected abstract function get_input_value($value);
c9c989a0 54
c7df5006 55 protected abstract function get_input_id(question_attempt $qa, $value);
c9c989a0
TH
56
57 /**
58 * Whether a choice should be considered right, wrong or partially right.
59 * @param question_answer $ans representing one of the choices.
60 * @return fload 1.0, 0.0 or something in between, respectively.
61 */
c7df5006 62 protected abstract function is_right(question_answer $ans);
c9c989a0 63
c7df5006 64 protected abstract function prompt();
c9c989a0
TH
65
66 public function formulation_and_controls(question_attempt $qa,
67 question_display_options $options) {
68
69 $question = $qa->get_question();
c9c989a0
TH
70 $response = $question->get_response($qa);
71
72 $inputname = $qa->get_qt_field_name('answer');
73 $inputattributes = array(
74 'type' => $this->get_input_type(),
75 'name' => $inputname,
76 );
77
78 if ($options->readonly) {
79 $inputattributes['disabled'] = 'disabled';
80 }
81
82 $radiobuttons = array();
83 $feedbackimg = array();
84 $feedback = array();
85 $classes = array();
7ac7977c 86 foreach ($question->get_order($qa) as $value => $ansid) {
c9c989a0
TH
87 $ans = $question->answers[$ansid];
88 $inputattributes['name'] = $this->get_input_name($qa, $value);
89 $inputattributes['value'] = $this->get_input_value($value);
90 $inputattributes['id'] = $this->get_input_id($qa, $value);
91 $isselected = $question->is_choice_selected($response, $value);
92 if ($isselected) {
93 $inputattributes['checked'] = 'checked';
94 } else {
95 unset($inputattributes['checked']);
96 }
97 $hidden = '';
98 if (!$options->readonly && $this->get_input_type() == 'checkbox') {
99 $hidden = html_writer::empty_tag('input', array(
100 'type' => 'hidden',
101 'name' => $inputattributes['name'],
102 'value' => 0,
103 ));
104 }
105 $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) .
12039f2e 106 html_writer::tag('label',
3543eabc 107 html_writer::span($this->number_in_style($value, $question->answernumbering), 'answernumber') .
40d1487b
BB
108 html_writer::tag('div',
109 $question->format_text(
110 $ans->answer, $ans->answerformat,
111 $qa, 'question', 'answer', $ansid),
112 array('class' => 'flex-fill ml-1')),
113 array('for' => $inputattributes['id'], 'class' => 'd-flex w-100'));
c9c989a0 114
3d9645ae 115 // Param $options->suppresschoicefeedback is a hack specific to the
c9c989a0
TH
116 // oumultiresponse question type. It would be good to refactor to
117 // avoid refering to it here.
118 if ($options->feedback && empty($options->suppresschoicefeedback) &&
119 $isselected && trim($ans->feedback)) {
120 $feedback[] = html_writer::tag('div',
35c9b652
TH
121 $question->make_html_inline($question->format_text(
122 $ans->feedback, $ans->feedbackformat,
123 $qa, 'question', 'answerfeedback', $ansid)),
c9c989a0
TH
124 array('class' => 'specificfeedback'));
125 } else {
126 $feedback[] = '';
127 }
128 $class = 'r' . ($value % 2);
129 if ($options->correctness && $isselected) {
130 $feedbackimg[] = $this->feedback_image($this->is_right($ans));
131 $class .= ' ' . $this->feedback_class($this->is_right($ans));
132 } else {
133 $feedbackimg[] = '';
134 }
135 $classes[] = $class;
136 }
137
138 $result = '';
139 $result .= html_writer::tag('div', $question->format_questiontext($qa),
140 array('class' => 'qtext'));
141
142 $result .= html_writer::start_tag('div', array('class' => 'ablock'));
00055ef6
MK
143 if ($question->showstandardinstruction == 1) {
144 $result .= html_writer::tag('div', $this->prompt(), array('class' => 'prompt'));
145 }
c9c989a0
TH
146
147 $result .= html_writer::start_tag('div', array('class' => 'answer'));
148 foreach ($radiobuttons as $key => $radio) {
a18fda20 149 $result .= html_writer::tag('div', $radio . ' ' . $feedbackimg[$key] . $feedback[$key],
c9c989a0
TH
150 array('class' => $classes[$key])) . "\n";
151 }
3d9645ae 152 $result .= html_writer::end_tag('div'); // Answer.
c9c989a0 153
f6a77849
SL
154 $result .= $this->after_choices($qa, $options);
155
3d9645ae 156 $result .= html_writer::end_tag('div'); // Ablock.
c9c989a0
TH
157
158 if ($qa->get_state() == question_state::$invalid) {
159 $result .= html_writer::nonempty_tag('div',
160 $question->get_validation_error($qa->get_last_qt_data()),
161 array('class' => 'validationerror'));
162 }
163
164 return $result;
165 }
166
167 protected function number_html($qnum) {
168 return $qnum . '. ';
169 }
170
171 /**
172 * @param int $num The number, starting at 0.
173 * @param string $style The style to render the number in. One of the
174 * options returned by {@link qtype_multichoice:;get_numbering_styles()}.
175 * @return string the number $num in the requested style.
176 */
177 protected function number_in_style($num, $style) {
178 switch($style) {
179 case 'abc':
180 $number = chr(ord('a') + $num);
181 break;
182 case 'ABCD':
183 $number = chr(ord('A') + $num);
184 break;
185 case '123':
186 $number = $num + 1;
187 break;
188 case 'iii':
189 $number = question_utils::int_to_roman($num + 1);
190 break;
191 case 'IIII':
192 $number = strtoupper(question_utils::int_to_roman($num + 1));
193 break;
194 case 'none':
195 return '';
196 default:
197 return 'ERR';
198 }
199 return $this->number_html($number);
200 }
201
202 public function specific_feedback(question_attempt $qa) {
203 return $this->combined_feedback($qa);
204 }
4358ee38
A
205
206 /**
207 * Function returns string based on number of correct answers
208 * @param array $right An Array of correct responses to the current question
209 * @return string based on number of correct responses
210 */
211 protected function correct_choices(array $right) {
212 // Return appropriate string for single/multiple correct answer(s).
213 if (count($right) == 1) {
214 return get_string('correctansweris', 'qtype_multichoice',
215 implode(', ', $right));
216 } else if (count($right) > 1) {
217 return get_string('correctanswersare', 'qtype_multichoice',
218 implode(', ', $right));
219 } else {
220 return "";
221 }
222 }
c9c989a0
TH
223}
224
225
226/**
227 * Subclass for generating the bits of output specific to multiple choice
228 * single questions.
229 *
7764183a
TH
230 * @copyright 2009 The Open University
231 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
c9c989a0
TH
232 */
233class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base {
234 protected function get_input_type() {
235 return 'radio';
236 }
237
238 protected function get_input_name(question_attempt $qa, $value) {
239 return $qa->get_qt_field_name('answer');
240 }
241
242 protected function get_input_value($value) {
243 return $value;
244 }
245
246 protected function get_input_id(question_attempt $qa, $value) {
247 return $qa->get_qt_field_name('answer' . $value);
248 }
249
250 protected function is_right(question_answer $ans) {
251 return $ans->fraction;
252 }
253
254 protected function prompt() {
255 return get_string('selectone', 'qtype_multichoice');
256 }
257
258 public function correct_response(question_attempt $qa) {
259 $question = $qa->get_question();
260
4358ee38
A
261 // Put all correct answers (100% grade) into $right.
262 $right = array();
c9c989a0
TH
263 foreach ($question->answers as $ansid => $ans) {
264 if (question_state::graded_state_for_fraction($ans->fraction) ==
265 question_state::$gradedright) {
4358ee38
A
266 $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat,
267 $qa, 'question', 'answer', $ansid));
c9c989a0
TH
268 }
269 }
4358ee38 270 return $this->correct_choices($right);
c9c989a0 271 }
f6a77849
SL
272
273 public function after_choices(question_attempt $qa, question_display_options $options) {
274 // Only load the clear choice feature if it's not read only.
275 if ($options->readonly) {
276 return '';
277 }
278
279 $question = $qa->get_question();
280 $response = $question->get_response($qa);
281 $hascheckedchoice = false;
282 foreach ($question->get_order($qa) as $value => $ansid) {
283 if ($question->is_choice_selected($response, $value)) {
284 $hascheckedchoice = true;
285 break;
286 }
287 }
288
e06b302e 289 $questiondivid = $qa->get_outer_question_div_unique_id();
f6a77849 290
f6a77849 291 // When no choice selected during rendering, then hide the clear choice option.
e06b302e 292 $cssclass = '';
f6a77849 293 if (!$hascheckedchoice && $response == -1) {
e06b302e 294 $cssclass = 'd-none';
f6a77849 295 }
f6a77849 296
e06b302e
BB
297 $clearchoicebutton = html_writer::tag('button', get_string('clearchoice', 'qtype_multichoice'), [
298 'class' => 'btn btn-link ml-3 ' . $cssclass,
299 'data-action' => 'clearresults',
300 'data-target' => '#' . $questiondivid
301 ]);
f6a77849
SL
302
303 // Load required clearchoice AMD module.
304 $this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init',
e06b302e 305 [$questiondivid]);
f6a77849 306
e06b302e 307 return $clearchoicebutton;
f6a77849
SL
308 }
309
c9c989a0
TH
310}
311
312/**
313 * Subclass for generating the bits of output specific to multiple choice
314 * multi=select questions.
315 *
7764183a
TH
316 * @copyright 2009 The Open University
317 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
c9c989a0
TH
318 */
319class qtype_multichoice_multi_renderer extends qtype_multichoice_renderer_base {
f6a77849
SL
320 protected function after_choices(question_attempt $qa, question_display_options $options) {
321 return '';
322 }
323
c9c989a0
TH
324 protected function get_input_type() {
325 return 'checkbox';
326 }
327
328 protected function get_input_name(question_attempt $qa, $value) {
329 return $qa->get_qt_field_name('choice' . $value);
330 }
331
332 protected function get_input_value($value) {
333 return 1;
334 }
335
336 protected function get_input_id(question_attempt $qa, $value) {
337 return $this->get_input_name($qa, $value);
338 }
339
340 protected function is_right(question_answer $ans) {
341 if ($ans->fraction > 0) {
342 return 1;
343 } else {
344 return 0;
345 }
346 }
347
348 protected function prompt() {
349 return get_string('selectmulti', 'qtype_multichoice');
350 }
351
352 public function correct_response(question_attempt $qa) {
353 $question = $qa->get_question();
354
355 $right = array();
356 foreach ($question->answers as $ansid => $ans) {
357 if ($ans->fraction > 0) {
99942334
TH
358 $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat,
359 $qa, 'question', 'answer', $ansid));
c9c989a0
TH
360 }
361 }
4358ee38 362 return $this->correct_choices($right);
c9c989a0
TH
363 }
364
365 protected function num_parts_correct(question_attempt $qa) {
366 if ($qa->get_question()->get_num_selected_choices($qa->get_last_qt_data()) >
367 $qa->get_question()->get_num_correct_choices()) {
368 return get_string('toomanyselected', 'qtype_multichoice');
369 }
370
371 return parent::num_parts_correct($qa);
372 }
373}