"MDL-16094, fixed file server function in match type question"
[moodle.git] / question / type / match / questiontype.php
CommitLineData
aeb15530 1<?php
516cf3eb 2
fe6ce234
DC
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/>.
17
516cf3eb 18/////////////
19/// MATCH ///
20/////////////
21
22/// QUESTION TYPE CLASS //////////////////
1976496e 23/**
24 * @package questionbank
25 * @subpackage questiontypes
7375c542 26 */
af3830ee 27class question_match_qtype extends default_questiontype {
516cf3eb 28
29 function name() {
30 return 'match';
31 }
32
33 function get_question_options(&$question) {
f34488b2 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');
516cf3eb 37 return true;
38 }
39
40 function save_question_options($question) {
f34488b2 41 global $DB;
fe6ce234 42 $context = $question->context;
a58ffe3f 43 $result = new stdClass;
069a820a 44
f34488b2 45 if (!$oldsubquestions = $DB->get_records("question_match_sub", array("question" => $question->id), "id ASC")) {
516cf3eb 46 $oldsubquestions = array();
47 }
48
516cf3eb 49 // $subquestions will be an array with subquestion ids
50 $subquestions = array();
51
52 // Insert all the new question+answer pairs
53 foreach ($question->subquestions as $key => $questiontext) {
fe6ce234
DC
54 $itemid = $questiontext['itemid'];
55 $format = $questiontext['format'];
56 $questiontext = trim($questiontext['text']);
9ce0983b 57 $answertext = trim($question->subanswers[$key]);
ebf83e2c 58 if ($questiontext != '' || $answertext != '') {
516cf3eb 59 if ($subquestion = array_shift($oldsubquestions)) { // Existing answer, so reuse it
516cf3eb 60 $subquestion->answertext = $answertext;
fe6ce234
DC
61 $subquestion->questiontext = file_save_draft_area_files($itemid, $context->id, 'qtype_match', 'subquestion', $subquestion->id, self::$fileoptions, $questiontext);
62 $subquestion->questiontextformat = $format;
bb4b6010 63 $DB->update_record("question_match_sub", $subquestion);
516cf3eb 64 } else {
a58ffe3f 65 $subquestion = new stdClass;
516cf3eb 66 // Determine a unique random code
fe6ce234 67 $subquestion->code = rand(1, 999999999);
f34488b2 68 while ($DB->record_exists('question_match_sub', array('code' => $subquestion->code, 'question' => $question->id))) {
516cf3eb 69 $subquestion->code = rand();
70 }
71 $subquestion->question = $question->id;
72 $subquestion->questiontext = $questiontext;
fe6ce234
DC
73 $subquestion->questiontextformat = $format;
74 $subquestion->answertext = $answertext;
bb4b6010 75 $subquestion->id = $DB->insert_record("question_match_sub", $subquestion);
fe6ce234
DC
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));
516cf3eb 78 }
79 $subquestions[] = $subquestion->id;
80 }
ebf83e2c 81 if ($questiontext != '' && $answertext == '') {
a58ffe3f 82 $result->notice = get_string('nomatchinganswer', 'quiz', $questiontext);
83 }
516cf3eb 84 }
85
86 // delete old subquestions records
87 if (!empty($oldsubquestions)) {
88 foreach($oldsubquestions as $os) {
f34488b2 89 $DB->delete_records('question_match_sub', array('id' => $os->id));
516cf3eb 90 }
91 }
92
f34488b2 93 if ($options = $DB->get_record("question_match", array("question" => $question->id))) {
516cf3eb 94 $options->subquestions = implode(",",$subquestions);
95 $options->shuffleanswers = $question->shuffleanswers;
bb4b6010 96 $DB->update_record("question_match", $options);
516cf3eb 97 } else {
98 unset($options);
99 $options->question = $question->id;
100 $options->subquestions = implode(",",$subquestions);
101 $options->shuffleanswers = $question->shuffleanswers;
bb4b6010 102 $DB->insert_record("question_match", $options);
516cf3eb 103 }
a58ffe3f 104
105 if (!empty($result->notice)) {
106 return $result;
107 }
108
109 if (count($subquestions) < 3) {
110 $result->notice = get_string('notenoughanswers', 'quiz', 3);
111 return $result;
112 }
113
516cf3eb 114 return true;
115 }
116
117 /**
118 * Deletes question from the question-type specific tables
119 *
120 * @return boolean Success/Failure
121 * @param integer $question->id
122 */
90c3f310 123 function delete_question($questionid) {
f34488b2 124 global $DB;
125 $DB->delete_records("question_match", array("question" => $questionid));
126 $DB->delete_records("question_match_sub", array("question" => $questionid));
516cf3eb 127 return true;
128 }
129
130 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
fef8f84e 131 global $DB, $OUTPUT;
f34488b2 132 if (!$state->options->subquestions = $DB->get_records('question_match_sub', array('question' => $question->id), 'id ASC')) {
fef8f84e 133 echo $OUTPUT->notification('Error: Missing subquestions!');
00c30069 134 return false;
516cf3eb 135 }
136
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;
f8394114 146 $state->options->subquestions[$key]->options->answers[$subquestion->code] = clone($answer);
516cf3eb 147
148 $state->responses[$key] = '';
149 }
150
151 // Shuffle the answers if required
152 if ($cmoptions->shuffleanswers and $question->options->shuffleanswers) {
153 $state->options->subquestions = swapshuffle_assoc($state->options->subquestions);
154 }
155
156 return true;
157 }
158
159 function restore_session_and_responses(&$question, &$state) {
fef8f84e 160 global $DB, $OUTPUT;
869309b8 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')) {
fef8f84e 164 echo $OUTPUT->notification('Error: Missing subquestions!');
869309b8 165 return false;
166 }
167 }
f8394114 168
516cf3eb 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
32a189d6 171 // both refer to the id in the table question_match_sub.
516cf3eb 172 $responses = explode(',', $state->responses['']);
f8394114 173 $responses = array_map(create_function('$val', 'return explode("-", $val);'), $responses);
516cf3eb 174
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];
869309b8 180 $state->options->subquestions[$response[0]] = clone($subquestions[$question->id][$response[0]]);
516cf3eb 181 }
182
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;
299d9fb8 190 $answer->answer = $subquestion->answertext;
516cf3eb 191 $answer->fraction = 1.0;
f8394114 192 $state->options->subquestions[$key]->options->answers[$subquestion->code] = clone($answer);
516cf3eb 193 }
194
195 return true;
196 }
197
198 function save_session_and_responses(&$question, &$state) {
f34488b2 199 global $DB;
87ee4968 200 $subquestions = &$state->options->subquestions;
201
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 }
069a820a 208
516cf3eb 209 // Serialize responses
210 $responses = array();
87ee4968 211 foreach ($subquestions as $key => $subquestion) {
7d6af8ca 212 $response = 0;
bcda9611 213 if ($subquestion->questiontext !== '' && !is_null($subquestion->questiontext)) {
87ee4968 214 if ($state->responses[$key]) {
215 $response = $state->responses[$key];
216 if (!array_key_exists($response, $subquestion->options->answers)) {
bcda9611 217 // If student's answer did not match by id, but there may be
87ee4968 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 }
87ee4968 226 }
0c24ee0f 227 }
7d6af8ca 228 $responses[] = $key.'-'.$response;
516cf3eb 229 }
230 $responses = implode(',', $responses);
231
232 // Set the legacy answer field
f685e830 233 $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id));
516cf3eb 234 return true;
235 }
236
237 function get_correct_responses(&$question, &$state) {
238 $responses = array();
239 foreach ($state->options->subquestions as $sub) {
240 foreach ($sub->options->answers as $answer) {
bcda9611 241 if (1 == $answer->fraction && $sub->questiontext != '' && !is_null($sub->questiontext)) {
516cf3eb 242 $responses[$sub->id] = $answer->id;
243 }
244 }
245 }
246 return empty($responses) ? null : $responses;
247 }
248
249 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
44c64ad8 250 global $CFG, $OUTPUT;
fe6ce234 251 $context = $this->get_context_by_category_id($question->category);
516cf3eb 252 $subquestions = $state->options->subquestions;
253 $correctanswers = $this->get_correct_responses($question, $state);
254 $nameprefix = $question->name_prefix;
299d9fb8 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.
516cf3eb 258 $responses = &$state->responses;
259
069a820a 260 // Prepare a list of answers, removing duplicates.
516cf3eb 261 foreach ($subquestions as $subquestion) {
262 foreach ($subquestion->options->answers as $ans) {
0b4ce29d 263 $allanswers[$ans->id] = $ans->answer;
87ee4968 264 if (!in_array($ans->answer, $answers)) {
299d9fb8 265 $answers[$ans->id] = strip_tags(format_string($ans->answer, false));
87ee4968 266 $answerids[$ans->answer] = $ans->id;
87ee4968 267 }
516cf3eb 268 }
269 }
069a820a 270
0b4ce29d 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 }
516cf3eb 280
281 // Shuffle the answers
282 $answers = draw_rand_array($answers, count($answers));
283
37a12367 284 // Print formulation
1b8a7434 285 $questiontext = $this->format_text($question->questiontext,
286 $question->questiontextformat, $cmoptions);
516cf3eb 287
069a820a 288 // Print the input controls
516cf3eb 289 foreach ($subquestions as $key => $subquestion) {
bcda9611 290 if ($subquestion->questiontext !== '' && !is_null($subquestion->questiontext)) {
069a820a 291 // Subquestion text:
a58ffe3f 292 $a = new stdClass;
fe6ce234
DC
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);
2b087056 295
069a820a 296 // Drop-down list:
a58ffe3f 297 $menuname = $nameprefix.$subquestion->id;
298 $response = isset($state->responses[$subquestion->id])
299 ? $state->responses[$subquestion->id] : '0';
2b087056 300
301 $a->class = ' ';
302 $a->feedbackimg = ' ';
303
069a820a 304 if ($options->readonly and $options->correct_responses) {
305 if (isset($correctanswers[$subquestion->id])
2b087056 306 and ($correctanswers[$subquestion->id] == $response)) {
069a820a 307 $correctresponse = 1;
308 } else {
309 $correctresponse = 0;
310 }
2b087056 311
1b16ecd1 312 if ($options->feedback && $response) {
069a820a 313 $a->class = question_get_feedback_class($correctresponse);
314 $a->feedbackimg = question_get_feedback_image($correctresponse);
315 }
2b087056 316 }
aeb15530 317
c5dbaba9
PS
318 $attributes = array();
319 $attributes['disabled'] = $options->readonly ? 'disabled' : null;
320 $a->control = html_writer::select($answers, $menuname, $response, array(''=>'choosedots'), $attributes);
069a820a 321
a58ffe3f 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 //}
2b087056 331
a58ffe3f 332 $anss[] = $a;
516cf3eb 333 }
516cf3eb 334 }
aaae75b0 335 include("$CFG->dirroot/question/type/match/display.html");
516cf3eb 336 }
337
338 function grade_responses(&$question, &$state, $cmoptions) {
339 $subquestions = &$state->options->subquestions;
340 $responses = &$state->responses;
341
87ee4968 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 }
069a820a 348
87ee4968 349 // Add up the grades from each subquestion.
516cf3eb 350 $sumgrade = 0;
a58ffe3f 351 $totalgrade = 0;
516cf3eb 352 foreach ($subquestions as $key => $sub) {
bcda9611 353 if ($sub->questiontext !== '' && !is_null($sub->questiontext)) {
a58ffe3f 354 $totalgrade += 1;
87ee4968 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;
a58ffe3f 367 }
516cf3eb 368 }
369 }
370
a58ffe3f 371 $state->raw_grade = $sumgrade/$totalgrade;
516cf3eb 372 if (empty($state->raw_grade)) {
373 $state->raw_grade = 0;
374 }
375
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;
380
f30bbcaf 381 // mark the state as graded
382 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
383
516cf3eb 384 return true;
385 }
386
b25486fc 387 function compare_responses($question, $state, $teststate) {
2c89cfb5 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]) {
b25486fc 394 return false;
395 }
396 }
397 return true;
398 }
399
516cf3eb 400 // ULPGC ecastro for stats report
401 function get_all_responses($question, $state) {
974383f9 402 $answers = array();
516cf3eb 403 if (is_array($question->options->subquestions)) {
645d7610 404 foreach ($question->options->subquestions as $aid => $answer) {
bcda9611 405 if ($answer->questiontext !== '' && !is_null($answer->questiontext)) {
645d7610 406 $r = new stdClass;
407 $r->answer = $answer->questiontext . ": " . $answer->answertext;
408 $r->credit = 1;
409 $answers[$aid] = $r;
410 }
516cf3eb 411 }
516cf3eb 412 }
a58ffe3f 413 $result = new stdClass;
516cf3eb 414 $result->id = $question->id;
415 $result->responses = $answers;
416 return $result;
417 }
418
869309b8 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 }
aeb15530 433
516cf3eb 434 // ULPGC ecastro
435 function get_actual_response($question, $state) {
6d6b454a
TH
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 }
aeb15530 448
869309b8 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 }
2280e147 474
a3b70fa9 475
6f51ed72 476 /**
477 * @param object $question
455c3efa 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.
6f51ed72 481 */
482 function get_random_guess_score($question) {
483 return 1 / count($question->options->subquestions);
484 }
069a820a 485
c5d94c41 486/// BACKUP FUNCTIONS ////////////////////////////
487
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) {
f34488b2 494 global $DB;
c5d94c41 495 $status = true;
496
7028ab33 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 }
504
f34488b2 505 $matchs = $DB->get_records('question_match_sub', array('question' => $question), 'id ASC');
c5d94c41 506 //If there are matchs
507 if ($matchs) {
7028ab33 508 //Print match contents
c5d94c41 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 }
516cf3eb 524
315559d3 525/// RESTORE FUNCTIONS /////////////////
526
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) {
9db7dab2 533 global $DB;
315559d3 534 $status = true;
535
536 //Get the matchs array
537 $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
538
539 //We have to build the subquestions field (a list of match_sub id)
540 $subquestions_field = "";
541 $in_first = true;
542
543 //Iterate over matchs
544 for($i = 0; $i < sizeof($matchs); $i++) {
545 $mat_info = $matchs[$i];
546
547 //We'll need this later!!
548 $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
549
550 //Now, build the question_match_SUB record structure
a58ffe3f 551 $match_sub = new stdClass;
315559d3 552 $match_sub->question = $new_question_id;
1f4d6e9a 553 $match_sub->code = isset($mat_info['#']['CODE']['0']['#'])?backup_todb($mat_info['#']['CODE']['0']['#']):'';
315559d3 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']['#']);
559
560 //The structure is equal to the db, so insert the question_match_sub
9db7dab2 561 $newid = $DB->insert_record ("question_match_sub",$match_sub);
315559d3 562
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 }
573
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 }
589
590 //We have created every match_sub, now create the match
87ee4968 591 $match = new stdClass;
315559d3 592 $match->question = $new_question_id;
593 $match->subquestions = $subquestions_field;
594
7028ab33 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 }
601
315559d3 602 //The structure is equal to the db, so insert the question_match_sub
9db7dab2 603 $newid = $DB->insert_record ("question_match",$match);
315559d3 604
605 if (!$newid) {
606 $status = false;
607 }
608
609 return $status;
610 }
611
612 function restore_map($old_question_id,$new_question_id,$info,$restore) {
f34488b2 613 global $DB;
315559d3 614 $status = true;
615
616 //Get the matchs array
617 $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
618
619 //We have to build the subquestions field (a list of match_sub id)
620 $subquestions_field = "";
621 $in_first = true;
622
623 //Iterate over matchs
624 for($i = 0; $i < sizeof($matchs); $i++) {
625 $mat_info = $matchs[$i];
626
627 //We'll need this later!!
628 $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
629
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']['#']);
634
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).
639
640 //Get the match_sub from DB (by question, questiontext and answertext)
f34488b2 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));
315559d3 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 }
654
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 }
664
665 return $status;
666 }
667
668 function restore_recode_answer($state, $restore) {
669
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];
3ff8a187 678 $match_answer_id = $exploded[1];
315559d3 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)) {
3ff8a187 681 echo 'Could not recode question in question_match_sub '.$match_question_id.'<br />';
7028ab33 682 } else {
3ff8a187 683 if ($in_first) {
3ff8a187 684 $in_first = false;
685 } else {
7028ab33 686 $answer_field .= ',';
3ff8a187 687 }
7028ab33 688 $answer_field .= $match_que->new_id.'-'.$match_answer_id;
315559d3 689 }
690 //check for next
691 $tok = strtok(",");
692 }
693 return $answer_field;
694 }
695
c85607f0 696 /**
697 * Decode links in question type specific tables.
698 * @return bool success or failure.
271e6dec 699 */
c85607f0 700 function decode_content_links_caller($questionids, $restore, &$i) {
44e1b7d7 701 global $DB;
702
e3b2eb60 703 $status = true;
704
c85607f0 705 // Decode links in the question_match_sub table.
44e1b7d7 706 if ($subquestions = $DB->get_records_list('question_match_sub', 'question', $questionids, '', 'id, questiontext')) {
c85607f0 707
708 foreach ($subquestions as $subquestion) {
709 $questiontext = restore_decode_content_links_worker($subquestion->questiontext, $restore);
710 if ($questiontext != $subquestion->questiontext) {
294ce987 711 $subquestion->questiontext = $questiontext;
bb4b6010 712 $DB->update_record('question_match_sub', $subquestion);
c85607f0 713 }
714
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 }
e3b2eb60 725
726 return $status;
c85607f0 727 }
271e6dec 728
729 function find_file_links($question, $courseid){
730 // find links in the question_match_sub table.
731 $urls = array();
6f9ce926 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 }
271e6dec 736
6f9ce926 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 }
271e6dec 741 }
742 $urls = array_merge_recursive($urls, parent::find_file_links($question, $courseid));
6f9ce926 743
271e6dec 744 return $urls;
745 }
746
747 function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
f34488b2 748 global $DB;
271e6dec 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
bb4b6010 756 $DB->update_record('question_match_sub', $subquestion);
271e6dec 757 }
758 }
759 }
760 }
b9bd6da4 761
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');
773
774 if ($courseid) {
775 $course = $DB->get_record('course', array('id' => $courseid));
776 }
777
778 return $this->save_question($question, $form, $course);
779 }
fe6ce234
DC
780
781 function move_files($question, $newcategory) {
782 global $DB;
783 // move files belonging to question component
784 parent::move_files($question, $newcategory);
785
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 }
792
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) {
813
814 $itemid = reset($args);
815 if ($filearea == 'subquestion') {
fe6ce234 816 // itemid is sub question id
b6664815 817 if (!array_key_exists($itemid, $question->options->subquestions)) {
fe6ce234
DC
818 return false;
819 }
b6664815 820
fe6ce234
DC
821 return true;
822 } else {
823 return parent::check_file_access($question, $state, $options, $contextid, $component,
824 $filearea, $args);
825 }
826 }
516cf3eb 827}
828//// END OF CLASS ////
829
830//////////////////////////////////////////////////////////////////////////
831//// INITIATION - Without this line the question type is not in use... ///
832//////////////////////////////////////////////////////////////////////////
a2156789 833question_register_questiontype(new question_match_qtype());