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