question bank: MDL-18215 select all checkbox does not always work.
[moodle.git] / question / type / random / questiontype.php
CommitLineData
516cf3eb 1<?php // $Id$
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){
91 list($heading, $langmodule) = $this->get_heading(empty($question->id));
92 print_heading_with_help($heading, $this->name(), $langmodule);
93 $mform->display();
94 }
95
516cf3eb 96 function get_question_options(&$question) {
97 // Don't do anything here, because the random question has no options.
98 // Everything is handled by the create- or restore_session_and_responses
99 // functions.
100 return true;
101 }
102
f59dba84 103 /**
104 * Random questions always get a question name that is Random (cateogryname).
105 * This function is a centralised place to calculate that, given the category.
106 */
107 function question_name($category) {
108 return get_string('random', 'quiz') .' ('. $category->name .')';
109 }
110
111 function save_question($question, $form, $course) {
f34488b2 112 global $DB;
f59dba84 113 // If the category is changing, set things up as default_questiontype::save_question expects.
114 list($formcategory, $unused) = explode(',', $form->category);
115 if (isset($question->id) && $formcategory != $question->category) {
116 $form->categorymoveto = $form->category;
117 }
118 $form->name = '';
119 $question = parent::save_question($question, $form, $course);
f34488b2 120 if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) {
2471ef86 121 print_error('cannotretrieveqcat', 'question');
f59dba84 122 }
123 $question->name = $this->question_name($category);
04c8148a 124 if (!$DB->set_field('question', 'name', $question->name, array('id' => $question->id))) {
2471ef86 125 print_error('cannotupdaterandomqname', 'question');
f59dba84 126 }
127 return $question;
128 }
129
516cf3eb 130 function save_question_options($question) {
f34488b2 131 global $DB;
8b192edb 132 // No options, but we set the parent field to the question's own id.
133 // Setting the parent field has the effect of hiding this question in
134 // various places.
f34488b2 135 return ($DB->set_field('question', 'parent', $question->id, array('id' => $question->id)) ? true : false);
516cf3eb 136 }
137
339ef4c2 138 /**
139 * Get all the usable questions from a particular question category.
140 *
141 * @param integer $categoryid the id of a question category.
142 * @param boolean whether to include questions from subcategories.
143 * @param string $questionsinuse comma-separated list of question ids to exclude from consideration.
144 * @return array of question records.
145 */
146 function get_usable_questions_from_category($categoryid, $subcategories, $questionsinuse) {
f24493ec 147 global $DB;
148 $this->init_qtype_lists();
339ef4c2 149 if ($subcategories) {
150 $categorylist = question_categorylist($categoryid);
151 } else {
152 $categorylist = $categoryid;
153 }
154 if (!$catrandoms = $DB->get_records_select('question',
155 "category IN ($categorylist)
f24493ec 156 AND parent = 0
157 AND hidden = 0
339ef4c2 158 AND id NOT IN ($questionsinuse)
f24493ec 159 AND qtype NOT IN ($this->excludedqtypes)", null, '', 'id')) {
339ef4c2 160 $catrandoms = array();
161 }
162 return $catrandoms;
163 }
164
516cf3eb 165 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
339ef4c2 166 global $QTYPES, $DB;
516cf3eb 167 // Choose a random question from the category:
168 // We need to make sure that no question is used more than once in the
169 // quiz. Therfore the following need to be excluded:
170 // 1. All questions that are explicitly assigned to the quiz
171 // 2. All random questions
172 // 3. All questions that are already chosen by an other random question
5dba6590 173 // 4. Deleted questions
516cf3eb 174 if (!isset($cmoptions->questionsinuse)) {
175 $cmoptions->questionsinuse = $attempt->layout;
176 }
177
178 if (!isset($this->catrandoms[$question->category][$question->questiontext])) {
41d37309 179 $catrandoms = $this->get_usable_questions_from_category($question->category,
339ef4c2 180 $question->questiontext == "1", $cmoptions->questionsinuse);
41d37309 181 $this->catrandoms[$question->category][$question->questiontext] = swapshuffle_assoc($catrandoms);
516cf3eb 182 }
183
339ef4c2 184 while ($wrappedquestion = array_pop(
185 $this->catrandoms[$question->category][$question->questiontext])) {
516cf3eb 186 if (!ereg("(^|,)$wrappedquestion->id(,|$)", $cmoptions->questionsinuse)) {
187 /// $randomquestion is not in use and will therefore be used
188 /// as the randomquestion here...
f34488b2 189 $wrappedquestion = $DB->get_record('question', array('id' => $wrappedquestion->id));
f02c6f01 190 global $QTYPES;
191 $QTYPES[$wrappedquestion->qtype]
339ef4c2 192 ->get_question_options($wrappedquestion);
f02c6f01 193 $QTYPES[$wrappedquestion->qtype]
339ef4c2 194 ->create_session_and_responses($wrappedquestion,
195 $state, $cmoptions, $attempt);
516cf3eb 196 $wrappedquestion->name_prefix = $question->name_prefix;
339ef4c2 197 $wrappedquestion->maxgrade = $question->maxgrade;
516cf3eb 198 $cmoptions->questionsinuse .= ",$wrappedquestion->id";
199 $state->options->question = &$wrappedquestion;
200 return true;
201 }
202 }
203 $question->questiontext = '<span class="notifyproblem">'.
339ef4c2 204 get_string('toomanyrandom', 'quiz'). '</span>';
dfa47f96 205 $question->qtype = 'description';
516cf3eb 206 $state->responses = array('' => '');
207 return true;
208 }
209
210 function restore_session_and_responses(&$question, &$state) {
211 /// The raw response records for random questions come in two flavours:
212 /// ---- 1 ----
213 /// For responses stored by Moodle version 1.5 and later the answer
214 /// field has the pattern random#-* where the # part is the numeric
215 /// question id of the actual question shown in the quiz attempt
216 /// and * represents the student response to that actual question.
217 /// ---- 2 ----
218 /// For responses stored by older Moodle versions - the answer field is
219 /// simply the question id of the actual question. The student response
220 /// to the actual question is stored in a separate response record.
221 /// -----------------------
222 /// This means that prior to Moodle version 1.5, random questions needed
223 /// two response records for storing the response to a single question.
224 /// From version 1.5 and later the question type random works like all
225 /// the other question types in that it now only needs one response
226 /// record per question.
f34488b2 227 global $QTYPES, $DB;
516cf3eb 228 if (!ereg('^random([0-9]+)-(.*)$', $state->responses[''], $answerregs)) {
229 if (empty($state->responses[''])) {
230 // This is the case if there weren't enough questions available in the category.
231 $question->questiontext = '<span class="notifyproblem">'.
232 get_string('toomanyrandom', 'quiz'). '</span>';
dfa47f96 233 $question->qtype = 'description';
516cf3eb 234 return true;
235 }
236 // this must be an old-style state which stores only the id for the wrapped question
f34488b2 237 if (!$wrappedquestion = $DB->get_record('question', array('id' => $state->responses['']))) {
516cf3eb 238 notify("Can not find wrapped question {$state->responses['']}");
239 }
240 // In the old model the actual response was stored in a separate entry in
241 // the state table and fortunately there was only a single state per question
f34488b2 242 if (!$state->responses[''] = $DB->get_field('question_states', 'answer', array('attempt' => $state->attempt, 'question' => $wrappedquestion->id))) {
516cf3eb 243 notify("Wrapped state missing");
244 }
245 } else {
f34488b2 246 if (!$wrappedquestion = $DB->get_record('question', array('id' => $answerregs[1]))) {
4572d78f 247 // The teacher must have deleted this question by mistake
248 // Convert it into a description type question with an explanation to the student
249 $wrappedquestion = clone($question);
250 $wrappedquestion->id = $answerregs[1];
251 $wrappedquestion->questiontext = get_string('questiondeleted', 'quiz');
f57c6242 252 $wrappedquestion->qtype = 'missingtype';
516cf3eb 253 }
254 $state->responses[''] = (false === $answerregs[2]) ? '' : $answerregs[2];
255 }
256
f02c6f01 257 if (!$QTYPES[$wrappedquestion->qtype]
516cf3eb 258 ->get_question_options($wrappedquestion)) {
259 return false;
260 }
261
f02c6f01 262 if (!$QTYPES[$wrappedquestion->qtype]
516cf3eb 263 ->restore_session_and_responses($wrappedquestion, $state)) {
264 return false;
265 }
266 $wrappedquestion->name_prefix = $question->name_prefix;
267 $wrappedquestion->maxgrade = $question->maxgrade;
268 $state->options->question = &$wrappedquestion;
269 return true;
270 }
271
272 function save_session_and_responses(&$question, &$state) {
f34488b2 273 global $QTYPES, $DB;
516cf3eb 274 $wrappedquestion = &$state->options->question;
275
276 // Trick the wrapped question into pretending to be the random one.
277 $realqid = $wrappedquestion->id;
278 $wrappedquestion->id = $question->id;
f02c6f01 279 $QTYPES[$wrappedquestion->qtype]
516cf3eb 280 ->save_session_and_responses($wrappedquestion, $state);
281
282 // Read what the wrapped question has just set the answer field to
283 // (if anything)
f34488b2 284 $response = $DB->get_field('question_states', 'answer', array('id' => $state->id));
516cf3eb 285 if(false === $response) {
286 return false;
287 }
288
289 // Prefix the answer field...
290 $response = "random$realqid-$response";
291
292 // ... and save it again.
f34488b2 293 if (!$DB->set_field('question_states', 'answer', $response, array('id' => $state->id))) {
516cf3eb 294 }
295
296 // Restore the real id
297 $wrappedquestion->id = $realqid;
298 return true;
299 }
300
301 function get_correct_responses(&$question, &$state) {
f02c6f01 302 global $QTYPES;
516cf3eb 303 $wrappedquestion = &$state->options->question;
f02c6f01 304 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 305 ->get_correct_responses($wrappedquestion, $state);
306 }
307
308 // ULPGC ecastro
309 function get_all_responses(&$question, &$state){
f02c6f01 310 global $QTYPES;
516cf3eb 311 $wrappedquestion = &$state->options->question;
f02c6f01 312 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 313 ->get_all_responses($wrappedquestion, $state);
314 }
315
316 // ULPGC ecastro
317 function get_actual_response(&$question, &$state){
f02c6f01 318 global $QTYPES;
516cf3eb 319 $wrappedquestion = &$state->options->question;
f02c6f01 320 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 321 ->get_actual_response($wrappedquestion, $state);
322 }
323
99ba746d 324 function get_html_head_contributions(&$question, &$state) {
325 global $QTYPES;
326 $wrappedquestion = &$state->options->question;
327 return $QTYPES[$wrappedquestion->qtype]
328 ->get_html_head_contributions($wrappedquestion, $state);
329 }
516cf3eb 330
331 function print_question(&$question, &$state, &$number, $cmoptions, $options) {
f02c6f01 332 global $QTYPES;
516cf3eb 333 $wrappedquestion = &$state->options->question;
8bb3fac1 334 $wrappedquestion->randomquestionid = $question->id;
f02c6f01 335 $QTYPES[$wrappedquestion->qtype]
516cf3eb 336 ->print_question($wrappedquestion, $state, $number, $cmoptions, $options);
337 }
338
339 function grade_responses(&$question, &$state, $cmoptions) {
f02c6f01 340 global $QTYPES;
516cf3eb 341 $wrappedquestion = &$state->options->question;
f02c6f01 342 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 343 ->grade_responses($wrappedquestion, $state, $cmoptions);
344 }
345
346 function get_texsource(&$question, &$state, $cmoptions, $type) {
f02c6f01 347 global $QTYPES;
516cf3eb 348 $wrappedquestion = &$state->options->question;
f02c6f01 349 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 350 ->get_texsource($wrappedquestion, $state, $cmoptions, $type);
351 }
352
353 function compare_responses(&$question, $state, $teststate) {
f02c6f01 354 global $QTYPES;
bb080d20 355 $wrappedquestion = &$teststate->options->question;
f02c6f01 356 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 357 ->compare_responses($wrappedquestion, $state, $teststate);
358 }
359
3ff8a187 360 function restore_recode_answer($state, $restore) {
361 // The answer looks like 'randomXX-ANSWER', where XX is
362 // the id of the used question and ANSWER the actual
363 // response to that question.
364 // However, there may still be old-style states around,
365 // which store the id of the wrapped question in the
366 // state of the random question and store the response
367 // in a separate state for the wrapped question
368
f34488b2 369 global $QTYPES, $DB;
3ff8a187 370 $answer_field = "";
371
372 if (ereg('^random([0-9]+)-(.*)$', $state->answer, $answerregs)) {
373 // Recode the question id in $answerregs[1]
374 // Get the question from backup_ids
375 if(!$wrapped = backup_getid($restore->backup_unique_code,"question",$answerregs[1])) {
376 echo 'Could not recode question in random-'.$answerregs[1].'<br />';
377 return($answer_field);
378 }
379 // Get the question type for recursion
f34488b2 380 if (!$wrappedquestion->qtype = $DB->get_field('question', 'qtype', array('id' => $wrapped->new_id))) {
3ff8a187 381 echo 'Could not get qtype while recoding question random-'.$answerregs[1].'<br />';
382 return($answer_field);
383 }
384 $newstate = $state;
385 $newstate->question = $wrapped->new_id;
386 $newstate->answer = $answerregs[2];
387 $answer_field = 'random'.$wrapped->new_id.'-';
388
389 // Recode the answer field in $answerregs[2] depending on
390 // the qtype of question with id $answerregs[1]
391 $answer_field .= $QTYPES[$wrappedquestion->qtype]->restore_recode_answer($newstate, $restore);
392 } else {
393 // Handle old-style states
394 $answer_link = backup_getid($restore->backup_unique_code,"question",$state->answer);
395 if ($answer_link) {
396 $answer_field = $answer_link->new_id;
397 }
398 }
399
400 return $answer_field;
401 }
455c3efa 402
403 /**
404 * For random question type return empty string which means won't calculate.
405 * @param object $question
406 * @return mixed either a integer score out of 1 that the average random
407 * guess by a student might give or an empty string which means will not
408 * calculate.
409 */
410 function get_random_guess_score($question) {
411 return '';
412 }
3ff8a187 413
516cf3eb 414}
415//// END OF CLASS ////
416
417//////////////////////////////////////////////////////////////////////////
418//// INITIATION - Without this line the question type is not in use... ///
419//////////////////////////////////////////////////////////////////////////
a2156789 420question_register_questiontype(new random_qtype());
516cf3eb 421
422?>