[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |