See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]
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 * Question type class for the random question type. 19 * 20 * @package qtype 21 * @subpackage random 22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 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->dirroot . '/question/type/questiontypebase.php'); 30 31 32 /** 33 * The random question type. 34 * 35 * This question type does not have a question definition class, nor any 36 * renderers. When you load a question of this type, it actually loads a 37 * question chosen randomly from a particular category in the question bank. 38 * 39 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 41 */ 42 class qtype_random extends question_type { 43 /** @var string comma-separated list of qytpe names not to select, can be used in SQL. */ 44 protected $excludedqtypes = null; 45 46 /** @var string comma-separated list of manually graded qytpe names, can be used in SQL. */ 47 protected $manualqtypes = null; 48 49 /** 50 * Cache of availabe question ids from a particular category. 51 * @var array two-dimensional array. The first key is a category id, the 52 * second key is wether subcategories should be included. 53 */ 54 private $availablequestionsbycategory = array(); 55 56 public function menu_name() { 57 // Don't include this question type in the 'add new question' menu. 58 return false; 59 } 60 61 public function is_manual_graded() { 62 return true; 63 } 64 65 public function is_usable_by_random() { 66 return false; 67 } 68 69 public function is_question_manual_graded($question, $otherquestionsinuse) { 70 global $DB; 71 // We take our best shot at working whether a particular question is manually 72 // graded follows: We look to see if any of the questions that this random 73 // question might select if of a manually graded type. If a category contains 74 // a mixture of manual and non-manual questions, and if all the attempts so 75 // far selected non-manual ones, this will give the wrong answer, but we 76 // don't care. Even so, this is an expensive calculation! 77 $this->init_qtype_lists(); 78 if (!$this->manualqtypes) { 79 return false; 80 } 81 if ($question->questiontext) { 82 $categorylist = question_categorylist($question->category); 83 } else { 84 $categorylist = array($question->category); 85 } 86 list($qcsql, $qcparams) = $DB->get_in_or_equal($categorylist); 87 // TODO use in_or_equal for $otherquestionsinuse and $this->manualqtypes. 88 89 $readystatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY; 90 $sql = "SELECT q.* 91 FROM {question} q 92 JOIN {question_versions} qv ON qv.questionid = q.id 93 JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid 94 WHERE qbe.questioncategoryid {$qcsql} 95 AND q.parent = 0 96 AND qv.status = '$readystatus' 97 AND q.id NOT IN ($otherquestionsinuse) 98 AND q.qtype IN ($this->manualqtypes)"; 99 100 return $DB->record_exists_sql($sql, $qcparams); 101 } 102 103 /** 104 * This method needs to be called before the ->excludedqtypes and 105 * ->manualqtypes fields can be used. 106 */ 107 protected function init_qtype_lists() { 108 if (!is_null($this->excludedqtypes)) { 109 return; // Already done. 110 } 111 $excludedqtypes = array(); 112 $manualqtypes = array(); 113 foreach (question_bank::get_all_qtypes() as $qtype) { 114 $quotedname = "'" . $qtype->name() . "'"; 115 if (!$qtype->is_usable_by_random()) { 116 $excludedqtypes[] = $quotedname; 117 } else if ($qtype->is_manual_graded()) { 118 $manualqtypes[] = $quotedname; 119 } 120 } 121 $this->excludedqtypes = implode(',', $excludedqtypes); 122 $this->manualqtypes = implode(',', $manualqtypes); 123 } 124 125 public function get_question_options($question) { 126 parent::get_question_options($question); 127 return true; 128 } 129 130 /** 131 * Random questions always get a question name that is Random (cateogryname). 132 * This function is a centralised place to calculate that, given the category. 133 * @param stdClass $category the category this question picks from. (->parent, ->name & ->contextid are used.) 134 * @param bool $includesubcategories whether this question also picks from subcategories. 135 * @param string[] $tagnames Name of tags this question picks from. 136 * @return string the name this question should have. 137 */ 138 public function question_name($category, $includesubcategories, $tagnames = []) { 139 $categoryname = ''; 140 if ($category->parent && $includesubcategories) { 141 $stringid = 'randomqplusname'; 142 $categoryname = shorten_text($category->name, 100); 143 } else if ($category->parent) { 144 $stringid = 'randomqname'; 145 $categoryname = shorten_text($category->name, 100); 146 } else if ($includesubcategories) { 147 $context = context::instance_by_id($category->contextid); 148 149 switch ($context->contextlevel) { 150 case CONTEXT_MODULE: 151 $stringid = 'randomqplusnamemodule'; 152 break; 153 case CONTEXT_COURSE: 154 $stringid = 'randomqplusnamecourse'; 155 break; 156 case CONTEXT_COURSECAT: 157 $stringid = 'randomqplusnamecoursecat'; 158 $categoryname = shorten_text($context->get_context_name(false), 100); 159 break; 160 case CONTEXT_SYSTEM: 161 $stringid = 'randomqplusnamesystem'; 162 break; 163 default: // Impossible. 164 } 165 } else { 166 // No question will ever be selected. So, let's warn the teacher. 167 $stringid = 'randomqnamefromtop'; 168 } 169 170 if ($tagnames) { 171 $stringid .= 'tags'; 172 $a = new stdClass(); 173 if ($categoryname) { 174 $a->category = $categoryname; 175 } 176 $a->tags = implode(', ', array_map(function($tagname) { 177 return explode(',', $tagname)[1]; 178 }, $tagnames)); 179 } else { 180 $a = $categoryname ? : null; 181 } 182 183 $name = get_string($stringid, 'qtype_random', $a); 184 185 return shorten_text($name, 255); 186 } 187 188 protected function set_selected_question_name($question, $randomname) { 189 $a = new stdClass(); 190 $a->randomname = $randomname; 191 $a->questionname = $question->name; 192 $question->name = get_string('selectedby', 'qtype_random', $a); 193 } 194 195 public function save_question($question, $form) { 196 global $DB; 197 198 $form->name = ''; 199 list($category) = explode(',', $form->category); 200 201 if (!$form->includesubcategories) { 202 if ($DB->record_exists('question_categories', ['id' => $category, 'parent' => 0])) { 203 // The chosen category is a top category. 204 $form->includesubcategories = true; 205 } 206 } 207 208 $form->tags = array(); 209 210 if (empty($form->fromtags)) { 211 $form->fromtags = array(); 212 } 213 214 $form->questiontext = array( 215 'text' => $form->includesubcategories ? '1' : '0', 216 'format' => 0 217 ); 218 219 // Name is not a required field for random questions, but 220 // parent::save_question Assumes that it is. 221 return parent::save_question($question, $form); 222 } 223 224 public function save_question_options($question) { 225 global $DB; 226 227 // No options, as such, but we set the parent field to the question's 228 // own id. Setting the parent field has the effect of hiding this 229 // question in various places. 230 $updateobject = new stdClass(); 231 $updateobject->id = $question->id; 232 $updateobject->parent = $question->id; 233 234 // We also force the question name to be 'Random (categoryname)'. 235 $category = $DB->get_record('question_categories', 236 array('id' => $question->category), '*', MUST_EXIST); 237 $updateobject->name = $this->question_name($category, $question->includesubcategories, $question->fromtags); 238 return $DB->update_record('question', $updateobject); 239 } 240 241 /** 242 * During unit tests we need to be able to reset all caches so that each new test starts in a known state. 243 * Intended for use only for testing. This is a stop gap until we start using the MUC caching api here. 244 * You need to call this before every test that loads one or more random questions. 245 */ 246 public function clear_caches_before_testing() { 247 $this->availablequestionsbycategory = array(); 248 } 249 250 /** 251 * Get all the usable questions from a particular question category. 252 * 253 * @param int $categoryid the id of a question category. 254 * @param bool whether to include questions from subcategories. 255 * @param string $questionsinuse comma-separated list of question ids to 256 * exclude from consideration. 257 * @return array of question records. 258 */ 259 public function get_available_questions_from_category($categoryid, $subcategories) { 260 if (isset($this->availablequestionsbycategory[$categoryid][$subcategories])) { 261 return $this->availablequestionsbycategory[$categoryid][$subcategories]; 262 } 263 264 $this->init_qtype_lists(); 265 if ($subcategories) { 266 $categoryids = question_categorylist($categoryid); 267 } else { 268 $categoryids = array($categoryid); 269 } 270 271 $questionids = question_bank::get_finder()->get_questions_from_categories( 272 $categoryids, 'qtype NOT IN (' . $this->excludedqtypes . ')'); 273 $this->availablequestionsbycategory[$categoryid][$subcategories] = $questionids; 274 return $questionids; 275 } 276 277 public function make_question($questiondata) { 278 return $this->choose_other_question($questiondata, array()); 279 } 280 281 /** 282 * Load the definition of another question picked randomly by this question. 283 * @param object $questiondata the data defining a random question. 284 * @param array $excludedquestions of question ids. We will no pick any question whose id is in this list. 285 * @param bool $allowshuffle if false, then any shuffle option on the selected quetsion is disabled. 286 * @param null|integer $forcequestionid if not null then force the picking of question with id $forcequestionid. 287 * @throws coding_exception 288 * @return question_definition|null the definition of the question that was 289 * selected, or null if no suitable question could be found. 290 */ 291 public function choose_other_question($questiondata, $excludedquestions, $allowshuffle = true, $forcequestionid = null) { 292 $available = $this->get_available_questions_from_category($questiondata->category, 293 !empty($questiondata->questiontext)); 294 shuffle($available); 295 296 if ($forcequestionid !== null) { 297 $forcedquestionkey = array_search($forcequestionid, $available); 298 if ($forcedquestionkey !== false) { 299 unset($available[$forcedquestionkey]); 300 array_unshift($available, $forcequestionid); 301 } else { 302 throw new coding_exception('thisquestionidisnotavailable', $forcequestionid); 303 } 304 } 305 306 foreach ($available as $questionid) { 307 if (in_array($questionid, $excludedquestions)) { 308 continue; 309 } 310 311 $question = question_bank::load_question($questionid, $allowshuffle); 312 $this->set_selected_question_name($question, $questiondata->name); 313 return $question; 314 } 315 return null; 316 } 317 318 public function get_random_guess_score($questiondata) { 319 return null; 320 } 321 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body