Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 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   * A collection of all the question statistics calculated for an activity instance ie. the stats calculated for slots and
  19   * sub-questions and variants of those questions.
  20   *
  21   * @package    core_question
  22   * @copyright  2014 The Open University
  23   * @author     James Pratt me@jamiep.org
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  namespace core_question\statistics\questions;
  28  
  29  /**
  30   * A collection of all the question statistics calculated for an activity instance.
  31   *
  32   * @package    core_question
  33   * @copyright  2014 The Open University
  34   * @author     James Pratt me@jamiep.org
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class all_calculated_for_qubaid_condition {
  38  
  39      /** @var int Time after which statistics are automatically recomputed. */
  40      const TIME_TO_CACHE = 900; // 15 minutes.
  41  
  42      /**
  43       * @var object[]
  44       */
  45      public $subquestions;
  46  
  47      /**
  48       * Holds slot (position) stats and stats for variants of questions in slots.
  49       *
  50       * @var calculated[]
  51       */
  52      public $questionstats = array();
  53  
  54      /**
  55       * Holds sub-question stats and stats for variants of subqs.
  56       *
  57       * @var calculated_for_subquestion[]
  58       */
  59      public $subquestionstats = array();
  60  
  61      /**
  62       * Set up a calculated_for_subquestion instance ready to store a randomly selected question's stats.
  63       *
  64       * @param object     $step
  65       * @param int|null   $variant Is this to keep track of a variant's stats? If so what is the variant, if not null.
  66       */
  67      public function initialise_for_subq($step, $variant = null) {
  68          $newsubqstat = new calculated_for_subquestion($step, $variant);
  69          if ($variant === null) {
  70              $this->subquestionstats[$step->questionid] = $newsubqstat;
  71          } else {
  72              $this->subquestionstats[$step->questionid]->variantstats[$variant] = $newsubqstat;
  73          }
  74      }
  75  
  76      /**
  77       * Set up a calculated instance ready to store a slot question's stats.
  78       *
  79       * @param int      $slot
  80       * @param object   $question
  81       * @param int|null $variant Is this to keep track of a variant's stats? If so what is the variant, if not null.
  82       */
  83      public function initialise_for_slot($slot, $question, $variant = null) {
  84          $newqstat = new calculated($question, $slot, $variant);
  85          if ($variant === null) {
  86              $this->questionstats[$slot] = $newqstat;
  87          } else {
  88              $this->questionstats[$slot]->variantstats[$variant] = $newqstat;
  89          }
  90      }
  91  
  92      /**
  93       * Do we have stats for a particular quesitonid (and optionally variant)?
  94       *
  95       * @param int  $questionid The id of the sub question.
  96       * @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats.
  97       * @return bool whether those stats exist (yet).
  98       */
  99      public function has_subq($questionid, $variant = null) {
 100          if ($variant === null) {
 101              return isset($this->subquestionstats[$questionid]);
 102          } else {
 103              return isset($this->subquestionstats[$questionid]->variantstats[$variant]);
 104          }
 105      }
 106  
 107      /**
 108       * Reference for a item stats instance for a questionid and optional variant no.
 109       *
 110       * @param int  $questionid The id of the sub question.
 111       * @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats.
 112       * @return calculated|calculated_for_subquestion stats instance for a questionid and optional variant no.
 113       *     Will be a calculated_for_subquestion if no variant specified.
 114       * @throws \coding_exception if there is an attempt to respond to a non-existant set of stats.
 115       */
 116      public function for_subq($questionid, $variant = null) {
 117          if ($variant === null) {
 118              if (!isset($this->subquestionstats[$questionid])) {
 119                  throw new \coding_exception('Reference to unknown question id ' . $questionid);
 120              } else {
 121                  return $this->subquestionstats[$questionid];
 122              }
 123          } else {
 124              if (!isset($this->subquestionstats[$questionid]->variantstats[$variant])) {
 125                  throw new \coding_exception('Reference to unknown question id ' . $questionid .
 126                          ' variant ' . $variant);
 127              } else {
 128                  return $this->subquestionstats[$questionid]->variantstats[$variant];
 129              }
 130          }
 131      }
 132  
 133      /**
 134       * ids of all randomly selected question for all slots.
 135       *
 136       * @return int[] An array of all sub-question ids.
 137       */
 138      public function get_all_subq_ids() {
 139          return array_keys($this->subquestionstats);
 140      }
 141  
 142      /**
 143       * All slots nos that stats have been calculated for.
 144       *
 145       * @return int[] An array of all slot nos.
 146       */
 147      public function get_all_slots() {
 148          return array_keys($this->questionstats);
 149      }
 150  
 151      /**
 152       * Do we have stats for a particular slot (and optionally variant)?
 153       *
 154       * @param int  $slot The slot no.
 155       * @param int|null $variant if provided then we want the object which stores a variant of a position's stats.
 156       * @return bool whether those stats exist (yet).
 157       */
 158      public function has_slot($slot, $variant = null) {
 159          if ($variant === null) {
 160              return isset($this->questionstats[$slot]);
 161          } else {
 162              return isset($this->questionstats[$slot]->variantstats[$variant]);
 163          }
 164      }
 165  
 166      /**
 167       * Get position stats instance for a slot and optional variant no.
 168       *
 169       * @param int  $slot The slot no.
 170       * @param int|null $variant if provided then we want the object which stores a variant of a position's stats.
 171       * @return calculated|calculated_for_subquestion An instance of the class storing the calculated position stats.
 172       * @throws \coding_exception if there is an attempt to respond to a non-existant set of stats.
 173       */
 174      public function for_slot($slot, $variant = null) {
 175          if ($variant === null) {
 176              if (!isset($this->questionstats[$slot])) {
 177                  throw new \coding_exception('Reference to unknown slot ' . $slot);
 178              } else {
 179                  return $this->questionstats[$slot];
 180              }
 181          } else {
 182              if (!isset($this->questionstats[$slot]->variantstats[$variant])) {
 183                  throw new \coding_exception('Reference to unknown slot ' . $slot . ' variant ' . $variant);
 184              } else {
 185                  return $this->questionstats[$slot]->variantstats[$variant];
 186              }
 187          }
 188      }
 189  
 190      /**
 191       * Load cached statistics from the database.
 192       *
 193       * @param \qubaid_condition $qubaids Which question usages to load stats for?
 194       */
 195      public function get_cached($qubaids) {
 196          global $DB;
 197  
 198          $timemodified = time() - self::TIME_TO_CACHE;
 199          $questionstatrecs = $DB->get_records_select('question_statistics', 'hashcode = ? AND timemodified > ?',
 200                                                      array($qubaids->get_hash_code(), $timemodified));
 201  
 202          $questionids = array();
 203          foreach ($questionstatrecs as $fromdb) {
 204              if (is_null($fromdb->variant) && !$fromdb->slot) {
 205                  $questionids[] = $fromdb->questionid;
 206              }
 207          }
 208          $this->subquestions = question_load_questions($questionids);
 209          foreach ($questionstatrecs as $fromdb) {
 210              if (is_null($fromdb->variant)) {
 211                  if ($fromdb->slot) {
 212                      $this->questionstats[$fromdb->slot]->populate_from_record($fromdb);
 213                      // Array created in constructor and populated from question.
 214                  } else {
 215                      $this->subquestionstats[$fromdb->questionid] = new calculated_for_subquestion();
 216                      $this->subquestionstats[$fromdb->questionid]->populate_from_record($fromdb);
 217                      $this->subquestionstats[$fromdb->questionid]->question = $this->subquestions[$fromdb->questionid];
 218                  }
 219              }
 220          }
 221          // Add cached variant stats to data structure.
 222          foreach ($questionstatrecs as $fromdb) {
 223              if (!is_null($fromdb->variant)) {
 224                  if ($fromdb->slot) {
 225                      $newcalcinstance = new calculated();
 226                      $this->questionstats[$fromdb->slot]->variantstats[$fromdb->variant] = $newcalcinstance;
 227                      $newcalcinstance->question = $this->questionstats[$fromdb->slot]->question;
 228                  } else {
 229                      $newcalcinstance = new calculated_for_subquestion();
 230                      $this->subquestionstats[$fromdb->questionid]->variantstats[$fromdb->variant] = $newcalcinstance;
 231                      $newcalcinstance->question = $this->subquestions[$fromdb->questionid];
 232                  }
 233                  $newcalcinstance->populate_from_record($fromdb);
 234              }
 235          }
 236      }
 237  
 238      /**
 239       * Find time of non-expired statistics in the database.
 240       *
 241       * @param \qubaid_condition $qubaids Which question usages to look for stats for?
 242       * @return int|bool Time of cached record that matches this qubaid_condition or false if non found.
 243       */
 244      public function get_last_calculated_time($qubaids) {
 245          global $DB;
 246  
 247          $timemodified = time() - self::TIME_TO_CACHE;
 248          return $DB->get_field_select('question_statistics', 'timemodified', 'hashcode = ? AND timemodified > ?',
 249                                       array($qubaids->get_hash_code(), $timemodified), IGNORE_MULTIPLE);
 250      }
 251  
 252      /**
 253       * Save stats to db.
 254       *
 255       * @param \qubaid_condition $qubaids Which question usages are we caching the stats of?
 256       */
 257      public function cache($qubaids) {
 258          foreach ($this->get_all_slots() as $slot) {
 259              $this->for_slot($slot)->cache($qubaids);
 260          }
 261  
 262          foreach ($this->get_all_subq_ids() as $subqid) {
 263              $this->for_subq($subqid)->cache($qubaids);
 264          }
 265      }
 266  
 267      /**
 268       * Return all sub-questions used.
 269       *
 270       * @return \object[] array of questions.
 271       */
 272      public function get_sub_questions() {
 273          return $this->subquestions;
 274      }
 275  
 276      /**
 277       * Return all stats for one slot, stats for the slot itself, and either :
 278       *  - variants of question
 279       *  - variants of randomly selected questions
 280       *  - randomly selected questions
 281       *
 282       * @param int      $slot          the slot no
 283       * @param bool|int $limitvariants limit number of variants and sub-questions displayed?
 284       * @return calculated|calculated_for_subquestion[] stats to display
 285       */
 286      public function structure_analysis_for_one_slot($slot, $limitvariants = false) {
 287          return array_merge(array($this->for_slot($slot)), $this->all_subq_and_variant_stats_for_slot($slot, $limitvariants));
 288      }
 289  
 290      /**
 291       * Call after calculations to output any error messages.
 292       *
 293       * @return string[] Array of strings describing error messages found during stats calculation.
 294       */
 295      public function any_error_messages() {
 296          $errors = array();
 297          foreach ($this->get_all_slots() as $slot) {
 298              foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
 299                  if ($this->for_subq($subqid)->differentweights) {
 300                      $name = $this->for_subq($subqid)->question->name;
 301                      $errors[] = get_string('erroritemappearsmorethanoncewithdifferentweight', 'question', $name);
 302                  }
 303              }
 304          }
 305          return $errors;
 306      }
 307  
 308      /**
 309       * Return all stats for variants of question in slot $slot.
 310       *
 311       * @param int $slot The slot no.
 312       * @return calculated[] The instances storing the calculated stats.
 313       */
 314      protected function all_variant_stats_for_one_slot($slot) {
 315          $toreturn = array();
 316          foreach ($this->for_slot($slot)->get_variants() as $variant) {
 317              $toreturn[] = $this->for_slot($slot, $variant);
 318          }
 319          return $toreturn;
 320      }
 321  
 322      /**
 323       * Return all stats for variants of randomly selected questions for one slot $slot.
 324       *
 325       * @param int $slot The slot no.
 326       * @return calculated[] The instances storing the calculated stats.
 327       */
 328      protected function all_subq_variants_for_one_slot($slot) {
 329          $toreturn = array();
 330          $displayorder = 1;
 331          foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
 332              if ($variants = $this->for_subq($subqid)->get_variants()) {
 333                  foreach ($variants as $variant) {
 334                      $toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid, $variant);
 335                  }
 336              }
 337              $displayorder++;
 338          }
 339          return $toreturn;
 340      }
 341  
 342      /**
 343       * Return all stats for randomly selected questions for one slot $slot.
 344       *
 345       * @param int $slot The slot no.
 346       * @return calculated[] The instances storing the calculated stats.
 347       */
 348      protected function all_subqs_for_one_slot($slot) {
 349          $displayorder = 1;
 350          $toreturn = array();
 351          foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
 352              $toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid);
 353              $displayorder++;
 354          }
 355          return $toreturn;
 356      }
 357  
 358      /**
 359       * Return all variant or 'sub-question' stats one slot, either :
 360       *  - variants of question
 361       *  - variants of randomly selected questions
 362       *  - randomly selected questions
 363       *
 364       * @param int $slot the slot no
 365       * @param bool $limited limit number of variants and sub-questions displayed?
 366       * @return calculated|calculated_for_subquestion|calculated_question_summary[] stats to display
 367       */
 368      protected function all_subq_and_variant_stats_for_slot($slot, $limited) {
 369          // Random question in this slot?
 370          if ($this->for_slot($slot)->get_sub_question_ids()) {
 371              $toreturn = array();
 372  
 373              if ($limited) {
 374                  $randomquestioncalculated = $this->for_slot($slot);
 375  
 376                  if ($subqvariantstats = $this->all_subq_variants_for_one_slot($slot)) {
 377                      // There are some variants from randomly selected questions.
 378                      // If we're showing a limited view of the statistics then add a question summary stat
 379                      // rather than a stat for each subquestion.
 380                      $summarystat = $this->make_new_calculated_question_summary_stat($randomquestioncalculated, $subqvariantstats);
 381  
 382                      $toreturn = array_merge($toreturn, [$summarystat]);
 383                  }
 384  
 385                  if ($subqstats = $this->all_subqs_for_one_slot($slot)) {
 386                      // There are some randomly selected questions.
 387                      // If we're showing a limited view of the statistics then add a question summary stat
 388                      // rather than a stat for each subquestion.
 389                      $summarystat = $this->make_new_calculated_question_summary_stat($randomquestioncalculated, $subqstats);
 390  
 391                      $toreturn = array_merge($toreturn, [$summarystat]);
 392                  }
 393  
 394                  foreach ($toreturn as $index => $calculated) {
 395                      $calculated->subqdisplayorder = $index;
 396                  }
 397              } else {
 398                  $displaynumber = 1;
 399                  foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
 400                      $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid);
 401                      if ($variants = $this->for_subq($subqid)->get_variants()) {
 402                          foreach ($variants as $variant) {
 403                              $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant);
 404                          }
 405                      }
 406                      $displaynumber++;
 407                  }
 408              }
 409  
 410              return $toreturn;
 411          } else {
 412              $variantstats = $this->all_variant_stats_for_one_slot($slot);
 413              if ($limited && $variantstats) {
 414                  $variantquestioncalculated = $this->for_slot($slot);
 415  
 416                  // If we're showing a limited view of the statistics then add a question summary stat
 417                  // rather than a stat for each variation.
 418                  $summarystat = $this->make_new_calculated_question_summary_stat($variantquestioncalculated, $variantstats);
 419  
 420                  return [$summarystat];
 421              } else {
 422                  return $variantstats;
 423              }
 424          }
 425      }
 426  
 427      /**
 428       * We need a new object for display. Sub-question stats can appear more than once in different slots.
 429       * So we create a clone of the object and then we can set properties on the object that are per slot.
 430       *
 431       * @param int  $displaynumber                   The display number for this sub question.
 432       * @param int  $slot                            The slot number.
 433       * @param int  $subqid                          The sub question id.
 434       * @param null|int $variant                     The variant no.
 435       * @return calculated_for_subquestion           The object for display.
 436       */
 437      protected function make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant = null) {
 438          $slotstat = fullclone($this->for_subq($subqid, $variant));
 439          $slotstat->question->number = $this->for_slot($slot)->question->number;
 440          $slotstat->subqdisplayorder = $displaynumber;
 441          return $slotstat;
 442      }
 443  
 444      /**
 445       * Create a summary calculated object for a calculated question. This is used as a placeholder
 446       * to indicate that a calculated question has sub questions or variations to show rather than listing each
 447       * subquestion or variation directly.
 448       *
 449       * @param  calculated $randomquestioncalculated The calculated instance for the random question slot.
 450       * @param  calculated[] $subquestionstats The instances of the calculated stats of the questions that are being summarised.
 451       * @return calculated_question_summary
 452       */
 453      protected function make_new_calculated_question_summary_stat($randomquestioncalculated, $subquestionstats) {
 454          $question = $randomquestioncalculated->question;
 455          $slot = $randomquestioncalculated->slot;
 456          $calculatedsummary = new calculated_question_summary($question, $slot, $subquestionstats);
 457  
 458          return $calculatedsummary;
 459      }
 460  }