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