MDL-40541 need to be able to select random q
[moodle.git] / question / type / random / questiontype.php
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/>.
17 /**
18  * Question type class for the random question type.
19  *
20  * @package    qtype
21  * @subpackage random
22  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/question/type/questiontypebase.php');
32 /**
33  * The random question type.
34  *
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.
38  *
39  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
40  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  */
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;
49     /**
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.
53      */
54     private $availablequestionsbycategory = array();
56     public function menu_name() {
57         // Don't include this question type in the 'add new question' menu.
58         return false;
59     }
61     public function is_manual_graded() {
62         return true;
63     }
65     public function is_usable_by_random() {
66         return false;
67     }
69     public function is_question_manual_graded($question, $otherquestionsinuse) {
70         global $DB;
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) {
79             return false;
80         }
81         if ($question->questiontext) {
82             $categorylist = question_categorylist($question->category);
83         } else {
84             $categorylist = array($question->category);
85         }
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',
89                 "category $qcsql
90                      AND parent = 0
91                      AND hidden = 0
92                      AND id NOT IN ($otherquestionsinuse)
93                      AND qtype IN ($this->manualqtypes)", $qcparams);
94     }
96     /**
97      * This method needs to be called before the ->excludedqtypes and
98      *      ->manualqtypes fields can be used.
99      */
100     protected function init_qtype_lists() {
101         if (!is_null($this->excludedqtypes)) {
102             return; // Already done.
103         }
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;
112             }
113         }
114         $this->excludedqtypes = implode(',', $excludedqtypes);
115         $this->manualqtypes = implode(',', $manualqtypes);
116     }
118     public function display_question_editing_page($mform, $question, $wizardnow) {
119         global $OUTPUT;
120         $heading = $this->get_heading(empty($question->id));
121         echo $OUTPUT->heading_with_help($heading, 'pluginname', $this->plugin_name());
122         $mform->display();
123     }
125     public function get_question_options($question) {
126         return true;
127     }
129     /**
130      * Random questions always get a question name that is Random (cateogryname).
131      * This function is a centralised place to calculate that, given the category.
132      * @param object $category the category this question picks from. (Only ->name is used.)
133      * @param bool $includesubcategories whether this question also picks from subcategories.
134      * @return string the name this question should have.
135      */
136     public function question_name($category, $includesubcategories) {
137         if ($includesubcategories) {
138             $string = 'randomqplusname';
139         } else {
140             $string = 'randomqname';
141         }
142         return get_string($string, 'qtype_random', shorten_text($category->name, 100));
143     }
145     protected function set_selected_question_name($question, $randomname) {
146         $a = new stdClass();
147         $a->randomname = $randomname;
148         $a->questionname = $question->name;
149         $question->name = get_string('selectedby', 'qtype_random', $a);
150     }
152     public function save_question($question, $form) {
153         $form->name = '';
154         $form->questiontextformat = FORMAT_MOODLE;
155         $form->tags = array();
157         // Name is not a required field for random questions, but
158         // parent::save_question Assumes that it is.
159         return parent::save_question($question, $form);
160     }
162     public function save_question_options($question) {
163         global $DB;
165         // No options, as such, but we set the parent field to the question's
166         // own id. Setting the parent field has the effect of hiding this
167         // question in various places.
168         $updateobject = new stdClass();
169         $updateobject->id = $question->id;
170         $updateobject->parent = $question->id;
172         // We also force the question name to be 'Random (categoryname)'.
173         $category = $DB->get_record('question_categories',
174                 array('id' => $question->category), '*', MUST_EXIST);
175         $updateobject->name = $this->question_name($category, !empty($question->questiontext));
176         return $DB->update_record('question', $updateobject);
177     }
179     /**
180      * During unit tests we need to be able to reset all caches so that each new test starts in a known state.
181      * Intended for use only for testing. This is a stop gap until we start using the MUC caching api here.
182      * You need to call this before every test that loads one or more random questions.
183      */
184     public function clear_caches_before_testing() {
185         $this->availablequestionsbycategory = array();
186     }
188     /**
189      * Get all the usable questions from a particular question category.
190      *
191      * @param int $categoryid the id of a question category.
192      * @param bool whether to include questions from subcategories.
193      * @param string $questionsinuse comma-separated list of question ids to
194      *      exclude from consideration.
195      * @return array of question records.
196      */
197     public function get_available_questions_from_category($categoryid, $subcategories) {
198         if (isset($this->availablequestionsbycategory[$categoryid][$subcategories])) {
199             return $this->availablequestionsbycategory[$categoryid][$subcategories];
200         }
202         $this->init_qtype_lists();
203         if ($subcategories) {
204             $categoryids = question_categorylist($categoryid);
205         } else {
206             $categoryids = array($categoryid);
207         }
209         $questionids = question_bank::get_finder()->get_questions_from_categories(
210                 $categoryids, 'qtype NOT IN (' . $this->excludedqtypes . ')');
211         $this->availablequestionsbycategory[$categoryid][$subcategories] = $questionids;
212         return $questionids;
213     }
215     public function make_question($questiondata) {
216         return $this->choose_other_question($questiondata, array());
217     }
219     /**
220      * Load the definition of another question picked randomly by this question.
221      * @param object       $questiondata the data defining a random question.
222      * @param array        $excludedquestions of question ids. We will no pick any question whose id is in this list.
223      * @param bool         $allowshuffle      if false, then any shuffle option on the selected quetsion is disabled.
224      * @param null|integer $forcequestionid   if not null then force the picking of question with id $forcequestionid.
225      * @throws coding_exception
226      * @return question_definition|null the definition of the question that was
227      *      selected, or null if no suitable question could be found.
228      */
229     public function choose_other_question($questiondata, $excludedquestions, $allowshuffle = true, $forcequestionid = null) {
230         $available = $this->get_available_questions_from_category($questiondata->category,
231                 !empty($questiondata->questiontext));
232         shuffle($available);
234         if ($forcequestionid !== null) {
235             $forcedquestionkey = array_search($forcequestionid, $available);
236             if ($forcedquestionkey !== false) {
237                 unset($available[$forcedquestionkey]);
238                 array_unshift($available, $forcequestionid);
239             } else {
240                 throw new coding_exception('thisquestionidisnotavailable', $forcequestionid);
241             }
242         }
244         foreach ($available as $questionid) {
245             if (in_array($questionid, $excludedquestions)) {
246                 continue;
247             }
249             $question = question_bank::load_question($questionid, $allowshuffle);
250             $this->set_selected_question_name($question, $questiondata->name);
251             return $question;
252         }
253         return null;
254     }
256     public function get_random_guess_score($questiondata) {
257         return null;
258     }