MDL-20636 Previewing a truefalse question in deferred feedback mode now works.
[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/>.
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  *
26  * @package moodlecore
27  * @subpackage questionbank
28  * @copyright 2009 The Open University
29  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30  */
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  *
38  * @copyright 2009 The Open University
39  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 abstract class question_bank {
42     /** @var array question type name => question_type subclass. */
43     private static $questiontypes = array();
45     /** @var array question type name => 1. Records which question definitions have been loaded. */
46     private static $loadedqdefs = array();
48     protected static $questionfinder = null;
50     /** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
51     private static $testmode = false;
52     private static $testdata = array();
54     /**
55      * Get the question type class for a particular question type.
56      * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
57      * @param boolean $mustexist if false, the missing question type is returned when
58      *      the requested question type is not installed.
59      * @return question_type the corresponding question type class.
60      */
61     public static function get_qtype($qtypename, $mustexist = true) {
62         global $CFG;
63         if (isset(self::$questiontypes[$qtypename])) {
64             return self::$questiontypes[$qtypename];
65         }
66         $file = get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
67         if (!is_readable($file)) {
68             echo 'problem';
69             if ($mustexist || $qtypename == 'missingtype') {
70                 throw new Exception('Unknown question type ' . $qtypename);
71             } else {
72                 return self::get_qtype('missingtype');
73             }
74         }
75         include_once($file);
76         $class = 'qtype_' . $qtypename;
77         if (!class_exists($class)) {
78             throw new coding_exception("Class $class must be defined in $file");
79         }
80         self::$questiontypes[$qtypename] = new $class();
81         return self::$questiontypes[$qtypename];
82     }
84     /**
85      * @param string $qtypename the internal name of a question type. For example multichoice.
86      * @return boolean whether users are allowed to create questions of this type.
87      */
88     public static function qtype_enabled($qtypename) {
89         return true; // TODO
90     }
92     /**
93      * @param $qtypename the internal name of a question type, for example multichoice.
94      * @return string the human_readable name of this question type, from the language pack.
95      */
96     public static function get_qtype_name($qtypename) {
97         return self::get_qtype($qtypename)->menu_name();
98     }
100     /**
101      * @return array all the installed question types.
102      */
103     public static function get_all_qtypes() {
104         $qtypes = array();
105         foreach (get_plugin_list('qtype') as $plugin => $notused) {
106             try {
107                 $qtypes[$plugin] = self::get_qtype($plugin);
108             } catch (Exception $e) {
109                 // TODO ingore, but reivew this later.
110             }
111         }
112         return $qtypes;
113     }
115     /**
116      * Load the question definition class(es) belonging to a question type. That is,
117      * include_once('/question/type/' . $qtypename . '/question.php'), with a bit
118      * of checking.
119      * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
120      */
121     public static function load_question_definition_classes($qtypename) {
122         global $CFG;
123         if (isset(self::$loadedqdefs[$qtypename])) {
124             return;
125         }
126         $file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
127         if (!is_readable($file)) {
128             throw new Exception('Unknown question type (no definition) ' . $qtypename);
129         }
130         include_once($file);
131         self::$loadedqdefs[$qtypename] = 1;
132     }
134     /**
135      * Load a question definition from the database. The object returned
136      * will actually be of an appropriate {@link question_definition} subclass.
137      * @param integer $questionid the id of the question to load.
138      * @return question_definition loaded from the database.
139      */
140     public static function load_question($questionid) {
141         global $DB;
143         if (self::$testmode) {
144             // Evil, test code in production, but now way round it.
145             return self::return_test_question_data($questionid);
146         }
148         $questiondata = $DB->get_record('question', array('id' => $questionid), '*', MUST_EXIST);
149         get_question_options($questiondata);
150         return self::make_question($questiondata);
151     }
153     /**
154      * Convert the question information loaded with {@link get_question_options()}
155      * to a question_definintion object.
156      * @param object $questiondata raw data loaded from the database.
157      * @return question_definition loaded from the database.
158      */
159     public static function make_question($questiondata) {
160         return self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
161     }
163     /**
164      * @return question_finder a question finder.
165      */
166     public static function get_finder() {
167         if (is_null(self::$questionfinder)) {
168             self::$questionfinder = new question_finder();
169         }
170         return self::$questionfinder;
171     }
173     /**
174      * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
175      */
176     public static function start_unit_test() {
177         self::$testmode = true;
178     }
180     /**
181      * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
182      */
183     public static function end_unit_test() {
184         self::$testmode = false;
185         self::$testdata = array();
186     }
188     private static function return_test_question_data($questionid) {
189         if (!isset(self::$testdata[$questionid])) {
190             throw new Exception('question_bank::return_test_data(' . $questionid .
191                     ') called, but no matching question has been loaded by load_test_data.');
192         }
193         return self::$testdata[$questionid];
194     }
196     /**
197      * To be used for unit testing only. Will throw an exception if
198      * {@link start_unit_test()} has not been called first.
199      * @param object $questiondata a question data object to put in the test data store.
200      */
201     public static function load_test_question_data(question_definition $question) {
202         if (!self::$testmode) {
203             throw new Exception('question_bank::load_test_data called when not in test mode.');
204         }
205         self::$testdata[$question->id] = $question;
206     }
209 class question_finder {
210     /**
211      * Get the ids of all the questions in a list of categoryies.
212      * @param integer|string|array $categoryids either a categoryid, or a comma-separated list
213      *      category ids, or an array of them.
214      * @param string $extraconditions extra conditions to AND with the rest of the where clause.
215      * @return array questionid => questionid.
216      */
217     public function get_questions_from_categories($categoryids, $extraconditions) {
218         if (is_array($categoryids)) {
219             $categoryids = implode(',', $categoryids);
220         }
222         if ($extraconditions) {
223             $extraconditions = ' AND (' . $extraconditions . ')';
224         }
225         // TODO switch to using $DB->in_or_equal.
226         $questionids = $DB->get_records_select_menu('question',
227                 "category IN ($categoryids)
228                  AND parent = 0
229                  AND hidden = 0
230                  $extraconditions", '', 'id,id AS id2');
231         if (!$questionids) {
232             $questionids = array();
233         }
234         return $questionids;
235     }