Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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   * Question behaviour where the student can submit questions one at a
  19   * time for immediate feedback.
  20   *
  21   * @package    qbehaviour
  22   * @subpackage interactive
  23   * @copyright  2009 The Open University
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  
  31  /**
  32   * Question behaviour for the interactive model.
  33   *
  34   * Each question has a submit button next to it which the student can use to
  35   * submit it. Once the question is submitted, it is not possible for the
  36   * student to change their answer any more, but the student gets full feedback
  37   * straight away.
  38   *
  39   * @copyright  2009 The Open University
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class qbehaviour_interactive extends question_behaviour_with_multiple_tries {
  43      /**
  44       * Constant used only in {@link adjust_display_options()} below and
  45       * {@link (qbehaviour_interactive_renderer}.
  46       * @var int
  47       */
  48      const TRY_AGAIN_VISIBLE = 0x10;
  49      /**
  50       * Constant used only in {@link adjust_display_options()} below and
  51       * {@link (qbehaviour_interactive_renderer}.
  52       * @var int
  53       */
  54      const TRY_AGAIN_VISIBLE_READONLY = 0x11;
  55  
  56      public function is_compatible_question(question_definition $question) {
  57          return $question instanceof question_automatically_gradable;
  58      }
  59  
  60      public function can_finish_during_attempt() {
  61          return true;
  62      }
  63  
  64      public function get_right_answer_summary() {
  65          return $this->question->get_right_answer_summary();
  66      }
  67  
  68      /**
  69       * @return bool are we are currently in the try_again state.
  70       */
  71      public function is_try_again_state() {
  72          $laststep = $this->qa->get_last_step();
  73          return $this->qa->get_state()->is_active() && $laststep->has_behaviour_var('submit') &&
  74                  $laststep->has_behaviour_var('_triesleft');
  75      }
  76  
  77      public function adjust_display_options(question_display_options $options) {
  78          // We only need different behaviour in try again states.
  79          if (!$this->is_try_again_state()) {
  80              parent::adjust_display_options($options);
  81              if ($this->qa->get_state() == question_state::$invalid &&
  82                      $options->marks == question_display_options::MARK_AND_MAX) {
  83                  $options->marks = question_display_options::MAX_ONLY;
  84              }
  85              return;
  86          }
  87  
  88          // The question in in a try-again state. We need the to let the renderer know this.
  89          // The API for question-rendering is defined by the question engine, but we
  90          // don't want to add logic in the renderer, so we are limited in how we can do this.
  91          // However, when the question is in this state, all the question-type controls
  92          // need to be rendered read-only. Therefore, we can conveniently pass this information
  93          // by setting special true-like values in $options->readonly (but this is a bit of a hack).
  94          $options->readonly = $options->readonly ? self::TRY_AGAIN_VISIBLE_READONLY : self::TRY_AGAIN_VISIBLE;
  95  
  96          // Let the hint adjust the options.
  97          $hint = $this->get_applicable_hint();
  98          if (!is_null($hint)) {
  99              $hint->adjust_display_options($options);
 100          }
 101  
 102          // Now call the base class method, but protect some fields from being overwritten.
 103          $save = clone($options);
 104          parent::adjust_display_options($options);
 105          $options->feedback = $save->feedback;
 106          $options->numpartscorrect = $save->numpartscorrect;
 107      }
 108  
 109      public function get_applicable_hint() {
 110          if (!$this->is_try_again_state()) {
 111              return null;
 112          }
 113          return $this->question->get_hint(count($this->question->hints) -
 114                  $this->qa->get_last_behaviour_var('_triesleft'), $this->qa);
 115      }
 116  
 117      public function get_expected_data() {
 118          if ($this->is_try_again_state()) {
 119              return array(
 120                  'tryagain' => PARAM_BOOL,
 121              );
 122          } else if ($this->qa->get_state()->is_active()) {
 123              return array(
 124                  'submit' => PARAM_BOOL,
 125              );
 126          }
 127          return parent::get_expected_data();
 128      }
 129  
 130      public function get_expected_qt_data() {
 131          $hint = $this->get_applicable_hint();
 132          if (!empty($hint->clearwrong)) {
 133              return $this->question->get_expected_data();
 134          }
 135          return parent::get_expected_qt_data();
 136      }
 137  
 138      public function get_state_string($showcorrectness) {
 139          $state = $this->qa->get_state();
 140          if (!$state->is_active() || $state == question_state::$invalid) {
 141              return parent::get_state_string($showcorrectness);
 142          }
 143  
 144          return get_string('triesremaining', 'qbehaviour_interactive',
 145                  $this->qa->get_last_behaviour_var('_triesleft'));
 146      }
 147  
 148      public function init_first_step(question_attempt_step $step, $variant) {
 149          parent::init_first_step($step, $variant);
 150          $step->set_behaviour_var('_triesleft', count($this->question->hints) + 1);
 151      }
 152  
 153      public function process_action(question_attempt_pending_step $pendingstep) {
 154          if ($pendingstep->has_behaviour_var('finish')) {
 155              return $this->process_finish($pendingstep);
 156          }
 157          if ($this->is_try_again_state()) {
 158              if ($pendingstep->has_behaviour_var('tryagain')) {
 159                  return $this->process_try_again($pendingstep);
 160              } else {
 161                  return question_attempt::DISCARD;
 162              }
 163          } else {
 164              if ($pendingstep->has_behaviour_var('comment')) {
 165                  return $this->process_comment($pendingstep);
 166              } else if ($pendingstep->has_behaviour_var('submit')) {
 167                  return $this->process_submit($pendingstep);
 168              } else {
 169                  return $this->process_save($pendingstep);
 170              }
 171          }
 172      }
 173  
 174      public function summarise_action(question_attempt_step $step) {
 175          if ($step->has_behaviour_var('comment')) {
 176              return $this->summarise_manual_comment($step);
 177          } else if ($step->has_behaviour_var('finish')) {
 178              return $this->summarise_finish($step);
 179          } else if ($step->has_behaviour_var('tryagain')) {
 180              return get_string('tryagain', 'qbehaviour_interactive');
 181          } else if ($step->has_behaviour_var('submit')) {
 182              return $this->summarise_submit($step);
 183          } else {
 184              return $this->summarise_save($step);
 185          }
 186      }
 187  
 188      public function process_try_again(question_attempt_pending_step $pendingstep) {
 189          $pendingstep->set_state(question_state::$todo);
 190          return question_attempt::KEEP;
 191      }
 192  
 193      public function process_submit(question_attempt_pending_step $pendingstep) {
 194          if ($this->qa->get_state()->is_finished()) {
 195              return question_attempt::DISCARD;
 196          }
 197  
 198          if (!$this->is_complete_response($pendingstep)) {
 199              $pendingstep->set_state(question_state::$invalid);
 200  
 201          } else {
 202              $triesleft = $this->qa->get_last_behaviour_var('_triesleft');
 203              $response = $pendingstep->get_qt_data();
 204              list($fraction, $state) = $this->question->grade_response($response);
 205              if ($state == question_state::$gradedright || $triesleft == 1) {
 206                  $pendingstep->set_state($state);
 207                  $pendingstep->set_fraction($this->adjust_fraction($fraction, $pendingstep));
 208  
 209              } else {
 210                  $pendingstep->set_behaviour_var('_triesleft', $triesleft - 1);
 211                  $pendingstep->set_state(question_state::$todo);
 212              }
 213              $pendingstep->set_new_response_summary($this->question->summarise_response($response));
 214          }
 215          return question_attempt::KEEP;
 216      }
 217  
 218      protected function adjust_fraction($fraction, question_attempt_pending_step $pendingstep) {
 219          $totaltries = $this->qa->get_step(0)->get_behaviour_var('_triesleft');
 220          $triesleft = $this->qa->get_last_behaviour_var('_triesleft');
 221  
 222          $fraction -= ($totaltries - $triesleft) * $this->question->penalty;
 223          $fraction = max($fraction, 0);
 224          return $fraction;
 225      }
 226  
 227      public function process_finish(question_attempt_pending_step $pendingstep) {
 228          if ($this->qa->get_state()->is_finished()) {
 229              return question_attempt::DISCARD;
 230          }
 231  
 232          $response = $this->qa->get_last_qt_data();
 233          if (!$this->question->is_gradable_response($response)) {
 234              $pendingstep->set_state(question_state::$gaveup);
 235  
 236          } else {
 237              list($fraction, $state) = $this->question->grade_response($response);
 238              $pendingstep->set_fraction($this->adjust_fraction($fraction, $pendingstep));
 239              $pendingstep->set_state($state);
 240          }
 241          $pendingstep->set_new_response_summary($this->question->summarise_response($response));
 242          return question_attempt::KEEP;
 243      }
 244  
 245      public function process_save(question_attempt_pending_step $pendingstep) {
 246          $status = parent::process_save($pendingstep);
 247          if ($status == question_attempt::KEEP &&
 248                  $pendingstep->get_state() == question_state::$complete) {
 249              $pendingstep->set_state(question_state::$todo);
 250          }
 251          return $status;
 252      }
 253  }