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