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