MDL-11517 reserved word MOD used in table alias in questions backup code
[moodle-pu.git] / question / type / multichoice / questiontype.php
blobddd3622b3863d61bbc1280aa56df10bc46b333a5
1 <?php // $Id$
2 /**
3 * The questiontype class for the multiple choice question type.
5 * Note, This class contains some special features in order to make the
6 * question type embeddable within a multianswer (cloze) question
8 * @package questionbank
9 * @subpackage questiontypes
11 class question_multichoice_qtype extends default_questiontype {
13 function name() {
14 return 'multichoice';
17 function has_html_answers() {
18 return true;
21 function get_question_options(&$question) {
22 // Get additional information from database
23 // and attach it to the question object
24 if (!$question->options = get_record('question_multichoice', 'question',
25 $question->id)) {
26 notify('Error: Missing question options for multichoice question'.$question->id.'!');
27 return false;
30 if (!$question->options->answers = get_records_select('question_answers', 'id IN ('.$question->options->answers.')', 'id')) {
31 notify('Error: Missing question answers for multichoice question'.$question->id.'!');
32 return false;
35 return true;
38 function save_question_options($question) {
39 $result = new stdClass;
40 if (!$oldanswers = get_records("question_answers", "question",
41 $question->id, "id ASC")) {
42 $oldanswers = array();
45 // following hack to check at least two answers exist
46 $answercount = 0;
47 foreach ($question->answer as $key=>$dataanswer) {
48 if ($dataanswer != "") {
49 $answercount++;
52 $answercount += count($oldanswers);
53 if ($answercount < 2) { // check there are at lest 2 answers for multiple choice
54 $result->notice = get_string("notenoughanswers", "qtype_multichoice", "2");
55 return $result;
58 // Insert all the new answers
60 $totalfraction = 0;
61 $maxfraction = -1;
63 $answers = array();
65 foreach ($question->answer as $key => $dataanswer) {
66 if ($dataanswer != "") {
67 if ($answer = array_shift($oldanswers)) { // Existing answer, so reuse it
68 $answer->answer = $dataanswer;
69 $answer->fraction = $question->fraction[$key];
70 $answer->feedback = $question->feedback[$key];
71 if (!update_record("question_answers", $answer)) {
72 $result->error = "Could not update quiz answer! (id=$answer->id)";
73 return $result;
75 } else {
76 unset($answer);
77 $answer->answer = $dataanswer;
78 $answer->question = $question->id;
79 $answer->fraction = $question->fraction[$key];
80 $answer->feedback = $question->feedback[$key];
81 if (!$answer->id = insert_record("question_answers", $answer)) {
82 $result->error = "Could not insert quiz answer! ";
83 return $result;
86 $answers[] = $answer->id;
88 if ($question->fraction[$key] > 0) { // Sanity checks
89 $totalfraction += $question->fraction[$key];
91 if ($question->fraction[$key] > $maxfraction) {
92 $maxfraction = $question->fraction[$key];
97 $update = true;
98 $options = get_record("question_multichoice", "question", $question->id);
99 if (!$options) {
100 $update = false;
101 $options = new stdClass;
102 $options->question = $question->id;
105 $options->answers = implode(",",$answers);
106 $options->single = $question->single;
107 $options->answernumbering = $question->answernumbering;
108 $options->shuffleanswers = $question->shuffleanswers;
109 $options->correctfeedback = trim($question->correctfeedback);
110 $options->partiallycorrectfeedback = trim($question->partiallycorrectfeedback);
111 $options->incorrectfeedback = trim($question->incorrectfeedback);
112 if ($update) {
113 if (!update_record("question_multichoice", $options)) {
114 $result->error = "Could not update quiz multichoice options! (id=$options->id)";
115 return $result;
117 } else {
118 if (!insert_record("question_multichoice", $options)) {
119 $result->error = "Could not insert quiz multichoice options!";
120 return $result;
124 // delete old answer records
125 if (!empty($oldanswers)) {
126 foreach($oldanswers as $oa) {
127 delete_records('question_answers', 'id', $oa->id);
131 /// Perform sanity checks on fractional grades
132 if ($options->single) {
133 if ($maxfraction != 1) {
134 $maxfraction = $maxfraction * 100;
135 $result->noticeyesno = get_string("fractionsnomax", "qtype_multichoice", $maxfraction);
136 return $result;
138 } else {
139 $totalfraction = round($totalfraction,2);
140 if ($totalfraction != 1) {
141 $totalfraction = $totalfraction * 100;
142 $result->noticeyesno = get_string("fractionsaddwrong", "qtype_multichoice", $totalfraction);
143 return $result;
146 return true;
150 * Deletes question from the question-type specific tables
152 * @return boolean Success/Failure
153 * @param object $question The question being deleted
155 function delete_question($questionid) {
156 delete_records("question_multichoice", "question", $questionid);
157 return true;
160 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
161 // create an array of answerids ??? why so complicated ???
162 $answerids = array_values(array_map(create_function('$val',
163 'return $val->id;'), $question->options->answers));
164 // Shuffle the answers if required
165 if ($cmoptions->shuffleanswers and $question->options->shuffleanswers) {
166 $answerids = swapshuffle($answerids);
168 $state->options->order = $answerids;
169 // Create empty responses
170 if ($question->options->single) {
171 $state->responses = array('' => '');
172 } else {
173 $state->responses = array();
175 return true;
179 function restore_session_and_responses(&$question, &$state) {
180 // The serialized format for multiple choice quetsions
181 // is an optional comma separated list of answer ids (the order of the
182 // answers) followed by a colon, followed by another comma separated
183 // list of answer ids, which are the radio/checkboxes that were
184 // ticked.
185 // E.g. 1,3,2,4:2,4 means that the answers were shown in the order
186 // 1, 3, 2 and then 4 and the answers 2 and 4 were checked.
188 $pos = strpos($state->responses[''], ':');
189 if (false === $pos) { // No order of answers is given, so use the default
190 $state->options->order = array_keys($question->options->answers);
191 } else { // Restore the order of the answers
192 $state->options->order = explode(',', substr($state->responses[''], 0, $pos));
193 $state->responses[''] = substr($state->responses[''], $pos + 1);
195 // Restore the responses
196 // This is done in different ways if only a single answer is allowed or
197 // if multiple answers are allowed. For single answers the answer id is
198 // saved in $state->responses[''], whereas for the multiple answers case
199 // the $state->responses array is indexed by the answer ids and the
200 // values are also the answer ids (i.e. key = value).
201 if (empty($state->responses[''])) { // No previous responses
202 $state->responses = array('' => '');
203 } else {
204 if ($question->options->single) {
205 $state->responses = array('' => $state->responses['']);
206 } else {
207 // Get array of answer ids
208 $state->responses = explode(',', $state->responses['']);
209 // Create an array indexed by these answer ids
210 $state->responses = array_flip($state->responses);
211 // Set the value of each element to be equal to the index
212 array_walk($state->responses, create_function('&$a, $b',
213 '$a = $b;'));
216 return true;
219 function save_session_and_responses(&$question, &$state) {
220 // Bundle the answer order and the responses into the legacy answer
221 // field.
222 // The serialized format for multiple choice quetsions
223 // is (optionally) a comma separated list of answer ids
224 // followed by a colon, followed by another comma separated
225 // list of answer ids, which are the radio/checkboxes that were
226 // ticked.
227 // E.g. 1,3,2,4:2,4 means that the answers were shown in the order
228 // 1, 3, 2 and then 4 and the answers 2 and 4 were checked.
229 $responses = implode(',', $state->options->order) . ':';
230 $responses .= implode(',', $state->responses);
232 // Set the legacy answer field
233 if (!set_field('question_states', 'answer', $responses, 'id',
234 $state->id)) {
235 return false;
237 return true;
240 function get_correct_responses(&$question, &$state) {
241 if ($question->options->single) {
242 foreach ($question->options->answers as $answer) {
243 if (((int) $answer->fraction) === 1) {
244 return array('' => $answer->id);
247 return null;
248 } else {
249 $responses = array();
250 foreach ($question->options->answers as $answer) {
251 if (((float) $answer->fraction) > 0.0) {
252 $responses[$answer->id] = (string) $answer->id;
255 return empty($responses) ? null : $responses;
259 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
260 global $CFG;
262 $answers = &$question->options->answers;
263 $correctanswers = $this->get_correct_responses($question, $state);
264 $readonly = empty($options->readonly) ? '' : 'disabled="disabled"';
266 $formatoptions = new stdClass;
267 $formatoptions->noclean = true;
268 $formatoptions->para = false;
270 // Print formulation
271 $questiontext = format_text($question->questiontext,
272 $question->questiontextformat,
273 $formatoptions, $cmoptions->course);
274 $image = get_question_image($question);
275 $answerprompt = ($question->options->single) ? get_string('singleanswer', 'quiz') :
276 get_string('multipleanswers', 'quiz');
278 // Print each answer in a separate row
279 foreach ($state->options->order as $key => $aid) {
280 $answer = &$answers[$aid];
281 $checked = '';
282 $chosen = false;
284 if ($question->options->single) {
285 $type = 'type="radio"';
286 $name = "name=\"{$question->name_prefix}\"";
287 if (isset($state->responses['']) and $aid == $state->responses['']) {
288 $checked = 'checked="checked"';
289 $chosen = true;
291 } else {
292 $type = ' type="checkbox" ';
293 $name = "name=\"{$question->name_prefix}{$aid}\"";
294 if (isset($state->responses[$aid])) {
295 $checked = 'checked="checked"';
296 $chosen = true;
300 $a = new stdClass;
301 $a->id = $question->name_prefix . $aid;
302 $a->class = '';
303 $a->feedbackimg = '';
305 // Print the control
306 $a->control = "<input $readonly id=\"$a->id\" $name $checked $type value=\"$aid\" />";
308 if ($options->correct_responses && $answer->fraction > 0) {
309 $a->class = question_get_feedback_class(1);
311 if (($options->feedback && $chosen) || $options->correct_responses) {
312 $a->feedbackimg = question_get_feedback_image($answer->fraction > 0 ? 1 : 0, $chosen && $options->feedback);
315 // Print the answer text
316 $a->text = $this->number_in_style($key, $question->options->answernumbering) .
317 format_text($answer->answer, FORMAT_MOODLE, $formatoptions, $cmoptions->course);
319 // Print feedback if feedback is on
320 if (($options->feedback || $options->correct_responses) && ($checked || $options->readonly)) {
321 $a->feedback = format_text($answer->feedback, true, $formatoptions, $cmoptions->course);
322 } else {
323 $a->feedback = '';
326 $anss[] = clone($a);
329 $feedback = '';
330 if ($options->feedback) {
331 if ($state->raw_grade >= $question->maxgrade/1.01) {
332 $feedback = $question->options->correctfeedback;
333 } else if ($state->raw_grade > 0) {
334 $feedback = $question->options->partiallycorrectfeedback;
335 } else {
336 $feedback = $question->options->incorrectfeedback;
338 $feedback = format_text($feedback,
339 $question->questiontextformat,
340 $formatoptions, $cmoptions->course);
343 include("$CFG->dirroot/question/type/multichoice/display.html");
346 function grade_responses(&$question, &$state, $cmoptions) {
347 $state->raw_grade = 0;
348 if($question->options->single) {
349 $response = reset($state->responses);
350 if ($response) {
351 $state->raw_grade = $question->options->answers[$response]->fraction;
353 } else {
354 foreach ($state->responses as $response) {
355 if ($response) {
356 $state->raw_grade += $question->options->answers[$response]->fraction;
361 // Make sure we don't assign negative or too high marks
362 $state->raw_grade = min(max((float) $state->raw_grade,
363 0.0), 1.0) * $question->maxgrade;
365 // Apply the penalty for this attempt
366 $state->penalty = $question->penalty * $question->maxgrade;
368 // mark the state as graded
369 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
371 return true;
374 // ULPGC ecastro
375 function get_actual_response($question, $state) {
376 $answers = $question->options->answers;
377 $responses = array();
378 if (!empty($state->responses)) {
379 foreach ($state->responses as $aid =>$rid){
380 if (!empty($answers[$rid])) {
381 $responses[] = $this->format_text($answers[$rid]->answer, $question->questiontextformat);
384 } else {
385 $responses[] = '';
387 return $responses;
390 function response_summary($question, $state, $length = 80) {
391 return implode(',', $this->get_actual_response($question, $state));
394 /// BACKUP FUNCTIONS ////////////////////////////
397 * Backup the data in the question
399 * This is used in question/backuplib.php
401 function backup($bf,$preferences,$question,$level=6) {
403 $status = true;
405 $multichoices = get_records("question_multichoice","question",$question,"id");
406 //If there are multichoices
407 if ($multichoices) {
408 //Iterate over each multichoice
409 foreach ($multichoices as $multichoice) {
410 $status = fwrite ($bf,start_tag("MULTICHOICE",$level,true));
411 //Print multichoice contents
412 fwrite ($bf,full_tag("LAYOUT",$level+1,false,$multichoice->layout));
413 fwrite ($bf,full_tag("ANSWERS",$level+1,false,$multichoice->answers));
414 fwrite ($bf,full_tag("SINGLE",$level+1,false,$multichoice->single));
415 fwrite ($bf,full_tag("SHUFFLEANSWERS",$level+1,false,$multichoice->shuffleanswers));
416 fwrite ($bf,full_tag("CORRECTFEEDBACK",$level+1,false,$multichoice->correctfeedback));
417 fwrite ($bf,full_tag("PARTIALLYCORRECTFEEDBACK",$level+1,false,$multichoice->partiallycorrectfeedback));
418 fwrite ($bf,full_tag("INCORRECTFEEDBACK",$level+1,false,$multichoice->incorrectfeedback));
419 $status = fwrite ($bf,end_tag("MULTICHOICE",$level,true));
422 //Now print question_answers
423 $status = question_backup_answers($bf,$preferences,$question);
425 return $status;
428 /// RESTORE FUNCTIONS /////////////////
431 * Restores the data in the question
433 * This is used in question/restorelib.php
435 function restore($old_question_id,$new_question_id,$info,$restore) {
437 $status = true;
439 //Get the multichoices array
440 $multichoices = $info['#']['MULTICHOICE'];
442 //Iterate over multichoices
443 for($i = 0; $i < sizeof($multichoices); $i++) {
444 $mul_info = $multichoices[$i];
446 //Now, build the question_multichoice record structure
447 $multichoice = new stdClass;
448 $multichoice->question = $new_question_id;
449 $multichoice->layout = backup_todb($mul_info['#']['LAYOUT']['0']['#']);
450 $multichoice->answers = backup_todb($mul_info['#']['ANSWERS']['0']['#']);
451 $multichoice->single = backup_todb($mul_info['#']['SINGLE']['0']['#']);
452 $multichoice->shuffleanswers = isset($mul_info['#']['SHUFFLEANSWERS']['0']['#'])?backup_todb($mul_info['#']['SHUFFLEANSWERS']['0']['#']):'';
453 if (array_key_exists("CORRECTFEEDBACK", $mul_info['#'])) {
454 $multichoice->correctfeedback = backup_todb($mul_info['#']['CORRECTFEEDBACK']['0']['#']);
455 } else {
456 $multichoice->correctfeedback = '';
458 if (array_key_exists("PARTIALLYCORRECTFEEDBACK", $mul_info['#'])) {
459 $multichoice->partiallycorrectfeedback = backup_todb($mul_info['#']['PARTIALLYCORRECTFEEDBACK']['0']['#']);
460 } else {
461 $multichoice->partiallycorrectfeedback = '';
463 if (array_key_exists("INCORRECTFEEDBACK", $mul_info['#'])) {
464 $multichoice->incorrectfeedback = backup_todb($mul_info['#']['INCORRECTFEEDBACK']['0']['#']);
465 } else {
466 $multichoice->incorrectfeedback = '';
469 //We have to recode the answers field (a list of answers id)
470 //Extracts answer id from sequence
471 $answers_field = "";
472 $in_first = true;
473 $tok = strtok($multichoice->answers,",");
474 while ($tok) {
475 //Get the answer from backup_ids
476 $answer = backup_getid($restore->backup_unique_code,"question_answers",$tok);
477 if ($answer) {
478 if ($in_first) {
479 $answers_field .= $answer->new_id;
480 $in_first = false;
481 } else {
482 $answers_field .= ",".$answer->new_id;
485 //check for next
486 $tok = strtok(",");
488 //We have the answers field recoded to its new ids
489 $multichoice->answers = $answers_field;
491 //The structure is equal to the db, so insert the question_shortanswer
492 $newid = insert_record ("question_multichoice",$multichoice);
494 //Do some output
495 if (($i+1) % 50 == 0) {
496 if (!defined('RESTORE_SILENTLY')) {
497 echo ".";
498 if (($i+1) % 1000 == 0) {
499 echo "<br />";
502 backup_flush(300);
505 if (!$newid) {
506 $status = false;
510 return $status;
513 function restore_recode_answer($state, $restore) {
514 $pos = strpos($state->answer, ':');
515 $order = array();
516 $responses = array();
517 if (false === $pos) { // No order of answers is given, so use the default
518 if ($state->answer) {
519 $responses = explode(',', $state->answer);
521 } else {
522 $order = explode(',', substr($state->answer, 0, $pos));
523 if ($responsestring = substr($state->answer, $pos + 1)) {
524 $responses = explode(',', $responsestring);
527 if ($order) {
528 foreach ($order as $key => $oldansid) {
529 $answer = backup_getid($restore->backup_unique_code,"question_answers",$oldansid);
530 if ($answer) {
531 $order[$key] = $answer->new_id;
532 } else {
533 echo 'Could not recode multichoice answer id '.$oldansid.' for state '.$state->oldid.'<br />';
537 if ($responses) {
538 foreach ($responses as $key => $oldansid) {
539 $answer = backup_getid($restore->backup_unique_code,"question_answers",$oldansid);
540 if ($answer) {
541 $responses[$key] = $answer->new_id;
542 } else {
543 echo 'Could not recode multichoice response answer id '.$oldansid.' for state '.$state->oldid.'<br />';
547 return implode(',', $order).':'.implode(',', $responses);
551 * Decode links in question type specific tables.
552 * @return bool success or failure.
554 function decode_content_links_caller($questionids, $restore, &$i) {
555 $status = true;
557 // Decode links in the question_multichoice table.
558 if ($multichoices = get_records_list('question_multichoice', 'question',
559 implode(',', $questionids), '', 'id, correctfeedback, partiallycorrectfeedback, incorrectfeedback')) {
561 foreach ($multichoices as $multichoice) {
562 $correctfeedback = restore_decode_content_links_worker($multichoice->correctfeedback, $restore);
563 $partiallycorrectfeedback = restore_decode_content_links_worker($multichoice->partiallycorrectfeedback, $restore);
564 $incorrectfeedback = restore_decode_content_links_worker($multichoice->incorrectfeedback, $restore);
565 if ($correctfeedback != $multichoice->correctfeedback ||
566 $partiallycorrectfeedback != $multichoice->partiallycorrectfeedback ||
567 $incorrectfeedback != $multichoice->incorrectfeedback) {
568 $subquestion->correctfeedback = addslashes($correctfeedback);
569 $subquestion->partiallycorrectfeedback = addslashes($partiallycorrectfeedback);
570 $subquestion->incorrectfeedback = addslashes($incorrectfeedback);
571 if (!update_record('question_multichoice', $multichoice)) {
572 $status = false;
576 // Do some output.
577 if (++$i % 5 == 0 && !defined('RESTORE_SILENTLY')) {
578 echo ".";
579 if ($i % 100 == 0) {
580 echo "<br />";
582 backup_flush(300);
587 return $status;
591 * @return array of the numbering styles supported. For each one, there
592 * should be a lang string answernumberingxxx in teh qtype_multichoice
593 * language file, and a case in the switch statement in number_in_style,
594 * and it should be listed in the definition of this column in install.xml.
596 function get_numbering_styles() {
597 return array('abc', 'ABCD', '123', 'none');
600 function number_html($qnum) {
601 return '<span class="anun">' . $qnum . '<span class="anumsep">.</span></span> ';
605 * @param int $num The number, starting at 0.
606 * @param string $style The style to render the number in. One of the ones returned by $numberingoptions.
607 * @return string the number $num in the requested style.
609 function number_in_style($num, $style) {
610 switch($style) {
611 case 'abc':
612 return $this->number_html(chr(ord('a') + $num));
613 case 'ABCD':
614 return $this->number_html(chr(ord('A') + $num));
615 case '123':
616 return $this->number_html(($num + 1));
617 case 'none':
618 return '';
619 default:
620 return 'ERR';
624 function find_file_links($question, $courseid){
625 $urls = array();
626 // find links in the answers table.
627 $urls += question_find_file_links_from_html($question->options->correctfeedback, $courseid);
628 $urls += question_find_file_links_from_html($question->options->partiallycorrectfeedback, $courseid);
629 $urls += question_find_file_links_from_html($question->options->incorrectfeedback, $courseid);
630 foreach ($question->options->answers as $answer) {
631 $urls += question_find_file_links_from_html($answer->answer, $courseid);
633 //set all the values of the array to the question id
634 if ($urls){
635 $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id)));
637 $urls = array_merge_recursive($urls, parent::find_file_links($question, $courseid));
638 return $urls;
641 function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
642 parent::replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination);
643 // replace links in the question_match_sub table.
644 $optionschanged = false;
645 $question->options->correctfeedback = question_replace_file_links_in_html($question->options->correctfeedback, $fromcourseid, $tocourseid, $url, $destination, $optionschanged);
646 $question->options->partiallycorrectfeedback = question_replace_file_links_in_html($question->options->partiallycorrectfeedback, $fromcourseid, $tocourseid, $url, $destination, $optionschanged);
647 $question->options->incorrectfeedback = question_replace_file_links_in_html($question->options->incorrectfeedback, $fromcourseid, $tocourseid, $url, $destination, $optionschanged);
648 if ($optionschanged){
649 if (!update_record('question_multichoice', addslashes_recursive($question->options))) {
650 error('Couldn\'t update \'question_multichoice\' record '.$question->options->id);
653 $answerchanged = false;
654 foreach ($question->options->answers as $answer) {
655 $answer->answer = question_replace_file_links_in_html($answer->answer, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
656 if ($answerchanged){
657 if (!update_record('question_answers', addslashes_recursive($answer))){
658 error('Couldn\'t update \'question_answers\' record '.$answer->id);
665 // Register this question type with the question bank.
666 question_register_questiontype(new question_multichoice_qtype());