get_config(): fix a call using the oldstyle return value
[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',
00c30069 114 'question', $question->id)) {
115 notify('Error: Missing subquestions!');
116 return false;
516cf3eb 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
00c30069 129 ->answers[$subquestion->code] = clone($answer);
516cf3eb 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) {
87ee4968 181 $subquestions = &$state->options->subquestions;
182
183 // Prepare an array to help when disambiguating equal answers.
184 $answertexts = array();
185 foreach ($subquestions as $subquestion) {
186 $ans = reset($subquestion->options->answers);
187 $answertexts[$ans->id] = $ans->answer;
188 }
189
516cf3eb 190 // Serialize responses
191 $responses = array();
87ee4968 192 foreach ($subquestions as $key => $subquestion) {
7d6af8ca 193 $response = 0;
0c24ee0f 194 if ($subquestion->questiontext) {
87ee4968 195 if ($state->responses[$key]) {
196 $response = $state->responses[$key];
197 if (!array_key_exists($response, $subquestion->options->answers)) {
198 // If studen's answer did not match by id, but there may be
199 // two answers with the same text, but different ids,
200 // so we need to try matching the answer text.
201 $expected_answer = reset($subquestion->options->answers);
202 if ($answertexts[$response] == $expected_answer->answer) {
203 $response = $expected_answer->id;
204 $state->responses[$key] = $response;
205 }
206 }
87ee4968 207 }
0c24ee0f 208 }
7d6af8ca 209 $responses[] = $key.'-'.$response;
516cf3eb 210 }
211 $responses = implode(',', $responses);
212
213 // Set the legacy answer field
0c24ee0f 214 if (!set_field('question_states', 'answer', $responses, 'id', $state->id)) {
516cf3eb 215 return false;
216 }
217 return true;
218 }
219
220 function get_correct_responses(&$question, &$state) {
221 $responses = array();
222 foreach ($state->options->subquestions as $sub) {
223 foreach ($sub->options->answers as $answer) {
a58ffe3f 224 if (1 == $answer->fraction && $sub->questiontext) {
516cf3eb 225 $responses[$sub->id] = $answer->id;
226 }
227 }
228 }
229 return empty($responses) ? null : $responses;
230 }
231
232 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
37a12367 233 global $CFG;
516cf3eb 234 $subquestions = $state->options->subquestions;
235 $correctanswers = $this->get_correct_responses($question, $state);
236 $nameprefix = $question->name_prefix;
237 $answers = array();
0b4ce29d 238 $allanswers = array();
87ee4968 239 $answerids = array();
516cf3eb 240 $responses = &$state->responses;
241
0b4ce29d 242 // Prepare a list of answers, removing duplicates.
516cf3eb 243 foreach ($subquestions as $subquestion) {
244 foreach ($subquestion->options->answers as $ans) {
0b4ce29d 245 $allanswers[$ans->id] = $ans->answer;
87ee4968 246 if (!in_array($ans->answer, $answers)) {
247 $answers[$ans->id] = $ans->answer;
248 $answerids[$ans->answer] = $ans->id;
87ee4968 249 }
516cf3eb 250 }
251 }
0b4ce29d 252
253 // Fix up the ids of any responses that point the the eliminated duplicates.
254 foreach ($responses as $subquestionid => $ignored) {
255 if ($responses[$subquestionid]) {
256 $responses[$subquestionid] = $answerids[$allanswers[$responses[$subquestionid]]];
257 }
258 }
259 foreach ($correctanswers as $subquestionid => $ignored) {
260 $correctanswers[$subquestionid] = $answerids[$allanswers[$correctanswers[$subquestionid]]];
261 }
516cf3eb 262
263 // Shuffle the answers
264 $answers = draw_rand_array($answers, count($answers));
265
37a12367 266 // Print formulation
1b8a7434 267 $questiontext = $this->format_text($question->questiontext,
268 $question->questiontextformat, $cmoptions);
37a12367 269 $image = get_question_image($question, $cmoptions->course);
516cf3eb 270
271 ///// Print the input controls //////
516cf3eb 272 foreach ($subquestions as $key => $subquestion) {
a58ffe3f 273 if ($subquestion->questiontext) {
274 /// Subquestion text:
275 $a = new stdClass;
1b8a7434 276 $a->text = $this->format_text($subquestion->questiontext,
277 $question->questiontextformat, $cmoptions);
2b087056 278
a58ffe3f 279 /// Drop-down list:
280 $menuname = $nameprefix.$subquestion->id;
281 $response = isset($state->responses[$subquestion->id])
282 ? $state->responses[$subquestion->id] : '0';
2b087056 283
284 $a->class = ' ';
285 $a->feedbackimg = ' ';
286
a58ffe3f 287 if ($options->readonly
2b087056 288 and $options->correct_responses
289 and isset($correctanswers[$subquestion->id])
290 and ($correctanswers[$subquestion->id] == $response)) {
291
292 $correctresponse = 1;
a58ffe3f 293 } else {
2b087056 294 $correctresponse = 0;
295 }
296
297 if ($response) {
298 $a->class = question_get_feedback_class($correctresponse);
299 $a->feedbackimg = question_get_feedback_image($correctresponse);
300 }
301
302 $a->control = choose_from_menu($answers, $menuname, $response, 'choose',
303 '', 0, true, $options->readonly);
a58ffe3f 304
305 // Neither the editing interface or the database allow to provide
306 // fedback for this question type.
307 // However (as was pointed out in bug bug 3294) the randomsamatch
308 // type which reuses this method can have feedback defined for
309 // the wrapped shortanswer questions.
310 //if ($options->feedback
311 // && !empty($subquestion->options->answers[$responses[$key]]->feedback)) {
312 // print_comment($subquestion->options->answers[$responses[$key]]->feedback);
313 //}
2b087056 314
a58ffe3f 315 $anss[] = $a;
516cf3eb 316 }
516cf3eb 317 }
aaae75b0 318 include("$CFG->dirroot/question/type/match/display.html");
516cf3eb 319 }
320
321 function grade_responses(&$question, &$state, $cmoptions) {
322 $subquestions = &$state->options->subquestions;
323 $responses = &$state->responses;
324
87ee4968 325 // Prepare an array to help when disambiguating equal answers.
326 $answertexts = array();
327 foreach ($subquestions as $subquestion) {
328 $ans = reset($subquestion->options->answers);
329 $answertexts[$ans->id] = $ans->answer;
330 }
331
332 // Add up the grades from each subquestion.
516cf3eb 333 $sumgrade = 0;
a58ffe3f 334 $totalgrade = 0;
516cf3eb 335 foreach ($subquestions as $key => $sub) {
a58ffe3f 336 if ($sub->questiontext) {
337 $totalgrade += 1;
87ee4968 338 $response = $responses[$key];
339 if ($response && !array_key_exists($response, $sub->options->answers)) {
340 // If studen's answer did not match by id, but there may be
341 // two answers with the same text, but different ids,
342 // so we need to try matching the answer text.
343 $expected_answer = reset($sub->options->answers);
344 if ($answertexts[$response] == $expected_answer->answer) {
345 $response = $expected_answer->id;
346 }
347 }
348 if (array_key_exists($response, $sub->options->answers)) {
349 $sumgrade += $sub->options->answers[$response]->fraction;
a58ffe3f 350 }
516cf3eb 351 }
352 }
353
a58ffe3f 354 $state->raw_grade = $sumgrade/$totalgrade;
516cf3eb 355 if (empty($state->raw_grade)) {
356 $state->raw_grade = 0;
357 }
358
359 // Make sure we don't assign negative or too high marks
360 $state->raw_grade = min(max((float) $state->raw_grade,
361 0.0), 1.0) * $question->maxgrade;
362 $state->penalty = $question->penalty * $question->maxgrade;
363
f30bbcaf 364 // mark the state as graded
365 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
366
516cf3eb 367 return true;
368 }
369
370 // ULPGC ecastro for stats report
371 function get_all_responses($question, $state) {
974383f9 372 $answers = array();
516cf3eb 373 if (is_array($question->options->subquestions)) {
645d7610 374 foreach ($question->options->subquestions as $aid => $answer) {
375 if ($answer->questiontext) {
376 $r = new stdClass;
377 $r->answer = $answer->questiontext . ": " . $answer->answertext;
378 $r->credit = 1;
379 $answers[$aid] = $r;
380 }
516cf3eb 381 }
516cf3eb 382 }
a58ffe3f 383 $result = new stdClass;
516cf3eb 384 $result->id = $question->id;
385 $result->responses = $answers;
386 return $result;
387 }
388
389 // ULPGC ecastro
390 function get_actual_response($question, $state) {
01bd54e0 391 $subquestions = &$state->options->subquestions;
392 $responses = &$state->responses;
393 $results=array();
394 foreach ($subquestions as $key => $sub) {
395 foreach ($responses as $ind => $code) {
396 if (isset($sub->options->answers[$code])) {
645d7610 397 $results[$ind] = $subquestions[$ind]->questiontext . ": " . $sub->options->answers[$code]->answer;
01bd54e0 398 }
399 }
400 }
401 return $results;
402 }
0a5b58af 403
404 function response_summary($question, $state, $length=80) {
405 // This should almost certainly be overridden
755bddf1 406 return substr(implode(', ', $this->get_actual_response($question, $state)), 0, $length);
0a5b58af 407 }
c5d94c41 408
409/// BACKUP FUNCTIONS ////////////////////////////
410
411 /*
412 * Backup the data in the question
413 *
414 * This is used in question/backuplib.php
415 */
416 function backup($bf,$preferences,$question,$level=6) {
417
418 $status = true;
419
420 $matchs = get_records("question_match_sub","question",$question,"id");
421 //If there are matchs
422 if ($matchs) {
423 $status = fwrite ($bf,start_tag("MATCHS",6,true));
424 //Iterate over each match
425 foreach ($matchs as $match) {
426 $status = fwrite ($bf,start_tag("MATCH",7,true));
427 //Print match contents
428 fwrite ($bf,full_tag("ID",8,false,$match->id));
429 fwrite ($bf,full_tag("CODE",8,false,$match->code));
430 fwrite ($bf,full_tag("QUESTIONTEXT",8,false,$match->questiontext));
431 fwrite ($bf,full_tag("ANSWERTEXT",8,false,$match->answertext));
432 $status = fwrite ($bf,end_tag("MATCH",7,true));
433 }
434 $status = fwrite ($bf,end_tag("MATCHS",6,true));
435 }
436 return $status;
437 }
516cf3eb 438
315559d3 439/// RESTORE FUNCTIONS /////////////////
440
441 /*
442 * Restores the data in the question
443 *
444 * This is used in question/restorelib.php
445 */
446 function restore($old_question_id,$new_question_id,$info,$restore) {
447
448 $status = true;
449
450 //Get the matchs array
451 $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
452
453 //We have to build the subquestions field (a list of match_sub id)
454 $subquestions_field = "";
455 $in_first = true;
456
457 //Iterate over matchs
458 for($i = 0; $i < sizeof($matchs); $i++) {
459 $mat_info = $matchs[$i];
460
461 //We'll need this later!!
462 $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
463
464 //Now, build the question_match_SUB record structure
a58ffe3f 465 $match_sub = new stdClass;
315559d3 466 $match_sub->question = $new_question_id;
1f4d6e9a 467 $match_sub->code = isset($mat_info['#']['CODE']['0']['#'])?backup_todb($mat_info['#']['CODE']['0']['#']):'';
315559d3 468 if (!$match_sub->code) {
469 $match_sub->code = $oldid;
470 }
471 $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']);
472 $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']);
473
474 //The structure is equal to the db, so insert the question_match_sub
475 $newid = insert_record ("question_match_sub",$match_sub);
476
477 //Do some output
478 if (($i+1) % 50 == 0) {
479 if (!defined('RESTORE_SILENTLY')) {
480 echo ".";
481 if (($i+1) % 1000 == 0) {
482 echo "<br />";
483 }
484 }
485 backup_flush(300);
486 }
487
488 if ($newid) {
489 //We have the newid, update backup_ids
490 backup_putid($restore->backup_unique_code,"question_match_sub",$oldid,
491 $newid);
492 //We have a new match_sub, append it to subquestions_field
493 if ($in_first) {
494 $subquestions_field .= $newid;
495 $in_first = false;
496 } else {
497 $subquestions_field .= ",".$newid;
498 }
499 } else {
500 $status = false;
501 }
502 }
503
504 //We have created every match_sub, now create the match
87ee4968 505 $match = new stdClass;
315559d3 506 $match->question = $new_question_id;
507 $match->subquestions = $subquestions_field;
508
509 //The structure is equal to the db, so insert the question_match_sub
510 $newid = insert_record ("question_match",$match);
511
512 if (!$newid) {
513 $status = false;
514 }
515
516 return $status;
517 }
518
519 function restore_map($old_question_id,$new_question_id,$info,$restore) {
520
521 $status = true;
522
523 //Get the matchs array
524 $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
525
526 //We have to build the subquestions field (a list of match_sub id)
527 $subquestions_field = "";
528 $in_first = true;
529
530 //Iterate over matchs
531 for($i = 0; $i < sizeof($matchs); $i++) {
532 $mat_info = $matchs[$i];
533
534 //We'll need this later!!
535 $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
536
537 //Now, build the question_match_SUB record structure
538 $match_sub->question = $new_question_id;
539 $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']);
540 $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']);
541
542 //If we are in this method is because the question exists in DB, so its
543 //match_sub must exist too.
544 //Now, we are going to look for that match_sub in DB and to create the
545 //mappings in backup_ids to use them later where restoring states (user level).
546
547 //Get the match_sub from DB (by question, questiontext and answertext)
548 $db_match_sub = get_record ("question_match_sub","question",$new_question_id,
549 "questiontext",$match_sub->questiontext,
550 "answertext",$match_sub->answertext);
551 //Do some output
552 if (($i+1) % 50 == 0) {
553 if (!defined('RESTORE_SILENTLY')) {
554 echo ".";
555 if (($i+1) % 1000 == 0) {
556 echo "<br />";
557 }
558 }
559 backup_flush(300);
560 }
561
562 //We have the database match_sub, so update backup_ids
563 if ($db_match_sub) {
564 //We have the newid, update backup_ids
565 backup_putid($restore->backup_unique_code,"question_match_sub",$oldid,
566 $db_match_sub->id);
567 } else {
568 $status = false;
569 }
570 }
571
572 return $status;
573 }
574
575 function restore_recode_answer($state, $restore) {
576
577 //The answer is a comma separated list of hypen separated math_subs (for question and answer)
578 $answer_field = "";
579 $in_first = true;
580 $tok = strtok($state->answer,",");
581 while ($tok) {
582 //Extract the match_sub for the question and the answer
583 $exploded = explode("-",$tok);
584 $match_question_id = $exploded[0];
585 $match_answer_code = $exploded[1];
586 //Get the match_sub from backup_ids (for the question)
587 if (!$match_que = backup_getid($restore->backup_unique_code,"question_match_sub",$match_question_id)) {
588 echo 'Could not recode question_match_sub '.$match_question_id.'<br />';
589 }
590 if ($in_first) {
591 $answer_field .= $match_que->new_id."-".$match_answer_code;
592 $in_first = false;
593 } else {
594 $answer_field .= ",".$match_que->new_id."-".$match_answer_code;
595 }
596 //check for next
597 $tok = strtok(",");
598 }
599 return $answer_field;
600 }
601
516cf3eb 602}
603//// END OF CLASS ////
604
605//////////////////////////////////////////////////////////////////////////
606//// INITIATION - Without this line the question type is not in use... ///
607//////////////////////////////////////////////////////////////////////////
a2156789 608question_register_questiontype(new question_match_qtype());
516cf3eb 609?>