Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * 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          foreach ($question->textfragments as $i => $fragment) {
  50              if ($i > 0) {
  51                  $index = $question->places[$i];
  52                  $token = 'qtypemultianswer' . $i . 'marker';
  53                  $token = '<span class="nolink">' . $token . '</span>';
  54                  $output .= $token;
  55                  $subquestions[$token] = $this->subquestion($qa, $options, $index,
  56                          $question->subquestions[$index]);
  57              }
  58              $output .= $fragment;
  59          }
  60          $output = $question->format_text($output, $question->questiontextformat,
  61                  $qa, 'question', 'questiontext', $question->id);
  62          $output = str_replace(array_keys($subquestions), array_values($subquestions), $output);
  63  
  64          if ($qa->get_state() == question_state::$invalid) {
  65              $output .= html_writer::nonempty_tag('div',
  66                      $question->get_validation_error($qa->get_last_qt_data()),
  67                      array('class' => 'validationerror'));
  68          }
  69  
  70          $this->page->requires->js_init_call('M.qtype_multianswer.init',
  71                  array('#' . $qa->get_outer_question_div_unique_id()), false, array(
  72                      'name'     => 'qtype_multianswer',
  73                      'fullpath' => '/question/type/multianswer/module.js',
  74                      'requires' => array('base', 'node', 'event', 'overlay'),
  75                  ));
  76  
  77          return $output;
  78      }
  79  
  80      public function subquestion(question_attempt $qa,
  81              question_display_options $options, $index, question_graded_automatically $subq) {
  82  
  83          $subtype = $subq->qtype->name();
  84          if ($subtype == 'numerical' || $subtype == 'shortanswer') {
  85              $subrenderer = 'textfield';
  86          } else if ($subtype == 'multichoice') {
  87              if ($subq instanceof qtype_multichoice_multi_question) {
  88                  if ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL) {
  89                      $subrenderer = 'multiresponse_vertical';
  90                  } else {
  91                      $subrenderer = 'multiresponse_horizontal';
  92                  }
  93              } else {
  94                  if ($subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) {
  95                      $subrenderer = 'multichoice_inline';
  96                  } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) {
  97                      $subrenderer = 'multichoice_horizontal';
  98                  } else {
  99                      $subrenderer = 'multichoice_vertical';
 100                  }
 101              }
 102          } else {
 103              throw new coding_exception('Unexpected subquestion type.', $subq);
 104          }
 105          $renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer);
 106          return $renderer->subquestion($qa, $options, $index, $subq);
 107      }
 108  
 109      public function correct_response(question_attempt $qa) {
 110          return '';
 111      }
 112  }
 113  
 114  
 115  /**
 116   * Subclass for generating the bits of output specific to shortanswer
 117   * subquestions.
 118   *
 119   * @copyright 2011 The Open University
 120   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 121   */
 122  abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
 123  
 124      abstract public function subquestion(question_attempt $qa,
 125              question_display_options $options, $index,
 126              question_graded_automatically $subq);
 127  
 128      /**
 129       * Render the feedback pop-up contents.
 130       *
 131       * @param question_graded_automatically $subq the subquestion.
 132       * @param float $fraction the mark the student got. null if this subq was not answered.
 133       * @param string $feedbacktext the feedback text, already processed with format_text etc.
 134       * @param string $rightanswer the right answer, already processed with format_text etc.
 135       * @param question_display_options $options the display options.
 136       * @return string the HTML for the feedback popup.
 137       */
 138      protected function feedback_popup(question_graded_automatically $subq,
 139              $fraction, $feedbacktext, $rightanswer, question_display_options $options) {
 140  
 141          $feedback = array();
 142          if ($options->correctness) {
 143              if (is_null($fraction)) {
 144                  $state = question_state::$gaveup;
 145              } else {
 146                  $state = question_state::graded_state_for_fraction($fraction);
 147              }
 148              $feedback[] = $state->default_string(true);
 149          }
 150  
 151          if ($options->feedback && $feedbacktext) {
 152              $feedback[] = $feedbacktext;
 153          }
 154  
 155          if ($options->rightanswer) {
 156              $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer);
 157          }
 158  
 159          $subfraction = '';
 160          if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->maxmark > 0
 161                  && (!is_null($fraction) || $feedback)) {
 162              $a = new stdClass();
 163              $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
 164              $a->max = format_float($subq->maxmark, $options->markdp);
 165              $feedback[] = get_string('markoutofmax', 'question', $a);
 166          }
 167  
 168          if (!$feedback) {
 169              return '';
 170          }
 171  
 172          return html_writer::tag('span', implode('<br />', $feedback),
 173                  array('class' => 'feedbackspan accesshide'));
 174      }
 175  }
 176  
 177  
 178  /**
 179   * Subclass for generating the bits of output specific to shortanswer
 180   * subquestions.
 181   *
 182   * @copyright 2011 The Open University
 183   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 184   */
 185  class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_renderer_base {
 186  
 187      public function subquestion(question_attempt $qa, question_display_options $options,
 188              $index, question_graded_automatically $subq) {
 189  
 190          $fieldprefix = 'sub' . $index . '_';
 191          $fieldname = $fieldprefix . 'answer';
 192  
 193          $response = $qa->get_last_qt_var($fieldname);
 194          if ($subq->qtype->name() == 'shortanswer') {
 195              $matchinganswer = $subq->get_matching_answer(array('answer' => $response));
 196          } else if ($subq->qtype->name() == 'numerical') {
 197              list($value, $unit, $multiplier) = $subq->ap->apply_units($response, '');
 198              $matchinganswer = $subq->get_matching_answer($value, 1);
 199          } else {
 200              $matchinganswer = $subq->get_matching_answer($response);
 201          }
 202  
 203          if (!$matchinganswer) {
 204              if (is_null($response) || $response === '') {
 205                  $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
 206              } else {
 207                  $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML);
 208              }
 209          }
 210  
 211          // Work out a good input field size.
 212          $size = max(1, core_text::strlen(trim($response)) + 1);
 213          foreach ($subq->answers as $ans) {
 214              $size = max($size, core_text::strlen(trim($ans->answer)));
 215          }
 216          $size = min(60, round($size + rand(0, $size * 0.15)));
 217          // The rand bit is to make guessing harder.
 218  
 219          $inputattributes = array(
 220              'type' => 'text',
 221              'name' => $qa->get_qt_field_name($fieldname),
 222              'value' => $response,
 223              'id' => $qa->get_qt_field_name($fieldname),
 224              'size' => $size,
 225              'class' => 'form-control mb-1',
 226          );
 227          if ($options->readonly) {
 228              $inputattributes['readonly'] = 'readonly';
 229          }
 230  
 231          $feedbackimg = '';
 232          if ($options->correctness) {
 233              $inputattributes['class'] .= ' ' . $this->feedback_class($matchinganswer->fraction);
 234              $feedbackimg = $this->feedback_image($matchinganswer->fraction);
 235          }
 236  
 237          if ($subq->qtype->name() == 'shortanswer') {
 238              $correctanswer = $subq->get_matching_answer($subq->get_correct_response());
 239          } else {
 240              $correctanswer = $subq->get_correct_answer();
 241          }
 242  
 243          $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
 244                  $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
 245                          $qa, 'question', 'answerfeedback', $matchinganswer->id),
 246                  s($correctanswer->answer), $options);
 247  
 248          $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline d-inline'));
 249          $output .= html_writer::tag('label', get_string('answer'),
 250                  array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
 251          $output .= html_writer::empty_tag('input', $inputattributes);
 252          $output .= $feedbackimg;
 253          $output .= $feedbackpopup;
 254          $output .= html_writer::end_tag('span');
 255  
 256          return $output;
 257      }
 258  }
 259  
 260  
 261  /**
 262   * Render an embedded multiple-choice question that is displayed as a select menu.
 263   *
 264   * @copyright  2011 The Open University
 265   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 266   */
 267  class qtype_multianswer_multichoice_inline_renderer
 268          extends qtype_multianswer_subq_renderer_base {
 269  
 270      public function subquestion(question_attempt $qa, question_display_options $options,
 271              $index, question_graded_automatically $subq) {
 272  
 273          $fieldprefix = 'sub' . $index . '_';
 274          $fieldname = $fieldprefix . 'answer';
 275  
 276          $response = $qa->get_last_qt_var($fieldname);
 277          $choices = array();
 278          $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
 279          $rightanswer = null;
 280          foreach ($subq->get_order($qa) as $value => $ansid) {
 281              $ans = $subq->answers[$ansid];
 282              $choices[$value] = $subq->format_text($ans->answer, $ans->answerformat,
 283                      $qa, 'question', 'answer', $ansid);
 284              if ($subq->is_choice_selected($response, $value)) {
 285                  $matchinganswer = $ans;
 286              }
 287          }
 288  
 289          $inputattributes = array(
 290              'id' => $qa->get_qt_field_name($fieldname),
 291          );
 292          if ($options->readonly) {
 293              $inputattributes['disabled'] = 'disabled';
 294          }
 295  
 296          $feedbackimg = '';
 297          if ($options->correctness) {
 298              $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
 299              $feedbackimg = $this->feedback_image($matchinganswer->fraction);
 300          }
 301          $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname),
 302                  $response, array('' => '&nbsp;'), $inputattributes);
 303  
 304          $order = $subq->get_order($qa);
 305          $correctresponses = $subq->get_correct_response();
 306          $rightanswer = $subq->answers[$order[reset($correctresponses)]];
 307          if (!$matchinganswer) {
 308              $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
 309          }
 310          $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
 311                  $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
 312                          $qa, 'question', 'answerfeedback', $matchinganswer->id),
 313                  $subq->format_text($rightanswer->answer, $rightanswer->answerformat,
 314                          $qa, 'question', 'answer', $rightanswer->id), $options);
 315  
 316          $output = html_writer::start_tag('span', array('class' => 'subquestion'));
 317          $output .= html_writer::tag('label', get_string('answer'),
 318                  array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
 319          $output .= $select;
 320          $output .= $feedbackimg;
 321          $output .= $feedbackpopup;
 322          $output .= html_writer::end_tag('span');
 323  
 324          return $output;
 325      }
 326  }
 327  
 328  
 329  /**
 330   * Render an embedded multiple-choice question vertically, like for a normal
 331   * multiple-choice question.
 332   *
 333   * @copyright  2010 Pierre Pichet
 334   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 335   */
 336  class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_subq_renderer_base {
 337  
 338      public function subquestion(question_attempt $qa, question_display_options $options,
 339              $index, question_graded_automatically $subq) {
 340  
 341          $fieldprefix = 'sub' . $index . '_';
 342          $fieldname = $fieldprefix . 'answer';
 343          $response = $qa->get_last_qt_var($fieldname);
 344  
 345          $inputattributes = array(
 346              'type' => 'radio',
 347              'name' => $qa->get_qt_field_name($fieldname),
 348          );
 349          if ($options->readonly) {
 350              $inputattributes['disabled'] = 'disabled';
 351          }
 352  
 353          $result = $this->all_choices_wrapper_start();
 354          $fraction = null;
 355          foreach ($subq->get_order($qa) as $value => $ansid) {
 356              $ans = $subq->answers[$ansid];
 357  
 358              $inputattributes['value'] = $value;
 359              $inputattributes['id'] = $inputattributes['name'] . $value;
 360  
 361              $isselected = $subq->is_choice_selected($response, $value);
 362              if ($isselected) {
 363                  $inputattributes['checked'] = 'checked';
 364                  $fraction = $ans->fraction;
 365              } else {
 366                  unset($inputattributes['checked']);
 367              }
 368  
 369              $class = 'r' . ($value % 2);
 370              if ($options->correctness && $isselected) {
 371                  $feedbackimg = $this->feedback_image($ans->fraction);
 372                  $class .= ' ' . $this->feedback_class($ans->fraction);
 373              } else {
 374                  $feedbackimg = '';
 375              }
 376  
 377              $result .= $this->choice_wrapper_start($class);
 378              $result .= html_writer::empty_tag('input', $inputattributes);
 379              $result .= html_writer::tag('label', $subq->format_text($ans->answer,
 380                      $ans->answerformat, $qa, 'question', 'answer', $ansid),
 381                      array('for' => $inputattributes['id']));
 382              $result .= $feedbackimg;
 383  
 384              if ($options->feedback && $isselected && trim($ans->feedback)) {
 385                  $result .= html_writer::tag('div',
 386                          $subq->format_text($ans->feedback, $ans->feedbackformat,
 387                                  $qa, 'question', 'answerfeedback', $ansid),
 388                          array('class' => 'specificfeedback'));
 389              }
 390  
 391              $result .= $this->choice_wrapper_end();
 392          }
 393  
 394          $result .= $this->all_choices_wrapper_end();
 395  
 396          $feedback = array();
 397          if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
 398                  $subq->maxmark > 0) {
 399              $a = new stdClass();
 400              $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
 401              $a->max = format_float($subq->maxmark, $options->markdp);
 402  
 403              $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
 404          }
 405  
 406          if ($options->rightanswer) {
 407              foreach ($subq->answers as $ans) {
 408                  if (question_state::graded_state_for_fraction($ans->fraction) ==
 409                          question_state::$gradedright) {
 410                      $feedback[] = get_string('correctansweris', 'qtype_multichoice',
 411                              $subq->format_text($ans->answer, $ans->answerformat,
 412                                      $qa, 'question', 'answer', $ansid));
 413                      break;
 414                  }
 415              }
 416          }
 417  
 418          $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
 419  
 420          return $result;
 421      }
 422  
 423      /**
 424       * @param string $class class attribute value.
 425       * @return string HTML to go before each choice.
 426       */
 427      protected function choice_wrapper_start($class) {
 428          return html_writer::start_tag('div', array('class' => $class));
 429      }
 430  
 431      /**
 432       * @return string HTML to go after each choice.
 433       */
 434      protected function choice_wrapper_end() {
 435          return html_writer::end_tag('div');
 436      }
 437  
 438      /**
 439       * @return string HTML to go before all the choices.
 440       */
 441      protected function all_choices_wrapper_start() {
 442          return html_writer::start_tag('div', array('class' => 'answer'));
 443      }
 444  
 445      /**
 446       * @return string HTML to go after all the choices.
 447       */
 448      protected function all_choices_wrapper_end() {
 449          return html_writer::end_tag('div');
 450      }
 451  }
 452  
 453  
 454  /**
 455   * Render an embedded multiple-choice question vertically, like for a normal
 456   * multiple-choice question.
 457   *
 458   * @copyright  2010 Pierre Pichet
 459   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 460   */
 461  class qtype_multianswer_multichoice_horizontal_renderer
 462          extends qtype_multianswer_multichoice_vertical_renderer {
 463  
 464      protected function choice_wrapper_start($class) {
 465          return html_writer::start_tag('td', array('class' => $class));
 466      }
 467  
 468      protected function choice_wrapper_end() {
 469          return html_writer::end_tag('td');
 470      }
 471  
 472      protected function all_choices_wrapper_start() {
 473          return html_writer::start_tag('table', array('class' => 'answer')) .
 474                  html_writer::start_tag('tbody') . html_writer::start_tag('tr');
 475      }
 476  
 477      protected function all_choices_wrapper_end() {
 478          return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
 479                  html_writer::end_tag('table');
 480      }
 481  }
 482  
 483  /**
 484   * Class qtype_multianswer_multiresponse_renderer
 485   *
 486   * @copyright  2016 Davo Smith, Synergy Learning
 487   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 488   */
 489  class qtype_multianswer_multiresponse_vertical_renderer extends qtype_multianswer_subq_renderer_base {
 490  
 491      /**
 492       * Output the content of the subquestion.
 493       *
 494       * @param question_attempt $qa
 495       * @param question_display_options $options
 496       * @param int $index
 497       * @param question_graded_automatically $subq
 498       * @return string
 499       */
 500      public function subquestion(question_attempt $qa, question_display_options $options,
 501                                  $index, question_graded_automatically $subq) {
 502  
 503          if (!$subq instanceof qtype_multichoice_multi_question) {
 504              throw new coding_exception('Expecting subquestion of type qtype_multichoice_multi_question');
 505          }
 506  
 507          $fieldprefix = 'sub' . $index . '_';
 508          $fieldname = $fieldprefix . 'choice';
 509  
 510          // Extract the responses that related to this question + strip off the prefix.
 511          $fieldprefixlen = strlen($fieldprefix);
 512          $response = [];
 513          foreach ($qa->get_last_qt_data() as $name => $val) {
 514              if (substr($name, 0, $fieldprefixlen) == $fieldprefix) {
 515                  $name = substr($name, $fieldprefixlen);
 516                  $response[$name] = $val;
 517              }
 518          }
 519  
 520          $basename = $qa->get_qt_field_name($fieldname);
 521          $inputattributes = array(
 522              'type' => 'checkbox',
 523              'value' => 1,
 524          );
 525          if ($options->readonly) {
 526              $inputattributes['disabled'] = 'disabled';
 527          }
 528  
 529          $result = $this->all_choices_wrapper_start();
 530  
 531          // Calculate the total score (as we need to know if choices should be marked as 'correct' or 'partial').
 532          $fraction = 0;
 533          foreach ($subq->get_order($qa) as $value => $ansid) {
 534              $ans = $subq->answers[$ansid];
 535              if ($subq->is_choice_selected($response, $value)) {
 536                  $fraction += $ans->fraction;
 537              }
 538          }
 539          // Display 'correct' answers as correct, if we are at 100%, otherwise mark them as 'partial'.
 540          $answerfraction = ($fraction > 0.999) ? 1.0 : 0.5;
 541  
 542          foreach ($subq->get_order($qa) as $value => $ansid) {
 543              $ans = $subq->answers[$ansid];
 544  
 545              $name = $basename.$value;
 546              $inputattributes['name'] = $name;
 547              $inputattributes['id'] = $name;
 548  
 549              $isselected = $subq->is_choice_selected($response, $value);
 550              if ($isselected) {
 551                  $inputattributes['checked'] = 'checked';
 552              } else {
 553                  unset($inputattributes['checked']);
 554              }
 555  
 556              $class = 'r' . ($value % 2);
 557              if ($options->correctness && $isselected) {
 558                  $thisfrac = ($ans->fraction > 0) ? $answerfraction : 0;
 559                  $feedbackimg = $this->feedback_image($thisfrac);
 560                  $class .= ' ' . $this->feedback_class($thisfrac);
 561              } else {
 562                  $feedbackimg = '';
 563              }
 564  
 565              $result .= $this->choice_wrapper_start($class);
 566              $result .= html_writer::empty_tag('input', $inputattributes);
 567              $result .= html_writer::tag('label', $subq->format_text($ans->answer,
 568                                                                      $ans->answerformat, $qa, 'question', 'answer', $ansid),
 569                                          array('for' => $inputattributes['id']));
 570              $result .= $feedbackimg;
 571  
 572              if ($options->feedback && $isselected && trim($ans->feedback)) {
 573                  $result .= html_writer::tag('div',
 574                                              $subq->format_text($ans->feedback, $ans->feedbackformat,
 575                                                                 $qa, 'question', 'answerfeedback', $ansid),
 576                                              array('class' => 'specificfeedback'));
 577              }
 578  
 579              $result .= $this->choice_wrapper_end();
 580          }
 581  
 582          $result .= $this->all_choices_wrapper_end();
 583  
 584          $feedback = array();
 585          if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
 586              $subq->maxmark > 0) {
 587              $a = new stdClass();
 588              $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
 589              $a->max = format_float($subq->maxmark, $options->markdp);
 590  
 591              $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
 592          }
 593  
 594          if ($options->rightanswer) {
 595              $correct = [];
 596              foreach ($subq->answers as $ans) {
 597                  if (question_state::graded_state_for_fraction($ans->fraction) != question_state::$gradedwrong) {
 598                      $correct[] = $subq->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ans->id);
 599                  }
 600              }
 601              $correct = '<ul><li>'.implode('</li><li>', $correct).'</li></ul>';
 602              $feedback[] = get_string('correctansweris', 'qtype_multichoice', $correct);
 603          }
 604  
 605          $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
 606  
 607          return $result;
 608      }
 609  
 610      /**
 611       * @param string $class class attribute value.
 612       * @return string HTML to go before each choice.
 613       */
 614      protected function choice_wrapper_start($class) {
 615          return html_writer::start_tag('div', array('class' => $class));
 616      }
 617  
 618      /**
 619       * @return string HTML to go after each choice.
 620       */
 621      protected function choice_wrapper_end() {
 622          return html_writer::end_tag('div');
 623      }
 624  
 625      /**
 626       * @return string HTML to go before all the choices.
 627       */
 628      protected function all_choices_wrapper_start() {
 629          return html_writer::start_tag('div', array('class' => 'answer'));
 630      }
 631  
 632      /**
 633       * @return string HTML to go after all the choices.
 634       */
 635      protected function all_choices_wrapper_end() {
 636          return html_writer::end_tag('div');
 637      }
 638  }
 639  
 640  /**
 641   * Render an embedded multiple-response question horizontally.
 642   *
 643   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 644   */
 645  class qtype_multianswer_multiresponse_horizontal_renderer
 646      extends qtype_multianswer_multiresponse_vertical_renderer {
 647  
 648      protected function choice_wrapper_start($class) {
 649          return html_writer::start_tag('td', array('class' => $class));
 650      }
 651  
 652      protected function choice_wrapper_end() {
 653          return html_writer::end_tag('td');
 654      }
 655  
 656      protected function all_choices_wrapper_start() {
 657          return html_writer::start_tag('table', array('class' => 'answer')) .
 658          html_writer::start_tag('tbody') . html_writer::start_tag('tr');
 659      }
 660  
 661      protected function all_choices_wrapper_end() {
 662          return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
 663          html_writer::end_tag('table');
 664      }
 665  }