[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/question/engine/ -> lib.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   * 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(dirname(__FILE__) . '/questionusage.php');
  31  require_once(dirname(__FILE__) . '/questionattempt.php');
  32  require_once(dirname(__FILE__) . '/questionattemptstep.php');
  33  require_once(dirname(__FILE__) . '/states.php');
  34  require_once(dirname(__FILE__) . '/datalib.php');
  35  require_once(dirname(__FILE__) . '/renderer.php');
  36  require_once(dirname(__FILE__) . '/bank.php');
  37  require_once(dirname(__FILE__) . '/../type/questiontypebase.php');
  38  require_once(dirname(__FILE__) . '/../type/questionbase.php');
  39  require_once(dirname(__FILE__) . '/../type/rendererbase.php');
  40  require_once(dirname(__FILE__) . '/../behaviour/behaviourtypebase.php');
  41  require_once(dirname(__FILE__) . '/../behaviour/behaviourbase.php');
  42  require_once(dirname(__FILE__) . '/../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 object $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 is_null($mark) || ($mark >= $minfraction * $maxmark && $mark <= $maxfraction * $maxmark);
 147      }
 148  
 149      /**
 150       * @param array $questionids of question ids.
 151       * @param qubaid_condition $qubaids ids of the usages to consider.
 152       * @return boolean whether any of these questions are being used by any of
 153       *      those usages.
 154       */
 155      public static function questions_in_use(array $questionids, qubaid_condition $qubaids = null) {
 156          if (is_null($qubaids)) {
 157              return false;
 158          }
 159          $dm = new question_engine_data_mapper();
 160          return $dm->questions_in_use($questionids, $qubaids);
 161      }
 162  
 163      /**
 164       * Create an archetypal behaviour for a particular question attempt.
 165       * Used by {@link question_definition::make_behaviour()}.
 166       *
 167       * @param string $preferredbehaviour the type of model required.
 168       * @param question_attempt $qa the question attempt the model will process.
 169       * @return question_behaviour an instance of appropriate behaviour class.
 170       */
 171      public static function make_archetypal_behaviour($preferredbehaviour, question_attempt $qa) {
 172          if (!self::is_behaviour_archetypal($preferredbehaviour)) {
 173              throw new coding_exception('The requested behaviour is not actually ' .
 174                      'an archetypal one.');
 175          }
 176  
 177          self::load_behaviour_class($preferredbehaviour);
 178          $class = 'qbehaviour_' . $preferredbehaviour;
 179          return new $class($qa, $preferredbehaviour);
 180      }
 181  
 182      /**
 183       * @param string $behaviour the name of a behaviour.
 184       * @return array of {@link question_display_options} field names, that are
 185       * not relevant to this behaviour before a 'finish' action.
 186       */
 187      public static function get_behaviour_unused_display_options($behaviour) {
 188          return self::get_behaviour_type($behaviour)->get_unused_display_options();
 189      }
 190  
 191      /**
 192       * Create a behaviour for a particular type. If that type cannot be
 193       * found, return an instance of qbehaviour_missing.
 194       *
 195       * Normally you should use {@link make_archetypal_behaviour()}, or
 196       * call the constructor of a particular model class directly. This method
 197       * is only intended for use by {@link question_attempt::load_from_records()}.
 198       *
 199       * @param string $behaviour the type of model to create.
 200       * @param question_attempt $qa the question attempt the model will process.
 201       * @param string $preferredbehaviour the preferred behaviour for the containing usage.
 202       * @return question_behaviour an instance of appropriate behaviour class.
 203       */
 204      public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) {
 205          try {
 206              self::load_behaviour_class($behaviour);
 207          } catch (Exception $e) {
 208              self::load_behaviour_class('missing');
 209              return new qbehaviour_missing($qa, $preferredbehaviour);
 210          }
 211          $class = 'qbehaviour_' . $behaviour;
 212          return new $class($qa, $preferredbehaviour);
 213      }
 214  
 215      /**
 216       * Load the behaviour class(es) belonging to a particular model. That is,
 217       * include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit
 218       * of checking.
 219       * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
 220       */
 221      public static function load_behaviour_class($behaviour) {
 222          global $CFG;
 223          if (isset(self::$loadedbehaviours[$behaviour])) {
 224              return;
 225          }
 226          $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php';
 227          if (!is_readable($file)) {
 228              throw new coding_exception('Unknown question behaviour ' . $behaviour);
 229          }
 230          include_once($file);
 231  
 232          $class = 'qbehaviour_' . $behaviour;
 233          if (!class_exists($class)) {
 234              throw new coding_exception('Question behaviour ' . $behaviour .
 235                      ' does not define the required class ' . $class . '.');
 236          }
 237  
 238          self::$loadedbehaviours[$behaviour] = 1;
 239      }
 240  
 241      /**
 242       * Create a behaviour for a particular type. If that type cannot be
 243       * found, return an instance of qbehaviour_missing.
 244       *
 245       * Normally you should use {@link make_archetypal_behaviour()}, or
 246       * call the constructor of a particular model class directly. This method
 247       * is only intended for use by {@link question_attempt::load_from_records()}.
 248       *
 249       * @param string $behaviour the type of model to create.
 250       * @param question_attempt $qa the question attempt the model will process.
 251       * @param string $preferredbehaviour the preferred behaviour for the containing usage.
 252       * @return question_behaviour_type an instance of appropriate behaviour class.
 253       */
 254      public static function get_behaviour_type($behaviour) {
 255  
 256          if (array_key_exists($behaviour, self::$behaviourtypes)) {
 257              return self::$behaviourtypes[$behaviour];
 258          }
 259  
 260          self::load_behaviour_type_class($behaviour);
 261  
 262          $class = 'qbehaviour_' . $behaviour . '_type';
 263          if (class_exists($class)) {
 264              self::$behaviourtypes[$behaviour] = new $class();
 265          } else {
 266              debugging('Question behaviour ' . $behaviour .
 267                      ' does not define the required class ' . $class . '.', DEBUG_DEVELOPER);
 268              self::$behaviourtypes[$behaviour] = new question_behaviour_type_fallback($behaviour);
 269          }
 270  
 271          return self::$behaviourtypes[$behaviour];
 272      }
 273  
 274      /**
 275       * Load the behaviour type class for a particular behaviour. That is,
 276       * include_once('/question/behaviour/' . $behaviour . '/behaviourtype.php').
 277       * @param string $behaviour the behaviour name. For example 'interactive' or 'deferredfeedback'.
 278       */
 279      protected static function load_behaviour_type_class($behaviour) {
 280          global $CFG;
 281          if (isset(self::$behaviourtypes[$behaviour])) {
 282              return;
 283          }
 284          $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviourtype.php';
 285          if (!is_readable($file)) {
 286              debugging('Question behaviour ' . $behaviour .
 287                      ' is missing the behaviourtype.php file.', DEBUG_DEVELOPER);
 288          }
 289          include_once($file);
 290      }
 291  
 292      /**
 293       * Return an array where the keys are the internal names of the archetypal
 294       * behaviours, and the values are a human-readable name. An
 295       * archetypal behaviour is one that is suitable to pass the name of to
 296       * {@link question_usage_by_activity::set_preferred_behaviour()}.
 297       *
 298       * @return array model name => lang string for this behaviour name.
 299       */
 300      public static function get_archetypal_behaviours() {
 301          $archetypes = array();
 302          $behaviours = core_component::get_plugin_list('qbehaviour');
 303          foreach ($behaviours as $behaviour => $notused) {
 304              if (self::is_behaviour_archetypal($behaviour)) {
 305                  $archetypes[$behaviour] = self::get_behaviour_name($behaviour);
 306              }
 307          }
 308          asort($archetypes, SORT_LOCALE_STRING);
 309          return $archetypes;
 310      }
 311  
 312      /**
 313       * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
 314       * @return bool whether this is an archetypal behaviour.
 315       */
 316      public static function is_behaviour_archetypal($behaviour) {
 317          return self::get_behaviour_type($behaviour)->is_archetypal();
 318      }
 319  
 320      /**
 321       * Return an array where the keys are the internal names of the behaviours
 322       * in preferred order and the values are a human-readable name.
 323       *
 324       * @param array $archetypes, array of behaviours
 325       * @param string $orderlist, a comma separated list of behaviour names
 326       * @param string $disabledlist, a comma separated list of behaviour names
 327       * @param string $current, current behaviour name
 328       * @return array model name => lang string for this behaviour name.
 329       */
 330      public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) {
 331  
 332          // Get disabled behaviours
 333          if ($disabledlist) {
 334              $disabled = explode(',', $disabledlist);
 335          } else {
 336              $disabled = array();
 337          }
 338  
 339          if ($orderlist) {
 340              $order = explode(',', $orderlist);
 341          } else {
 342              $order = array();
 343          }
 344  
 345          foreach ($disabled as $behaviour) {
 346              if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {
 347                  unset($archetypes[$behaviour]);
 348              }
 349          }
 350  
 351          // Get behaviours in preferred order
 352          $behaviourorder = array();
 353          foreach ($order as $behaviour) {
 354              if (array_key_exists($behaviour, $archetypes)) {
 355                  $behaviourorder[$behaviour] = $archetypes[$behaviour];
 356              }
 357          }
 358          // Get the rest of behaviours and sort them alphabetically
 359          $leftover = array_diff_key($archetypes, $behaviourorder);
 360          asort($leftover, SORT_LOCALE_STRING);
 361  
 362          // Set up the final order to be displayed
 363          return $behaviourorder + $leftover;
 364      }
 365  
 366      /**
 367       * Return an array where the keys are the internal names of the behaviours
 368       * in preferred order and the values are a human-readable name.
 369       *
 370       * @param string $currentbehaviour
 371       * @return array model name => lang string for this behaviour name.
 372       */
 373      public static function get_behaviour_options($currentbehaviour) {
 374          $config = question_bank::get_config();
 375          $archetypes = self::get_archetypal_behaviours();
 376  
 377          // If no admin setting return all behavious
 378          if (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) {
 379              return $archetypes;
 380          }
 381  
 382          if (empty($config->behavioursortorder)) {
 383              $order = '';
 384          } else {
 385              $order = $config->behavioursortorder;
 386          }
 387          if (empty($config->disabledbehaviours)) {
 388              $disabled = '';
 389          } else {
 390              $disabled = $config->disabledbehaviours;
 391          }
 392  
 393          return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);
 394      }
 395  
 396      /**
 397       * Get the translated name of a behaviour, for display in the UI.
 398       * @param string $behaviour the internal name of the model.
 399       * @return string name from the current language pack.
 400       */
 401      public static function get_behaviour_name($behaviour) {
 402          return get_string('pluginname', 'qbehaviour_' . $behaviour);
 403      }
 404  
 405      /**
 406       * @return array all the file area names that may contain response files.
 407       */
 408      public static function get_all_response_file_areas() {
 409          $variables = array();
 410          foreach (question_bank::get_all_qtypes() as $qtype) {
 411              $variables += $qtype->response_file_areas();
 412          }
 413  
 414          $areas = array();
 415          foreach (array_unique($variables) as $variable) {
 416              $areas[] = 'response_' . $variable;
 417          }
 418          return $areas;
 419      }
 420  
 421      /**
 422       * Returns the valid choices for the number of decimal places for showing
 423       * question marks. For use in the user interface.
 424       * @return array suitable for passing to {@link choose_from_menu()} or similar.
 425       */
 426      public static function get_dp_options() {
 427          return question_display_options::get_dp_options();
 428      }
 429  
 430      /**
 431       * Initialise the JavaScript required on pages where questions will be displayed.
 432       */
 433      public static function initialise_js() {
 434          return question_flags::initialise_js();
 435      }
 436  }
 437  
 438  
 439  /**
 440   * This class contains all the options that controls how a question is displayed.
 441   *
 442   * Normally, what will happen is that the calling code will set up some display
 443   * options to indicate what sort of question display it wants, and then before the
 444   * question is rendered, the behaviour will be given a chance to modify the
 445   * display options, so that, for example, A question that is finished will only
 446   * be shown read-only, and a question that has not been submitted will not have
 447   * any sort of feedback displayed.
 448   *
 449   * @copyright  2009 The Open University
 450   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 451   */
 452  class question_display_options {
 453      /**#@+ @var integer named constants for the values that most of the options take. */
 454      const HIDDEN = 0;
 455      const VISIBLE = 1;
 456      const EDITABLE = 2;
 457      /**#@-*/
 458  
 459      /**#@+ @var integer named constants for the {@link $marks} option. */
 460      const MAX_ONLY = 1;
 461      const MARK_AND_MAX = 2;
 462      /**#@-*/
 463  
 464      /**
 465       * @var integer maximum value for the {@link $markpd} option. This is
 466       * effectively set by the database structure, which uses NUMBER(12,7) columns
 467       * for question marks/fractions.
 468       */
 469      const MAX_DP = 7;
 470  
 471      /**
 472       * @var boolean whether the question should be displayed as a read-only review,
 473       * or in an active state where you can change the answer.
 474       */
 475      public $readonly = false;
 476  
 477      /**
 478       * @var boolean whether the question type should output hidden form fields
 479       * to reset any incorrect parts of the resonse to blank.
 480       */
 481      public $clearwrong = false;
 482  
 483      /**
 484       * Should the student have what they got right and wrong clearly indicated.
 485       * This includes the green/red hilighting of the bits of their response,
 486       * whether the one-line summary of the current state of the question says
 487       * correct/incorrect or just answered.
 488       * @var integer {@link question_display_options::HIDDEN} or
 489       * {@link question_display_options::VISIBLE}
 490       */
 491      public $correctness = self::VISIBLE;
 492  
 493      /**
 494       * The the mark and/or the maximum available mark for this question be visible?
 495       * @var integer {@link question_display_options::HIDDEN},
 496       * {@link question_display_options::MAX_ONLY} or {@link question_display_options::MARK_AND_MAX}
 497       */
 498      public $marks = self::MARK_AND_MAX;
 499  
 500      /** @var number of decimal places to use when formatting marks for output. */
 501      public $markdp = 2;
 502  
 503      /**
 504       * Should the flag this question UI element be visible, and if so, should the
 505       * flag state be changable?
 506       * @var integer {@link question_display_options::HIDDEN},
 507       * {@link question_display_options::VISIBLE} or {@link question_display_options::EDITABLE}
 508       */
 509      public $flags = self::VISIBLE;
 510  
 511      /**
 512       * Should the specific feedback be visible.
 513       * @var integer {@link question_display_options::HIDDEN} or
 514       * {@link question_display_options::VISIBLE}
 515       */
 516      public $feedback = self::VISIBLE;
 517  
 518      /**
 519       * For questions with a number of sub-parts (like matching, or
 520       * multiple-choice, multiple-reponse) display the number of sub-parts that
 521       * were correct.
 522       * @var integer {@link question_display_options::HIDDEN} or
 523       * {@link question_display_options::VISIBLE}
 524       */
 525      public $numpartscorrect = self::VISIBLE;
 526  
 527      /**
 528       * Should the general feedback be visible?
 529       * @var integer {@link question_display_options::HIDDEN} or
 530       * {@link question_display_options::VISIBLE}
 531       */
 532      public $generalfeedback = self::VISIBLE;
 533  
 534      /**
 535       * Should the automatically generated display of what the correct answer is
 536       * be visible?
 537       * @var integer {@link question_display_options::HIDDEN} or
 538       * {@link question_display_options::VISIBLE}
 539       */
 540      public $rightanswer = self::VISIBLE;
 541  
 542      /**
 543       * Should the manually added marker's comment be visible. Should the link for
 544       * adding/editing the comment be there.
 545       * @var integer {@link question_display_options::HIDDEN},
 546       * {@link question_display_options::VISIBLE}, or {@link question_display_options::EDITABLE}.
 547       * Editable means that form fields are displayed inline.
 548       */
 549      public $manualcomment = self::VISIBLE;
 550  
 551      /**
 552       * Should we show a 'Make comment or override grade' link?
 553       * @var string base URL for the edit comment script, which will be shown if
 554       * $manualcomment = self::VISIBLE.
 555       */
 556      public $manualcommentlink = null;
 557  
 558      /**
 559       * Used in places like the question history table, to show a link to review
 560       * this question in a certain state. If blank, a link is not shown.
 561       * @var string base URL for a review question script.
 562       */
 563      public $questionreviewlink = null;
 564  
 565      /**
 566       * Should the history of previous question states table be visible?
 567       * @var integer {@link question_display_options::HIDDEN} or
 568       * {@link question_display_options::VISIBLE}
 569       */
 570      public $history = self::HIDDEN;
 571  
 572      /**
 573       * If not empty, then a link to edit the question will be included in
 574       * the info box for the question.
 575       *
 576       * If used, this array must contain an element courseid or cmid.
 577       *
 578       * It shoudl also contain a parameter returnurl => moodle_url giving a
 579       * sensible URL to go back to when the editing form is submitted or cancelled.
 580       *
 581       * @var array url parameter for the edit link. id => questiosnid will be
 582       * added automatically.
 583       */
 584      public $editquestionparams = array();
 585  
 586      /**
 587       * @var int the context the attempt being output belongs to.
 588       */
 589      public $context;
 590  
 591      /**
 592       * Set all the feedback-related fields {@link $feedback}, {@link generalfeedback},
 593       * {@link rightanswer} and {@link manualcomment} to
 594       * {@link question_display_options::HIDDEN}.
 595       */
 596      public function hide_all_feedback() {
 597          $this->feedback = self::HIDDEN;
 598          $this->numpartscorrect = self::HIDDEN;
 599          $this->generalfeedback = self::HIDDEN;
 600          $this->rightanswer = self::HIDDEN;
 601          $this->manualcomment = self::HIDDEN;
 602          $this->correctness = self::HIDDEN;
 603      }
 604  
 605      /**
 606       * Returns the valid choices for the number of decimal places for showing
 607       * question marks. For use in the user interface.
 608       *
 609       * Calling code should probably use {@link question_engine::get_dp_options()}
 610       * rather than calling this method directly.
 611       *
 612       * @return array suitable for passing to {@link choose_from_menu()} or similar.
 613       */
 614      public static function get_dp_options() {
 615          $options = array();
 616          for ($i = 0; $i <= self::MAX_DP; $i += 1) {
 617              $options[$i] = $i;
 618          }
 619          return $options;
 620      }
 621  }
 622  
 623  
 624  /**
 625   * Contains the logic for handling question flags.
 626   *
 627   * @copyright  2010 The Open University
 628   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 629   */
 630  abstract class question_flags {
 631      /**
 632       * Get the checksum that validates that a toggle request is valid.
 633       * @param int $qubaid the question usage id.
 634       * @param int $questionid the question id.
 635       * @param int $sessionid the question_attempt id.
 636       * @param object $user the user. If null, defaults to $USER.
 637       * @return string that needs to be sent to question/toggleflag.php for it to work.
 638       */
 639      protected static function get_toggle_checksum($qubaid, $questionid,
 640              $qaid, $slot, $user = null) {
 641          if (is_null($user)) {
 642              global $USER;
 643              $user = $USER;
 644          }
 645          return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);
 646      }
 647  
 648      /**
 649       * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state.
 650       * You need to append &newstate=0/1 to this.
 651       * @return the post data to send.
 652       */
 653      public static function get_postdata(question_attempt $qa) {
 654          $qaid = $qa->get_database_id();
 655          $qubaid = $qa->get_usage_id();
 656          $qid = $qa->get_question()->id;
 657          $slot = $qa->get_slot();
 658          $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot);
 659          return "qaid={$qaid}&qubaid={$qubaid}&qid={$qid}&slot={$slot}&checksum={$checksum}&sesskey=" .
 660                  sesskey() . '&newstate=';
 661      }
 662  
 663      /**
 664       * If the request seems valid, update the flag state of a question attempt.
 665       * Throws exceptions if this is not a valid update request.
 666       * @param int $qubaid the question usage id.
 667       * @param int $questionid the question id.
 668       * @param int $sessionid the question_attempt id.
 669       * @param string $checksum checksum, as computed by {@link get_toggle_checksum()}
 670       *      corresponding to the last three arguments.
 671       * @param bool $newstate the new state of the flag. true = flagged.
 672       */
 673      public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {
 674          // Check the checksum - it is very hard to know who a question session belongs
 675          // to, so we require that checksum parameter is matches an md5 hash of the
 676          // three ids and the users username. Since we are only updating a flag, that
 677          // probably makes it sufficiently difficult for malicious users to toggle
 678          // other users flags.
 679          if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {
 680              throw new moodle_exception('errorsavingflags', 'question');
 681          }
 682  
 683          $dm = new question_engine_data_mapper();
 684          $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
 685      }
 686  
 687      public static function initialise_js() {
 688          global $CFG, $PAGE, $OUTPUT;
 689          static $done = false;
 690          if ($done) {
 691              return;
 692          }
 693          $module = array(
 694              'name' => 'core_question_flags',
 695              'fullpath' => '/question/flags.js',
 696              'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
 697          );
 698          $actionurl = $CFG->wwwroot . '/question/toggleflag.php';
 699          $flagtext = array(
 700              0 => get_string('clickflag', 'question'),
 701              1 => get_string('clickunflag', 'question')
 702          );
 703          $flagattributes = array(
 704              0 => array(
 705                  'src' => $OUTPUT->pix_url('i/unflagged') . '',
 706                  'title' => get_string('clicktoflag', 'question'),
 707                  'alt' => get_string('notflagged', 'question'),
 708                //  'text' => get_string('clickflag', 'question'),
 709              ),
 710              1 => array(
 711                  'src' => $OUTPUT->pix_url('i/flagged') . '',
 712                  'title' => get_string('clicktounflag', 'question'),
 713                  'alt' => get_string('flagged', 'question'),
 714                 // 'text' => get_string('clickunflag', 'question'),
 715              ),
 716          );
 717          $PAGE->requires->js_init_call('M.core_question_flags.init',
 718                  array($actionurl, $flagattributes, $flagtext), false, $module);
 719          $done = true;
 720      }
 721  }
 722  
 723  
 724  /**
 725   * Exception thrown when the system detects that a student has done something
 726   * out-of-order to a question. This can happen, for example, if they click
 727   * the browser's back button in a quiz, then try to submit a different response.
 728   *
 729   * @copyright  2010 The Open University
 730   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 731   */
 732  class question_out_of_sequence_exception extends moodle_exception {
 733      public function __construct($qubaid, $slot, $postdata) {
 734          if ($postdata == null) {
 735              $postdata = data_submitted();
 736          }
 737          parent::__construct('submissionoutofsequence', 'question', '', null,
 738                  "QUBAid: {$qubaid}, slot: {$slot}, post data: " . print_r($postdata, true));
 739      }
 740  }
 741  
 742  
 743  /**
 744   * Useful functions for writing question types and behaviours.
 745   *
 746   * @copyright 2010 The Open University
 747   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 748   */
 749  abstract class question_utils {
 750      /**
 751       * Tests to see whether two arrays have the same keys, with the same values
 752       * (as compared by ===) for each key. However, the order of the arrays does
 753       * not have to be the same.
 754       * @param array $array1 the first array.
 755       * @param array $array2 the second array.
 756       * @return bool whether the two arrays have the same keys with the same
 757       *      corresponding values.
 758       */
 759      public static function arrays_have_same_keys_and_values(array $array1, array $array2) {
 760          if (count($array1) != count($array2)) {
 761              return false;
 762          }
 763          foreach ($array1 as $key => $value1) {
 764              if (!array_key_exists($key, $array2)) {
 765                  return false;
 766              }
 767              if (((string) $value1) !== ((string) $array2[$key])) {
 768                  return false;
 769              }
 770          }
 771          return true;
 772      }
 773  
 774      /**
 775       * Tests to see whether two arrays have the same value at a particular key.
 776       * This method will return true if:
 777       * 1. Neither array contains the key; or
 778       * 2. Both arrays contain the key, and the corresponding values compare
 779       *      identical when cast to strings and compared with ===.
 780       * @param array $array1 the first array.
 781       * @param array $array2 the second array.
 782       * @param string $key an array key.
 783       * @return bool whether the two arrays have the same value (or lack of
 784       *      one) for a given key.
 785       */
 786      public static function arrays_same_at_key(array $array1, array $array2, $key) {
 787          if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) {
 788              return ((string) $array1[$key]) === ((string) $array2[$key]);
 789          }
 790          if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) {
 791              return true;
 792          }
 793          return false;
 794      }
 795  
 796      /**
 797       * Tests to see whether two arrays have the same value at a particular key.
 798       * Missing values are replaced by '', and then the values are cast to
 799       * strings and compared with ===.
 800       * @param array $array1 the first array.
 801       * @param array $array2 the second array.
 802       * @param string $key an array key.
 803       * @return bool whether the two arrays have the same value (or lack of
 804       *      one) for a given key.
 805       */
 806      public static function arrays_same_at_key_missing_is_blank(
 807              array $array1, array $array2, $key) {
 808          if (array_key_exists($key, $array1)) {
 809              $value1 = $array1[$key];
 810          } else {
 811              $value1 = '';
 812          }
 813          if (array_key_exists($key, $array2)) {
 814              $value2 = $array2[$key];
 815          } else {
 816              $value2 = '';
 817          }
 818          return ((string) $value1) === ((string) $value2);
 819      }
 820  
 821      /**
 822       * Tests to see whether two arrays have the same value at a particular key.
 823       * Missing values are replaced by 0, and then the values are cast to
 824       * integers and compared with ===.
 825       * @param array $array1 the first array.
 826       * @param array $array2 the second array.
 827       * @param string $key an array key.
 828       * @return bool whether the two arrays have the same value (or lack of
 829       *      one) for a given key.
 830       */
 831      public static function arrays_same_at_key_integer(
 832              array $array1, array $array2, $key) {
 833          if (array_key_exists($key, $array1)) {
 834              $value1 = (int) $array1[$key];
 835          } else {
 836              $value1 = 0;
 837          }
 838          if (array_key_exists($key, $array2)) {
 839              $value2 = (int) $array2[$key];
 840          } else {
 841              $value2 = 0;
 842          }
 843          return $value1 === $value2;
 844      }
 845  
 846      private static $units     = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix');
 847      private static $tens      = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc');
 848      private static $hundreds  = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm');
 849      private static $thousands = array('', 'm', 'mm', 'mmm');
 850  
 851      /**
 852       * Convert an integer to roman numerals.
 853       * @param int $number an integer between 1 and 3999 inclusive. Anything else
 854       *      will throw an exception.
 855       * @return string the number converted to lower case roman numerals.
 856       */
 857      public static function int_to_roman($number) {
 858          if (!is_integer($number) || $number < 1 || $number > 3999) {
 859              throw new coding_exception('Only integers between 0 and 3999 can be ' .
 860                      'converted to roman numerals.', $number);
 861          }
 862  
 863          return self::$thousands[$number / 1000 % 10] . self::$hundreds[$number / 100 % 10] .
 864                  self::$tens[$number / 10 % 10] . self::$units[$number % 10];
 865      }
 866  
 867      /**
 868       * Typically, $mark will have come from optional_param($name, null, PARAM_RAW_TRIMMED).
 869       * This method copes with:
 870       *  - keeping null or '' input unchanged.
 871       *  - nubmers that were typed as either 1.00 or 1,00 form.
 872       *
 873       * @param string|null $mark raw use input of a mark.
 874       * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
 875       */
 876      public static function clean_param_mark($mark) {
 877          if ($mark === '' || is_null($mark)) {
 878              return $mark;
 879          }
 880  
 881          return clean_param(str_replace(',', '.', $mark), PARAM_FLOAT);
 882      }
 883  
 884      /**
 885       * Get a sumitted variable (from the GET or POST data) that is a mark.
 886       * @param string $parname the submitted variable name.
 887       * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
 888       */
 889      public static function optional_param_mark($parname) {
 890          return self::clean_param_mark(
 891                  optional_param($parname, null, PARAM_RAW_TRIMMED));
 892      }
 893  
 894      /**
 895       * Convert part of some question content to plain text.
 896       * @param string $text the text.
 897       * @param int $format the text format.
 898       * @param array $options formatting options. Passed to {@link format_text}.
 899       * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
 900       */
 901      public static function to_plain_text($text, $format, $options = array('noclean' => 'true')) {
 902          // The following call to html_to_text uses the option that strips out
 903          // all URLs, but format_text complains if it finds @@PLUGINFILE@@ tokens.
 904          // So, we need to replace @@PLUGINFILE@@ with a real URL, but it doesn't
 905          // matter what. We use http://example.com/.
 906          $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $text);
 907          return html_to_text(format_text($text, $format, $options), 0, false);
 908      }
 909  }
 910  
 911  
 912  /**
 913   * The interface for strategies for controlling which variant of each question is used.
 914   *
 915   * @copyright  2011 The Open University
 916   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 917   */
 918  interface question_variant_selection_strategy {
 919      /**
 920       * @param int $maxvariants the num
 921       * @param string $seed data that can be used to controls how the variant is selected
 922       *      in a semi-random way.
 923       * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive.
 924       */
 925      public function choose_variant($maxvariants, $seed);
 926  }
 927  
 928  
 929  /**
 930   * A {@link question_variant_selection_strategy} that is completely random.
 931   *
 932   * @copyright  2011 The Open University
 933   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 934   */
 935  class question_variant_random_strategy implements question_variant_selection_strategy {
 936      public function choose_variant($maxvariants, $seed) {
 937          return rand(1, $maxvariants);
 938      }
 939  }
 940  
 941  
 942  /**
 943   * A {@link question_variant_selection_strategy} that is effectively random
 944   * for the first attempt, and then after that cycles through the available
 945   * variants so that the students will not get a repeated variant until they have
 946   * seen them all.
 947   *
 948   * @copyright  2011 The Open University
 949   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 950   */
 951  class question_variant_pseudorandom_no_repeats_strategy
 952          implements question_variant_selection_strategy {
 953  
 954      /** @var int the number of attempts this users has had, including the curent one. */
 955      protected $attemptno;
 956  
 957      /** @var int the user id the attempt belongs to. */
 958      protected $userid;
 959  
 960      /** @var string extra input fed into the pseudo-random code. */
 961      protected $extrarandomness = '';
 962  
 963      /**
 964       * Constructor.
 965       * @param int $attemptno The attempt number.
 966       * @param int $userid the user the attempt is for (defaults to $USER->id).
 967       */
 968      public function __construct($attemptno, $userid = null, $extrarandomness = '') {
 969          $this->attemptno = $attemptno;
 970          if (is_null($userid)) {
 971              global $USER;
 972              $this->userid = $USER->id;
 973          } else {
 974              $this->userid = $userid;
 975          }
 976  
 977          if ($extrarandomness) {
 978              $this->extrarandomness = '|' . $extrarandomness;
 979          }
 980      }
 981  
 982      public function choose_variant($maxvariants, $seed) {
 983          if ($maxvariants == 1) {
 984              return 1;
 985          }
 986  
 987          $hash = sha1($seed . '|user' . $this->userid . $this->extrarandomness);
 988          $randint = hexdec(substr($hash, 17, 7));
 989  
 990          return ($randint + $this->attemptno) % $maxvariants + 1;
 991      }
 992  }
 993  
 994  /**
 995   * A {@link question_variant_selection_strategy} designed ONLY for testing.
 996   * For selected questions it wil return a specific variants. For the other
 997   * slots it will use a fallback strategy.
 998   *
 999   * @copyright  2013 The Open University
1000   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1001   */
1002  class question_variant_forced_choices_selection_strategy
1003      implements question_variant_selection_strategy {
1004  
1005      /** @var array seed => variant to select. */
1006      protected $forcedchoices;
1007  
1008      /** @var question_variant_selection_strategy strategy used to make the non-forced choices. */
1009      protected $basestrategy;
1010  
1011      /**
1012       * Constructor.
1013       * @param array $forcedchoices array seed => variant to select.
1014       * @param question_variant_selection_strategy $basestrategy strategy used
1015       *      to make the non-forced choices.
1016       */
1017      public function __construct(array $forcedchoices, question_variant_selection_strategy $basestrategy) {
1018          $this->forcedchoices = $forcedchoices;
1019          $this->basestrategy  = $basestrategy;
1020      }
1021  
1022      public function choose_variant($maxvariants, $seed) {
1023          if (array_key_exists($seed, $this->forcedchoices)) {
1024              if ($this->forcedchoices[$seed] > $maxvariants) {
1025                  throw new coding_exception('Forced variant out of range.');
1026              }
1027              return $this->forcedchoices[$seed];
1028          } else {
1029              return $this->basestrategy->choose_variant($maxvariants, $seed);
1030          }
1031      }
1032  
1033      /**
1034       * Helper method for preparing the $forcedchoices array.
1035       * @param array                      $variantsbyslot slot number => variant to select.
1036       * @param question_usage_by_activity $quba           the question usage we need a strategy for.
1037       * @throws coding_exception when variant cannot be forced as doesn't work.
1038       * @return array that can be passed to the constructor as $forcedchoices.
1039       */
1040      public static function prepare_forced_choices_array(array $variantsbyslot,
1041                                                          question_usage_by_activity $quba) {
1042  
1043          $forcedchoices = array();
1044  
1045          foreach ($variantsbyslot as $slot => $varianttochoose) {
1046              $question = $quba->get_question($slot);
1047              $seed = $question->get_variants_selection_seed();
1048              if (array_key_exists($seed, $forcedchoices) && $forcedchoices[$seed] != $varianttochoose) {
1049                  throw new coding_exception('Inconsistent forced variant detected at slot ' . $slot);
1050              }
1051              if ($varianttochoose > $question->get_num_variants()) {
1052                  throw new coding_exception('Forced variant out of range at slot ' . $slot);
1053              }
1054              $forcedchoices[$seed] = $varianttochoose;
1055          }
1056          return $forcedchoices;
1057      }
1058  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1