| 1 | efrain | 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 |  * More object oriented wrappers around parts of the Moodle question bank.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * In due course, I expect that the question bank will be converted to a
 | 
        
           |  |  | 21 |  * fully object oriented structure, at which point this file can be a
 | 
        
           |  |  | 22 |  * starting point.
 | 
        
           |  |  | 23 |  *
 | 
        
           |  |  | 24 |  * @package    moodlecore
 | 
        
           |  |  | 25 |  * @subpackage questionbank
 | 
        
           |  |  | 26 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 27 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 28 |  */
 | 
        
           |  |  | 29 |   | 
        
           | 1441 | ariadna | 30 | use core\output\notification;
 | 
        
           |  |  | 31 | use core_cache\application_cache;
 | 
        
           |  |  | 32 | use core_cache\data_source_interface;
 | 
        
           |  |  | 33 | use core_cache\definition;
 | 
        
           | 1 | efrain | 34 | use core_question\local\bank\question_version_status;
 | 
        
           |  |  | 35 | use core_question\output\question_version_info;
 | 
        
           | 1441 | ariadna | 36 | use qbank_previewquestion\question_preview_options;
 | 
        
           | 1 | efrain | 37 |   | 
        
           |  |  | 38 | defined('MOODLE_INTERNAL') || die();
 | 
        
           |  |  | 39 |   | 
        
           |  |  | 40 | require_once(__DIR__ . '/../type/questiontypebase.php');
 | 
        
           |  |  | 41 |   | 
        
           |  |  | 42 |   | 
        
           |  |  | 43 | /**
 | 
        
           |  |  | 44 |  * This static class provides access to the other question bank.
 | 
        
           |  |  | 45 |  *
 | 
        
           |  |  | 46 |  * It provides functions for managing question types and question definitions.
 | 
        
           |  |  | 47 |  *
 | 
        
           |  |  | 48 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 49 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 50 |  */
 | 
        
           |  |  | 51 | abstract class question_bank {
 | 
        
           |  |  | 52 |     // TODO: This limit can be deleted if someday we move all TEXTS to BIG ones. MDL-19603
 | 
        
           |  |  | 53 |     const MAX_SUMMARY_LENGTH = 32000;
 | 
        
           |  |  | 54 |   | 
        
           |  |  | 55 |     /** @var array question type name => question_type subclass. */
 | 
        
           |  |  | 56 |     private static $questiontypes = array();
 | 
        
           |  |  | 57 |   | 
        
           |  |  | 58 |     /** @var array question type name => 1. Records which question definitions have been loaded. */
 | 
        
           |  |  | 59 |     private static $loadedqdefs = array();
 | 
        
           |  |  | 60 |   | 
        
           |  |  | 61 |     /** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
 | 
        
           |  |  | 62 |     private static $testmode = false;
 | 
        
           |  |  | 63 |     private static $testdata = array();
 | 
        
           |  |  | 64 |   | 
        
           |  |  | 65 |     private static $questionconfig = null;
 | 
        
           |  |  | 66 |   | 
        
           |  |  | 67 |     /**
 | 
        
           |  |  | 68 |      * @var array string => string The standard set of grade options (fractions)
 | 
        
           |  |  | 69 |      * to use when editing questions, in the range 0 to 1 inclusive. Array keys
 | 
        
           |  |  | 70 |      * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
 | 
        
           |  |  | 71 |      * have float array keys in PHP.
 | 
        
           |  |  | 72 |      * Initialised by {@link ensure_grade_options_initialised()}.
 | 
        
           |  |  | 73 |      */
 | 
        
           |  |  | 74 |     private static $fractionoptions = null;
 | 
        
           |  |  | 75 |     /** @var array string => string The full standard set of (fractions) -1 to 1 inclusive. */
 | 
        
           |  |  | 76 |     private static $fractionoptionsfull = null;
 | 
        
           |  |  | 77 |   | 
        
           |  |  | 78 |     /**
 | 
        
           |  |  | 79 |      * @param string $qtypename a question type name, e.g. 'multichoice'.
 | 
        
           |  |  | 80 |      * @return bool whether that question type is installed in this Moodle.
 | 
        
           |  |  | 81 |      */
 | 
        
           |  |  | 82 |     public static function is_qtype_installed($qtypename) {
 | 
        
           |  |  | 83 |         $plugindir = core_component::get_plugin_directory('qtype', $qtypename);
 | 
        
           |  |  | 84 |         return $plugindir && is_readable($plugindir . '/questiontype.php');
 | 
        
           |  |  | 85 |     }
 | 
        
           |  |  | 86 |   | 
        
           |  |  | 87 |     /**
 | 
        
           | 1441 | ariadna | 88 |      * Check if a given question type is one that is installed and usable.
 | 
        
           |  |  | 89 |      *
 | 
        
           |  |  | 90 |      * Use this before doing things like rendering buttons/options which will only work for
 | 
        
           |  |  | 91 |      * installed question types.
 | 
        
           |  |  | 92 |      *
 | 
        
           |  |  | 93 |      * When loaded through most of the core_question areas, qtype will still be the uninstalled type, e.g. 'mytype',
 | 
        
           |  |  | 94 |      * but when we get to the quiz pages, it will have been converted to 'missingtype'. So we need to check that
 | 
        
           |  |  | 95 |      * as well here.
 | 
        
           |  |  | 96 |      *
 | 
        
           |  |  | 97 |      * @param string $qtypename e.g. 'multichoice'.
 | 
        
           |  |  | 98 |      * @return bool
 | 
        
           |  |  | 99 |      */
 | 
        
           |  |  | 100 |     public static function is_qtype_usable(string $qtypename): bool {
 | 
        
           |  |  | 101 |         return self::is_qtype_installed($qtypename) && $qtypename !== 'missingtype';
 | 
        
           |  |  | 102 |     }
 | 
        
           |  |  | 103 |   | 
        
           |  |  | 104 |     /**
 | 
        
           | 1 | efrain | 105 |      * Get the question type class for a particular question type.
 | 
        
           |  |  | 106 |      * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
 | 
        
           |  |  | 107 |      * @param bool $mustexist if false, the missing question type is returned when
 | 
        
           |  |  | 108 |      *      the requested question type is not installed.
 | 
        
           |  |  | 109 |      * @return question_type the corresponding question type class.
 | 
        
           |  |  | 110 |      */
 | 
        
           |  |  | 111 |     public static function get_qtype($qtypename, $mustexist = true) {
 | 
        
           |  |  | 112 |         global $CFG;
 | 
        
           |  |  | 113 |         if (isset(self::$questiontypes[$qtypename])) {
 | 
        
           |  |  | 114 |             return self::$questiontypes[$qtypename];
 | 
        
           |  |  | 115 |         }
 | 
        
           |  |  | 116 |         $file = core_component::get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
 | 
        
           |  |  | 117 |         if (!is_readable($file)) {
 | 
        
           |  |  | 118 |             if ($mustexist || $qtypename == 'missingtype') {
 | 
        
           |  |  | 119 |                 throw new coding_exception('Unknown question type ' . $qtypename);
 | 
        
           |  |  | 120 |             } else {
 | 
        
           |  |  | 121 |                 return self::get_qtype('missingtype');
 | 
        
           |  |  | 122 |             }
 | 
        
           |  |  | 123 |         }
 | 
        
           |  |  | 124 |         include_once($file);
 | 
        
           |  |  | 125 |         $class = 'qtype_' . $qtypename;
 | 
        
           |  |  | 126 |         if (!class_exists($class)) {
 | 
        
           |  |  | 127 |             throw new coding_exception("Class {$class} must be defined in {$file}.");
 | 
        
           |  |  | 128 |         }
 | 
        
           |  |  | 129 |         self::$questiontypes[$qtypename] = new $class();
 | 
        
           |  |  | 130 |         return self::$questiontypes[$qtypename];
 | 
        
           |  |  | 131 |     }
 | 
        
           |  |  | 132 |   | 
        
           |  |  | 133 |     /**
 | 
        
           |  |  | 134 |      * Load the question configuration data from config_plugins.
 | 
        
           |  |  | 135 |      * @return object get_config('question') with caching.
 | 
        
           |  |  | 136 |      */
 | 
        
           |  |  | 137 |     public static function get_config() {
 | 
        
           |  |  | 138 |         if (is_null(self::$questionconfig)) {
 | 
        
           |  |  | 139 |             self::$questionconfig = get_config('question');
 | 
        
           |  |  | 140 |         }
 | 
        
           |  |  | 141 |         return self::$questionconfig;
 | 
        
           |  |  | 142 |     }
 | 
        
           |  |  | 143 |   | 
        
           |  |  | 144 |     /**
 | 
        
           |  |  | 145 |      * @param string $qtypename the internal name of a question type. For example multichoice.
 | 
        
           |  |  | 146 |      * @return bool whether users are allowed to create questions of this type.
 | 
        
           |  |  | 147 |      */
 | 
        
           |  |  | 148 |     public static function qtype_enabled($qtypename) {
 | 
        
           |  |  | 149 |         $config = self::get_config();
 | 
        
           |  |  | 150 |         $enabledvar = $qtypename . '_disabled';
 | 
        
           |  |  | 151 |         return self::qtype_exists($qtypename) && empty($config->$enabledvar) &&
 | 
        
           |  |  | 152 |                 self::get_qtype($qtypename)->menu_name() != '';
 | 
        
           |  |  | 153 |     }
 | 
        
           |  |  | 154 |   | 
        
           |  |  | 155 |     /**
 | 
        
           |  |  | 156 |      * @param string $qtypename the internal name of a question type. For example multichoice.
 | 
        
           |  |  | 157 |      * @return bool whether this question type exists.
 | 
        
           |  |  | 158 |      */
 | 
        
           |  |  | 159 |     public static function qtype_exists($qtypename) {
 | 
        
           |  |  | 160 |         return array_key_exists($qtypename, core_component::get_plugin_list('qtype'));
 | 
        
           |  |  | 161 |     }
 | 
        
           |  |  | 162 |   | 
        
           |  |  | 163 |     /**
 | 
        
           |  |  | 164 |      * @param $qtypename the internal name of a question type, for example multichoice.
 | 
        
           |  |  | 165 |      * @return string the human_readable name of this question type, from the language pack.
 | 
        
           |  |  | 166 |      */
 | 
        
           |  |  | 167 |     public static function get_qtype_name($qtypename) {
 | 
        
           |  |  | 168 |         return self::get_qtype($qtypename)->local_name();
 | 
        
           |  |  | 169 |     }
 | 
        
           |  |  | 170 |   | 
        
           |  |  | 171 |     /**
 | 
        
           |  |  | 172 |      * @return array all the installed question types.
 | 
        
           |  |  | 173 |      */
 | 
        
           |  |  | 174 |     public static function get_all_qtypes() {
 | 
        
           |  |  | 175 |         $qtypes = array();
 | 
        
           |  |  | 176 |         foreach (core_component::get_plugin_list('qtype') as $plugin => $notused) {
 | 
        
           |  |  | 177 |             try {
 | 
        
           |  |  | 178 |                 $qtypes[$plugin] = self::get_qtype($plugin);
 | 
        
           |  |  | 179 |             } catch (coding_exception $e) {
 | 
        
           |  |  | 180 |                 // Catching coding_exceptions here means that incompatible
 | 
        
           |  |  | 181 |                 // question types do not cause the rest of Moodle to break.
 | 
        
           |  |  | 182 |             }
 | 
        
           |  |  | 183 |         }
 | 
        
           |  |  | 184 |         return $qtypes;
 | 
        
           |  |  | 185 |     }
 | 
        
           |  |  | 186 |   | 
        
           |  |  | 187 |     /**
 | 
        
           |  |  | 188 |      * Sort an array of question types according to the order the admin set up,
 | 
        
           |  |  | 189 |      * and then alphabetically for the rest.
 | 
        
           |  |  | 190 |      * @param array qtype->name() => qtype->local_name().
 | 
        
           |  |  | 191 |      * @return array sorted array.
 | 
        
           |  |  | 192 |      */
 | 
        
           |  |  | 193 |     public static function sort_qtype_array($qtypes, $config = null) {
 | 
        
           |  |  | 194 |         if (is_null($config)) {
 | 
        
           |  |  | 195 |             $config = self::get_config();
 | 
        
           |  |  | 196 |         }
 | 
        
           |  |  | 197 |   | 
        
           |  |  | 198 |         $sortorder = array();
 | 
        
           |  |  | 199 |         $otherqtypes = array();
 | 
        
           |  |  | 200 |         foreach ($qtypes as $name => $localname) {
 | 
        
           |  |  | 201 |             $sortvar = $name . '_sortorder';
 | 
        
           |  |  | 202 |             if (isset($config->$sortvar)) {
 | 
        
           |  |  | 203 |                 $sortorder[$config->$sortvar] = $name;
 | 
        
           |  |  | 204 |             } else {
 | 
        
           |  |  | 205 |                 $otherqtypes[$name] = $localname;
 | 
        
           |  |  | 206 |             }
 | 
        
           |  |  | 207 |         }
 | 
        
           |  |  | 208 |   | 
        
           |  |  | 209 |         ksort($sortorder);
 | 
        
           |  |  | 210 |         core_collator::asort($otherqtypes);
 | 
        
           |  |  | 211 |   | 
        
           |  |  | 212 |         $sortedqtypes = array();
 | 
        
           |  |  | 213 |         foreach ($sortorder as $name) {
 | 
        
           |  |  | 214 |             $sortedqtypes[$name] = $qtypes[$name];
 | 
        
           |  |  | 215 |         }
 | 
        
           |  |  | 216 |         foreach ($otherqtypes as $name => $notused) {
 | 
        
           |  |  | 217 |             $sortedqtypes[$name] = $qtypes[$name];
 | 
        
           |  |  | 218 |         }
 | 
        
           |  |  | 219 |         return $sortedqtypes;
 | 
        
           |  |  | 220 |     }
 | 
        
           |  |  | 221 |   | 
        
           |  |  | 222 |     /**
 | 
        
           |  |  | 223 |      * @return array all the question types that users are allowed to create,
 | 
        
           |  |  | 224 |      *      sorted into the preferred order set on the admin screen.
 | 
        
           |  |  | 225 |      */
 | 
        
           |  |  | 226 |     public static function get_creatable_qtypes() {
 | 
        
           |  |  | 227 |         $config = self::get_config();
 | 
        
           |  |  | 228 |         $allqtypes = self::get_all_qtypes();
 | 
        
           |  |  | 229 |   | 
        
           |  |  | 230 |         $qtypenames = array();
 | 
        
           |  |  | 231 |         foreach ($allqtypes as $name => $qtype) {
 | 
        
           |  |  | 232 |             if (self::qtype_enabled($name)) {
 | 
        
           |  |  | 233 |                 $qtypenames[$name] = $qtype->local_name();
 | 
        
           |  |  | 234 |             }
 | 
        
           |  |  | 235 |         }
 | 
        
           |  |  | 236 |   | 
        
           |  |  | 237 |         $qtypenames = self::sort_qtype_array($qtypenames);
 | 
        
           |  |  | 238 |   | 
        
           |  |  | 239 |         $creatableqtypes = array();
 | 
        
           |  |  | 240 |         foreach ($qtypenames as $name => $notused) {
 | 
        
           |  |  | 241 |             $creatableqtypes[$name] = $allqtypes[$name];
 | 
        
           |  |  | 242 |         }
 | 
        
           |  |  | 243 |         return $creatableqtypes;
 | 
        
           |  |  | 244 |     }
 | 
        
           |  |  | 245 |   | 
        
           |  |  | 246 |     /**
 | 
        
           |  |  | 247 |      * Load the question definition class(es) belonging to a question type. That is,
 | 
        
           |  |  | 248 |      * include_once('/question/type/' . $qtypename . '/question.php'), with a bit
 | 
        
           |  |  | 249 |      * of checking.
 | 
        
           |  |  | 250 |      * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
 | 
        
           |  |  | 251 |      */
 | 
        
           |  |  | 252 |     public static function load_question_definition_classes($qtypename) {
 | 
        
           |  |  | 253 |         global $CFG;
 | 
        
           |  |  | 254 |         if (isset(self::$loadedqdefs[$qtypename])) {
 | 
        
           |  |  | 255 |             return;
 | 
        
           |  |  | 256 |         }
 | 
        
           |  |  | 257 |         $file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
 | 
        
           |  |  | 258 |         if (!is_readable($file)) {
 | 
        
           |  |  | 259 |             throw new coding_exception('Unknown question type (no definition) ' . $qtypename);
 | 
        
           |  |  | 260 |         }
 | 
        
           |  |  | 261 |         include_once($file);
 | 
        
           |  |  | 262 |         self::$loadedqdefs[$qtypename] = 1;
 | 
        
           |  |  | 263 |     }
 | 
        
           |  |  | 264 |   | 
        
           |  |  | 265 |     /**
 | 
        
           |  |  | 266 |      * This method needs to be called whenever a question is edited.
 | 
        
           |  |  | 267 |      */
 | 
        
           |  |  | 268 |     public static function notify_question_edited($questionid) {
 | 
        
           |  |  | 269 |         question_finder::get_instance()->uncache_question($questionid);
 | 
        
           |  |  | 270 |     }
 | 
        
           |  |  | 271 |   | 
        
           |  |  | 272 |     /**
 | 
        
           |  |  | 273 |      * Load a question definition data from the database. The data will be
 | 
        
           |  |  | 274 |      * returned as a plain stdClass object.
 | 
        
           |  |  | 275 |      * @param int $questionid the id of the question to load.
 | 
        
           |  |  | 276 |      * @return object question definition loaded from the database.
 | 
        
           |  |  | 277 |      */
 | 
        
           |  |  | 278 |     public static function load_question_data($questionid) {
 | 
        
           |  |  | 279 |         return question_finder::get_instance()->load_question_data($questionid);
 | 
        
           |  |  | 280 |     }
 | 
        
           |  |  | 281 |   | 
        
           |  |  | 282 |     /**
 | 
        
           |  |  | 283 |      * Load a question definition from the database. The object returned
 | 
        
           |  |  | 284 |      * will actually be of an appropriate {@link question_definition} subclass.
 | 
        
           |  |  | 285 |      * @param int $questionid the id of the question to load.
 | 
        
           |  |  | 286 |      * @param bool $allowshuffle if false, then any shuffle option on the selected
 | 
        
           |  |  | 287 |      *      quetsion is disabled.
 | 
        
           |  |  | 288 |      * @return question_definition loaded from the database.
 | 
        
           |  |  | 289 |      */
 | 
        
           |  |  | 290 |     public static function load_question($questionid, $allowshuffle = true) {
 | 
        
           |  |  | 291 |   | 
        
           |  |  | 292 |         if (self::$testmode) {
 | 
        
           |  |  | 293 |             // Evil, test code in production, but no way round it.
 | 
        
           |  |  | 294 |             return self::return_test_question_data($questionid);
 | 
        
           |  |  | 295 |         }
 | 
        
           |  |  | 296 |   | 
        
           |  |  | 297 |         $questiondata = self::load_question_data($questionid);
 | 
        
           |  |  | 298 |   | 
        
           |  |  | 299 |         if (!$allowshuffle) {
 | 
        
           |  |  | 300 |             $questiondata->options->shuffleanswers = false;
 | 
        
           |  |  | 301 |         }
 | 
        
           |  |  | 302 |         return self::make_question($questiondata);
 | 
        
           |  |  | 303 |     }
 | 
        
           |  |  | 304 |   | 
        
           |  |  | 305 |     /**
 | 
        
           |  |  | 306 |      * Convert the question information loaded with {@link get_question_options()}
 | 
        
           |  |  | 307 |      * to a question_definintion object.
 | 
        
           |  |  | 308 |      * @param object $questiondata raw data loaded from the database.
 | 
        
           |  |  | 309 |      * @return question_definition loaded from the database.
 | 
        
           |  |  | 310 |      */
 | 
        
           |  |  | 311 |     public static function make_question($questiondata) {
 | 
        
           |  |  | 312 |         $definition = self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
 | 
        
           |  |  | 313 |         question_version_info::$pendingdefinitions[$definition->id] = $definition;
 | 
        
           |  |  | 314 |         return $definition;
 | 
        
           |  |  | 315 |     }
 | 
        
           |  |  | 316 |   | 
        
           |  |  | 317 |     /**
 | 
        
           | 1441 | ariadna | 318 |      * Render a throw-away preview of a question.
 | 
        
           |  |  | 319 |      *
 | 
        
           |  |  | 320 |      * If the question cannot be rendered (e.g. because it is not installed)
 | 
        
           |  |  | 321 |      * then display a message instead.
 | 
        
           |  |  | 322 |      *
 | 
        
           |  |  | 323 |      * @param question_definition $question a question.
 | 
        
           |  |  | 324 |      * @return string HTML to output.
 | 
        
           |  |  | 325 |      */
 | 
        
           |  |  | 326 |     public static function render_preview_of_question(question_definition $question): string {
 | 
        
           |  |  | 327 |         global $DB, $OUTPUT, $USER;
 | 
        
           |  |  | 328 |   | 
        
           |  |  | 329 |         if (!self::is_qtype_usable($question->qtype->name())) {
 | 
        
           |  |  | 330 |             // TODO MDL-84902 ideally this would be changed to render at least the qeuestion text.
 | 
        
           |  |  | 331 |             // See, for example, test_render_missing in question/type/missingtype/tests/missingtype_test.php.
 | 
        
           |  |  | 332 |             return $OUTPUT->notification(
 | 
        
           |  |  | 333 |                 get_string('invalidquestiontype', 'question', $question->qtype->name()),
 | 
        
           |  |  | 334 |                 notification::NOTIFY_WARNING,
 | 
        
           |  |  | 335 |                 closebutton: false);
 | 
        
           |  |  | 336 |         }
 | 
        
           |  |  | 337 |   | 
        
           |  |  | 338 |         // TODO MDL-84902 remove this dependency on a class from qbank_previewquestion plugin.
 | 
        
           |  |  | 339 |         if (!class_exists(question_preview_options::class)) {
 | 
        
           |  |  | 340 |             debugging('Preview cannot be rendered. The standard plugin ' .
 | 
        
           |  |  | 341 |                 'qbank_previewquestion plugin has been removed.', DEBUG_DEVELOPER);
 | 
        
           |  |  | 342 |             return '';
 | 
        
           |  |  | 343 |         }
 | 
        
           |  |  | 344 |   | 
        
           |  |  | 345 |         $quba = question_engine::make_questions_usage_by_activity(
 | 
        
           |  |  | 346 |             'core_question_preview', context_user::instance($USER->id));
 | 
        
           |  |  | 347 |         $options = new question_preview_options($question);
 | 
        
           |  |  | 348 |         $quba->set_preferred_behaviour($options->behaviour);
 | 
        
           |  |  | 349 |   | 
        
           |  |  | 350 |         $slot = $quba->add_question($question, $options->maxmark);
 | 
        
           |  |  | 351 |         $quba->start_question($slot, $options->variant);
 | 
        
           |  |  | 352 |   | 
        
           |  |  | 353 |         $transaction = $DB->start_delegated_transaction();
 | 
        
           |  |  | 354 |         question_engine::save_questions_usage_by_activity($quba);
 | 
        
           |  |  | 355 |         $transaction->allow_commit();
 | 
        
           |  |  | 356 |   | 
        
           |  |  | 357 |         return $quba->render_question($slot, $options, '1');
 | 
        
           |  |  | 358 |     }
 | 
        
           |  |  | 359 |   | 
        
           |  |  | 360 |     /**
 | 
        
           | 1 | efrain | 361 |      * Get all the versions of a particular question.
 | 
        
           |  |  | 362 |      *
 | 
        
           |  |  | 363 |      * @param int $questionid id of the question
 | 
        
           |  |  | 364 |      * @return array The array keys are version number, and the values are objects with three int fields
 | 
        
           |  |  | 365 |      * version (same as array key), versionid and questionid.
 | 
        
           |  |  | 366 |      */
 | 
        
           |  |  | 367 |     public static function get_all_versions_of_question(int $questionid): array {
 | 
        
           |  |  | 368 |         global $DB;
 | 
        
           |  |  | 369 |         $sql = "SELECT qv.id AS versionid, qv.version, qv.questionid
 | 
        
           |  |  | 370 |                   FROM {question_versions} qv
 | 
        
           |  |  | 371 |                  WHERE qv.questionbankentryid = (SELECT DISTINCT qbe.id
 | 
        
           |  |  | 372 |                                                    FROM {question_bank_entries} qbe
 | 
        
           |  |  | 373 |                                                    JOIN {question_versions} qv ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 374 |                                                    JOIN {question} q ON qv.questionid = q.id
 | 
        
           |  |  | 375 |                                                   WHERE q.id = ?)
 | 
        
           |  |  | 376 |               ORDER BY qv.version DESC";
 | 
        
           |  |  | 377 |   | 
        
           |  |  | 378 |         return $DB->get_records_sql($sql, [$questionid]);
 | 
        
           |  |  | 379 |     }
 | 
        
           |  |  | 380 |   | 
        
           |  |  | 381 |     /**
 | 
        
           |  |  | 382 |      * Get all the versions of questions.
 | 
        
           |  |  | 383 |      *
 | 
        
           |  |  | 384 |      * @param array $questionids Array of question ids.
 | 
        
           |  |  | 385 |      * @return array two dimensional array question_bank_entries.id => version number => question.id.
 | 
        
           |  |  | 386 |      *      Versions in descending order.
 | 
        
           |  |  | 387 |      */
 | 
        
           |  |  | 388 |     public static function get_all_versions_of_questions(array $questionids): array {
 | 
        
           |  |  | 389 |         global $DB;
 | 
        
           |  |  | 390 |   | 
        
           |  |  | 391 |         [$listquestionid, $params] = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED);
 | 
        
           |  |  | 392 |         $sql = "SELECT qv.questionid, qv.version, qv.questionbankentryid
 | 
        
           |  |  | 393 |                   FROM {question_versions} qv
 | 
        
           |  |  | 394 |                   JOIN {question_versions} qv2 ON qv.questionbankentryid = qv2.questionbankentryid
 | 
        
           |  |  | 395 |                  WHERE qv2.questionid $listquestionid
 | 
        
           |  |  | 396 |               ORDER BY qv.questionbankentryid, qv.version DESC";
 | 
        
           |  |  | 397 |         $result = [];
 | 
        
           |  |  | 398 |         $rows = $DB->get_recordset_sql($sql, $params);
 | 
        
           |  |  | 399 |         foreach ($rows as $row) {
 | 
        
           |  |  | 400 |             $result[$row->questionbankentryid][$row->version] = $row->questionid;
 | 
        
           |  |  | 401 |         }
 | 
        
           |  |  | 402 |   | 
        
           |  |  | 403 |         return $result;
 | 
        
           |  |  | 404 |     }
 | 
        
           |  |  | 405 |   | 
        
           |  |  | 406 |     /**
 | 
        
           | 1441 | ariadna | 407 |      * Retrieves version information for a list of questions.
 | 
        
           |  |  | 408 |      *
 | 
        
           |  |  | 409 |      * @param array $questionids Array of question ids.
 | 
        
           |  |  | 410 |      * @return array An array question_bank_entries.id => version number => question.id.
 | 
        
           |  |  | 411 |      */
 | 
        
           |  |  | 412 |     public static function get_version_of_questions(array $questionids): array {
 | 
        
           |  |  | 413 |         global $DB;
 | 
        
           |  |  | 414 |   | 
        
           |  |  | 415 |         [$listquestionid, $params] = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED);
 | 
        
           |  |  | 416 |         $sql = "SELECT qv.questionid, qv.version, qv.questionbankentryid
 | 
        
           |  |  | 417 |                   FROM {question_versions} qv
 | 
        
           |  |  | 418 |                   JOIN {question_bank_entries} qbe ON qv.questionbankentryid = qbe.id
 | 
        
           |  |  | 419 |                  WHERE qv.questionid $listquestionid
 | 
        
           |  |  | 420 |               ORDER BY qv.version DESC";
 | 
        
           |  |  | 421 |   | 
        
           |  |  | 422 |         $rows = $DB->get_recordset_sql($sql, $params);
 | 
        
           |  |  | 423 |         $result = [];
 | 
        
           |  |  | 424 |         foreach ($rows as $row) {
 | 
        
           |  |  | 425 |             $result[$row->questionbankentryid][$row->version] = $row->questionid;
 | 
        
           |  |  | 426 |         }
 | 
        
           |  |  | 427 |   | 
        
           |  |  | 428 |         return $result;
 | 
        
           |  |  | 429 |     }
 | 
        
           |  |  | 430 |   | 
        
           |  |  | 431 |     /**
 | 
        
           | 1 | efrain | 432 |      * @return question_finder a question finder.
 | 
        
           |  |  | 433 |      */
 | 
        
           |  |  | 434 |     public static function get_finder() {
 | 
        
           |  |  | 435 |         return question_finder::get_instance();
 | 
        
           |  |  | 436 |     }
 | 
        
           |  |  | 437 |   | 
        
           |  |  | 438 |     /**
 | 
        
           |  |  | 439 |      * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
 | 
        
           |  |  | 440 |      */
 | 
        
           |  |  | 441 |     public static function start_unit_test() {
 | 
        
           |  |  | 442 |         self::$testmode = true;
 | 
        
           |  |  | 443 |     }
 | 
        
           |  |  | 444 |   | 
        
           |  |  | 445 |     /**
 | 
        
           |  |  | 446 |      * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
 | 
        
           |  |  | 447 |      */
 | 
        
           |  |  | 448 |     public static function end_unit_test() {
 | 
        
           |  |  | 449 |         self::$testmode = false;
 | 
        
           |  |  | 450 |         self::$testdata = array();
 | 
        
           |  |  | 451 |     }
 | 
        
           |  |  | 452 |   | 
        
           |  |  | 453 |     private static function return_test_question_data($questionid) {
 | 
        
           |  |  | 454 |         if (!isset(self::$testdata[$questionid])) {
 | 
        
           |  |  | 455 |             throw new coding_exception('question_bank::return_test_data(' . $questionid .
 | 
        
           |  |  | 456 |                     ') called, but no matching question has been loaded by load_test_data.');
 | 
        
           |  |  | 457 |         }
 | 
        
           |  |  | 458 |         return self::$testdata[$questionid];
 | 
        
           |  |  | 459 |     }
 | 
        
           |  |  | 460 |   | 
        
           |  |  | 461 |     /**
 | 
        
           |  |  | 462 |      * To be used for unit testing only. Will throw an exception if
 | 
        
           |  |  | 463 |      * {@link start_unit_test()} has not been called first.
 | 
        
           |  |  | 464 |      * @param object $questiondata a question data object to put in the test data store.
 | 
        
           |  |  | 465 |      */
 | 
        
           |  |  | 466 |     public static function load_test_question_data(question_definition $question) {
 | 
        
           |  |  | 467 |         if (!self::$testmode) {
 | 
        
           |  |  | 468 |             throw new coding_exception('question_bank::load_test_data called when ' .
 | 
        
           |  |  | 469 |                     'not in test mode.');
 | 
        
           |  |  | 470 |         }
 | 
        
           |  |  | 471 |         self::$testdata[$question->id] = $question;
 | 
        
           |  |  | 472 |     }
 | 
        
           |  |  | 473 |   | 
        
           |  |  | 474 |     protected static function ensure_fraction_options_initialised() {
 | 
        
           |  |  | 475 |         if (!is_null(self::$fractionoptions)) {
 | 
        
           |  |  | 476 |             return;
 | 
        
           |  |  | 477 |         }
 | 
        
           |  |  | 478 |   | 
        
           |  |  | 479 |         // define basic array of grades. This list comprises all fractions of the form:
 | 
        
           |  |  | 480 |         // a. p/q for q <= 6, 0 <= p <= q
 | 
        
           |  |  | 481 |         // b. p/10 for 0 <= p <= 10
 | 
        
           |  |  | 482 |         // c. 1/q for 1 <= q <= 10
 | 
        
           |  |  | 483 |         // d. 1/20
 | 
        
           |  |  | 484 |         $rawfractions = array(
 | 
        
           |  |  | 485 |             0.9000000,
 | 
        
           |  |  | 486 |             0.8333333,
 | 
        
           |  |  | 487 |             0.8000000,
 | 
        
           |  |  | 488 |             0.7500000,
 | 
        
           |  |  | 489 |             0.7000000,
 | 
        
           |  |  | 490 |             0.6666667,
 | 
        
           |  |  | 491 |             0.6000000,
 | 
        
           |  |  | 492 |             0.5000000,
 | 
        
           |  |  | 493 |             0.4000000,
 | 
        
           |  |  | 494 |             0.3333333,
 | 
        
           |  |  | 495 |             0.3000000,
 | 
        
           |  |  | 496 |             0.2500000,
 | 
        
           |  |  | 497 |             0.2000000,
 | 
        
           |  |  | 498 |             0.1666667,
 | 
        
           |  |  | 499 |             0.1428571,
 | 
        
           |  |  | 500 |             0.1250000,
 | 
        
           |  |  | 501 |             0.1111111,
 | 
        
           |  |  | 502 |             0.1000000,
 | 
        
           |  |  | 503 |             0.0500000,
 | 
        
           |  |  | 504 |         );
 | 
        
           |  |  | 505 |   | 
        
           |  |  | 506 |         // Put the None option at the top.
 | 
        
           |  |  | 507 |         self::$fractionoptions = array(
 | 
        
           |  |  | 508 |             '0.0' => get_string('none'),
 | 
        
           |  |  | 509 |             '1.0' => '100%',
 | 
        
           |  |  | 510 |         );
 | 
        
           |  |  | 511 |         self::$fractionoptionsfull = array(
 | 
        
           |  |  | 512 |             '0.0' => get_string('none'),
 | 
        
           |  |  | 513 |             '1.0' => '100%',
 | 
        
           |  |  | 514 |         );
 | 
        
           |  |  | 515 |   | 
        
           |  |  | 516 |         // The the positive grades in descending order.
 | 
        
           |  |  | 517 |         foreach ($rawfractions as $fraction) {
 | 
        
           |  |  | 518 |             $percentage = format_float(100 * $fraction, 5, true, true) . '%';
 | 
        
           |  |  | 519 |             self::$fractionoptions["{$fraction}"] = $percentage;
 | 
        
           |  |  | 520 |             self::$fractionoptionsfull["{$fraction}"] = $percentage;
 | 
        
           |  |  | 521 |         }
 | 
        
           |  |  | 522 |   | 
        
           |  |  | 523 |         // The the negative grades in descending order.
 | 
        
           |  |  | 524 |         foreach (array_reverse($rawfractions) as $fraction) {
 | 
        
           |  |  | 525 |             self::$fractionoptionsfull['' . (-$fraction)] =
 | 
        
           |  |  | 526 |                     format_float(-100 * $fraction, 5, true, true) . '%';
 | 
        
           |  |  | 527 |         }
 | 
        
           |  |  | 528 |   | 
        
           |  |  | 529 |         self::$fractionoptionsfull['-1.0'] = '-100%';
 | 
        
           |  |  | 530 |     }
 | 
        
           |  |  | 531 |   | 
        
           |  |  | 532 |     /**
 | 
        
           |  |  | 533 |      * @return array string => string The standard set of grade options (fractions)
 | 
        
           |  |  | 534 |      * to use when editing questions, in the range 0 to 1 inclusive. Array keys
 | 
        
           |  |  | 535 |      * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
 | 
        
           |  |  | 536 |      * have float array keys in PHP.
 | 
        
           |  |  | 537 |      * Initialised by {@link ensure_grade_options_initialised()}.
 | 
        
           |  |  | 538 |      */
 | 
        
           |  |  | 539 |     public static function fraction_options() {
 | 
        
           |  |  | 540 |         self::ensure_fraction_options_initialised();
 | 
        
           |  |  | 541 |         return self::$fractionoptions;
 | 
        
           |  |  | 542 |     }
 | 
        
           |  |  | 543 |   | 
        
           |  |  | 544 |     /** @return array string => string The full standard set of (fractions) -1 to 1 inclusive. */
 | 
        
           |  |  | 545 |     public static function fraction_options_full() {
 | 
        
           |  |  | 546 |         self::ensure_fraction_options_initialised();
 | 
        
           |  |  | 547 |         return self::$fractionoptionsfull;
 | 
        
           |  |  | 548 |     }
 | 
        
           |  |  | 549 |   | 
        
           |  |  | 550 |     /**
 | 
        
           |  |  | 551 |      * Return a list of the different question types present in the given categories.
 | 
        
           |  |  | 552 |      *
 | 
        
           |  |  | 553 |      * @param  array $categories a list of category ids
 | 
        
           |  |  | 554 |      * @return array the list of question types in the categories
 | 
        
           |  |  | 555 |      * @since  Moodle 3.1
 | 
        
           |  |  | 556 |      */
 | 
        
           |  |  | 557 |     public static function get_all_question_types_in_categories($categories) {
 | 
        
           |  |  | 558 |         global $DB;
 | 
        
           |  |  | 559 |   | 
        
           |  |  | 560 |         list($categorysql, $params) = $DB->get_in_or_equal($categories);
 | 
        
           |  |  | 561 |         $sql = "SELECT DISTINCT q.qtype
 | 
        
           |  |  | 562 |                            FROM {question} q
 | 
        
           |  |  | 563 |                            JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 564 |                            JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 565 |                           WHERE qbe.questioncategoryid $categorysql";
 | 
        
           |  |  | 566 |   | 
        
           |  |  | 567 |         $qtypes = $DB->get_fieldset_sql($sql, $params);
 | 
        
           |  |  | 568 |         return $qtypes;
 | 
        
           |  |  | 569 |     }
 | 
        
           |  |  | 570 | }
 | 
        
           |  |  | 571 |   | 
        
           |  |  | 572 |   | 
        
           |  |  | 573 | /**
 | 
        
           |  |  | 574 |  * Class for loading questions according to various criteria.
 | 
        
           |  |  | 575 |  *
 | 
        
           |  |  | 576 |  * @copyright  2009 The Open University
 | 
        
           |  |  | 577 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 578 |  */
 | 
        
           | 1441 | ariadna | 579 | class question_finder implements data_source_interface {
 | 
        
           | 1 | efrain | 580 |     /** @var question_finder the singleton instance of this class. */
 | 
        
           |  |  | 581 |     protected static $questionfinder = null;
 | 
        
           |  |  | 582 |   | 
        
           |  |  | 583 |     /**
 | 
        
           |  |  | 584 |      * @return question_finder a question finder.
 | 
        
           |  |  | 585 |      */
 | 
        
           |  |  | 586 |     public static function get_instance() {
 | 
        
           |  |  | 587 |         if (is_null(self::$questionfinder)) {
 | 
        
           |  |  | 588 |             self::$questionfinder = new question_finder();
 | 
        
           |  |  | 589 |         }
 | 
        
           |  |  | 590 |         return self::$questionfinder;
 | 
        
           |  |  | 591 |     }
 | 
        
           |  |  | 592 |   | 
        
           | 1441 | ariadna | 593 |     #[\Override]
 | 
        
           |  |  | 594 |     public static function get_instance_for_cache(definition $definition) {
 | 
        
           | 1 | efrain | 595 |         return self::get_instance();
 | 
        
           |  |  | 596 |     }
 | 
        
           |  |  | 597 |   | 
        
           |  |  | 598 |     /**
 | 
        
           | 1441 | ariadna | 599 |      * @return application_cache the question definition cache we are using.
 | 
        
           | 1 | efrain | 600 |      */
 | 
        
           |  |  | 601 |     protected function get_data_cache() {
 | 
        
           |  |  | 602 |         // Do not double cache here because it may break cache resetting.
 | 
        
           |  |  | 603 |         return cache::make('core', 'questiondata');
 | 
        
           |  |  | 604 |     }
 | 
        
           |  |  | 605 |   | 
        
           |  |  | 606 |     /**
 | 
        
           |  |  | 607 |      * This method needs to be called whenever a question is edited.
 | 
        
           |  |  | 608 |      */
 | 
        
           |  |  | 609 |     public function uncache_question($questionid) {
 | 
        
           |  |  | 610 |         $this->get_data_cache()->delete($questionid);
 | 
        
           |  |  | 611 |     }
 | 
        
           |  |  | 612 |   | 
        
           |  |  | 613 |     /**
 | 
        
           |  |  | 614 |      * Load a question definition data from the database. The data will be
 | 
        
           |  |  | 615 |      * returned as a plain stdClass object.
 | 
        
           |  |  | 616 |      * @param int $questionid the id of the question to load.
 | 
        
           |  |  | 617 |      * @return object question definition loaded from the database.
 | 
        
           |  |  | 618 |      */
 | 
        
           |  |  | 619 |     public function load_question_data($questionid) {
 | 
        
           |  |  | 620 |         return $this->get_data_cache()->get($questionid);
 | 
        
           |  |  | 621 |     }
 | 
        
           |  |  | 622 |   | 
        
           |  |  | 623 |     /**
 | 
        
           |  |  | 624 |      * Get the ids of all the questions in a list of categories.
 | 
        
           |  |  | 625 |      * @param array $categoryids either a category id, or a comma-separated list
 | 
        
           |  |  | 626 |      *      of category ids, or an array of them.
 | 
        
           |  |  | 627 |      * @param string $extraconditions extra conditions to AND with the rest of
 | 
        
           |  |  | 628 |      *      the where clause. Must use named parameters.
 | 
        
           |  |  | 629 |      * @param array $extraparams any parameters used by $extraconditions.
 | 
        
           |  |  | 630 |      * @return array questionid => questionid.
 | 
        
           |  |  | 631 |      */
 | 
        
           |  |  | 632 |     public function get_questions_from_categories($categoryids, $extraconditions,
 | 
        
           |  |  | 633 |             $extraparams = array()) {
 | 
        
           |  |  | 634 |         global $DB;
 | 
        
           |  |  | 635 |   | 
        
           |  |  | 636 |         list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
 | 
        
           |  |  | 637 |   | 
        
           |  |  | 638 |         if ($extraconditions) {
 | 
        
           |  |  | 639 |             $extraconditions = ' AND (' . $extraconditions . ')';
 | 
        
           |  |  | 640 |         }
 | 
        
           |  |  | 641 |         $qcparams['readystatus'] = question_version_status::QUESTION_STATUS_READY;
 | 
        
           |  |  | 642 |         $qcparams['readystatusqv'] = question_version_status::QUESTION_STATUS_READY;
 | 
        
           |  |  | 643 |         $sql = "SELECT q.id, q.id AS id2
 | 
        
           |  |  | 644 |                   FROM {question} q
 | 
        
           |  |  | 645 |                   JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 646 |                   JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 647 |                  WHERE qbe.questioncategoryid {$qcsql}
 | 
        
           |  |  | 648 |                        AND q.parent = 0
 | 
        
           |  |  | 649 |                        AND qv.status = :readystatus
 | 
        
           |  |  | 650 |                        AND qv.version = (SELECT MAX(v.version)
 | 
        
           |  |  | 651 |                                           FROM {question_versions} v
 | 
        
           |  |  | 652 |                                           JOIN {question_bank_entries} be
 | 
        
           |  |  | 653 |                                             ON be.id = v.questionbankentryid
 | 
        
           |  |  | 654 |                                          WHERE be.id = qbe.id
 | 
        
           |  |  | 655 |                                            AND v.status = :readystatusqv)
 | 
        
           |  |  | 656 |                        {$extraconditions}";
 | 
        
           |  |  | 657 |   | 
        
           |  |  | 658 |         return $DB->get_records_sql_menu($sql, $qcparams + $extraparams);
 | 
        
           |  |  | 659 |     }
 | 
        
           |  |  | 660 |   | 
        
           |  |  | 661 |     /**
 | 
        
           |  |  | 662 |      * Get the ids of all the questions in a list of categories, with the number
 | 
        
           |  |  | 663 |      * of times they have already been used in a given set of usages.
 | 
        
           |  |  | 664 |      *
 | 
        
           |  |  | 665 |      * The result array is returned in order of increasing (count previous uses).
 | 
        
           |  |  | 666 |      *
 | 
        
           |  |  | 667 |      * @param array $categoryids an array question_category ids.
 | 
        
           |  |  | 668 |      * @param qubaid_condition $qubaids which question_usages to count previous uses from.
 | 
        
           |  |  | 669 |      * @param string $extraconditions extra conditions to AND with the rest of
 | 
        
           |  |  | 670 |      *      the where clause. Must use named parameters.
 | 
        
           |  |  | 671 |      * @param array $extraparams any parameters used by $extraconditions.
 | 
        
           |  |  | 672 |      * @return array questionid => count of number of previous uses.
 | 
        
           |  |  | 673 |      *
 | 
        
           |  |  | 674 |      * @deprecated since Moodle 4.3
 | 
        
           |  |  | 675 |      * @todo Final deprecation on Moodle 4.7 MDL-78091
 | 
        
           |  |  | 676 |      */
 | 
        
           |  |  | 677 |     public function get_questions_from_categories_with_usage_counts($categoryids,
 | 
        
           |  |  | 678 |             qubaid_condition $qubaids, $extraconditions = '', $extraparams = array()) {
 | 
        
           |  |  | 679 |         debugging(
 | 
        
           |  |  | 680 |             'Function get_questions_from_categories_with_usage_counts() is deprecated, please do not use the function.',
 | 
        
           |  |  | 681 |             DEBUG_DEVELOPER
 | 
        
           |  |  | 682 |         );
 | 
        
           |  |  | 683 |         return $this->get_questions_from_categories_and_tags_with_usage_counts(
 | 
        
           |  |  | 684 |                 $categoryids, $qubaids, $extraconditions, $extraparams);
 | 
        
           |  |  | 685 |     }
 | 
        
           |  |  | 686 |   | 
        
           |  |  | 687 |     /**
 | 
        
           |  |  | 688 |      * Get the ids of all the questions in a list of categories that have ALL the provided tags,
 | 
        
           |  |  | 689 |      * with the number of times they have already been used in a given set of usages.
 | 
        
           |  |  | 690 |      *
 | 
        
           |  |  | 691 |      * The result array is returned in order of increasing (count previous uses).
 | 
        
           |  |  | 692 |      *
 | 
        
           |  |  | 693 |      * @param array $categoryids an array of question_category ids.
 | 
        
           |  |  | 694 |      * @param qubaid_condition $qubaids which question_usages to count previous uses from.
 | 
        
           |  |  | 695 |      * @param string $extraconditions extra conditions to AND with the rest of
 | 
        
           |  |  | 696 |      *      the where clause. Must use named parameters.
 | 
        
           |  |  | 697 |      * @param array $extraparams any parameters used by $extraconditions.
 | 
        
           |  |  | 698 |      * @param array $tagids an array of tag ids
 | 
        
           |  |  | 699 |      * @return array questionid => count of number of previous uses.
 | 
        
           |  |  | 700 |      * @deprecated since Moodle 4.3
 | 
        
           |  |  | 701 |      * @todo Final deprecation on Moodle 4.7 MDL-78091
 | 
        
           |  |  | 702 |      */
 | 
        
           |  |  | 703 |     public function get_questions_from_categories_and_tags_with_usage_counts($categoryids,
 | 
        
           |  |  | 704 |             qubaid_condition $qubaids, $extraconditions = '', $extraparams = array(), $tagids = array()) {
 | 
        
           |  |  | 705 |         debugging(
 | 
        
           |  |  | 706 |             'Function get_questions_from_categories_and_tags_with_usage_counts() is deprecated, please do not use the function.',
 | 
        
           |  |  | 707 |             DEBUG_DEVELOPER
 | 
        
           |  |  | 708 |         );
 | 
        
           |  |  | 709 |         global $DB;
 | 
        
           |  |  | 710 |   | 
        
           |  |  | 711 |         list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
 | 
        
           |  |  | 712 |   | 
        
           |  |  | 713 |         $readystatus = question_version_status::QUESTION_STATUS_READY;
 | 
        
           |  |  | 714 |         $select = "q.id, (SELECT COUNT(1)
 | 
        
           |  |  | 715 |                             FROM " . $qubaids->from_question_attempts('qa') . "
 | 
        
           |  |  | 716 |                            WHERE qa.questionid = q.id AND " . $qubaids->where() . "
 | 
        
           |  |  | 717 |                          ) AS previous_attempts";
 | 
        
           |  |  | 718 |         $from   = "{question} q";
 | 
        
           |  |  | 719 |         $join   = "JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 720 |                    JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid";
 | 
        
           |  |  | 721 |         $from = $from . " " . $join;
 | 
        
           |  |  | 722 |         $where  = "qbe.questioncategoryid {$qcsql}
 | 
        
           |  |  | 723 |                AND q.parent = 0
 | 
        
           |  |  | 724 |                AND qv.status = '$readystatus'
 | 
        
           |  |  | 725 |                AND qv.version = (SELECT MAX(v.version)
 | 
        
           |  |  | 726 |                                   FROM {question_versions} v
 | 
        
           |  |  | 727 |                                   JOIN {question_bank_entries} be
 | 
        
           |  |  | 728 |                                     ON be.id = v.questionbankentryid
 | 
        
           |  |  | 729 |                                  WHERE be.id = qbe.id)";
 | 
        
           |  |  | 730 |         $params = $qcparams;
 | 
        
           |  |  | 731 |   | 
        
           |  |  | 732 |         if (!empty($tagids)) {
 | 
        
           |  |  | 733 |             // We treat each additional tag as an AND condition rather than
 | 
        
           |  |  | 734 |             // an OR condition.
 | 
        
           |  |  | 735 |             //
 | 
        
           |  |  | 736 |             // For example, if the user filters by the tags "foo" and "bar" then
 | 
        
           |  |  | 737 |             // we reduce the question list to questions that are tagged with both
 | 
        
           |  |  | 738 |             // "foo" AND "bar". Any question that does not have ALL of the specified
 | 
        
           |  |  | 739 |             // tags will be omitted.
 | 
        
           |  |  | 740 |             list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids, SQL_PARAMS_NAMED, 'ti');
 | 
        
           |  |  | 741 |             $tagparams['tagcount'] = count($tagids);
 | 
        
           |  |  | 742 |             $tagparams['questionitemtype'] = 'question';
 | 
        
           |  |  | 743 |             $tagparams['questioncomponent'] = 'core_question';
 | 
        
           |  |  | 744 |             $where .= " AND q.id IN (SELECT ti.itemid
 | 
        
           |  |  | 745 |                                        FROM {tag_instance} ti
 | 
        
           |  |  | 746 |                                       WHERE ti.itemtype = :questionitemtype
 | 
        
           |  |  | 747 |                                             AND ti.component = :questioncomponent
 | 
        
           |  |  | 748 |                                             AND ti.tagid {$tagsql}
 | 
        
           |  |  | 749 |                                    GROUP BY ti.itemid
 | 
        
           |  |  | 750 |                                      HAVING COUNT(itemid) = :tagcount)";
 | 
        
           |  |  | 751 |             $params += $tagparams;
 | 
        
           |  |  | 752 |         }
 | 
        
           |  |  | 753 |   | 
        
           |  |  | 754 |         if ($extraconditions) {
 | 
        
           |  |  | 755 |             $extraconditions = ' AND (' . $extraconditions . ')';
 | 
        
           |  |  | 756 |         }
 | 
        
           |  |  | 757 |   | 
        
           |  |  | 758 |         return $DB->get_records_sql_menu("SELECT $select
 | 
        
           |  |  | 759 |                                                 FROM $from
 | 
        
           |  |  | 760 |                                                WHERE $where $extraconditions
 | 
        
           |  |  | 761 |                                             ORDER BY previous_attempts",
 | 
        
           |  |  | 762 |                 $qubaids->from_where_params() + $params + $extraparams);
 | 
        
           |  |  | 763 |     }
 | 
        
           |  |  | 764 |   | 
        
           | 1441 | ariadna | 765 |     #[\Override]
 | 
        
           | 1 | efrain | 766 |     public function load_for_cache($questionid) {
 | 
        
           |  |  | 767 |         global $DB;
 | 
        
           |  |  | 768 |   | 
        
           |  |  | 769 |         $sql = 'SELECT q.id, qc.id as category, q.parent, q.name, q.questiontext, q.questiontextformat,
 | 
        
           |  |  | 770 |                        q.generalfeedback, q.generalfeedbackformat, q.defaultmark, q.penalty, q.qtype,
 | 
        
           |  |  | 771 |                        q.length, q.stamp, q.timecreated, q.timemodified,
 | 
        
           |  |  | 772 |                        q.createdby, q.modifiedby, qbe.idnumber,
 | 
        
           |  |  | 773 |                        qc.contextid,
 | 
        
           |  |  | 774 |                        qv.status,
 | 
        
           |  |  | 775 |                        qv.id as versionid,
 | 
        
           |  |  | 776 |                        qv.version,
 | 
        
           |  |  | 777 |                        qv.questionbankentryid
 | 
        
           |  |  | 778 |                   FROM {question} q
 | 
        
           |  |  | 779 |                   JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 780 |                   JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 781 |                   JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 | 
        
           |  |  | 782 |                  WHERE q.id = :id';
 | 
        
           |  |  | 783 |   | 
        
           |  |  | 784 |         $questiondata = $DB->get_record_sql($sql, ['id' => $questionid], MUST_EXIST);
 | 
        
           |  |  | 785 |         get_question_options($questiondata);
 | 
        
           |  |  | 786 |         return $questiondata;
 | 
        
           |  |  | 787 |     }
 | 
        
           |  |  | 788 |   | 
        
           | 1441 | ariadna | 789 |     #[\Override]
 | 
        
           | 1 | efrain | 790 |     public function load_many_for_cache(array $questionids) {
 | 
        
           |  |  | 791 |         global $DB;
 | 
        
           |  |  | 792 |   | 
        
           |  |  | 793 |         list($idcondition, $params) = $DB->get_in_or_equal($questionids);
 | 
        
           |  |  | 794 |         $sql = 'SELECT q.id, qc.id as category, q.parent, q.name, q.questiontext, q.questiontextformat,
 | 
        
           |  |  | 795 |                        q.generalfeedback, q.generalfeedbackformat, q.defaultmark, q.penalty, q.qtype,
 | 
        
           |  |  | 796 |                        q.length, q.stamp, q.timecreated, q.timemodified,
 | 
        
           |  |  | 797 |                        q.createdby, q.modifiedby, qbe.idnumber,
 | 
        
           |  |  | 798 |                        qc.contextid,
 | 
        
           |  |  | 799 |                        qv.status,
 | 
        
           |  |  | 800 |                        qv.id as versionid,
 | 
        
           |  |  | 801 |                        qv.version,
 | 
        
           |  |  | 802 |                        qv.questionbankentryid
 | 
        
           |  |  | 803 |                   FROM {question} q
 | 
        
           |  |  | 804 |                   JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 805 |                   JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 806 |                   JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 | 
        
           |  |  | 807 |                  WHERE q.id ';
 | 
        
           |  |  | 808 |   | 
        
           |  |  | 809 |         $questiondata = $DB->get_records_sql($sql . $idcondition, $params);
 | 
        
           |  |  | 810 |   | 
        
           |  |  | 811 |         foreach ($questionids as $id) {
 | 
        
           |  |  | 812 |             if (!array_key_exists($id, $questiondata)) {
 | 
        
           |  |  | 813 |                 throw new dml_missing_record_exception('question', '', ['id' => $id]);
 | 
        
           |  |  | 814 |             }
 | 
        
           |  |  | 815 |             get_question_options($questiondata[$id]);
 | 
        
           |  |  | 816 |         }
 | 
        
           |  |  | 817 |         return $questiondata;
 | 
        
           |  |  | 818 |     }
 | 
        
           |  |  | 819 | }
 |