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) { |
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', |
516cf3eb |
114 | 'question', $question->id)) { |
115 | notify('Error: Missing subquestions!'); |
116 | return false; |
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 |
129 | ->answers[$subquestion->code] = clone($answer); |
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) { |
181 | // Serialize responses |
182 | $responses = array(); |
183 | foreach ($state->options->subquestions as $key => $subquestion) { |
0c24ee0f |
184 | if ($subquestion->questiontext) { |
185 | $responses[] = $key.'-'.($state->responses[$key] ? $state->responses[$key] : 0); |
186 | } |
516cf3eb |
187 | } |
188 | $responses = implode(',', $responses); |
189 | |
190 | // Set the legacy answer field |
0c24ee0f |
191 | if (!set_field('question_states', 'answer', $responses, 'id', $state->id)) { |
516cf3eb |
192 | return false; |
193 | } |
194 | return true; |
195 | } |
196 | |
197 | function get_correct_responses(&$question, &$state) { |
198 | $responses = array(); |
199 | foreach ($state->options->subquestions as $sub) { |
200 | foreach ($sub->options->answers as $answer) { |
a58ffe3f |
201 | if (1 == $answer->fraction && $sub->questiontext) { |
516cf3eb |
202 | $responses[$sub->id] = $answer->id; |
203 | } |
204 | } |
205 | } |
206 | return empty($responses) ? null : $responses; |
207 | } |
208 | |
209 | function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) { |
37a12367 |
210 | global $CFG; |
516cf3eb |
211 | $subquestions = $state->options->subquestions; |
212 | $correctanswers = $this->get_correct_responses($question, $state); |
213 | $nameprefix = $question->name_prefix; |
214 | $answers = array(); |
215 | $responses = &$state->responses; |
216 | |
a58ffe3f |
217 | $formatoptions = new stdClass; |
7347c60b |
218 | $formatoptions->noclean = true; |
219 | $formatoptions->para = false; |
516cf3eb |
220 | |
221 | foreach ($subquestions as $subquestion) { |
222 | foreach ($subquestion->options->answers as $ans) { |
223 | $answers[$ans->id] = $ans->answer; |
224 | } |
225 | } |
226 | |
227 | // Shuffle the answers |
228 | $answers = draw_rand_array($answers, count($answers)); |
229 | |
37a12367 |
230 | // Print formulation |
231 | $questiontext = format_text($question->questiontext, |
232 | $question->questiontextformat, |
7347c60b |
233 | $formatoptions, $cmoptions->course); |
37a12367 |
234 | $image = get_question_image($question, $cmoptions->course); |
516cf3eb |
235 | |
236 | ///// Print the input controls ////// |
516cf3eb |
237 | foreach ($subquestions as $key => $subquestion) { |
37a12367 |
238 | |
a58ffe3f |
239 | if ($subquestion->questiontext) { |
240 | /// Subquestion text: |
241 | $a = new stdClass; |
242 | $a->text = format_text($subquestion->questiontext, |
243 | $question->questiontextformat, $formatoptions, $cmoptions->course); |
244 | |
245 | /// Drop-down list: |
246 | $menuname = $nameprefix.$subquestion->id; |
247 | $response = isset($state->responses[$subquestion->id]) |
248 | ? $state->responses[$subquestion->id] : '0'; |
249 | if ($options->readonly |
250 | and $options->correct_responses |
251 | and isset($correctanswers[$subquestion->id]) |
252 | and ($correctanswers[$subquestion->id] == $response)) { |
253 | $a->class = ' highlight '; |
254 | } else { |
255 | $a->class = ''; |
256 | } |
257 | |
258 | $a->control = choose_from_menu($answers, $menuname, $response, 'choose', '', 0, |
259 | true, $options->readonly); |
260 | |
261 | // Neither the editing interface or the database allow to provide |
262 | // fedback for this question type. |
263 | // However (as was pointed out in bug bug 3294) the randomsamatch |
264 | // type which reuses this method can have feedback defined for |
265 | // the wrapped shortanswer questions. |
266 | //if ($options->feedback |
267 | // && !empty($subquestion->options->answers[$responses[$key]]->feedback)) { |
268 | // print_comment($subquestion->options->answers[$responses[$key]]->feedback); |
269 | //} |
270 | |
271 | $anss[] = $a; |
516cf3eb |
272 | } |
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; |
a58ffe3f |
282 | $totalgrade = 0; |
516cf3eb |
283 | foreach ($subquestions as $key => $sub) { |
a58ffe3f |
284 | if ($sub->questiontext) { |
285 | $totalgrade += 1; |
286 | if (isset($sub->options->answers[$responses[$key]])) { |
287 | $sumgrade += $sub->options->answers[$responses[$key]]->fraction; |
288 | } |
516cf3eb |
289 | } |
290 | } |
291 | |
a58ffe3f |
292 | $state->raw_grade = $sumgrade/$totalgrade; |
516cf3eb |
293 | if (empty($state->raw_grade)) { |
294 | $state->raw_grade = 0; |
295 | } |
296 | |
297 | // Make sure we don't assign negative or too high marks |
298 | $state->raw_grade = min(max((float) $state->raw_grade, |
299 | 0.0), 1.0) * $question->maxgrade; |
300 | $state->penalty = $question->penalty * $question->maxgrade; |
301 | |
f30bbcaf |
302 | // mark the state as graded |
303 | $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE; |
304 | |
516cf3eb |
305 | return true; |
306 | } |
307 | |
308 | // ULPGC ecastro for stats report |
309 | function get_all_responses($question, $state) { |
974383f9 |
310 | $answers = array(); |
516cf3eb |
311 | if (is_array($question->options->subquestions)) { |
645d7610 |
312 | foreach ($question->options->subquestions as $aid => $answer) { |
313 | if ($answer->questiontext) { |
314 | $r = new stdClass; |
315 | $r->answer = $answer->questiontext . ": " . $answer->answertext; |
316 | $r->credit = 1; |
317 | $answers[$aid] = $r; |
318 | } |
516cf3eb |
319 | } |
516cf3eb |
320 | } |
a58ffe3f |
321 | $result = new stdClass; |
516cf3eb |
322 | $result->id = $question->id; |
323 | $result->responses = $answers; |
324 | return $result; |
325 | } |
326 | |
327 | // ULPGC ecastro |
328 | function get_actual_response($question, $state) { |
01bd54e0 |
329 | $subquestions = &$state->options->subquestions; |
330 | $responses = &$state->responses; |
331 | $results=array(); |
332 | foreach ($subquestions as $key => $sub) { |
333 | foreach ($responses as $ind => $code) { |
334 | if (isset($sub->options->answers[$code])) { |
645d7610 |
335 | $results[$ind] = $subquestions[$ind]->questiontext . ": " . $sub->options->answers[$code]->answer; |
01bd54e0 |
336 | } |
337 | } |
338 | } |
339 | return $results; |
340 | } |
0a5b58af |
341 | |
342 | function response_summary($question, $state, $length=80) { |
343 | // This should almost certainly be overridden |
755bddf1 |
344 | return substr(implode(', ', $this->get_actual_response($question, $state)), 0, $length); |
0a5b58af |
345 | } |
c5d94c41 |
346 | |
347 | /// BACKUP FUNCTIONS //////////////////////////// |
348 | |
349 | /* |
350 | * Backup the data in the question |
351 | * |
352 | * This is used in question/backuplib.php |
353 | */ |
354 | function backup($bf,$preferences,$question,$level=6) { |
355 | |
356 | $status = true; |
357 | |
358 | $matchs = get_records("question_match_sub","question",$question,"id"); |
359 | //If there are matchs |
360 | if ($matchs) { |
361 | $status = fwrite ($bf,start_tag("MATCHS",6,true)); |
362 | //Iterate over each match |
363 | foreach ($matchs as $match) { |
364 | $status = fwrite ($bf,start_tag("MATCH",7,true)); |
365 | //Print match contents |
366 | fwrite ($bf,full_tag("ID",8,false,$match->id)); |
367 | fwrite ($bf,full_tag("CODE",8,false,$match->code)); |
368 | fwrite ($bf,full_tag("QUESTIONTEXT",8,false,$match->questiontext)); |
369 | fwrite ($bf,full_tag("ANSWERTEXT",8,false,$match->answertext)); |
370 | $status = fwrite ($bf,end_tag("MATCH",7,true)); |
371 | } |
372 | $status = fwrite ($bf,end_tag("MATCHS",6,true)); |
373 | } |
374 | return $status; |
375 | } |
516cf3eb |
376 | |
315559d3 |
377 | /// RESTORE FUNCTIONS ///////////////// |
378 | |
379 | /* |
380 | * Restores the data in the question |
381 | * |
382 | * This is used in question/restorelib.php |
383 | */ |
384 | function restore($old_question_id,$new_question_id,$info,$restore) { |
385 | |
386 | $status = true; |
387 | |
388 | //Get the matchs array |
389 | $matchs = $info['#']['MATCHS']['0']['#']['MATCH']; |
390 | |
391 | //We have to build the subquestions field (a list of match_sub id) |
392 | $subquestions_field = ""; |
393 | $in_first = true; |
394 | |
395 | //Iterate over matchs |
396 | for($i = 0; $i < sizeof($matchs); $i++) { |
397 | $mat_info = $matchs[$i]; |
398 | |
399 | //We'll need this later!! |
400 | $oldid = backup_todb($mat_info['#']['ID']['0']['#']); |
401 | |
402 | //Now, build the question_match_SUB record structure |
a58ffe3f |
403 | $match_sub = new stdClass; |
315559d3 |
404 | $match_sub->question = $new_question_id; |
405 | $match_sub->code = backup_todb($mat_info['#']['CODE']['0']['#']); |
406 | if (!$match_sub->code) { |
407 | $match_sub->code = $oldid; |
408 | } |
409 | $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']); |
410 | $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']); |
411 | |
412 | //The structure is equal to the db, so insert the question_match_sub |
413 | $newid = insert_record ("question_match_sub",$match_sub); |
414 | |
415 | //Do some output |
416 | if (($i+1) % 50 == 0) { |
417 | if (!defined('RESTORE_SILENTLY')) { |
418 | echo "."; |
419 | if (($i+1) % 1000 == 0) { |
420 | echo "<br />"; |
421 | } |
422 | } |
423 | backup_flush(300); |
424 | } |
425 | |
426 | if ($newid) { |
427 | //We have the newid, update backup_ids |
428 | backup_putid($restore->backup_unique_code,"question_match_sub",$oldid, |
429 | $newid); |
430 | //We have a new match_sub, append it to subquestions_field |
431 | if ($in_first) { |
432 | $subquestions_field .= $newid; |
433 | $in_first = false; |
434 | } else { |
435 | $subquestions_field .= ",".$newid; |
436 | } |
437 | } else { |
438 | $status = false; |
439 | } |
440 | } |
441 | |
442 | //We have created every match_sub, now create the match |
443 | $match->question = $new_question_id; |
444 | $match->subquestions = $subquestions_field; |
445 | |
446 | //The structure is equal to the db, so insert the question_match_sub |
447 | $newid = insert_record ("question_match",$match); |
448 | |
449 | if (!$newid) { |
450 | $status = false; |
451 | } |
452 | |
453 | return $status; |
454 | } |
455 | |
456 | function restore_map($old_question_id,$new_question_id,$info,$restore) { |
457 | |
458 | $status = true; |
459 | |
460 | //Get the matchs array |
461 | $matchs = $info['#']['MATCHS']['0']['#']['MATCH']; |
462 | |
463 | //We have to build the subquestions field (a list of match_sub id) |
464 | $subquestions_field = ""; |
465 | $in_first = true; |
466 | |
467 | //Iterate over matchs |
468 | for($i = 0; $i < sizeof($matchs); $i++) { |
469 | $mat_info = $matchs[$i]; |
470 | |
471 | //We'll need this later!! |
472 | $oldid = backup_todb($mat_info['#']['ID']['0']['#']); |
473 | |
474 | //Now, build the question_match_SUB record structure |
475 | $match_sub->question = $new_question_id; |
476 | $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']); |
477 | $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']); |
478 | |
479 | //If we are in this method is because the question exists in DB, so its |
480 | //match_sub must exist too. |
481 | //Now, we are going to look for that match_sub in DB and to create the |
482 | //mappings in backup_ids to use them later where restoring states (user level). |
483 | |
484 | //Get the match_sub from DB (by question, questiontext and answertext) |
485 | $db_match_sub = get_record ("question_match_sub","question",$new_question_id, |
486 | "questiontext",$match_sub->questiontext, |
487 | "answertext",$match_sub->answertext); |
488 | //Do some output |
489 | if (($i+1) % 50 == 0) { |
490 | if (!defined('RESTORE_SILENTLY')) { |
491 | echo "."; |
492 | if (($i+1) % 1000 == 0) { |
493 | echo "<br />"; |
494 | } |
495 | } |
496 | backup_flush(300); |
497 | } |
498 | |
499 | //We have the database match_sub, so update backup_ids |
500 | if ($db_match_sub) { |
501 | //We have the newid, update backup_ids |
502 | backup_putid($restore->backup_unique_code,"question_match_sub",$oldid, |
503 | $db_match_sub->id); |
504 | } else { |
505 | $status = false; |
506 | } |
507 | } |
508 | |
509 | return $status; |
510 | } |
511 | |
512 | function restore_recode_answer($state, $restore) { |
513 | |
514 | //The answer is a comma separated list of hypen separated math_subs (for question and answer) |
515 | $answer_field = ""; |
516 | $in_first = true; |
517 | $tok = strtok($state->answer,","); |
518 | while ($tok) { |
519 | //Extract the match_sub for the question and the answer |
520 | $exploded = explode("-",$tok); |
521 | $match_question_id = $exploded[0]; |
522 | $match_answer_code = $exploded[1]; |
523 | //Get the match_sub from backup_ids (for the question) |
524 | if (!$match_que = backup_getid($restore->backup_unique_code,"question_match_sub",$match_question_id)) { |
525 | echo 'Could not recode question_match_sub '.$match_question_id.'<br />'; |
526 | } |
527 | if ($in_first) { |
528 | $answer_field .= $match_que->new_id."-".$match_answer_code; |
529 | $in_first = false; |
530 | } else { |
531 | $answer_field .= ",".$match_que->new_id."-".$match_answer_code; |
532 | } |
533 | //check for next |
534 | $tok = strtok(","); |
535 | } |
536 | return $answer_field; |
537 | } |
538 | |
516cf3eb |
539 | } |
540 | //// END OF CLASS //// |
541 | |
542 | ////////////////////////////////////////////////////////////////////////// |
543 | //// INITIATION - Without this line the question type is not in use... /// |
544 | ////////////////////////////////////////////////////////////////////////// |
dfa47f96 |
545 | $QTYPES['match']= new question_match_qtype(); |
ccccf04f |
546 | // The following adds the questiontype to the menu of types shown to teachers |
dfa47f96 |
547 | $QTYPE_MENU['match'] = get_string("match", "quiz"); |
516cf3eb |
548 | |
549 | ?> |