See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
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 * Multianswer question renderer classes. 19 * Handle shortanswer, numerical and various multichoice subquestions 20 * 21 * @package qtype 22 * @subpackage multianswer 23 * @copyright 2010 Pierre Pichet 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 */ 26 27 28 require_once($CFG->dirroot . '/question/type/shortanswer/renderer.php'); 29 30 31 /** 32 * Base class for generating the bits of output common to multianswer 33 * (Cloze) questions. 34 * This render the main question text and transfer to the subquestions 35 * the task of display their input elements and status 36 * feedback, grade, correct answer(s) 37 * 38 * @copyright 2010 Pierre Pichet 39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 40 */ 41 class qtype_multianswer_renderer extends qtype_renderer { 42 43 public function formulation_and_controls(question_attempt $qa, 44 question_display_options $options) { 45 $question = $qa->get_question(); 46 47 $output = ''; 48 $subquestions = array(); 49 50 $missingsubquestions = false; 51 foreach ($question->textfragments as $i => $fragment) { 52 if ($i > 0) { 53 $index = $question->places[$i]; 54 $questionisvalid = !empty($question->subquestions[$index]) && 55 $question->subquestions[$index]->qtype->name() !== 'subquestion_replacement'; 56 57 if (!$questionisvalid) { 58 $missingsubquestions = true; 59 $questionreplacement = qtype_multianswer::deleted_subquestion_replacement(); 60 61 // It is possible that the subquestion index does not exist. When corrupted quizzes (see MDL-54724) are 62 // restored, the sequence column of mdl_quiz_multianswer can be empty, in this case 63 // qtype_multianswer::get_question_options cannot fill in deleted questions, so we need to do it here. 64 $question->subquestions[$index] = $question->subquestions[$index] ?? $questionreplacement; 65 } 66 67 $token = 'qtypemultianswer' . $i . 'marker'; 68 $token = '<span class="nolink">' . $token . '</span>'; 69 $output .= $token; 70 $subquestions[$token] = $this->subquestion($qa, $options, $index, 71 $question->subquestions[$index]); 72 } 73 74 $output .= $fragment; 75 } 76 77 if ($missingsubquestions) { 78 $output = $this->notification(get_string('corruptedquestion', 'qtype_multianswer'), 'error') . $output; 79 } 80 81 $output = $question->format_text($output, $question->questiontextformat, 82 $qa, 'question', 'questiontext', $question->id); 83 $output = str_replace(array_keys($subquestions), array_values($subquestions), $output); 84 85 if ($qa->get_state() == question_state::$invalid) { 86 $output .= html_writer::nonempty_tag('div', 87 $question->get_validation_error($qa->get_last_qt_data()), 88 array('class' => 'validationerror')); 89 } 90 91 return $output; 92 } 93 94 public function subquestion(question_attempt $qa, 95 question_display_options $options, $index, question_automatically_gradable $subq) { 96 97 $subtype = $subq->qtype->name(); 98 if ($subtype == 'numerical' || $subtype == 'shortanswer') { 99 $subrenderer = 'textfield'; 100 } else if ($subtype == 'multichoice') { 101 if ($subq instanceof qtype_multichoice_multi_question) { 102 if ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL) { 103 $subrenderer = 'multiresponse_vertical'; 104 } else { 105 $subrenderer = 'multiresponse_horizontal'; 106 } 107 } else { 108 if ($subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) { 109 $subrenderer = 'multichoice_inline'; 110 } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) { 111 $subrenderer = 'multichoice_horizontal'; 112 } else { 113 $subrenderer = 'multichoice_vertical'; 114 } 115 } 116 } else if ($subtype == 'subquestion_replacement') { 117 return html_writer::div( 118 get_string('missingsubquestion', 'qtype_multianswer'), 119 'notifyproblem' 120 ); 121 } else { 122 throw new coding_exception('Unexpected subquestion type.', $subq); 123 } 124 /** @var qtype_multianswer_subq_renderer_base $renderer */ 125 $renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer); 126 return $renderer->subquestion($qa, $options, $index, $subq); 127 } 128 129 public function correct_response(question_attempt $qa) { 130 return ''; 131 } 132 } 133 134 135 /** 136 * Subclass for generating the bits of output specific to shortanswer 137 * subquestions. 138 * 139 * @copyright 2011 The Open University 140 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 141 */ 142 abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer { 143 144 /** @var int[] Stores the counts of answer instances for questions. */ 145 protected static $answercount = []; 146 147 /** @var question_display_options Question display options instance for any necessary information for rendering the question. */ 148 protected $displayoptions; 149 150 abstract public function subquestion(question_attempt $qa, 151 question_display_options $options, $index, 152 question_graded_automatically $subq); 153 154 /** 155 * Render the feedback pop-up contents. 156 * 157 * @param question_graded_automatically $subq the subquestion. 158 * @param float $fraction the mark the student got. null if this subq was not answered. 159 * @param string $feedbacktext the feedback text, already processed with format_text etc. 160 * @param string $rightanswer the right answer, already processed with format_text etc. 161 * @param question_display_options $options the display options. 162 * @return string the HTML for the feedback popup. 163 */ 164 protected function feedback_popup(question_graded_automatically $subq, 165 $fraction, $feedbacktext, $rightanswer, question_display_options $options) { 166 167 $feedback = array(); 168 if ($options->correctness) { 169 if (is_null($fraction)) { 170 $state = question_state::$gaveup; 171 } else { 172 $state = question_state::graded_state_for_fraction($fraction); 173 } 174 $feedback[] = $state->default_string(true); 175 } 176 177 if ($options->feedback && $feedbacktext) { 178 $feedback[] = $feedbacktext; 179 } 180 181 if ($options->rightanswer) { 182 $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer); 183 } 184 185 $subfraction = ''; 186 if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->maxmark > 0 187 && (!is_null($fraction) || $feedback)) { 188 $a = new stdClass(); 189 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp); 190 $a->max = format_float($subq->maxmark, $options->markdp); 191 $feedback[] = get_string('markoutofmax', 'question', $a); 192 } 193 194 if (!$feedback) { 195 return ''; 196 } 197 198 return html_writer::tag('span', implode('<br />', $feedback), [ 199 'class' => 'feedbackspan', 200 ]); 201 } 202 203 /** 204 * Render the feedback icon for a sub-question which is also the trigger for the feedback popover. 205 * 206 * @param string $icon The feedback icon 207 * @param string $feedbackcontents The feedback contents to be shown on the popover. 208 * @return string 209 */ 210 protected function get_feedback_image(string $icon, string $feedbackcontents): string { 211 global $PAGE; 212 if ($icon === '') { 213 return ''; 214 } 215 216 $PAGE->requires->js_call_amd('qtype_multianswer/feedback', 'initPopovers'); 217 218 return html_writer::link('#', $icon, [ 219 'role' => 'button', 220 'tabindex' => 0, 221 'class' => 'feedbacktrigger btn btn-link p-0', 222 'data-toggle' => 'popover', 223 'data-container' => 'body', 224 'data-content' => $feedbackcontents, 225 'data-placement' => 'right', 226 'data-trigger' => 'hover focus', 227 'data-html' => 'true', 228 ]); 229 } 230 231 /** 232 * Generates a label for an answer field. 233 * 234 * If the question number is set ({@see qtype_renderer::$questionnumber}), the label will 235 * include the question number in order to indicate which question the answer field belongs to. 236 * 237 * @param string $langkey The lang string key for the lang string that does not include the question number. 238 * @param string $component The Frankenstyle component name. 239 * @return string 240 * @throws coding_exception 241 */ 242 protected function get_answer_label( 243 string $langkey = 'answerx', 244 string $component = 'question' 245 ): string { 246 // There may be multiple answer fields for a question, so we need to increment the answer fields in order to distinguish 247 // them from one another. 248 $questionnumber = $this->displayoptions->questionidentifier ?? ''; 249 $questionnumberindex = $questionnumber !== '' ? $questionnumber : 0; 250 if (isset(self::$answercount[$questionnumberindex][$langkey])) { 251 self::$answercount[$questionnumberindex][$langkey]++; 252 } else { 253 self::$answercount[$questionnumberindex][$langkey] = 1; 254 } 255 256 $params = self::$answercount[$questionnumberindex][$langkey]; 257 258 return $this->displayoptions->add_question_identifier_to_label(get_string($langkey, $component, $params)); 259 } 260 } 261 262 263 /** 264 * Subclass for generating the bits of output specific to shortanswer 265 * subquestions. 266 * 267 * @copyright 2011 The Open University 268 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 269 */ 270 class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_renderer_base { 271 272 public function subquestion(question_attempt $qa, question_display_options $options, 273 $index, question_graded_automatically $subq) { 274 275 $this->displayoptions = $options; 276 277 $fieldprefix = 'sub' . $index . '_'; 278 $fieldname = $fieldprefix . 'answer'; 279 280 $response = $qa->get_last_qt_var($fieldname); 281 if ($subq->qtype->name() == 'shortanswer') { 282 $matchinganswer = $subq->get_matching_answer(array('answer' => $response)); 283 } else if ($subq->qtype->name() == 'numerical') { 284 list($value, $unit, $multiplier) = $subq->ap->apply_units($response, ''); 285 $matchinganswer = $subq->get_matching_answer($value, 1); 286 } else { 287 $matchinganswer = $subq->get_matching_answer($response); 288 } 289 290 if (!$matchinganswer) { 291 if (is_null($response) || $response === '') { 292 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML); 293 } else { 294 $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML); 295 } 296 } 297 298 // Work out a good input field size. 299 $size = max(1, core_text::strlen(trim($response ?? '')) + 1); 300 foreach ($subq->answers as $ans) { 301 $size = max($size, core_text::strlen(trim($ans->answer))); 302 } 303 $size = min(60, round($size + rand(0, (int)($size * 0.15)))); 304 // The rand bit is to make guessing harder. 305 306 $inputattributes = array( 307 'type' => 'text', 308 'name' => $qa->get_qt_field_name($fieldname), 309 'value' => $response, 310 'id' => $qa->get_qt_field_name($fieldname), 311 'size' => $size, 312 'class' => 'form-control mb-1', 313 ); 314 if ($options->readonly) { 315 $inputattributes['readonly'] = 'readonly'; 316 } 317 318 $feedbackimg = ''; 319 if ($options->correctness) { 320 $inputattributes['class'] .= ' ' . $this->feedback_class($matchinganswer->fraction); 321 $feedbackimg = $this->feedback_image($matchinganswer->fraction); 322 } 323 324 if ($subq->qtype->name() == 'shortanswer') { 325 $correctanswer = $subq->get_matching_answer($subq->get_correct_response()); 326 } else { 327 $correctanswer = $subq->get_correct_answer(); 328 } 329 330 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction, 331 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat, 332 $qa, 'question', 'answerfeedback', $matchinganswer->id), 333 s($correctanswer->answer), $options); 334 335 $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline d-inline')); 336 337 $output .= html_writer::tag('label', $this->get_answer_label(), 338 array('class' => 'subq accesshide', 'for' => $inputattributes['id'])); 339 $output .= html_writer::empty_tag('input', $inputattributes); 340 $output .= $this->get_feedback_image($feedbackimg, $feedbackpopup); 341 $output .= html_writer::end_tag('span'); 342 343 return $output; 344 } 345 } 346 347 348 /** 349 * Render an embedded multiple-choice question that is displayed as a select menu. 350 * 351 * @copyright 2011 The Open University 352 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 353 */ 354 class qtype_multianswer_multichoice_inline_renderer 355 extends qtype_multianswer_subq_renderer_base { 356 357 public function subquestion(question_attempt $qa, question_display_options $options, 358 $index, question_graded_automatically $subq) { 359 360 $this->displayoptions = $options; 361 362 $fieldprefix = 'sub' . $index . '_'; 363 $fieldname = $fieldprefix . 'answer'; 364 365 $response = $qa->get_last_qt_var($fieldname); 366 $choices = array(); 367 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML); 368 $rightanswer = null; 369 foreach ($subq->get_order($qa) as $value => $ansid) { 370 $ans = $subq->answers[$ansid]; 371 $choices[$value] = $subq->format_text($ans->answer, $ans->answerformat, 372 $qa, 'question', 'answer', $ansid); 373 if ($subq->is_choice_selected($response, $value)) { 374 $matchinganswer = $ans; 375 } 376 } 377 378 $inputattributes = array( 379 'id' => $qa->get_qt_field_name($fieldname), 380 ); 381 if ($options->readonly) { 382 $inputattributes['disabled'] = 'disabled'; 383 } 384 385 $feedbackimg = ''; 386 if ($options->correctness) { 387 $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction); 388 $feedbackimg = $this->feedback_image($matchinganswer->fraction); 389 } 390 $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname), 391 $response, array('' => ' '), $inputattributes); 392 393 $order = $subq->get_order($qa); 394 $correctresponses = $subq->get_correct_response(); 395 $rightanswer = $subq->answers[$order[reset($correctresponses)]]; 396 if (!$matchinganswer) { 397 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML); 398 } 399 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction, 400 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat, 401 $qa, 'question', 'answerfeedback', $matchinganswer->id), 402 $subq->format_text($rightanswer->answer, $rightanswer->answerformat, 403 $qa, 'question', 'answer', $rightanswer->id), $options); 404 405 $output = html_writer::start_tag('span', array('class' => 'subquestion')); 406 $output .= html_writer::tag('label', $this->get_answer_label(), 407 array('class' => 'subq accesshide', 'for' => $inputattributes['id'])); 408 $output .= $select; 409 $output .= $this->get_feedback_image($feedbackimg, $feedbackpopup); 410 $output .= html_writer::end_tag('span'); 411 412 return $output; 413 } 414 } 415 416 417 /** 418 * Render an embedded multiple-choice question vertically, like for a normal 419 * multiple-choice question. 420 * 421 * @copyright 2010 Pierre Pichet 422 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 423 */ 424 class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_subq_renderer_base { 425 426 public function subquestion(question_attempt $qa, question_display_options $options, 427 $index, question_graded_automatically $subq) { 428 429 $this->displayoptions = $options; 430 431 $fieldprefix = 'sub' . $index . '_'; 432 $fieldname = $fieldprefix . 'answer'; 433 $response = $qa->get_last_qt_var($fieldname); 434 435 $inputattributes = array( 436 'type' => 'radio', 437 'name' => $qa->get_qt_field_name($fieldname), 438 'class' => 'form-check-input', 439 ); 440 if ($options->readonly) { 441 $inputattributes['disabled'] = 'disabled'; 442 } 443 444 $result = $this->all_choices_wrapper_start(); 445 $fraction = null; 446 foreach ($subq->get_order($qa) as $value => $ansid) { 447 $ans = $subq->answers[$ansid]; 448 449 $inputattributes['value'] = $value; 450 $inputattributes['id'] = $inputattributes['name'] . $value; 451 452 $isselected = $subq->is_choice_selected($response, $value); 453 if ($isselected) { 454 $inputattributes['checked'] = 'checked'; 455 $fraction = $ans->fraction; 456 } else { 457 unset($inputattributes['checked']); 458 } 459 460 $class = 'form-check text-wrap text-break'; 461 if ($options->correctness && $isselected) { 462 $feedbackimg = $this->feedback_image($ans->fraction); 463 $class .= ' ' . $this->feedback_class($ans->fraction); 464 } else { 465 $feedbackimg = ''; 466 } 467 468 $result .= $this->choice_wrapper_start($class); 469 $result .= html_writer::empty_tag('input', $inputattributes); 470 $result .= html_writer::tag('label', $subq->format_text($ans->answer, 471 $ans->answerformat, $qa, 'question', 'answer', $ansid), 472 array('for' => $inputattributes['id'], 'class' => 'form-check-label text-body')); 473 $result .= $feedbackimg; 474 475 if ($options->feedback && $isselected && trim($ans->feedback)) { 476 $result .= html_writer::tag('div', 477 $subq->format_text($ans->feedback, $ans->feedbackformat, 478 $qa, 'question', 'answerfeedback', $ansid), 479 array('class' => 'specificfeedback')); 480 } 481 482 $result .= $this->choice_wrapper_end(); 483 } 484 485 $result .= $this->all_choices_wrapper_end(); 486 487 $feedback = array(); 488 if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX && 489 $subq->maxmark > 0) { 490 $a = new stdClass(); 491 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp); 492 $a->max = format_float($subq->maxmark, $options->markdp); 493 494 $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a)); 495 } 496 497 if ($options->rightanswer) { 498 foreach ($subq->answers as $ans) { 499 if (question_state::graded_state_for_fraction($ans->fraction) == 500 question_state::$gradedright) { 501 $feedback[] = get_string('correctansweris', 'qtype_multichoice', 502 $subq->format_text($ans->answer, $ans->answerformat, 503 $qa, 'question', 'answer', $ansid)); 504 break; 505 } 506 } 507 } 508 509 $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome')); 510 511 return $result; 512 } 513 514 /** 515 * @param string $class class attribute value. 516 * @return string HTML to go before each choice. 517 */ 518 protected function choice_wrapper_start($class) { 519 return html_writer::start_tag('div', array('class' => $class)); 520 } 521 522 /** 523 * @return string HTML to go after each choice. 524 */ 525 protected function choice_wrapper_end() { 526 return html_writer::end_tag('div'); 527 } 528 529 /** 530 * @return string HTML to go before all the choices. 531 */ 532 protected function all_choices_wrapper_start() { 533 $wrapperstart = html_writer::start_tag('fieldset', array('class' => 'answer')); 534 $legendtext = $this->get_answer_label('multichoicex', 'qtype_multianswer'); 535 $wrapperstart .= html_writer::tag('legend', $legendtext, ['class' => 'sr-only']); 536 return $wrapperstart; 537 } 538 539 /** 540 * @return string HTML to go after all the choices. 541 */ 542 protected function all_choices_wrapper_end() { 543 return html_writer::end_tag('fieldset'); 544 } 545 } 546 547 548 /** 549 * Render an embedded multiple-choice question vertically, like for a normal 550 * multiple-choice question. 551 * 552 * @copyright 2010 Pierre Pichet 553 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 554 */ 555 class qtype_multianswer_multichoice_horizontal_renderer 556 extends qtype_multianswer_multichoice_vertical_renderer { 557 558 protected function choice_wrapper_start($class) { 559 return html_writer::start_tag('div', array('class' => $class . ' form-check-inline')); 560 } 561 562 protected function choice_wrapper_end() { 563 return html_writer::end_tag('div'); 564 } 565 566 protected function all_choices_wrapper_start() { 567 $wrapperstart = html_writer::start_tag('fieldset', ['class' => 'answer']); 568 $captiontext = $this->get_answer_label('multichoicex', 'qtype_multianswer'); 569 $wrapperstart .= html_writer::tag('legend', $captiontext, ['class' => 'sr-only']); 570 return $wrapperstart; 571 } 572 573 protected function all_choices_wrapper_end() { 574 return html_writer::end_tag('fieldset'); 575 } 576 } 577 578 /** 579 * Class qtype_multianswer_multiresponse_renderer 580 * 581 * @copyright 2016 Davo Smith, Synergy Learning 582 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 583 */ 584 class qtype_multianswer_multiresponse_vertical_renderer extends qtype_multianswer_subq_renderer_base { 585 586 /** 587 * Output the content of the subquestion. 588 * 589 * @param question_attempt $qa 590 * @param question_display_options $options 591 * @param int $index 592 * @param question_graded_automatically $subq 593 * @return string 594 */ 595 public function subquestion(question_attempt $qa, question_display_options $options, 596 $index, question_graded_automatically $subq) { 597 598 if (!$subq instanceof qtype_multichoice_multi_question) { 599 throw new coding_exception('Expecting subquestion of type qtype_multichoice_multi_question'); 600 } 601 602 $fieldprefix = 'sub' . $index . '_'; 603 $fieldname = $fieldprefix . 'choice'; 604 605 // Extract the responses that related to this question + strip off the prefix. 606 $fieldprefixlen = strlen($fieldprefix); 607 $response = []; 608 foreach ($qa->get_last_qt_data() as $name => $val) { 609 if (substr($name, 0, $fieldprefixlen) == $fieldprefix) { 610 $name = substr($name, $fieldprefixlen); 611 $response[$name] = $val; 612 } 613 } 614 615 $basename = $qa->get_qt_field_name($fieldname); 616 $inputattributes = array( 617 'type' => 'checkbox', 618 'value' => 1, 619 ); 620 if ($options->readonly) { 621 $inputattributes['disabled'] = 'disabled'; 622 } 623 624 $result = $this->all_choices_wrapper_start(); 625 626 // Calculate the total score (as we need to know if choices should be marked as 'correct' or 'partial'). 627 $fraction = 0; 628 foreach ($subq->get_order($qa) as $value => $ansid) { 629 $ans = $subq->answers[$ansid]; 630 if ($subq->is_choice_selected($response, $value)) { 631 $fraction += $ans->fraction; 632 } 633 } 634 // Display 'correct' answers as correct, if we are at 100%, otherwise mark them as 'partial'. 635 $answerfraction = ($fraction > 0.999) ? 1.0 : 0.5; 636 637 foreach ($subq->get_order($qa) as $value => $ansid) { 638 $ans = $subq->answers[$ansid]; 639 640 $name = $basename.$value; 641 $inputattributes['name'] = $name; 642 $inputattributes['id'] = $name; 643 644 $isselected = $subq->is_choice_selected($response, $value); 645 if ($isselected) { 646 $inputattributes['checked'] = 'checked'; 647 } else { 648 unset($inputattributes['checked']); 649 } 650 651 $class = 'r' . ($value % 2); 652 if ($options->correctness && $isselected) { 653 $thisfrac = ($ans->fraction > 0) ? $answerfraction : 0; 654 $feedbackimg = $this->feedback_image($thisfrac); 655 $class .= ' ' . $this->feedback_class($thisfrac); 656 } else { 657 $feedbackimg = ''; 658 } 659 660 $result .= $this->choice_wrapper_start($class); 661 $result .= html_writer::empty_tag('input', $inputattributes); 662 $result .= html_writer::tag('label', $subq->format_text($ans->answer, 663 $ans->answerformat, $qa, 'question', 'answer', $ansid), 664 array('for' => $inputattributes['id'])); 665 $result .= $feedbackimg; 666 667 if ($options->feedback && $isselected && trim($ans->feedback)) { 668 $result .= html_writer::tag('div', 669 $subq->format_text($ans->feedback, $ans->feedbackformat, 670 $qa, 'question', 'answerfeedback', $ansid), 671 array('class' => 'specificfeedback')); 672 } 673 674 $result .= $this->choice_wrapper_end(); 675 } 676 677 $result .= $this->all_choices_wrapper_end(); 678 679 $feedback = array(); 680 if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX && 681 $subq->maxmark > 0) { 682 $a = new stdClass(); 683 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp); 684 $a->max = format_float($subq->maxmark, $options->markdp); 685 686 $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a)); 687 } 688 689 if ($options->rightanswer) { 690 $correct = []; 691 foreach ($subq->answers as $ans) { 692 if (question_state::graded_state_for_fraction($ans->fraction) != question_state::$gradedwrong) { 693 $correct[] = $subq->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ans->id); 694 } 695 } 696 $correct = '<ul><li>'.implode('</li><li>', $correct).'</li></ul>'; 697 $feedback[] = get_string('correctansweris', 'qtype_multichoice', $correct); 698 } 699 700 $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome')); 701 702 return $result; 703 } 704 705 /** 706 * @param string $class class attribute value. 707 * @return string HTML to go before each choice. 708 */ 709 protected function choice_wrapper_start($class) { 710 return html_writer::start_tag('div', array('class' => $class)); 711 } 712 713 /** 714 * @return string HTML to go after each choice. 715 */ 716 protected function choice_wrapper_end() { 717 return html_writer::end_tag('div'); 718 } 719 720 /** 721 * @return string HTML to go before all the choices. 722 */ 723 protected function all_choices_wrapper_start() { 724 return html_writer::start_tag('div', array('class' => 'answer')); 725 } 726 727 /** 728 * @return string HTML to go after all the choices. 729 */ 730 protected function all_choices_wrapper_end() { 731 return html_writer::end_tag('div'); 732 } 733 } 734 735 /** 736 * Render an embedded multiple-response question horizontally. 737 * 738 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 739 */ 740 class qtype_multianswer_multiresponse_horizontal_renderer 741 extends qtype_multianswer_multiresponse_vertical_renderer { 742 743 protected function choice_wrapper_start($class) { 744 return html_writer::start_tag('td', array('class' => $class)); 745 } 746 747 protected function choice_wrapper_end() { 748 return html_writer::end_tag('td'); 749 } 750 751 protected function all_choices_wrapper_start() { 752 return html_writer::start_tag('table', array('class' => 'answer')) . 753 html_writer::start_tag('tbody') . html_writer::start_tag('tr'); 754 } 755 756 protected function all_choices_wrapper_end() { 757 return html_writer::end_tag('tr') . html_writer::end_tag('tbody') . 758 html_writer::end_tag('table'); 759 } 760 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body