2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Question type class for the random question type.
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/question/type/questiontypebase.php');
33 * The random question type.
35 * This question type does not have a question definition class, nor any
36 * renderers. When you load a question of this type, it actually loads a
37 * question chosen randomly from a particular category in the question bank.
39 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42 class qtype_random extends question_type {
43 /** @var string comma-separated list of qytpe names not to select, can be used in SQL. */
44 protected $excludedqtypes = null;
46 /** @var string comma-separated list of manually graded qytpe names, can be used in SQL. */
47 protected $manualqtypes = null;
50 * Cache of availabe question ids from a particular category.
51 * @var array two-dimensional array. The first key is a category id, the
52 * second key is wether subcategories should be included.
54 private $availablequestionsbycategory = array();
56 public function menu_name() {
57 // Don't include this question type in the 'add new question' menu.
61 public function is_manual_graded() {
65 public function is_usable_by_random() {
69 public function is_question_manual_graded($question, $otherquestionsinuse) {
71 // We take our best shot at working whether a particular question is manually
72 // graded follows: We look to see if any of the questions that this random
73 // question might select if of a manually graded type. If a category contains
74 // a mixture of manual and non-manual questions, and if all the attempts so
75 // far selected non-manual ones, this will give the wrong answer, but we
76 // don't care. Even so, this is an expensive calculation!
77 $this->init_qtype_lists();
78 if (!$this->manualqtypes) {
81 if ($question->questiontext) {
82 $categorylist = question_categorylist($question->category);
84 $categorylist = array($question->category);
86 list($qcsql, $qcparams) = $DB->get_in_or_equal($categorylist);
87 // TODO use in_or_equal for $otherquestionsinuse and $this->manualqtypes.
88 return $DB->record_exists_select('question',
92 AND id NOT IN ($otherquestionsinuse)
93 AND qtype IN ($this->manualqtypes)", $qcparams);
97 * This method needs to be called before the ->excludedqtypes and
98 * ->manualqtypes fields can be used.
100 protected function init_qtype_lists() {
101 if (!is_null($this->excludedqtypes)) {
102 return; // Already done.
104 $excludedqtypes = array();
105 $manualqtypes = array();
106 foreach (question_bank::get_all_qtypes() as $qtype) {
107 $quotedname = "'" . $qtype->name() . "'";
108 if (!$qtype->is_usable_by_random()) {
109 $excludedqtypes[] = $quotedname;
110 } else if ($qtype->is_manual_graded()) {
111 $manualqtypes[] = $quotedname;
114 $this->excludedqtypes = implode(',', $excludedqtypes);
115 $this->manualqtypes = implode(',', $manualqtypes);
118 public function get_question_options($question) {
123 * Random questions always get a question name that is Random (cateogryname).
124 * This function is a centralised place to calculate that, given the category.
125 * @param object $category the category this question picks from. (Only ->name is used.)
126 * @param bool $includesubcategories whether this question also picks from subcategories.
127 * @return string the name this question should have.
129 public function question_name($category, $includesubcategories) {
130 if ($includesubcategories) {
131 $string = 'randomqplusname';
133 $string = 'randomqname';
135 return get_string($string, 'qtype_random', shorten_text($category->name, 100));
138 protected function set_selected_question_name($question, $randomname) {
140 $a->randomname = $randomname;
141 $a->questionname = $question->name;
142 $question->name = get_string('selectedby', 'qtype_random', $a);
145 public function save_question($question, $form) {
148 // In case someone set the question text to true/false in the old style, set it properly.
149 if ($form->questiontext['text']) {
150 $form->questiontext['text'] = '1';
152 $form->questiontext['text'] = '0';
154 $form->tags = array();
156 // Name is not a required field for random questions, but
157 // parent::save_question Assumes that it is.
158 return parent::save_question($question, $form);
161 public function save_question_options($question) {
164 // No options, as such, but we set the parent field to the question's
165 // own id. Setting the parent field has the effect of hiding this
166 // question in various places.
167 $updateobject = new stdClass();
168 $updateobject->id = $question->id;
169 $updateobject->parent = $question->id;
171 // We also force the question name to be 'Random (categoryname)'.
172 $category = $DB->get_record('question_categories',
173 array('id' => $question->category), '*', MUST_EXIST);
174 $updateobject->name = $this->question_name($category, !empty($question->questiontext));
175 return $DB->update_record('question', $updateobject);
179 * During unit tests we need to be able to reset all caches so that each new test starts in a known state.
180 * Intended for use only for testing. This is a stop gap until we start using the MUC caching api here.
181 * You need to call this before every test that loads one or more random questions.
183 public function clear_caches_before_testing() {
184 $this->availablequestionsbycategory = array();
188 * Get all the usable questions from a particular question category.
190 * @param int $categoryid the id of a question category.
191 * @param bool whether to include questions from subcategories.
192 * @param string $questionsinuse comma-separated list of question ids to
193 * exclude from consideration.
194 * @return array of question records.
196 public function get_available_questions_from_category($categoryid, $subcategories) {
197 if (isset($this->availablequestionsbycategory[$categoryid][$subcategories])) {
198 return $this->availablequestionsbycategory[$categoryid][$subcategories];
201 $this->init_qtype_lists();
202 if ($subcategories) {
203 $categoryids = question_categorylist($categoryid);
205 $categoryids = array($categoryid);
208 $questionids = question_bank::get_finder()->get_questions_from_categories(
209 $categoryids, 'qtype NOT IN (' . $this->excludedqtypes . ')');
210 $this->availablequestionsbycategory[$categoryid][$subcategories] = $questionids;
214 public function make_question($questiondata) {
215 return $this->choose_other_question($questiondata, array());
219 * Load the definition of another question picked randomly by this question.
220 * @param object $questiondata the data defining a random question.
221 * @param array $excludedquestions of question ids. We will no pick any question whose id is in this list.
222 * @param bool $allowshuffle if false, then any shuffle option on the selected quetsion is disabled.
223 * @param null|integer $forcequestionid if not null then force the picking of question with id $forcequestionid.
224 * @throws coding_exception
225 * @return question_definition|null the definition of the question that was
226 * selected, or null if no suitable question could be found.
228 public function choose_other_question($questiondata, $excludedquestions, $allowshuffle = true, $forcequestionid = null) {
229 $available = $this->get_available_questions_from_category($questiondata->category,
230 !empty($questiondata->questiontext));
233 if ($forcequestionid !== null) {
234 $forcedquestionkey = array_search($forcequestionid, $available);
235 if ($forcedquestionkey !== false) {
236 unset($available[$forcedquestionkey]);
237 array_unshift($available, $forcequestionid);
239 throw new coding_exception('thisquestionidisnotavailable', $forcequestionid);
243 foreach ($available as $questionid) {
244 if (in_array($questionid, $excludedquestions)) {
248 $question = question_bank::load_question($questionid, $allowshuffle);
249 $this->set_selected_question_name($question, $questiondata->name);
255 public function get_random_guess_score($questiondata) {