Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 namespace mod_quiz\question\bank; 18 19 use core_question\local\bank\question_version_status; 20 use core_question\local\bank\random_question_loader; 21 use core_question\question_reference_manager; 22 use qbank_tagquestion\tag_condition; 23 use qubaid_condition; 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 require_once($CFG->dirroot . '/question/engine/bank.php'); 28 29 /** 30 * Helper class for question bank and its associated data. 31 * 32 * @package mod_quiz 33 * @category question 34 * @copyright 2021 Catalyst IT Australia Pty Ltd 35 * @author Safat Shahin <safatshahin@catalyst-au.net> 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class qbank_helper { 39 40 /** 41 * Get the available versions of a question where one of the version has the given question id. 42 * 43 * @param int $questionid id of a question. 44 * @return \stdClass[] other versions of this question. Each object has fields versionid, 45 * version and questionid. Array is returned most recent version first. 46 */ 47 public static function get_version_options(int $questionid): array { 48 global $DB; 49 50 return $DB->get_records_sql(" 51 SELECT allversions.id AS versionid, 52 allversions.version, 53 allversions.questionid 54 55 FROM {question_versions} allversions 56 57 WHERE allversions.questionbankentryid = ( 58 SELECT givenversion.questionbankentryid 59 FROM {question_versions} givenversion 60 WHERE givenversion.questionid = ? 61 ) 62 AND allversions.status <> ? 63 64 ORDER BY allversions.version DESC 65 ", [$questionid, question_version_status::QUESTION_STATUS_DRAFT]); 66 } 67 68 /** 69 * Get the information about which questions should be used to create a quiz attempt. 70 * 71 * Each element in the returned array is indexed by slot.slot (slot number) an each object hass: 72 * - All the field of the slot table. 73 * - contextid for where the question(s) come from. 74 * - category id for where the questions come from. 75 * - For non-random questions, All the fields of the question table (but id is in questionid). 76 * Also question version and question bankentryid. 77 * - For random questions, filtercondition, which is also unpacked into category, randomrecurse, 78 * randomtags, and note that these also have a ->name set and ->qtype set to 'random'. 79 * 80 * @param int $quizid the id of the quiz to load the data for. 81 * @param \context_module $quizcontext the context of this quiz. 82 * @param int|null $slotid optional, if passed only load the data for this one slot (if it is in this quiz). 83 * @return array indexed by slot, with information about the content of each slot. 84 */ 85 public static function get_question_structure(int $quizid, \context_module $quizcontext, 86 int $slotid = null): array { 87 global $DB; 88 89 $params = [ 90 'draft' => question_version_status::QUESTION_STATUS_DRAFT, 91 'quizcontextid' => $quizcontext->id, 92 'quizcontextid2' => $quizcontext->id, 93 'quizcontextid3' => $quizcontext->id, 94 'quizid' => $quizid, 95 'quizid2' => $quizid, 96 ]; 97 $slotidtest = ''; 98 $slotidtest2 = ''; 99 if ($slotid !== null) { 100 $params['slotid'] = $slotid; 101 $params['slotid2'] = $slotid; 102 $slotidtest = ' AND slot.id = :slotid'; 103 $slotidtest2 = ' AND lslot.id = :slotid2'; 104 } 105 106 // Load all the data about each slot. 107 $slotdata = $DB->get_records_sql(" 108 SELECT slot.slot, 109 slot.id AS slotid, 110 slot.page, 111 slot.maxmark, 112 slot.displaynumber, 113 slot.requireprevious, 114 qsr.filtercondition, 115 qsr.usingcontextid, 116 qv.status, 117 qv.id AS versionid, 118 qv.version, 119 qr.version AS requestedversion, 120 qv.questionbankentryid, 121 q.id AS questionid, 122 q.*, 123 qc.id AS category, 124 COALESCE(qc.contextid, qsr.questionscontextid) AS contextid 125 126 FROM {quiz_slots} slot 127 128 -- case where a particular question has been added to the quiz. 129 LEFT JOIN {question_references} qr ON qr.usingcontextid = :quizcontextid AND qr.component = 'mod_quiz' 130 AND qr.questionarea = 'slot' AND qr.itemid = slot.id 131 LEFT JOIN {question_bank_entries} qbe ON qbe.id = qr.questionbankentryid 132 133 -- This way of getting the latest version for each slot is a bit more complicated 134 -- than we would like, but the simpler SQL did not work in Oracle 11.2. 135 -- (It did work fine in Oracle 19.x, so once we have updated our min supported 136 -- version we could consider digging the old code out of git history from 137 -- just before the commit that added this comment. 138 -- For relevant question_bank_entries, this gets the latest non-draft slot number. 139 LEFT JOIN ( 140 SELECT lv.questionbankentryid, 141 MAX(CASE WHEN lv.status <> :draft THEN lv.version END) AS usableversion, 142 MAX(lv.version) AS anyversion 143 FROM {quiz_slots} lslot 144 JOIN {question_references} lqr ON lqr.usingcontextid = :quizcontextid2 AND lqr.component = 'mod_quiz' 145 AND lqr.questionarea = 'slot' AND lqr.itemid = lslot.id 146 JOIN {question_versions} lv ON lv.questionbankentryid = lqr.questionbankentryid 147 WHERE lslot.quizid = :quizid2 148 $slotidtest2 149 AND lqr.version IS NULL 150 GROUP BY lv.questionbankentryid 151 ) latestversions ON latestversions.questionbankentryid = qr.questionbankentryid 152 153 LEFT JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id 154 -- Either specified version, or latest usable version, or a draft version. 155 AND qv.version = COALESCE(qr.version, 156 latestversions.usableversion, 157 latestversions.anyversion) 158 LEFT JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid 159 LEFT JOIN {question} q ON q.id = qv.questionid 160 161 -- Case where a random question has been added. 162 LEFT JOIN {question_set_references} qsr ON qsr.usingcontextid = :quizcontextid3 AND qsr.component = 'mod_quiz' 163 AND qsr.questionarea = 'slot' AND qsr.itemid = slot.id 164 165 WHERE slot.quizid = :quizid 166 $slotidtest 167 168 ORDER BY slot.slot 169 ", $params); 170 171 // Unpack the random info from question_set_reference. 172 foreach ($slotdata as $slot) { 173 // Ensure the right id is the id. 174 $slot->id = $slot->slotid; 175 176 if ($slot->filtercondition) { 177 // Unpack the information about a random question. 178 $slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique. 179 $filter = json_decode($slot->filtercondition, true); 180 $slot->filtercondition = question_reference_manager::convert_legacy_set_reference_filter_condition($filter); 181 182 $slot->category = $slot->filtercondition['filter']['category']['values'][0] ?? 0; 183 184 $slot->qtype = 'random'; 185 $slot->name = get_string('random', 'quiz'); 186 $slot->length = 1; 187 } else if ($slot->qtype === null) { 188 // This question must have gone missing. Put in a placeholder. 189 $slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique. 190 $slot->category = 0; 191 $slot->qtype = 'missingtype'; 192 $slot->name = get_string('missingquestion', 'quiz'); 193 $slot->questiontext = ' '; 194 $slot->questiontextformat = FORMAT_HTML; 195 $slot->length = 1; 196 } else if (!\question_bank::qtype_exists($slot->qtype)) { 197 // Question of unknown type found in the database. Set to placeholder question types instead. 198 $slot->qtype = 'missingtype'; 199 } else { 200 $slot->_partiallyloaded = 1; 201 } 202 } 203 204 return $slotdata; 205 } 206 207 /** 208 * Get this list of random selection tag ids from one of the slots returned by get_question_structure. 209 * 210 * @param \stdClass $slotdata one of the array elements returned by get_question_structure. 211 * @return array list of tag ids. 212 */ 213 public static function get_tag_ids_for_slot(\stdClass $slotdata): array { 214 $tagids = []; 215 if (!isset($slotdata->filtercondition['filter'])) { 216 return $tagids; 217 } 218 $filter = $slotdata->filtercondition['filter']; 219 if (isset($filter['qtagids'])) { 220 $tagids = $filter['qtagids']['values']; 221 } 222 return $tagids; 223 } 224 225 /** 226 * Given a slot from the array returned by get_question_structure, describe the random question it represents. 227 * 228 * @param \stdClass $slotdata one of the array elements returned by get_question_structure. 229 * @return string that can be used to display the random slot. 230 */ 231 public static function describe_random_question(\stdClass $slotdata): string { 232 $qtagids = self::get_tag_ids_for_slot($slotdata); 233 234 if ($qtagids) { 235 $tagnames = []; 236 $tags = \core_tag_tag::get_bulk($qtagids, 'id, name'); 237 foreach ($tags as $tag) { 238 $tagnames[] = $tag->name; 239 } 240 $description = get_string('randomqnametags', 'mod_quiz', implode(",", $tagnames)); 241 } else { 242 $description = get_string('randomqname', 'mod_quiz'); 243 } 244 return shorten_text($description, 255); 245 } 246 247 /** 248 * Choose question for redo in a particular slot. 249 * 250 * @param int $quizid the id of the quiz to load the data for. 251 * @param \context_module $quizcontext the context of this quiz. 252 * @param int $slotid optional, if passed only load the data for this one slot (if it is in this quiz). 253 * @param qubaid_condition $qubaids attempts to consider when avoiding picking repeats of random questions. 254 * @return int the id of the question to use. 255 */ 256 public static function choose_question_for_redo(int $quizid, \context_module $quizcontext, 257 int $slotid, qubaid_condition $qubaids): int { 258 $slotdata = self::get_question_structure($quizid, $quizcontext, $slotid); 259 $slotdata = reset($slotdata); 260 261 // Non-random question. 262 if ($slotdata->qtype != 'random') { 263 return $slotdata->questionid; 264 } 265 266 // Random question. 267 $randomloader = new random_question_loader($qubaids, []); 268 $fitlercondition = $slotdata->filtercondition; 269 $filter = $fitlercondition['filter'] ?? []; 270 $newqusetionid = $randomloader->get_next_filtered_question_id($filter); 271 272 if ($newqusetionid === null) { 273 throw new \moodle_exception('notenoughrandomquestions', 'quiz'); 274 } 275 return $newqusetionid; 276 } 277 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body