2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * More object oriented wrappers around parts of the Moodle question bank.
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
25 * @subpackage questionbank
26 * @copyright 2009 The Open University
27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 defined('MOODLE_INTERNAL') || die();
33 require_once(dirname(__FILE__) . '/../type/questiontypebase.php');
37 * This static class provides access to the other question bank.
39 * It provides functions for managing question types and question definitions.
41 * @copyright 2009 The Open University
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44 abstract class question_bank {
45 // TODO: This limit can be deleted if someday we move all TEXTS to BIG ones. MDL-19603
46 const MAX_SUMMARY_LENGTH = 32000;
48 /** @var array question type name => question_type subclass. */
49 private static $questiontypes = array();
51 /** @var array question type name => 1. Records which question definitions have been loaded. */
52 private static $loadedqdefs = array();
54 protected static $questionfinder = null;
56 /** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
57 private static $testmode = false;
58 private static $testdata = array();
60 private static $questionconfig = null;
63 * @var array string => string The standard set of grade options (fractions)
64 * to use when editing questions, in the range 0 to 1 inclusive. Array keys
65 * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
66 * have float array keys in PHP.
67 * Initialised by {@link ensure_grade_options_initialised()}.
69 private static $fractionoptions = null;
70 /** @var array string => string The full standard set of (fractions) -1 to 1 inclusive. */
71 private static $fractionoptionsfull = null;
74 * @param string $qtypename a question type name, e.g. 'multichoice'.
75 * @return bool whether that question type is installed in this Moodle.
77 public static function is_qtype_installed($qtypename) {
78 $plugindir = get_plugin_directory('qtype', $qtypename);
79 return $plugindir && is_readable($plugindir . '/questiontype.php');
83 * Get the question type class for a particular question type.
84 * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
85 * @param bool $mustexist if false, the missing question type is returned when
86 * the requested question type is not installed.
87 * @return question_type the corresponding question type class.
89 public static function get_qtype($qtypename, $mustexist = true) {
91 if (isset(self::$questiontypes[$qtypename])) {
92 return self::$questiontypes[$qtypename];
94 $file = get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
95 if (!is_readable($file)) {
96 if ($mustexist || $qtypename == 'missingtype') {
97 throw new coding_exception('Unknown question type ' . $qtypename);
99 return self::get_qtype('missingtype');
103 $class = 'qtype_' . $qtypename;
104 if (!class_exists($class)) {
105 throw new coding_exception("Class $class must be defined in $file");
107 self::$questiontypes[$qtypename] = new $class();
108 return self::$questiontypes[$qtypename];
112 * Load the question configuration data from config_plugins.
113 * @return object get_config('question') with caching.
115 public static function get_config() {
116 if (is_null(self::$questionconfig)) {
117 self::$questionconfig = get_config('question');
119 return self::$questionconfig;
123 * @param string $qtypename the internal name of a question type. For example multichoice.
124 * @return bool whether users are allowed to create questions of this type.
126 public static function qtype_enabled($qtypename) {
127 $config = self::get_config();
128 $enabledvar = $qtypename . '_disabled';
129 return self::qtype_exists($qtypename) && empty($config->$enabledvar) &&
130 self::get_qtype($qtypename)->menu_name() != '';
134 * @param string $qtypename the internal name of a question type. For example multichoice.
135 * @return bool whether this question type exists.
137 public static function qtype_exists($qtypename) {
138 return array_key_exists($qtypename, get_plugin_list('qtype'));
142 * @param $qtypename the internal name of a question type, for example multichoice.
143 * @return string the human_readable name of this question type, from the language pack.
145 public static function get_qtype_name($qtypename) {
146 return self::get_qtype($qtypename)->local_name();
150 * @return array all the installed question types.
152 public static function get_all_qtypes() {
154 foreach (get_plugin_list('qtype') as $plugin => $notused) {
156 $qtypes[$plugin] = self::get_qtype($plugin);
157 } catch (coding_exception $e) {
158 // Catching coding_exceptions here means that incompatible
159 // question types do not cause the rest of Moodle to break.
166 * Sort an array of question types according to the order the admin set up,
167 * and then alphabetically for the rest.
168 * @param array qtype->name() => qtype->local_name().
169 * @return array sorted array.
171 public static function sort_qtype_array($qtypes, $config = null) {
172 if (is_null($config)) {
173 $config = self::get_config();
176 $sortorder = array();
177 $otherqtypes = array();
178 foreach ($qtypes as $name => $localname) {
179 $sortvar = $name . '_sortorder';
180 if (isset($config->$sortvar)) {
181 $sortorder[$config->$sortvar] = $name;
183 $otherqtypes[$name] = $localname;
188 collatorlib::asort($otherqtypes);
190 $sortedqtypes = array();
191 foreach ($sortorder as $name) {
192 $sortedqtypes[$name] = $qtypes[$name];
194 foreach ($otherqtypes as $name => $notused) {
195 $sortedqtypes[$name] = $qtypes[$name];
197 return $sortedqtypes;
201 * @return array all the question types that users are allowed to create,
202 * sorted into the preferred order set on the admin screen.
204 public static function get_creatable_qtypes() {
205 $config = self::get_config();
206 $allqtypes = self::get_all_qtypes();
208 $qtypenames = array();
209 foreach ($allqtypes as $name => $qtype) {
210 if (self::qtype_enabled($name)) {
211 $qtypenames[$name] = $qtype->local_name();
215 $qtypenames = self::sort_qtype_array($qtypenames);
217 $creatableqtypes = array();
218 foreach ($qtypenames as $name => $notused) {
219 $creatableqtypes[$name] = $allqtypes[$name];
221 return $creatableqtypes;
225 * Load the question definition class(es) belonging to a question type. That is,
226 * include_once('/question/type/' . $qtypename . '/question.php'), with a bit
228 * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
230 public static function load_question_definition_classes($qtypename) {
232 if (isset(self::$loadedqdefs[$qtypename])) {
235 $file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
236 if (!is_readable($file)) {
237 throw new coding_exception('Unknown question type (no definition) ' . $qtypename);
240 self::$loadedqdefs[$qtypename] = 1;
244 * Load a question definition from the database. The object returned
245 * will actually be of an appropriate {@link question_definition} subclass.
246 * @param int $questionid the id of the question to load.
247 * @param bool $allowshuffle if false, then any shuffle option on the selected
248 * quetsion is disabled.
249 * @return question_definition loaded from the database.
251 public static function load_question($questionid, $allowshuffle = true) {
254 if (self::$testmode) {
255 // Evil, test code in production, but now way round it.
256 return self::return_test_question_data($questionid);
259 $questiondata = $DB->get_record_sql('
260 SELECT q.*, qc.contextid
262 JOIN {question_categories} qc ON q.category = qc.id
263 WHERE q.id = :id', array('id' => $questionid), MUST_EXIST);
264 get_question_options($questiondata);
265 if (!$allowshuffle) {
266 $questiondata->options->shuffleanswers = false;
268 return self::make_question($questiondata);
272 * Convert the question information loaded with {@link get_question_options()}
273 * to a question_definintion object.
274 * @param object $questiondata raw data loaded from the database.
275 * @return question_definition loaded from the database.
277 public static function make_question($questiondata) {
278 return self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
282 * @return question_finder a question finder.
284 public static function get_finder() {
285 if (is_null(self::$questionfinder)) {
286 self::$questionfinder = new question_finder();
288 return self::$questionfinder;
292 * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
294 public static function start_unit_test() {
295 self::$testmode = true;
299 * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
301 public static function end_unit_test() {
302 self::$testmode = false;
303 self::$testdata = array();
306 private static function return_test_question_data($questionid) {
307 if (!isset(self::$testdata[$questionid])) {
308 throw new coding_exception('question_bank::return_test_data(' . $questionid .
309 ') called, but no matching question has been loaded by load_test_data.');
311 return self::$testdata[$questionid];
315 * To be used for unit testing only. Will throw an exception if
316 * {@link start_unit_test()} has not been called first.
317 * @param object $questiondata a question data object to put in the test data store.
319 public static function load_test_question_data(question_definition $question) {
320 if (!self::$testmode) {
321 throw new coding_exception('question_bank::load_test_data called when ' .
322 'not in test mode.');
324 self::$testdata[$question->id] = $question;
327 protected function ensure_fraction_options_initialised() {
328 if (!is_null(self::$fractionoptions)) {
332 // define basic array of grades. This list comprises all fractions of the form:
333 // a. p/q for q <= 6, 0 <= p <= q
334 // b. p/10 for 0 <= p <= 10
335 // c. 1/q for 1 <= q <= 10
337 $rawfractions = array(
359 // Put the None option at the top.
360 self::$fractionoptions = array(
361 '0.0' => get_string('none'),
364 self::$fractionoptionsfull = array(
365 '0.0' => get_string('none'),
369 // The the positive grades in descending order.
370 foreach ($rawfractions as $fraction) {
371 $percentage = (100 * $fraction) . '%';
372 self::$fractionoptions["$fraction"] = $percentage;
373 self::$fractionoptionsfull["$fraction"] = $percentage;
376 // The the negative grades in descending order.
377 foreach (array_reverse($rawfractions) as $fraction) {
378 self::$fractionoptionsfull['' . (-$fraction)] = (-100 * $fraction) . '%';
381 self::$fractionoptionsfull['-1.0'] = '-100%';
385 * @return array string => string The standard set of grade options (fractions)
386 * to use when editing questions, in the range 0 to 1 inclusive. Array keys
387 * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
388 * have float array keys in PHP.
389 * Initialised by {@link ensure_grade_options_initialised()}.
391 public static function fraction_options() {
392 self::ensure_fraction_options_initialised();
393 return self::$fractionoptions;
396 /** @return array string => string The full standard set of (fractions) -1 to 1 inclusive. */
397 public static function fraction_options_full() {
398 self::ensure_fraction_options_initialised();
399 return self::$fractionoptionsfull;
405 * Class for loading questions according to various criteria.
407 * @copyright 2009 The Open University
408 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
410 class question_finder {
412 * Get the ids of all the questions in a list of categoryies.
413 * @param array $categoryids either a categoryid, or a comma-separated list
414 * category ids, or an array of them.
415 * @param string $extraconditions extra conditions to AND with the rest of
416 * the where clause. Must use named parameters.
417 * @param array $extraparams any parameters used by $extraconditions.
418 * @return array questionid => questionid.
420 public function get_questions_from_categories($categoryids, $extraconditions,
421 $extraparams = array()) {
424 list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
426 if ($extraconditions) {
427 $extraconditions = ' AND (' . $extraconditions . ')';
430 return $DB->get_records_select_menu('question',
434 $extraconditions", $qcparams + $extraparams, '', 'id,id AS id2');