MDL-20636 Review option defaults in the admin settings.
[moodle.git] / question / type / random / questiontype.php
CommitLineData
aeb15530 1<?php
1976496e 2/**
8b192edb 3 * Class for the random question type.
f34488b2 4 *
5 * The random question type does not have any options. When the question is
8b192edb 6 * attempted, it picks a question at random from the category it is in (and
7 * optionally its subcategories). For details see create_session_and_responses.
8 * Then all other method calls as delegated to that other question.
f34488b2 9 *
1976496e 10 * @package questionbank
11 * @subpackage questiontypes
53a4d39f 12 */
7518b645 13class random_qtype extends default_questiontype {
f24493ec 14 protected $excludedqtypes = null;
15 protected $manualqtypes = null;
516cf3eb 16
8b192edb 17 // Caches questions available as randoms sorted by category
18 // This is a 2-d array. The first key is question category, and the
19 // second is whether to include subcategories.
f24493ec 20 private $catrandoms = array();
516cf3eb 21
22 function name() {
23 return 'random';
24 }
25
a2156789 26 function menu_name() {
8b192edb 27 // Don't include this question type in the 'add new question' menu.
a2156789 28 return false;
29 }
30
f24493ec 31 function show_analysis_of_responses() {
32 return true;
33 }
34
35 function is_manual_graded() {
36 return true;
37 }
38
39 function is_question_manual_graded($question, $otherquestionsinuse) {
40 global $DB;
41 // We take our best shot at working whether a particular question is manually
42 // graded follows: We look to see if any of the questions that this random
43 // question might select if of a manually graded type. If a category contains
44 // a mixture of manual and non-manual questions, and if all the attempts so
45 // far selected non-manual ones, this will give the wrong answer, but we
46 // don't care. Even so, this is an expensive calculation!
47 $this->init_qtype_lists();
48 if (!$this->manualqtypes) {
49 return false;
50 }
51 if ($question->questiontext) {
52 $categorylist = question_categorylist($question->category);
53 } else {
54 $categorylist = $question->category;
55 }
56 return $DB->record_exists_select('question',
57 "category IN ($categorylist)
58 AND parent = 0
59 AND hidden = 0
60 AND id NOT IN ($otherquestionsinuse)
61 AND qtype IN ($this->manualqtypes)");
62 }
63
a2156789 64 function is_usable_by_random() {
65 return false;
66 }
67
f24493ec 68 /**
69 * This method needs to be called before the ->excludedqtypes and
70 * ->manualqtypes fields can be used.
71 */
72 function init_qtype_lists() {
73 global $QTYPES;
74 if (is_null($this->excludedqtypes)) {
75 $excludedqtypes = array();
76 $manualqtypes = array();
77 foreach ($QTYPES as $qtype) {
78 $quotedname = "'" . $qtype->name() . "'";
79 if (!$qtype->is_usable_by_random()) {
80 $excludedqtypes[] = $quotedname;
81 } else if ($qtype->is_manual_graded()) {
82 $manualqtypes[] = $quotedname;
83 }
84 }
85 $this->excludedqtypes = implode(',', $excludedqtypes);
86 $this->manualqtypes = implode(',', $manualqtypes);
87 }
88 }
89
bcc234b0 90 function display_question_editing_page(&$mform, $question, $wizardnow){
fe6ce234 91 global $OUTPUT;
4995b9c1 92 $heading = $this->get_heading(empty($question->id));
4bcc5118 93 echo $OUTPUT->heading_with_help($heading, $this->name(), $this->plugin_name());
bcc234b0 94 $mform->display();
95 }
96
516cf3eb 97 function get_question_options(&$question) {
98 // Don't do anything here, because the random question has no options.
99 // Everything is handled by the create- or restore_session_and_responses
100 // functions.
101 return true;
102 }
103
f59dba84 104 /**
105 * Random questions always get a question name that is Random (cateogryname).
106 * This function is a centralised place to calculate that, given the category.
3cac440b 107 * @param object $category the category this question picks from. (Only $category->name is used.)
108 * @param boolean $includesubcategories whether this question also picks from subcategories.
109 * @return string the name this question should have.
f59dba84 110 */
3cac440b 111 function question_name($category, $includesubcategories) {
112 if ($includesubcategories) {
113 $string = 'randomqplusname';
114 } else {
115 $string = 'randomqname';
116 }
117 return get_string($string, 'qtype_random', $category->name);
f59dba84 118 }
119
94dbfb3a 120 function save_question($question, $form) {
5348b899 121 $form->name = '';
122 // Name is not a required field for random questions, but parent::save_question
123 // Assumes that it is.
94dbfb3a 124 return parent::save_question($question, $form);
5348b899 125 }
126
24e8b9b6 127 function save_question_options($question) {
f34488b2 128 global $DB;
24e8b9b6 129
130 // No options, as such, but we set the parent field to the question's
131 // own id. Setting the parent field has the effect of hiding this
132 // question in various places.
133 $updateobject = new stdClass;
134 $updateobject->id = $question->id;
135 $updateobject->parent = $question->id;
136
137 // We also force the question name to be 'Random (categoryname)'.
f34488b2 138 if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) {
2471ef86 139 print_error('cannotretrieveqcat', 'question');
f59dba84 140 }
3cac440b 141 $updateobject->name = $this->question_name($category, !empty($question->questiontext));
24e8b9b6 142 return $DB->update_record('question', $updateobject);
516cf3eb 143 }
144
339ef4c2 145 /**
146 * Get all the usable questions from a particular question category.
147 *
148 * @param integer $categoryid the id of a question category.
149 * @param boolean whether to include questions from subcategories.
150 * @param string $questionsinuse comma-separated list of question ids to exclude from consideration.
151 * @return array of question records.
152 */
153 function get_usable_questions_from_category($categoryid, $subcategories, $questionsinuse) {
f24493ec 154 global $DB;
155 $this->init_qtype_lists();
339ef4c2 156 if ($subcategories) {
157 $categorylist = question_categorylist($categoryid);
158 } else {
159 $categorylist = $categoryid;
160 }
161 if (!$catrandoms = $DB->get_records_select('question',
162 "category IN ($categorylist)
f24493ec 163 AND parent = 0
164 AND hidden = 0
339ef4c2 165 AND id NOT IN ($questionsinuse)
f24493ec 166 AND qtype NOT IN ($this->excludedqtypes)", null, '', 'id')) {
339ef4c2 167 $catrandoms = array();
168 }
169 return $catrandoms;
170 }
171
516cf3eb 172 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
339ef4c2 173 global $QTYPES, $DB;
516cf3eb 174 // Choose a random question from the category:
175 // We need to make sure that no question is used more than once in the
176 // quiz. Therfore the following need to be excluded:
177 // 1. All questions that are explicitly assigned to the quiz
178 // 2. All random questions
179 // 3. All questions that are already chosen by an other random question
5dba6590 180 // 4. Deleted questions
516cf3eb 181 if (!isset($cmoptions->questionsinuse)) {
182 $cmoptions->questionsinuse = $attempt->layout;
183 }
184
185 if (!isset($this->catrandoms[$question->category][$question->questiontext])) {
41d37309 186 $catrandoms = $this->get_usable_questions_from_category($question->category,
339ef4c2 187 $question->questiontext == "1", $cmoptions->questionsinuse);
41d37309 188 $this->catrandoms[$question->category][$question->questiontext] = swapshuffle_assoc($catrandoms);
516cf3eb 189 }
190
339ef4c2 191 while ($wrappedquestion = array_pop(
192 $this->catrandoms[$question->category][$question->questiontext])) {
6dbcacee 193 if (!preg_match("~(^|,)$wrappedquestion->id(,|$)~", $cmoptions->questionsinuse)) {
516cf3eb 194 /// $randomquestion is not in use and will therefore be used
195 /// as the randomquestion here...
f34488b2 196 $wrappedquestion = $DB->get_record('question', array('id' => $wrappedquestion->id));
f02c6f01 197 global $QTYPES;
198 $QTYPES[$wrappedquestion->qtype]
339ef4c2 199 ->get_question_options($wrappedquestion);
f02c6f01 200 $QTYPES[$wrappedquestion->qtype]
339ef4c2 201 ->create_session_and_responses($wrappedquestion,
202 $state, $cmoptions, $attempt);
516cf3eb 203 $wrappedquestion->name_prefix = $question->name_prefix;
339ef4c2 204 $wrappedquestion->maxgrade = $question->maxgrade;
516cf3eb 205 $cmoptions->questionsinuse .= ",$wrappedquestion->id";
206 $state->options->question = &$wrappedquestion;
207 return true;
208 }
209 }
210 $question->questiontext = '<span class="notifyproblem">'.
339ef4c2 211 get_string('toomanyrandom', 'quiz'). '</span>';
dfa47f96 212 $question->qtype = 'description';
516cf3eb 213 $state->responses = array('' => '');
214 return true;
215 }
216
217 function restore_session_and_responses(&$question, &$state) {
218 /// The raw response records for random questions come in two flavours:
219 /// ---- 1 ----
220 /// For responses stored by Moodle version 1.5 and later the answer
221 /// field has the pattern random#-* where the # part is the numeric
222 /// question id of the actual question shown in the quiz attempt
223 /// and * represents the student response to that actual question.
224 /// ---- 2 ----
225 /// For responses stored by older Moodle versions - the answer field is
226 /// simply the question id of the actual question. The student response
227 /// to the actual question is stored in a separate response record.
228 /// -----------------------
229 /// This means that prior to Moodle version 1.5, random questions needed
230 /// two response records for storing the response to a single question.
231 /// From version 1.5 and later the question type random works like all
232 /// the other question types in that it now only needs one response
233 /// record per question.
fef8f84e 234 global $QTYPES, $DB, $OUTPUT;
6dbcacee 235 if (!preg_match('~^random([0-9]+)-(.*)$~', $state->responses[''], $answerregs)) {
516cf3eb 236 if (empty($state->responses[''])) {
237 // This is the case if there weren't enough questions available in the category.
238 $question->questiontext = '<span class="notifyproblem">'.
239 get_string('toomanyrandom', 'quiz'). '</span>';
dfa47f96 240 $question->qtype = 'description';
516cf3eb 241 return true;
242 }
243 // this must be an old-style state which stores only the id for the wrapped question
f34488b2 244 if (!$wrappedquestion = $DB->get_record('question', array('id' => $state->responses['']))) {
fef8f84e 245 echo $OUTPUT->notification("Can not find wrapped question {$state->responses['']}");
516cf3eb 246 }
247 // In the old model the actual response was stored in a separate entry in
248 // the state table and fortunately there was only a single state per question
f34488b2 249 if (!$state->responses[''] = $DB->get_field('question_states', 'answer', array('attempt' => $state->attempt, 'question' => $wrappedquestion->id))) {
fef8f84e 250 echo $OUTPUT->notification("Wrapped state missing");
516cf3eb 251 }
252 } else {
f34488b2 253 if (!$wrappedquestion = $DB->get_record('question', array('id' => $answerregs[1]))) {
4572d78f 254 // The teacher must have deleted this question by mistake
255 // Convert it into a description type question with an explanation to the student
256 $wrappedquestion = clone($question);
257 $wrappedquestion->id = $answerregs[1];
258 $wrappedquestion->questiontext = get_string('questiondeleted', 'quiz');
f57c6242 259 $wrappedquestion->qtype = 'missingtype';
516cf3eb 260 }
261 $state->responses[''] = (false === $answerregs[2]) ? '' : $answerregs[2];
262 }
263
f02c6f01 264 if (!$QTYPES[$wrappedquestion->qtype]
516cf3eb 265 ->get_question_options($wrappedquestion)) {
266 return false;
267 }
268
f02c6f01 269 if (!$QTYPES[$wrappedquestion->qtype]
516cf3eb 270 ->restore_session_and_responses($wrappedquestion, $state)) {
271 return false;
272 }
273 $wrappedquestion->name_prefix = $question->name_prefix;
274 $wrappedquestion->maxgrade = $question->maxgrade;
275 $state->options->question = &$wrappedquestion;
276 return true;
277 }
278
279 function save_session_and_responses(&$question, &$state) {
f34488b2 280 global $QTYPES, $DB;
516cf3eb 281 $wrappedquestion = &$state->options->question;
282
283 // Trick the wrapped question into pretending to be the random one.
284 $realqid = $wrappedquestion->id;
285 $wrappedquestion->id = $question->id;
f02c6f01 286 $QTYPES[$wrappedquestion->qtype]
516cf3eb 287 ->save_session_and_responses($wrappedquestion, $state);
288
289 // Read what the wrapped question has just set the answer field to
290 // (if anything)
f34488b2 291 $response = $DB->get_field('question_states', 'answer', array('id' => $state->id));
516cf3eb 292 if(false === $response) {
293 return false;
294 }
295
296 // Prefix the answer field...
297 $response = "random$realqid-$response";
298
299 // ... and save it again.
f685e830 300 $DB->set_field('question_states', 'answer', $response, array('id' => $state->id));
516cf3eb 301
302 // Restore the real id
303 $wrappedquestion->id = $realqid;
304 return true;
305 }
306
307 function get_correct_responses(&$question, &$state) {
f02c6f01 308 global $QTYPES;
516cf3eb 309 $wrappedquestion = &$state->options->question;
f02c6f01 310 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 311 ->get_correct_responses($wrappedquestion, $state);
312 }
313
314 // ULPGC ecastro
315 function get_all_responses(&$question, &$state){
f02c6f01 316 global $QTYPES;
516cf3eb 317 $wrappedquestion = &$state->options->question;
f02c6f01 318 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 319 ->get_all_responses($wrappedquestion, $state);
320 }
321
322 // ULPGC ecastro
323 function get_actual_response(&$question, &$state){
f02c6f01 324 global $QTYPES;
516cf3eb 325 $wrappedquestion = &$state->options->question;
f02c6f01 326 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 327 ->get_actual_response($wrappedquestion, $state);
328 }
329
99ba746d 330 function get_html_head_contributions(&$question, &$state) {
331 global $QTYPES;
332 $wrappedquestion = &$state->options->question;
333 return $QTYPES[$wrappedquestion->qtype]
334 ->get_html_head_contributions($wrappedquestion, $state);
335 }
516cf3eb 336
337 function print_question(&$question, &$state, &$number, $cmoptions, $options) {
f02c6f01 338 global $QTYPES;
516cf3eb 339 $wrappedquestion = &$state->options->question;
8bb3fac1 340 $wrappedquestion->randomquestionid = $question->id;
f02c6f01 341 $QTYPES[$wrappedquestion->qtype]
516cf3eb 342 ->print_question($wrappedquestion, $state, $number, $cmoptions, $options);
343 }
344
345 function grade_responses(&$question, &$state, $cmoptions) {
f02c6f01 346 global $QTYPES;
516cf3eb 347 $wrappedquestion = &$state->options->question;
f02c6f01 348 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 349 ->grade_responses($wrappedquestion, $state, $cmoptions);
350 }
351
352 function get_texsource(&$question, &$state, $cmoptions, $type) {
f02c6f01 353 global $QTYPES;
516cf3eb 354 $wrappedquestion = &$state->options->question;
f02c6f01 355 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 356 ->get_texsource($wrappedquestion, $state, $cmoptions, $type);
357 }
358
359 function compare_responses(&$question, $state, $teststate) {
f02c6f01 360 global $QTYPES;
bb080d20 361 $wrappedquestion = &$teststate->options->question;
f02c6f01 362 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 363 ->compare_responses($wrappedquestion, $state, $teststate);
364 }
365
455c3efa 366 /**
367 * For random question type return empty string which means won't calculate.
368 * @param object $question
369 * @return mixed either a integer score out of 1 that the average random
370 * guess by a student might give or an empty string which means will not
371 * calculate.
372 */
373 function get_random_guess_score($question) {
374 return '';
375 }
3ff8a187 376
516cf3eb 377}
378//// END OF CLASS ////
379
380//////////////////////////////////////////////////////////////////////////
381//// INITIATION - Without this line the question type is not in use... ///
382//////////////////////////////////////////////////////////////////////////
a2156789 383question_register_questiontype(new random_qtype());