Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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('' => '&nbsp;'), $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  }