MDL-14204 "Content for Quiz Statistics report table - Random_guess_score" added metho...
[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 {
516cf3eb 14
8b192edb 15 // Caches questions available as randoms sorted by category
16 // This is a 2-d array. The first key is question category, and the
17 // second is whether to include subcategories.
516cf3eb 18 var $catrandoms = array();
19
20 function name() {
21 return 'random';
22 }
23
a2156789 24 function menu_name() {
8b192edb 25 // Don't include this question type in the 'add new question' menu.
a2156789 26 return false;
27 }
28
29 function is_usable_by_random() {
30 return false;
31 }
32
516cf3eb 33 function get_question_options(&$question) {
34 // Don't do anything here, because the random question has no options.
35 // Everything is handled by the create- or restore_session_and_responses
36 // functions.
37 return true;
38 }
39
f59dba84 40 /**
41 * Random questions always get a question name that is Random (cateogryname).
42 * This function is a centralised place to calculate that, given the category.
43 */
44 function question_name($category) {
45 return get_string('random', 'quiz') .' ('. $category->name .')';
46 }
47
48 function save_question($question, $form, $course) {
f34488b2 49 global $DB;
f59dba84 50 // If the category is changing, set things up as default_questiontype::save_question expects.
51 list($formcategory, $unused) = explode(',', $form->category);
52 if (isset($question->id) && $formcategory != $question->category) {
53 $form->categorymoveto = $form->category;
54 }
55 $form->name = '';
56 $question = parent::save_question($question, $form, $course);
f34488b2 57 if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) {
2471ef86 58 print_error('cannotretrieveqcat', 'question');
f59dba84 59 }
60 $question->name = $this->question_name($category);
f34488b2 61 if (!$DB->set_field('question', 'name', $question->name, array('id', $question->id))) {
2471ef86 62 print_error('cannotupdaterandomqname', 'question');
f59dba84 63 }
64 return $question;
65 }
66
516cf3eb 67 function save_question_options($question) {
f34488b2 68 global $DB;
8b192edb 69 // No options, but we set the parent field to the question's own id.
70 // Setting the parent field has the effect of hiding this question in
71 // various places.
f34488b2 72 return ($DB->set_field('question', 'parent', $question->id, array('id' => $question->id)) ? true : false);
516cf3eb 73 }
74
75 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
f34488b2 76 global $DB;
a2156789 77 global $QTYPE_EXCLUDE_FROM_RANDOM;
516cf3eb 78 // Choose a random question from the category:
79 // We need to make sure that no question is used more than once in the
80 // quiz. Therfore the following need to be excluded:
81 // 1. All questions that are explicitly assigned to the quiz
82 // 2. All random questions
83 // 3. All questions that are already chosen by an other random question
5dba6590 84 // 4. Deleted questions
516cf3eb 85 if (!isset($cmoptions->questionsinuse)) {
86 $cmoptions->questionsinuse = $attempt->layout;
87 }
88
89 if (!isset($this->catrandoms[$question->category][$question->questiontext])) {
90 // Need to fetch random questions from category $question->category"
91 // (Note: $this refers to the questiontype, not the question.)
92 global $CFG;
516cf3eb 93 if ($question->questiontext == "1") {
94 // recurse into subcategories
dc1f00de 95 $categorylist = question_categorylist($question->category);
516cf3eb 96 } else {
97 $categorylist = $question->category;
98 }
f34488b2 99 if ($catrandoms = $DB->get_records_select('question',
5dba6590 100 "category IN ($categorylist)
516cf3eb 101 AND parent = '0'
5dba6590 102 AND hidden = '0'
516cf3eb 103 AND id NOT IN ($cmoptions->questionsinuse)
6f51ed72 104 AND qtype NOT IN ($QTYPE_EXCLUDE_FROM_RANDOM)", array(), '', 'id')) {
516cf3eb 105 $this->catrandoms[$question->category][$question->questiontext] =
5dba6590 106 draw_rand_array($catrandoms, count($catrandoms));
516cf3eb 107 } else {
108 $this->catrandoms[$question->category][$question->questiontext] = array();
109 }
110 }
111
112 while ($wrappedquestion =
113 array_pop($this->catrandoms[$question->category][$question->questiontext])) {
114 if (!ereg("(^|,)$wrappedquestion->id(,|$)", $cmoptions->questionsinuse)) {
115 /// $randomquestion is not in use and will therefore be used
116 /// as the randomquestion here...
f34488b2 117 $wrappedquestion = $DB->get_record('question', array('id' => $wrappedquestion->id));
f02c6f01 118 global $QTYPES;
119 $QTYPES[$wrappedquestion->qtype]
516cf3eb 120 ->get_question_options($wrappedquestion);
f02c6f01 121 $QTYPES[$wrappedquestion->qtype]
516cf3eb 122 ->create_session_and_responses($wrappedquestion,
123 $state, $cmoptions, $attempt);
124 $wrappedquestion->name_prefix = $question->name_prefix;
125 $wrappedquestion->maxgrade = $question->maxgrade;
126 $cmoptions->questionsinuse .= ",$wrappedquestion->id";
127 $state->options->question = &$wrappedquestion;
128 return true;
129 }
130 }
131 $question->questiontext = '<span class="notifyproblem">'.
132 get_string('toomanyrandom', 'quiz'). '</span>';
dfa47f96 133 $question->qtype = 'description';
516cf3eb 134 $state->responses = array('' => '');
135 return true;
136 }
137
138 function restore_session_and_responses(&$question, &$state) {
139 /// The raw response records for random questions come in two flavours:
140 /// ---- 1 ----
141 /// For responses stored by Moodle version 1.5 and later the answer
142 /// field has the pattern random#-* where the # part is the numeric
143 /// question id of the actual question shown in the quiz attempt
144 /// and * represents the student response to that actual question.
145 /// ---- 2 ----
146 /// For responses stored by older Moodle versions - the answer field is
147 /// simply the question id of the actual question. The student response
148 /// to the actual question is stored in a separate response record.
149 /// -----------------------
150 /// This means that prior to Moodle version 1.5, random questions needed
151 /// two response records for storing the response to a single question.
152 /// From version 1.5 and later the question type random works like all
153 /// the other question types in that it now only needs one response
154 /// record per question.
f34488b2 155 global $QTYPES, $DB;
516cf3eb 156 if (!ereg('^random([0-9]+)-(.*)$', $state->responses[''], $answerregs)) {
157 if (empty($state->responses[''])) {
158 // This is the case if there weren't enough questions available in the category.
159 $question->questiontext = '<span class="notifyproblem">'.
160 get_string('toomanyrandom', 'quiz'). '</span>';
dfa47f96 161 $question->qtype = 'description';
516cf3eb 162 return true;
163 }
164 // this must be an old-style state which stores only the id for the wrapped question
f34488b2 165 if (!$wrappedquestion = $DB->get_record('question', array('id' => $state->responses['']))) {
516cf3eb 166 notify("Can not find wrapped question {$state->responses['']}");
167 }
168 // In the old model the actual response was stored in a separate entry in
169 // the state table and fortunately there was only a single state per question
f34488b2 170 if (!$state->responses[''] = $DB->get_field('question_states', 'answer', array('attempt' => $state->attempt, 'question' => $wrappedquestion->id))) {
516cf3eb 171 notify("Wrapped state missing");
172 }
173 } else {
f34488b2 174 if (!$wrappedquestion = $DB->get_record('question', array('id' => $answerregs[1]))) {
4572d78f 175 // The teacher must have deleted this question by mistake
176 // Convert it into a description type question with an explanation to the student
177 $wrappedquestion = clone($question);
178 $wrappedquestion->id = $answerregs[1];
179 $wrappedquestion->questiontext = get_string('questiondeleted', 'quiz');
f57c6242 180 $wrappedquestion->qtype = 'missingtype';
516cf3eb 181 }
182 $state->responses[''] = (false === $answerregs[2]) ? '' : $answerregs[2];
183 }
184
f02c6f01 185 if (!$QTYPES[$wrappedquestion->qtype]
516cf3eb 186 ->get_question_options($wrappedquestion)) {
187 return false;
188 }
189
f02c6f01 190 if (!$QTYPES[$wrappedquestion->qtype]
516cf3eb 191 ->restore_session_and_responses($wrappedquestion, $state)) {
192 return false;
193 }
194 $wrappedquestion->name_prefix = $question->name_prefix;
195 $wrappedquestion->maxgrade = $question->maxgrade;
196 $state->options->question = &$wrappedquestion;
197 return true;
198 }
199
200 function save_session_and_responses(&$question, &$state) {
f34488b2 201 global $QTYPES, $DB;
516cf3eb 202 $wrappedquestion = &$state->options->question;
203
204 // Trick the wrapped question into pretending to be the random one.
205 $realqid = $wrappedquestion->id;
206 $wrappedquestion->id = $question->id;
f02c6f01 207 $QTYPES[$wrappedquestion->qtype]
516cf3eb 208 ->save_session_and_responses($wrappedquestion, $state);
209
210 // Read what the wrapped question has just set the answer field to
211 // (if anything)
f34488b2 212 $response = $DB->get_field('question_states', 'answer', array('id' => $state->id));
516cf3eb 213 if(false === $response) {
214 return false;
215 }
216
217 // Prefix the answer field...
218 $response = "random$realqid-$response";
219
220 // ... and save it again.
f34488b2 221 if (!$DB->set_field('question_states', 'answer', $response, array('id' => $state->id))) {
516cf3eb 222 }
223
224 // Restore the real id
225 $wrappedquestion->id = $realqid;
226 return true;
227 }
228
229 function get_correct_responses(&$question, &$state) {
f02c6f01 230 global $QTYPES;
516cf3eb 231 $wrappedquestion = &$state->options->question;
f02c6f01 232 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 233 ->get_correct_responses($wrappedquestion, $state);
234 }
235
236 // ULPGC ecastro
237 function get_all_responses(&$question, &$state){
f02c6f01 238 global $QTYPES;
516cf3eb 239 $wrappedquestion = &$state->options->question;
f02c6f01 240 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 241 ->get_all_responses($wrappedquestion, $state);
242 }
243
244 // ULPGC ecastro
245 function get_actual_response(&$question, &$state){
f02c6f01 246 global $QTYPES;
516cf3eb 247 $wrappedquestion = &$state->options->question;
f02c6f01 248 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 249 ->get_actual_response($wrappedquestion, $state);
250 }
251
99ba746d 252 function get_html_head_contributions(&$question, &$state) {
253 global $QTYPES;
254 $wrappedquestion = &$state->options->question;
255 return $QTYPES[$wrappedquestion->qtype]
256 ->get_html_head_contributions($wrappedquestion, $state);
257 }
516cf3eb 258
259 function print_question(&$question, &$state, &$number, $cmoptions, $options) {
f02c6f01 260 global $QTYPES;
516cf3eb 261 $wrappedquestion = &$state->options->question;
8bb3fac1 262 $wrappedquestion->randomquestionid = $question->id;
f02c6f01 263 $QTYPES[$wrappedquestion->qtype]
516cf3eb 264 ->print_question($wrappedquestion, $state, $number, $cmoptions, $options);
265 }
266
267 function grade_responses(&$question, &$state, $cmoptions) {
f02c6f01 268 global $QTYPES;
516cf3eb 269 $wrappedquestion = &$state->options->question;
f02c6f01 270 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 271 ->grade_responses($wrappedquestion, $state, $cmoptions);
272 }
273
274 function get_texsource(&$question, &$state, $cmoptions, $type) {
f02c6f01 275 global $QTYPES;
516cf3eb 276 $wrappedquestion = &$state->options->question;
f02c6f01 277 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 278 ->get_texsource($wrappedquestion, $state, $cmoptions, $type);
279 }
280
281 function compare_responses(&$question, $state, $teststate) {
f02c6f01 282 global $QTYPES;
bb080d20 283 $wrappedquestion = &$teststate->options->question;
f02c6f01 284 return $QTYPES[$wrappedquestion->qtype]
516cf3eb 285 ->compare_responses($wrappedquestion, $state, $teststate);
286 }
287
3ff8a187 288 function restore_recode_answer($state, $restore) {
289 // The answer looks like 'randomXX-ANSWER', where XX is
290 // the id of the used question and ANSWER the actual
291 // response to that question.
292 // However, there may still be old-style states around,
293 // which store the id of the wrapped question in the
294 // state of the random question and store the response
295 // in a separate state for the wrapped question
296
f34488b2 297 global $QTYPES, $DB;
3ff8a187 298 $answer_field = "";
299
300 if (ereg('^random([0-9]+)-(.*)$', $state->answer, $answerregs)) {
301 // Recode the question id in $answerregs[1]
302 // Get the question from backup_ids
303 if(!$wrapped = backup_getid($restore->backup_unique_code,"question",$answerregs[1])) {
304 echo 'Could not recode question in random-'.$answerregs[1].'<br />';
305 return($answer_field);
306 }
307 // Get the question type for recursion
f34488b2 308 if (!$wrappedquestion->qtype = $DB->get_field('question', 'qtype', array('id' => $wrapped->new_id))) {
3ff8a187 309 echo 'Could not get qtype while recoding question random-'.$answerregs[1].'<br />';
310 return($answer_field);
311 }
312 $newstate = $state;
313 $newstate->question = $wrapped->new_id;
314 $newstate->answer = $answerregs[2];
315 $answer_field = 'random'.$wrapped->new_id.'-';
316
317 // Recode the answer field in $answerregs[2] depending on
318 // the qtype of question with id $answerregs[1]
319 $answer_field .= $QTYPES[$wrappedquestion->qtype]->restore_recode_answer($newstate, $restore);
320 } else {
321 // Handle old-style states
322 $answer_link = backup_getid($restore->backup_unique_code,"question",$state->answer);
323 if ($answer_link) {
324 $answer_field = $answer_link->new_id;
325 }
326 }
327
328 return $answer_field;
329 }
330
516cf3eb 331}
332//// END OF CLASS ////
333
334//////////////////////////////////////////////////////////////////////////
335//// INITIATION - Without this line the question type is not in use... ///
336//////////////////////////////////////////////////////////////////////////
a2156789 337question_register_questiontype(new random_qtype());
516cf3eb 338
339?>