MDL-20636 Remove all references to global $QTYPES.
[moodle.git] / question / engine / bank.php
CommitLineData
d1b7e03d
TH
1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
d1b7e03d
TH
18/**
19 * More object oriented wrappers around parts of the Moodle question bank.
20 *
21 * In due course, I expect that the question bank will be converted to a
22 * fully object oriented structure, at which point this file can be a
23 * starting point.
24 *
017bc1d9 25 * @package moodlecore
d1b7e03d 26 * @subpackage questionbank
017bc1d9
TH
27 * @copyright 2009 The Open University
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
29 */
30
31
a17b297d
TH
32defined('MOODLE_INTERNAL') || die();
33
34
d1b7e03d
TH
35/**
36 * This static class provides access to the other question bank.
37 *
38 * It provides functions for managing question types and question definitions.
39 *
017bc1d9
TH
40 * @copyright 2009 The Open University
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d1b7e03d
TH
42 */
43abstract class question_bank {
44 /** @var array question type name => question_type subclass. */
45 private static $questiontypes = array();
46
47 /** @var array question type name => 1. Records which question definitions have been loaded. */
48 private static $loadedqdefs = array();
49
50 protected static $questionfinder = null;
51
52 /** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
53 private static $testmode = false;
54 private static $testdata = array();
55
f9b0500f
TH
56 private static $questionconfig = null;
57
d649fb02
TH
58 /**
59 * @param string $qtypename a question type name, e.g. 'multichoice'.
60 * @return bool whether that question type is installed in this Moodle.
61 */
62 public static function is_qtype_installed($qtypename) {
63 $plugindir = get_plugin_directory('qtype', $qtypename);
64 return $plugindir && is_readable($plugindir . '/questiontype.php');
65 }
66
d1b7e03d
TH
67 /**
68 * Get the question type class for a particular question type.
69 * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
f7970e3c 70 * @param bool $mustexist if false, the missing question type is returned when
d1b7e03d
TH
71 * the requested question type is not installed.
72 * @return question_type the corresponding question type class.
73 */
74 public static function get_qtype($qtypename, $mustexist = true) {
75 global $CFG;
76 if (isset(self::$questiontypes[$qtypename])) {
77 return self::$questiontypes[$qtypename];
78 }
f29aeb5a 79 $file = get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
d1b7e03d
TH
80 if (!is_readable($file)) {
81 if ($mustexist || $qtypename == 'missingtype') {
88f0eb15 82 throw new coding_exception('Unknown question type ' . $qtypename);
d1b7e03d
TH
83 } else {
84 return self::get_qtype('missingtype');
85 }
86 }
87 include_once($file);
88 $class = 'qtype_' . $qtypename;
f29aeb5a
TH
89 if (!class_exists($class)) {
90 throw new coding_exception("Class $class must be defined in $file");
91 }
d1b7e03d
TH
92 self::$questiontypes[$qtypename] = new $class();
93 return self::$questiontypes[$qtypename];
94 }
95
f9b0500f
TH
96 /**
97 * Load the question configuration data from config_plugins.
98 * @return object get_config('question') with caching.
99 */
100 protected static function get_config() {
101 if (is_null(self::$questionconfig)) {
102 $questionconfig = get_config('question');
103 }
104 return $questionconfig;
105 }
106
06f8ed54
TH
107 /**
108 * @param string $qtypename the internal name of a question type. For example multichoice.
f7970e3c 109 * @return bool whether users are allowed to create questions of this type.
06f8ed54
TH
110 */
111 public static function qtype_enabled($qtypename) {
f9b0500f
TH
112 $config = self::get_config();
113 $enabledvar = $qtypename . '_disabled';
114 return self::qtype_exists($qtypename) && empty($config->$enabledvar) &&
115 self::get_qtype($qtypename)->menu_name() != '';
116 }
117
118 /**
119 * @param string $qtypename the internal name of a question type. For example multichoice.
f7970e3c 120 * @return bool whether this question type exists.
f9b0500f
TH
121 */
122 public static function qtype_exists($qtypename) {
123 return array_key_exists($qtypename, get_plugin_list('qtype'));
06f8ed54
TH
124 }
125
d1b7e03d
TH
126 /**
127 * @param $qtypename the internal name of a question type, for example multichoice.
128 * @return string the human_readable name of this question type, from the language pack.
129 */
130 public static function get_qtype_name($qtypename) {
f9b0500f 131 return self::get_qtype($qtypename)->local_name();
d1b7e03d
TH
132 }
133
134 /**
135 * @return array all the installed question types.
136 */
137 public static function get_all_qtypes() {
138 $qtypes = array();
f29aeb5a
TH
139 foreach (get_plugin_list('qtype') as $plugin => $notused) {
140 try {
141 $qtypes[$plugin] = self::get_qtype($plugin);
142 } catch (Exception $e) {
d649fb02 143 // TODO ingore, but review this later.
f29aeb5a 144 }
d1b7e03d
TH
145 }
146 return $qtypes;
147 }
148
f9b0500f 149 /**
d649fb02
TH
150 * Sort an array of question types according to the order the admin set up,
151 * and then alphabetically for the rest.
152 * @param array qtype->name() => qtype->local_name().
153 * @return array sorted array.
f9b0500f 154 */
d649fb02
TH
155 public static function sort_qtype_array($qtypes, $config = null) {
156 if (is_null($config)) {
157 $config = self::get_config();
158 }
f9b0500f
TH
159
160 $sortorder = array();
161 $otherqtypes = array();
d649fb02 162 foreach ($qtypes as $name => $localname) {
f9b0500f
TH
163 $sortvar = $name . '_sortorder';
164 if (isset($config->$sortvar)) {
165 $sortorder[$config->$sortvar] = $name;
166 } else {
d649fb02 167 $otherqtypes[$name] = $localname;
f9b0500f
TH
168 }
169 }
170
171 ksort($sortorder);
172 textlib_get_instance()->asort($otherqtypes);
173
d649fb02 174 $sortedqtypes = array();
f9b0500f 175 foreach ($sortorder as $name) {
d649fb02 176 $sortedqtypes[$name] = $qtypes[$name];
f9b0500f
TH
177 }
178 foreach ($otherqtypes as $name => $notused) {
d649fb02
TH
179 $sortedqtypes[$name] = $qtypes[$name];
180 }
181 return $sortedqtypes;
182 }
183
184 /**
185 * @return array all the question types that users are allowed to create,
186 * sorted into the preferred order set on the admin screen.
187 */
188 public static function get_creatable_qtypes() {
189 $config = self::get_config();
190 $allqtypes = self::get_all_qtypes();
191
192 $qtypenames = array();
193 foreach ($allqtypes as $name => $qtype) {
194 if (self::qtype_enabled($name)) {
195 $qtypenames[$name] = $qtype->local_name();
196 }
197 }
198
199 $qtypenames = self::sort_qtype_array($qtypenames);
200
201 $creatableqtypes = array();
202 foreach ($qtypenames as $name => $notused) {
f9b0500f
TH
203 $creatableqtypes[$name] = $allqtypes[$name];
204 }
a13d4fbd 205 return $creatableqtypes;
f9b0500f
TH
206 }
207
d1b7e03d
TH
208 /**
209 * Load the question definition class(es) belonging to a question type. That is,
210 * include_once('/question/type/' . $qtypename . '/question.php'), with a bit
211 * of checking.
212 * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
213 */
214 public static function load_question_definition_classes($qtypename) {
215 global $CFG;
216 if (isset(self::$loadedqdefs[$qtypename])) {
217 return;
218 }
219 $file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
220 if (!is_readable($file)) {
88f0eb15 221 throw new coding_exception('Unknown question type (no definition) ' . $qtypename);
d1b7e03d
TH
222 }
223 include_once($file);
224 self::$loadedqdefs[$qtypename] = 1;
225 }
226
227 /**
228 * Load a question definition from the database. The object returned
229 * will actually be of an appropriate {@link question_definition} subclass.
f7970e3c
TH
230 * @param int $questionid the id of the question to load.
231 * @param bool $allowshuffle if false, then any shuffle option on the selected quetsion is disabled.
d1b7e03d
TH
232 * @return question_definition loaded from the database.
233 */
a31689a4 234 public static function load_question($questionid, $allowshuffle = true) {
c76145d3
TH
235 global $DB;
236
d1b7e03d
TH
237 if (self::$testmode) {
238 // Evil, test code in production, but now way round it.
239 return self::return_test_question_data($questionid);
240 }
241
56e82d99
TH
242 $questiondata = $DB->get_record_sql('
243 SELECT q.*, qc.contextid
244 FROM {question} q
245 JOIN {question_categories} qc ON q.category = qc.id
246 WHERE q.id = :id', array('id' => $questionid), MUST_EXIST);
d1b7e03d 247 get_question_options($questiondata);
a31689a4
TH
248 if (!$allowshuffle) {
249 $questiondata->options->shuffleanswers = false;
250 }
d1b7e03d
TH
251 return self::make_question($questiondata);
252 }
253
254 /**
255 * Convert the question information loaded with {@link get_question_options()}
256 * to a question_definintion object.
257 * @param object $questiondata raw data loaded from the database.
258 * @return question_definition loaded from the database.
259 */
260 public static function make_question($questiondata) {
261 return self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
262 }
263
264 /**
265 * @return question_finder a question finder.
266 */
267 public static function get_finder() {
268 if (is_null(self::$questionfinder)) {
269 self::$questionfinder = new question_finder();
270 }
271 return self::$questionfinder;
272 }
273
274 /**
275 * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
276 */
277 public static function start_unit_test() {
278 self::$testmode = true;
279 }
280
281 /**
282 * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
283 */
284 public static function end_unit_test() {
285 self::$testmode = false;
286 self::$testdata = array();
287 }
288
289 private static function return_test_question_data($questionid) {
290 if (!isset(self::$testdata[$questionid])) {
88f0eb15 291 throw new coding_exception('question_bank::return_test_data(' . $questionid .
d1b7e03d
TH
292 ') called, but no matching question has been loaded by load_test_data.');
293 }
294 return self::$testdata[$questionid];
295 }
296
297 /**
298 * To be used for unit testing only. Will throw an exception if
299 * {@link start_unit_test()} has not been called first.
300 * @param object $questiondata a question data object to put in the test data store.
301 */
302 public static function load_test_question_data(question_definition $question) {
303 if (!self::$testmode) {
88f0eb15 304 throw new coding_exception('question_bank::load_test_data called when not in test mode.');
d1b7e03d
TH
305 }
306 self::$testdata[$question->id] = $question;
307 }
308}
309
f7970e3c
TH
310
311/**
312 * Class for loading questions according to various criteria.
313 *
314 * @copyright 2009 The Open University
315 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
316 */
d1b7e03d
TH
317class question_finder {
318 /**
319 * Get the ids of all the questions in a list of categoryies.
f7970e3c 320 * @param int|string|array $categoryids either a categoryid, or a comma-separated list
d1b7e03d
TH
321 * category ids, or an array of them.
322 * @param string $extraconditions extra conditions to AND with the rest of the where clause.
323 * @return array questionid => questionid.
324 */
325 public function get_questions_from_categories($categoryids, $extraconditions) {
f9b0500f
TH
326 global $DB;
327
d1b7e03d
TH
328 if (is_array($categoryids)) {
329 $categoryids = implode(',', $categoryids);
330 }
331
332 if ($extraconditions) {
333 $extraconditions = ' AND (' . $extraconditions . ')';
334 }
c76145d3
TH
335 // TODO switch to using $DB->in_or_equal.
336 $questionids = $DB->get_records_select_menu('question',
d1b7e03d
TH
337 "category IN ($categoryids)
338 AND parent = 0
339 AND hidden = 0
f9b0500f 340 $extraconditions", array(), '', 'id,id AS id2');
d1b7e03d
TH
341 return $questionids;
342 }
343}