516cf3eb |
1 | <?php // $Id$ |
2 | |
3 | ///////////// |
4 | /// MATCH /// |
5 | ///////////// |
6 | |
7 | /// QUESTION TYPE CLASS ////////////////// |
8 | class quiz_match_qtype extends quiz_default_questiontype { |
9 | |
10 | function name() { |
11 | return 'match'; |
12 | } |
13 | |
14 | function get_question_options(&$question) { |
15 | $question->options = get_record('quiz_match', 'question', $question->id); |
16 | $question->options->subquestions = get_records("quiz_match_sub", "question", $question->id, "id ASC" ); |
17 | return true; |
18 | } |
19 | |
20 | function save_question_options($question) { |
21 | |
22 | if (!$oldsubquestions = get_records("quiz_match_sub", "question", $question->id, "id ASC")) { |
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; |
50 | if (!update_record("quiz_match_sub", $subquestion)) { |
51 | $result->error = "Could not insert quiz match subquestion! (id=$subquestion->id)"; |
52 | return $result; |
53 | } |
54 | } else { |
55 | unset($subquestion); |
56 | // Determine a unique random code |
57 | $subquestion->code = rand(1,999999999); |
58 | while (record_exists('quiz_match_sub', 'code', $subquestion->code)) { |
59 | $subquestion->code = rand(); |
60 | } |
61 | $subquestion->question = $question->id; |
62 | $subquestion->questiontext = $questiontext; |
63 | $subquestion->answertext = $answertext; |
64 | if (!$subquestion->id = insert_record("quiz_match_sub", $subquestion)) { |
65 | $result->error = "Could not insert quiz match subquestion!"; |
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) { |
76 | delete_records('quiz_match_sub', 'id', $os->id); |
77 | } |
78 | } |
79 | |
80 | if (count($subquestions) < 3) { |
81 | $result->noticeyesno = get_string("notenoughsubquestions", "quiz"); |
82 | return $result; |
83 | } |
84 | |
85 | if ($options = get_record("quiz_match", "question", $question->id)) { |
86 | $options->subquestions = implode(",",$subquestions); |
87 | $options->shuffleanswers = $question->shuffleanswers; |
88 | if (!update_record("quiz_match", $options)) { |
89 | $result->error = "Could not update quiz match options! (id=$options->id)"; |
90 | return $result; |
91 | } |
92 | } else { |
93 | unset($options); |
94 | $options->question = $question->id; |
95 | $options->subquestions = implode(",",$subquestions); |
96 | $options->shuffleanswers = $question->shuffleanswers; |
97 | if (!insert_record("quiz_match", $options)) { |
98 | $result->error = "Could not insert quiz match options!"; |
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 | */ |
111 | function delete_question($question) { |
112 | delete_records("quiz_match", "question", $question->id); |
113 | delete_records("quiz_match_sub", "question", $question->id); |
114 | return true; |
115 | } |
116 | |
117 | function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) { |
118 | if (!$state->options->subquestions = get_records('quiz_match_sub', |
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 |
150 | // both refer to the id in the table quiz_match_sub. |
151 | $responses = explode(',', $state->responses['']); |
152 | $responses = array_map(create_function('$val', |
153 | 'return explode("-", $val);'), $responses); |
154 | |
155 | if (!$questions = get_records('quiz_match_sub', |
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 |
194 | if (!set_field('quiz_states', 'answer', $responses, 'id', |
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) { |
214 | $subquestions = $state->options->subquestions; |
215 | $correctanswers = $this->get_correct_responses($question, $state); |
216 | $nameprefix = $question->name_prefix; |
217 | $answers = array(); |
218 | $responses = &$state->responses; |
219 | |
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 | |
230 | // Print question text and possible image |
231 | if (!empty($question->questiontext)) { |
232 | echo format_text($question->questiontext, |
233 | $question->questiontextformat, |
234 | NULL, $cmoptions->course); |
235 | } |
236 | quiz_print_possible_question_image($question, $cmoptions->course); |
237 | |
238 | ///// Print the input controls ////// |
239 | echo '<table border="0" cellpadding="10" align="right">'; |
240 | foreach ($subquestions as $key => $subquestion) { |
241 | /// Subquestion text: |
242 | echo '<tr><td align="left" valign="top">'; |
243 | echo format_text($subquestion->questiontext, |
244 | $question->questiontextformat, NULL, $cmoptions->course); |
245 | echo '</td>'; |
246 | |
247 | /// Drop-down list: |
248 | $menuname = $nameprefix.$subquestion->id; |
249 | $response = isset($state->responses[$subquestion->id]) |
250 | ? $state->responses[$subquestion->id] : '0'; |
251 | if ($options->readonly |
252 | and $options->correct_responses |
253 | and isset($correctanswers[$subquestion->id]) |
254 | and ($correctanswers[$subquestion->id] == $response)) { |
255 | $class = ' class="highlight" '; |
256 | } else { |
257 | $class = ''; |
258 | } |
259 | echo "<td align=\"right\" valign=\"top\" $class>"; |
260 | |
261 | choose_from_menu($answers, $menuname, $response, 'choose', '', 0, |
262 | false, $options->readonly); |
263 | |
264 | // Neither the editing interface or the database allow to provide |
265 | // fedback for this question type. |
266 | // However (as was pointed out in bug bug 3294) the randomsamatch |
267 | // type which reuses this method can have feedback defined for |
268 | // the wrapped shortanswer questions. |
269 | //if ($options->feedback |
270 | // && !empty($subquestion->options->answers[$responses[$key]]->feedback)) { |
271 | // quiz_print_comment($subquestion->options->answers[$responses[$key]]->feedback); |
272 | //} |
273 | echo '</td></tr>'; |
274 | } |
275 | echo '</table>'; |
276 | } |
277 | |
278 | function grade_responses(&$question, &$state, $cmoptions) { |
279 | $subquestions = &$state->options->subquestions; |
280 | $responses = &$state->responses; |
281 | |
282 | $sumgrade = 0; |
283 | foreach ($subquestions as $key => $sub) { |
284 | if (isset($sub->options->answers[$responses[$key]])) { |
285 | $sumgrade += $sub->options->answers[$responses[$key]]->fraction; |
286 | } |
287 | } |
288 | |
289 | $state->raw_grade = $sumgrade/count($subquestions); |
290 | if (empty($state->raw_grade)) { |
291 | $state->raw_grade = 0; |
292 | } |
293 | |
294 | // Make sure we don't assign negative or too high marks |
295 | $state->raw_grade = min(max((float) $state->raw_grade, |
296 | 0.0), 1.0) * $question->maxgrade; |
297 | $state->penalty = $question->penalty * $question->maxgrade; |
298 | |
299 | return true; |
300 | } |
301 | |
302 | // ULPGC ecastro for stats report |
303 | function get_all_responses($question, $state) { |
304 | unset($answers); |
305 | if (is_array($question->options->subquestions)) { |
306 | foreach ($question->options->subquestions as $aid=>$answer) { |
307 | unset ($r); |
308 | $r->answer = $answer->questiontext." : ".$answer->answertext; |
309 | $r->credit = 1; |
310 | $answers[$aid] = $r; |
311 | } |
312 | } else { |
313 | $answers[]="error"; // just for debugging, eliminate |
314 | } |
315 | $result->id = $question->id; |
316 | $result->responses = $answers; |
317 | return $result; |
318 | } |
319 | |
320 | // ULPGC ecastro |
321 | function get_actual_response($question, $state) { |
322 | unset($results); |
323 | if (isset($state->responses)) { |
324 | foreach($state->responses as $left=>$right){ |
325 | $lpair = $question->options->subquestions[$left]->questiontext; |
326 | $rpair = $question->options->subquestions[$right]->answertext; |
327 | $results[$left] = $lpair." : ".$rpair; |
328 | } |
329 | return $results; |
330 | } else { |
331 | return null; |
332 | } |
333 | } |
334 | |
335 | |
336 | } |
337 | //// END OF CLASS //// |
338 | |
339 | ////////////////////////////////////////////////////////////////////////// |
340 | //// INITIATION - Without this line the question type is not in use... /// |
341 | ////////////////////////////////////////////////////////////////////////// |
342 | $QUIZ_QTYPES[MATCH]= new quiz_match_qtype(); |
343 | |
344 | ?> |