[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/question/behaviour/ -> behaviourbase.php (source)

   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   * Defines the question behaviour base class
  19   *
  20   * @package    moodlecore
  21   * @subpackage questionbehaviours
  22   * @copyright  2009 The Open University
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  
  30  /**
  31   * The base class for question behaviours.
  32   *
  33   * A question behaviour is used by the question engine, specifically by
  34   * a {@link question_attempt} to manage the flow of actions a student can take
  35   * as they work through a question, and later, as a teacher manually grades it.
  36   * In turn, the behaviour will delegate certain processing to the
  37   * relevant {@link question_definition}.
  38   *
  39   * @copyright  2009 The Open University
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  abstract class question_behaviour {
  43  
  44      /** @var question_attempt the question attempt we are managing. */
  45      protected $qa;
  46  
  47      /** @var question_definition shortcut to $qa->get_question(). */
  48      protected $question;
  49  
  50      /**
  51       * Normally you should not call this constuctor directly. The appropriate
  52       * behaviour object is created automatically as part of
  53       * {@link question_attempt::start()}.
  54       * @param question_attempt $qa the question attempt we will be managing.
  55       * @param string $preferredbehaviour the type of behaviour that was actually
  56       *      requested. This information is not needed in most cases, the type of
  57       *      subclass is enough, but occasionally it is needed.
  58       */
  59      public function __construct(question_attempt $qa, $preferredbehaviour) {
  60          $this->qa = $qa;
  61          $this->question = $qa->get_question();
  62          if (!$this->is_compatible_question($this->question)) {
  63              throw new coding_exception('This behaviour (' . $this->get_name() .
  64                      ') cannot work with this question (' . get_class($this->question) . ')');
  65          }
  66      }
  67  
  68      /**
  69       * Some behaviours can only work with certing types of question. This method
  70       * allows the behaviour to verify that a question is compatible.
  71       *
  72       * This implementation is only provided for backwards-compatibility. You should
  73       * override this method if you are implementing a behaviour.
  74       *
  75       * @param question_definition $question the question.
  76       */
  77      public abstract function is_compatible_question(question_definition $question);
  78  
  79      /**
  80       * @return string the name of this behaviour. For example the name of
  81       * qbehaviour_mymodle is 'mymodel'.
  82       */
  83      public function get_name() {
  84          return substr(get_class($this), 11);
  85      }
  86  
  87      /**
  88       * Whether the current attempt at this question could be completed just by the
  89       * student interacting with the question, before $qa->finish() is called.
  90       *
  91       * @return boolean whether the attempt can finish naturally.
  92       */
  93      public function can_finish_during_attempt() {
  94          return false;
  95      }
  96  
  97      /**
  98       * Cause the question to be renderered. This gets the appropriate behaviour
  99       * renderer using {@link get_renderer()}, and adjusts the display
 100       * options using {@link adjust_display_options()} and then calls
 101       * {@link core_question_renderer::question()} to do the work.
 102       * @param question_display_options $options controls what should and should not be displayed.
 103       * @param unknown_type $number the question number to display.
 104       * @param core_question_renderer $qoutput the question renderer that will coordinate everything.
 105       * @param qtype_renderer $qtoutput the question type renderer that will be helping.
 106       * @return HTML fragment.
 107       */
 108      public function render(question_display_options $options, $number,
 109              core_question_renderer $qoutput, qtype_renderer $qtoutput) {
 110          $behaviouroutput = $this->get_renderer($qoutput->get_page());
 111          $options = clone($options);
 112          $this->adjust_display_options($options);
 113          return $qoutput->question($this->qa, $behaviouroutput, $qtoutput, $options, $number);
 114      }
 115  
 116      /**
 117       * Checks whether the users is allow to be served a particular file.
 118       * @param question_display_options $options the options that control display of the question.
 119       * @param string $component the name of the component we are serving files for.
 120       * @param string $filearea the name of the file area.
 121       * @param array $args the remaining bits of the file path.
 122       * @param bool $forcedownload whether the user must be forced to download the file.
 123       * @return bool true if the user can access this file.
 124       */
 125      public function check_file_access($options, $component, $filearea, $args, $forcedownload) {
 126          $this->adjust_display_options($options);
 127          return $this->question->check_file_access($this->qa, $options, $component,
 128                  $filearea, $args, $forcedownload);
 129      }
 130  
 131      /**
 132       * @param moodle_page $page the page to render for.
 133       * @return qbehaviour_renderer get the appropriate renderer to use for this model.
 134       */
 135      public function get_renderer(moodle_page $page) {
 136          return $page->get_renderer(get_class($this));
 137      }
 138  
 139      /**
 140       * Make any changes to the display options before a question is rendered, so
 141       * that it can be displayed in a way that is appropriate for the statue it is
 142       * currently in. For example, by default, if the question is finished, we
 143       * ensure that it is only ever displayed read-only.
 144       * @param question_display_options $options the options to adjust. Just change
 145       * the properties of this object - objects are passed by referece.
 146       */
 147      public function adjust_display_options(question_display_options $options) {
 148          if (!$this->qa->has_marks()) {
 149              $options->correctness = false;
 150              $options->numpartscorrect = false;
 151          }
 152          if ($this->qa->get_state()->is_finished()) {
 153              $options->readonly = true;
 154              $options->numpartscorrect = $options->numpartscorrect &&
 155                      $this->qa->get_state()->is_partially_correct() &&
 156                      !empty($this->question->shownumcorrect);
 157          } else {
 158              $options->hide_all_feedback();
 159          }
 160      }
 161  
 162      /**
 163       * Get the most applicable hint for the question in its current state.
 164       * @return question_hint the most applicable hint, or null, if none.
 165       */
 166      public function get_applicable_hint() {
 167          return null;
 168      }
 169  
 170      /**
 171       * What is the minimum fraction that can be scored for this question.
 172       * Normally this will be based on $this->question->get_min_fraction(),
 173       * but may be modified in some way by the behaviour.
 174       *
 175       * @return number the minimum fraction when this question is attempted under
 176       * this behaviour.
 177       */
 178      public function get_min_fraction() {
 179          return 0;
 180      }
 181  
 182      /**
 183       * Return the maximum possible fraction that can be scored for this question.
 184       * Normally this will be based on $this->question->get_max_fraction(),
 185       * but may be modified in some way by the behaviour.
 186       *
 187       * @return number the maximum fraction when this question is attempted under
 188       * this behaviour.
 189       */
 190      public function get_max_fraction() {
 191          return $this->question->get_max_fraction();
 192      }
 193  
 194      /**
 195       * Return an array of the behaviour variables that could be submitted
 196       * as part of a question of this type, with their types, so they can be
 197       * properly cleaned.
 198       * @return array variable name => PARAM_... constant.
 199       */
 200      public function get_expected_data() {
 201          if (!$this->qa->get_state()->is_finished()) {
 202              return array();
 203          }
 204  
 205          $vars = array('comment' => PARAM_RAW, 'commentformat' => PARAM_INT);
 206          if ($this->qa->get_max_mark()) {
 207              $vars['mark'] = PARAM_RAW_TRIMMED;
 208              $vars['maxmark'] = PARAM_FLOAT;
 209          }
 210          return $vars;
 211      }
 212  
 213      /**
 214       * Return an array of question type variables for the question in its current
 215       * state. Normally, if {@link adjust_display_options()} would set
 216       * {@link question_display_options::$readonly} to true, then this method
 217       * should return an empty array, otherwise it should return
 218       * $this->question->get_expected_data(). Thus, there should be little need to
 219       * override this method.
 220       * @return array|string variable name => PARAM_... constant, or, as a special case
 221       *      that should only be used in unavoidable, the constant question_attempt::USE_RAW_DATA
 222       *      meaning take all the raw submitted data belonging to this question.
 223       */
 224      public function get_expected_qt_data() {
 225          $fakeoptions = new question_display_options();
 226          $fakeoptions->readonly = false;
 227          $this->adjust_display_options($fakeoptions);
 228          if ($fakeoptions->readonly) {
 229              return array();
 230          } else {
 231              return $this->question->get_expected_data();
 232          }
 233      }
 234  
 235      /**
 236       * Return an array of any im variables, and the value required to get full
 237       * marks.
 238       * @return array variable name => value.
 239       */
 240      public function get_correct_response() {
 241          return array();
 242      }
 243  
 244      /**
 245       * Generate a brief, plain-text, summary of this question. This is used by
 246       * various reports. This should show the particular variant of the question
 247       * as presented to students. For example, the calculated quetsion type would
 248       * fill in the particular numbers that were presented to the student.
 249       * This method will return null if such a summary is not possible, or
 250       * inappropriate.
 251       *
 252       * Normally, this method delegates to {question_definition::get_question_summary()}.
 253       *
 254       * @return string|null a plain text summary of this question.
 255       */
 256      public function get_question_summary() {
 257          return $this->question->get_question_summary();
 258      }
 259  
 260      /**
 261       * Generate a brief, plain-text, summary of the correct answer to this question.
 262       * This is used by various reports, and can also be useful when testing.
 263       * This method will return null if such a summary is not possible, or
 264       * inappropriate.
 265       *
 266       * @return string|null a plain text summary of the right answer to this question.
 267       */
 268      public function get_right_answer_summary() {
 269          return null;
 270      }
 271  
 272      /**
 273       * Used by {@link start_based_on()} to get the data needed to start a new
 274       * attempt from the point this attempt has go to.
 275       * @return array name => value pairs.
 276       */
 277      public function get_resume_data() {
 278          $olddata = $this->qa->get_step(0)->get_all_data();
 279          $olddata = $this->qa->get_last_qt_data() + $olddata;
 280          $olddata = $this->get_our_resume_data() + $olddata;
 281          return $olddata;
 282      }
 283  
 284      /**
 285       * Used by {@link start_based_on()} to get the data needed to start a new
 286       * attempt from the point this attempt has go to.
 287       * @return unknown_type
 288       */
 289      protected function get_our_resume_data() {
 290          return array();
 291      }
 292  
 293      /**
 294       * Classify responses for this question into a number of sub parts and response classes as defined by
 295       * {@link \question_type::get_possible_responses} for this question type.
 296       *
 297       * @param string $whichtries         which tries to analyse for response analysis. Will be one of
 298       *                                   question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
 299       *                                   Defaults to question_attempt::LAST_TRY.
 300       * @return (question_classified_response|array)[] If $whichtries is question_attempt::FIRST_TRY or LAST_TRY index is subpartid
 301       *                                   and values are question_classified_response instances.
 302       *                                   If $whichtries is question_attempt::ALL_TRIES then first key is submitted response no
 303       *                                   and the second key is subpartid.
 304       */
 305      public function classify_response($whichtries = question_attempt::LAST_TRY) {
 306          if ($whichtries == question_attempt::LAST_TRY) {
 307              return $this->question->classify_response($this->qa->get_last_qt_data());
 308          } else {
 309              $stepswithsubmit = $this->qa->get_steps_with_submitted_response_iterator();
 310              if ($whichtries == question_attempt::FIRST_TRY) {
 311                  $firsttry = $stepswithsubmit[1];
 312                  if ($firsttry) {
 313                      return $this->question->classify_response($firsttry->get_qt_data());
 314                  } else {
 315                      return $this->question->classify_response(array());
 316                  }
 317              } else {
 318                  $classifiedresponses = array();
 319                  foreach ($stepswithsubmit as $submittedresponseno => $step) {
 320                      $classifiedresponses[$submittedresponseno] = $this->question->classify_response($step->get_qt_data());
 321                  }
 322                  return $classifiedresponses;
 323              }
 324          }
 325      }
 326  
 327      /**
 328       * Generate a brief textual description of the current state of the question,
 329       * normally displayed under the question number.
 330       *
 331       * @param bool $showcorrectness Whether right/partial/wrong states should
 332       * be distinguised.
 333       * @return string a brief summary of the current state of the qestion attempt.
 334       */
 335      public function get_state_string($showcorrectness) {
 336          return $this->qa->get_state()->default_string($showcorrectness);
 337      }
 338  
 339      public abstract function summarise_action(question_attempt_step $step);
 340  
 341      /**
 342       * Initialise the first step in a question attempt when a new
 343       * {@link question_attempt} is being started.
 344       *
 345       * This method must call $this->question->start_attempt($step, $variant), and may
 346       * perform additional processing if the behaviour requries it.
 347       *
 348       * @param question_attempt_step $step the first step of the
 349       *      question_attempt being started.
 350       * @param int $variant which variant of the question to use.
 351       */
 352      public function init_first_step(question_attempt_step $step, $variant) {
 353          $this->question->start_attempt($step, $variant);
 354          $step->set_state(question_state::$todo);
 355      }
 356  
 357      /**
 358       * When an attempt is started based on a previous attempt (see
 359       * {@link question_attempt::start_based_on}) this method is called to setup
 360       * the new attempt.
 361       *
 362       * This method must call $this->question->apply_attempt_state($step), and may
 363       * perform additional processing if the behaviour requries it.
 364       *
 365       * @param question_attempt_step The first step of the {@link question_attempt}
 366       *      being loaded.
 367       */
 368      public function apply_attempt_state(question_attempt_step $step) {
 369          $this->question->apply_attempt_state($step);
 370          $step->set_state(question_state::$todo);
 371      }
 372  
 373      /**
 374       * Checks whether two manual grading actions are the same. That is, whether
 375       * the comment, and the mark (if given) is the same.
 376       *
 377       * @param question_attempt_step $pendingstep contains the new responses.
 378       * @return bool whether the new response is the same as we already have.
 379       */
 380      protected function is_same_comment($pendingstep) {
 381          $previouscomment = $this->qa->get_last_behaviour_var('comment');
 382          $newcomment = $pendingstep->get_behaviour_var('comment');
 383  
 384          if (is_null($previouscomment) && !html_is_blank($newcomment) ||
 385                  $previouscomment != $newcomment) {
 386              return false;
 387          }
 388  
 389          // So, now we know the comment is the same, so check the mark, if present.
 390          $previousfraction = $this->qa->get_fraction();
 391          $newmark = question_utils::clean_param_mark($pendingstep->get_behaviour_var('mark'));
 392  
 393          if (is_null($previousfraction)) {
 394              return is_null($newmark) || $newmark === '';
 395          } else if (is_null($newmark) || $newmark === '') {
 396              return false;
 397          }
 398  
 399          $newfraction = $newmark / $pendingstep->get_behaviour_var('maxmark');
 400  
 401          return abs($newfraction - $previousfraction) < 0.0000001;
 402      }
 403  
 404      /**
 405       * The main entry point for processing an action.
 406       *
 407       * All the various operations that can be performed on a
 408       * {@link question_attempt} get channeled through this function, except for
 409       * {@link question_attempt::start()} which goes to {@link init_first_step()}.
 410       * {@link question_attempt::finish()} becomes an action with im vars
 411       * finish => 1, and manual comment/grade becomes an action with im vars
 412       * comment => comment text, and mark => ..., max_mark => ... if the question
 413       * is graded.
 414       *
 415       * This method should first determine whether the action is significant. For
 416       * example, if no actual action is being performed, but instead the current
 417       * responses are being saved, and there has been no change since the last
 418       * set of responses that were saved, this the action is not significatn. In
 419       * this case, this method should return {@link question_attempt::DISCARD}.
 420       * Otherwise it should return {@link question_attempt::KEEP}.
 421       *
 422       * If the action is significant, this method should also perform any
 423       * necessary updates to $pendingstep. For example, it should call
 424       * {@link question_attempt_step::set_state()} to set the state that results
 425       * from this action, and if this is a grading action, it should call
 426       * {@link question_attempt_step::set_fraction()}.
 427       *
 428       * This method can also call {@link question_attempt_step::set_behaviour_var()} to
 429       * store additional infomation. There are two main uses for this. This can
 430       * be used to store the result of any randomisation done. It is important to
 431       * store the result of randomisation once, and then in future use the same
 432       * outcome if the actions are ever replayed. This is how regrading works.
 433       * The other use is to cache the result of expensive computations performed
 434       * on the raw response data, so that subsequent display and review of the
 435       * question does not have to repeat the same expensive computations.
 436       *
 437       * Often this method is implemented as a dispatching method that examines
 438       * the pending step to determine the kind of action being performed, and
 439       * then calls a more specific method like {@link process_save()} or
 440       * {@link process_comment()}. Look at some of the standard behaviours
 441       * for examples.
 442       *
 443       * @param question_attempt_pending_step $pendingstep a partially initialised step
 444       *      containing all the information about the action that is being peformed. This
 445       *      information can be accessed using {@link question_attempt_step::get_behaviour_var()}.
 446       * @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
 447       */
 448      public abstract function process_action(question_attempt_pending_step $pendingstep);
 449  
 450      /**
 451       * Auto-saved data. By default this does nothing. interesting processing is
 452       * done in {@link question_behaviour_with_save}.
 453       *
 454       * @param question_attempt_pending_step $pendingstep a partially initialised step
 455       *      containing all the information about the action that is being peformed. This
 456       *      information can be accessed using {@link question_attempt_step::get_behaviour_var()}.
 457       * @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
 458       */
 459      public function process_autosave(question_attempt_pending_step $pendingstep) {
 460          return question_attempt::DISCARD;
 461      }
 462  
 463      /**
 464       * Implementation of processing a manual comment/grade action that should
 465       * be suitable for most subclasses.
 466       * @param question_attempt_pending_step $pendingstep a partially initialised step
 467       *      containing all the information about the action that is being peformed.
 468       * @return bool either {@link question_attempt::KEEP}
 469       */
 470      public function process_comment(question_attempt_pending_step $pendingstep) {
 471          if (!$this->qa->get_state()->is_finished()) {
 472              throw new coding_exception('Cannot manually grade a question before it is finshed.');
 473          }
 474  
 475          if ($this->is_same_comment($pendingstep)) {
 476              return question_attempt::DISCARD;
 477          }
 478  
 479          if ($pendingstep->has_behaviour_var('mark')) {
 480              $mark = question_utils::clean_param_mark($pendingstep->get_behaviour_var('mark'));
 481              if ($mark === null) {
 482                  throw new coding_exception('Inalid number format ' . $pendingstep->get_behaviour_var('mark') .
 483                          ' when processing a manual grading action.', 'Question ' . $this->question->id .
 484                          ', slot ' . $this->qa->get_slot());
 485  
 486              } else if ($mark === '') {
 487                  $fraction = null;
 488  
 489              } else {
 490                  $fraction = $mark / $pendingstep->get_behaviour_var('maxmark');
 491                  if ($fraction > $this->qa->get_max_fraction() || $fraction < $this->qa->get_min_fraction()) {
 492                      throw new coding_exception('Score out of range when processing ' .
 493                              'a manual grading action.', 'Question ' . $this->question->id .
 494                              ', slot ' . $this->qa->get_slot() . ', fraction ' . $fraction);
 495                  }
 496              }
 497  
 498              $pendingstep->set_fraction($fraction);
 499          }
 500  
 501          $pendingstep->set_state($this->qa->get_state()->corresponding_commented_state(
 502                  $pendingstep->get_fraction()));
 503          return question_attempt::KEEP;
 504      }
 505  
 506      /**
 507       * @param $comment the comment text to format. If omitted,
 508       *      $this->qa->get_manual_comment() is used.
 509       * @param $commentformat the format of the comment, one of the FORMAT_... constants.
 510       * @return string the comment, ready to be output.
 511       */
 512      public function format_comment($comment = null, $commentformat = null) {
 513          $formatoptions = new stdClass();
 514          $formatoptions->noclean = true;
 515          $formatoptions->para = false;
 516  
 517          if (is_null($comment)) {
 518              list($comment, $commentformat) = $this->qa->get_manual_comment();
 519          }
 520  
 521          return format_text($comment, $commentformat, $formatoptions);
 522      }
 523  
 524      /**
 525       * @return string a summary of a manual comment action.
 526       * @param unknown_type $step
 527       */
 528      protected function summarise_manual_comment($step) {
 529          $a = new stdClass();
 530          if ($step->has_behaviour_var('comment')) {
 531              $a->comment = shorten_text(html_to_text($this->format_comment(
 532                      $step->get_behaviour_var('comment')), 0, false), 200);
 533          } else {
 534              $a->comment = '';
 535          }
 536  
 537          $mark = question_utils::clean_param_mark($step->get_behaviour_var('mark'));
 538          if (is_null($mark) || $mark === '') {
 539              return get_string('commented', 'question', $a->comment);
 540          } else {
 541              $a->mark = $mark / $step->get_behaviour_var('maxmark') * $this->qa->get_max_mark();
 542              return get_string('manuallygraded', 'question', $a);
 543          }
 544      }
 545  
 546      public function summarise_start($step) {
 547          return get_string('started', 'question');
 548      }
 549  
 550      public function summarise_finish($step) {
 551          return get_string('attemptfinished', 'question');
 552      }
 553  
 554      /**
 555       * Does this step include a response submitted by a student?
 556       *
 557       * This method should return true for any attempt explicitly submitted by a student. The question engine itself will also
 558       * automatically recognise any last saved response before the attempt is finished, you don't need to return true here for these
 559       * steps with responses which are not explicitly submitted by the student.
 560       *
 561       * @param question_attempt_step $step
 562       * @return bool is this a step within a question attempt that includes a submitted response by a student.
 563       */
 564      public function step_has_a_submitted_response($step) {
 565          return false;
 566      }
 567  }
 568  
 569  
 570  /**
 571   * A subclass of {@link question_behaviour} that implements a save
 572   * action that is suitable for most questions that implement the
 573   * {@link question_manually_gradable} interface.
 574   *
 575   * @copyright  2009 The Open University
 576   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 577   */
 578  abstract class question_behaviour_with_save extends question_behaviour {
 579      public function required_question_definition_type() {
 580          return 'question_manually_gradable';
 581      }
 582  
 583      public function apply_attempt_state(question_attempt_step $step) {
 584          parent::apply_attempt_state($step);
 585          if ($this->question->is_complete_response($step->get_qt_data())) {
 586              $step->set_state(question_state::$complete);
 587          }
 588      }
 589  
 590      /**
 591       * Work out whether the response in $pendingstep are significantly different
 592       * from the last set of responses we have stored.
 593       * @param question_attempt_step $pendingstep contains the new responses.
 594       * @return bool whether the new response is the same as we already have.
 595       */
 596      protected function is_same_response(question_attempt_step $pendingstep) {
 597          return $this->question->is_same_response(
 598                  $this->qa->get_last_step()->get_qt_data(), $pendingstep->get_qt_data());
 599      }
 600  
 601      /**
 602       * Work out whether the response in $pendingstep represent a complete answer
 603       * to the question. Normally this will call
 604       * {@link question_manually_gradable::is_complete_response}, but some
 605       * behaviours, for example the CBM ones, have their own parts to the
 606       * response.
 607       * @param question_attempt_step $pendingstep contains the new responses.
 608       * @return bool whether the new response is complete.
 609       */
 610      protected function is_complete_response(question_attempt_step $pendingstep) {
 611          return $this->question->is_complete_response($pendingstep->get_qt_data());
 612      }
 613  
 614      public function process_autosave(question_attempt_pending_step $pendingstep) {
 615          // If already finished. Nothing to do.
 616          if ($this->qa->get_state()->is_finished()) {
 617              return question_attempt::DISCARD;
 618          }
 619  
 620          // If the new data is the same as we already have, then we don't need it.
 621          if ($this->is_same_response($pendingstep)) {
 622              return question_attempt::DISCARD;
 623          }
 624  
 625          // Repeat that test discarding any existing autosaved data.
 626          if ($this->qa->has_autosaved_step()) {
 627              $this->qa->discard_autosaved_step();
 628              if ($this->is_same_response($pendingstep)) {
 629                  return question_attempt::DISCARD;
 630              }
 631          }
 632  
 633          // OK, we need to save.
 634          return $this->process_save($pendingstep);
 635      }
 636  
 637      /**
 638       * Implementation of processing a save action that should be suitable for
 639       * most subclasses.
 640       * @param question_attempt_pending_step $pendingstep a partially initialised step
 641       *      containing all the information about the action that is being peformed.
 642       * @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
 643       */
 644      public function process_save(question_attempt_pending_step $pendingstep) {
 645          if ($this->qa->get_state()->is_finished()) {
 646              return question_attempt::DISCARD;
 647          } else if (!$this->qa->get_state()->is_active()) {
 648              throw new coding_exception('Question is not active, cannot process_actions.');
 649          }
 650  
 651          if ($this->is_same_response($pendingstep)) {
 652              return question_attempt::DISCARD;
 653          }
 654  
 655          if ($this->is_complete_response($pendingstep)) {
 656              $pendingstep->set_state(question_state::$complete);
 657          } else {
 658              $pendingstep->set_state(question_state::$todo);
 659          }
 660          return question_attempt::KEEP;
 661      }
 662  
 663      public function summarise_submit(question_attempt_step $step) {
 664          return get_string('submitted', 'question',
 665                  $this->question->summarise_response($step->get_qt_data()));
 666      }
 667  
 668      public function summarise_save(question_attempt_step $step) {
 669          $data = $step->get_submitted_data();
 670          if (empty($data)) {
 671              return $this->summarise_start($step);
 672          }
 673          return get_string('saved', 'question',
 674                  $this->question->summarise_response($step->get_qt_data()));
 675      }
 676  
 677  
 678      public function summarise_finish($step) {
 679          $data = $step->get_qt_data();
 680          if ($data) {
 681              return get_string('attemptfinishedsubmitting', 'question',
 682                      $this->question->summarise_response($data));
 683          }
 684          return get_string('attemptfinished', 'question');
 685      }
 686  }
 687  
 688  abstract class question_behaviour_with_multiple_tries extends question_behaviour_with_save {
 689      public function step_has_a_submitted_response($step) {
 690          return $step->has_behaviour_var('submit') && $step->get_state() != question_state::$invalid;
 691      }
 692  }
 693  
 694  /**
 695   * This helper class contains the constants and methods required for
 696   * manipulating scores for certainty based marking.
 697   *
 698   * @copyright  2009 The Open University
 699   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 700   */
 701  abstract class question_cbm {
 702      /**#@+ @var integer named constants for the certainty levels. */
 703      const LOW = 1;
 704      const MED = 2;
 705      const HIGH = 3;
 706      /**#@-*/
 707  
 708      /** @var array list of all the certainty levels. */
 709      public static $certainties = array(self::LOW, self::MED, self::HIGH);
 710  
 711      /**#@+ @var array coefficients used to adjust the fraction based on certainty. */
 712      protected static $rightscore = array(
 713          self::LOW  => 1,
 714          self::MED  => 2,
 715          self::HIGH => 3,
 716      );
 717      protected static $wrongscore = array(
 718          self::LOW  =>  0,
 719          self::MED  => -2,
 720          self::HIGH => -6,
 721      );
 722      /**#@-*/
 723  
 724      /**#@+ @var array upper and lower limits of the optimal window. */
 725      protected static $lowlimit = array(
 726          self::LOW  => 0,
 727          self::MED  => 0.666666666666667,
 728          self::HIGH => 0.8,
 729      );
 730      protected static $highlimit = array(
 731          self::LOW  => 0.666666666666667,
 732          self::MED  => 0.8,
 733          self::HIGH => 1,
 734      );
 735      /**#@-*/
 736  
 737      /**
 738       * @return int the default certaintly level that should be assuemd if
 739       * the student does not choose one.
 740       */
 741      public static function default_certainty() {
 742          return self::LOW;
 743      }
 744  
 745      /**
 746       * Given a fraction, and a certainty, compute the adjusted fraction.
 747       * @param number $fraction the raw fraction for this question.
 748       * @param int $certainty one of the certainty level constants.
 749       * @return number the adjusted fraction taking the certainty into account.
 750       */
 751      public static function adjust_fraction($fraction, $certainty) {
 752          if ($certainty == -1) {
 753              // Certainty -1 has never been used in standard Moodle, but is
 754              // used in Tony-Gardiner Medwin's patches to mean 'No idea' which
 755              // we intend to implement: MDL-42077. In the mean time, avoid
 756              // errors for people who have used TGM's patches.
 757              return 0;
 758          }
 759          if ($fraction <= 0.00000005) {
 760              return self::$wrongscore[$certainty];
 761          } else {
 762              return self::$rightscore[$certainty] * $fraction;
 763          }
 764      }
 765  
 766      /**
 767       * @param int $certainty one of the LOW/MED/HIGH constants.
 768       * @return string a textual description of this certainty.
 769       */
 770      public static function get_string($certainty) {
 771          return get_string('certainty' . $certainty, 'qbehaviour_deferredcbm');
 772      }
 773  
 774      /**
 775       * @param int $certainty one of the LOW/MED/HIGH constants.
 776       * @return string a short textual description of this certainty.
 777       */
 778      public static function get_short_string($certainty) {
 779          return get_string('certaintyshort' . $certainty, 'qbehaviour_deferredcbm');
 780      }
 781  
 782      /**
 783       * Add information about certainty to a response summary.
 784       * @param string $summary the response summary.
 785       * @param int $certainty the level of certainty to add.
 786       * @return string the summary with information about the certainty added.
 787       */
 788      public static function summary_with_certainty($summary, $certainty) {
 789          if (is_null($certainty)) {
 790              return $summary;
 791          }
 792          return $summary . ' [' . self::get_short_string($certainty) . ']';
 793      }
 794  
 795      /**
 796       * @param int $certainty one of the LOW/MED/HIGH constants.
 797       * @return float the lower limit of the optimal probability range for this certainty.
 798       */
 799      public static function optimal_probablility_low($certainty) {
 800          return self::$lowlimit[$certainty];
 801      }
 802  
 803      /**
 804       * @param int $certainty one of the LOW/MED/HIGH constants.
 805       * @return float the upper limit of the optimal probability range for this certainty.
 806       */
 807      public static function optimal_probablility_high($certainty) {
 808          return self::$highlimit[$certainty];
 809      }
 810  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1