Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 $this->page->requires->js_init_call('M.qtype_multianswer.init', 92 array('#' . $qa->get_outer_question_div_unique_id()), false, array( 93 'name' => 'qtype_multianswer', 94 'fullpath' => '/question/type/multianswer/module.js', 95 'requires' => array('base', 'node', 'event', 'overlay'), 96 )); 97 98 return $output; 99 } 100 101 public function subquestion(question_attempt $qa, 102 question_display_options $options, $index, question_automatically_gradable $subq) { 103 104 $subtype = $subq->qtype->name(); 105 if ($subtype == 'numerical' || $subtype == 'shortanswer') { 106 $subrenderer = 'textfield'; 107 } else if ($subtype == 'multichoice') { 108 if ($subq instanceof qtype_multichoice_multi_question) { 109 if ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL) { 110 $subrenderer = 'multiresponse_vertical'; 111 } else { 112 $subrenderer = 'multiresponse_horizontal'; 113 } 114 } else { 115 if ($subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) { 116 $subrenderer = 'multichoice_inline'; 117 } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) { 118 $subrenderer = 'multichoice_horizontal'; 119 } else { 120 $subrenderer = 'multichoice_vertical'; 121 } 122 } 123 } else if ($subtype == 'subquestion_replacement') { 124 return html_writer::div( 125 get_string('missingsubquestion', 'qtype_multianswer'), 126 'notifyproblem' 127 ); 128 } else { 129 throw new coding_exception('Unexpected subquestion type.', $subq); 130 } 131 $renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer); 132 return $renderer->subquestion($qa, $options, $index, $subq); 133 } 134 135 public function correct_response(question_attempt $qa) { 136 return ''; 137 } 138 } 139 140 141 /** 142 * Subclass for generating the bits of output specific to shortanswer 143 * subquestions. 144 * 145 * @copyright 2011 The Open University 146 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 147 */ 148 abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer { 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 array('class' => 'feedbackspan accesshide')); 200 } 201 } 202 203 204 /** 205 * Subclass for generating the bits of output specific to shortanswer 206 * subquestions. 207 * 208 * @copyright 2011 The Open University 209 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 210 */ 211 class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_renderer_base { 212 213 public function subquestion(question_attempt $qa, question_display_options $options, 214 $index, question_graded_automatically $subq) { 215 216 $fieldprefix = 'sub' . $index . '_'; 217 $fieldname = $fieldprefix . 'answer'; 218 219 $response = $qa->get_last_qt_var($fieldname); 220 if ($subq->qtype->name() == 'shortanswer') { 221 $matchinganswer = $subq->get_matching_answer(array('answer' => $response)); 222 } else if ($subq->qtype->name() == 'numerical') { 223 list($value, $unit, $multiplier) = $subq->ap->apply_units($response, ''); 224 $matchinganswer = $subq->get_matching_answer($value, 1); 225 } else { 226 $matchinganswer = $subq->get_matching_answer($response); 227 } 228 229 if (!$matchinganswer) { 230 if (is_null($response) || $response === '') { 231 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML); 232 } else { 233 $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML); 234 } 235 } 236 237 // Work out a good input field size. 238 $size = max(1, core_text::strlen(trim($response)) + 1); 239 foreach ($subq->answers as $ans) { 240 $size = max($size, core_text::strlen(trim($ans->answer))); 241 } 242 $size = min(60, round($size + rand(0, $size * 0.15))); 243 // The rand bit is to make guessing harder. 244 245 $inputattributes = array( 246 'type' => 'text', 247 'name' => $qa->get_qt_field_name($fieldname), 248 'value' => $response, 249 'id' => $qa->get_qt_field_name($fieldname), 250 'size' => $size, 251 'class' => 'form-control mb-1', 252 ); 253 if ($options->readonly) { 254 $inputattributes['readonly'] = 'readonly'; 255 } 256 257 $feedbackimg = ''; 258 if ($options->correctness) { 259 $inputattributes['class'] .= ' ' . $this->feedback_class($matchinganswer->fraction); 260 $feedbackimg = $this->feedback_image($matchinganswer->fraction); 261 } 262 263 if ($subq->qtype->name() == 'shortanswer') { 264 $correctanswer = $subq->get_matching_answer($subq->get_correct_response()); 265 } else { 266 $correctanswer = $subq->get_correct_answer(); 267 } 268 269 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction, 270 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat, 271 $qa, 'question', 'answerfeedback', $matchinganswer->id), 272 s($correctanswer->answer), $options); 273 274 $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline d-inline')); 275 $output .= html_writer::tag('label', get_string('answer'), 276 array('class' => 'subq accesshide', 'for' => $inputattributes['id'])); 277 $output .= html_writer::empty_tag('input', $inputattributes); 278 $output .= $feedbackimg; 279 $output .= $feedbackpopup; 280 $output .= html_writer::end_tag('span'); 281 282 return $output; 283 } 284 } 285 286 287 /** 288 * Render an embedded multiple-choice question that is displayed as a select menu. 289 * 290 * @copyright 2011 The Open University 291 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 292 */ 293 class qtype_multianswer_multichoice_inline_renderer 294 extends qtype_multianswer_subq_renderer_base { 295 296 public function subquestion(question_attempt $qa, question_display_options $options, 297 $index, question_graded_automatically $subq) { 298 299 $fieldprefix = 'sub' . $index . '_'; 300 $fieldname = $fieldprefix . 'answer'; 301 302 $response = $qa->get_last_qt_var($fieldname); 303 $choices = array(); 304 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML); 305 $rightanswer = null; 306 foreach ($subq->get_order($qa) as $value => $ansid) { 307 $ans = $subq->answers[$ansid]; 308 $choices[$value] = $subq->format_text($ans->answer, $ans->answerformat, 309 $qa, 'question', 'answer', $ansid); 310 if ($subq->is_choice_selected($response, $value)) { 311 $matchinganswer = $ans; 312 } 313 } 314 315 $inputattributes = array( 316 'id' => $qa->get_qt_field_name($fieldname), 317 ); 318 if ($options->readonly) { 319 $inputattributes['disabled'] = 'disabled'; 320 } 321 322 $feedbackimg = ''; 323 if ($options->correctness) { 324 $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction); 325 $feedbackimg = $this->feedback_image($matchinganswer->fraction); 326 } 327 $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname), 328 $response, array('' => ' '), $inputattributes); 329 330 $order = $subq->get_order($qa); 331 $correctresponses = $subq->get_correct_response(); 332 $rightanswer = $subq->answers[$order[reset($correctresponses)]]; 333 if (!$matchinganswer) { 334 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML); 335 } 336 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction, 337 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat, 338 $qa, 'question', 'answerfeedback', $matchinganswer->id), 339 $subq->format_text($rightanswer->answer, $rightanswer->answerformat, 340 $qa, 'question', 'answer', $rightanswer->id), $options); 341 342 $output = html_writer::start_tag('span', array('class' => 'subquestion')); 343 $output .= html_writer::tag('label', get_string('answer'), 344 array('class' => 'subq accesshide', 'for' => $inputattributes['id'])); 345 $output .= $select; 346 $output .= $feedbackimg; 347 $output .= $feedbackpopup; 348 $output .= html_writer::end_tag('span'); 349 350 return $output; 351 } 352 } 353 354 355 /** 356 * Render an embedded multiple-choice question vertically, like for a normal 357 * multiple-choice question. 358 * 359 * @copyright 2010 Pierre Pichet 360 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 361 */ 362 class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_subq_renderer_base { 363 364 public function subquestion(question_attempt $qa, question_display_options $options, 365 $index, question_graded_automatically $subq) { 366 367 $fieldprefix = 'sub' . $index . '_'; 368 $fieldname = $fieldprefix . 'answer'; 369 $response = $qa->get_last_qt_var($fieldname); 370 371 $inputattributes = array( 372 'type' => 'radio', 373 'name' => $qa->get_qt_field_name($fieldname), 374 ); 375 if ($options->readonly) { 376 $inputattributes['disabled'] = 'disabled'; 377 } 378 379 $result = $this->all_choices_wrapper_start(); 380 $fraction = null; 381 foreach ($subq->get_order($qa) as $value => $ansid) { 382 $ans = $subq->answers[$ansid]; 383 384 $inputattributes['value'] = $value; 385 $inputattributes['id'] = $inputattributes['name'] . $value; 386 387 $isselected = $subq->is_choice_selected($response, $value); 388 if ($isselected) { 389 $inputattributes['checked'] = 'checked'; 390 $fraction = $ans->fraction; 391 } else { 392 unset($inputattributes['checked']); 393 } 394 395 $class = 'r' . ($value % 2); 396 if ($options->correctness && $isselected) { 397 $feedbackimg = $this->feedback_image($ans->fraction); 398 $class .= ' ' . $this->feedback_class($ans->fraction); 399 } else { 400 $feedbackimg = ''; 401 } 402 403 $result .= $this->choice_wrapper_start($class); 404 $result .= html_writer::empty_tag('input', $inputattributes); 405 $result .= html_writer::tag('label', $subq->format_text($ans->answer, 406 $ans->answerformat, $qa, 'question', 'answer', $ansid), 407 array('for' => $inputattributes['id'])); 408 $result .= $feedbackimg; 409 410 if ($options->feedback && $isselected && trim($ans->feedback)) { 411 $result .= html_writer::tag('div', 412 $subq->format_text($ans->feedback, $ans->feedbackformat, 413 $qa, 'question', 'answerfeedback', $ansid), 414 array('class' => 'specificfeedback')); 415 } 416 417 $result .= $this->choice_wrapper_end(); 418 } 419 420 $result .= $this->all_choices_wrapper_end(); 421 422 $feedback = array(); 423 if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX && 424 $subq->maxmark > 0) { 425 $a = new stdClass(); 426 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp); 427 $a->max = format_float($subq->maxmark, $options->markdp); 428 429 $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a)); 430 } 431 432 if ($options->rightanswer) { 433 foreach ($subq->answers as $ans) { 434 if (question_state::graded_state_for_fraction($ans->fraction) == 435 question_state::$gradedright) { 436 $feedback[] = get_string('correctansweris', 'qtype_multichoice', 437 $subq->format_text($ans->answer, $ans->answerformat, 438 $qa, 'question', 'answer', $ansid)); 439 break; 440 } 441 } 442 } 443 444 $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome')); 445 446 return $result; 447 } 448 449 /** 450 * @param string $class class attribute value. 451 * @return string HTML to go before each choice. 452 */ 453 protected function choice_wrapper_start($class) { 454 return html_writer::start_tag('div', array('class' => $class)); 455 } 456 457 /** 458 * @return string HTML to go after each choice. 459 */ 460 protected function choice_wrapper_end() { 461 return html_writer::end_tag('div'); 462 } 463 464 /** 465 * @return string HTML to go before all the choices. 466 */ 467 protected function all_choices_wrapper_start() { 468 return html_writer::start_tag('div', array('class' => 'answer')); 469 } 470 471 /** 472 * @return string HTML to go after all the choices. 473 */ 474 protected function all_choices_wrapper_end() { 475 return html_writer::end_tag('div'); 476 } 477 } 478 479 480 /** 481 * Render an embedded multiple-choice question vertically, like for a normal 482 * multiple-choice question. 483 * 484 * @copyright 2010 Pierre Pichet 485 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 486 */ 487 class qtype_multianswer_multichoice_horizontal_renderer 488 extends qtype_multianswer_multichoice_vertical_renderer { 489 490 protected function choice_wrapper_start($class) { 491 return html_writer::start_tag('td', array('class' => $class)); 492 } 493 494 protected function choice_wrapper_end() { 495 return html_writer::end_tag('td'); 496 } 497 498 protected function all_choices_wrapper_start() { 499 return html_writer::start_tag('table', array('class' => 'answer')) . 500 html_writer::start_tag('tbody') . html_writer::start_tag('tr'); 501 } 502 503 protected function all_choices_wrapper_end() { 504 return html_writer::end_tag('tr') . html_writer::end_tag('tbody') . 505 html_writer::end_tag('table'); 506 } 507 } 508 509 /** 510 * Class qtype_multianswer_multiresponse_renderer 511 * 512 * @copyright 2016 Davo Smith, Synergy Learning 513 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 514 */ 515 class qtype_multianswer_multiresponse_vertical_renderer extends qtype_multianswer_subq_renderer_base { 516 517 /** 518 * Output the content of the subquestion. 519 * 520 * @param question_attempt $qa 521 * @param question_display_options $options 522 * @param int $index 523 * @param question_graded_automatically $subq 524 * @return string 525 */ 526 public function subquestion(question_attempt $qa, question_display_options $options, 527 $index, question_graded_automatically $subq) { 528 529 if (!$subq instanceof qtype_multichoice_multi_question) { 530 throw new coding_exception('Expecting subquestion of type qtype_multichoice_multi_question'); 531 } 532 533 $fieldprefix = 'sub' . $index . '_'; 534 $fieldname = $fieldprefix . 'choice'; 535 536 // Extract the responses that related to this question + strip off the prefix. 537 $fieldprefixlen = strlen($fieldprefix); 538 $response = []; 539 foreach ($qa->get_last_qt_data() as $name => $val) { 540 if (substr($name, 0, $fieldprefixlen) == $fieldprefix) { 541 $name = substr($name, $fieldprefixlen); 542 $response[$name] = $val; 543 } 544 } 545 546 $basename = $qa->get_qt_field_name($fieldname); 547 $inputattributes = array( 548 'type' => 'checkbox', 549 'value' => 1, 550 ); 551 if ($options->readonly) { 552 $inputattributes['disabled'] = 'disabled'; 553 } 554 555 $result = $this->all_choices_wrapper_start(); 556 557 // Calculate the total score (as we need to know if choices should be marked as 'correct' or 'partial'). 558 $fraction = 0; 559 foreach ($subq->get_order($qa) as $value => $ansid) { 560 $ans = $subq->answers[$ansid]; 561 if ($subq->is_choice_selected($response, $value)) { 562 $fraction += $ans->fraction; 563 } 564 } 565 // Display 'correct' answers as correct, if we are at 100%, otherwise mark them as 'partial'. 566 $answerfraction = ($fraction > 0.999) ? 1.0 : 0.5; 567 568 foreach ($subq->get_order($qa) as $value => $ansid) { 569 $ans = $subq->answers[$ansid]; 570 571 $name = $basename.$value; 572 $inputattributes['name'] = $name; 573 $inputattributes['id'] = $name; 574 575 $isselected = $subq->is_choice_selected($response, $value); 576 if ($isselected) { 577 $inputattributes['checked'] = 'checked'; 578 } else { 579 unset($inputattributes['checked']); 580 } 581 582 $class = 'r' . ($value % 2); 583 if ($options->correctness && $isselected) { 584 $thisfrac = ($ans->fraction > 0) ? $answerfraction : 0; 585 $feedbackimg = $this->feedback_image($thisfrac); 586 $class .= ' ' . $this->feedback_class($thisfrac); 587 } else { 588 $feedbackimg = ''; 589 } 590 591 $result .= $this->choice_wrapper_start($class); 592 $result .= html_writer::empty_tag('input', $inputattributes); 593 $result .= html_writer::tag('label', $subq->format_text($ans->answer, 594 $ans->answerformat, $qa, 'question', 'answer', $ansid), 595 array('for' => $inputattributes['id'])); 596 $result .= $feedbackimg; 597 598 if ($options->feedback && $isselected && trim($ans->feedback)) { 599 $result .= html_writer::tag('div', 600 $subq->format_text($ans->feedback, $ans->feedbackformat, 601 $qa, 'question', 'answerfeedback', $ansid), 602 array('class' => 'specificfeedback')); 603 } 604 605 $result .= $this->choice_wrapper_end(); 606 } 607 608 $result .= $this->all_choices_wrapper_end(); 609 610 $feedback = array(); 611 if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX && 612 $subq->maxmark > 0) { 613 $a = new stdClass(); 614 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp); 615 $a->max = format_float($subq->maxmark, $options->markdp); 616 617 $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a)); 618 } 619 620 if ($options->rightanswer) { 621 $correct = []; 622 foreach ($subq->answers as $ans) { 623 if (question_state::graded_state_for_fraction($ans->fraction) != question_state::$gradedwrong) { 624 $correct[] = $subq->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ans->id); 625 } 626 } 627 $correct = '<ul><li>'.implode('</li><li>', $correct).'</li></ul>'; 628 $feedback[] = get_string('correctansweris', 'qtype_multichoice', $correct); 629 } 630 631 $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome')); 632 633 return $result; 634 } 635 636 /** 637 * @param string $class class attribute value. 638 * @return string HTML to go before each choice. 639 */ 640 protected function choice_wrapper_start($class) { 641 return html_writer::start_tag('div', array('class' => $class)); 642 } 643 644 /** 645 * @return string HTML to go after each choice. 646 */ 647 protected function choice_wrapper_end() { 648 return html_writer::end_tag('div'); 649 } 650 651 /** 652 * @return string HTML to go before all the choices. 653 */ 654 protected function all_choices_wrapper_start() { 655 return html_writer::start_tag('div', array('class' => 'answer')); 656 } 657 658 /** 659 * @return string HTML to go after all the choices. 660 */ 661 protected function all_choices_wrapper_end() { 662 return html_writer::end_tag('div'); 663 } 664 } 665 666 /** 667 * Render an embedded multiple-response question horizontally. 668 * 669 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 670 */ 671 class qtype_multianswer_multiresponse_horizontal_renderer 672 extends qtype_multianswer_multiresponse_vertical_renderer { 673 674 protected function choice_wrapper_start($class) { 675 return html_writer::start_tag('td', array('class' => $class)); 676 } 677 678 protected function choice_wrapper_end() { 679 return html_writer::end_tag('td'); 680 } 681 682 protected function all_choices_wrapper_start() { 683 return html_writer::start_tag('table', array('class' => 'answer')) . 684 html_writer::start_tag('tbody') . html_writer::start_tag('tr'); 685 } 686 687 protected function all_choices_wrapper_end() { 688 return html_writer::end_tag('tr') . html_writer::end_tag('tbody') . 689 html_writer::end_tag('table'); 690 } 691 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body