469e11a55ffd9b7c3cc45aaba87c471c5e065afb
[moodle.git] / question / type / match / questiontype.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /////////////
19 /// MATCH ///
20 /////////////
22 /// QUESTION TYPE CLASS //////////////////
23 /**
24  * @package questionbank
25  * @subpackage questiontypes
26  */
27 class question_match_qtype extends default_questiontype {
29     function name() {
30         return 'match';
31     }
33     function get_question_options(&$question) {
34         global $DB;
35         $question->options = $DB->get_record('question_match', array('question' => $question->id));
36         $question->options->subquestions = $DB->get_records('question_match_sub', array('question' => $question->id), 'id ASC');
37         return true;
38     }
40     function save_question_options($question) {
41         global $DB;
42         $context = $question->context;
43         $result = new stdClass;
45         if (!$oldsubquestions = $DB->get_records("question_match_sub", array("question" => $question->id), "id ASC")) {
46             $oldsubquestions = array();
47         }
49         // $subquestions will be an array with subquestion ids
50         $subquestions = array();
52         // Insert all the new question+answer pairs
53         foreach ($question->subquestions as $key => $questiontext) {
54             $itemid = $questiontext['itemid'];
55             $format = $questiontext['format'];
56             $questiontext = trim($questiontext['text']);
57             $answertext = trim($question->subanswers[$key]);
58             if ($questiontext != '' || $answertext != '') {
59                 if ($subquestion = array_shift($oldsubquestions)) {  // Existing answer, so reuse it
60                     $subquestion->answertext   = $answertext;
61                     $subquestion->questiontext = file_save_draft_area_files($itemid, $context->id, 'qtype_match', 'subquestion', $subquestion->id, self::$fileoptions, $questiontext);
62                     $subquestion->questiontextformat = $format;
63                     $DB->update_record("question_match_sub", $subquestion);
64                 } else {
65                     $subquestion = new stdClass;
66                     // Determine a unique random code
67                     $subquestion->code = rand(1, 999999999);
68                     while ($DB->record_exists('question_match_sub', array('code' => $subquestion->code, 'question' => $question->id))) {
69                         $subquestion->code = rand();
70                     }
71                     $subquestion->question = $question->id;
72                     $subquestion->questiontext = $questiontext;
73                     $subquestion->questiontextformat = $format;
74                     $subquestion->answertext = $answertext;
75                     $subquestion->id = $DB->insert_record("question_match_sub", $subquestion);
76                     $questiontext = file_save_draft_area_files($itemid, $context->id, 'qtype_match', 'subquestion', $subquestion->id, self::$fileoptions, $questiontext);
77                     $DB->set_field('question_match_sub', 'questiontext', $questiontext, array('id'=>$subquestion->id));
78                 }
79                 $subquestions[] = $subquestion->id;
80             }
81             if ($questiontext != '' && $answertext == '') {
82                 $result->notice = get_string('nomatchinganswer', 'quiz', $questiontext);
83             }
84         }
86         // delete old subquestions records
87         if (!empty($oldsubquestions)) {
88             foreach($oldsubquestions as $os) {
89                 $DB->delete_records('question_match_sub', array('id' => $os->id));
90             }
91         }
93         if ($options = $DB->get_record("question_match", array("question" => $question->id))) {
94             $options->subquestions = implode(",",$subquestions);
95             $options->shuffleanswers = $question->shuffleanswers;
96             $DB->update_record("question_match", $options);
97         } else {
98             unset($options);
99             $options->question = $question->id;
100             $options->subquestions = implode(",",$subquestions);
101             $options->shuffleanswers = $question->shuffleanswers;
102             $DB->insert_record("question_match", $options);
103         }
105         if (!empty($result->notice)) {
106             return $result;
107         }
109         if (count($subquestions) < 3) {
110             $result->notice = get_string('notenoughanswers', 'quiz', 3);
111             return $result;
112         }
114         return true;
115     }
117     /**
118     * Deletes question from the question-type specific tables
119     *
120     * @return boolean Success/Failure
121     * @param integer $question->id
122     */
123     function delete_question($questionid) {
124         global $DB;
125         $DB->delete_records("question_match", array("question" => $questionid));
126         $DB->delete_records("question_match_sub", array("question" => $questionid));
127         return true;
128     }
130     function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
131         global $DB, $OUTPUT;
132         if (!$state->options->subquestions = $DB->get_records('question_match_sub', array('question' => $question->id), 'id ASC')) {
133             echo $OUTPUT->notification('Error: Missing subquestions!');
134             return false;
135         }
137         foreach ($state->options->subquestions as $key => $subquestion) {
138             // This seems rather over complicated, but it is useful for the
139             // randomsamatch questiontype, which can then inherit the print
140             // and grading functions. This way it is possible to define multiple
141             // answers per question, each with different marks and feedback.
142             $answer = new stdClass();
143             $answer->id       = $subquestion->code;
144             $answer->answer   = $subquestion->answertext;
145             $answer->fraction = 1.0;
146             $state->options->subquestions[$key]->options->answers[$subquestion->code] = clone($answer);
148             $state->responses[$key] = '';
149         }
151         // Shuffle the answers if required
152         if ($cmoptions->shuffleanswers and $question->options->shuffleanswers) {
153            $state->options->subquestions = swapshuffle_assoc($state->options->subquestions);
154         }
156         return true;
157     }
159     function restore_session_and_responses(&$question, &$state) {
160         global $DB, $OUTPUT;
161         static $subquestions = array();
162         if (!isset($subquestions[$question->id])){
163             if (!$subquestions[$question->id] = $DB->get_records('question_match_sub', array('question' => $question->id), 'id ASC')) {
164                echo $OUTPUT->notification('Error: Missing subquestions!');
165                return false;
166             }
167         }
169         // The serialized format for matching questions is a comma separated
170         // list of question answer pairs (e.g. 1-1,2-3,3-2), where the ids of
171         // both refer to the id in the table question_match_sub.
172         $responses = explode(',', $state->responses['']);
173         $responses = array_map(create_function('$val', 'return explode("-", $val);'), $responses);
175         // Restore the previous responses and place the questions into the state options
176         $state->responses = array();
177         $state->options->subquestions = array();
178         foreach ($responses as $response) {
179             $state->responses[$response[0]] = $response[1];
180             $state->options->subquestions[$response[0]] = clone($subquestions[$question->id][$response[0]]);
181         }
183         foreach ($state->options->subquestions as $key => $subquestion) {
184             // This seems rather over complicated, but it is useful for the
185             // randomsamatch questiontype, which can then inherit the print
186             // and grading functions. This way it is possible to define multiple
187             // answers per question, each with different marks and feedback.
188             $answer = new stdClass();
189             $answer->id       = $subquestion->code;
190             $answer->answer   = $subquestion->answertext;
191             $answer->fraction = 1.0;
192             $state->options->subquestions[$key]->options->answers[$subquestion->code] = clone($answer);
193         }
195         return true;
196     }
198     function save_session_and_responses(&$question, &$state) {
199         global $DB;
200          $subquestions = &$state->options->subquestions;
202         // Prepare an array to help when disambiguating equal answers.
203         $answertexts = array();
204         foreach ($subquestions as $subquestion) {
205             $ans = reset($subquestion->options->answers);
206             $answertexts[$ans->id] = $ans->answer;
207         }
209         // Serialize responses
210         $responses = array();
211         foreach ($subquestions as $key => $subquestion) {
212             $response = 0;
213             if ($subquestion->questiontext !== '' && !is_null($subquestion->questiontext)) {
214                 if ($state->responses[$key]) {
215                     $response = $state->responses[$key];
216                     if (!array_key_exists($response, $subquestion->options->answers)) {
217                         // If student's answer did not match by id, but there may be
218                         // two answers with the same text, but different ids,
219                         // so we need to try matching the answer text.
220                         $expected_answer = reset($subquestion->options->answers);
221                         if ($answertexts[$response] == $expected_answer->answer) {
222                             $response = $expected_answer->id;
223                             $state->responses[$key] = $response;
224                         }
225                     }
226                 }
227             }
228             $responses[] = $key.'-'.$response;
229         }
230         $responses = implode(',', $responses);
232         // Set the legacy answer field
233         $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id));
234         return true;
235     }
237     function get_correct_responses(&$question, &$state) {
238         $responses = array();
239         foreach ($state->options->subquestions as $sub) {
240             foreach ($sub->options->answers as $answer) {
241                 if (1 == $answer->fraction && $sub->questiontext != '' && !is_null($sub->questiontext)) {
242                     $responses[$sub->id] = $answer->id;
243                 }
244             }
245         }
246         return empty($responses) ? null : $responses;
247     }
249     function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
250         global $CFG, $OUTPUT;
251         $context        = $this->get_context_by_category_id($question->category);
252         $subquestions   = $state->options->subquestions;
253         $correctanswers = $this->get_correct_responses($question, $state);
254         $nameprefix     = $question->name_prefix;
255         $answers        = array(); // Answer choices formatted ready for output.
256         $allanswers     = array(); // This and the next used to detect identical answers
257         $answerids      = array(); // and adjust ids.
258         $responses      = &$state->responses;
260         // Prepare a list of answers, removing duplicates.
261         foreach ($subquestions as $subquestion) {
262             foreach ($subquestion->options->answers as $ans) {
263                 $allanswers[$ans->id] = $ans->answer;
264                 if (!in_array($ans->answer, $answers)) {
265                     $answers[$ans->id] = strip_tags(format_string($ans->answer, false));
266                     $answerids[$ans->answer] = $ans->id;
267                 }
268             }
269         }
271         // Fix up the ids of any responses that point the the eliminated duplicates.
272         foreach ($responses as $subquestionid => $ignored) {
273             if ($responses[$subquestionid]) {
274                 $responses[$subquestionid] = $answerids[$allanswers[$responses[$subquestionid]]];
275             }
276         }
277         foreach ($correctanswers as $subquestionid => $ignored) {
278             $correctanswers[$subquestionid] = $answerids[$allanswers[$correctanswers[$subquestionid]]];
279         }
281         // Shuffle the answers
282         $answers = draw_rand_array($answers, count($answers));
284         // Print formulation
285         $questiontext = $this->format_text($question->questiontext,
286                 $question->questiontextformat, $cmoptions);
288         // Print the input controls
289         foreach ($subquestions as $key => $subquestion) {
290             if ($subquestion->questiontext !== '' && !is_null($subquestion->questiontext)) {
291                 // Subquestion text:
292                 $a = new stdClass;
293                 $text = quiz_rewrite_question_urls($subquestion->questiontext, 'pluginfile.php', $context->id, 'qtype_match', 'subquestion', array($state->attempt, $state->question), $subquestion->id);
294                 $a->text = $this->format_text($text, $subquestion->questiontextformat, $cmoptions);
296                 // Drop-down list:
297                 $menuname = $nameprefix.$subquestion->id;
298                 $response = isset($state->responses[$subquestion->id])
299                             ? $state->responses[$subquestion->id] : '0';
301                 $a->class = ' ';
302                 $a->feedbackimg = ' ';
304                 if ($options->readonly and $options->correct_responses) {
305                     if (isset($correctanswers[$subquestion->id])
306                             and ($correctanswers[$subquestion->id] == $response)) {
307                         $correctresponse = 1;
308                     } else {
309                         $correctresponse = 0;
310                     }
312                     if ($options->feedback && $response) {
313                         $a->class = question_get_feedback_class($correctresponse);
314                         $a->feedbackimg = question_get_feedback_image($correctresponse);
315                     }
316                 }
318                 $attributes = array();
319                 $attributes['disabled'] = $options->readonly ? 'disabled' : null;
320                 $a->control = html_writer::select($answers, $menuname, $response, array(''=>'choosedots'), $attributes);
322                 // Neither the editing interface or the database allow to provide
323                 // fedback for this question type.
324                 // However (as was pointed out in bug bug 3294) the randomsamatch
325                 // type which reuses this method can have feedback defined for
326                 // the wrapped shortanswer questions.
327                 //if ($options->feedback
328                 // && !empty($subquestion->options->answers[$responses[$key]]->feedback)) {
329                 //    print_comment($subquestion->options->answers[$responses[$key]]->feedback);
330                 //}
332                 $anss[] = $a;
333             }
334         }
335         include("$CFG->dirroot/question/type/match/display.html");
336     }
338     function grade_responses(&$question, &$state, $cmoptions) {
339         $subquestions = &$state->options->subquestions;
340         $responses    = &$state->responses;
342         // Prepare an array to help when disambiguating equal answers.
343         $answertexts = array();
344         foreach ($subquestions as $subquestion) {
345             $ans = reset($subquestion->options->answers);
346             $answertexts[$ans->id] = $ans->answer;
347         }
349         // Add up the grades from each subquestion.
350         $sumgrade = 0;
351         $totalgrade = 0;
352         foreach ($subquestions as $key => $sub) {
353             if ($sub->questiontext !== '' && !is_null($sub->questiontext)) {
354                 $totalgrade += 1;
355                 $response = $responses[$key];
356                 if ($response && !array_key_exists($response, $sub->options->answers)) {
357                     // If studen's answer did not match by id, but there may be
358                     // two answers with the same text, but different ids,
359                     // so we need to try matching the answer text.
360                     $expected_answer = reset($sub->options->answers);
361                     if ($answertexts[$response] == $expected_answer->answer) {
362                         $response = $expected_answer->id;
363                     }
364                 }
365                 if (array_key_exists($response, $sub->options->answers)) {
366                     $sumgrade += $sub->options->answers[$response]->fraction;
367                 }
368             }
369         }
371         $state->raw_grade = $sumgrade/$totalgrade;
372         if (empty($state->raw_grade)) {
373             $state->raw_grade = 0;
374         }
376         // Make sure we don't assign negative or too high marks
377         $state->raw_grade = min(max((float) $state->raw_grade,
378                             0.0), 1.0) * $question->maxgrade;
379         $state->penalty = $question->penalty * $question->maxgrade;
381         // mark the state as graded
382         $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
384         return true;
385     }
387     function compare_responses($question, $state, $teststate) {
388         foreach ($state->responses as $i=>$sr) {
389             if (empty($teststate->responses[$i])) {
390                 if (!empty($state->responses[$i])) {
391                     return false;
392                 }
393             } else if ($state->responses[$i] != $teststate->responses[$i]) {
394                 return false;
395             }
396         }
397         return true;
398     }
400     // ULPGC ecastro for stats report
401     function get_all_responses($question, $state) {
402         $answers = array();
403         if (is_array($question->options->subquestions)) {
404             foreach ($question->options->subquestions as $aid => $answer) {
405                 if ($answer->questiontext !== '' && !is_null($answer->questiontext)) {
406                     $r = new stdClass;
407                     $r->answer = $answer->questiontext . ": " . $answer->answertext;
408                     $r->credit = 1;
409                     $answers[$aid] = $r;
410                 }
411             }
412         }
413         $result = new stdClass;
414         $result->id = $question->id;
415         $result->responses = $answers;
416         return $result;
417     }
419     function get_possible_responses(&$question) {
420         $answers = array();
421         if (is_array($question->options->subquestions)) {
422             foreach ($question->options->subquestions as $subqid => $answer) {
423                 if ($answer->questiontext) {
424                     $r = new stdClass;
425                     $r->answer = $answer->questiontext . ": " . $answer->answertext;
426                     $r->credit = 1;
427                     $answers[$subqid] = array($answer->id =>$r);
428                 }
429             }
430         }
431         return $answers;
432     }
434     // ULPGC ecastro
435     function get_actual_response($question, $state) {
436         $subquestions = &$state->options->subquestions;
437         $responses    = &$state->responses;
438         $results=array();
439         foreach ($responses as $ind => $code) {
440             foreach ($subquestions as $key => $sub) {
441                 if (isset($sub->options->answers[$code])) {
442                     $results[$ind] =  $subquestions[$ind]->questiontext . ": " . $sub->options->answers[$code]->answer;
443                 }
444             }
445         }
446         return $results;
447     }
449    function get_actual_response_details($question, $state) {
450         $responses = $this->get_actual_response($question, $state);
451         $teacherresponses = $this->get_possible_responses($question, $state);
452         //only one response
453         $responsedetails =array();
454         foreach ($responses as $tsubqid => $response){
455             $responsedetail = new object();
456             $responsedetail->subqid = $tsubqid;
457             $responsedetail->response = $response;
458             foreach ($teacherresponses[$tsubqid] as $aid => $tresponse){
459                 if ($tresponse->answer == $response){
460                     $responsedetail->aid = $aid;
461                     break;
462                 }
463             }
464             if (isset($responsedetail->aid)){
465                 $responsedetail->credit = $teacherresponses[$tsubqid][$aid]->credit;
466             } else {
467                 $responsedetail->aid = 0;
468                 $responsedetail->credit = 0;
469             }
470             $responsedetails[] = $responsedetail;
471         }
472         return $responsedetails;
473     }
476     /**
477      * @param object $question
478      * @return mixed either a integer score out of 1 that the average random
479      * guess by a student might give or an empty string which means will not
480      * calculate.
481      */
482     function get_random_guess_score($question) {
483         return 1 / count($question->options->subquestions);
484     }
486 /// BACKUP FUNCTIONS ////////////////////////////
488     /*
489      * Backup the data in the question
490      *
491      * This is used in question/backuplib.php
492      */
493     function backup($bf,$preferences,$question,$level=6) {
494         global $DB;
495         $status = true;
497         // Output the shuffleanswers setting.
498         $matchoptions = $DB->get_record('question_match', array('question' => $question));
499         if ($matchoptions) {
500             $status = fwrite ($bf,start_tag("MATCHOPTIONS",6,true));
501             fwrite ($bf,full_tag("SHUFFLEANSWERS",7,false,$matchoptions->shuffleanswers));
502             $status = fwrite ($bf,end_tag("MATCHOPTIONS",6,true));
503         }
505         $matchs = $DB->get_records('question_match_sub', array('question' =>  $question), 'id ASC');
506         //If there are matchs
507         if ($matchs) {
508             //Print match contents
509             $status = fwrite ($bf,start_tag("MATCHS",6,true));
510             //Iterate over each match
511             foreach ($matchs as $match) {
512                 $status = fwrite ($bf,start_tag("MATCH",7,true));
513                 //Print match contents
514                 fwrite ($bf,full_tag("ID",8,false,$match->id));
515                 fwrite ($bf,full_tag("CODE",8,false,$match->code));
516                 fwrite ($bf,full_tag("QUESTIONTEXT",8,false,$match->questiontext));
517                 fwrite ($bf,full_tag("ANSWERTEXT",8,false,$match->answertext));
518                 $status = fwrite ($bf,end_tag("MATCH",7,true));
519             }
520             $status = fwrite ($bf,end_tag("MATCHS",6,true));
521         }
522         return $status;
523     }
525 /// RESTORE FUNCTIONS /////////////////
527     /*
528      * Restores the data in the question
529      *
530      * This is used in question/restorelib.php
531      */
532     function restore($old_question_id,$new_question_id,$info,$restore) {
533         global $DB;
534         $status = true;
536         //Get the matchs array
537         $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
539         //We have to build the subquestions field (a list of match_sub id)
540         $subquestions_field = "";
541         $in_first = true;
543         //Iterate over matchs
544         for($i = 0; $i < sizeof($matchs); $i++) {
545             $mat_info = $matchs[$i];
547             //We'll need this later!!
548             $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
550             //Now, build the question_match_SUB record structure
551             $match_sub = new stdClass;
552             $match_sub->question = $new_question_id;
553             $match_sub->code = isset($mat_info['#']['CODE']['0']['#'])?backup_todb($mat_info['#']['CODE']['0']['#']):'';
554             if (!$match_sub->code) {
555                 $match_sub->code = $oldid;
556             }
557             $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']);
558             $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']);
560             //The structure is equal to the db, so insert the question_match_sub
561             $newid = $DB->insert_record ("question_match_sub",$match_sub);
563             //Do some output
564             if (($i+1) % 50 == 0) {
565                 if (!defined('RESTORE_SILENTLY')) {
566                     echo ".";
567                     if (($i+1) % 1000 == 0) {
568                         echo "<br />";
569                     }
570                 }
571                 backup_flush(300);
572             }
574             if ($newid) {
575                 //We have the newid, update backup_ids
576                 backup_putid($restore->backup_unique_code,"question_match_sub",$oldid,
577                              $newid);
578                 //We have a new match_sub, append it to subquestions_field
579                 if ($in_first) {
580                     $subquestions_field .= $newid;
581                     $in_first = false;
582                 } else {
583                     $subquestions_field .= ",".$newid;
584                 }
585             } else {
586                 $status = false;
587             }
588         }
590         //We have created every match_sub, now create the match
591         $match = new stdClass;
592         $match->question = $new_question_id;
593         $match->subquestions = $subquestions_field;
595         // Get the shuffleanswers option, if it is there.
596         if (!empty($info['#']['MATCHOPTIONS']['0']['#']['SHUFFLEANSWERS'])) {
597             $match->shuffleanswers = backup_todb($info['#']['MATCHOPTIONS']['0']['#']['SHUFFLEANSWERS']['0']['#']);
598         } else {
599             $match->shuffleanswers = 1;
600         }
602         //The structure is equal to the db, so insert the question_match_sub
603         $newid = $DB->insert_record ("question_match",$match);
605         if (!$newid) {
606             $status = false;
607         }
609         return $status;
610     }
612     function restore_map($old_question_id,$new_question_id,$info,$restore) {
613         global $DB;
614         $status = true;
616         //Get the matchs array
617         $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
619         //We have to build the subquestions field (a list of match_sub id)
620         $subquestions_field = "";
621         $in_first = true;
623         //Iterate over matchs
624         for($i = 0; $i < sizeof($matchs); $i++) {
625             $mat_info = $matchs[$i];
627             //We'll need this later!!
628             $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
630             //Now, build the question_match_SUB record structure
631             $match_sub->question = $new_question_id;
632             $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']);
633             $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']);
635             //If we are in this method is because the question exists in DB, so its
636             //match_sub must exist too.
637             //Now, we are going to look for that match_sub in DB and to create the
638             //mappings in backup_ids to use them later where restoring states (user level).
640             //Get the match_sub from DB (by question, questiontext and answertext)
641             $db_match_sub = $DB->get_record ("question_match_sub",array("question"=>$new_question_id,
642                                                       "questiontext"=>$match_sub->questiontext,
643                                                       "answertext"=>$match_sub->answertext));
644             //Do some output
645             if (($i+1) % 50 == 0) {
646                 if (!defined('RESTORE_SILENTLY')) {
647                     echo ".";
648                     if (($i+1) % 1000 == 0) {
649                         echo "<br />";
650                     }
651                 }
652                 backup_flush(300);
653             }
655             //We have the database match_sub, so update backup_ids
656             if ($db_match_sub) {
657                 //We have the newid, update backup_ids
658                 backup_putid($restore->backup_unique_code,"question_match_sub",$oldid,
659                              $db_match_sub->id);
660             } else {
661                 $status = false;
662             }
663         }
665         return $status;
666     }
668     function restore_recode_answer($state, $restore) {
670         //The answer is a comma separated list of hypen separated math_subs (for question and answer)
671         $answer_field = "";
672         $in_first = true;
673         $tok = strtok($state->answer,",");
674         while ($tok) {
675             //Extract the match_sub for the question and the answer
676             $exploded = explode("-",$tok);
677             $match_question_id = $exploded[0];
678             $match_answer_id = $exploded[1];
679             //Get the match_sub from backup_ids (for the question)
680             if (!$match_que = backup_getid($restore->backup_unique_code,"question_match_sub",$match_question_id)) {
681                 echo 'Could not recode question in question_match_sub '.$match_question_id.'<br />';
682             } else {
683                 if ($in_first) {
684                     $in_first = false;
685                 } else {
686                     $answer_field .= ',';
687                 }
688                 $answer_field .= $match_que->new_id.'-'.$match_answer_id;
689             }
690             //check for next
691             $tok = strtok(",");
692         }
693         return $answer_field;
694     }
696     /**
697      * Decode links in question type specific tables.
698      * @return bool success or failure.
699      */
700     function decode_content_links_caller($questionids, $restore, &$i) {
701         global $DB;
703         $status = true;
705         // Decode links in the question_match_sub table.
706         if ($subquestions = $DB->get_records_list('question_match_sub', 'question', $questionids, '', 'id, questiontext')) {
708             foreach ($subquestions as $subquestion) {
709                 $questiontext = restore_decode_content_links_worker($subquestion->questiontext, $restore);
710                 if ($questiontext != $subquestion->questiontext) {
711                     $subquestion->questiontext = $questiontext;
712                     $DB->update_record('question_match_sub', $subquestion);
713                 }
715                 // Do some output.
716                 if (++$i % 5 == 0 && !defined('RESTORE_SILENTLY')) {
717                     echo ".";
718                     if ($i % 100 == 0) {
719                         echo "<br />";
720                     }
721                     backup_flush(300);
722                 }
723             }
724         }
726         return $status;
727     }
729     function find_file_links($question, $courseid){
730         // find links in the question_match_sub table.
731         $urls = array();
732         if (isset($question->options->subquestions)){
733             foreach ($question->options->subquestions as $subquestion) {
734                 $urls += question_find_file_links_from_html($subquestion->questiontext, $courseid);
735             }
737             //set all the values of the array to the question object
738             if ($urls){
739                 $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id)));
740             }
741         }
742         $urls = array_merge_recursive($urls, parent::find_file_links($question, $courseid));
744         return $urls;
745     }
747     function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
748         global $DB;
749         parent::replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination);
750         // replace links in the question_match_sub table.
751         if (isset($question->options->subquestions)){
752             foreach ($question->options->subquestions as $subquestion) {
753                 $subquestionchanged = false;
754                 $subquestion->questiontext = question_replace_file_links_in_html($subquestion->questiontext, $fromcourseid, $tocourseid, $url, $destination, $subquestionchanged);
755                 if ($subquestionchanged){//need to update rec in db
756                     $DB->update_record('question_match_sub', $subquestion);
757                 }
758             }
759         }
760     }
762     /**
763      * Runs all the code required to set up and save an essay question for testing purposes.
764      * Alternate DB table prefix may be used to facilitate data deletion.
765      */
766     function generate_test($name, $courseid = null) {
767         global $DB;
768         list($form, $question) = parent::generate_test($name, $courseid);
769         $form->shuffleanswers = 1;
770         $form->noanswers = 3;
771         $form->subquestions = array('cat', 'dog', 'cow');
772         $form->subanswers = array('feline', 'canine', 'bovine');
774         if ($courseid) {
775             $course = $DB->get_record('course', array('id' => $courseid));
776         }
778         return $this->save_question($question, $form, $course);
779     }
781     function move_files($question, $newcategory) {
782         global $DB;
783         // move files belonging to question component
784         parent::move_files($question, $newcategory);
786         // move files belonging to qtype_multichoice
787         $fs = get_file_storage();
788         // process files in answer
789         if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) {
790             $oldanswers = array();
791         }
793         // process files in sub questions
794         if (!$subquestions = $DB->get_records('question_match_sub', array('question' => $question->id), 'id ASC')) {
795             $subquestions = array();
796         }
797         $component = 'qtype_match';
798         $filearea = 'subquestion';
799         foreach ($subquestions as $sub) {
800             $files = $fs->get_area_files($question->contextid, $component, $filearea, $sub->id);
801             foreach ($files as $storedfile) {
802                 if (!$storedfile->is_directory()) {
803                     $newfile = new object();
804                     $newfile->contextid = (int)$newcategory->contextid;
805                     $fs->create_file_from_storedfile($newfile, $storedfile);
806                     $storedfile->delete();
807                 }
808             }
809         }
810     }
811     function check_file_access($question, $state, $options, $contextid, $component,
812             $filearea, $args) {
814         $itemid = reset($args);
815         if ($filearea == 'subquestion') {
816             // always display quetion images
817             // itemid is sub question id
818             if ($itemid != $question->id) {
819                 return false;
820             }
821             return true;
822         } else {
823             return parent::check_file_access($question, $state, $options, $contextid, $component,
824                     $filearea, $args);
825         }
826     }
828 //// END OF CLASS ////
830 //////////////////////////////////////////////////////////////////////////
831 //// INITIATION - Without this line the question type is not in use... ///
832 //////////////////////////////////////////////////////////////////////////
833 question_register_questiontype(new question_match_qtype());