Moving quiz-independent question scripts to their new location. In a following commit...
[moodle.git] / question / questiontypes / numerical / questiontype.php
CommitLineData
516cf3eb 1<?php // $Id$
2
3/////////////////
4/// NUMERICAL ///
5/////////////////
6
7/// QUESTION TYPE CLASS //////////////////
8
9///
10/// This class contains some special features in order to make the
11/// question type embeddable within a multianswer (cloze) question
12///
13
14/// This question type behaves like shortanswer in most cases.
15/// Therefore, it extends the shortanswer question type...
16
17require_once("$CFG->dirroot/mod/quiz/questiontypes/shortanswer/questiontype.php");
18
19class quiz_numerical_qtype extends quiz_shortanswer_qtype {
20
21 function name() {
22 return 'numerical';
23 }
24
25 function get_question_options(&$question) {
26 // Get the question answers and their respective tolerances
27 // Note: quiz_numerical is an extension of the answer table rather than
28 // the quiz_questions table as is usually the case for qtype
29 // specific tables.
30 global $CFG;
31 if (!$question->options->answers = get_records_sql(
32 "SELECT a.*, n.tolerance " .
33 "FROM {$CFG->prefix}quiz_answers a, " .
34 " {$CFG->prefix}quiz_numerical n " .
35 "WHERE a.question = $question->id " .
36 "AND a.id = n.answer;")) {
37 notify('Error: Missing question answer!');
38 return false;
39 }
40 $this->get_numerical_units($question);
41
42 // If units are defined we strip off the defaultunit from the answer, if
43 // it is present. (Required for compatibility with the old code and DB).
44 if ($defaultunit = $this->get_default_numerical_unit($question)) {
45 foreach($question->options->answers as $key => $val) {
46 $answer = trim($val->answer);
47 $length = strlen($defaultunit->unit);
48 if (substr($answer, -$length) == $defaultunit->unit) {
49 $question->options->answers[$key]->answer =
50 substr($answer, 0, strlen($answer)-$length);
51 }
52 }
53 }
54 return true;
55 }
56
57 function get_numerical_units(&$question) {
58 if ($question->options->units = get_records('quiz_numerical_units',
59 'question', $question->id, 'id ASC')) {
60 $question->options->units = array_values($question->options->units);
61 usort($question->options->units, create_function('$a, $b', // make sure the default unit is at index 0
62 'if (1.0 === (float)$a->multiplier) { return -1; } else '.
63 'if (1.0 === (float)$b->multiplier) { return 1; } else { return 0; }'));
64 array_walk($question->options->units, create_function('$val',
65 '$val->multiplier = (float)$val->multiplier;'));
66 } else {
67 $question->options->units = array();
68 }
69 return true;
70 }
71
72 function get_default_numerical_unit(&$question) {
73 $unit = new stdClass;
74 $unit->unit = '';
75 $unit->multiplier = 1.0;
76 if (!isset($question->options->units[0])) {
77 // do nothing
78 } else if (1.0 === (float)$question->options->units[0]->multiplier) {
79 $unit->unit = $question->options->units[0]->unit;
80 } else {
81 foreach ($question->options->units as $u) {
82 if (1.0 === (float)$unit->multiplier) {
83 $unit->unit = $u->unit;
84 break;
85 }
86 }
87 }
88 return $unit;
89 }
90
91 function save_question_options($question) {
92 // save_question_options supports the definition of multiple answers
93 // for numerical questions. This is not currently used by the editing
94 // interface, but the GIFT format supports it. The multianswer qtype,
95 // for example can make use of this feature.
96 // Get old versions of the objects
97 if (!$oldanswers = get_records("quiz_answers", "question", $question->id)) {
98 $oldanswers = array();
99 }
100
101 if (!$oldoptions = get_records("quiz_numerical", "question", $question->id)) {
102 $oldoptions = array();
103 }
104
105 $result = $this->save_numerical_units($question);
106 if (isset($result->error)) {
107 return $result;
108 } else {
109 $units = &$result->units;
110 }
111
112 // Insert all the new answers
113 foreach ($question->answer as $key => $dataanswer) {
114 if ($dataanswer != "") {
115 $answer = new stdClass;
116 $answer->question = $question->id;
117 $answer->answer = trim($dataanswer);
118 $answer->fraction = $question->fraction[$key];
119 $answer->feedback = trim($question->feedback[$key]);
120
121 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
122 $answer->id = $oldanswer->id;
123 if (! update_record("quiz_answers", $answer)) {
124 $result->error = "Could not update quiz answer! (id=$answer->id)";
125 return $result;
126 }
127 } else { // This is a completely new answer
128 if (! $answer->id = insert_record("quiz_answers", $answer)) {
129 $result->error = "Could not insert quiz answer!";
130 return $result;
131 }
132 }
133
134 // Set up the options object
135 if (!$options = array_shift($oldoptions)) {
136 $options = new stdClass;
137 }
138 $options->question = $question->id;
139 $options->answer = $answer->id;
140 $options->tolerance = $this->apply_unit($question->tolerance[$key], $units);
141
142 // Save options
143 if (isset($options->id)) { // reusing existing record
144 if (! update_record('quiz_numerical', $options)) {
145 $result->error = "Could not update quiz numerical options! (id=$options->id)";
146 return $result;
147 }
148 } else { // new options
149 if (! insert_record('quiz_numerical', $options)) {
150 $result->error = "Could not insert quiz numerical options!";
151 return $result;
152 }
153 }
154
155 // delete old answer records
156 if (!empty($oldanswers)) {
157 foreach($oldanswers as $oa) {
158 delete_records('quiz_answers', 'id', $oa->id);
159 }
160 }
161
162 // delete old answer records
163 if (!empty($oldoptions)) {
164 foreach($oldoptions as $oo) {
165 delete_records('quiz_numerical', 'id', $oo->id);
166 }
167 }
168
169 }
170 }
171 }
172
173 function save_numerical_units($question) {
174 if (!$oldunits = get_records("quiz_numerical_units", "question", $question->id)) {
175 $oldunits = array();
176 }
177
178 // Set the units
179 $units = array();
180 $keys = array();
181 $oldunits = array_values($oldunits);
182 usort($oldunits, create_function('$a, $b', // make sure the default unit is at index 0
183 'if (1.0 === (float)$a->multiplier) { return -1; } else '.
184 'if (1.0 === (float)$b->multiplier) { return 1; } else { return 0; }'));
185 foreach ($oldunits as $unit) {
186 $units[] = clone($unit);
187 }
188 $n = isset($question->multiplier) ? count($question->multiplier) : 0;
189 for ($i = 0; $i < $n; $i++) {
190 // Discard any unit which doesn't specify the unit or the multiplier
191 if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])) {
192 $units[$i]->question = $question->id;
193 $units[$i]->multiplier =
194 $this->apply_unit($question->multiplier[$i], array());
195 $units[$i]->unit = $question->unit[$i];
196 } else {
197 unset($units[$i]);
198 }
199 }
200 unset($question->multiplier, $question->unit);
201
202 /// Save units
203 for ($i = 0; $i < $n; $i++) {
204 if (!isset($units[$i]) && isset($oldunits[$i])) { // Delete if it hasn't been resubmitted
205 delete_records('quiz_numerical_units', 'id', $oldunits[$i]->id);
206 } else if ($oldunits != $units) { // answer has changed or is new
207 if (isset($oldunits[$i]->id)) { // answer has changed
208 $units[$i]->id = $oldunits[$i]->id;
209 if (! update_record('quiz_numerical_units', $units[$i])) {
210 $result->error = "Could not update quiz_numerical_unit $units[$i]->unit";
211 return $result;
212 }
213 } else if (isset($units[$i])) { // answer is new
214 if (! insert_record('quiz_numerical_units', $units[$i])) {
215 $result->error = "Unable to insert new unit $units[$i]->unit";
216 return $result;
217 }
218 }
219 }
220 }
221 $result->units = &$units;
222 return $result;
223 }
224
225 /**
226 * Deletes question from the question-type specific tables
227 *
228 * @return boolean Success/Failure
229 * @param object $question The question being deleted
230 */
231 function delete_question($question) {
232 delete_records("quiz_numerical", "question", $question->id);
233 delete_records("quiz_numerical_units", "question", $question->id);
234 return true;
235 }
236
237 function compare_responses(&$question, &$state, &$teststate) {
238 $response = isset($state->responses['']) ? $state->responses[''] : '';
239 $testresponse = isset($teststate->responses[''])
240 ? $teststate->responses[''] : '';
241 return ($response == $testresponse);
242 }
243
244
245
246 // Checks whether a response matches a given answer, taking the tolerance
247 // into account. Returns a true for if a response matches the answer, false
248 // if it doesn't.
249 function test_response(&$question, &$state, $answer) {
250 if (isset($state->responses[''])) {
251 $response = $this->apply_unit(stripslashes($state->responses['']),
252 $question->options->units);
253 } else {
254 $response = '';
255 }
256
257 if (is_numeric($response) && is_numeric($answer->answer)) {
258 $this->get_tolerance_interval($answer);
259 return ($answer->min <= $response && $answer->max >= $response);
260 } else {
261 return ($response == $answer->answer);
262 }
263 }
264
265 // ULPGC ecastro
266 function check_response(&$question, &$state){
267 $answers = &$question->options->answers;
268 foreach($answers as $aid => $answer) {
269 if($this->test_response($question, $state, $answer)) {
270 return $aid;
271 }
272 }
273 return false;
274 }
275
276 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
277 /// This implementation is very similar to the code used by question type SHORTANSWER
278
279 $answers = &$question->options->answers;
280 $correctanswers = $this->get_correct_responses($question, $state);
281 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
282 $nameprefix = $question->name_prefix;
283
284 /// Print question text and media
285
286 echo format_text($question->questiontext,
287 $question->questiontextformat,
288 NULL, $cmoptions->course);
289 quiz_print_possible_question_image($question, $cmoptions->course);
290
291 /// Print input controls
292
293 $stranswer = get_string("answer", "quiz");
294 if (isset($state->responses[''])) {
295 $value = ' value="'.htmlSpecialChars($state->responses['']).'" ';
296 } else {
297 $value = ' value="" ';
298 }
299 $inputname = ' name="'.$nameprefix.'" ';
300 echo "<p align=\"right\">$stranswer: <input type=\"text\" $readonly $inputname size=\"20\" $value /></p>";
301
302 if ($options->feedback) {
303 foreach($answers as $answer) {
304 if($this->test_response($question, $state, $answer)) {
305 quiz_print_comment($answer->feedback);
306 break;
307 }
308 }
309 }
310
311 if ($options->readonly && $options->correct_responses) {
312 $delimiter = '';
313 $correct = '';
314 if ($correctanswers) {
315 foreach ($correctanswers as $correctanswer) {
316 $correct .= $delimiter.$correctanswer;
317 $delimiter = ', ';
318 }
319 }
320 quiz_print_correctanswer($correct);
321 }
322 }
323
324 function grade_responses(&$question, &$state, $cmoptions) {
325 $answers = &$question->options->answers;
326 $state->raw_grade = 0;
327 foreach($answers as $answer) {
328 if($this->test_response($question, $state, $answer)) {
329 if ($state->raw_grade < $answer->fraction) {
330 $state->raw_grade = $answer->fraction;
331 }
332 }
333 }
334 if (empty($state->raw_grade)) {
335 $state->raw_grade = 0;
336 }
337
338 // Make sure we don't assign negative or too high marks
339 $state->raw_grade = min(max((float) $state->raw_grade,
340 0.0), 1.0) * $question->maxgrade;
341 $state->penalty = $question->penalty * $question->maxgrade;
342
343 return true;
344 }
345
346 function get_correct_responses(&$question, &$state) {
347 $correct = parent::get_correct_responses($question, $state);
348 if ($unit = $this->get_default_numerical_unit($question)) {
349 $correct[''] .= ' '.$unit->unit;
350 }
351 return $correct;
352 }
353
354 // ULPGC ecastro
355 function get_all_responses(&$question, &$state) {
356 unset($answers);
357 $unit = $this->get_default_numerical_unit($question);
358 if (is_array($question->options->answers)) {
359 foreach ($question->options->answers as $aid=>$answer) {
360 unset ($r);
361 $r->answer = $answer->answer;
362 $r->credit = $answer->fraction;
363 $this->get_tolerance_interval($answer);
364 if ($unit) {
365 $r->answer .= ' '.$unit->unit;
366 }
367 if ($answer->max != $answer->min) {
368 $max = "$answer->max"; //format_float($answer->max, 2);
369 $min = "$answer->min"; //format_float($answer->max, 2);
370 $r->answer .= ' ('.$min.'..'.$max.')';
371 }
372 $answers[$aid] = $r;
373 }
374 } else {
375 $answers[]="error"; // just for debugging, eliminate
376 }
377 $result->id = $question->id;
378 $result->responses = $answers;
379 return $result;
380 }
381
382 function get_tolerance_interval(&$answer) {
383 // No tolerance
384 if (empty($answer->tolerance)) {
385 $answer->min = $answer->max = $answer->answer;
386 return true;
387 }
388
389 // Calculate the interval of correct responses (min/max)
390 if (!isset($answer->tolerancetype)) {
391 $answer->tolerancetype = 2; // nominal
392 }
393
394 // We need to add a tiny fraction (0.00000000000000001) to make the
395 // comparison work correctly. Otherwise seemingly equal values can yield
396 // false. (fixes bug #3225)
397 $tolerance = (float)$answer->tolerance + 0.00000000000000001;
398 switch ($answer->tolerancetype) {
399 case '1': case 'relative':
400 /// Recalculate the tolerance and fall through
401 /// to the nominal case:
402 $tolerance = $answer->answer * $tolerance;
403 // Falls through to the nominal case -
404 case '2': case 'nominal':
405 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
406 $max = $answer->answer + $tolerance;
407 $min = $answer->answer - $tolerance;
408 break;
409 case '3': case 'geometric':
410 $quotient = 1 + abs($tolerance);
411 $max = $answer->answer * $quotient;
412 $min = $answer->answer / $quotient;
413 break;
414 default:
415 error("Unknown tolerance type $answer->tolerancetype");
416 }
417
418 $answer->min = $min;
419 $answer->max = $max;
420 return true;
421 }
422
423 /**
424 * Checks if the $rawresponse has a unit and applys it if appropriate.
425 *
426 * @param string $rawresponse The response string to be converted to a float.
427 * @param array $units An array with the defined units, where the
428 * unit is the key and the multiplier the value.
429 * @return float The rawresponse with the unit taken into
430 * account as a float.
431 */
432 function apply_unit($rawresponse, $units) {
433 // Make units more useful
434 $tmpunits = array();
435 foreach ($units as $unit) {
436 $tmpunits[$unit->unit] = $unit->multiplier;
437 }
438
439 $search = array(' ', ',');
440 $replace = array('', '.');
441 $rawresponse = str_replace($search, $replace, $rawresponse); // remove spaces
442 if (ereg(
443 '^([+-]?([0-9]+(\\.[0-9]*)?|[.][0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$',
444 $rawresponse, $responseparts)) {
445 $responsenum = (float)$responseparts[1];
446 if (isset($tmpunits[$responseparts[5]])) {
447 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
448 } else {
449 return (float)$responseparts[1];
450 }
451 }
452 return $rawresponse;
453 }
454}
455//// END OF CLASS ////
456
457//////////////////////////////////////////////////////////////////////////
458//// INITIATION - Without this line the question type is not in use... ///
459//////////////////////////////////////////////////////////////////////////
460$QUIZ_QTYPES[NUMERICAL]= new quiz_numerical_qtype();
461
462?>