Fixed bug in make_mod_upload_directory() vecause of new lang packs
[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
e586cfb4 17require_once("$CFG->dirroot/question/questiontypes/shortanswer/questiontype.php");
516cf3eb 18
32a189d6 19class question_numerical_qtype extends question_shortanswer_qtype {
516cf3eb 20
21 function name() {
22 return 'numerical';
23 }
24
25 function get_question_options(&$question) {
26 // Get the question answers and their respective tolerances
32a189d6 27 // Note: question_numerical is an extension of the answer table rather than
4f48fb42 28 // the question table as is usually the case for qtype
516cf3eb 29 // specific tables.
30 global $CFG;
31 if (!$question->options->answers = get_records_sql(
32 "SELECT a.*, n.tolerance " .
dc1f00de 33 "FROM {$CFG->prefix}question_answers a, " .
32a189d6 34 " {$CFG->prefix}question_numerical n " .
516cf3eb 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) {
32a189d6 58 if ($question->options->units = get_records('question_numerical_units',
516cf3eb 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
dc1f00de 97 if (!$oldanswers = get_records("question_answers", "question", $question->id)) {
516cf3eb 98 $oldanswers = array();
99 }
100
32a189d6 101 if (!$oldoptions = get_records("question_numerical", "question", $question->id)) {
516cf3eb 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;
dc1f00de 123 if (! update_record("question_answers", $answer)) {
516cf3eb 124 $result->error = "Could not update quiz answer! (id=$answer->id)";
125 return $result;
126 }
127 } else { // This is a completely new answer
dc1f00de 128 if (! $answer->id = insert_record("question_answers", $answer)) {
516cf3eb 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
32a189d6 144 if (! update_record('question_numerical', $options)) {
516cf3eb 145 $result->error = "Could not update quiz numerical options! (id=$options->id)";
146 return $result;
147 }
148 } else { // new options
32a189d6 149 if (! insert_record('question_numerical', $options)) {
516cf3eb 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) {
dc1f00de 158 delete_records('question_answers', 'id', $oa->id);
516cf3eb 159 }
160 }
161
162 // delete old answer records
163 if (!empty($oldoptions)) {
164 foreach($oldoptions as $oo) {
32a189d6 165 delete_records('question_numerical', 'id', $oo->id);
516cf3eb 166 }
167 }
168
169 }
170 }
171 }
172
173 function save_numerical_units($question) {
32a189d6 174 if (!$oldunits = get_records("question_numerical_units", "question", $question->id)) {
516cf3eb 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
32a189d6 205 delete_records('question_numerical_units', 'id', $oldunits[$i]->id);
516cf3eb 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;
32a189d6 209 if (! update_record('question_numerical_units', $units[$i])) {
210 $result->error = "Could not update question_numerical_unit $units[$i]->unit";
516cf3eb 211 return $result;
212 }
213 } else if (isset($units[$i])) { // answer is new
32a189d6 214 if (! insert_record('question_numerical_units', $units[$i])) {
516cf3eb 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 */
90c3f310 231 function delete_question($questionid) {
232 delete_records("question_numerical", "question", $questionid);
233 delete_records("question_numerical_units", "question", $questionid);
516cf3eb 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
516cf3eb 276 function grade_responses(&$question, &$state, $cmoptions) {
277 $answers = &$question->options->answers;
278 $state->raw_grade = 0;
279 foreach($answers as $answer) {
280 if($this->test_response($question, $state, $answer)) {
281 if ($state->raw_grade < $answer->fraction) {
282 $state->raw_grade = $answer->fraction;
283 }
284 }
285 }
286 if (empty($state->raw_grade)) {
287 $state->raw_grade = 0;
288 }
289
290 // Make sure we don't assign negative or too high marks
291 $state->raw_grade = min(max((float) $state->raw_grade,
292 0.0), 1.0) * $question->maxgrade;
293 $state->penalty = $question->penalty * $question->maxgrade;
294
f30bbcaf 295 // mark the state as graded
296 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
297
516cf3eb 298 return true;
299 }
300
301 function get_correct_responses(&$question, &$state) {
302 $correct = parent::get_correct_responses($question, $state);
303 if ($unit = $this->get_default_numerical_unit($question)) {
304 $correct[''] .= ' '.$unit->unit;
305 }
306 return $correct;
307 }
308
309 // ULPGC ecastro
310 function get_all_responses(&$question, &$state) {
311 unset($answers);
312 $unit = $this->get_default_numerical_unit($question);
313 if (is_array($question->options->answers)) {
314 foreach ($question->options->answers as $aid=>$answer) {
315 unset ($r);
316 $r->answer = $answer->answer;
317 $r->credit = $answer->fraction;
318 $this->get_tolerance_interval($answer);
319 if ($unit) {
320 $r->answer .= ' '.$unit->unit;
321 }
322 if ($answer->max != $answer->min) {
323 $max = "$answer->max"; //format_float($answer->max, 2);
324 $min = "$answer->min"; //format_float($answer->max, 2);
325 $r->answer .= ' ('.$min.'..'.$max.')';
326 }
327 $answers[$aid] = $r;
328 }
329 } else {
330 $answers[]="error"; // just for debugging, eliminate
331 }
332 $result->id = $question->id;
333 $result->responses = $answers;
334 return $result;
335 }
336
337 function get_tolerance_interval(&$answer) {
338 // No tolerance
339 if (empty($answer->tolerance)) {
340 $answer->min = $answer->max = $answer->answer;
341 return true;
342 }
343
344 // Calculate the interval of correct responses (min/max)
345 if (!isset($answer->tolerancetype)) {
346 $answer->tolerancetype = 2; // nominal
347 }
348
349 // We need to add a tiny fraction (0.00000000000000001) to make the
350 // comparison work correctly. Otherwise seemingly equal values can yield
351 // false. (fixes bug #3225)
352 $tolerance = (float)$answer->tolerance + 0.00000000000000001;
353 switch ($answer->tolerancetype) {
354 case '1': case 'relative':
355 /// Recalculate the tolerance and fall through
356 /// to the nominal case:
357 $tolerance = $answer->answer * $tolerance;
358 // Falls through to the nominal case -
359 case '2': case 'nominal':
360 $tolerance = abs($tolerance); // important - otherwise min and max are swapped
361 $max = $answer->answer + $tolerance;
362 $min = $answer->answer - $tolerance;
363 break;
364 case '3': case 'geometric':
365 $quotient = 1 + abs($tolerance);
366 $max = $answer->answer * $quotient;
367 $min = $answer->answer / $quotient;
368 break;
369 default:
370 error("Unknown tolerance type $answer->tolerancetype");
371 }
372
373 $answer->min = $min;
374 $answer->max = $max;
375 return true;
376 }
377
378 /**
379 * Checks if the $rawresponse has a unit and applys it if appropriate.
380 *
381 * @param string $rawresponse The response string to be converted to a float.
382 * @param array $units An array with the defined units, where the
383 * unit is the key and the multiplier the value.
384 * @return float The rawresponse with the unit taken into
385 * account as a float.
386 */
387 function apply_unit($rawresponse, $units) {
388 // Make units more useful
389 $tmpunits = array();
390 foreach ($units as $unit) {
391 $tmpunits[$unit->unit] = $unit->multiplier;
392 }
393
394 $search = array(' ', ',');
395 $replace = array('', '.');
396 $rawresponse = str_replace($search, $replace, $rawresponse); // remove spaces
397 if (ereg(
398 '^([+-]?([0-9]+(\\.[0-9]*)?|[.][0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$',
399 $rawresponse, $responseparts)) {
400 $responsenum = (float)$responseparts[1];
401 if (isset($tmpunits[$responseparts[5]])) {
402 return (float)$responseparts[1] / $tmpunits[$responseparts[5]];
403 } else {
404 return (float)$responseparts[1];
405 }
406 }
407 return $rawresponse;
408 }
409}
410//// END OF CLASS ////
411
412//////////////////////////////////////////////////////////////////////////
413//// INITIATION - Without this line the question type is not in use... ///
414//////////////////////////////////////////////////////////////////////////
ccccf04f 415// define("NUMERICAL", "8"); // already defined in questionlib.php
32a189d6 416$QTYPES[NUMERICAL]= new question_numerical_qtype();
ccccf04f 417// The following adds the questiontype to the menu of types shown to teachers
418$QTYPE_MENU[NUMERICAL] = get_string("numerical", "quiz");
516cf3eb 419
420?>