MDL-28364 handle question formats that support multiple file types
[moodle.git] / question / format / blackboard_six / format.php
1 <?php
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/>.
17 /**
18  * Blackboard 6.0 question importer.
19  *
20  * @package    qformat
21  * @subpackage blackboard_six
22  * @copyright  2005 Michael Penney
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
29 require_once ($CFG->libdir . '/xmlize.php');
32 /**
33  * Blackboard 6.0 question importer.
34  *
35  * @copyright  2005 Michael Penney
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class qformat_blackboard_six extends qformat_default {
39     function provide_import() {
40         return true;
41     }
42     
43     public function can_import_file($file) {
44         $mimetypes = array(
45             mimeinfo('type', '.dat'),
46             mimeinfo('type', '.zip')
47         );
48         return in_array($file->get_mimetype(), $mimetypes);
49     }
52     //Function to check and create the needed dir to unzip file to
53     function check_and_create_import_dir($unique_code) {
55         global $CFG;
57         $status = $this->check_dir_exists($CFG->tempdir."",true);
58         if ($status) {
59             $status = $this->check_dir_exists($CFG->tempdir."/bbquiz_import",true);
60         }
61         if ($status) {
62             $status = $this->check_dir_exists($CFG->tempdir."/bbquiz_import/".$unique_code,true);
63         }
65         return $status;
66     }
68     function clean_temp_dir($dir='') {
69         global $CFG;
71         // for now we will just say everything happened okay note
72         // that a mess may be piling up in $CFG->tempdir/bbquiz_import
73         // TODO return true at top of the function renders all the following code useless
74         return true;
76         if ($dir == '') {
77             $dir = $this->temp_dir;
78         }
79         $slash = "/";
81         // Create arrays to store files and directories
82         $dir_files      = array();
83         $dir_subdirs    = array();
85         // Make sure we can delete it
86         chmod($dir, $CFG->directorypermissions);
88         if ((($handle = opendir($dir))) == FALSE) {
89             // The directory could not be opened
90             return false;
91         }
93         // Loop through all directory entries, and construct two temporary arrays containing files and sub directories
94         while(false !== ($entry = readdir($handle))) {
95             if (is_dir($dir. $slash .$entry) && $entry != ".." && $entry != ".") {
96                 $dir_subdirs[] = $dir. $slash .$entry;
97             }
98             else if ($entry != ".." && $entry != ".") {
99                 $dir_files[] = $dir. $slash .$entry;
100             }
101         }
103         // Delete all files in the curent directory return false and halt if a file cannot be removed
104         $countdir_files = count($dir_files);
105         for($i=0; $i<$countdir_files; $i++) {
106             chmod($dir_files[$i], $CFG->directorypermissions);
107             if (((unlink($dir_files[$i]))) == FALSE) {
108                 return false;
109             }
110         }
112         // Empty sub directories and then remove the directory
113         $countdir_subdirs = count($dir_subdirs);
114         for($i=0; $i<$countdir_subdirs; $i++) {
115             chmod($dir_subdirs[$i], $CFG->directorypermissions);
116             if ($this->clean_temp_dir($dir_subdirs[$i]) == FALSE) {
117                 return false;
118             }
119             else {
120                 if (rmdir($dir_subdirs[$i]) == FALSE) {
121                 return false;
122                 }
123             }
124         }
126         // Close directory
127         closedir($handle);
128         if (rmdir($this->temp_dir) == FALSE) {
129             return false;
130         }
131         // Success, every thing is gone return true
132         return true;
133     }
135     //Function to check if a directory exists and, optionally, create it
136     function check_dir_exists($dir,$create=false) {
138         global $CFG;
140         $status = true;
141         if(!is_dir($dir)) {
142             if (!$create) {
143                 $status = false;
144             } else {
145                 umask(0000);
146                 $status = mkdir ($dir,$CFG->directorypermissions);
147             }
148         }
149         return $status;
150     }
152     function importpostprocess() {
153     /// Does any post-processing that may be desired
154     /// Argument is a simple array of question ids that
155     /// have just been added.
157         // need to clean up temporary directory
158         return $this->clean_temp_dir();
159     }
161     function copy_file_to_course($filename) {
162         global $CFG, $COURSE;
163         $filename = str_replace('\\','/',$filename);
164         $fullpath = $this->temp_dir.'/res00001/'.$filename;
165         $basename = basename($filename);
167         $copy_to = $CFG->dataroot.'/'.$COURSE->id.'/bb_import';
169         if ($this->check_dir_exists($copy_to,true)) {
170             if(is_readable($fullpath)) {
171                 $copy_to.= '/'.$basename;
172                 if (!copy($fullpath, $copy_to)) {
173                     return false;
174                 }
175                 else {
176                     return $copy_to;
177                 }
178             }
179         }
180         else {
181             return false;
182         }
183     }
185     function readdata($filename) {
186     /// Returns complete file with an array, one item per line
187         global $CFG;
189         // if the extension is .dat we just return that,
190         // if .zip we unzip the file and get the data
191         $ext = substr($this->realfilename, strpos($this->realfilename,'.'), strlen($this->realfilename)-1);
192         if ($ext=='.dat') {
193             if (!is_readable($filename)) {
194                 print_error('filenotreadable', 'error');
195             }
196             return file($filename);
197         }
199         $unique_code = time();
200         $temp_dir = $CFG->tempdir."/bbquiz_import/".$unique_code;
201         $this->temp_dir = $temp_dir;
202         if ($this->check_and_create_import_dir($unique_code)) {
203             if(is_readable($filename)) {
204                 if (!copy($filename, "$temp_dir/bboard.zip")) {
205                     print_error('cannotcopybackup', 'question');
206                 }
207                 if(unzip_file("$temp_dir/bboard.zip", '', false)) {
208                     // assuming that the information is in res0001.dat
209                     // after looking at 6 examples this was always the case
210                     $q_file = "$temp_dir/res00001.dat";
211                     if (is_file($q_file)) {
212                         if (is_readable($q_file)) {
213                             $filearray = file($q_file);
214                             /// Check for Macintosh OS line returns (ie file on one line), and fix
215                             if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) {
216                                 return explode("\r", $filearray[0]);
217                             } else {
218                                 return $filearray;
219                             }
220                         }
221                     }
222                     else {
223                         print_error('cannotfindquestionfile', 'questioni');
224                     }
225                 }
226                 else {
227                     print "filename: $filename<br />tempdir: $temp_dir <br />";
228                     print_error('cannotunzip', 'question');
229                 }
230             }
231             else {
232                 print_error('cannotreaduploadfile');
233             }
234         }
235         else {
236             print_error('cannotcreatetempdir');
237         }
238     }
240     function save_question_options($question) {
241         return true;
242     }
246   function readquestions ($lines) {
247     /// Parses an array of lines into an array of questions,
248     /// where each item is a question object as defined by
249     /// readquestion().
251     $text = implode($lines, " ");
252     $xml = xmlize($text, 0);
254     $raw_questions = $xml['questestinterop']['#']['assessment'][0]['#']['section'][0]['#']['item'];
255     $questions = array();
257     foreach($raw_questions as $quest) {
258         $question = $this->create_raw_question($quest);
260         switch($question->qtype) {
261             case "Matching":
262                 $this->process_matching($question, $questions);
263                 break;
264             case "Multiple Choice":
265                 $this->process_mc($question, $questions);
266                 break;
267             case "Essay":
268                 $this->process_essay($question, $questions);
269                 break;
270             case "Multiple Answer":
271                 $this->process_ma($question, $questions);
272                 break;
273             case "True/False":
274                 $this->process_tf($question, $questions);
275                 break;
276             case 'Fill in the Blank':
277                 $this->process_fblank($question, $questions);
278                 break;
279             case 'Short Response':
280                 $this->process_essay($question, $questions);
281                 break;
282             default:
283                 print "Unknown or unhandled question type: \"$question->qtype\"<br />";
284                 break;
285         }
287     }
288     return $questions;
289   }
292 // creates a cleaner object to deal with for processing into moodle
293 // the object created is NOT a moodle question object
294 function create_raw_question($quest) {
296     $question = new stdClass();
297     $question->qtype = $quest['#']['itemmetadata'][0]['#']['bbmd_questiontype'][0]['#'];
298     $question->id = $quest['#']['itemmetadata'][0]['#']['bbmd_asi_object_id'][0]['#'];
299     $presentation->blocks = $quest['#']['presentation'][0]['#']['flow'][0]['#']['flow'];
301     foreach($presentation->blocks as $pblock) {
303         $block = NULL;
304         $block->type = $pblock['@']['class'];
306         switch($block->type) {
307             case 'QUESTION_BLOCK':
308                 $sub_blocks = $pblock['#']['flow'];
309                 foreach($sub_blocks as $sblock) {
310                     //echo "Calling process_block from line 263<br>";
311                     $this->process_block($sblock, $block);
312                 }
313                 break;
315             case 'RESPONSE_BLOCK':
316                 $choices = NULL;
317                 switch($question->qtype) {
318                     case 'Matching':
319                         $bb_subquestions = $pblock['#']['flow'];
320                         $sub_questions = array();
321                         foreach($bb_subquestions as $bb_subquestion) {
322                             $sub_question = NULL;
323                             $sub_question->ident = $bb_subquestion['#']['response_lid'][0]['@']['ident'];
324                             $this->process_block($bb_subquestion['#']['flow'][0], $sub_question);
325                             $bb_choices = $bb_subquestion['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label'];
326                             $choices = array();
327                             $this->process_choices($bb_choices, $choices);
328                             $sub_question->choices = $choices;
329                             if (!isset($block->subquestions)) {
330                                 $block->subquestions = array();
331                             }
332                             $block->subquestions[] = $sub_question;
333                         }
334                         break;
335                     case 'Multiple Answer':
336                         $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'];
337                         $choices = array();
338                         $this->process_choices($bb_choices, $choices);
339                         $block->choices = $choices;
340                         break;
341                     case 'Essay':
342                         // Doesn't apply since the user responds with text input
343                         break;
344                     case 'Multiple Choice':
345                         $mc_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'];
346                             foreach($mc_choices as $mc_choice) {
347                             $choices = NULL;
348                             $choices = $this->process_block($mc_choice, $choices);
349                             $block->choices[] = $choices;
350                         }
351                         break;
352                     case 'Short Response':
353                         // do nothing?
354                         break;
355                     case 'Fill in the Blank':
356                         // do nothing?
357                         break;
358                     default:
359                         $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label'];
360                         $choices = array();
361                         $this->process_choices($bb_choices, $choices);
362                         $block->choices = $choices;
363                 }
364                 break;
365             case 'RIGHT_MATCH_BLOCK':
366                 $matching_answerset = $pblock['#']['flow'];
368                 $answerset = array();
369                 foreach($matching_answerset as $answer) {
370                     // $answerset[] = $this->process_block($answer, $bb_answer);
371                     $bb_answer = null;
372                     $bb_answer->text = $answer['#']['flow'][0]['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'];
373                     $answerset[] = $bb_answer;
374                 }
375                 $block->matching_answerset = $answerset;
376                 break;
377             default:
378                 print "UNHANDLED PRESENTATION BLOCK";
379                 break;
380         }
381         $question->{$block->type} = $block;
382     }
384     // determine response processing
385     // there is a section called 'outcomes' that I don't know what to do with
386     $resprocessing = $quest['#']['resprocessing'];
387     $respconditions = $resprocessing[0]['#']['respcondition'];
388     $reponses = array();
389     if ($question->qtype == 'Matching') {
390         $this->process_matching_responses($respconditions, $responses);
391     }
392     else {
393         $this->process_responses($respconditions, $responses);
394     }
395     $question->responses = $responses;
396     $feedbackset = $quest['#']['itemfeedback'];
397     $feedbacks = array();
398     $this->process_feedback($feedbackset, $feedbacks);
399     $question->feedback = $feedbacks;
400     return $question;
403 function process_block($cur_block, &$block) {
404     global $COURSE, $CFG;
406     $cur_type = $cur_block['@']['class'];
407     switch($cur_type) {
408         case 'FORMATTED_TEXT_BLOCK':
409             $block->text = $this->strip_applet_tags_get_mathml($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']);
410             break;
411         case 'FILE_BLOCK':
412             //revisit this to make sure it is working correctly
413             // Commented out ['matapplication']..., etc. because I
414             // noticed that when I imported a new Blackboard 6 file
415             // and printed out the block, the tree did not extend past ['material'][0]['#'] - CT 8/3/06
416             $block->file = $cur_block['#']['material'][0]['#'];//['matapplication'][0]['@']['uri'];
417             if ($block->file != '') {
418                 // if we have a file copy it to the course dir and adjust its name to be visible over the web.
419                 $block->file = $this->copy_file_to_course($block->file);
420                 $block->file = $CFG->wwwroot.'/file.php/'.$COURSE->id.'/bb_import/'.basename($block->file);
421             }
422             break;
423         case 'Block':
424             if (isset($cur_block['#']['material'][0]['#']['mattext'][0]['#'])) {
425             $block->text = $cur_block['#']['material'][0]['#']['mattext'][0]['#'];
426             }
427             else if (isset($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'])) {
428                 $block->text = $cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'];
429             }
430             else if (isset($cur_block['#']['response_label'])) {
431                 // this is a response label block
432                 $sub_blocks = $cur_block['#']['response_label'][0];
433                 if(!isset($block->ident)) {
434                     if(isset($sub_blocks['@']['ident'])) {
435                         $block->ident = $sub_blocks['@']['ident'];
436                     }
437                 }
438                 foreach($sub_blocks['#']['flow_mat'] as $sub_block) {
439                     $this->process_block($sub_block, $block);
440                 }
441             }
442             else {
443                 if (isset($cur_block['#']['flow_mat']) || isset($cur_block['#']['flow'])) {
444                     if (isset($cur_block['#']['flow_mat'])) {
445                         $sub_blocks = $cur_block['#']['flow_mat'];
446                     }
447                     elseif (isset($cur_block['#']['flow'])) {
448                         $sub_blocks = $cur_block['#']['flow'];
449                     }
450                    foreach ($sub_blocks as $sblock) {
451                         // this will recursively grab the sub blocks which should be of one of the other types
452                         $this->process_block($sblock, $block);
453                     }
454                 }
455             }
456             break;
457         case 'LINK_BLOCK':
458             // not sure how this should be included
459             if (!empty($cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri'])) {
460                 $block->link = $cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri'];
461             }
462             else {
463                $block->link = '';
464             }
465             break;
466     }
467     return $block;
470 function process_choices($bb_choices, &$choices) {
471     foreach($bb_choices as $choice) {
472             if (isset($choice['@']['ident'])) {
473             $cur_choice = $choice['@']['ident'];
474         }
475         else { //for multiple answer
476             $cur_choice = $choice['#']['response_label'][0];//['@']['ident'];
477         }
478         if (isset($choice['#']['flow_mat'][0])) { //for multiple answer
479             $cur_block = $choice['#']['flow_mat'][0];
480             // Reset $cur_choice to NULL because process_block is expecting an object
481             // for the second argument and not a string, which is what is was set as
482             // originally - CT 8/7/06
483             $cur_choice = null;
484             $this->process_block($cur_block, $cur_choice);
485         }
486         elseif (isset($choice['#']['response_label'])) {
487             // Reset $cur_choice to NULL because process_block is expecting an object
488             // for the second argument and not a string, which is what is was set as
489             // originally - CT 8/7/06
490             $cur_choice = null;
491             $this->process_block($choice, $cur_choice);
492         }
493         $choices[] = $cur_choice;
494     }
497 function process_matching_responses($bb_responses, &$responses) {
498     foreach($bb_responses as $bb_response) {
499         $response = NULL;
500         if (isset($bb_response['#']['conditionvar'][0]['#']['varequal'])) {
501             $response->correct = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['#'];
502             $response->ident = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['@']['respident'];
503         }
504         else {
505             $response->correct =  'Broken Question?';
506             $response->ident = 'Broken Question?';
507         }
508         $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid'];
509         $responses[] = $response;
510     }
513 function process_responses($bb_responses, &$responses) {
514     foreach($bb_responses as $bb_response) {
515         //Added this line to instantiate $response.
516         // Without instantiating the $response variable, the same object
517         // gets added to the array
518         $response = new stdClass();
519         if (isset($bb_response['@']['title'])) {
520                 $response->title = $bb_response['@']['title'];
521             }
522             else {
523                 $reponse->title = $bb_response['#']['displayfeedback'][0]['@']['linkrefid'];
524             }
525             $reponse->ident = array();
526             if (isset($bb_response['#']['conditionvar'][0]['#'])){//['varequal'][0]['#'])) {
527                 $response->ident[0] = $bb_response['#']['conditionvar'][0]['#'];//['varequal'][0]['#'];
528             }
529             else if (isset($bb_response['#']['conditionvar'][0]['#']['other'][0]['#'])) {
530                 $response->ident[0] = $bb_response['#']['conditionvar'][0]['#']['other'][0]['#'];
531             }
533             if (isset($bb_response['#']['conditionvar'][0]['#']['and'])){//[0]['#'])) {
534                 $responseset = $bb_response['#']['conditionvar'][0]['#']['and'];//[0]['#']['varequal'];
535                 foreach($responseset as $rs) {
536                     $response->ident[] = $rs['#'];
537                     if(!isset($response->feedback) and isset( $rs['@'] ) ) {
538                         $response->feedback = $rs['@']['respident'];
539                     }
540                 }
541             }
542             else {
543                 $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid'];
544             }
546             // determine what point value to give response
547             if (isset($bb_response['#']['setvar'])) {
548                 switch ($bb_response['#']['setvar'][0]['#']) {
549                     case "SCORE.max":
550                         $response->fraction = 1;
551                         break;
552                     default:
553                         // I have only seen this being 0 or unset
554                         // there are probably fractional values of SCORE.max, but I'm not sure what they look like
555                         $response->fraction = 0;
556                         break;
557                 }
558             }
559             else {
560                // just going to assume this is the case this is probably not correct.
561                $response->fraction = 0;
562             }
564             $responses[] = $response;
565         }
568 function process_feedback($feedbackset, &$feedbacks) {
569     foreach($feedbackset as $bb_feedback) {
570         // Added line $feedback=null so that $feedback does not get reused in the loop
571         // and added the the $feedbacks[] array multiple times
572         $feedback = null;
573         $feedback->ident = $bb_feedback['@']['ident'];
574         if (isset($bb_feedback['#']['flow_mat'][0])) {
575             $this->process_block($bb_feedback['#']['flow_mat'][0], $feedback);
576         }
577         elseif (isset($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0])) {
578             $this->process_block($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0], $feedback);
579         }
580         $feedbacks[] = $feedback;
581     }
584 /**
585  * Create common parts of question
586  */
587 function process_common( $quest ) {
588     $question = $this->defaultquestion();
589     $question->questiontext = $quest->QUESTION_BLOCK->text;
590     $question->name = shorten_text( $quest->id, 250 );
592     return $question;
595 //----------------------------------------
596 // Process True / False Questions
597 //----------------------------------------
598 function process_tf($quest, &$questions) {
599     $question = $this->process_common( $quest );
601     $question->qtype = TRUEFALSE;
602     $question->single = 1; // Only one answer is allowed
603     // 0th [response] is the correct answer.
604     $responses = $quest->responses;
605     $correctresponse = $responses[0]->ident[0]['varequal'][0]['#'];
606     if ($correctresponse != 'false') {
607         $correct = true;
608     }
609     else {
610         $correct = false;
611     }
613     foreach($quest->feedback as $fb) {
614         $fback->{$fb->ident} = $fb->text;
615     }
617     if ($correct) {  // true is correct
618         $question->answer = 1;
619         $question->feedbacktrue = $fback->correct;
620         $question->feedbackfalse = $fback->incorrect;
621     } else {  // false is correct
622         $question->answer = 0;
623         $question->feedbacktrue = $fback->incorrect;
624         $question->feedbackfalse = $fback->correct;
625     }
626     $question->correctanswer = $question->answer;
627     $questions[] = $question;
631 //----------------------------------------
632 // Process Fill in the Blank
633 //----------------------------------------
634 function process_fblank($quest, &$questions) {
635     $question = $this->process_common( $quest );
636     $question->qtype = SHORTANSWER;
637     $question->single = 1;
639     $answers = array();
640     $fractions = array();
641     $feedbacks = array();
643     // extract the feedback
644     $feedback = array();
645     foreach($quest->feedback as $fback) {
646         if (isset($fback->ident)) {
647             if ($fback->ident == 'correct' || $fback->ident == 'incorrect') {
648                 $feedback[$fback->ident] = $fback->text;
649             }
650         }
651     }
653     foreach($quest->responses as $response) {
654         if(isset($response->title)) {
655             if (isset($response->ident[0]['varequal'][0]['#'])) {
656                 //for BB Fill in the Blank, only interested in correct answers
657                 if ($response->feedback = 'correct') {
658                     $answers[] = $response->ident[0]['varequal'][0]['#'];
659                     $fractions[] = 1;
660                     if (isset($feedback['correct'])) {
661                         $feedbacks[] = $feedback['correct'];
662                     }
663                     else {
664                         $feedbacks[] = '';
665                     }
666                 }
667             }
669         }
670     }
672     //Adding catchall to so that students can see feedback for incorrect answers when they enter something the
673     //instructor did not enter
674     $answers[] = '*';
675     $fractions[] = 0;
676     if (isset($feedback['incorrect'])) {
677         $feedbacks[] = $feedback['incorrect'];
678     }
679     else {
680         $feedbacks[] = '';
681     }
683     $question->answer = $answers;
684     $question->fraction = $fractions;
685     $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of
687     if (!empty($question)) {
688         $questions[] = $question;
689     }
693 //----------------------------------------
694 // Process Multiple Choice Questions
695 //----------------------------------------
696 function process_mc($quest, &$questions) {
697     $question = $this->process_common( $quest );
698     $question->qtype = MULTICHOICE;
699     $question->single = 1;
701     $feedback = array();
702     foreach($quest->feedback as $fback) {
703         $feedback[$fback->ident] = $fback->text;
704     }
706     foreach($quest->responses as $response) {
707         if (isset($response->title)) {
708             if ($response->title == 'correct') {
709                 // only one answer possible for this qtype so first index is correct answer
710                 $correct = $response->ident[0]['varequal'][0]['#'];
711             }
712         }
713         else {
714             // fallback method for when the title is not set
715             if ($response->feedback == 'correct') {
716                // only one answer possible for this qtype so first index is correct answer
717                $correct = $response->ident[0]['varequal'][0]['#']; // added [0]['varequal'][0]['#'] to $response->ident - CT 8/9/06
718             }
719         }
720     }
722     $i = 0;
723     foreach($quest->RESPONSE_BLOCK->choices as $response) {
724         $question->answer[$i] = $response->text;
725         if ($correct == $response->ident) {
726             $question->fraction[$i] = 1;
727             // this is a bit of a hack to catch the feedback... first we see if a 'correct' feedback exists
728             // then specific feedback for this question (maybe this should be switched?, but from my example
729             // question pools I have not seen response specific feedback, only correct or incorrect feedback
730             if (!empty($feedback['correct'])) {
731                 $question->feedback[$i] = $feedback['correct'];
732             }
733             elseif (!empty($feedback[$i])) {
734                 $question->feedback[$i] = $feedback[$i];
735             }
736             else {
737                 // failsafe feedback (should be '' instead?)
738                 $question->feedback[$i] = "correct";
739             }
740         }
741         else {
742             $question->fraction[$i] = 0;
743             if (!empty($feedback['incorrect'])) {
744                 $question->feedback[$i] = $feedback['incorrect'];
745             }
746             elseif (!empty($feedback[$i])) {
747                 $question->feedback[$i] = $feedback[$i];
748             }
749             else {
750                 // failsafe feedback (should be '' instead?)
751                 $question->feedback[$i] = 'incorrect';
752             }
753         }
754         $i++;
755     }
757     if (!empty($question)) {
758         $questions[] = $question;
759     }
762 //----------------------------------------
763 // Process Multiple Choice Questions With Multiple Answers
764 //----------------------------------------
765 function process_ma($quest, &$questions) {
766     $question = $this->process_common( $quest ); // copied this from process_mc
767     $question->qtype = MULTICHOICE;
768     $question->single = 0; // More than one answer allowed
770     $answers = $quest->responses;
771     $correct_answers = array();
772     foreach($answers as $answer) {
773         if($answer->title == 'correct') {
774             $answerset = $answer->ident[0]['and'][0]['#']['varequal'];
775             foreach($answerset as $ans) {
776                 $correct_answers[] = $ans['#'];
777             }
778         }
779     }
781     foreach ($quest->feedback as $fb) {
782         $feedback->{$fb->ident} = trim($fb->text);
783     }
785     $correct_answer_count = count($correct_answers);
786     $choiceset = $quest->RESPONSE_BLOCK->choices;
787     $i = 0;
788     foreach($choiceset as $choice) {
789         $question->answer[$i] = trim($choice->text);
790         if (in_array($choice->ident, $correct_answers)) {
791             // correct answer
792             $question->fraction[$i] = floor(100000/$correct_answer_count)/100000; // strange behavior if we have more than 5 decimal places
793             $question->feedback[$i] = $feedback->correct;
794         }
795         else {
796             // wrong answer
797             $question->fraction[$i] = 0;
798             $question->feedback[$i] = $feedback->incorrect;
799         }
800         $i++;
801     }
803     $questions[] = $question;
806 //----------------------------------------
807 // Process Essay Questions
808 //----------------------------------------
809 function process_essay($quest, &$questions) {
810 // this should be rewritten to accomodate moodle 1.6 essay question type eventually
812     if (defined("ESSAY")) {
813         // treat as short answer
814         $question = $this->process_common( $quest ); // copied this from process_mc
815         $question->qtype = ESSAY;
817         $question->feedback = array();
818         // not sure where to get the correct answer from
819         foreach($quest->feedback as $feedback) {
820             // Added this code to put the possible solution that the
821             // instructor gives as the Moodle answer for an essay question
822             if ($feedback->ident == 'solution') {
823                 $question->feedback = $feedback->text;
824             }
825         }
826         //Added because essay/questiontype.php:save_question_option is expecting a
827         //fraction property - CT 8/10/06
828         $question->fraction[] = 1;
829         if (!empty($question)) {
830             $questions[]=$question;
831         }
832     }
833     else {
834         print "Essay question types are not handled because the quiz question type 'Essay' does not exist in this installation of Moodle<br/>";
835         print "&nbsp;&nbsp;&nbsp;&nbsp;Omitted Question: ".$quest->QUESTION_BLOCK->text.'<br/><br/>';
836     }
839 //----------------------------------------
840 // Process Matching Questions
841 //----------------------------------------
842 function process_matching($quest, &$questions) {
843     // renderedmatch is an optional plugin, so we need to check if it is defined
844     if (question_bank::is_qtype_installed('renderedmatch')) {
845         $question = $this->process_common($quest);
846         $question->valid = true;
847         $question->qtype = 'renderedmatch';
849         foreach($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
850             foreach($quest->responses as $rid => $resp) {
851                 if ($resp->ident == $subq->ident) {
852                     $correct = $resp->correct;
853                     $feedback = $resp->feedback;
854                 }
855             }
857             foreach($subq->choices as $cid => $choice) {
858                 if ($choice == $correct) {
859                     $question->subquestions[] = $subq->text;
860                     $question->subanswers[] = $quest->RIGHT_MATCH_BLOCK->matching_answerset[$cid]->text;
861                 }
862             }
863         }
865         // check format
866         $status = true;
867         if ( count($quest->RESPONSE_BLOCK->subquestions) > count($quest->RIGHT_MATCH_BLOCK->matching_answerset) || count($question->subquestions) < 2) {
868             $status = false;
869         }
870         else {
871             // need to redo to make sure that no two questions have the same answer (rudimentary now)
872             foreach($question->subanswers as $qstn) {
873                 if(isset($previous)) {
874                     if ($qstn == $previous) {
875                         $status = false;
876                     }
877                 }
878                 $previous = $qstn;
879                 if ($qstn == '') {
880                     $status = false;
881                 }
882             }
883         }
885         if ($status) {
886             $questions[] = $question;
887         }
888         else {
889             global $COURSE, $CFG;
890             print '<table class="boxaligncenter" border="1">';
891             print '<tr><td colspan="2" style="background-color:#FF8888;">This matching question is malformed. Please ensure there are no blank answers, no two questions have the same answer, and/or there are correct answers for each question. There must be at least as many subanswers as subquestions, and at least one subquestion.</td></tr>';
893             print "<tr><td>Question:</td><td>".$quest->QUESTION_BLOCK->text;
894             if (isset($quest->QUESTION_BLOCK->file)) {
895                 print '<br/><font color="red">There is a subfile contained in the zipfile that has been copied to course files: bb_import/'.basename($quest->QUESTION_BLOCK->file).'</font>';
896                 if (preg_match('/(gif|jpg|jpeg|png)$/i', $quest->QUESTION_BLOCK->file)) {
897                     print '<img src="'.$CFG->wwwroot.'/file.php/'.$COURSE->id.'/bb_import/'.basename($quest->QUESTION_BLOCK->file).'" />';
898                 }
899             }
900             print "</td></tr>";
901             print "<tr><td>Subquestions:</td><td><ul>";
902             foreach($quest->responses as $rs) {
903                 $correct_responses->{$rs->ident} = $rs->correct;
904             }
905             foreach($quest->RESPONSE_BLOCK->subquestions as $subq) {
906                 print '<li>'.$subq->text.'<ul>';
907                 foreach($subq->choices as $id=>$choice) {
908                     print '<li>';
909                     if ($choice == $correct_responses->{$subq->ident}) {
910                         print '<font color="green">';
911                     }
912                     else {
913                         print '<font color="red">';
914                     }
915                     print $quest->RIGHT_MATCH_BLOCK->matching_answerset[$id]->text.'</font></li>';
916                 }
917                 print '</ul>';
918             }
919             print '</ul></td></tr>';
921             print '<tr><td>Feedback:</td><td><ul>';
922             foreach($quest->feedback as $fb) {
923                 print '<li>'.$fb->ident.': '.$fb->text.'</li>';
924             }
925             print '</ul></td></tr></table>';
926         }
927     }
928     else {
929         print "Matching question types are not handled because the quiz question type 'Rendered Matching' does not exist in this installation of Moodle<br/>";
930         print "&nbsp;&nbsp;&nbsp;&nbsp;Omitted Question: ".$quest->QUESTION_BLOCK->text.'<br/><br/>';
931     }
935 function strip_applet_tags_get_mathml($string) {
936     if(stristr($string, '</APPLET>') === FALSE) {
937         return $string;
938     }
939     else {
940         // strip all applet tags keeping stuff before/after and inbetween (if mathml) them
941         while (stristr($string, '</APPLET>') !== FALSE) {
942             preg_match("/(.*)\<applet.*value=\"(\<math\>.*\<\/math\>)\".*\<\/applet\>(.*)/i",$string, $mathmls);
943             $string = $mathmls[1].$mathmls[2].$mathmls[3];
944         }
945         return $string;
946     }
949 } // close object