MDL-20636 Review option defaults in the admin settings.
[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 *
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 */
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 *
38 * @copyright 2009 The Open University
39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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
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 }
f29aeb5a 66 $file = get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
d1b7e03d
TH
67 if (!is_readable($file)) {
68 if ($mustexist || $qtypename == 'missingtype') {
69 throw new Exception('Unknown question type ' . $qtypename);
70 } else {
71 return self::get_qtype('missingtype');
72 }
73 }
74 include_once($file);
75 $class = 'qtype_' . $qtypename;
f29aeb5a
TH
76 if (!class_exists($class)) {
77 throw new coding_exception("Class $class must be defined in $file");
78 }
d1b7e03d
TH
79 self::$questiontypes[$qtypename] = new $class();
80 return self::$questiontypes[$qtypename];
81 }
82
06f8ed54
TH
83 /**
84 * @param string $qtypename the internal name of a question type. For example multichoice.
85 * @return boolean whether users are allowed to create questions of this type.
86 */
87 public static function qtype_enabled($qtypename) {
f29aeb5a 88 return true; // TODO
06f8ed54
TH
89 }
90
d1b7e03d
TH
91 /**
92 * @param $qtypename the internal name of a question type, for example multichoice.
93 * @return string the human_readable name of this question type, from the language pack.
94 */
95 public static function get_qtype_name($qtypename) {
96 return self::get_qtype($qtypename)->menu_name();
97 }
98
99 /**
100 * @return array all the installed question types.
101 */
102 public static function get_all_qtypes() {
103 $qtypes = array();
f29aeb5a
TH
104 foreach (get_plugin_list('qtype') as $plugin => $notused) {
105 try {
106 $qtypes[$plugin] = self::get_qtype($plugin);
107 } catch (Exception $e) {
108 // TODO ingore, but reivew this later.
109 }
d1b7e03d
TH
110 }
111 return $qtypes;
112 }
113
114 /**
115 * Load the question definition class(es) belonging to a question type. That is,
116 * include_once('/question/type/' . $qtypename . '/question.php'), with a bit
117 * of checking.
118 * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
119 */
120 public static function load_question_definition_classes($qtypename) {
121 global $CFG;
122 if (isset(self::$loadedqdefs[$qtypename])) {
123 return;
124 }
125 $file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
126 if (!is_readable($file)) {
127 throw new Exception('Unknown question type (no definition) ' . $qtypename);
128 }
129 include_once($file);
130 self::$loadedqdefs[$qtypename] = 1;
131 }
132
133 /**
134 * Load a question definition from the database. The object returned
135 * will actually be of an appropriate {@link question_definition} subclass.
136 * @param integer $questionid the id of the question to load.
a31689a4 137 * @param boolean $allowshuffle if false, then any shuffle option on the selected quetsion is disabled.
d1b7e03d
TH
138 * @return question_definition loaded from the database.
139 */
a31689a4 140 public static function load_question($questionid, $allowshuffle = true) {
c76145d3
TH
141 global $DB;
142
d1b7e03d
TH
143 if (self::$testmode) {
144 // Evil, test code in production, but now way round it.
145 return self::return_test_question_data($questionid);
146 }
147
56e82d99
TH
148 $questiondata = $DB->get_record_sql('
149 SELECT q.*, qc.contextid
150 FROM {question} q
151 JOIN {question_categories} qc ON q.category = qc.id
152 WHERE q.id = :id', array('id' => $questionid), MUST_EXIST);
d1b7e03d 153 get_question_options($questiondata);
a31689a4
TH
154 if (!$allowshuffle) {
155 $questiondata->options->shuffleanswers = false;
156 }
d1b7e03d
TH
157 return self::make_question($questiondata);
158 }
159
160 /**
161 * Convert the question information loaded with {@link get_question_options()}
162 * to a question_definintion object.
163 * @param object $questiondata raw data loaded from the database.
164 * @return question_definition loaded from the database.
165 */
166 public static function make_question($questiondata) {
167 return self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
168 }
169
170 /**
171 * @return question_finder a question finder.
172 */
173 public static function get_finder() {
174 if (is_null(self::$questionfinder)) {
175 self::$questionfinder = new question_finder();
176 }
177 return self::$questionfinder;
178 }
179
180 /**
181 * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
182 */
183 public static function start_unit_test() {
184 self::$testmode = true;
185 }
186
187 /**
188 * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
189 */
190 public static function end_unit_test() {
191 self::$testmode = false;
192 self::$testdata = array();
193 }
194
195 private static function return_test_question_data($questionid) {
196 if (!isset(self::$testdata[$questionid])) {
197 throw new Exception('question_bank::return_test_data(' . $questionid .
198 ') called, but no matching question has been loaded by load_test_data.');
199 }
200 return self::$testdata[$questionid];
201 }
202
203 /**
204 * To be used for unit testing only. Will throw an exception if
205 * {@link start_unit_test()} has not been called first.
206 * @param object $questiondata a question data object to put in the test data store.
207 */
208 public static function load_test_question_data(question_definition $question) {
209 if (!self::$testmode) {
210 throw new Exception('question_bank::load_test_data called when not in test mode.');
211 }
212 self::$testdata[$question->id] = $question;
213 }
214}
215
216class question_finder {
217 /**
218 * Get the ids of all the questions in a list of categoryies.
219 * @param integer|string|array $categoryids either a categoryid, or a comma-separated list
220 * category ids, or an array of them.
221 * @param string $extraconditions extra conditions to AND with the rest of the where clause.
222 * @return array questionid => questionid.
223 */
224 public function get_questions_from_categories($categoryids, $extraconditions) {
225 if (is_array($categoryids)) {
226 $categoryids = implode(',', $categoryids);
227 }
228
229 if ($extraconditions) {
230 $extraconditions = ' AND (' . $extraconditions . ')';
231 }
c76145d3
TH
232 // TODO switch to using $DB->in_or_equal.
233 $questionids = $DB->get_records_select_menu('question',
d1b7e03d
TH
234 "category IN ($categoryids)
235 AND parent = 0
236 AND hidden = 0
237 $extraconditions", '', 'id,id AS id2');
238 if (!$questionids) {
239 $questionids = array();
240 }
241 return $questionids;
242 }
243}