fix for mysqladmin links; merged from MOODLE_16_STABLE
[moodle.git] / question / type / match / questiontype.php
CommitLineData
516cf3eb 1<?php // $Id$
2
3/////////////
4/// MATCH ///
5/////////////
6
7/// QUESTION TYPE CLASS //////////////////
af3830ee 8class question_match_qtype extends default_questiontype {
516cf3eb 9
10 function name() {
11 return 'match';
12 }
13
14 function get_question_options(&$question) {
32a189d6 15 $question->options = get_record('question_match', 'question', $question->id);
16 $question->options->subquestions = get_records("question_match_sub", "question", $question->id, "id ASC" );
516cf3eb 17 return true;
18 }
19
20 function save_question_options($question) {
21
32a189d6 22 if (!$oldsubquestions = get_records("question_match_sub", "question", $question->id, "id ASC")) {
516cf3eb 23 $oldsubquestions = array();
24 }
25
26 // following hack to check at least three answers exist
27 $answercount = 0;
28 foreach ($question->subquestions as $key=>$questiontext) {
29 $answertext = $question->subanswers[$key];
30 if (!empty($questiontext) or !empty($answertext)) {
31 $answercount++;
32 }
33 }
34 $answercount += count($oldsubquestions);
35 if ($answercount < 3) { // check there are at lest 3 answers for matching type questions
36 $result->notice = get_string("notenoughanswers", "quiz", "3");
37 return $result;
38 }
39
40 // $subquestions will be an array with subquestion ids
41 $subquestions = array();
42
43 // Insert all the new question+answer pairs
44 foreach ($question->subquestions as $key => $questiontext) {
45 $answertext = $question->subanswers[$key];
46 if (!empty($questiontext) or !empty($answertext)) {
47 if ($subquestion = array_shift($oldsubquestions)) { // Existing answer, so reuse it
48 $subquestion->questiontext = $questiontext;
49 $subquestion->answertext = $answertext;
32a189d6 50 if (!update_record("question_match_sub", $subquestion)) {
7518b645 51 $result->error = "Could not insert match subquestion! (id=$subquestion->id)";
516cf3eb 52 return $result;
53 }
54 } else {
55 unset($subquestion);
56 // Determine a unique random code
57 $subquestion->code = rand(1,999999999);
18bd0d68 58 while (record_exists('question_match_sub', 'code', $subquestion->code, 'question', $question->id)) {
516cf3eb 59 $subquestion->code = rand();
60 }
61 $subquestion->question = $question->id;
62 $subquestion->questiontext = $questiontext;
63 $subquestion->answertext = $answertext;
32a189d6 64 if (!$subquestion->id = insert_record("question_match_sub", $subquestion)) {
7518b645 65 $result->error = "Could not insert match subquestion!";
516cf3eb 66 return $result;
67 }
68 }
69 $subquestions[] = $subquestion->id;
70 }
71 }
72
73 // delete old subquestions records
74 if (!empty($oldsubquestions)) {
75 foreach($oldsubquestions as $os) {
32a189d6 76 delete_records('question_match_sub', 'id', $os->id);
516cf3eb 77 }
78 }
79
80 if (count($subquestions) < 3) {
81 $result->noticeyesno = get_string("notenoughsubquestions", "quiz");
82 return $result;
83 }
84
32a189d6 85 if ($options = get_record("question_match", "question", $question->id)) {
516cf3eb 86 $options->subquestions = implode(",",$subquestions);
87 $options->shuffleanswers = $question->shuffleanswers;
32a189d6 88 if (!update_record("question_match", $options)) {
7518b645 89 $result->error = "Could not update match options! (id=$options->id)";
516cf3eb 90 return $result;
91 }
92 } else {
93 unset($options);
94 $options->question = $question->id;
95 $options->subquestions = implode(",",$subquestions);
96 $options->shuffleanswers = $question->shuffleanswers;
32a189d6 97 if (!insert_record("question_match", $options)) {
7518b645 98 $result->error = "Could not insert match options!";
516cf3eb 99 return $result;
100 }
101 }
102 return true;
103 }
104
105 /**
106 * Deletes question from the question-type specific tables
107 *
108 * @return boolean Success/Failure
109 * @param integer $question->id
110 */
90c3f310 111 function delete_question($questionid) {
112 delete_records("question_match", "question", $questionid);
113 delete_records("question_match_sub", "question", $questionid);
516cf3eb 114 return true;
115 }
116
117 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
32a189d6 118 if (!$state->options->subquestions = get_records('question_match_sub',
516cf3eb 119 'question', $question->id)) {
120 notify('Error: Missing subquestions!');
121 return false;
122 }
123
124 foreach ($state->options->subquestions as $key => $subquestion) {
125 // This seems rather over complicated, but it is useful for the
126 // randomsamatch questiontype, which can then inherit the print
127 // and grading functions. This way it is possible to define multiple
128 // answers per question, each with different marks and feedback.
129 $answer = new stdClass();
130 $answer->id = $subquestion->code;
131 $answer->answer = $subquestion->answertext;
132 $answer->fraction = 1.0;
133 $state->options->subquestions[$key]->options
134 ->answers[$subquestion->code] = clone($answer);
135
136 $state->responses[$key] = '';
137 }
138
139 // Shuffle the answers if required
140 if ($cmoptions->shuffleanswers and $question->options->shuffleanswers) {
141 $state->options->subquestions = swapshuffle_assoc($state->options->subquestions);
142 }
143
144 return true;
145 }
146
147 function restore_session_and_responses(&$question, &$state) {
148 // The serialized format for matching questions is a comma separated
149 // list of question answer pairs (e.g. 1-1,2-3,3-2), where the ids of
32a189d6 150 // both refer to the id in the table question_match_sub.
516cf3eb 151 $responses = explode(',', $state->responses['']);
152 $responses = array_map(create_function('$val',
153 'return explode("-", $val);'), $responses);
154
32a189d6 155 if (!$questions = get_records('question_match_sub',
516cf3eb 156 'question', $question->id)) {
157 notify('Error: Missing subquestions!');
158 return false;
159 }
160
161 // Restore the previous responses and place the questions into the state options
162 $state->responses = array();
163 $state->options->subquestions = array();
164 foreach ($responses as $response) {
165 $state->responses[$response[0]] = $response[1];
166 $state->options->subquestions[$response[0]] = $questions[$response[0]];
167 }
168
169 foreach ($state->options->subquestions as $key => $subquestion) {
170 // This seems rather over complicated, but it is useful for the
171 // randomsamatch questiontype, which can then inherit the print
172 // and grading functions. This way it is possible to define multiple
173 // answers per question, each with different marks and feedback.
174 $answer = new stdClass();
175 $answer->id = $subquestion->code;
176 $answer->answer = $subquestion->answertext;
177 $answer->fraction = 1.0;
178 $state->options->subquestions[$key]->options
179 ->answers[$subquestion->code] = clone($answer);
180 }
181
182 return true;
183 }
184
185 function save_session_and_responses(&$question, &$state) {
186 // Serialize responses
187 $responses = array();
188 foreach ($state->options->subquestions as $key => $subquestion) {
189 $responses[] = $key.'-'.($state->responses[$key] ? $state->responses[$key] : 0);
190 }
191 $responses = implode(',', $responses);
192
193 // Set the legacy answer field
4f48fb42 194 if (!set_field('question_states', 'answer', $responses, 'id',
516cf3eb 195 $state->id)) {
196 return false;
197 }
198 return true;
199 }
200
201 function get_correct_responses(&$question, &$state) {
202 $responses = array();
203 foreach ($state->options->subquestions as $sub) {
204 foreach ($sub->options->answers as $answer) {
205 if (1 == $answer->fraction) {
206 $responses[$sub->id] = $answer->id;
207 }
208 }
209 }
210 return empty($responses) ? null : $responses;
211 }
212
213 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
37a12367 214 global $CFG;
516cf3eb 215 $subquestions = $state->options->subquestions;
216 $correctanswers = $this->get_correct_responses($question, $state);
217 $nameprefix = $question->name_prefix;
218 $answers = array();
219 $responses = &$state->responses;
220
7347c60b 221 $formatoptions->noclean = true;
222 $formatoptions->para = false;
516cf3eb 223
224 foreach ($subquestions as $subquestion) {
225 foreach ($subquestion->options->answers as $ans) {
226 $answers[$ans->id] = $ans->answer;
227 }
228 }
229
230 // Shuffle the answers
231 $answers = draw_rand_array($answers, count($answers));
232
37a12367 233 // Print formulation
234 $questiontext = format_text($question->questiontext,
235 $question->questiontextformat,
7347c60b 236 $formatoptions, $cmoptions->course);
37a12367 237 $image = get_question_image($question, $cmoptions->course);
516cf3eb 238
239 ///// Print the input controls //////
516cf3eb 240 foreach ($subquestions as $key => $subquestion) {
37a12367 241
516cf3eb 242 /// Subquestion text:
37a12367 243 $a->text = format_text($subquestion->questiontext,
7347c60b 244 $question->questiontextformat, $formatoptions, $cmoptions->course);
516cf3eb 245
246 /// Drop-down list:
247 $menuname = $nameprefix.$subquestion->id;
248 $response = isset($state->responses[$subquestion->id])
249 ? $state->responses[$subquestion->id] : '0';
250 if ($options->readonly
251 and $options->correct_responses
252 and isset($correctanswers[$subquestion->id])
253 and ($correctanswers[$subquestion->id] == $response)) {
0a5b58af 254 $a->class = ' highlight ';
516cf3eb 255 } else {
37a12367 256 $a->class = '';
516cf3eb 257 }
37a12367 258
259 $a->control = choose_from_menu($answers, $menuname, $response, 'choose', '', 0,
260 true, $options->readonly);
516cf3eb 261
262 // Neither the editing interface or the database allow to provide
263 // fedback for this question type.
264 // However (as was pointed out in bug bug 3294) the randomsamatch
265 // type which reuses this method can have feedback defined for
266 // the wrapped shortanswer questions.
267 //if ($options->feedback
268 // && !empty($subquestion->options->answers[$responses[$key]]->feedback)) {
7518b645 269 // print_comment($subquestion->options->answers[$responses[$key]]->feedback);
516cf3eb 270 //}
37a12367 271
272 $anss[] = clone($a);
516cf3eb 273 }
aaae75b0 274 include("$CFG->dirroot/question/type/match/display.html");
516cf3eb 275 }
276
277 function grade_responses(&$question, &$state, $cmoptions) {
278 $subquestions = &$state->options->subquestions;
279 $responses = &$state->responses;
280
281 $sumgrade = 0;
282 foreach ($subquestions as $key => $sub) {
283 if (isset($sub->options->answers[$responses[$key]])) {
284 $sumgrade += $sub->options->answers[$responses[$key]]->fraction;
285 }
286 }
287
288 $state->raw_grade = $sumgrade/count($subquestions);
289 if (empty($state->raw_grade)) {
290 $state->raw_grade = 0;
291 }
292
293 // Make sure we don't assign negative or too high marks
294 $state->raw_grade = min(max((float) $state->raw_grade,
295 0.0), 1.0) * $question->maxgrade;
296 $state->penalty = $question->penalty * $question->maxgrade;
297
f30bbcaf 298 // mark the state as graded
299 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
300
516cf3eb 301 return true;
302 }
303
304 // ULPGC ecastro for stats report
305 function get_all_responses($question, $state) {
306 unset($answers);
307 if (is_array($question->options->subquestions)) {
308 foreach ($question->options->subquestions as $aid=>$answer) {
309 unset ($r);
310 $r->answer = $answer->questiontext." : ".$answer->answertext;
311 $r->credit = 1;
312 $answers[$aid] = $r;
313 }
314 } else {
315 $answers[]="error"; // just for debugging, eliminate
316 }
317 $result->id = $question->id;
318 $result->responses = $answers;
319 return $result;
320 }
321
322 // ULPGC ecastro
323 function get_actual_response($question, $state) {
01bd54e0 324 $subquestions = &$state->options->subquestions;
325 $responses = &$state->responses;
326 $results=array();
327 foreach ($subquestions as $key => $sub) {
328 foreach ($responses as $ind => $code) {
329 if (isset($sub->options->answers[$code])) {
755bddf1 330 $results[$ind] = $subquestions[$ind]->questiontext . ":" . $sub->options->answers[$code]->answer;
01bd54e0 331 }
332 }
333 }
334 return $results;
335 }
0a5b58af 336
337 function response_summary($question, $state, $length=80) {
338 // This should almost certainly be overridden
755bddf1 339 return substr(implode(', ', $this->get_actual_response($question, $state)), 0, $length);
0a5b58af 340 }
c5d94c41 341
342/// BACKUP FUNCTIONS ////////////////////////////
343
344 /*
345 * Backup the data in the question
346 *
347 * This is used in question/backuplib.php
348 */
349 function backup($bf,$preferences,$question,$level=6) {
350
351 $status = true;
352
353 $matchs = get_records("question_match_sub","question",$question,"id");
354 //If there are matchs
355 if ($matchs) {
356 $status = fwrite ($bf,start_tag("MATCHS",6,true));
357 //Iterate over each match
358 foreach ($matchs as $match) {
359 $status = fwrite ($bf,start_tag("MATCH",7,true));
360 //Print match contents
361 fwrite ($bf,full_tag("ID",8,false,$match->id));
362 fwrite ($bf,full_tag("CODE",8,false,$match->code));
363 fwrite ($bf,full_tag("QUESTIONTEXT",8,false,$match->questiontext));
364 fwrite ($bf,full_tag("ANSWERTEXT",8,false,$match->answertext));
365 $status = fwrite ($bf,end_tag("MATCH",7,true));
366 }
367 $status = fwrite ($bf,end_tag("MATCHS",6,true));
368 }
369 return $status;
370 }
516cf3eb 371
315559d3 372/// RESTORE FUNCTIONS /////////////////
373
374 /*
375 * Restores the data in the question
376 *
377 * This is used in question/restorelib.php
378 */
379 function restore($old_question_id,$new_question_id,$info,$restore) {
380
381 $status = true;
382
383 //Get the matchs array
384 $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
385
386 //We have to build the subquestions field (a list of match_sub id)
387 $subquestions_field = "";
388 $in_first = true;
389
390 //Iterate over matchs
391 for($i = 0; $i < sizeof($matchs); $i++) {
392 $mat_info = $matchs[$i];
393
394 //We'll need this later!!
395 $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
396
397 //Now, build the question_match_SUB record structure
398 $match_sub->question = $new_question_id;
399 $match_sub->code = backup_todb($mat_info['#']['CODE']['0']['#']);
400 if (!$match_sub->code) {
401 $match_sub->code = $oldid;
402 }
403 $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']);
404 $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']);
405
406 //The structure is equal to the db, so insert the question_match_sub
407 $newid = insert_record ("question_match_sub",$match_sub);
408
409 //Do some output
410 if (($i+1) % 50 == 0) {
411 if (!defined('RESTORE_SILENTLY')) {
412 echo ".";
413 if (($i+1) % 1000 == 0) {
414 echo "<br />";
415 }
416 }
417 backup_flush(300);
418 }
419
420 if ($newid) {
421 //We have the newid, update backup_ids
422 backup_putid($restore->backup_unique_code,"question_match_sub",$oldid,
423 $newid);
424 //We have a new match_sub, append it to subquestions_field
425 if ($in_first) {
426 $subquestions_field .= $newid;
427 $in_first = false;
428 } else {
429 $subquestions_field .= ",".$newid;
430 }
431 } else {
432 $status = false;
433 }
434 }
435
436 //We have created every match_sub, now create the match
437 $match->question = $new_question_id;
438 $match->subquestions = $subquestions_field;
439
440 //The structure is equal to the db, so insert the question_match_sub
441 $newid = insert_record ("question_match",$match);
442
443 if (!$newid) {
444 $status = false;
445 }
446
447 return $status;
448 }
449
450 function restore_map($old_question_id,$new_question_id,$info,$restore) {
451
452 $status = true;
453
454 //Get the matchs array
455 $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
456
457 //We have to build the subquestions field (a list of match_sub id)
458 $subquestions_field = "";
459 $in_first = true;
460
461 //Iterate over matchs
462 for($i = 0; $i < sizeof($matchs); $i++) {
463 $mat_info = $matchs[$i];
464
465 //We'll need this later!!
466 $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
467
468 //Now, build the question_match_SUB record structure
469 $match_sub->question = $new_question_id;
470 $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']);
471 $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']);
472
473 //If we are in this method is because the question exists in DB, so its
474 //match_sub must exist too.
475 //Now, we are going to look for that match_sub in DB and to create the
476 //mappings in backup_ids to use them later where restoring states (user level).
477
478 //Get the match_sub from DB (by question, questiontext and answertext)
479 $db_match_sub = get_record ("question_match_sub","question",$new_question_id,
480 "questiontext",$match_sub->questiontext,
481 "answertext",$match_sub->answertext);
482 //Do some output
483 if (($i+1) % 50 == 0) {
484 if (!defined('RESTORE_SILENTLY')) {
485 echo ".";
486 if (($i+1) % 1000 == 0) {
487 echo "<br />";
488 }
489 }
490 backup_flush(300);
491 }
492
493 //We have the database match_sub, so update backup_ids
494 if ($db_match_sub) {
495 //We have the newid, update backup_ids
496 backup_putid($restore->backup_unique_code,"question_match_sub",$oldid,
497 $db_match_sub->id);
498 } else {
499 $status = false;
500 }
501 }
502
503 return $status;
504 }
505
506 function restore_recode_answer($state, $restore) {
507
508 //The answer is a comma separated list of hypen separated math_subs (for question and answer)
509 $answer_field = "";
510 $in_first = true;
511 $tok = strtok($state->answer,",");
512 while ($tok) {
513 //Extract the match_sub for the question and the answer
514 $exploded = explode("-",$tok);
515 $match_question_id = $exploded[0];
516 $match_answer_code = $exploded[1];
517 //Get the match_sub from backup_ids (for the question)
518 if (!$match_que = backup_getid($restore->backup_unique_code,"question_match_sub",$match_question_id)) {
519 echo 'Could not recode question_match_sub '.$match_question_id.'<br />';
520 }
521 if ($in_first) {
522 $answer_field .= $match_que->new_id."-".$match_answer_code;
523 $in_first = false;
524 } else {
525 $answer_field .= ",".$match_que->new_id."-".$match_answer_code;
526 }
527 //check for next
528 $tok = strtok(",");
529 }
530 return $answer_field;
531 }
532
516cf3eb 533}
534//// END OF CLASS ////
535
536//////////////////////////////////////////////////////////////////////////
537//// INITIATION - Without this line the question type is not in use... ///
538//////////////////////////////////////////////////////////////////////////
dfa47f96 539$QTYPES['match']= new question_match_qtype();
ccccf04f 540// The following adds the questiontype to the menu of types shown to teachers
dfa47f96 541$QTYPE_MENU['match'] = get_string("match", "quiz");
516cf3eb 542
543?>