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