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