[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/question/classes/bank/ -> random_question_loader.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   * A class for efficiently finds questions at random from the question bank.
  19   *
  20   * @package   core_question
  21   * @copyright 2015 The Open University
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_question\bank;
  26  
  27  
  28  /**
  29   * This class efficiently finds questions at random from the question bank.
  30   *
  31   * You can ask for questions at random one at a time. Each time you ask, you
  32   * pass a category id, and whether to pick from that category and all subcategories
  33   * or just that category.
  34   *
  35   * The number of teams each question has been used is tracked, and we will always
  36   * return a question from among those elegible that has been used the fewest times.
  37   * So, if there are questions that have not been used yet in the category asked for,
  38   * one of those will be returned. However, within one instantiation of this class,
  39   * we will never return a given question more than once, and we will never return
  40   * questions passed into the constructor as $usedquestions.
  41   *
  42   * @copyright 2015 The Open University
  43   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  class random_question_loader {
  46      /** @var \qubaid_condition which usages to consider previous attempts from. */
  47      protected $qubaids;
  48  
  49      /** @var array qtypes that cannot be used by random questions. */
  50      protected $excludedqtypes;
  51  
  52      /** @var array categoryid & include subcategories => num previous uses => questionid => 1. */
  53      protected $availablequestionscache = array();
  54  
  55      /**
  56       * @var array questionid => num recent uses. Questions that have been used,
  57       * but that is not yet recorded in the DB.
  58       */
  59      protected $recentlyusedquestions;
  60  
  61      /**
  62       * Constructor.
  63       * @param \qubaid_condition $qubaids the usages to consider when counting previous uses of each question.
  64       * @param array $usedquestions questionid => number of times used count. If we should allow for
  65       *      further existing uses of a question in addition to the ones in $qubaids.
  66       */
  67      public function __construct(\qubaid_condition $qubaids, array $usedquestions = array()) {
  68          $this->qubaids = $qubaids;
  69          $this->recentlyusedquestions = $usedquestions;
  70  
  71          foreach (\question_bank::get_all_qtypes() as $qtype) {
  72              if (!$qtype->is_usable_by_random()) {
  73                  $this->excludedqtypes[] = $qtype->name();
  74              }
  75          }
  76      }
  77  
  78      /**
  79       * Pick a question at random from the given category, from among those with the fewest uses.
  80       *
  81       * It is up the the caller to verify that the cateogry exists. An unknown category
  82       * behaves like an empty one.
  83       *
  84       * @param int $categoryid the id of a category in the question bank.
  85       * @param bool $includesubcategories wether to pick a question from exactly
  86       *      that category, or that category and subcategories.
  87       * @return int|null the id of the question picked, or null if there aren't any.
  88       */
  89      public function get_next_question_id($categoryid, $includesubcategories) {
  90          $this->ensure_questions_for_category_loaded($categoryid, $includesubcategories);
  91  
  92          $categorykey = $this->get_category_key($categoryid, $includesubcategories);
  93          if (empty($this->availablequestionscache[$categorykey])) {
  94              return null;
  95          }
  96  
  97          reset($this->availablequestionscache[$categorykey]);
  98          $lowestcount = key($this->availablequestionscache[$categorykey]);
  99          reset($this->availablequestionscache[$categorykey][$lowestcount]);
 100          $questionid = key($this->availablequestionscache[$categorykey][$lowestcount]);
 101          $this->use_question($questionid);
 102          return $questionid;
 103      }
 104  
 105      /**
 106       * Get the key into {@link $availablequestionscache} for this combination of options.
 107       * @param int $categoryid the id of a category in the question bank.
 108       * @param bool $includesubcategories wether to pick a question from exactly
 109       *      that category, or that category and subcategories.
 110       * @return string the cache key.
 111       */
 112      protected function get_category_key($categoryid, $includesubcategories) {
 113          if ($includesubcategories) {
 114              return $categoryid . '|1';
 115          } else {
 116              return $categoryid . '|0';
 117          }
 118      }
 119  
 120      /**
 121       * Populate {@link $availablequestionscache} for this combination of options.
 122       * @param int $categoryid the id of a category in the question bank.
 123       * @param bool $includesubcategories wether to pick a question from exactly
 124       *      that category, or that category and subcategories.
 125       */
 126      protected function ensure_questions_for_category_loaded($categoryid, $includesubcategories) {
 127          global $DB;
 128  
 129          $categorykey = $this->get_category_key($categoryid, $includesubcategories);
 130  
 131          if (isset($this->availablequestionscache[$categorykey])) {
 132              // Data is already in the cache, nothing to do.
 133              return;
 134          }
 135  
 136          // Load the available questions from the question bank.
 137          if ($includesubcategories) {
 138              $categoryids = question_categorylist($categoryid);
 139          } else {
 140              $categoryids = array($categoryid);
 141          }
 142  
 143          list($extraconditions, $extraparams) = $DB->get_in_or_equal($this->excludedqtypes,
 144                  SQL_PARAMS_NAMED, 'excludedqtype', false);
 145  
 146          $questionidsandcounts = \question_bank::get_finder()->get_questions_from_categories_with_usage_counts(
 147                  $categoryids, $this->qubaids, 'q.qtype ' . $extraconditions, $extraparams);
 148          if (!$questionidsandcounts) {
 149              // No questions in this category.
 150              $this->availablequestionscache[$categorykey] = array();
 151              return;
 152          }
 153  
 154          // Put all the questions with each value of $prevusecount in separate arrays.
 155          $idsbyusecount = array();
 156          foreach ($questionidsandcounts as $questionid => $prevusecount) {
 157              if (isset($this->recentlyusedquestions[$questionid])) {
 158                  // Recently used questions are never returned.
 159                  continue;
 160              }
 161              $idsbyusecount[$prevusecount][] = $questionid;
 162          }
 163  
 164          // Now put that data into our cache. For each count, we need to shuffle
 165          // questionids, and make those the keys of an array.
 166          $this->availablequestionscache[$categorykey] = array();
 167          foreach ($idsbyusecount as $prevusecount => $questionids) {
 168              shuffle($questionids);
 169              $this->availablequestionscache[$categorykey][$prevusecount] = array_combine(
 170                      $questionids, array_fill(0, count($questionids), 1));
 171          }
 172          ksort($this->availablequestionscache[$categorykey]);
 173      }
 174  
 175      /**
 176       * Update the internal data structures to indicate that a given question has
 177       * been used one more time.
 178       *
 179       * @param int $questionid the question that is being used.
 180       */
 181      protected function use_question($questionid) {
 182          if (isset($this->recentlyusedquestions[$questionid])) {
 183              $this->recentlyusedquestions[$questionid] += 1;
 184          } else {
 185              $this->recentlyusedquestions[$questionid] = 1;
 186          }
 187  
 188          foreach ($this->availablequestionscache as $categorykey => $questionsforcategory) {
 189              foreach ($questionsforcategory as $numuses => $questionids) {
 190                  if (!isset($questionids[$questionid])) {
 191                      continue;
 192                  }
 193                  unset($this->availablequestionscache[$categorykey][$numuses][$questionid]);
 194                  if (empty($this->availablequestionscache[$categorykey][$numuses])) {
 195                      unset($this->availablequestionscache[$categorykey][$numuses]);
 196                  }
 197              }
 198          }
 199      }
 200  
 201      /**
 202       * Check whether a given question is available in a given category. If so, mark it used.
 203       *
 204       * @param int $categoryid the id of a category in the question bank.
 205       * @param bool $includesubcategories wether to pick a question from exactly
 206       *      that category, or that category and subcategories.
 207       * @param int $questionid the question that is being used.
 208       * @return bool whether the question is available in the requested category.
 209       */
 210      public function is_question_available($categoryid, $includesubcategories, $questionid) {
 211          $this->ensure_questions_for_category_loaded($categoryid, $includesubcategories);
 212          $categorykey = $this->get_category_key($categoryid, $includesubcategories);
 213  
 214          foreach ($this->availablequestionscache[$categorykey] as $questionids) {
 215              if (isset($questionids[$questionid])) {
 216                  $this->use_question($questionid);
 217                  return true;
 218              }
 219          }
 220  
 221          return false;
 222      }
 223  }


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