Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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

   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   * This defines the core classes of the Moodle question engine.
  19   *
  20   * @package    moodlecore
  21   * @subpackage questionengine
  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  require_once($CFG->libdir . '/filelib.php');
  30  require_once (__DIR__ . '/questionusage.php');
  31  require_once (__DIR__ . '/questionattempt.php');
  32  require_once (__DIR__ . '/questionattemptstep.php');
  33  require_once (__DIR__ . '/states.php');
  34  require_once (__DIR__ . '/datalib.php');
  35  require_once (__DIR__ . '/renderer.php');
  36  require_once (__DIR__ . '/bank.php');
  37  require_once (__DIR__ . '/../type/questiontypebase.php');
  38  require_once (__DIR__ . '/../type/questionbase.php');
  39  require_once (__DIR__ . '/../type/rendererbase.php');
  40  require_once (__DIR__ . '/../behaviour/behaviourtypebase.php');
  41  require_once (__DIR__ . '/../behaviour/behaviourbase.php');
  42  require_once (__DIR__ . '/../behaviour/rendererbase.php');
  43  require_once($CFG->libdir . '/questionlib.php');
  44  
  45  
  46  /**
  47   * This static class provides access to the other question engine classes.
  48   *
  49   * It provides functions for managing question behaviours), and for
  50   * creating, loading, saving and deleting {@link question_usage_by_activity}s,
  51   * which is the main class that is used by other code that wants to use questions.
  52   *
  53   * @copyright  2009 The Open University
  54   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  55   */
  56  abstract class question_engine {
  57      /** @var array behaviour name => 1. Records which behaviours have been loaded. */
  58      private static $loadedbehaviours = array();
  59  
  60      /** @var array behaviour name => question_behaviour_type for this behaviour. */
  61      private static $behaviourtypes = array();
  62  
  63      /**
  64       * Create a new {@link question_usage_by_activity}. The usage is
  65       * created in memory. If you want it to persist, you will need to call
  66       * {@link save_questions_usage_by_activity()}.
  67       *
  68       * @param string $component the plugin creating this attempt. For example mod_quiz.
  69       * @param context $context the context this usage belongs to.
  70       * @return question_usage_by_activity the newly created object.
  71       */
  72      public static function make_questions_usage_by_activity($component, $context) {
  73          return new question_usage_by_activity($component, $context);
  74      }
  75  
  76      /**
  77       * Load a {@link question_usage_by_activity} from the database, based on its id.
  78       * @param int $qubaid the id of the usage to load.
  79       * @param moodle_database $db a database connectoin. Defaults to global $DB.
  80       * @return question_usage_by_activity loaded from the database.
  81       */
  82      public static function load_questions_usage_by_activity($qubaid, moodle_database $db = null) {
  83          $dm = new question_engine_data_mapper($db);
  84          return $dm->load_questions_usage_by_activity($qubaid);
  85      }
  86  
  87      /**
  88       * Save a {@link question_usage_by_activity} to the database. This works either
  89       * if the usage was newly created by {@link make_questions_usage_by_activity()}
  90       * or loaded from the database using {@link load_questions_usage_by_activity()}
  91       * @param question_usage_by_activity the usage to save.
  92       * @param moodle_database $db a database connectoin. Defaults to global $DB.
  93       */
  94      public static function save_questions_usage_by_activity(question_usage_by_activity $quba, moodle_database $db = null) {
  95          $dm = new question_engine_data_mapper($db);
  96          $observer = $quba->get_observer();
  97          if ($observer instanceof question_engine_unit_of_work) {
  98              $observer->save($dm);
  99          } else {
 100              $dm->insert_questions_usage_by_activity($quba);
 101          }
 102      }
 103  
 104      /**
 105       * Delete a {@link question_usage_by_activity} from the database, based on its id.
 106       * @param int $qubaid the id of the usage to delete.
 107       */
 108      public static function delete_questions_usage_by_activity($qubaid) {
 109          self::delete_questions_usage_by_activities(new qubaid_list(array($qubaid)));
 110      }
 111  
 112      /**
 113       * Delete {@link question_usage_by_activity}s from the database.
 114       * @param qubaid_condition $qubaids identifies which questions usages to delete.
 115       */
 116      public static function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
 117          $dm = new question_engine_data_mapper();
 118          $dm->delete_questions_usage_by_activities($qubaids);
 119      }
 120  
 121      /**
 122       * Change the maxmark for the question_attempt with number in usage $slot
 123       * for all the specified question_attempts.
 124       * @param qubaid_condition $qubaids Selects which usages are updated.
 125       * @param int $slot the number is usage to affect.
 126       * @param number $newmaxmark the new max mark to set.
 127       */
 128      public static function set_max_mark_in_attempts(qubaid_condition $qubaids,
 129              $slot, $newmaxmark) {
 130          $dm = new question_engine_data_mapper();
 131          $dm->set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);
 132      }
 133  
 134      /**
 135       * Validate that the manual grade submitted for a particular question is in range.
 136       * @param int $qubaid the question_usage id.
 137       * @param int $slot the slot number within the usage.
 138       * @return bool whether the submitted data is in range.
 139       */
 140      public static function is_manual_grade_in_range($qubaid, $slot) {
 141          $prefix = 'q' . $qubaid . ':' . $slot . '_';
 142          $mark = question_utils::optional_param_mark($prefix . '-mark');
 143          $maxmark = optional_param($prefix . '-maxmark', null, PARAM_FLOAT);
 144          $minfraction = optional_param($prefix . ':minfraction', null, PARAM_FLOAT);
 145          $maxfraction = optional_param($prefix . ':maxfraction', null, PARAM_FLOAT);
 146          return $mark === '' ||
 147                  ($mark !== null && $mark >= $minfraction * $maxmark && $mark <= $maxfraction * $maxmark) ||
 148                  ($mark === null && $maxmark === null);
 149      }
 150  
 151      /**
 152       * @param array $questionids of question ids.
 153       * @param qubaid_condition $qubaids ids of the usages to consider.
 154       * @return boolean whether any of these questions are being used by any of
 155       *      those usages.
 156       */
 157      public static function questions_in_use(array $questionids, qubaid_condition $qubaids = null) {
 158          if (is_null($qubaids)) {
 159              return false;
 160          }
 161          $dm = new question_engine_data_mapper();
 162          return $dm->questions_in_use($questionids, $qubaids);
 163      }
 164  
 165      /**
 166       * Get the number of times each variant has been used for each question in a list
 167       * in a set of usages.
 168       * @param array $questionids of question ids.
 169       * @param qubaid_condition $qubaids ids of the usages to consider.
 170       * @return array questionid => variant number => num uses.
 171       */
 172      public static function load_used_variants(array $questionids, qubaid_condition $qubaids) {
 173          $dm = new question_engine_data_mapper();
 174          return $dm->load_used_variants($questionids, $qubaids);
 175      }
 176  
 177      /**
 178       * Create an archetypal behaviour for a particular question attempt.
 179       * Used by {@link question_definition::make_behaviour()}.
 180       *
 181       * @param string $preferredbehaviour the type of model required.
 182       * @param question_attempt $qa the question attempt the model will process.
 183       * @return question_behaviour an instance of appropriate behaviour class.
 184       */
 185      public static function make_archetypal_behaviour($preferredbehaviour, question_attempt $qa) {
 186          if (!self::is_behaviour_archetypal($preferredbehaviour)) {
 187              throw new coding_exception('The requested behaviour is not actually ' .
 188                      'an archetypal one.');
 189          }
 190  
 191          self::load_behaviour_class($preferredbehaviour);
 192          $class = 'qbehaviour_' . $preferredbehaviour;
 193          return new $class($qa, $preferredbehaviour);
 194      }
 195  
 196      /**
 197       * @param string $behaviour the name of a behaviour.
 198       * @return array of {@link question_display_options} field names, that are
 199       * not relevant to this behaviour before a 'finish' action.
 200       */
 201      public static function get_behaviour_unused_display_options($behaviour) {
 202          return self::get_behaviour_type($behaviour)->get_unused_display_options();
 203      }
 204  
 205      /**
 206       * With this behaviour, is it possible that a question might finish as the student
 207       * interacts with it, without a call to the {@link question_attempt::finish()} method?
 208       * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
 209       * @return bool whether with this behaviour, questions may finish naturally.
 210       */
 211      public static function can_questions_finish_during_the_attempt($behaviour) {
 212          return self::get_behaviour_type($behaviour)->can_questions_finish_during_the_attempt();
 213      }
 214  
 215      /**
 216       * Create a behaviour for a particular type. If that type cannot be
 217       * found, return an instance of qbehaviour_missing.
 218       *
 219       * Normally you should use {@link make_archetypal_behaviour()}, or
 220       * call the constructor of a particular model class directly. This method
 221       * is only intended for use by {@link question_attempt::load_from_records()}.
 222       *
 223       * @param string $behaviour the type of model to create.
 224       * @param question_attempt $qa the question attempt the model will process.
 225       * @param string $preferredbehaviour the preferred behaviour for the containing usage.
 226       * @return question_behaviour an instance of appropriate behaviour class.
 227       */
 228      public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) {
 229          try {
 230              self::load_behaviour_class($behaviour);
 231          } catch (Exception $e) {
 232              self::load_behaviour_class('missing');
 233              return new qbehaviour_missing($qa, $preferredbehaviour);
 234          }
 235          $class = 'qbehaviour_' . $behaviour;
 236          return new $class($qa, $preferredbehaviour);
 237      }
 238  
 239      /**
 240       * Load the behaviour class(es) belonging to a particular model. That is,
 241       * include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit
 242       * of checking.
 243       * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
 244       */
 245      public static function load_behaviour_class($behaviour) {
 246          global $CFG;
 247          if (isset(self::$loadedbehaviours[$behaviour])) {
 248              return;
 249          }
 250          $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php';
 251          if (!is_readable($file)) {
 252              throw new coding_exception('Unknown question behaviour ' . $behaviour);
 253          }
 254          include_once($file);
 255  
 256          $class = 'qbehaviour_' . $behaviour;
 257          if (!class_exists($class)) {
 258              throw new coding_exception('Question behaviour ' . $behaviour .
 259                      ' does not define the required class ' . $class . '.');
 260          }
 261  
 262          self::$loadedbehaviours[$behaviour] = 1;
 263      }
 264  
 265      /**
 266       * Create a behaviour for a particular type. If that type cannot be
 267       * found, return an instance of qbehaviour_missing.
 268       *
 269       * Normally you should use {@link make_archetypal_behaviour()}, or
 270       * call the constructor of a particular model class directly. This method
 271       * is only intended for use by {@link question_attempt::load_from_records()}.
 272       *
 273       * @param string $behaviour the type of model to create.
 274       * @param question_attempt $qa the question attempt the model will process.
 275       * @param string $preferredbehaviour the preferred behaviour for the containing usage.
 276       * @return question_behaviour_type an instance of appropriate behaviour class.
 277       */
 278      public static function get_behaviour_type($behaviour) {
 279  
 280          if (array_key_exists($behaviour, self::$behaviourtypes)) {
 281              return self::$behaviourtypes[$behaviour];
 282          }
 283  
 284          self::load_behaviour_type_class($behaviour);
 285  
 286          $class = 'qbehaviour_' . $behaviour . '_type';
 287          if (class_exists($class)) {
 288              self::$behaviourtypes[$behaviour] = new $class();
 289          } else {
 290              debugging('Question behaviour ' . $behaviour .
 291                      ' does not define the required class ' . $class . '.', DEBUG_DEVELOPER);
 292              self::$behaviourtypes[$behaviour] = new question_behaviour_type_fallback($behaviour);
 293          }
 294  
 295          return self::$behaviourtypes[$behaviour];
 296      }
 297  
 298      /**
 299       * Load the behaviour type class for a particular behaviour. That is,
 300       * include_once('/question/behaviour/' . $behaviour . '/behaviourtype.php').
 301       * @param string $behaviour the behaviour name. For example 'interactive' or 'deferredfeedback'.
 302       */
 303      protected static function load_behaviour_type_class($behaviour) {
 304          global $CFG;
 305          if (isset(self::$behaviourtypes[$behaviour])) {
 306              return;
 307          }
 308          $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviourtype.php';
 309          if (!is_readable($file)) {
 310              debugging('Question behaviour ' . $behaviour .
 311                      ' is missing the behaviourtype.php file.', DEBUG_DEVELOPER);
 312          }
 313          include_once($file);
 314      }
 315  
 316      /**
 317       * Return an array where the keys are the internal names of the archetypal
 318       * behaviours, and the values are a human-readable name. An
 319       * archetypal behaviour is one that is suitable to pass the name of to
 320       * {@link question_usage_by_activity::set_preferred_behaviour()}.
 321       *
 322       * @return array model name => lang string for this behaviour name.
 323       */
 324      public static function get_archetypal_behaviours() {
 325          $archetypes = array();
 326          $behaviours = core_component::get_plugin_list('qbehaviour');
 327          foreach ($behaviours as $behaviour => $notused) {
 328              if (self::is_behaviour_archetypal($behaviour)) {
 329                  $archetypes[$behaviour] = self::get_behaviour_name($behaviour);
 330              }
 331          }
 332          asort($archetypes, SORT_LOCALE_STRING);
 333          return $archetypes;
 334      }
 335  
 336      /**
 337       * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
 338       * @return bool whether this is an archetypal behaviour.
 339       */
 340      public static function is_behaviour_archetypal($behaviour) {
 341          return self::get_behaviour_type($behaviour)->is_archetypal();
 342      }
 343  
 344      /**
 345       * Return an array where the keys are the internal names of the behaviours
 346       * in preferred order and the values are a human-readable name.
 347       *
 348       * @param array $archetypes, array of behaviours
 349       * @param string $orderlist, a comma separated list of behaviour names
 350       * @param string $disabledlist, a comma separated list of behaviour names
 351       * @param string $current, current behaviour name
 352       * @return array model name => lang string for this behaviour name.
 353       */
 354      public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) {
 355  
 356          // Get disabled behaviours
 357          if ($disabledlist) {
 358              $disabled = explode(',', $disabledlist);
 359          } else {
 360              $disabled = array();
 361          }
 362  
 363          if ($orderlist) {
 364              $order = explode(',', $orderlist);
 365          } else {
 366              $order = array();
 367          }
 368  
 369          foreach ($disabled as $behaviour) {
 370              if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {
 371                  unset($archetypes[$behaviour]);
 372              }
 373          }
 374  
 375          // Get behaviours in preferred order
 376          $behaviourorder = array();
 377          foreach ($order as $behaviour) {
 378              if (array_key_exists($behaviour, $archetypes)) {
 379                  $behaviourorder[$behaviour] = $archetypes[$behaviour];
 380              }
 381          }
 382          // Get the rest of behaviours and sort them alphabetically
 383          $leftover = array_diff_key($archetypes, $behaviourorder);
 384          asort($leftover, SORT_LOCALE_STRING);
 385  
 386          // Set up the final order to be displayed
 387          return $behaviourorder + $leftover;
 388      }
 389  
 390      /**
 391       * Return an array where the keys are the internal names of the behaviours
 392       * in preferred order and the values are a human-readable name.
 393       *
 394       * @param string $currentbehaviour
 395       * @return array model name => lang string for this behaviour name.
 396       */
 397      public static function get_behaviour_options($currentbehaviour) {
 398          $config = question_bank::get_config();
 399          $archetypes = self::get_archetypal_behaviours();
 400  
 401          // If no admin setting return all behavious
 402          if (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) {
 403              return $archetypes;
 404          }
 405  
 406          if (empty($config->behavioursortorder)) {
 407              $order = '';
 408          } else {
 409              $order = $config->behavioursortorder;
 410          }
 411          if (empty($config->disabledbehaviours)) {
 412              $disabled = '';
 413          } else {
 414              $disabled = $config->disabledbehaviours;
 415          }
 416  
 417          return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);
 418      }
 419  
 420      /**
 421       * Get the translated name of a behaviour, for display in the UI.
 422       * @param string $behaviour the internal name of the model.
 423       * @return string name from the current language pack.
 424       */
 425      public static function get_behaviour_name($behaviour) {
 426          return get_string('pluginname', 'qbehaviour_' . $behaviour);
 427      }
 428  
 429      /**
 430       * @return array all the file area names that may contain response files.
 431       */
 432      public static function get_all_response_file_areas() {
 433          $variables = array();
 434          foreach (question_bank::get_all_qtypes() as $qtype) {
 435              $variables = array_merge($variables, $qtype->response_file_areas());
 436          }
 437  
 438          $areas = array();
 439          foreach (array_unique($variables) as $variable) {
 440              $areas[] = 'response_' . $variable;
 441          }
 442          return $areas;
 443      }
 444  
 445      /**
 446       * Returns the valid choices for the number of decimal places for showing
 447       * question marks. For use in the user interface.
 448       * @return array suitable for passing to {@link html_writer::select()} or similar.
 449       */
 450      public static function get_dp_options() {
 451          return question_display_options::get_dp_options();
 452      }
 453  
 454      /**
 455       * Initialise the JavaScript required on pages where questions will be displayed.
 456       *
 457       * @return string
 458       */
 459      public static function initialise_js() {
 460          return question_flags::initialise_js();
 461      }
 462  }
 463  
 464  
 465  /**
 466   * This class contains all the options that controls how a question is displayed.
 467   *
 468   * Normally, what will happen is that the calling code will set up some display
 469   * options to indicate what sort of question display it wants, and then before the
 470   * question is rendered, the behaviour will be given a chance to modify the
 471   * display options, so that, for example, A question that is finished will only
 472   * be shown read-only, and a question that has not been submitted will not have
 473   * any sort of feedback displayed.
 474   *
 475   * @copyright  2009 The Open University
 476   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 477   */
 478  class question_display_options {
 479      /**#@+
 480       * @var integer named constants for the values that most of the options take.
 481       */
 482      const SHOW_ALL = -1;
 483      const HIDDEN = 0;
 484      const VISIBLE = 1;
 485      const EDITABLE = 2;
 486      /**#@-*/
 487  
 488      /**#@+ @var integer named constants for the {@link $marks} option. */
 489      const MAX_ONLY = 1;
 490      const MARK_AND_MAX = 2;
 491      /**#@-*/
 492  
 493      /**
 494       * @var integer maximum value for the {@link $markpd} option. This is
 495       * effectively set by the database structure, which uses NUMBER(12,7) columns
 496       * for question marks/fractions.
 497       */
 498      const MAX_DP = 7;
 499  
 500      /**
 501       * @var boolean whether the question should be displayed as a read-only review,
 502       * or in an active state where you can change the answer.
 503       */
 504      public $readonly = false;
 505  
 506      /**
 507       * @var boolean whether the question type should output hidden form fields
 508       * to reset any incorrect parts of the resonse to blank.
 509       */
 510      public $clearwrong = false;
 511  
 512      /**
 513       * Should the student have what they got right and wrong clearly indicated.
 514       * This includes the green/red hilighting of the bits of their response,
 515       * whether the one-line summary of the current state of the question says
 516       * correct/incorrect or just answered.
 517       * @var integer {@link question_display_options::HIDDEN} or
 518       * {@link question_display_options::VISIBLE}
 519       */
 520      public $correctness = self::VISIBLE;
 521  
 522      /**
 523       * The the mark and/or the maximum available mark for this question be visible?
 524       * @var integer {@link question_display_options::HIDDEN},
 525       * {@link question_display_options::MAX_ONLY} or {@link question_display_options::MARK_AND_MAX}
 526       */
 527      public $marks = self::MARK_AND_MAX;
 528  
 529      /** @var number of decimal places to use when formatting marks for output. */
 530      public $markdp = 2;
 531  
 532      /**
 533       * Should the flag this question UI element be visible, and if so, should the
 534       * flag state be changable?
 535       * @var integer {@link question_display_options::HIDDEN},
 536       * {@link question_display_options::VISIBLE} or {@link question_display_options::EDITABLE}
 537       */
 538      public $flags = self::VISIBLE;
 539  
 540      /**
 541       * Should the specific feedback be visible.
 542       * @var integer {@link question_display_options::HIDDEN} or
 543       * {@link question_display_options::VISIBLE}
 544       */
 545      public $feedback = self::VISIBLE;
 546  
 547      /**
 548       * For questions with a number of sub-parts (like matching, or
 549       * multiple-choice, multiple-reponse) display the number of sub-parts that
 550       * were correct.
 551       * @var integer {@link question_display_options::HIDDEN} or
 552       * {@link question_display_options::VISIBLE}
 553       */
 554      public $numpartscorrect = self::VISIBLE;
 555  
 556      /**
 557       * Should the general feedback be visible?
 558       * @var integer {@link question_display_options::HIDDEN} or
 559       * {@link question_display_options::VISIBLE}
 560       */
 561      public $generalfeedback = self::VISIBLE;
 562  
 563      /**
 564       * Should the automatically generated display of what the correct answer is
 565       * be visible?
 566       * @var integer {@link question_display_options::HIDDEN} or
 567       * {@link question_display_options::VISIBLE}
 568       */
 569      public $rightanswer = self::VISIBLE;
 570  
 571      /**
 572       * Should the manually added marker's comment be visible. Should the link for
 573       * adding/editing the comment be there.
 574       * @var integer {@link question_display_options::HIDDEN},
 575       * {@link question_display_options::VISIBLE}, or {@link question_display_options::EDITABLE}.
 576       * Editable means that form fields are displayed inline.
 577       */
 578      public $manualcomment = self::VISIBLE;
 579  
 580      /**
 581       * Should we show a 'Make comment or override grade' link?
 582       * @var string base URL for the edit comment script, which will be shown if
 583       * $manualcomment = self::VISIBLE.
 584       */
 585      public $manualcommentlink = null;
 586  
 587      /**
 588       * Used in places like the question history table, to show a link to review
 589       * this question in a certain state. If blank, a link is not shown.
 590       * @var moodle_url base URL for a review question script.
 591       */
 592      public $questionreviewlink = null;
 593  
 594      /**
 595       * Should the history of previous question states table be visible?
 596       * @var integer {@link question_display_options::HIDDEN} or
 597       * {@link question_display_options::VISIBLE}
 598       */
 599      public $history = self::HIDDEN;
 600  
 601      /**
 602       * @since 2.9
 603       * @var string extra HTML to include at the end of the outcome (feedback) box
 604       * of the question display.
 605       *
 606       * This field is now badly named. The place it included is was changed
 607       * (for the better) but the name was left unchanged for backwards compatibility.
 608       */
 609      public $extrainfocontent = '';
 610  
 611      /**
 612       * @since 2.9
 613       * @var string extra HTML to include in the history box of the question display,
 614       * if it is shown.
 615       */
 616      public $extrahistorycontent = '';
 617  
 618      /**
 619       * If not empty, then a link to edit the question will be included in
 620       * the info box for the question.
 621       *
 622       * If used, this array must contain an element courseid or cmid.
 623       *
 624       * It shoudl also contain a parameter returnurl => moodle_url giving a
 625       * sensible URL to go back to when the editing form is submitted or cancelled.
 626       *
 627       * @var array url parameter for the edit link. id => questiosnid will be
 628       * added automatically.
 629       */
 630      public $editquestionparams = array();
 631  
 632      /**
 633       * @var context the context the attempt being output belongs to.
 634       */
 635      public $context;
 636  
 637      /**
 638       * @var int The option to show the action author in the response history.
 639       */
 640      public $userinfoinhistory = self::HIDDEN;
 641  
 642      /**
 643       * This identifier should be added to the labels of all input fields in the question.
 644       *
 645       * This is so people using assistive technology can easily tell which input belong to
 646       * which question. The helper {@see self::add_question_identifier_to_label() makes this easier.
 647       *
 648       * If not set before the question is rendered, then it defaults to 'Question N'.
 649       * (lang string)
 650       *
 651       * @var string The identifier that the question being rendered is associated with.
 652       *              E.g. The question number when it is rendered on a quiz.
 653       */
 654      public $questionidentifier = null;
 655  
 656      /**
 657       * @var ?bool $versioninfo Should we display the version in the question info?
 658       */
 659      public ?bool $versioninfo = null;
 660  
 661      /**
 662       * Set all the feedback-related fields {@link $feedback}, {@link generalfeedback},
 663       * {@link rightanswer} and {@link manualcomment} to
 664       * {@link question_display_options::HIDDEN}.
 665       */
 666      public function hide_all_feedback() {
 667          $this->feedback = self::HIDDEN;
 668          $this->numpartscorrect = self::HIDDEN;
 669          $this->generalfeedback = self::HIDDEN;
 670          $this->rightanswer = self::HIDDEN;
 671          $this->manualcomment = self::HIDDEN;
 672          $this->correctness = self::HIDDEN;
 673      }
 674  
 675      /**
 676       * Returns the valid choices for the number of decimal places for showing
 677       * question marks. For use in the user interface.
 678       *
 679       * Calling code should probably use {@link question_engine::get_dp_options()}
 680       * rather than calling this method directly.
 681       *
 682       * @return array suitable for passing to {@link html_writer::select()} or similar.
 683       */
 684      public static function get_dp_options() {
 685          $options = array();
 686          for ($i = 0; $i <= self::MAX_DP; $i += 1) {
 687              $options[$i] = $i;
 688          }
 689          return $options;
 690      }
 691  
 692      /**
 693       * Helper to add the question identify (if there is one) to the label of an input field in a question.
 694       *
 695       * @param string $label The plain field label. E.g. 'Answer 1'
 696       * @param bool $sridentifier If true, the question identifier, if added, will be wrapped in a sr-only span. Default false.
 697       * @param bool $addbefore If true, the question identifier will be added before the label.
 698       * @return string The amended label. For example 'Answer 1, Question 1'.
 699       */
 700      public function add_question_identifier_to_label(string $label, bool $sridentifier = false, bool $addbefore = false): string {
 701          if (!$this->has_question_identifier()) {
 702              return $label;
 703          }
 704          $identifier = $this->questionidentifier;
 705          if ($sridentifier) {
 706              $identifier = html_writer::span($identifier, 'sr-only');
 707          }
 708          $fieldlang = 'fieldinquestion';
 709          if ($addbefore) {
 710              $fieldlang = 'fieldinquestionpre';
 711          }
 712          return get_string($fieldlang, 'question', (object)['fieldname' => $label, 'questionindentifier' => $identifier]);
 713      }
 714  
 715      /**
 716       * Whether a question number has been provided for the question that is being displayed.
 717       *
 718       * @return bool
 719       */
 720      public function has_question_identifier(): bool {
 721          return $this->questionidentifier !== null && trim($this->questionidentifier) !== '';
 722      }
 723  }
 724  
 725  
 726  /**
 727   * Contains the logic for handling question flags.
 728   *
 729   * @copyright  2010 The Open University
 730   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 731   */
 732  abstract class question_flags {
 733      /**
 734       * Get the checksum that validates that a toggle request is valid.
 735       * @param int $qubaid the question usage id.
 736       * @param int $questionid the question id.
 737       * @param int $sessionid the question_attempt id.
 738       * @param object $user the user. If null, defaults to $USER.
 739       * @return string that needs to be sent to question/toggleflag.php for it to work.
 740       */
 741      protected static function get_toggle_checksum($qubaid, $questionid,
 742              $qaid, $slot, $user = null) {
 743          if (is_null($user)) {
 744              global $USER;
 745              $user = $USER;
 746          }
 747          return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);
 748      }
 749  
 750      /**
 751       * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state.
 752       * You need to append &newstate=0/1 to this.
 753       * @return the post data to send.
 754       */
 755      public static function get_postdata(question_attempt $qa) {
 756          $qaid = $qa->get_database_id();
 757          $qubaid = $qa->get_usage_id();
 758          $qid = $qa->get_question_id();
 759          $slot = $qa->get_slot();
 760          $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot);
 761          return "qaid={$qaid}&qubaid={$qubaid}&qid={$qid}&slot={$slot}&checksum={$checksum}&sesskey=" .
 762                  sesskey() . '&newstate=';
 763      }
 764  
 765      /**
 766       * If the request seems valid, update the flag state of a question attempt.
 767       * Throws exceptions if this is not a valid update request.
 768       * @param int $qubaid the question usage id.
 769       * @param int $questionid the question id.
 770       * @param int $sessionid the question_attempt id.
 771       * @param string $checksum checksum, as computed by {@link get_toggle_checksum()}
 772       *      corresponding to the last three arguments.
 773       * @param bool $newstate the new state of the flag. true = flagged.
 774       */
 775      public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {
 776          // Check the checksum - it is very hard to know who a question session belongs
 777          // to, so we require that checksum parameter is matches an md5 hash of the
 778          // three ids and the users username. Since we are only updating a flag, that
 779          // probably makes it sufficiently difficult for malicious users to toggle
 780          // other users flags.
 781          if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {
 782              throw new moodle_exception('errorsavingflags', 'question');
 783          }
 784  
 785          $dm = new question_engine_data_mapper();
 786          $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
 787      }
 788  
 789      public static function initialise_js() {
 790          global $CFG, $PAGE, $OUTPUT;
 791          static $done = false;
 792          if ($done) {
 793              return;
 794          }
 795          $module = array(
 796              'name' => 'core_question_flags',
 797              'fullpath' => '/question/flags.js',
 798              'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
 799          );
 800          $actionurl = $CFG->wwwroot . '/question/toggleflag.php';
 801          $flagattributes = array(
 802              0 => array(
 803                  'src' => $OUTPUT->image_url('i/unflagged') . '',
 804                  'title' => get_string('clicktoflag', 'question'),
 805                  'alt' => get_string('flagged', 'question'), // Label on toggle should not change.
 806                  'text' => get_string('clickflag', 'question'),
 807              ),
 808              1 => array(
 809                  'src' => $OUTPUT->image_url('i/flagged') . '',
 810                  'title' => get_string('clicktounflag', 'question'),
 811                  'alt' => get_string('flagged', 'question'),
 812                  'text' => get_string('clickunflag', 'question'),
 813              ),
 814          );
 815          $PAGE->requires->js_init_call('M.core_question_flags.init',
 816                  array($actionurl, $flagattributes), false, $module);
 817          $done = true;
 818      }
 819  }
 820  
 821  
 822  /**
 823   * Exception thrown when the system detects that a student has done something
 824   * out-of-order to a question. This can happen, for example, if they click
 825   * the browser's back button in a quiz, then try to submit a different response.
 826   *
 827   * @copyright  2010 The Open University
 828   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 829   */
 830  class question_out_of_sequence_exception extends moodle_exception {
 831      public function __construct($qubaid, $slot, $postdata) {
 832          if ($postdata == null) {
 833              $postdata = data_submitted();
 834          }
 835          parent::__construct('submissionoutofsequence', 'question', '', null,
 836                  "QUBAid: {$qubaid}, slot: {$slot}, post data: " . print_r($postdata, true));
 837      }
 838  }
 839  
 840  
 841  /**
 842   * Useful functions for writing question types and behaviours.
 843   *
 844   * @copyright 2010 The Open University
 845   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 846   */
 847  abstract class question_utils {
 848      /**
 849       * @var float tolerance to use when comparing question mark/fraction values.
 850       *
 851       * When comparing floating point numbers in a computer, the representation is not
 852       * necessarily exact. Therefore, we need to allow a tolerance.
 853       * Question marks are stored in the database as decimal numbers with 7 decimal places.
 854       * Therefore, this is the appropriate tolerance to use.
 855       */
 856      const MARK_TOLERANCE = 0.00000005;
 857  
 858      /**
 859       * Tests to see whether two arrays have the same keys, with the same values
 860       * (as compared by ===) for each key. However, the order of the arrays does
 861       * not have to be the same.
 862       * @param array $array1 the first array.
 863       * @param array $array2 the second array.
 864       * @return bool whether the two arrays have the same keys with the same
 865       *      corresponding values.
 866       */
 867      public static function arrays_have_same_keys_and_values(array $array1, array $array2) {
 868          if (count($array1) != count($array2)) {
 869              return false;
 870          }
 871          foreach ($array1 as $key => $value1) {
 872              if (!array_key_exists($key, $array2)) {
 873                  return false;
 874              }
 875              if (((string) $value1) !== ((string) $array2[$key])) {
 876                  return false;
 877              }
 878          }
 879          return true;
 880      }
 881  
 882      /**
 883       * Tests to see whether two arrays have the same value at a particular key.
 884       * This method will return true if:
 885       * 1. Neither array contains the key; or
 886       * 2. Both arrays contain the key, and the corresponding values compare
 887       *      identical when cast to strings and compared with ===.
 888       * @param array $array1 the first array.
 889       * @param array $array2 the second array.
 890       * @param string $key an array key.
 891       * @return bool whether the two arrays have the same value (or lack of
 892       *      one) for a given key.
 893       */
 894      public static function arrays_same_at_key(array $array1, array $array2, $key) {
 895          if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) {
 896              return ((string) $array1[$key]) === ((string) $array2[$key]);
 897          }
 898          if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) {
 899              return true;
 900          }
 901          return false;
 902      }
 903  
 904      /**
 905       * Tests to see whether two arrays have the same value at a particular key.
 906       * Missing values are replaced by '', and then the values are cast to
 907       * strings and compared with ===.
 908       * @param array $array1 the first array.
 909       * @param array $array2 the second array.
 910       * @param string $key an array key.
 911       * @return bool whether the two arrays have the same value (or lack of
 912       *      one) for a given key.
 913       */
 914      public static function arrays_same_at_key_missing_is_blank(
 915              array $array1, array $array2, $key) {
 916          if (array_key_exists($key, $array1)) {
 917              $value1 = $array1[$key];
 918          } else {
 919              $value1 = '';
 920          }
 921          if (array_key_exists($key, $array2)) {
 922              $value2 = $array2[$key];
 923          } else {
 924              $value2 = '';
 925          }
 926          return ((string) $value1) === ((string) $value2);
 927      }
 928  
 929      /**
 930       * Tests to see whether two arrays have the same value at a particular key.
 931       * Missing values are replaced by 0, and then the values are cast to
 932       * integers and compared with ===.
 933       * @param array $array1 the first array.
 934       * @param array $array2 the second array.
 935       * @param string $key an array key.
 936       * @return bool whether the two arrays have the same value (or lack of
 937       *      one) for a given key.
 938       */
 939      public static function arrays_same_at_key_integer(
 940              array $array1, array $array2, $key) {
 941          if (array_key_exists($key, $array1)) {
 942              $value1 = (int) $array1[$key];
 943          } else {
 944              $value1 = 0;
 945          }
 946          if (array_key_exists($key, $array2)) {
 947              $value2 = (int) $array2[$key];
 948          } else {
 949              $value2 = 0;
 950          }
 951          return $value1 === $value2;
 952      }
 953  
 954      private static $units     = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix');
 955      private static $tens      = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc');
 956      private static $hundreds  = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm');
 957      private static $thousands = array('', 'm', 'mm', 'mmm');
 958  
 959      /**
 960       * Convert an integer to roman numerals.
 961       * @param int $number an integer between 1 and 3999 inclusive. Anything else
 962       *      will throw an exception.
 963       * @return string the number converted to lower case roman numerals.
 964       */
 965      public static function int_to_roman($number) {
 966          if (!is_integer($number) || $number < 1 || $number > 3999) {
 967              throw new coding_exception('Only integers between 0 and 3999 can be ' .
 968                      'converted to roman numerals.', $number);
 969          }
 970  
 971          return self::$thousands[floor($number / 1000) % 10] . self::$hundreds[floor($number / 100) % 10] .
 972                  self::$tens[floor($number / 10) % 10] . self::$units[$number % 10];
 973      }
 974  
 975      /**
 976       * Convert an integer to a letter of alphabet.
 977       * @param int $number an integer between 1 and 26 inclusive.
 978       * Anything else will throw an exception.
 979       * @return string the number converted to upper case letter of alphabet.
 980       */
 981      public static function int_to_letter($number) {
 982          $alphabet = [
 983                  '1' => 'A',
 984                  '2' => 'B',
 985                  '3' => 'C',
 986                  '4' => 'D',
 987                  '5' => 'E',
 988                  '6' => 'F',
 989                  '7' => 'G',
 990                  '8' => 'H',
 991                  '9' => 'I',
 992                  '10' => 'J',
 993                  '11' => 'K',
 994                  '12' => 'L',
 995                  '13' => 'M',
 996                  '14' => 'N',
 997                  '15' => 'O',
 998                  '16' => 'P',
 999                  '17' => 'Q',
1000                  '18' => 'R',
1001                  '19' => 'S',
1002                  '20' => 'T',
1003                  '21' => 'U',
1004                  '22' => 'V',
1005                  '23' => 'W',
1006                  '24' => 'X',
1007                  '25' => 'Y',
1008                  '26' => 'Z'
1009          ];
1010          if (!is_integer($number) || $number < 1 || $number > count($alphabet)) {
1011              throw new coding_exception('Only integers between 1 and 26 can be converted to letters.', $number);
1012          }
1013          return $alphabet[$number];
1014      }
1015  
1016      /**
1017       * Typically, $mark will have come from optional_param($name, null, PARAM_RAW_TRIMMED).
1018       * This method copes with:
1019       *  - keeping null or '' input unchanged - important to let teaches set a question back to requries grading.
1020       *  - numbers that were typed as either 1.00 or 1,00 form.
1021       *  - invalid things, which get turned into null.
1022       *
1023       * @param string|null $mark raw use input of a mark.
1024       * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
1025       */
1026      public static function clean_param_mark($mark) {
1027          if ($mark === '' || is_null($mark)) {
1028              return $mark;
1029          }
1030  
1031          $mark = str_replace(',', '.', $mark);
1032          // This regexp should match the one in validate_param.
1033          if (!preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', $mark)) {
1034              return null;
1035          }
1036  
1037          return clean_param($mark, PARAM_FLOAT);
1038      }
1039  
1040      /**
1041       * Get a sumitted variable (from the GET or POST data) that is a mark.
1042       * @param string $parname the submitted variable name.
1043       * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
1044       */
1045      public static function optional_param_mark($parname) {
1046          return self::clean_param_mark(
1047                  optional_param($parname, null, PARAM_RAW_TRIMMED));
1048      }
1049  
1050      /**
1051       * Convert part of some question content to plain text.
1052       * @param string $text the text.
1053       * @param int $format the text format.
1054       * @param array $options formatting options. Passed to {@link format_text}.
1055       * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
1056       */
1057      public static function to_plain_text($text, $format, $options = array('noclean' => 'true')) {
1058          // The following call to html_to_text uses the option that strips out
1059          // all URLs, but format_text complains if it finds @@PLUGINFILE@@ tokens.
1060          // So, we need to replace @@PLUGINFILE@@ with a real URL, but it doesn't
1061          // matter what. We use http://example.com/.
1062          $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $text);
1063          return html_to_text(format_text($text, $format, $options), 0, false);
1064      }
1065  
1066      /**
1067       * Get the options required to configure the filepicker for one of the editor
1068       * toolbar buttons.
1069       *
1070       * @param mixed $acceptedtypes array of types of '*'.
1071       * @param int $draftitemid the draft area item id.
1072       * @param context $context the context.
1073       * @return object the required options.
1074       */
1075      protected static function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
1076          $filepickeroptions = new stdClass();
1077          $filepickeroptions->accepted_types = $acceptedtypes;
1078          $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
1079          $filepickeroptions->context = $context;
1080          $filepickeroptions->env = 'filepicker';
1081  
1082          $options = initialise_filepicker($filepickeroptions);
1083          $options->context = $context;
1084          $options->client_id = uniqid();
1085          $options->env = 'editor';
1086          $options->itemid = $draftitemid;
1087  
1088          return $options;
1089      }
1090  
1091      /**
1092       * Get filepicker options for question related text areas.
1093       *
1094       * @param context $context the context.
1095       * @param int $draftitemid the draft area item id.
1096       * @return array An array of options
1097       */
1098      public static function get_filepicker_options($context, $draftitemid) {
1099          return [
1100                  'image' => self::specific_filepicker_options(['image'], $draftitemid, $context),
1101                  'media' => self::specific_filepicker_options(['video', 'audio'], $draftitemid, $context),
1102                  'link'  => self::specific_filepicker_options('*', $draftitemid, $context),
1103              ];
1104      }
1105  
1106      /**
1107       * Get editor options for question related text areas.
1108       *
1109       * @param context $context the context.
1110       * @return array An array of options
1111       */
1112      public static function get_editor_options($context) {
1113          global $CFG;
1114  
1115          $editoroptions = [
1116                  'subdirs'  => 0,
1117                  'context'  => $context,
1118                  'maxfiles' => EDITOR_UNLIMITED_FILES,
1119                  'maxbytes' => $CFG->maxbytes,
1120                  'noclean' => 0,
1121                  'trusttext' => 0,
1122                  'autosave' => false
1123          ];
1124  
1125          return $editoroptions;
1126      }
1127  }
1128  
1129  
1130  /**
1131   * The interface for strategies for controlling which variant of each question is used.
1132   *
1133   * @copyright  2011 The Open University
1134   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1135   */
1136  interface question_variant_selection_strategy {
1137      /**
1138       * @param int $maxvariants the num
1139       * @param string $seed data that can be used to controls how the variant is selected
1140       *      in a semi-random way.
1141       * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive.
1142       */
1143      public function choose_variant($maxvariants, $seed);
1144  }
1145  
1146  
1147  /**
1148   * A {@link question_variant_selection_strategy} that is completely random.
1149   *
1150   * @copyright  2011 The Open University
1151   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1152   */
1153  class question_variant_random_strategy implements question_variant_selection_strategy {
1154      public function choose_variant($maxvariants, $seed) {
1155          return rand(1, $maxvariants);
1156      }
1157  }
1158  
1159  
1160  /**
1161   * A {@link question_variant_selection_strategy} that is effectively random
1162   * for the first attempt, and then after that cycles through the available
1163   * variants so that the students will not get a repeated variant until they have
1164   * seen them all.
1165   *
1166   * @copyright  2011 The Open University
1167   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1168   */
1169  class question_variant_pseudorandom_no_repeats_strategy
1170          implements question_variant_selection_strategy {
1171  
1172      /** @var int the number of attempts this users has had, including the curent one. */
1173      protected $attemptno;
1174  
1175      /** @var int the user id the attempt belongs to. */
1176      protected $userid;
1177  
1178      /** @var string extra input fed into the pseudo-random code. */
1179      protected $extrarandomness = '';
1180  
1181      /**
1182       * Constructor.
1183       * @param int $attemptno The attempt number.
1184       * @param int $userid the user the attempt is for (defaults to $USER->id).
1185       */
1186      public function __construct($attemptno, $userid = null, $extrarandomness = '') {
1187          $this->attemptno = $attemptno;
1188          if (is_null($userid)) {
1189              global $USER;
1190              $this->userid = $USER->id;
1191          } else {
1192              $this->userid = $userid;
1193          }
1194  
1195          if ($extrarandomness) {
1196              $this->extrarandomness = '|' . $extrarandomness;
1197          }
1198      }
1199  
1200      public function choose_variant($maxvariants, $seed) {
1201          if ($maxvariants == 1) {
1202              return 1;
1203          }
1204  
1205          $hash = sha1($seed . '|user' . $this->userid . $this->extrarandomness);
1206          $randint = hexdec(substr($hash, 17, 7));
1207  
1208          return ($randint + $this->attemptno) % $maxvariants + 1;
1209      }
1210  }
1211  
1212  /**
1213   * A {@link question_variant_selection_strategy} designed ONLY for testing.
1214   * For selected questions it wil return a specific variants. For the other
1215   * slots it will use a fallback strategy.
1216   *
1217   * @copyright  2013 The Open University
1218   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1219   */
1220  class question_variant_forced_choices_selection_strategy
1221      implements question_variant_selection_strategy {
1222  
1223      /** @var array seed => variant to select. */
1224      protected $forcedchoices;
1225  
1226      /** @var question_variant_selection_strategy strategy used to make the non-forced choices. */
1227      protected $basestrategy;
1228  
1229      /**
1230       * Constructor.
1231       * @param array $forcedchoices array seed => variant to select.
1232       * @param question_variant_selection_strategy $basestrategy strategy used
1233       *      to make the non-forced choices.
1234       */
1235      public function __construct(array $forcedchoices, question_variant_selection_strategy $basestrategy) {
1236          $this->forcedchoices = $forcedchoices;
1237          $this->basestrategy  = $basestrategy;
1238      }
1239  
1240      public function choose_variant($maxvariants, $seed) {
1241          if (array_key_exists($seed, $this->forcedchoices)) {
1242              if ($this->forcedchoices[$seed] > $maxvariants) {
1243                  throw new coding_exception('Forced variant out of range.');
1244              }
1245              return $this->forcedchoices[$seed];
1246          } else {
1247              return $this->basestrategy->choose_variant($maxvariants, $seed);
1248          }
1249      }
1250  
1251      /**
1252       * Helper method for preparing the $forcedchoices array.
1253       * @param array                      $variantsbyslot slot number => variant to select.
1254       * @param question_usage_by_activity $quba           the question usage we need a strategy for.
1255       * @throws coding_exception when variant cannot be forced as doesn't work.
1256       * @return array that can be passed to the constructor as $forcedchoices.
1257       */
1258      public static function prepare_forced_choices_array(array $variantsbyslot,
1259                                                          question_usage_by_activity $quba) {
1260  
1261          $forcedchoices = array();
1262  
1263          foreach ($variantsbyslot as $slot => $varianttochoose) {
1264              $question = $quba->get_question($slot);
1265              $seed = $question->get_variants_selection_seed();
1266              if (array_key_exists($seed, $forcedchoices) && $forcedchoices[$seed] != $varianttochoose) {
1267                  throw new coding_exception('Inconsistent forced variant detected at slot ' . $slot);
1268              }
1269              if ($varianttochoose > $question->get_num_variants()) {
1270                  throw new coding_exception('Forced variant out of range at slot ' . $slot);
1271              }
1272              $forcedchoices[$seed] = $varianttochoose;
1273          }
1274          return $forcedchoices;
1275      }
1276  }