MDL-27411 upgrade the calculatedsimple qtype to the new question engine.
[moodle.git] / question / type / calculatedmulti / questiontype.php
CommitLineData
2d279432 1<?php
fe6ce234
DC
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
d3603157
TH
17/**
18 * Question type class for the calculated multiple-choice question type.
19 *
20 * @package qtype
21 * @subpackage calculatedmulti
22 * @copyright 2009 Pierre Pichet
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26
a17b297d
TH
27defined('MOODLE_INTERNAL') || die();
28
29
d3603157
TH
30/**
31 * The calculated multiple-choice question type.
32 *
33 * @copyright 2009 Pierre Pichet
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
29b68914 36class qtype_calculatedmulti extends qtype_calculated {
f5382dd2 37
2d279432 38 // Used by the function custom_generator_tools:
fe6ce234 39 public $calcgenerateidhasbeenadded = false;
2d279432
PP
40 public $virtualqtype = false;
41
29b68914
TH
42 public function requires_qtypes() {
43 return array('calculated', 'multichoice');
2d279432 44 }
2d279432 45
29b68914
TH
46 public function save_question_options($question) {
47 global $CFG, $DB;
fe6ce234 48 $context = $question->context;
2d279432
PP
49 if (isset($question->answer) && !isset($question->answers)) {
50 $question->answers = $question->answer;
51 }
52 // calculated options
29b68914
TH
53 $update = true;
54 $options = $DB->get_record('question_calculated_options',
55 array('question' => $question->id));
2d279432
PP
56 if (!$options) {
57 $update = false;
0ff4bd08 58 $options = new stdClass();
2d279432
PP
59 $options->question = $question->id;
60 }
61 $options->synchronize = $question->synchronize;
2d279432
PP
62 $options->single = $question->single;
63 $options->answernumbering = $question->answernumbering;
64 $options->shuffleanswers = $question->shuffleanswers;
fe6ce234
DC
65
66 // save question feedback files
67 foreach (array('correct', 'partiallycorrect', 'incorrect') as $feedbacktype) {
68 $feedbackname = $feedbacktype . 'feedback';
69 $feedbackformat = $feedbackname . 'format';
70 $feedback = $question->$feedbackname;
71 $options->$feedbackformat = $feedback['format'];
cde2709a
DC
72 if (isset($feedback['files'])) {
73 $options->$feedbackname = trim($feedback['text']);
74 $files = $feedback['files'];
75 foreach ($files as $file) {
29b68914
TH
76 $this->import_file($question->context, 'qtype_calculatedmulti',
77 $feedbackname, $question->id, $file);
cde2709a
DC
78 }
79 } else {
29b68914
TH
80 $options->$feedbackname = file_save_draft_area_files($feedback['itemid'],
81 $context->id, 'qtype_calculatedmulti', $feedbackname,
82 $question->id, $this->fileoptionsa, trim($feedback['text']));
cde2709a 83 }
fe6ce234
DC
84 }
85
2d279432 86 if ($update) {
29b68914 87 $DB->update_record('question_calculated_options', $options);
2d279432 88 } else {
29b68914 89 $DB->insert_record('question_calculated_options', $options);
2d279432
PP
90 }
91
92 // Get old versions of the objects
29b68914
TH
93 if (!$oldanswers = $DB->get_records('question_answers',
94 array('question' => $question->id), 'id ASC')) {
2d279432
PP
95 $oldanswers = array();
96 }
97
29b68914
TH
98 if (!$oldoptions = $DB->get_records('question_calculated',
99 array('question' => $question->id), 'answer ASC')) {
2d279432
PP
100 $oldoptions = array();
101 }
102
103 // Save the units.
fe6ce234 104 $virtualqtype = $this->get_virtual_qtype($question);
2d279432
PP
105 if (isset($result->error)) {
106 return $result;
107 } else {
108 $units = &$result->units;
109 }
110 // Insert all the new answers
111 if (isset($question->answer) && !isset($question->answers)) {
fe6ce234 112 $question->answers = $question->answer;
2d279432
PP
113 }
114 foreach ($question->answers as $key => $dataanswer) {
cde2709a
DC
115 if (is_array($dataanswer)) {
116 $dataanswer = $dataanswer['text'];
117 }
29b68914 118 if (trim($dataanswer) != '') {
0ff4bd08 119 $answer = new stdClass();
2d279432
PP
120 $answer->question = $question->id;
121 $answer->answer = trim($dataanswer);
122 $answer->fraction = $question->fraction[$key];
fe6ce234
DC
123 $answer->feedback = trim($question->feedback[$key]['text']);
124 $answer->feedbackformat = $question->feedback[$key]['format'];
cde2709a
DC
125 if (isset($question->feedback[$key]['files'])) {
126 $files = $question->feedback[$key]['files'];
127 }
2d279432 128
29b68914
TH
129 if ($oldanswer = array_shift($oldanswers)) {
130 // Existing answer, so reuse it
2d279432 131 $answer->id = $oldanswer->id;
29b68914
TH
132 $answer->feedback = file_save_draft_area_files(
133 $question->feedback[$key]['itemid'], $context->id, 'question',
134 'answerfeedback', $answer->id, $this->fileoptionsa, $answer->feedback);
135 $DB->update_record('question_answers', $answer);
136 } else {
137 // This is a completely new answer
138 $answer->id = $DB->insert_record('question_answers', $answer);
cde2709a
DC
139 if (isset($files)) {
140 $feedbacktext = $answer->feedback;
141 foreach ($files as $file) {
29b68914
TH
142 $this->import_file($context, 'question', 'answerfeedback',
143 $answer->id, $file);
cde2709a
DC
144 }
145 } else {
29b68914
TH
146 $feedbacktext = file_save_draft_area_files(
147 $question->feedback[$key]['itemid'], $context->id,
148 'question', 'answerfeedback', $answer->id,
149 $this->fileoptionsa, $answer->feedback);
cde2709a 150 }
29b68914
TH
151 $DB->set_field('question_answers', 'feedback', $feedbacktext,
152 array('id'=>$answer->id));
2d279432
PP
153 }
154
155 // Set up the options object
156 if (!$options = array_shift($oldoptions)) {
0ff4bd08 157 $options = new stdClass();
2d279432
PP
158 }
159 $options->question = $question->id;
160 $options->answer = $answer->id;
161 $options->tolerance = trim($question->tolerance[$key]);
162 $options->tolerancetype = trim($question->tolerancetype[$key]);
163 $options->correctanswerlength = trim($question->correctanswerlength[$key]);
164 $options->correctanswerformat = trim($question->correctanswerformat[$key]);
165
166 // Save options
167 if (isset($options->id)) { // reusing existing record
168 $DB->update_record('question_calculated', $options);
169 } else { // new options
170 $DB->insert_record('question_calculated', $options);
171 }
172 }
173 }
174 // delete old answer records
175 if (!empty($oldanswers)) {
29b68914 176 foreach ($oldanswers as $oa) {
2d279432
PP
177 $DB->delete_records('question_answers', array('id' => $oa->id));
178 }
179 }
180
181 // delete old answer records
182 if (!empty($oldoptions)) {
29b68914 183 foreach ($oldoptions as $oo) {
2d279432
PP
184 $DB->delete_records('question_calculated', array('id' => $oo->id));
185 }
186 }
2d279432 187
29b68914 188 if (isset($question->import_process) && $question->import_process) {
2d279432 189 $this->import_datasets($question);
fe6ce234 190 }
2d279432
PP
191 // Report any problems.
192 if (!empty($result->notice)) {
193 return $result;
194 }
195 return true;
196 }
197
29b68914 198 public function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
2d279432 199 // Find out how many datasets are available
29b68914 200 global $CFG, $DB, $OUTPUT;
fe6ce234
DC
201 $maxnumber = (int)$DB->get_field_sql(
202 "SELECT MIN(a.itemcount)
203 FROM {question_dataset_definitions} a, {question_datasets} b
204 WHERE b.question = ? AND a.id = b.datasetdefinition", array($question->id));
205 if (!$maxnumber) {
2d279432
PP
206 print_error('cannotgetdsforquestion', 'question', '', $question->id);
207 }
fe6ce234
DC
208 $sql = "SELECT i.*
209 FROM {question_datasets} d, {question_dataset_definitions} i
210 WHERE d.question = ? AND d.datasetdefinition = i.id AND i.category != 0";
29b68914
TH
211 if (!$question->options->synchronize || !$records = $DB->get_records_sql($sql,
212 array($question->id))) {
213 $synchronize_calculated = false;
fe6ce234
DC
214 } else {
215 // i.e records is true so test coherence
29b68914
TH
216 $coherence = true;
217 $a = new stdClass();
218 $a->qid = $question->id;
219 $a->qcat = $question->category;
220 foreach ($records as $def) {
221 if ($def->category != $question->category) {
d90b016b 222 $a->name = $def->name;
29b68914
TH
223 $a->sharedcat = $def->category;
224 $coherence = false;
d90b016b
PP
225 break;
226 }
227 }
29b68914
TH
228 if (!$coherence) {
229 echo $OUTPUT->notification(
230 get_string('nocoherencequestionsdatyasetcategory', 'qtype_calculated', $a));
fe6ce234
DC
231 }
232
29b68914 233 $synchronize_calculated = true;
fe6ce234 234 }
2d279432
PP
235
236 // Choose a random dataset
d90b016b 237 // maxnumber sould not be breater than 100
f184c65c
TH
238 if ($maxnumber > qtype_calculated::MAX_DATASET_ITEMS) {
239 $maxnumber = qtype_calculated::MAX_DATASET_ITEMS;
d90b016b 240 }
29b68914 241 if ($synchronize_calculated === false) {
2d279432 242 $state->options->datasetitem = rand(1, $maxnumber);
29b68914
TH
243 } else {
244 $state->options->datasetitem =
245 intval($maxnumber * substr($attempt->timestart, -2) /100);
2d279432 246 if ($state->options->datasetitem < 1) {
29b68914
TH
247 $state->options->datasetitem =1;
248 } else if ($state->options->datasetitem > $maxnumber) {
249 $state->options->datasetitem = $maxnumber;
2d279432 250 }
fe6ce234
DC
251
252 };
2d279432 253 $state->options->dataset =
29b68914 254 $this->pick_question_dataset($question, $state->options->datasetitem);
fe6ce234
DC
255 // create an array of answerids ??? why so complicated ???
256 $answerids = array_values(array_map(create_function('$val',
257 'return $val->id;'), $question->options->answers));
258 // Shuffle the answers if required
259 if (!empty($cmoptions->shuffleanswers) and !empty($question->options->shuffleanswers)) {
260 $answerids = swapshuffle($answerids);
261 }
262 $state->options->order = $answerids;
263 // Create empty responses
264 if ($question->options->single) {
265 $state->responses = array('' => '');
266 } else {
267 $state->responses = array();
268 }
269 return true;
2d279432 270 }
fe6ce234 271
29b68914 272 public function save_session_and_responses(&$question, &$state) {
2d279432 273 global $DB;
29b68914 274 $responses = 'dataset'.$state->options->datasetitem.'-';
2d279432
PP
275 $responses .= implode(',', $state->options->order) . ':';
276 $responses .= implode(',', $state->responses);
fe6ce234
DC
277
278 // Set the legacy answer field
f685e830 279 $DB->set_field('question_states', 'answer', $responses, array('id'=> $state->id));
2d279432
PP
280 return true;
281 }
282
29b68914 283 public function get_default_numerical_unit($question, $virtualqtype) {
fe6ce234 284 $unit = '';
29b68914 285 return $unit;
fe6ce234 286 }
29b68914
TH
287
288 public function grade_responses(&$question, &$state, $cmoptions) {
2d279432
PP
289 // Forward the grading to the virtual qtype
290 // We modify the question to look like a multichoice question
fe6ce234 291 // for grading nothing to do
29b68914
TH
292 $virtualqtype = $this->get_virtual_qtype($question);
293 return $virtualqtype->grade_responses($question, $state, $cmoptions);
2d279432
PP
294 }
295
29b68914 296 public function create_virtual_qtype() {
2d279432 297 global $CFG;
fe6ce234
DC
298 require_once("$CFG->dirroot/question/type/multichoice/questiontype.php");
299 return new question_multichoice_qtype();
2d279432
PP
300 }
301
29b68914 302 public function comment_header($question) {
2d279432
PP
303 $strheader = '';
304 $delimiter = '';
305
306 $answers = $question->options->answers;
307
308 foreach ($answers as $key => $answer) {
309 if (is_string($answer)) {
310 $strheader .= $delimiter.$answer;
311 } else {
312 $strheader .= $delimiter.$answer->answer;
313 }
fe6ce234 314 $delimiter = '<br/>';
2d279432
PP
315 }
316 return $strheader;
317 }
318
29b68914
TH
319 public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
320 $answers, $data, $number) {
2d279432 321 global $DB;
0ff4bd08 322 $comment = new stdClass();
2d279432 323 $comment->stranswers = array();
29b68914 324 $comment->outsidelimit = false;
2d279432 325 $comment->answers = array();
2d279432
PP
326
327 $answers = fullclone($answers);
5e8a85aa
TH
328 $strmin = get_string('min');
329 $strmax = get_string('max');
2d279432
PP
330 $errors = '';
331 $delimiter = ': ';
332 foreach ($answers as $key => $answer) {
fe6ce234
DC
333 $answer->answer = $this->substitute_variables($answer->answer, $data);
334 //evaluate the equations i.e {=5+4)
29b68914
TH
335 $qtext = '';
336 $qtextremaining = $answer->answer;
337 while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
fe6ce234
DC
338 $qtextsplits = explode($regs1[0], $qtextremaining, 2);
339 $qtext =$qtext.$qtextsplits[0];
340 $qtextremaining = $qtextsplits[1];
341 if (empty($regs1[1])) {
342 $str = '';
343 } else {
29b68914
TH
344 if ($formulaerrors = qtype_calculated_find_formula_errors($regs1[1])) {
345 $str=$formulaerrors;
346 } else {
fe6ce234
DC
347 eval('$str = '.$regs1[1].';');
348 }
2d279432 349 }
29b68914 350 $qtext = $qtext.$str;
fe6ce234
DC
351 }
352 $answer->answer = $qtext.$qtextremaining;
353 $comment->stranswers[$key] = $answer->answer;
2d279432
PP
354 }
355 return fullclone($comment);
356 }
357
29b68914
TH
358 public function get_correct_responses1(&$question, &$state) {
359 $virtualqtype = $this->get_vir(al_qtype($question));
360
361 return $virtualqtype->get_correct_responses($question, $state);
2d279432
PP
362 }
363
29b68914
TH
364 public function get_virtual_qtype() {
365 $this->virtualqtype = $QTYPES['multichoice'];
2d279432
PP
366 return $this->virtualqtype;
367 }
368
fe6ce234
DC
369 /**
370 * Runs all the code required to set up and save an essay question for testing purposes.
371 * Alternate DB table prefix may be used to facilitate data deletion.
372 */
29b68914 373 public function generate_test($name, $courseid = null) {
fe6ce234
DC
374 global $DB;
375 list($form, $question) = parent::generate_test($name, $courseid);
376 $form->feedback = 1;
377 $form->multiplier = array(1, 1);
378 $form->shuffleanswers = 1;
379 $form->noanswers = 1;
380 $form->qtype ='calculatedmulti';
381 $question->qtype ='calculatedmulti';
382 $form->answers = array('{a} + {b}');
383 $form->fraction = array(1);
384 $form->tolerance = array(0.01);
385 $form->tolerancetype = array(1);
386 $form->correctanswerlength = array(2);
387 $form->correctanswerformat = array(1);
388 $form->questiontext = "What is {a} + {b}?";
389
390 if ($courseid) {
391 $course = $DB->get_record('course', array('id'=> $courseid));
392 }
393
94dbfb3a 394 $new_question = $this->save_question($question, $form);
fe6ce234
DC
395
396 $dataset_form = new stdClass();
29b68914 397 $dataset_form->nextpageparam['forceregeneration']= 1;
fe6ce234
DC
398 $dataset_form->calcmin = array(1 => 1.0, 2 => 1.0);
399 $dataset_form->calcmax = array(1 => 10.0, 2 => 10.0);
400 $dataset_form->calclength = array(1 => 1, 2 => 1);
29b68914
TH
401 $dataset_form->number = array(1 => 5.4, 2 => 4.9);
402 $dataset_form->itemid = array(1 => '', 2 => '');
fe6ce234 403 $dataset_form->calcdistribution = array(1 => 'uniform', 2 => 'uniform');
29b68914 404 $dataset_form->definition = array(1 => "1-0-a", 2 => "1-0-b");
fe6ce234
DC
405 $dataset_form->nextpageparam = array('forceregeneration' => false);
406 $dataset_form->addbutton = 1;
407 $dataset_form->selectadd = 1;
408 $dataset_form->courseid = $courseid;
409 $dataset_form->cmid = 0;
410 $dataset_form->id = $new_question->id;
411 $this->save_dataset_items($new_question, $dataset_form);
412
413 return $new_question;
414 }
415
29b68914 416 public function move_files($questionid, $oldcontextid, $newcontextid) {
fe6ce234 417 $fs = get_file_storage();
fe6ce234 418
5d548d3e
TH
419 parent::move_files($questionid, $oldcontextid, $newcontextid);
420 $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid, true);
421
422 $fs->move_area_files_to_new_context($oldcontextid,
423 $newcontextid, 'qtype_calculatedmulti', 'correctfeedback', $questionid);
424 $fs->move_area_files_to_new_context($oldcontextid,
425 $newcontextid, 'qtype_calculatedmulti', 'partiallycorrectfeedback', $questionid);
426 $fs->move_area_files_to_new_context($oldcontextid,
427 $newcontextid, 'qtype_calculatedmulti', 'incorrectfeedback', $questionid);
fe6ce234
DC
428 }
429
9203b705
TH
430 protected function delete_files($questionid, $contextid) {
431 $fs = get_file_storage();
432
433 parent::delete_files($questionid, $contextid);
434 $this->delete_files_in_answers($questionid, $contextid, true);
29b68914
TH
435 $fs->delete_area_files($contextid, 'qtype_calculatedmulti',
436 'correctfeedback', $questionid);
437 $fs->delete_area_files($contextid, 'qtype_calculatedmulti',
438 'partiallycorrectfeedback', $questionid);
439 $fs->delete_area_files($contextid, 'qtype_calculatedmulti',
440 'incorrectfeedback', $questionid);
9203b705
TH
441 }
442
29b68914 443 public function check_file_access($question, $state, $options, $contextid, $component,
fe6ce234
DC
444 $filearea, $args) {
445 $itemid = reset($args);
446
447 if (empty($question->maxgrade)) {
448 $question->maxgrade = $question->defaultgrade;
449 }
450
29b68914
TH
451 if (in_array($filearea, array('correctfeedback', 'partiallycorrectfeedback',
452 'incorrectfeedback'))) {
fe6ce234
DC
453 $result = $options->feedback && ($itemid == $question->id);
454 if (!$result) {
455 return false;
456 }
457 if ($state->raw_grade >= $question->maxgrade/1.01) {
458 $feedbacktype = 'correctfeedback';
459 } else if ($state->raw_grade > 0) {
460 $feedbacktype = 'partiallycorrectfeedback';
461 } else {
462 $feedbacktype = 'incorrectfeedback';
463 }
464 if ($feedbacktype != $filearea) {
465 return false;
466 }
467 return true;
468 } else if ($component == 'question' && $filearea == 'answerfeedback') {
29b68914 469 return $options->feedback && array_key_exists($itemid, $question->options->answers);
fe6ce234
DC
470 } else {
471 return parent::check_file_access($question, $state, $options, $contextid, $component,
472 $filearea, $args);
473 }
474 }
2d279432 475}