Issue:
[moodle-pu.git] / question / type / randomsamatch / questiontype.php
blobfb4a8306dce0baa295841ffee8c0fc1abe0c8fa0
1 <?php // $Id$
3 /////////////////////
4 /// RANDOMSAMATCH ///
5 /////////////////////
7 /// TODO: Make sure short answer questions chosen by a randomsamatch question
8 /// can not also be used by a random question
10 /// QUESTION TYPE CLASS //////////////////
11 /**
12 * @package questionbank
13 * @subpackage questiontypes
15 class question_randomsamatch_qtype extends question_match_qtype {
16 /// Extends 'match' as there are quite a few simularities...
18 function name() {
19 return 'randomsamatch';
22 function is_usable_by_random() {
23 return false;
26 function get_question_options(&$question) {
27 if (!$question->options = get_record('question_randomsamatch', 'question', $question->id)) {
28 notify('Error: Missing question options for random short answer question '.$question->id.'!');
29 return false;
32 // This could be included as a flag in the database. It's already
33 // supported by the code.
34 // Recurse subcategories: 0 = no recursion, 1 = recursion
35 $question->options->subcats = 1;
36 return true;
40 function save_question_options($question) {
41 $options->question = $question->id;
42 $options->choose = $question->choose;
44 if (2 > $question->choose) {
45 $result->error = "At least two shortanswer questions need to be chosen!";
46 return $result;
49 if ($existing = get_record("question_randomsamatch",
50 "question", $options->question)) {
51 $options->id = $existing->id;
52 if (!update_record("question_randomsamatch", $options)) {
53 $result->error = "Could not update quiz randomsamatch options!";
54 return $result;
56 } else {
57 if (!insert_record("question_randomsamatch", $options)) {
58 $result->error = "Could not insert quiz randomsamatch options!";
59 return $result;
62 return true;
65 /**
66 * Deletes question from the question-type specific tables
68 * @return boolean Success/Failure
69 * @param object $question The question being deleted
71 function delete_question($questionid) {
72 delete_records("question_randomsamatch", "question", $questionid);
73 return true;
76 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
77 // Choose a random shortanswer question from the category:
78 // We need to make sure that no question is used more than once in the
79 // quiz. Therfore the following need to be excluded:
80 // 1. All questions that are explicitly assigned to the quiz
81 // 2. All random questions
82 // 3. All questions that are already chosen by an other random question
83 global $QTYPES;
84 if (!isset($cmoptions->questionsinuse)) {
85 $cmoptions->questionsinuse = $cmoptions->questions;
88 if ($question->options->subcats) {
89 // recurse into subcategories
90 $categorylist = question_categorylist($question->category);
91 } else {
92 $categorylist = $question->category;
95 $saquestions = $this->get_sa_candidates($categorylist, $cmoptions->questionsinuse);
97 $count = count($saquestions);
98 $wanted = $question->options->choose;
99 $errorstr = '';
100 if ($count < $wanted && isteacherinanycourse()) {
101 if ($count >= 2) {
102 $errorstr = "Error: could not get enough Short-Answer questions!
103 Got $count Short-Answer questions, but wanted $wanted.
104 Reducing number to choose from to $count!";
105 $wanted = $question->options->choose = $count;
106 } else {
107 $errorstr = "Error: could not get enough Short-Answer questions!
108 This can happen if all available Short-Answer questions are already
109 taken up by other Random questions or Random Short-Answer question.
110 Another possible cause for this error is that Short-Answer
111 questions were deleted after this Random Short-Answer question was
112 created.";
114 notify($errorstr);
115 $errorstr = '<span class="notifyproblem">' . $errorstr . '</span>';
118 if ($count < $wanted) {
119 $question->questiontext = "$errorstr<br /><br />Insufficient selection options are
120 available for this question, therefore it is not available in this
121 quiz. Please inform your teacher.";
122 // Treat this as a description from this point on
123 $question->qtype = DESCRIPTION;
124 return true;
127 $saquestions =
128 draw_rand_array($saquestions, $question->options->choose); // from bug 1889
130 foreach ($saquestions as $key => $wrappedquestion) {
131 if (!$QTYPES[$wrappedquestion->qtype]
132 ->get_question_options($wrappedquestion)) {
133 return false;
136 // Now we overwrite the $question->options->answers field to only
137 // *one* (the first) correct answer. This loop can be deleted to
138 // take all answers into account (i.e. put them all into the
139 // drop-down menu.
140 $foundcorrect = false;
141 foreach ($wrappedquestion->options->answers as $answer) {
142 if ($foundcorrect || $answer->fraction != 1.0) {
143 unset($wrappedquestion->options->answers[$answer->id]);
144 } else if (!$foundcorrect) {
145 $foundcorrect = true;
149 if (!$QTYPES[$wrappedquestion->qtype]
150 ->create_session_and_responses($wrappedquestion, $state, $cmoptions,
151 $attempt)) {
152 return false;
154 $wrappedquestion->name_prefix = $question->name_prefix;
155 $wrappedquestion->maxgrade = $question->maxgrade;
156 $cmoptions->questionsinuse .= ",$wrappedquestion->id";
157 $state->options->subquestions[$key] = clone($wrappedquestion);
160 // Shuffle the answers (Do this always because this is a random question type)
161 $subquestionids = array_values(array_map(create_function('$val',
162 'return $val->id;'), $state->options->subquestions));
163 $subquestionids = swapshuffle($subquestionids);
165 // Create empty responses
166 foreach ($subquestionids as $val) {
167 $state->responses[$val] = '';
169 return true;
172 function restore_session_and_responses(&$question, &$state) {
173 global $QTYPES;
174 if (empty($state->responses[''])) {
175 $question->questiontext = "Insufficient selection options are
176 available for this question, therefore it is not available in this
177 quiz. Please inform your teacher.";
178 // Treat this as a description from this point on
179 $question->qtype = DESCRIPTION;
180 } else {
181 $responses = explode(',', $state->responses['']);
182 $responses = array_map(create_function('$val',
183 'return explode("-", $val);'), $responses);
185 // Restore the previous responses
186 $state->responses = array();
187 foreach ($responses as $response) {
188 $state->responses[$response[0]] = $response[1];
189 if (!$wrappedquestion = get_record('question', 'id',
190 $response[0])) {
191 notify("Couldn't get question (id=$response[0])!");
192 return false;
194 if (!$QTYPES[$wrappedquestion->qtype]
195 ->get_question_options($wrappedquestion)) {
196 notify("Couldn't get question options (id=$response[0])!");
197 return false;
200 // Now we overwrite the $question->options->answers field to only
201 // *one* (the first) correct answer. This loop can be deleted to
202 // take all answers into account (i.e. put them all into the
203 // drop-down menu.
204 $foundcorrect = false;
205 foreach ($wrappedquestion->options->answers as $answer) {
206 if ($foundcorrect || $answer->fraction != 1.0) {
207 unset($wrappedquestion->options->answers[$answer->id]);
208 } else if (!$foundcorrect) {
209 $foundcorrect = true;
213 if (!$QTYPES[$wrappedquestion->qtype]
214 ->restore_session_and_responses($wrappedquestion, $state)) {
215 notify("Couldn't restore session of question (id=$response[0])!");
216 return false;
218 $wrappedquestion->name_prefix = $question->name_prefix;
219 $wrappedquestion->maxgrade = $question->maxgrade;
221 $state->options->subquestions[$wrappedquestion->id] =
222 clone($wrappedquestion);
225 return true;
228 function extract_response($rawresponse, $nameprefix) {
229 /// Simple implementation that does not check with the database
230 /// and thus - does not bother to check whether there has been
231 /// any changes to the question options.
232 $response = array();
233 $rawitems = explode(',', $rawresponse->answer);
234 foreach ($rawitems as $rawitem) {
235 $splits = explode('-', $rawitem, 2);
236 $response[$nameprefix.$splits[0]] = $splits[1];
238 return $response;
241 function get_sa_candidates($categorylist, $questionsinuse=0) {
242 return get_records_select('question',
243 "qtype = '".'shortanswer'."' " .
244 "AND category IN ($categorylist) " .
245 "AND parent = '0' " .
246 "AND hidden = '0'" .
247 "AND id NOT IN ($questionsinuse)");
250 /// BACKUP FUNCTIONS ////////////////////////////
253 * Backup the data in the question
255 * This is used in question/backuplib.php
257 function backup($bf,$preferences,$question,$level=6) {
259 $status = true;
261 $randomsamatchs = get_records("question_randomsamatch","question",$question,"id");
262 //If there are randomsamatchs
263 if ($randomsamatchs) {
264 //Iterate over each randomsamatch
265 foreach ($randomsamatchs as $randomsamatch) {
266 $status = fwrite ($bf,start_tag("RANDOMSAMATCH",6,true));
267 //Print randomsamatch contents
268 fwrite ($bf,full_tag("CHOOSE",7,false,$randomsamatch->choose));
269 $status = fwrite ($bf,end_tag("RANDOMSAMATCH",6,true));
272 return $status;
275 /// RESTORE FUNCTIONS /////////////////
278 * Restores the data in the question
280 * This is used in question/restorelib.php
282 function restore($old_question_id,$new_question_id,$info,$restore) {
284 $status = true;
286 //Get the randomsamatchs array
287 $randomsamatchs = $info['#']['RANDOMSAMATCH'];
289 //Iterate over randomsamatchs
290 for($i = 0; $i < sizeof($randomsamatchs); $i++) {
291 $ran_info = $randomsamatchs[$i];
293 //Now, build the question_randomsamatch record structure
294 $randomsamatch->question = $new_question_id;
295 $randomsamatch->choose = backup_todb($ran_info['#']['CHOOSE']['0']['#']);
297 //The structure is equal to the db, so insert the question_randomsamatch
298 $newid = insert_record ("question_randomsamatch",$randomsamatch);
300 //Do some output
301 if (($i+1) % 50 == 0) {
302 if (!defined('RESTORE_SILENTLY')) {
303 echo ".";
304 if (($i+1) % 1000 == 0) {
305 echo "<br />";
308 backup_flush(300);
311 if (!$newid) {
312 $status = false;
316 return $status;
319 function restore_recode_answer($state, $restore) {
321 //The answer is a comma separated list of hypen separated question_id and answer_id. We must recode them
322 $answer_field = "";
323 $in_first = true;
324 $tok = strtok($state->answer,",");
325 while ($tok) {
326 //Extract the question_id and the answer_id
327 $exploded = explode("-",$tok);
328 $question_id = $exploded[0];
329 $answer_id = $exploded[1];
330 //Get the question from backup_ids
331 if (!$que = backup_getid($restore->backup_unique_code,"question",$question_id)) {
332 echo 'Could not recode randomsamatch question '.$question_id.'<br />';
335 if ($answer_id == 0) { // no response yet
336 $ans->new_id = 0;
337 } else {
338 //Get the answer from backup_ids
339 if (!$ans = backup_getid($restore->backup_unique_code,"question_answers",$answer_id)) {
340 echo 'Could not recode randomsamatch answer '.$answer_id.'<br />';
343 if ($in_first) {
344 $answer_field .= $que->new_id."-".$ans->new_id;
345 $in_first = false;
346 } else {
347 $answer_field .= ",".$que->new_id."-".$ans->new_id;
349 //check for next
350 $tok = strtok(",");
352 return $answer_field;
357 //// END OF CLASS ////
359 //////////////////////////////////////////////////////////////////////////
360 //// INITIATION - Without this line the question type is not in use... ///
361 //////////////////////////////////////////////////////////////////////////
362 question_register_questiontype(new question_randomsamatch_qtype());