MDL-27490 Implement a manage question behaviours admin page
[moodle.git] / question / engine / bank.php
blob656531510dfb6028fe7efc2ddf41c386d418d5f6
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.
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/>.
17 /**
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
22 * starting point.
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
31 defined('MOODLE_INTERNAL') || die();
33 require_once(dirname(__FILE__) . '/../type/questiontypebase.php');
36 /**
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 /** @var array question type name => question_type subclass. */
46 private static $questiontypes = array();
48 /** @var array question type name => 1. Records which question definitions have been loaded. */
49 private static $loadedqdefs = array();
51 protected static $questionfinder = null;
53 /** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
54 private static $testmode = false;
55 private static $testdata = array();
57 private static $questionconfig = null;
59 /**
60 * @var array string => string The standard set of grade options (fractions)
61 * to use when editing questions, in the range 0 to 1 inclusive. Array keys
62 * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
63 * have float array keys in PHP.
64 * Initialised by {@link ensure_grade_options_initialised()}.
66 private static $fractionoptions = null;
67 /** @var array string => string The full standard set of (fractions) -1 to 1 inclusive. */
68 private static $fractionoptionsfull = null;
70 /**
71 * @param string $qtypename a question type name, e.g. 'multichoice'.
72 * @return bool whether that question type is installed in this Moodle.
74 public static function is_qtype_installed($qtypename) {
75 $plugindir = get_plugin_directory('qtype', $qtypename);
76 return $plugindir && is_readable($plugindir . '/questiontype.php');
79 /**
80 * Get the question type class for a particular question type.
81 * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
82 * @param bool $mustexist if false, the missing question type is returned when
83 * the requested question type is not installed.
84 * @return question_type the corresponding question type class.
86 public static function get_qtype($qtypename, $mustexist = true) {
87 global $CFG;
88 if (isset(self::$questiontypes[$qtypename])) {
89 return self::$questiontypes[$qtypename];
91 $file = get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
92 if (!is_readable($file)) {
93 if ($mustexist || $qtypename == 'missingtype') {
94 throw new coding_exception('Unknown question type ' . $qtypename);
95 } else {
96 return self::get_qtype('missingtype');
99 include_once($file);
100 $class = 'qtype_' . $qtypename;
101 if (!class_exists($class)) {
102 throw new coding_exception("Class $class must be defined in $file");
104 self::$questiontypes[$qtypename] = new $class();
105 return self::$questiontypes[$qtypename];
109 * Load the question configuration data from config_plugins.
110 * @return object get_config('question') with caching.
112 public static function get_config() {
113 if (is_null(self::$questionconfig)) {
114 self::$questionconfig = get_config('question');
116 return self::$questionconfig;
120 * @param string $qtypename the internal name of a question type. For example multichoice.
121 * @return bool whether users are allowed to create questions of this type.
123 public static function qtype_enabled($qtypename) {
124 $config = self::get_config();
125 $enabledvar = $qtypename . '_disabled';
126 return self::qtype_exists($qtypename) && empty($config->$enabledvar) &&
127 self::get_qtype($qtypename)->menu_name() != '';
131 * @param string $qtypename the internal name of a question type. For example multichoice.
132 * @return bool whether this question type exists.
134 public static function qtype_exists($qtypename) {
135 return array_key_exists($qtypename, get_plugin_list('qtype'));
139 * @param $qtypename the internal name of a question type, for example multichoice.
140 * @return string the human_readable name of this question type, from the language pack.
142 public static function get_qtype_name($qtypename) {
143 return self::get_qtype($qtypename)->local_name();
147 * @return array all the installed question types.
149 public static function get_all_qtypes() {
150 $qtypes = array();
151 foreach (get_plugin_list('qtype') as $plugin => $notused) {
152 try {
153 $qtypes[$plugin] = self::get_qtype($plugin);
154 } catch (coding_exception $e) {
155 // Catching coding_exceptions here means that incompatible
156 // question types do not cause the rest of Moodle to break.
159 return $qtypes;
163 * Sort an array of question types according to the order the admin set up,
164 * and then alphabetically for the rest.
165 * @param array qtype->name() => qtype->local_name().
166 * @return array sorted array.
168 public static function sort_qtype_array($qtypes, $config = null) {
169 if (is_null($config)) {
170 $config = self::get_config();
173 $sortorder = array();
174 $otherqtypes = array();
175 foreach ($qtypes as $name => $localname) {
176 $sortvar = $name . '_sortorder';
177 if (isset($config->$sortvar)) {
178 $sortorder[$config->$sortvar] = $name;
179 } else {
180 $otherqtypes[$name] = $localname;
184 ksort($sortorder);
185 textlib_get_instance()->asort($otherqtypes);
187 $sortedqtypes = array();
188 foreach ($sortorder as $name) {
189 $sortedqtypes[$name] = $qtypes[$name];
191 foreach ($otherqtypes as $name => $notused) {
192 $sortedqtypes[$name] = $qtypes[$name];
194 return $sortedqtypes;
198 * @return array all the question types that users are allowed to create,
199 * sorted into the preferred order set on the admin screen.
201 public static function get_creatable_qtypes() {
202 $config = self::get_config();
203 $allqtypes = self::get_all_qtypes();
205 $qtypenames = array();
206 foreach ($allqtypes as $name => $qtype) {
207 if (self::qtype_enabled($name)) {
208 $qtypenames[$name] = $qtype->local_name();
212 $qtypenames = self::sort_qtype_array($qtypenames);
214 $creatableqtypes = array();
215 foreach ($qtypenames as $name => $notused) {
216 $creatableqtypes[$name] = $allqtypes[$name];
218 return $creatableqtypes;
222 * Load the question definition class(es) belonging to a question type. That is,
223 * include_once('/question/type/' . $qtypename . '/question.php'), with a bit
224 * of checking.
225 * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
227 public static function load_question_definition_classes($qtypename) {
228 global $CFG;
229 if (isset(self::$loadedqdefs[$qtypename])) {
230 return;
232 $file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
233 if (!is_readable($file)) {
234 throw new coding_exception('Unknown question type (no definition) ' . $qtypename);
236 include_once($file);
237 self::$loadedqdefs[$qtypename] = 1;
241 * Load a question definition from the database. The object returned
242 * will actually be of an appropriate {@link question_definition} subclass.
243 * @param int $questionid the id of the question to load.
244 * @param bool $allowshuffle if false, then any shuffle option on the selected
245 * quetsion is disabled.
246 * @return question_definition loaded from the database.
248 public static function load_question($questionid, $allowshuffle = true) {
249 global $DB;
251 if (self::$testmode) {
252 // Evil, test code in production, but now way round it.
253 return self::return_test_question_data($questionid);
256 $questiondata = $DB->get_record_sql('
257 SELECT q.*, qc.contextid
258 FROM {question} q
259 JOIN {question_categories} qc ON q.category = qc.id
260 WHERE q.id = :id', array('id' => $questionid), MUST_EXIST);
261 get_question_options($questiondata);
262 if (!$allowshuffle) {
263 $questiondata->options->shuffleanswers = false;
265 return self::make_question($questiondata);
269 * Convert the question information loaded with {@link get_question_options()}
270 * to a question_definintion object.
271 * @param object $questiondata raw data loaded from the database.
272 * @return question_definition loaded from the database.
274 public static function make_question($questiondata) {
275 return self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
279 * @return question_finder a question finder.
281 public static function get_finder() {
282 if (is_null(self::$questionfinder)) {
283 self::$questionfinder = new question_finder();
285 return self::$questionfinder;
289 * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
291 public static function start_unit_test() {
292 self::$testmode = true;
296 * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
298 public static function end_unit_test() {
299 self::$testmode = false;
300 self::$testdata = array();
303 private static function return_test_question_data($questionid) {
304 if (!isset(self::$testdata[$questionid])) {
305 throw new coding_exception('question_bank::return_test_data(' . $questionid .
306 ') called, but no matching question has been loaded by load_test_data.');
308 return self::$testdata[$questionid];
312 * To be used for unit testing only. Will throw an exception if
313 * {@link start_unit_test()} has not been called first.
314 * @param object $questiondata a question data object to put in the test data store.
316 public static function load_test_question_data(question_definition $question) {
317 if (!self::$testmode) {
318 throw new coding_exception('question_bank::load_test_data called when ' .
319 'not in test mode.');
321 self::$testdata[$question->id] = $question;
324 protected function ensure_fraction_options_initialised() {
325 if (!is_null(self::$fractionoptions)) {
326 return;
329 // define basic array of grades. This list comprises all fractions of the form:
330 // a. p/q for q <= 6, 0 <= p <= q
331 // b. p/10 for 0 <= p <= 10
332 // c. 1/q for 1 <= q <= 10
333 // d. 1/20
334 $rawfractions = array(
335 0.9000000,
336 0.8333333,
337 0.8000000,
338 0.7500000,
339 0.7000000,
340 0.6666667,
341 0.6000000,
342 0.5000000,
343 0.4000000,
344 0.3333333,
345 0.3000000,
346 0.2500000,
347 0.2000000,
348 0.1666667,
349 0.1428571,
350 0.1250000,
351 0.1111111,
352 0.1000000,
353 0.0500000,
356 // Put the None option at the top.
357 self::$fractionoptions = array(
358 '0.0' => get_string('none'),
359 '1.0' => '100%',
361 self::$fractionoptionsfull = array(
362 '0.0' => get_string('none'),
363 '1.0' => '100%',
366 // The the positive grades in descending order.
367 foreach ($rawfractions as $fraction) {
368 $percentage = (100 * $fraction) . '%';
369 self::$fractionoptions["$fraction"] = $percentage;
370 self::$fractionoptionsfull["$fraction"] = $percentage;
373 // The the negative grades in descending order.
374 foreach (array_reverse($rawfractions) as $fraction) {
375 self::$fractionoptionsfull['' . (-$fraction)] = (-100 * $fraction) . '%';
378 self::$fractionoptionsfull['-1.0'] = '-100%';
382 * @return array string => string The standard set of grade options (fractions)
383 * to use when editing questions, in the range 0 to 1 inclusive. Array keys
384 * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
385 * have float array keys in PHP.
386 * Initialised by {@link ensure_grade_options_initialised()}.
388 public static function fraction_options() {
389 self::ensure_fraction_options_initialised();
390 return self::$fractionoptions;
393 /** @return array string => string The full standard set of (fractions) -1 to 1 inclusive. */
394 public static function fraction_options_full() {
395 self::ensure_fraction_options_initialised();
396 return self::$fractionoptionsfull;
402 * Class for loading questions according to various criteria.
404 * @copyright 2009 The Open University
405 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
407 class question_finder {
409 * Get the ids of all the questions in a list of categoryies.
410 * @param array $categoryids either a categoryid, or a comma-separated list
411 * category ids, or an array of them.
412 * @param string $extraconditions extra conditions to AND with the rest of
413 * the where clause. Must use named parameters.
414 * @param array $extraparams any parameters used by $extraconditions.
415 * @return array questionid => questionid.
417 public function get_questions_from_categories($categoryids, $extraconditions,
418 $extraparams = array()) {
419 global $DB;
421 list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
423 if ($extraconditions) {
424 $extraconditions = ' AND (' . $extraconditions . ')';
427 return $DB->get_records_select_menu('question',
428 "category $qcsql
429 AND parent = 0
430 AND hidden = 0
431 $extraconditions", $qcparams + $extraparams, '', 'id,id AS id2');