Commit | Line | Data |
---|---|---|
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 |
27 | defined('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 | */ |
37 | abstract 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 | */ |
233 | class 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 | */ |
319 | class 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 | } |