Merge branch 'MDL-68438_310' of https://github.com/t-schroeder/moodle into MOODLE_310...
[moodle.git] / question / type / random / questiontype.php
CommitLineData
aeb15530 1<?php
f9b0500f
TH
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
1976496e 17/**
f9b0500f 18 * Question type class for the random question type.
f34488b2 19 *
7764183a
TH
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
f9b0500f
TH
24 */
25
26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
2779cc7d
TH
29require_once($CFG->dirroot . '/question/type/questiontypebase.php');
30
a17b297d 31
f9b0500f
TH
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.
f34488b2 38 *
7764183a
TH
39 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
53a4d39f 41 */
f9b0500f
TH
42class qtype_random extends question_type {
43 /** @var string comma-separated list of qytpe names not to select, can be used in SQL. */
f24493ec 44 protected $excludedqtypes = null;
516cf3eb 45
f9b0500f
TH
46 /** @var string comma-separated list of manually graded qytpe names, can be used in SQL. */
47 protected $manualqtypes = null;
516cf3eb 48
f9b0500f
TH
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();
516cf3eb 55
f9b0500f 56 public function menu_name() {
8b192edb 57 // Don't include this question type in the 'add new question' menu.
a2156789 58 return false;
59 }
60
f9b0500f 61 public function is_manual_graded() {
f24493ec 62 return true;
63 }
64
f9b0500f
TH
65 public function is_usable_by_random() {
66 return false;
f24493ec 67 }
68
f9b0500f 69 public function is_question_manual_graded($question, $otherquestionsinuse) {
f24493ec 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 {
2daffca5 84 $categorylist = array($question->category);
f24493ec 85 }
2daffca5 86 list($qcsql, $qcparams) = $DB->get_in_or_equal($categorylist);
3d9645ae 87 // TODO use in_or_equal for $otherquestionsinuse and $this->manualqtypes.
f24493ec 88 return $DB->record_exists_select('question',
f4fe3968 89 "category {$qcsql}
f24493ec 90 AND parent = 0
91 AND hidden = 0
92 AND id NOT IN ($otherquestionsinuse)
2daffca5 93 AND qtype IN ($this->manualqtypes)", $qcparams);
f24493ec 94 }
95
f24493ec 96 /**
97 * This method needs to be called before the ->excludedqtypes and
98 * ->manualqtypes fields can be used.
99 */
f9b0500f
TH
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;
f24493ec 112 }
f24493ec 113 }
f9b0500f
TH
114 $this->excludedqtypes = implode(',', $excludedqtypes);
115 $this->manualqtypes = implode(',', $manualqtypes);
f24493ec 116 }
117
f9b0500f 118 public function get_question_options($question) {
bb930b7d 119 parent::get_question_options($question);
516cf3eb 120 return true;
121 }
122
f59dba84 123 /**
124 * Random questions always get a question name that is Random (cateogryname).
125 * This function is a centralised place to calculate that, given the category.
4f964a1c 126 * @param stdClass $category the category this question picks from. (Only ->name is used.)
f7970e3c 127 * @param bool $includesubcategories whether this question also picks from subcategories.
4f964a1c 128 * @param string[] $tagnames Name of tags this question picks from.
3cac440b 129 * @return string the name this question should have.
f59dba84 130 */
4f964a1c
SR
131 public function question_name($category, $includesubcategories, $tagnames = []) {
132 $categoryname = '';
3b8f3198 133 if ($category->parent && $includesubcategories) {
4f964a1c
SR
134 $stringid = 'randomqplusname';
135 $categoryname = shorten_text($category->name, 100);
3b8f3198 136 } else if ($category->parent) {
4f964a1c
SR
137 $stringid = 'randomqname';
138 $categoryname = shorten_text($category->name, 100);
3b8f3198
SR
139 } else if ($includesubcategories) {
140 $context = context::instance_by_id($category->contextid);
141
142 switch ($context->contextlevel) {
143 case CONTEXT_MODULE:
4f964a1c 144 $stringid = 'randomqplusnamemodule';
3b8f3198
SR
145 break;
146 case CONTEXT_COURSE:
4f964a1c 147 $stringid = 'randomqplusnamecourse';
3b8f3198
SR
148 break;
149 case CONTEXT_COURSECAT:
4f964a1c
SR
150 $stringid = 'randomqplusnamecoursecat';
151 $categoryname = shorten_text($context->get_context_name(false), 100);
3b8f3198
SR
152 break;
153 case CONTEXT_SYSTEM:
4f964a1c 154 $stringid = 'randomqplusnamesystem';
3b8f3198
SR
155 break;
156 default: // Impossible.
3b8f3198 157 }
3cac440b 158 } else {
3b8f3198 159 // No question will ever be selected. So, let's warn the teacher.
4f964a1c 160 $stringid = 'randomqnamefromtop';
3cac440b 161 }
3b8f3198 162
4f964a1c
SR
163 if ($tagnames) {
164 $stringid .= 'tags';
165 $a = new stdClass();
166 if ($categoryname) {
167 $a->category = $categoryname;
168 }
89c8a076 169 $a->tags = implode(', ', array_map(function($tagname) {
4f964a1c
SR
170 return explode(',', $tagname)[1];
171 }, $tagnames));
172 } else {
173 $a = $categoryname ? : null;
174 }
175
176 $name = get_string($stringid, 'qtype_random', $a);
177
178 return shorten_text($name, 255);
f59dba84 179 }
180
f9b0500f 181 protected function set_selected_question_name($question, $randomname) {
0ff4bd08 182 $a = new stdClass();
f9b0500f
TH
183 $a->randomname = $randomname;
184 $a->questionname = $question->name;
185 $question->name = get_string('selectedby', 'qtype_random', $a);
5348b899 186 }
187
a13d4fbd 188 public function save_question($question, $form) {
3b8f3198
SR
189 global $DB;
190
f9b0500f 191 $form->name = '';
3b8f3198 192 list($category) = explode(',', $form->category);
516a3ddc 193
4f964a1c
SR
194 if (!$form->includesubcategories) {
195 if ($DB->record_exists('question_categories', ['id' => $category, 'parent' => 0])) {
196 // The chosen category is a top category.
197 $form->includesubcategories = true;
198 }
a871a7a7 199 }
4f964a1c 200
72553162
TH
201 $form->tags = array();
202
4f964a1c
SR
203 if (empty($form->fromtags)) {
204 $form->fromtags = array();
205 }
206
207 $form->questiontext = array(
208 'text' => $form->includesubcategories ? '1' : '0',
209 'format' => 0
210 );
211
f9b0500f
TH
212 // Name is not a required field for random questions, but
213 // parent::save_question Assumes that it is.
a13d4fbd 214 return parent::save_question($question, $form);
f9b0500f 215 }
24e8b9b6 216
f9b0500f 217 public function save_question_options($question) {
a13d4fbd
TH
218 global $DB;
219
24e8b9b6 220 // No options, as such, but we set the parent field to the question's
221 // own id. Setting the parent field has the effect of hiding this
222 // question in various places.
0ff4bd08 223 $updateobject = new stdClass();
24e8b9b6 224 $updateobject->id = $question->id;
225 $updateobject->parent = $question->id;
226
227 // We also force the question name to be 'Random (categoryname)'.
59f26004
TH
228 $category = $DB->get_record('question_categories',
229 array('id' => $question->category), '*', MUST_EXIST);
4f964a1c 230 $updateobject->name = $this->question_name($category, $question->includesubcategories, $question->fromtags);
24e8b9b6 231 return $DB->update_record('question', $updateobject);
516cf3eb 232 }
233
09ff04bc
JP
234 /**
235 * During unit tests we need to be able to reset all caches so that each new test starts in a known state.
236 * Intended for use only for testing. This is a stop gap until we start using the MUC caching api here.
237 * You need to call this before every test that loads one or more random questions.
238 */
239 public function clear_caches_before_testing() {
240 $this->availablequestionsbycategory = array();
241 }
242
339ef4c2 243 /**
244 * Get all the usable questions from a particular question category.
245 *
f7970e3c
TH
246 * @param int $categoryid the id of a question category.
247 * @param bool whether to include questions from subcategories.
59f26004
TH
248 * @param string $questionsinuse comma-separated list of question ids to
249 * exclude from consideration.
339ef4c2 250 * @return array of question records.
251 */
f9b0500f
TH
252 public function get_available_questions_from_category($categoryid, $subcategories) {
253 if (isset($this->availablequestionsbycategory[$categoryid][$subcategories])) {
254 return $this->availablequestionsbycategory[$categoryid][$subcategories];
255 }
256
f24493ec 257 $this->init_qtype_lists();
339ef4c2 258 if ($subcategories) {
f9b0500f 259 $categoryids = question_categorylist($categoryid);
339ef4c2 260 } else {
2daffca5 261 $categoryids = array($categoryid);
516cf3eb 262 }
263
f9b0500f
TH
264 $questionids = question_bank::get_finder()->get_questions_from_categories(
265 $categoryids, 'qtype NOT IN (' . $this->excludedqtypes . ')');
266 $this->availablequestionsbycategory[$categoryid][$subcategories] = $questionids;
267 return $questionids;
516cf3eb 268 }
269
f9b0500f
TH
270 public function make_question($questiondata) {
271 return $this->choose_other_question($questiondata, array());
516cf3eb 272 }
273
f9b0500f
TH
274 /**
275 * Load the definition of another question picked randomly by this question.
c2f2e7f0
JP
276 * @param object $questiondata the data defining a random question.
277 * @param array $excludedquestions of question ids. We will no pick any question whose id is in this list.
278 * @param bool $allowshuffle if false, then any shuffle option on the selected quetsion is disabled.
279 * @param null|integer $forcequestionid if not null then force the picking of question with id $forcequestionid.
280 * @throws coding_exception
f9b0500f
TH
281 * @return question_definition|null the definition of the question that was
282 * selected, or null if no suitable question could be found.
283 */
c2f2e7f0 284 public function choose_other_question($questiondata, $excludedquestions, $allowshuffle = true, $forcequestionid = null) {
f9b0500f
TH
285 $available = $this->get_available_questions_from_category($questiondata->category,
286 !empty($questiondata->questiontext));
287 shuffle($available);
288
c2f2e7f0
JP
289 if ($forcequestionid !== null) {
290 $forcedquestionkey = array_search($forcequestionid, $available);
291 if ($forcedquestionkey !== false) {
292 unset($available[$forcedquestionkey]);
293 array_unshift($available, $forcequestionid);
294 } else {
295 throw new coding_exception('thisquestionidisnotavailable', $forcequestionid);
296 }
297 }
298
f9b0500f
TH
299 foreach ($available as $questionid) {
300 if (in_array($questionid, $excludedquestions)) {
301 continue;
302 }
516cf3eb 303
f9b0500f
TH
304 $question = question_bank::load_question($questionid, $allowshuffle);
305 $this->set_selected_question_name($question, $questiondata->name);
306 return $question;
516cf3eb 307 }
f9b0500f 308 return null;
516cf3eb 309 }
310
c7df5006 311 public function get_random_guess_score($questiondata) {
f9b0500f 312 return null;
516cf3eb 313 }
516cf3eb 314}