Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
1 <?php 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 17 /** 18 * Multiple choice question renderer classes. 19 * 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 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 30 /** 31 * Base class for generating the bits of output common to multiple choice 32 * single and multiple questions. 33 * 34 * @copyright 2009 The Open University 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedback_renderer { 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 49 protected abstract function get_input_type(); 50 51 protected abstract function get_input_name(question_attempt $qa, $value); 52 53 protected abstract function get_input_value($value); 54 55 protected abstract function get_input_id(question_attempt $qa, $value); 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 */ 62 protected abstract function is_right(question_answer $ans); 63 64 protected abstract function prompt(); 65 66 public function formulation_and_controls(question_attempt $qa, 67 question_display_options $options) { 68 69 $question = $qa->get_question(); 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(); 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 $inputattributes['aria-labelledby'] = $inputattributes['id'] . '_label'; 92 $isselected = $question->is_choice_selected($response, $value); 93 if ($isselected) { 94 $inputattributes['checked'] = 'checked'; 95 } else { 96 unset($inputattributes['checked']); 97 } 98 $hidden = ''; 99 if (!$options->readonly && $this->get_input_type() == 'checkbox') { 100 $hidden = html_writer::empty_tag('input', array( 101 'type' => 'hidden', 102 'name' => $inputattributes['name'], 103 'value' => 0, 104 )); 105 } 106 107 $questionnumber = ''; 108 if ($question->answernumbering !== 'none') { 109 $questionnumber = html_writer::span( 110 $this->number_in_style($value, $question->answernumbering), 'answernumber'); 111 } 112 $answertext = $question->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ansid); 113 $questionanswer = html_writer::div($answertext, 'flex-fill ml-1'); 114 115 $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) . 116 html_writer::div($questionnumber . $questionanswer, 'd-flex w-auto', [ 117 'id' => $inputattributes['id'] . '_label', 118 'data-region' => 'answer-label', 119 ]); 120 121 // Param $options->suppresschoicefeedback is a hack specific to the 122 // oumultiresponse question type. It would be good to refactor to 123 // avoid refering to it here. 124 if ($options->feedback && empty($options->suppresschoicefeedback) && 125 $isselected && trim($ans->feedback)) { 126 $feedback[] = html_writer::tag('div', 127 $question->make_html_inline($question->format_text( 128 $ans->feedback, $ans->feedbackformat, 129 $qa, 'question', 'answerfeedback', $ansid)), 130 array('class' => 'specificfeedback')); 131 } else { 132 $feedback[] = ''; 133 } 134 $class = 'r' . ($value % 2); 135 if ($options->correctness && $isselected) { 136 $feedbackimg[] = $this->feedback_image($this->is_right($ans)); 137 $class .= ' ' . $this->feedback_class($this->is_right($ans)); 138 } else { 139 $feedbackimg[] = ''; 140 } 141 $classes[] = $class; 142 } 143 144 $result = ''; 145 $result .= html_writer::tag('div', $question->format_questiontext($qa), 146 array('class' => 'qtext')); 147 148 $result .= html_writer::start_tag('div', array('class' => 'ablock no-overflow visual-scroll-x')); 149 if ($question->showstandardinstruction == 1) { 150 $result .= html_writer::tag('div', $this->prompt(), array('class' => 'prompt')); 151 } 152 153 $result .= html_writer::start_tag('div', array('class' => 'answer')); 154 foreach ($radiobuttons as $key => $radio) { 155 $result .= html_writer::tag('div', $radio . ' ' . $feedbackimg[$key] . $feedback[$key], 156 array('class' => $classes[$key])) . "\n"; 157 } 158 $result .= html_writer::end_tag('div'); // Answer. 159 160 // Load JS module for the question answers. 161 $this->page->requires->js_call_amd('qtype_multichoice/answers', 'init', 162 [$qa->get_outer_question_div_unique_id()]); 163 $result .= $this->after_choices($qa, $options); 164 165 $result .= html_writer::end_tag('div'); // Ablock. 166 167 if ($qa->get_state() == question_state::$invalid) { 168 $result .= html_writer::nonempty_tag('div', 169 $question->get_validation_error($qa->get_last_qt_data()), 170 array('class' => 'validationerror')); 171 } 172 173 return $result; 174 } 175 176 protected function number_html($qnum) { 177 return $qnum . '. '; 178 } 179 180 /** 181 * @param int $num The number, starting at 0. 182 * @param string $style The style to render the number in. One of the 183 * options returned by {@link qtype_multichoice:;get_numbering_styles()}. 184 * @return string the number $num in the requested style. 185 */ 186 protected function number_in_style($num, $style) { 187 switch($style) { 188 case 'abc': 189 $number = chr(ord('a') + $num); 190 break; 191 case 'ABCD': 192 $number = chr(ord('A') + $num); 193 break; 194 case '123': 195 $number = $num + 1; 196 break; 197 case 'iii': 198 $number = question_utils::int_to_roman($num + 1); 199 break; 200 case 'IIII': 201 $number = strtoupper(question_utils::int_to_roman($num + 1)); 202 break; 203 case 'none': 204 return ''; 205 default: 206 return 'ERR'; 207 } 208 return $this->number_html($number); 209 } 210 211 public function specific_feedback(question_attempt $qa) { 212 return $this->combined_feedback($qa); 213 } 214 215 /** 216 * Function returns string based on number of correct answers 217 * @param array $right An Array of correct responses to the current question 218 * @return string based on number of correct responses 219 */ 220 protected function correct_choices(array $right) { 221 // Return appropriate string for single/multiple correct answer(s). 222 if (count($right) == 1) { 223 return get_string('correctansweris', 'qtype_multichoice', 224 implode(', ', $right)); 225 } else if (count($right) > 1) { 226 return get_string('correctanswersare', 'qtype_multichoice', 227 implode(', ', $right)); 228 } else { 229 return ""; 230 } 231 } 232 } 233 234 235 /** 236 * Subclass for generating the bits of output specific to multiple choice 237 * single questions. 238 * 239 * @copyright 2009 The Open University 240 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 241 */ 242 class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base { 243 protected function get_input_type() { 244 return 'radio'; 245 } 246 247 protected function get_input_name(question_attempt $qa, $value) { 248 return $qa->get_qt_field_name('answer'); 249 } 250 251 protected function get_input_value($value) { 252 return $value; 253 } 254 255 protected function get_input_id(question_attempt $qa, $value) { 256 return $qa->get_qt_field_name('answer' . $value); 257 } 258 259 protected function is_right(question_answer $ans) { 260 return $ans->fraction; 261 } 262 263 protected function prompt() { 264 return get_string('selectone', 'qtype_multichoice'); 265 } 266 267 public function correct_response(question_attempt $qa) { 268 $question = $qa->get_question(); 269 270 // Put all correct answers (100% grade) into $right. 271 $right = array(); 272 foreach ($question->answers as $ansid => $ans) { 273 if (question_state::graded_state_for_fraction($ans->fraction) == 274 question_state::$gradedright) { 275 $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat, 276 $qa, 'question', 'answer', $ansid)); 277 } 278 } 279 return $this->correct_choices($right); 280 } 281 282 public function after_choices(question_attempt $qa, question_display_options $options) { 283 // Only load the clear choice feature if it's not read only. 284 if ($options->readonly) { 285 return ''; 286 } 287 288 $question = $qa->get_question(); 289 $response = $question->get_response($qa); 290 $hascheckedchoice = false; 291 foreach ($question->get_order($qa) as $value => $ansid) { 292 if ($question->is_choice_selected($response, $value)) { 293 $hascheckedchoice = true; 294 break; 295 } 296 } 297 298 $clearchoiceid = $this->get_input_id($qa, -1); 299 $clearchoicefieldname = $qa->get_qt_field_name('clearchoice'); 300 $clearchoiceradioattrs = [ 301 'type' => $this->get_input_type(), 302 'name' => $qa->get_qt_field_name('answer'), 303 'id' => $clearchoiceid, 304 'value' => -1, 305 'class' => 'sr-only', 306 'aria-hidden' => 'true' 307 ]; 308 $clearchoicewrapperattrs = [ 309 'id' => $clearchoicefieldname, 310 'class' => 'qtype_multichoice_clearchoice', 311 ]; 312 313 // When no choice selected during rendering, then hide the clear choice option. 314 // We are using .sr-only and aria-hidden together so while the element is hidden 315 // from both the monitor and the screen-reader, it is still tabbable. 316 $linktabindex = 0; 317 if (!$hascheckedchoice && $response == -1) { 318 $clearchoicewrapperattrs['class'] .= ' sr-only'; 319 $clearchoicewrapperattrs['aria-hidden'] = 'true'; 320 $clearchoiceradioattrs['checked'] = 'checked'; 321 $linktabindex = -1; 322 } 323 // Adds an hidden radio that will be checked to give the impression the choice has been cleared. 324 $clearchoiceradio = html_writer::empty_tag('input', $clearchoiceradioattrs); 325 $clearchoice = html_writer::link('#', get_string('clearchoice', 'qtype_multichoice'), 326 ['tabindex' => $linktabindex, 'role' => 'button', 'class' => 'btn btn-link ml-3 mt-n1 mb-n1']); 327 $clearchoiceradio .= html_writer::label($clearchoice, $clearchoiceid); 328 329 // Now wrap the radio and label inside a div. 330 $result = html_writer::tag('div', $clearchoiceradio, $clearchoicewrapperattrs); 331 332 // Load required clearchoice AMD module. 333 $this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init', 334 [$qa->get_outer_question_div_unique_id(), $clearchoicefieldname]); 335 336 return $result; 337 } 338 339 } 340 341 /** 342 * Subclass for generating the bits of output specific to multiple choice 343 * multi=select questions. 344 * 345 * @copyright 2009 The Open University 346 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 347 */ 348 class qtype_multichoice_multi_renderer extends qtype_multichoice_renderer_base { 349 protected function after_choices(question_attempt $qa, question_display_options $options) { 350 return ''; 351 } 352 353 protected function get_input_type() { 354 return 'checkbox'; 355 } 356 357 protected function get_input_name(question_attempt $qa, $value) { 358 return $qa->get_qt_field_name('choice' . $value); 359 } 360 361 protected function get_input_value($value) { 362 return 1; 363 } 364 365 protected function get_input_id(question_attempt $qa, $value) { 366 return $this->get_input_name($qa, $value); 367 } 368 369 protected function is_right(question_answer $ans) { 370 if ($ans->fraction > 0) { 371 return 1; 372 } else { 373 return 0; 374 } 375 } 376 377 protected function prompt() { 378 return get_string('selectmulti', 'qtype_multichoice'); 379 } 380 381 public function correct_response(question_attempt $qa) { 382 $question = $qa->get_question(); 383 384 $right = array(); 385 foreach ($question->answers as $ansid => $ans) { 386 if ($ans->fraction > 0) { 387 $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat, 388 $qa, 'question', 'answer', $ansid)); 389 } 390 } 391 return $this->correct_choices($right); 392 } 393 394 protected function num_parts_correct(question_attempt $qa) { 395 if ($qa->get_question()->get_num_selected_choices($qa->get_last_qt_data()) > 396 $qa->get_question()->get_num_correct_choices()) { 397 return get_string('toomanyselected', 'qtype_multichoice'); 398 } 399 400 return parent::num_parts_correct($qa); 401 } 402 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body