2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Multiple choice question renderer classes.
21 * @subpackage multichoice
22 * @copyright 2009 The Open University
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
31 * Base class for generating the bits of output common to multiple choice
32 * single and multiple questions.
34 * @copyright 2009 The Open University
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedback_renderer {
40 * Method to generating the bits of output after question choices.
42 * @param question_attempt $qa The question attempt object.
43 * @param question_display_options $options controls what should and should not be displayed.
45 * @return string HTML output.
47 protected abstract function after_choices(question_attempt $qa, question_display_options $options);
49 protected abstract function get_input_type();
51 protected abstract function get_input_name(question_attempt $qa, $value);
53 protected abstract function get_input_value($value);
55 protected abstract function get_input_id(question_attempt $qa, $value);
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.
62 protected abstract function is_right(question_answer $ans);
64 protected abstract function prompt();
66 public function formulation_and_controls(question_attempt $qa,
67 question_display_options $options) {
69 $question = $qa->get_question();
70 $response = $question->get_response($qa);
72 $inputname = $qa->get_qt_field_name('answer');
73 $inputattributes = array(
74 'type' => $this->get_input_type(),
78 if ($options->readonly) {
79 $inputattributes['disabled'] = 'disabled';
82 $radiobuttons = array();
83 $feedbackimg = array();
86 foreach ($question->get_order($qa) as $value => $ansid) {
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);
93 $inputattributes['checked'] = 'checked';
95 unset($inputattributes['checked']);
98 if (!$options->readonly && $this->get_input_type() == 'checkbox') {
99 $hidden = html_writer::empty_tag('input', array(
101 'name' => $inputattributes['name'],
105 $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) .
106 html_writer::tag('label',
107 html_writer::span($this->number_in_style($value, $question->answernumbering), 'answernumber') .
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'));
115 // Param $options->suppresschoicefeedback is a hack specific to the
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',
121 $question->make_html_inline($question->format_text(
122 $ans->feedback, $ans->feedbackformat,
123 $qa, 'question', 'answerfeedback', $ansid)),
124 array('class' => 'specificfeedback'));
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));
139 $result .= html_writer::tag('div', $question->format_questiontext($qa),
140 array('class' => 'qtext'));
142 $result .= html_writer::start_tag('div', array('class' => 'ablock'));
143 if ($question->showstandardinstruction == 1) {
144 $result .= html_writer::tag('div', $this->prompt(), array('class' => 'prompt'));
147 $result .= html_writer::start_tag('div', array('class' => 'answer'));
148 foreach ($radiobuttons as $key => $radio) {
149 $result .= html_writer::tag('div', $radio . ' ' . $feedbackimg[$key] . $feedback[$key],
150 array('class' => $classes[$key])) . "\n";
152 $result .= html_writer::end_tag('div'); // Answer.
154 $result .= $this->after_choices($qa, $options);
156 $result .= html_writer::end_tag('div'); // Ablock.
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'));
167 protected function number_html($qnum) {
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.
177 protected function number_in_style($num, $style) {
180 $number = chr(ord('a') + $num);
183 $number = chr(ord('A') + $num);
189 $number = question_utils::int_to_roman($num + 1);
192 $number = strtoupper(question_utils::int_to_roman($num + 1));
199 return $this->number_html($number);
202 public function specific_feedback(question_attempt $qa) {
203 return $this->combined_feedback($qa);
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
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));
227 * Subclass for generating the bits of output specific to multiple choice
230 * @copyright 2009 The Open University
231 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
233 class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base {
234 protected function get_input_type() {
238 protected function get_input_name(question_attempt $qa, $value) {
239 return $qa->get_qt_field_name('answer');
242 protected function get_input_value($value) {
246 protected function get_input_id(question_attempt $qa, $value) {
247 return $qa->get_qt_field_name('answer' . $value);
250 protected function is_right(question_answer $ans) {
251 return $ans->fraction;
254 protected function prompt() {
255 return get_string('selectone', 'qtype_multichoice');
258 public function correct_response(question_attempt $qa) {
259 $question = $qa->get_question();
261 // Put all correct answers (100% grade) into $right.
263 foreach ($question->answers as $ansid => $ans) {
264 if (question_state::graded_state_for_fraction($ans->fraction) ==
265 question_state::$gradedright) {
266 $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat,
267 $qa, 'question', 'answer', $ansid));
270 return $this->correct_choices($right);
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) {
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;
289 $clearchoiceid = $this->get_input_id($qa, -1);
290 $clearchoicefieldname = $qa->get_qt_field_name('clearchoice');
291 $clearchoiceradioattrs = [
292 'type' => $this->get_input_type(),
293 'name' => $qa->get_qt_field_name('answer'),
294 'id' => $clearchoiceid,
299 $cssclass = 'qtype_multichoice_clearchoice';
300 // When no choice selected during rendering, then hide the clear choice option.
302 if (!$hascheckedchoice && $response == -1) {
303 $cssclass .= ' sr-only';
304 $clearchoiceradioattrs['checked'] = 'checked';
307 // Adds an hidden radio that will be checked to give the impression the choice has been cleared.
308 $clearchoiceradio = html_writer::empty_tag('input', $clearchoiceradioattrs);
309 $clearchoiceradio .= html_writer::link('', get_string('clearchoice', 'qtype_multichoice'),
310 ['for' => $clearchoiceid, 'role' => 'button', 'tabindex' => $linktabindex]);
312 // Now wrap the radio and label inside a div.
313 $result = html_writer::tag('div', $clearchoiceradio, ['id' => $clearchoicefieldname, 'class' => $cssclass]);
315 // Load required clearchoice AMD module.
316 $this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init',
317 [$qa->get_outer_question_div_unique_id(), $clearchoicefieldname]);
325 * Subclass for generating the bits of output specific to multiple choice
326 * multi=select questions.
328 * @copyright 2009 The Open University
329 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
331 class qtype_multichoice_multi_renderer extends qtype_multichoice_renderer_base {
332 protected function after_choices(question_attempt $qa, question_display_options $options) {
336 protected function get_input_type() {
340 protected function get_input_name(question_attempt $qa, $value) {
341 return $qa->get_qt_field_name('choice' . $value);
344 protected function get_input_value($value) {
348 protected function get_input_id(question_attempt $qa, $value) {
349 return $this->get_input_name($qa, $value);
352 protected function is_right(question_answer $ans) {
353 if ($ans->fraction > 0) {
360 protected function prompt() {
361 return get_string('selectmulti', 'qtype_multichoice');
364 public function correct_response(question_attempt $qa) {
365 $question = $qa->get_question();
368 foreach ($question->answers as $ansid => $ans) {
369 if ($ans->fraction > 0) {
370 $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat,
371 $qa, 'question', 'answer', $ansid));
374 return $this->correct_choices($right);
377 protected function num_parts_correct(question_attempt $qa) {
378 if ($qa->get_question()->get_num_selected_choices($qa->get_last_qt_data()) >
379 $qa->get_question()->get_num_correct_choices()) {
380 return get_string('toomanyselected', 'qtype_multichoice');
383 return parent::num_parts_correct($qa);