Automatic installer.php lang files by installer_builder (20070726)
[moodle-linuxchix.git] / question / type / match / questiontype.php
blob352c3c5fada0811167caf2c690dfc3b7e5d1011e
1 <?php // $Id$
3 /////////////
4 /// MATCH ///
5 /////////////
7 /// QUESTION TYPE CLASS //////////////////
8 /**
9 * @package questionbank
10 * @subpackage questiontypes
12 class question_match_qtype extends default_questiontype {
14 function name() {
15 return 'match';
18 function get_question_options(&$question) {
19 $question->options = get_record('question_match', 'question', $question->id);
20 $question->options->subquestions = get_records('question_match_sub', 'question', $question->id, 'id ASC');
21 return true;
24 function save_question_options($question) {
25 $result = new stdClass;
27 if (!$oldsubquestions = get_records("question_match_sub", "question", $question->id, "id ASC")) {
28 $oldsubquestions = array();
31 // $subquestions will be an array with subquestion ids
32 $subquestions = array();
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;
41 if (!update_record("question_match_sub", $subquestion)) {
42 $result->error = "Could not insert match subquestion! (id=$subquestion->id)";
43 return $result;
45 } else {
46 $subquestion = new stdClass;
47 // Determine a unique random code
48 $subquestion->code = rand(1,999999999);
49 while (record_exists('question_match_sub', 'code', $subquestion->code, 'question', $question->id)) {
50 $subquestion->code = rand();
52 $subquestion->question = $question->id;
53 $subquestion->questiontext = $questiontext;
54 $subquestion->answertext = $answertext;
55 if (!$subquestion->id = insert_record("question_match_sub", $subquestion)) {
56 $result->error = "Could not insert match subquestion!";
57 return $result;
60 $subquestions[] = $subquestion->id;
62 if (!empty($questiontext) && empty($answertext)) {
63 $result->notice = get_string('nomatchinganswer', 'quiz', $questiontext);
67 // delete old subquestions records
68 if (!empty($oldsubquestions)) {
69 foreach($oldsubquestions as $os) {
70 delete_records('question_match_sub', 'id', $os->id);
74 if ($options = get_record("question_match", "question", $question->id)) {
75 $options->subquestions = implode(",",$subquestions);
76 $options->shuffleanswers = $question->shuffleanswers;
77 if (!update_record("question_match", $options)) {
78 $result->error = "Could not update match options! (id=$options->id)";
79 return $result;
81 } else {
82 unset($options);
83 $options->question = $question->id;
84 $options->subquestions = implode(",",$subquestions);
85 $options->shuffleanswers = $question->shuffleanswers;
86 if (!insert_record("question_match", $options)) {
87 $result->error = "Could not insert match options!";
88 return $result;
92 if (!empty($result->notice)) {
93 return $result;
96 if (count($subquestions) < 3) {
97 $result->notice = get_string('notenoughanswers', 'quiz', 3);
98 return $result;
101 return true;
105 * Deletes question from the question-type specific tables
107 * @return boolean Success/Failure
108 * @param integer $question->id
110 function delete_question($questionid) {
111 delete_records("question_match", "question", $questionid);
112 delete_records("question_match_sub", "question", $questionid);
113 return true;
116 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
117 if (!$state->options->subquestions = get_records('question_match_sub', 'question', $question->id, 'id ASC')) {
118 notify('Error: Missing subquestions!');
119 return false;
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
132 ->answers[$subquestion->code] = clone($answer);
134 $state->responses[$key] = '';
137 // Shuffle the answers if required
138 if ($cmoptions->shuffleanswers and $question->options->shuffleanswers) {
139 $state->options->subquestions = swapshuffle_assoc($state->options->subquestions);
142 return true;
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
148 // both refer to the id in the table question_match_sub.
149 $responses = explode(',', $state->responses['']);
150 $responses = array_map(create_function('$val',
151 'return explode("-", $val);'), $responses);
153 if (!$questions = get_records('question_match_sub', 'question', $question->id, 'id ASC')) {
154 notify('Error: Missing subquestions!');
155 return false;
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]];
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);
179 return true;
182 function save_session_and_responses(&$question, &$state) {
183 $subquestions = &$state->options->subquestions;
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;
192 // Serialize responses
193 $responses = array();
194 foreach ($subquestions as $key => $subquestion) {
195 $response = 0;
196 if ($subquestion->questiontext) {
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;
211 $responses[] = $key.'-'.$response;
213 $responses = implode(',', $responses);
215 // Set the legacy answer field
216 if (!set_field('question_states', 'answer', $responses, 'id', $state->id)) {
217 return false;
219 return true;
222 function get_correct_responses(&$question, &$state) {
223 $responses = array();
224 foreach ($state->options->subquestions as $sub) {
225 foreach ($sub->options->answers as $answer) {
226 if (1 == $answer->fraction && $sub->questiontext) {
227 $responses[$sub->id] = $answer->id;
231 return empty($responses) ? null : $responses;
234 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
235 global $CFG;
236 $subquestions = $state->options->subquestions;
237 $correctanswers = $this->get_correct_responses($question, $state);
238 $nameprefix = $question->name_prefix;
239 $answers = array();
240 $allanswers = array();
241 $answerids = array();
242 $responses = &$state->responses;
244 // Prepare a list of answers, removing duplicates.
245 foreach ($subquestions as $subquestion) {
246 foreach ($subquestion->options->answers as $ans) {
247 $allanswers[$ans->id] = $ans->answer;
248 if (!in_array($ans->answer, $answers)) {
249 $answers[$ans->id] = $ans->answer;
250 $answerids[$ans->answer] = $ans->id;
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]]];
261 foreach ($correctanswers as $subquestionid => $ignored) {
262 $correctanswers[$subquestionid] = $answerids[$allanswers[$correctanswers[$subquestionid]]];
265 // Shuffle the answers
266 $answers = draw_rand_array($answers, count($answers));
268 // Print formulation
269 $questiontext = $this->format_text($question->questiontext,
270 $question->questiontextformat, $cmoptions);
271 $image = get_question_image($question, $cmoptions->course);
273 // Print the input controls
274 foreach ($subquestions as $key => $subquestion) {
275 if ($subquestion->questiontext) {
276 // Subquestion text:
277 $a = new stdClass;
278 $a->text = $this->format_text($subquestion->questiontext,
279 $question->questiontextformat, $cmoptions);
281 // Drop-down list:
282 $menuname = $nameprefix.$subquestion->id;
283 $response = isset($state->responses[$subquestion->id])
284 ? $state->responses[$subquestion->id] : '0';
286 $a->class = ' ';
287 $a->feedbackimg = ' ';
289 if ($options->readonly and $options->correct_responses) {
290 if (isset($correctanswers[$subquestion->id])
291 and ($correctanswers[$subquestion->id] == $response)) {
292 $correctresponse = 1;
293 } else {
294 $correctresponse = 0;
297 if ($options->feedback && $response) {
298 $a->class = question_get_feedback_class($correctresponse);
299 $a->feedbackimg = question_get_feedback_image($correctresponse);
303 $a->control = choose_from_menu($answers, $menuname, $response, 'choose',
304 '', 0, true, $options->readonly);
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);
316 $anss[] = $a;
319 include("$CFG->dirroot/question/type/match/display.html");
322 function grade_responses(&$question, &$state, $cmoptions) {
323 $subquestions = &$state->options->subquestions;
324 $responses = &$state->responses;
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;
333 // Add up the grades from each subquestion.
334 $sumgrade = 0;
335 $totalgrade = 0;
336 foreach ($subquestions as $key => $sub) {
337 if ($sub->questiontext) {
338 $totalgrade += 1;
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;
349 if (array_key_exists($response, $sub->options->answers)) {
350 $sumgrade += $sub->options->answers[$response]->fraction;
355 $state->raw_grade = $sumgrade/$totalgrade;
356 if (empty($state->raw_grade)) {
357 $state->raw_grade = 0;
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;
365 // mark the state as graded
366 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
368 return true;
371 function compare_responses($question, $state, $teststate) {
372 foreach ($state->responses as $i=>$sr) {
373 if (empty($teststate->responses[$i])) {
374 if (!empty($state->responses[$i])) {
375 return false;
377 } else if ($state->responses[$i] != $teststate->responses[$i]) {
378 return false;
381 return true;
384 // ULPGC ecastro for stats report
385 function get_all_responses($question, $state) {
386 $answers = array();
387 if (is_array($question->options->subquestions)) {
388 foreach ($question->options->subquestions as $aid => $answer) {
389 if ($answer->questiontext) {
390 $r = new stdClass;
391 $r->answer = $answer->questiontext . ": " . $answer->answertext;
392 $r->credit = 1;
393 $answers[$aid] = $r;
397 $result = new stdClass;
398 $result->id = $question->id;
399 $result->responses = $answers;
400 return $result;
403 // ULPGC ecastro
404 function get_actual_response($question, $state) {
405 $subquestions = &$state->options->subquestions;
406 $responses = &$state->responses;
407 $results=array();
408 foreach ($subquestions as $key => $sub) {
409 foreach ($responses as $ind => $code) {
410 if (isset($sub->options->answers[$code])) {
411 $results[$ind] = $subquestions[$ind]->questiontext . ": " . $sub->options->answers[$code]->answer;
415 return $results;
418 function response_summary($question, $state, $length=80) {
419 // This should almost certainly be overridden
420 return substr(implode(', ', $this->get_actual_response($question, $state)), 0, $length);
423 /// BACKUP FUNCTIONS ////////////////////////////
426 * Backup the data in the question
428 * This is used in question/backuplib.php
430 function backup($bf,$preferences,$question,$level=6) {
432 $status = true;
434 $matchs = get_records('question_match_sub', 'question', $question, 'id ASC');
435 //If there are matchs
436 if ($matchs) {
437 $status = fwrite ($bf,start_tag("MATCHS",6,true));
438 //Iterate over each match
439 foreach ($matchs as $match) {
440 $status = fwrite ($bf,start_tag("MATCH",7,true));
441 //Print match contents
442 fwrite ($bf,full_tag("ID",8,false,$match->id));
443 fwrite ($bf,full_tag("CODE",8,false,$match->code));
444 fwrite ($bf,full_tag("QUESTIONTEXT",8,false,$match->questiontext));
445 fwrite ($bf,full_tag("ANSWERTEXT",8,false,$match->answertext));
446 $status = fwrite ($bf,end_tag("MATCH",7,true));
448 $status = fwrite ($bf,end_tag("MATCHS",6,true));
450 return $status;
453 /// RESTORE FUNCTIONS /////////////////
456 * Restores the data in the question
458 * This is used in question/restorelib.php
460 function restore($old_question_id,$new_question_id,$info,$restore) {
462 $status = true;
464 //Get the matchs array
465 $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
467 //We have to build the subquestions field (a list of match_sub id)
468 $subquestions_field = "";
469 $in_first = true;
471 //Iterate over matchs
472 for($i = 0; $i < sizeof($matchs); $i++) {
473 $mat_info = $matchs[$i];
475 //We'll need this later!!
476 $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
478 //Now, build the question_match_SUB record structure
479 $match_sub = new stdClass;
480 $match_sub->question = $new_question_id;
481 $match_sub->code = isset($mat_info['#']['CODE']['0']['#'])?backup_todb($mat_info['#']['CODE']['0']['#']):'';
482 if (!$match_sub->code) {
483 $match_sub->code = $oldid;
485 $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']);
486 $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']);
488 //The structure is equal to the db, so insert the question_match_sub
489 $newid = insert_record ("question_match_sub",$match_sub);
491 //Do some output
492 if (($i+1) % 50 == 0) {
493 if (!defined('RESTORE_SILENTLY')) {
494 echo ".";
495 if (($i+1) % 1000 == 0) {
496 echo "<br />";
499 backup_flush(300);
502 if ($newid) {
503 //We have the newid, update backup_ids
504 backup_putid($restore->backup_unique_code,"question_match_sub",$oldid,
505 $newid);
506 //We have a new match_sub, append it to subquestions_field
507 if ($in_first) {
508 $subquestions_field .= $newid;
509 $in_first = false;
510 } else {
511 $subquestions_field .= ",".$newid;
513 } else {
514 $status = false;
518 //We have created every match_sub, now create the match
519 $match = new stdClass;
520 $match->question = $new_question_id;
521 $match->subquestions = $subquestions_field;
523 //The structure is equal to the db, so insert the question_match_sub
524 $newid = insert_record ("question_match",$match);
526 if (!$newid) {
527 $status = false;
530 return $status;
533 function restore_map($old_question_id,$new_question_id,$info,$restore) {
535 $status = true;
537 //Get the matchs array
538 $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
540 //We have to build the subquestions field (a list of match_sub id)
541 $subquestions_field = "";
542 $in_first = true;
544 //Iterate over matchs
545 for($i = 0; $i < sizeof($matchs); $i++) {
546 $mat_info = $matchs[$i];
548 //We'll need this later!!
549 $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
551 //Now, build the question_match_SUB record structure
552 $match_sub->question = $new_question_id;
553 $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']);
554 $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']);
556 //If we are in this method is because the question exists in DB, so its
557 //match_sub must exist too.
558 //Now, we are going to look for that match_sub in DB and to create the
559 //mappings in backup_ids to use them later where restoring states (user level).
561 //Get the match_sub from DB (by question, questiontext and answertext)
562 $db_match_sub = get_record ("question_match_sub","question",$new_question_id,
563 "questiontext",$match_sub->questiontext,
564 "answertext",$match_sub->answertext);
565 //Do some output
566 if (($i+1) % 50 == 0) {
567 if (!defined('RESTORE_SILENTLY')) {
568 echo ".";
569 if (($i+1) % 1000 == 0) {
570 echo "<br />";
573 backup_flush(300);
576 //We have the database match_sub, so update backup_ids
577 if ($db_match_sub) {
578 //We have the newid, update backup_ids
579 backup_putid($restore->backup_unique_code,"question_match_sub",$oldid,
580 $db_match_sub->id);
581 } else {
582 $status = false;
586 return $status;
589 function restore_recode_answer($state, $restore) {
591 //The answer is a comma separated list of hypen separated math_subs (for question and answer)
592 $answer_field = "";
593 $in_first = true;
594 $tok = strtok($state->answer,",");
595 while ($tok) {
596 //Extract the match_sub for the question and the answer
597 $exploded = explode("-",$tok);
598 $match_question_id = $exploded[0];
599 $match_answer_code = $exploded[1];
600 //Get the match_sub from backup_ids (for the question)
601 if (!$match_que = backup_getid($restore->backup_unique_code,"question_match_sub",$match_question_id)) {
602 echo 'Could not recode question_match_sub '.$match_question_id.'<br />';
604 if ($in_first) {
605 $answer_field .= $match_que->new_id."-".$match_answer_code;
606 $in_first = false;
607 } else {
608 $answer_field .= ",".$match_que->new_id."-".$match_answer_code;
610 //check for next
611 $tok = strtok(",");
613 return $answer_field;
617 * Decode links in question type specific tables.
618 * @return bool success or failure.
620 function decode_content_links_caller($questionids, $restore, &$i) {
621 $status = true;
623 // Decode links in the question_match_sub table.
624 if ($subquestions = get_records_list('question_match_sub', 'question',
625 implode(',', $questionids), '', 'id, questiontext')) {
627 foreach ($subquestions as $subquestion) {
628 $questiontext = restore_decode_content_links_worker($subquestion->questiontext, $restore);
629 if ($questiontext != $subquestion->questiontext) {
630 $subquestion->questiontext = addslashes($questiontext);
631 if (!update_record('question_match_sub', $subquestion)) {
632 $status = false;
636 // Do some output.
637 if (++$i % 5 == 0 && !defined('RESTORE_SILENTLY')) {
638 echo ".";
639 if ($i % 100 == 0) {
640 echo "<br />";
642 backup_flush(300);
647 return $status;
650 //// END OF CLASS ////
652 //////////////////////////////////////////////////////////////////////////
653 //// INITIATION - Without this line the question type is not in use... ///
654 //////////////////////////////////////////////////////////////////////////
655 question_register_questiontype(new question_match_qtype());