7 /// QUESTION TYPE CLASS //////////////////
9 * @package questionbank
10 * @subpackage questiontypes
12 class question_match_qtype
extends default_questiontype
{
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');
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 ($questiontext != '' ||
$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)";
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!";
60 $subquestions[] = $subquestion->id
;
62 if ($questiontext != '' && $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)";
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!";
92 if (!empty($result->notice
)) {
96 if (count($subquestions) < 3) {
97 $result->notice
= get_string('notenoughanswers', 'quiz', 3);
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);
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!');
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
);
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!');
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
= format_string($subquestion->answertext
);
174 $answer->fraction
= 1.0;
175 $state->options
->subquestions
[$key]->options
176 ->answers
[$subquestion->code
] = clone($answer);
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) {
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
)) {
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) {
236 $subquestions = $state->options
->subquestions
;
237 $correctanswers = $this->get_correct_responses($question, $state);
238 $nameprefix = $question->name_prefix
;
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));
269 $questiontext = $this->format_text($question->questiontext
,
270 $question->questiontextformat
, $cmoptions);
271 $image = get_question_image($question);
273 // Print the input controls
274 foreach ($subquestions as $key => $subquestion) {
275 if ($subquestion->questiontext
!= '') {
278 $a->text
= $this->format_text($subquestion->questiontext
,
279 $question->questiontextformat
, $cmoptions);
282 $menuname = $nameprefix.$subquestion->id
;
283 $response = isset($state->responses
[$subquestion->id
])
284 ?
$state->responses
[$subquestion->id
] : '0';
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;
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);
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.
336 foreach ($subquestions as $key => $sub) {
337 if ($sub->questiontext
) {
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
;
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])) {
377 } else if ($state->responses
[$i] != $teststate->responses
[$i]) {
384 // ULPGC ecastro for stats report
385 function get_all_responses($question, $state) {
387 if (is_array($question->options
->subquestions
)) {
388 foreach ($question->options
->subquestions
as $aid => $answer) {
389 if ($answer->questiontext
) {
391 $r->answer
= $answer->questiontext
. ": " . $answer->answertext
;
397 $result = new stdClass
;
398 $result->id
= $question->id
;
399 $result->responses
= $answers;
404 function get_actual_response($question, $state) {
405 $subquestions = &$state->options
->subquestions
;
406 $responses = &$state->responses
;
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
;
418 function response_summary($question, $state, $length=80) {
419 // This should almost certainly be overridden
420 return shorten_text(implode(', ', $this->get_actual_response($question, $state)), $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) {
434 $matchs = get_records('question_match_sub', 'question', $question, 'id ASC');
435 //If there are 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));
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) {
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 = "";
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);
492 if (($i+
1) %
50 == 0) {
493 if (!defined('RESTORE_SILENTLY')) {
495 if (($i+
1) %
1000 == 0) {
503 //We have the newid, update backup_ids
504 backup_putid($restore->backup_unique_code
,"question_match_sub",$oldid,
506 //We have a new match_sub, append it to subquestions_field
508 $subquestions_field .= $newid;
511 $subquestions_field .= ",".$newid;
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);
533 function restore_map($old_question_id,$new_question_id,$info,$restore) {
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 = "";
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
);
566 if (($i+
1) %
50 == 0) {
567 if (!defined('RESTORE_SILENTLY')) {
569 if (($i+
1) %
1000 == 0) {
576 //We have the database match_sub, so update backup_ids
578 //We have the newid, update backup_ids
579 backup_putid($restore->backup_unique_code
,"question_match_sub",$oldid,
589 function restore_recode_answer($state, $restore) {
591 //The answer is a comma separated list of hypen separated math_subs (for question and answer)
594 $tok = strtok($state->answer
,",");
596 //Extract the match_sub for the question and the answer
597 $exploded = explode("-",$tok);
598 $match_question_id = $exploded[0];
599 $match_answer_id = $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 in question_match_sub '.$match_question_id.'<br />';
604 //Get the match_sub from backup_ids (for the answer)
605 if ($match_answer_id) { // only recode answer if not 0, not answered yet
606 if (!$match_ans = backup_getid($restore->backup_unique_code
,"question_match_sub",$match_answer_id)) {
607 echo 'Could not recode answer in question_match_sub '.$match_answer_id.'<br />';
612 //If the question hasn't response, it must be 0
613 if (!$match_ans and $match_answer_id == 0) {
614 $match_ans->new_id
= 0;
618 $answer_field .= $match_que->new_id
."-".$match_ans->new_id
;
621 $answer_field .= ",".$match_que->new_id
."-".$match_ans->new_id
;
627 return $answer_field;
631 * Decode links in question type specific tables.
632 * @return bool success or failure.
634 function decode_content_links_caller($questionids, $restore, &$i) {
637 // Decode links in the question_match_sub table.
638 if ($subquestions = get_records_list('question_match_sub', 'question',
639 implode(',', $questionids), '', 'id, questiontext')) {
641 foreach ($subquestions as $subquestion) {
642 $questiontext = restore_decode_content_links_worker($subquestion->questiontext
, $restore);
643 if ($questiontext != $subquestion->questiontext
) {
644 $subquestion->questiontext
= addslashes($questiontext);
645 if (!update_record('question_match_sub', $subquestion)) {
651 if (++
$i %
5 == 0 && !defined('RESTORE_SILENTLY')) {
664 function find_file_links($question, $courseid){
665 // find links in the question_match_sub table.
667 if (isset($question->options
->subquestions
)){
668 foreach ($question->options
->subquestions
as $subquestion) {
669 $urls +
= question_find_file_links_from_html($subquestion->questiontext
, $courseid);
672 //set all the values of the array to the question object
674 $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id
)));
677 $urls = array_merge_recursive($urls, parent
::find_file_links($question, $courseid));
682 function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
683 parent
::replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination);
684 // replace links in the question_match_sub table.
685 if (isset($question->options
->subquestions
)){
686 foreach ($question->options
->subquestions
as $subquestion) {
687 $subquestionchanged = false;
688 $subquestion->questiontext
= question_replace_file_links_in_html($subquestion->questiontext
, $fromcourseid, $tocourseid, $url, $destination, $subquestionchanged);
689 if ($subquestionchanged){//need to update rec in db
690 if (!update_record('question_match_sub', addslashes_recursive($subquestion))) {
691 error('Couldn\'t update \'question_match_sub\' record '.$subquestion->id
);
700 * Runs all the code required to set up and save an essay question for testing purposes.
701 * Alternate DB table prefix may be used to facilitate data deletion.
703 function generate_test($name, $courseid = null) {
704 list($form, $question) = parent
::generate_test($name, $courseid);
705 $form->shuffleanswers
= 1;
706 $form->noanswers
= 3;
707 $form->subquestions
= array('cat', 'dog', 'cow');
708 $form->subanswers
= array('feline', 'canine', 'bovine');
711 $course = get_record('course', 'id', $courseid);
714 return $this->save_question($question, $form, $course);
717 //// END OF CLASS ////
719 //////////////////////////////////////////////////////////////////////////
720 //// INITIATION - Without this line the question type is not in use... ///
721 //////////////////////////////////////////////////////////////////////////
722 question_register_questiontype(new question_match_qtype());