See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]
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 $choicenumber = ''; 108 if ($question->answernumbering !== 'none') { 109 $choicenumber = html_writer::span( 110 $this->number_in_style($value, $question->answernumbering), 'answernumber'); 111 } 112 $choicetext = $question->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ansid); 113 $choice = html_writer::div($choicetext, 'flex-fill ml-1'); 114 115 $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) . 116 html_writer::div($choicenumber . $choice, '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 // Feedback images will be rendered using Font awesome. 137 // Font awesome icons are actually characters(text) with special glyphs, 138 // so the icons cannot be aligned correctly even if the parent div wrapper is using align-items: flex-start. 139 // To make the Font awesome icons follow align-items: flex-start, we need to wrap them inside a span tag. 140 $feedbackimg[] = html_writer::span($this->feedback_image($this->is_right($ans)), 'ml-1'); 141 $class .= ' ' . $this->feedback_class($this->is_right($ans)); 142 } else { 143 $feedbackimg[] = ''; 144 } 145 $classes[] = $class; 146 } 147 148 $result = ''; 149 $result .= html_writer::tag('div', $question->format_questiontext($qa), 150 array('class' => 'qtext')); 151 152 $result .= html_writer::start_tag('fieldset', array('class' => 'ablock no-overflow visual-scroll-x')); 153 if ($question->showstandardinstruction == 1) { 154 $legendclass = ''; 155 $questionnumber = $options->add_question_identifier_to_label($this->prompt(), true, true); 156 } else { 157 $questionnumber = $options->add_question_identifier_to_label(get_string('answer'), true, true); 158 $legendclass = 'sr-only'; 159 } 160 $legendattrs = [ 161 'class' => 'prompt h6 font-weight-normal ' . $legendclass, 162 ]; 163 $result .= html_writer::tag('legend', $questionnumber, $legendattrs); 164 165 $result .= html_writer::start_tag('div', array('class' => 'answer')); 166 foreach ($radiobuttons as $key => $radio) { 167 $result .= html_writer::tag('div', $radio . ' ' . $feedbackimg[$key] . $feedback[$key], 168 array('class' => $classes[$key])) . "\n"; 169 } 170 $result .= html_writer::end_tag('div'); // Answer. 171 172 // Load JS module for the question answers. 173 $this->page->requires->js_call_amd('qtype_multichoice/answers', 'init', 174 [$qa->get_outer_question_div_unique_id()]); 175 $result .= $this->after_choices($qa, $options); 176 177 $result .= html_writer::end_tag('fieldset'); // Ablock. 178 179 if ($qa->get_state() == question_state::$invalid) { 180 $result .= html_writer::nonempty_tag('div', 181 $question->get_validation_error($qa->get_last_qt_data()), 182 array('class' => 'validationerror')); 183 } 184 185 return $result; 186 } 187 188 protected function number_html($qnum) { 189 return $qnum . '. '; 190 } 191 192 /** 193 * @param int $num The number, starting at 0. 194 * @param string $style The style to render the number in. One of the 195 * options returned by {@link qtype_multichoice:;get_numbering_styles()}. 196 * @return string the number $num in the requested style. 197 */ 198 protected function number_in_style($num, $style) { 199 switch($style) { 200 case 'abc': 201 $number = chr(ord('a') + $num); 202 break; 203 case 'ABCD': 204 $number = chr(ord('A') + $num); 205 break; 206 case '123': 207 $number = $num + 1; 208 break; 209 case 'iii': 210 $number = question_utils::int_to_roman($num + 1); 211 break; 212 case 'IIII': 213 $number = strtoupper(question_utils::int_to_roman($num + 1)); 214 break; 215 case 'none': 216 return ''; 217 default: 218 return 'ERR'; 219 } 220 return $this->number_html($number); 221 } 222 223 public function specific_feedback(question_attempt $qa) { 224 return $this->combined_feedback($qa); 225 } 226 227 /** 228 * Function returns string based on number of correct answers 229 * @param array $right An Array of correct responses to the current question 230 * @return string based on number of correct responses 231 */ 232 protected function correct_choices(array $right) { 233 // Return appropriate string for single/multiple correct answer(s). 234 if (count($right) == 1) { 235 return get_string('correctansweris', 'qtype_multichoice', 236 implode(', ', $right)); 237 } else if (count($right) > 1) { 238 return get_string('correctanswersare', 'qtype_multichoice', 239 implode(', ', $right)); 240 } else { 241 return ""; 242 } 243 } 244 } 245 246 247 /** 248 * Subclass for generating the bits of output specific to multiple choice 249 * single questions. 250 * 251 * @copyright 2009 The Open University 252 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 253 */ 254 class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base { 255 protected function get_input_type() { 256 return 'radio'; 257 } 258 259 protected function get_input_name(question_attempt $qa, $value) { 260 return $qa->get_qt_field_name('answer'); 261 } 262 263 protected function get_input_value($value) { 264 return $value; 265 } 266 267 protected function get_input_id(question_attempt $qa, $value) { 268 return $qa->get_qt_field_name('answer' . $value); 269 } 270 271 protected function is_right(question_answer $ans) { 272 return $ans->fraction; 273 } 274 275 protected function prompt() { 276 return get_string('selectone', 'qtype_multichoice'); 277 } 278 279 public function correct_response(question_attempt $qa) { 280 $question = $qa->get_question(); 281 282 // Put all correct answers (100% grade) into $right. 283 $right = array(); 284 foreach ($question->answers as $ansid => $ans) { 285 if (question_state::graded_state_for_fraction($ans->fraction) == 286 question_state::$gradedright) { 287 $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat, 288 $qa, 'question', 'answer', $ansid)); 289 } 290 } 291 return $this->correct_choices($right); 292 } 293 294 public function after_choices(question_attempt $qa, question_display_options $options) { 295 // Only load the clear choice feature if it's not read only. 296 if ($options->readonly) { 297 return ''; 298 } 299 300 $question = $qa->get_question(); 301 $response = $question->get_response($qa); 302 $hascheckedchoice = false; 303 foreach ($question->get_order($qa) as $value => $ansid) { 304 if ($question->is_choice_selected($response, $value)) { 305 $hascheckedchoice = true; 306 break; 307 } 308 } 309 310 $clearchoiceid = $this->get_input_id($qa, -1); 311 $clearchoicefieldname = $qa->get_qt_field_name('clearchoice'); 312 $clearchoiceradioattrs = [ 313 'type' => $this->get_input_type(), 314 'name' => $qa->get_qt_field_name('answer'), 315 'id' => $clearchoiceid, 316 'value' => -1, 317 'class' => 'sr-only', 318 'aria-hidden' => 'true' 319 ]; 320 $clearchoicewrapperattrs = [ 321 'id' => $clearchoicefieldname, 322 'class' => 'qtype_multichoice_clearchoice', 323 ]; 324 325 // When no choice selected during rendering, then hide the clear choice option. 326 // We are using .sr-only and aria-hidden together so while the element is hidden 327 // from both the monitor and the screen-reader, it is still tabbable. 328 $linktabindex = 0; 329 if (!$hascheckedchoice && $response == -1) { 330 $clearchoicewrapperattrs['class'] .= ' sr-only'; 331 $clearchoicewrapperattrs['aria-hidden'] = 'true'; 332 $clearchoiceradioattrs['checked'] = 'checked'; 333 $linktabindex = -1; 334 } 335 // Adds an hidden radio that will be checked to give the impression the choice has been cleared. 336 $clearchoiceradio = html_writer::empty_tag('input', $clearchoiceradioattrs); 337 $clearchoice = html_writer::link('#', get_string('clearchoice', 'qtype_multichoice'), 338 ['tabindex' => $linktabindex, 'role' => 'button', 'class' => 'btn btn-link ml-3 mt-n1']); 339 $clearchoiceradio .= html_writer::label($clearchoice, $clearchoiceid); 340 341 // Now wrap the radio and label inside a div. 342 $result = html_writer::tag('div', $clearchoiceradio, $clearchoicewrapperattrs); 343 344 // Load required clearchoice AMD module. 345 $this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init', 346 [$qa->get_outer_question_div_unique_id(), $clearchoicefieldname]); 347 348 return $result; 349 } 350 351 } 352 353 /** 354 * Subclass for generating the bits of output specific to multiple choice 355 * multi=select questions. 356 * 357 * @copyright 2009 The Open University 358 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 359 */ 360 class qtype_multichoice_multi_renderer extends qtype_multichoice_renderer_base { 361 protected function after_choices(question_attempt $qa, question_display_options $options) { 362 return ''; 363 } 364 365 protected function get_input_type() { 366 return 'checkbox'; 367 } 368 369 protected function get_input_name(question_attempt $qa, $value) { 370 return $qa->get_qt_field_name('choice' . $value); 371 } 372 373 protected function get_input_value($value) { 374 return 1; 375 } 376 377 protected function get_input_id(question_attempt $qa, $value) { 378 return $this->get_input_name($qa, $value); 379 } 380 381 protected function is_right(question_answer $ans) { 382 if ($ans->fraction > 0) { 383 return 1; 384 } else { 385 return 0; 386 } 387 } 388 389 protected function prompt() { 390 return get_string('selectmulti', 'qtype_multichoice'); 391 } 392 393 public function correct_response(question_attempt $qa) { 394 $question = $qa->get_question(); 395 396 $right = array(); 397 foreach ($question->answers as $ansid => $ans) { 398 if ($ans->fraction > 0) { 399 $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat, 400 $qa, 'question', 'answer', $ansid)); 401 } 402 } 403 return $this->correct_choices($right); 404 } 405 406 protected function num_parts_correct(question_attempt $qa) { 407 if ($qa->get_question()->get_num_selected_choices($qa->get_last_qt_data()) > 408 $qa->get_question()->get_num_correct_choices()) { 409 return get_string('toomanyselected', 'qtype_multichoice'); 410 } 411 412 return parent::num_parts_correct($qa); 413 } 414 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body