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