/question/engine/bank.php
PHP | 538 lines | 287 code | 64 blank | 187 comment | 29 complexity | e82197a3a0a750ab1b77995a5cf6ab13 MD5 | raw file
- <?php
- // This file is part of Moodle - http://moodle.org/
- //
- // Moodle is free software: you can redistribute it and/or modify
- // it under the terms of the GNU General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // Moodle is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU General Public License for more details.
- //
- // You should have received a copy of the GNU General Public License
- // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
- /**
- * More object oriented wrappers around parts of the Moodle question bank.
- *
- * In due course, I expect that the question bank will be converted to a
- * fully object oriented structure, at which point this file can be a
- * starting point.
- *
- * @package moodlecore
- * @subpackage questionbank
- * @copyright 2009 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
- defined('MOODLE_INTERNAL') || die();
- require_once(dirname(__FILE__) . '/../type/questiontypebase.php');
- /**
- * This static class provides access to the other question bank.
- *
- * It provides functions for managing question types and question definitions.
- *
- * @copyright 2009 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
- abstract class question_bank {
- // TODO: This limit can be deleted if someday we move all TEXTS to BIG ones. MDL-19603
- const MAX_SUMMARY_LENGTH = 32000;
- /** @var array question type name => question_type subclass. */
- private static $questiontypes = array();
- /** @var array question type name => 1. Records which question definitions have been loaded. */
- private static $loadedqdefs = array();
- /** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
- private static $testmode = false;
- private static $testdata = array();
- private static $questionconfig = null;
- /**
- * @var array string => string The standard set of grade options (fractions)
- * to use when editing questions, in the range 0 to 1 inclusive. Array keys
- * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
- * have float array keys in PHP.
- * Initialised by {@link ensure_grade_options_initialised()}.
- */
- private static $fractionoptions = null;
- /** @var array string => string The full standard set of (fractions) -1 to 1 inclusive. */
- private static $fractionoptionsfull = null;
- /**
- * @param string $qtypename a question type name, e.g. 'multichoice'.
- * @return bool whether that question type is installed in this Moodle.
- */
- public static function is_qtype_installed($qtypename) {
- $plugindir = get_plugin_directory('qtype', $qtypename);
- return $plugindir && is_readable($plugindir . '/questiontype.php');
- }
- /**
- * Get the question type class for a particular question type.
- * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
- * @param bool $mustexist if false, the missing question type is returned when
- * the requested question type is not installed.
- * @return question_type the corresponding question type class.
- */
- public static function get_qtype($qtypename, $mustexist = true) {
- global $CFG;
- if (isset(self::$questiontypes[$qtypename])) {
- return self::$questiontypes[$qtypename];
- }
- $file = get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
- if (!is_readable($file)) {
- if ($mustexist || $qtypename == 'missingtype') {
- throw new coding_exception('Unknown question type ' . $qtypename);
- } else {
- return self::get_qtype('missingtype');
- }
- }
- include_once($file);
- $class = 'qtype_' . $qtypename;
- if (!class_exists($class)) {
- throw new coding_exception("Class $class must be defined in $file");
- }
- self::$questiontypes[$qtypename] = new $class();
- return self::$questiontypes[$qtypename];
- }
- /**
- * Load the question configuration data from config_plugins.
- * @return object get_config('question') with caching.
- */
- public static function get_config() {
- if (is_null(self::$questionconfig)) {
- self::$questionconfig = get_config('question');
- }
- return self::$questionconfig;
- }
- /**
- * @param string $qtypename the internal name of a question type. For example multichoice.
- * @return bool whether users are allowed to create questions of this type.
- */
- public static function qtype_enabled($qtypename) {
- $config = self::get_config();
- $enabledvar = $qtypename . '_disabled';
- return self::qtype_exists($qtypename) && empty($config->$enabledvar) &&
- self::get_qtype($qtypename)->menu_name() != '';
- }
- /**
- * @param string $qtypename the internal name of a question type. For example multichoice.
- * @return bool whether this question type exists.
- */
- public static function qtype_exists($qtypename) {
- return array_key_exists($qtypename, get_plugin_list('qtype'));
- }
- /**
- * @param $qtypename the internal name of a question type, for example multichoice.
- * @return string the human_readable name of this question type, from the language pack.
- */
- public static function get_qtype_name($qtypename) {
- return self::get_qtype($qtypename)->local_name();
- }
- /**
- * @return array all the installed question types.
- */
- public static function get_all_qtypes() {
- $qtypes = array();
- foreach (get_plugin_list('qtype') as $plugin => $notused) {
- try {
- $qtypes[$plugin] = self::get_qtype($plugin);
- } catch (coding_exception $e) {
- // Catching coding_exceptions here means that incompatible
- // question types do not cause the rest of Moodle to break.
- }
- }
- return $qtypes;
- }
- /**
- * Sort an array of question types according to the order the admin set up,
- * and then alphabetically for the rest.
- * @param array qtype->name() => qtype->local_name().
- * @return array sorted array.
- */
- public static function sort_qtype_array($qtypes, $config = null) {
- if (is_null($config)) {
- $config = self::get_config();
- }
- $sortorder = array();
- $otherqtypes = array();
- foreach ($qtypes as $name => $localname) {
- $sortvar = $name . '_sortorder';
- if (isset($config->$sortvar)) {
- $sortorder[$config->$sortvar] = $name;
- } else {
- $otherqtypes[$name] = $localname;
- }
- }
- ksort($sortorder);
- collatorlib::asort($otherqtypes);
- $sortedqtypes = array();
- foreach ($sortorder as $name) {
- $sortedqtypes[$name] = $qtypes[$name];
- }
- foreach ($otherqtypes as $name => $notused) {
- $sortedqtypes[$name] = $qtypes[$name];
- }
- return $sortedqtypes;
- }
- /**
- * @return array all the question types that users are allowed to create,
- * sorted into the preferred order set on the admin screen.
- */
- public static function get_creatable_qtypes() {
- $config = self::get_config();
- $allqtypes = self::get_all_qtypes();
- $qtypenames = array();
- foreach ($allqtypes as $name => $qtype) {
- if (self::qtype_enabled($name)) {
- $qtypenames[$name] = $qtype->local_name();
- }
- }
- $qtypenames = self::sort_qtype_array($qtypenames);
- $creatableqtypes = array();
- foreach ($qtypenames as $name => $notused) {
- $creatableqtypes[$name] = $allqtypes[$name];
- }
- return $creatableqtypes;
- }
- /**
- * Load the question definition class(es) belonging to a question type. That is,
- * include_once('/question/type/' . $qtypename . '/question.php'), with a bit
- * of checking.
- * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
- */
- public static function load_question_definition_classes($qtypename) {
- global $CFG;
- if (isset(self::$loadedqdefs[$qtypename])) {
- return;
- }
- $file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
- if (!is_readable($file)) {
- throw new coding_exception('Unknown question type (no definition) ' . $qtypename);
- }
- include_once($file);
- self::$loadedqdefs[$qtypename] = 1;
- }
- /**
- * This method needs to be called whenever a question is edited.
- */
- public static function notify_question_edited($questionid) {
- question_finder::get_instance()->uncache_question($questionid);
- }
- /**
- * Load a question definition data from the database. The data will be
- * returned as a plain stdClass object.
- * @param int $questionid the id of the question to load.
- * @return object question definition loaded from the database.
- */
- public static function load_question_data($questionid) {
- return question_finder::get_instance()->load_question_data($questionid);
- }
- /**
- * Load a question definition from the database. The object returned
- * will actually be of an appropriate {@link question_definition} subclass.
- * @param int $questionid the id of the question to load.
- * @param bool $allowshuffle if false, then any shuffle option on the selected
- * quetsion is disabled.
- * @return question_definition loaded from the database.
- */
- public static function load_question($questionid, $allowshuffle = true) {
- global $DB;
- if (self::$testmode) {
- // Evil, test code in production, but now way round it.
- return self::return_test_question_data($questionid);
- }
- $questiondata = self::load_question_data($questionid);
- if (!$allowshuffle) {
- $questiondata->options->shuffleanswers = false;
- }
- return self::make_question($questiondata);
- }
- /**
- * Convert the question information loaded with {@link get_question_options()}
- * to a question_definintion object.
- * @param object $questiondata raw data loaded from the database.
- * @return question_definition loaded from the database.
- */
- public static function make_question($questiondata) {
- return self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
- }
- /**
- * @return question_finder a question finder.
- */
- public static function get_finder() {
- return question_finder::get_instance();
- if (is_null(self::$questionfinder)) {
- self::$questionfinder = new question_finder();
- }
- return self::$questionfinder;
- }
- /**
- * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
- */
- public static function start_unit_test() {
- self::$testmode = true;
- }
- /**
- * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
- */
- public static function end_unit_test() {
- self::$testmode = false;
- self::$testdata = array();
- }
- private static function return_test_question_data($questionid) {
- if (!isset(self::$testdata[$questionid])) {
- throw new coding_exception('question_bank::return_test_data(' . $questionid .
- ') called, but no matching question has been loaded by load_test_data.');
- }
- return self::$testdata[$questionid];
- }
- /**
- * To be used for unit testing only. Will throw an exception if
- * {@link start_unit_test()} has not been called first.
- * @param object $questiondata a question data object to put in the test data store.
- */
- public static function load_test_question_data(question_definition $question) {
- if (!self::$testmode) {
- throw new coding_exception('question_bank::load_test_data called when ' .
- 'not in test mode.');
- }
- self::$testdata[$question->id] = $question;
- }
- protected static function ensure_fraction_options_initialised() {
- if (!is_null(self::$fractionoptions)) {
- return;
- }
- // define basic array of grades. This list comprises all fractions of the form:
- // a. p/q for q <= 6, 0 <= p <= q
- // b. p/10 for 0 <= p <= 10
- // c. 1/q for 1 <= q <= 10
- // d. 1/20
- $rawfractions = array(
- 0.9000000,
- 0.8333333,
- 0.8000000,
- 0.7500000,
- 0.7000000,
- 0.6666667,
- 0.6000000,
- 0.5000000,
- 0.4000000,
- 0.3333333,
- 0.3000000,
- 0.2500000,
- 0.2000000,
- 0.1666667,
- 0.1428571,
- 0.1250000,
- 0.1111111,
- 0.1000000,
- 0.0500000,
- );
- // Put the None option at the top.
- self::$fractionoptions = array(
- '0.0' => get_string('none'),
- '1.0' => '100%',
- );
- self::$fractionoptionsfull = array(
- '0.0' => get_string('none'),
- '1.0' => '100%',
- );
- // The the positive grades in descending order.
- foreach ($rawfractions as $fraction) {
- $percentage = (100 * $fraction) . '%';
- self::$fractionoptions["$fraction"] = $percentage;
- self::$fractionoptionsfull["$fraction"] = $percentage;
- }
- // The the negative grades in descending order.
- foreach (array_reverse($rawfractions) as $fraction) {
- self::$fractionoptionsfull['' . (-$fraction)] = (-100 * $fraction) . '%';
- }
- self::$fractionoptionsfull['-1.0'] = '-100%';
- }
- /**
- * @return array string => string The standard set of grade options (fractions)
- * to use when editing questions, in the range 0 to 1 inclusive. Array keys
- * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
- * have float array keys in PHP.
- * Initialised by {@link ensure_grade_options_initialised()}.
- */
- public static function fraction_options() {
- self::ensure_fraction_options_initialised();
- return self::$fractionoptions;
- }
- /** @return array string => string The full standard set of (fractions) -1 to 1 inclusive. */
- public static function fraction_options_full() {
- self::ensure_fraction_options_initialised();
- return self::$fractionoptionsfull;
- }
- /**
- * Perform scheduled maintenance tasks relating to the question bank.
- */
- public static function cron() {
- global $CFG;
- // Delete any old question preview that got left in the database.
- require_once($CFG->dirroot . '/question/previewlib.php');
- question_preview_cron();
- }
- }
- /**
- * Class for loading questions according to various criteria.
- *
- * @copyright 2009 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
- class question_finder implements cache_data_source {
- /** @var question_finder the singleton instance of this class. */
- protected static $questionfinder = null;
- /** @var cache the question definition cache. */
- protected $cache = null;
- /**
- * @return question_finder a question finder.
- */
- public static function get_instance() {
- if (is_null(self::$questionfinder)) {
- self::$questionfinder = new question_finder();
- }
- return self::$questionfinder;
- }
- /* See cache_data_source::get_instance_for_cache. */
- public static function get_instance_for_cache(cache_definition $definition) {
- return self::get_instance();
- }
- /**
- * @return get the question definition cache we are using.
- */
- protected function get_data_cache() {
- if ($this->cache == null) {
- $this->cache = cache::make('core', 'questiondata');
- }
- return $this->cache;
- }
- /**
- * This method needs to be called whenever a question is edited.
- */
- public function uncache_question($questionid) {
- $this->get_data_cache()->delete($questionid);
- }
- /**
- * Load a question definition data from the database. The data will be
- * returned as a plain stdClass object.
- * @param int $questionid the id of the question to load.
- * @return object question definition loaded from the database.
- */
- public function load_question_data($questionid) {
- return $this->get_data_cache()->get($questionid);
- }
- /**
- * Get the ids of all the questions in a list of categoryies.
- * @param array $categoryids either a categoryid, or a comma-separated list
- * category ids, or an array of them.
- * @param string $extraconditions extra conditions to AND with the rest of
- * the where clause. Must use named parameters.
- * @param array $extraparams any parameters used by $extraconditions.
- * @return array questionid => questionid.
- */
- public function get_questions_from_categories($categoryids, $extraconditions,
- $extraparams = array()) {
- global $DB;
- list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
- if ($extraconditions) {
- $extraconditions = ' AND (' . $extraconditions . ')';
- }
- return $DB->get_records_select_menu('question',
- "category $qcsql
- AND parent = 0
- AND hidden = 0
- $extraconditions", $qcparams + $extraparams, '', 'id,id AS id2');
- }
- /* See cache_data_source::load_for_cache. */
- public function load_for_cache($questionid) {
- global $DB;
- $questiondata = $DB->get_record_sql('
- SELECT q.*, qc.contextid
- FROM {question} q
- JOIN {question_categories} qc ON q.category = qc.id
- WHERE q.id = :id', array('id' => $questionid), MUST_EXIST);
- get_question_options($questiondata);
- return $questiondata;
- }
- /* See cache_data_source::load_many_for_cache. */
- public function load_many_for_cache(array $questionids) {
- global $DB;
- list($idcondition, $params) = $DB->get_in_or_equal($questionids);
- $questiondata = $DB->get_records_sql('
- SELECT q.*, qc.contextid
- FROM {question} q
- JOIN {question_categories} qc ON q.category = qc.id
- WHERE q.id ' . $idcondition, $params);
- foreach ($questionids as $id) {
- if (!array_key_exists($id, $questionids)) {
- throw new dml_missing_record_exception('question', '', array('id' => $id));
- }
- get_question_options($questiondata[$id]);
- }
- return $questiondata;
- }
- }