PageRenderTime 940ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/question/engine/bank.php

https://bitbucket.org/kudutest1/moodlegit
PHP | 538 lines | 287 code | 64 blank | 187 comment | 29 complexity | e82197a3a0a750ab1b77995a5cf6ab13 MD5 | raw file
  1. <?php
  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. /**
  17. * More object oriented wrappers around parts of the Moodle question bank.
  18. *
  19. * In due course, I expect that the question bank will be converted to a
  20. * fully object oriented structure, at which point this file can be a
  21. * starting point.
  22. *
  23. * @package moodlecore
  24. * @subpackage questionbank
  25. * @copyright 2009 The Open University
  26. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27. */
  28. defined('MOODLE_INTERNAL') || die();
  29. require_once(dirname(__FILE__) . '/../type/questiontypebase.php');
  30. /**
  31. * This static class provides access to the other question bank.
  32. *
  33. * It provides functions for managing question types and question definitions.
  34. *
  35. * @copyright 2009 The Open University
  36. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37. */
  38. abstract class question_bank {
  39. // TODO: This limit can be deleted if someday we move all TEXTS to BIG ones. MDL-19603
  40. const MAX_SUMMARY_LENGTH = 32000;
  41. /** @var array question type name => question_type subclass. */
  42. private static $questiontypes = array();
  43. /** @var array question type name => 1. Records which question definitions have been loaded. */
  44. private static $loadedqdefs = array();
  45. /** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
  46. private static $testmode = false;
  47. private static $testdata = array();
  48. private static $questionconfig = null;
  49. /**
  50. * @var array string => string The standard set of grade options (fractions)
  51. * to use when editing questions, in the range 0 to 1 inclusive. Array keys
  52. * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
  53. * have float array keys in PHP.
  54. * Initialised by {@link ensure_grade_options_initialised()}.
  55. */
  56. private static $fractionoptions = null;
  57. /** @var array string => string The full standard set of (fractions) -1 to 1 inclusive. */
  58. private static $fractionoptionsfull = null;
  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. /**
  68. * Get the question type class for a particular question type.
  69. * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
  70. * @param bool $mustexist if false, the missing question type is returned when
  71. * the requested question type is not installed.
  72. * @return question_type the corresponding question type class.
  73. */
  74. public static function get_qtype($qtypename, $mustexist = true) {
  75. global $CFG;
  76. if (isset(self::$questiontypes[$qtypename])) {
  77. return self::$questiontypes[$qtypename];
  78. }
  79. $file = get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
  80. if (!is_readable($file)) {
  81. if ($mustexist || $qtypename == 'missingtype') {
  82. throw new coding_exception('Unknown question type ' . $qtypename);
  83. } else {
  84. return self::get_qtype('missingtype');
  85. }
  86. }
  87. include_once($file);
  88. $class = 'qtype_' . $qtypename;
  89. if (!class_exists($class)) {
  90. throw new coding_exception("Class $class must be defined in $file");
  91. }
  92. self::$questiontypes[$qtypename] = new $class();
  93. return self::$questiontypes[$qtypename];
  94. }
  95. /**
  96. * Load the question configuration data from config_plugins.
  97. * @return object get_config('question') with caching.
  98. */
  99. public static function get_config() {
  100. if (is_null(self::$questionconfig)) {
  101. self::$questionconfig = get_config('question');
  102. }
  103. return self::$questionconfig;
  104. }
  105. /**
  106. * @param string $qtypename the internal name of a question type. For example multichoice.
  107. * @return bool whether users are allowed to create questions of this type.
  108. */
  109. public static function qtype_enabled($qtypename) {
  110. $config = self::get_config();
  111. $enabledvar = $qtypename . '_disabled';
  112. return self::qtype_exists($qtypename) && empty($config->$enabledvar) &&
  113. self::get_qtype($qtypename)->menu_name() != '';
  114. }
  115. /**
  116. * @param string $qtypename the internal name of a question type. For example multichoice.
  117. * @return bool whether this question type exists.
  118. */
  119. public static function qtype_exists($qtypename) {
  120. return array_key_exists($qtypename, get_plugin_list('qtype'));
  121. }
  122. /**
  123. * @param $qtypename the internal name of a question type, for example multichoice.
  124. * @return string the human_readable name of this question type, from the language pack.
  125. */
  126. public static function get_qtype_name($qtypename) {
  127. return self::get_qtype($qtypename)->local_name();
  128. }
  129. /**
  130. * @return array all the installed question types.
  131. */
  132. public static function get_all_qtypes() {
  133. $qtypes = array();
  134. foreach (get_plugin_list('qtype') as $plugin => $notused) {
  135. try {
  136. $qtypes[$plugin] = self::get_qtype($plugin);
  137. } catch (coding_exception $e) {
  138. // Catching coding_exceptions here means that incompatible
  139. // question types do not cause the rest of Moodle to break.
  140. }
  141. }
  142. return $qtypes;
  143. }
  144. /**
  145. * Sort an array of question types according to the order the admin set up,
  146. * and then alphabetically for the rest.
  147. * @param array qtype->name() => qtype->local_name().
  148. * @return array sorted array.
  149. */
  150. public static function sort_qtype_array($qtypes, $config = null) {
  151. if (is_null($config)) {
  152. $config = self::get_config();
  153. }
  154. $sortorder = array();
  155. $otherqtypes = array();
  156. foreach ($qtypes as $name => $localname) {
  157. $sortvar = $name . '_sortorder';
  158. if (isset($config->$sortvar)) {
  159. $sortorder[$config->$sortvar] = $name;
  160. } else {
  161. $otherqtypes[$name] = $localname;
  162. }
  163. }
  164. ksort($sortorder);
  165. collatorlib::asort($otherqtypes);
  166. $sortedqtypes = array();
  167. foreach ($sortorder as $name) {
  168. $sortedqtypes[$name] = $qtypes[$name];
  169. }
  170. foreach ($otherqtypes as $name => $notused) {
  171. $sortedqtypes[$name] = $qtypes[$name];
  172. }
  173. return $sortedqtypes;
  174. }
  175. /**
  176. * @return array all the question types that users are allowed to create,
  177. * sorted into the preferred order set on the admin screen.
  178. */
  179. public static function get_creatable_qtypes() {
  180. $config = self::get_config();
  181. $allqtypes = self::get_all_qtypes();
  182. $qtypenames = array();
  183. foreach ($allqtypes as $name => $qtype) {
  184. if (self::qtype_enabled($name)) {
  185. $qtypenames[$name] = $qtype->local_name();
  186. }
  187. }
  188. $qtypenames = self::sort_qtype_array($qtypenames);
  189. $creatableqtypes = array();
  190. foreach ($qtypenames as $name => $notused) {
  191. $creatableqtypes[$name] = $allqtypes[$name];
  192. }
  193. return $creatableqtypes;
  194. }
  195. /**
  196. * Load the question definition class(es) belonging to a question type. That is,
  197. * include_once('/question/type/' . $qtypename . '/question.php'), with a bit
  198. * of checking.
  199. * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
  200. */
  201. public static function load_question_definition_classes($qtypename) {
  202. global $CFG;
  203. if (isset(self::$loadedqdefs[$qtypename])) {
  204. return;
  205. }
  206. $file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
  207. if (!is_readable($file)) {
  208. throw new coding_exception('Unknown question type (no definition) ' . $qtypename);
  209. }
  210. include_once($file);
  211. self::$loadedqdefs[$qtypename] = 1;
  212. }
  213. /**
  214. * This method needs to be called whenever a question is edited.
  215. */
  216. public static function notify_question_edited($questionid) {
  217. question_finder::get_instance()->uncache_question($questionid);
  218. }
  219. /**
  220. * Load a question definition data from the database. The data will be
  221. * returned as a plain stdClass object.
  222. * @param int $questionid the id of the question to load.
  223. * @return object question definition loaded from the database.
  224. */
  225. public static function load_question_data($questionid) {
  226. return question_finder::get_instance()->load_question_data($questionid);
  227. }
  228. /**
  229. * Load a question definition from the database. The object returned
  230. * will actually be of an appropriate {@link question_definition} subclass.
  231. * @param int $questionid the id of the question to load.
  232. * @param bool $allowshuffle if false, then any shuffle option on the selected
  233. * quetsion is disabled.
  234. * @return question_definition loaded from the database.
  235. */
  236. public static function load_question($questionid, $allowshuffle = true) {
  237. global $DB;
  238. if (self::$testmode) {
  239. // Evil, test code in production, but now way round it.
  240. return self::return_test_question_data($questionid);
  241. }
  242. $questiondata = self::load_question_data($questionid);
  243. if (!$allowshuffle) {
  244. $questiondata->options->shuffleanswers = false;
  245. }
  246. return self::make_question($questiondata);
  247. }
  248. /**
  249. * Convert the question information loaded with {@link get_question_options()}
  250. * to a question_definintion object.
  251. * @param object $questiondata raw data loaded from the database.
  252. * @return question_definition loaded from the database.
  253. */
  254. public static function make_question($questiondata) {
  255. return self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
  256. }
  257. /**
  258. * @return question_finder a question finder.
  259. */
  260. public static function get_finder() {
  261. return question_finder::get_instance();
  262. if (is_null(self::$questionfinder)) {
  263. self::$questionfinder = new question_finder();
  264. }
  265. return self::$questionfinder;
  266. }
  267. /**
  268. * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
  269. */
  270. public static function start_unit_test() {
  271. self::$testmode = true;
  272. }
  273. /**
  274. * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
  275. */
  276. public static function end_unit_test() {
  277. self::$testmode = false;
  278. self::$testdata = array();
  279. }
  280. private static function return_test_question_data($questionid) {
  281. if (!isset(self::$testdata[$questionid])) {
  282. throw new coding_exception('question_bank::return_test_data(' . $questionid .
  283. ') called, but no matching question has been loaded by load_test_data.');
  284. }
  285. return self::$testdata[$questionid];
  286. }
  287. /**
  288. * To be used for unit testing only. Will throw an exception if
  289. * {@link start_unit_test()} has not been called first.
  290. * @param object $questiondata a question data object to put in the test data store.
  291. */
  292. public static function load_test_question_data(question_definition $question) {
  293. if (!self::$testmode) {
  294. throw new coding_exception('question_bank::load_test_data called when ' .
  295. 'not in test mode.');
  296. }
  297. self::$testdata[$question->id] = $question;
  298. }
  299. protected static function ensure_fraction_options_initialised() {
  300. if (!is_null(self::$fractionoptions)) {
  301. return;
  302. }
  303. // define basic array of grades. This list comprises all fractions of the form:
  304. // a. p/q for q <= 6, 0 <= p <= q
  305. // b. p/10 for 0 <= p <= 10
  306. // c. 1/q for 1 <= q <= 10
  307. // d. 1/20
  308. $rawfractions = array(
  309. 0.9000000,
  310. 0.8333333,
  311. 0.8000000,
  312. 0.7500000,
  313. 0.7000000,
  314. 0.6666667,
  315. 0.6000000,
  316. 0.5000000,
  317. 0.4000000,
  318. 0.3333333,
  319. 0.3000000,
  320. 0.2500000,
  321. 0.2000000,
  322. 0.1666667,
  323. 0.1428571,
  324. 0.1250000,
  325. 0.1111111,
  326. 0.1000000,
  327. 0.0500000,
  328. );
  329. // Put the None option at the top.
  330. self::$fractionoptions = array(
  331. '0.0' => get_string('none'),
  332. '1.0' => '100%',
  333. );
  334. self::$fractionoptionsfull = array(
  335. '0.0' => get_string('none'),
  336. '1.0' => '100%',
  337. );
  338. // The the positive grades in descending order.
  339. foreach ($rawfractions as $fraction) {
  340. $percentage = (100 * $fraction) . '%';
  341. self::$fractionoptions["$fraction"] = $percentage;
  342. self::$fractionoptionsfull["$fraction"] = $percentage;
  343. }
  344. // The the negative grades in descending order.
  345. foreach (array_reverse($rawfractions) as $fraction) {
  346. self::$fractionoptionsfull['' . (-$fraction)] = (-100 * $fraction) . '%';
  347. }
  348. self::$fractionoptionsfull['-1.0'] = '-100%';
  349. }
  350. /**
  351. * @return array string => string The standard set of grade options (fractions)
  352. * to use when editing questions, in the range 0 to 1 inclusive. Array keys
  353. * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
  354. * have float array keys in PHP.
  355. * Initialised by {@link ensure_grade_options_initialised()}.
  356. */
  357. public static function fraction_options() {
  358. self::ensure_fraction_options_initialised();
  359. return self::$fractionoptions;
  360. }
  361. /** @return array string => string The full standard set of (fractions) -1 to 1 inclusive. */
  362. public static function fraction_options_full() {
  363. self::ensure_fraction_options_initialised();
  364. return self::$fractionoptionsfull;
  365. }
  366. /**
  367. * Perform scheduled maintenance tasks relating to the question bank.
  368. */
  369. public static function cron() {
  370. global $CFG;
  371. // Delete any old question preview that got left in the database.
  372. require_once($CFG->dirroot . '/question/previewlib.php');
  373. question_preview_cron();
  374. }
  375. }
  376. /**
  377. * Class for loading questions according to various criteria.
  378. *
  379. * @copyright 2009 The Open University
  380. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  381. */
  382. class question_finder implements cache_data_source {
  383. /** @var question_finder the singleton instance of this class. */
  384. protected static $questionfinder = null;
  385. /** @var cache the question definition cache. */
  386. protected $cache = null;
  387. /**
  388. * @return question_finder a question finder.
  389. */
  390. public static function get_instance() {
  391. if (is_null(self::$questionfinder)) {
  392. self::$questionfinder = new question_finder();
  393. }
  394. return self::$questionfinder;
  395. }
  396. /* See cache_data_source::get_instance_for_cache. */
  397. public static function get_instance_for_cache(cache_definition $definition) {
  398. return self::get_instance();
  399. }
  400. /**
  401. * @return get the question definition cache we are using.
  402. */
  403. protected function get_data_cache() {
  404. if ($this->cache == null) {
  405. $this->cache = cache::make('core', 'questiondata');
  406. }
  407. return $this->cache;
  408. }
  409. /**
  410. * This method needs to be called whenever a question is edited.
  411. */
  412. public function uncache_question($questionid) {
  413. $this->get_data_cache()->delete($questionid);
  414. }
  415. /**
  416. * Load a question definition data from the database. The data will be
  417. * returned as a plain stdClass object.
  418. * @param int $questionid the id of the question to load.
  419. * @return object question definition loaded from the database.
  420. */
  421. public function load_question_data($questionid) {
  422. return $this->get_data_cache()->get($questionid);
  423. }
  424. /**
  425. * Get the ids of all the questions in a list of categoryies.
  426. * @param array $categoryids either a categoryid, or a comma-separated list
  427. * category ids, or an array of them.
  428. * @param string $extraconditions extra conditions to AND with the rest of
  429. * the where clause. Must use named parameters.
  430. * @param array $extraparams any parameters used by $extraconditions.
  431. * @return array questionid => questionid.
  432. */
  433. public function get_questions_from_categories($categoryids, $extraconditions,
  434. $extraparams = array()) {
  435. global $DB;
  436. list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
  437. if ($extraconditions) {
  438. $extraconditions = ' AND (' . $extraconditions . ')';
  439. }
  440. return $DB->get_records_select_menu('question',
  441. "category $qcsql
  442. AND parent = 0
  443. AND hidden = 0
  444. $extraconditions", $qcparams + $extraparams, '', 'id,id AS id2');
  445. }
  446. /* See cache_data_source::load_for_cache. */
  447. public function load_for_cache($questionid) {
  448. global $DB;
  449. $questiondata = $DB->get_record_sql('
  450. SELECT q.*, qc.contextid
  451. FROM {question} q
  452. JOIN {question_categories} qc ON q.category = qc.id
  453. WHERE q.id = :id', array('id' => $questionid), MUST_EXIST);
  454. get_question_options($questiondata);
  455. return $questiondata;
  456. }
  457. /* See cache_data_source::load_many_for_cache. */
  458. public function load_many_for_cache(array $questionids) {
  459. global $DB;
  460. list($idcondition, $params) = $DB->get_in_or_equal($questionids);
  461. $questiondata = $DB->get_records_sql('
  462. SELECT q.*, qc.contextid
  463. FROM {question} q
  464. JOIN {question_categories} qc ON q.category = qc.id
  465. WHERE q.id ' . $idcondition, $params);
  466. foreach ($questionids as $id) {
  467. if (!array_key_exists($id, $questionids)) {
  468. throw new dml_missing_record_exception('question', '', array('id' => $id));
  469. }
  470. get_question_options($questiondata[$id]);
  471. }
  472. return $questiondata;
  473. }
  474. }