3 * Class for the random question type.
5 * The random question type does not have any options. When the question is
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.
10 * @package questionbank
11 * @subpackage questiontypes
13 class random_qtype
extends default_questiontype
{
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.
18 var $catrandoms = array();
24 function menu_name() {
25 // Don't include this question type in the 'add new question' menu.
29 function is_usable_by_random() {
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
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.
44 function question_name($category) {
45 return get_string('random', 'quiz') .' ('. $category->name
.')';
48 function save_question($question, $form, $course) {
49 // If the category is changing, set things up as default_questiontype::save_question expects.
50 list($formcategory, $unused) = explode(',', $form->category
);
51 if (isset($question->id
) && $formcategory != $question->category
) {
52 $form->categorymoveto
= $form->category
;
55 $question = parent
::save_question($question, $form, $course);
56 if (!$category = get_record('question_categories', 'id', $question->category
)) {
57 error('Could retrieve question category');
59 $question->name
= $this->question_name($category);
60 if (!set_field('question', 'name', addslashes($question->name
), 'id', $question->id
)) {
61 error('Could not update random question name');
66 function save_question_options($question) {
67 // No options, but we set the parent field to the question's own id.
68 // Setting the parent field has the effect of hiding this question in
70 return (set_field('question', 'parent', $question->id
, 'id',
71 $question->id
) ?
true : false);
74 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
75 global $QTYPE_EXCLUDE_FROM_RANDOM;
76 // Choose a random question from the category:
77 // We need to make sure that no question is used more than once in the
78 // quiz. Therfore the following need to be excluded:
79 // 1. All questions that are explicitly assigned to the quiz
80 // 2. All random questions
81 // 3. All questions that are already chosen by an other random question
82 // 4. Deleted questions
83 if (!isset($cmoptions->questionsinuse
)) {
84 $cmoptions->questionsinuse
= $attempt->layout
;
87 if (!isset($this->catrandoms
[$question->category
][$question->questiontext
])) {
88 // Need to fetch random questions from category $question->category"
89 // (Note: $this refers to the questiontype, not the question.)
91 if ($question->questiontext
== "1") {
92 // recurse into subcategories
93 $categorylist = question_categorylist($question->category
);
95 $categorylist = $question->category
;
97 if ($catrandoms = get_records_select('question',
98 "category IN ($categorylist)
101 AND id NOT IN ($cmoptions->questionsinuse)
102 AND qtype NOT IN ($QTYPE_EXCLUDE_FROM_RANDOM)", '', 'id')) {
103 $this->catrandoms
[$question->category
][$question->questiontext
] =
104 draw_rand_array($catrandoms, count($catrandoms));
106 $this->catrandoms
[$question->category
][$question->questiontext
] = array();
110 while ($wrappedquestion =
111 array_pop($this->catrandoms
[$question->category
][$question->questiontext
])) {
112 if (!ereg("(^|,)$wrappedquestion->id(,|$)", $cmoptions->questionsinuse
)) {
113 /// $randomquestion is not in use and will therefore be used
114 /// as the randomquestion here...
115 $wrappedquestion = get_record('question', 'id', $wrappedquestion->id
);
117 $QTYPES[$wrappedquestion->qtype
]
118 ->get_question_options($wrappedquestion);
119 $QTYPES[$wrappedquestion->qtype
]
120 ->create_session_and_responses($wrappedquestion,
121 $state, $cmoptions, $attempt);
122 $wrappedquestion->name_prefix
= $question->name_prefix
;
123 $wrappedquestion->maxgrade
= $question->maxgrade
;
124 $cmoptions->questionsinuse
.= ",$wrappedquestion->id";
125 $state->options
->question
= &$wrappedquestion;
129 $question->questiontext
= '<span class="notifyproblem">'.
130 get_string('toomanyrandom', 'quiz'). '</span>';
131 $question->qtype
= 'description';
132 $state->responses
= array('' => '');
136 function restore_session_and_responses(&$question, &$state) {
137 /// The raw response records for random questions come in two flavours:
139 /// For responses stored by Moodle version 1.5 and later the answer
140 /// field has the pattern random#-* where the # part is the numeric
141 /// question id of the actual question shown in the quiz attempt
142 /// and * represents the student response to that actual question.
144 /// For responses stored by older Moodle versions - the answer field is
145 /// simply the question id of the actual question. The student response
146 /// to the actual question is stored in a separate response record.
147 /// -----------------------
148 /// This means that prior to Moodle version 1.5, random questions needed
149 /// two response records for storing the response to a single question.
150 /// From version 1.5 and later the question type random works like all
151 /// the other question types in that it now only needs one response
152 /// record per question.
154 if (!ereg('^random([0-9]+)-(.*)$', $state->responses
[''], $answerregs)) {
155 if (empty($state->responses
[''])) {
156 // This is the case if there weren't enough questions available in the category.
157 $question->questiontext
= '<span class="notifyproblem">'.
158 get_string('toomanyrandom', 'quiz'). '</span>';
159 $question->qtype
= 'description';
162 // this must be an old-style state which stores only the id for the wrapped question
163 if (!$wrappedquestion = get_record('question', 'id', $state->responses
[''])) {
164 notify("Can not find wrapped question {$state->responses['']}");
166 // In the old model the actual response was stored in a separate entry in
167 // the state table and fortunately there was only a single state per question
168 if (!$state->responses
[''] = get_field('question_states', 'answer', 'attempt', $state->attempt
, 'question', $wrappedquestion->id
)) {
169 notify("Wrapped state missing");
172 if (!$wrappedquestion = get_record('question', 'id', $answerregs[1])) {
173 // The teacher must have deleted this question by mistake
174 // Convert it into a description type question with an explanation to the student
175 $wrappedquestion = clone($question);
176 $wrappedquestion->id
= $answerregs[1];
177 $wrappedquestion->questiontext
= get_string('questiondeleted', 'quiz');
178 $wrappedquestion->qtype
= 'missingtype';
180 $state->responses
[''] = (false === $answerregs[2]) ?
'' : $answerregs[2];
183 if (!$QTYPES[$wrappedquestion->qtype
]
184 ->get_question_options($wrappedquestion)) {
188 if (!$QTYPES[$wrappedquestion->qtype
]
189 ->restore_session_and_responses($wrappedquestion, $state)) {
192 $wrappedquestion->name_prefix
= $question->name_prefix
;
193 $wrappedquestion->maxgrade
= $question->maxgrade
;
194 $state->options
->question
= &$wrappedquestion;
198 function save_session_and_responses(&$question, &$state) {
200 $wrappedquestion = &$state->options
->question
;
202 // Trick the wrapped question into pretending to be the random one.
203 $realqid = $wrappedquestion->id
;
204 $wrappedquestion->id
= $question->id
;
205 $QTYPES[$wrappedquestion->qtype
]
206 ->save_session_and_responses($wrappedquestion, $state);
208 // Read what the wrapped question has just set the answer field to
210 $response = get_field('question_states', 'answer', 'id', $state->id
);
211 if(false === $response) {
215 // Prefix the answer field...
216 $response = "random$realqid-$response";
218 // ... and save it again.
219 if (!set_field('question_states', 'answer', addslashes($response), 'id', $state->id
)) {
223 // Restore the real id
224 $wrappedquestion->id
= $realqid;
228 function get_correct_responses(&$question, &$state) {
230 $wrappedquestion = &$state->options
->question
;
231 return $QTYPES[$wrappedquestion->qtype
]
232 ->get_correct_responses($wrappedquestion, $state);
236 function get_all_responses(&$question, &$state){
238 $wrappedquestion = &$state->options
->question
;
239 return $QTYPES[$wrappedquestion->qtype
]
240 ->get_all_responses($wrappedquestion, $state);
244 function get_actual_response(&$question, &$state){
246 $wrappedquestion = &$state->options
->question
;
247 return $QTYPES[$wrappedquestion->qtype
]
248 ->get_actual_response($wrappedquestion, $state);
251 function get_html_head_contributions(&$question, &$state) {
253 $wrappedquestion = &$state->options
->question
;
254 return $QTYPES[$wrappedquestion->qtype
]
255 ->get_html_head_contributions($wrappedquestion, $state);
258 function print_question(&$question, &$state, &$number, $cmoptions, $options) {
260 $wrappedquestion = &$state->options
->question
;
261 $wrappedquestion->randomquestionid
= $question->id
;
262 $QTYPES[$wrappedquestion->qtype
]
263 ->print_question($wrappedquestion, $state, $number, $cmoptions, $options);
266 function grade_responses(&$question, &$state, $cmoptions) {
268 $wrappedquestion = &$state->options
->question
;
269 return $QTYPES[$wrappedquestion->qtype
]
270 ->grade_responses($wrappedquestion, $state, $cmoptions);
273 function get_texsource(&$question, &$state, $cmoptions, $type) {
275 $wrappedquestion = &$state->options
->question
;
276 return $QTYPES[$wrappedquestion->qtype
]
277 ->get_texsource($wrappedquestion, $state, $cmoptions, $type);
280 function compare_responses(&$question, $state, $teststate) {
282 $wrappedquestion = &$teststate->options
->question
;
283 return $QTYPES[$wrappedquestion->qtype
]
284 ->compare_responses($wrappedquestion, $state, $teststate);
287 function restore_recode_answer($state, $restore) {
288 // The answer looks like 'randomXX-ANSWER', where XX is
289 // the id of the used question and ANSWER the actual
290 // response to that question.
291 // However, there may still be old-style states around,
292 // which store the id of the wrapped question in the
293 // state of the random question and store the response
294 // in a separate state for the wrapped question
299 if (ereg('^random([0-9]+)-(.*)$', $state->answer
, $answerregs)) {
300 // Recode the question id in $answerregs[1]
301 // Get the question from backup_ids
302 if(!$wrapped = backup_getid($restore->backup_unique_code
,"question",$answerregs[1])) {
303 echo 'Could not recode question in random-'.$answerregs[1].'<br />';
304 return($answer_field);
306 // Get the question type for recursion
307 if (!$wrappedquestion->qtype
= get_field('question', 'qtype', 'id', $wrapped->new_id
)) {
308 echo 'Could not get qtype while recoding question random-'.$answerregs[1].'<br />';
309 return($answer_field);
312 $newstate->question
= $wrapped->new_id
;
313 $newstate->answer
= $answerregs[2];
314 $answer_field = 'random'.$wrapped->new_id
.'-';
316 // Recode the answer field in $answerregs[2] depending on
317 // the qtype of question with id $answerregs[1]
318 $answer_field .= $QTYPES[$wrappedquestion->qtype
]->restore_recode_answer($newstate, $restore);
320 // Handle old-style states
321 $answer_link = backup_getid($restore->backup_unique_code
,"question",$state->answer
);
323 $answer_field = $answer_link->new_id
;
327 return $answer_field;
331 //// END OF CLASS ////
333 //////////////////////////////////////////////////////////////////////////
334 //// INITIATION - Without this line the question type is not in use... ///
335 //////////////////////////////////////////////////////////////////////////
336 question_register_questiontype(new random_qtype());