Moving quiz-independent question scripts to their new location. In a following commit...
[moodle.git] / question / questiontypes / match / questiontype.php
CommitLineData
516cf3eb 1<?php // $Id$
2
3/////////////
4/// MATCH ///
5/////////////
6
7/// QUESTION TYPE CLASS //////////////////
8class 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?>