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