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