6b9eddafbbf7c608f2c37ec3d83b902f5d99c6a3
[moodle.git] / question / format / blackboard_six / formatqti.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 V5 and V6 question importer.
19  *
20  * @package    qformat_blackboard_six
21  * @copyright  2005 Michael Penney
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 require_once($CFG->libdir . '/xmlize.php');
29 /**
30  * Blackboard 6.0 question importer.
31  *
32  * @copyright  2005 Michael Penney
33  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34  */
35 class qformat_blackboard_six_qti extends qformat_blackboard_six_base {
36     /**
37      * Parse the xml document into an array of questions
38      * this *could* burn memory - but it won't happen that much
39      * so fingers crossed!
40      * @param array of lines from the input file.
41      * @param stdClass $context
42      * @return array (of objects) questions objects.
43      */
44     protected function readquestions($text) {
46         // This converts xml to big nasty data structure,
47         // the 0 means keep white space as it is.
48         try {
49             $xml = xmlize($text, 0, 'UTF-8', true);
50         } catch (xml_format_exception $e) {
51             $this->error($e->getMessage(), '');
52             return false;
53         }
55         $questions = array();
56         // First step : we are only interested in the <item> tags.
57         $rawquestions = $this->getpath($xml,
58                 array('questestinterop', '#', 'assessment', 0, '#', 'section', 0, '#', 'item'),
59                 array(), false);
60         // Each <item> tag contains data related to a single question.
61         foreach ($rawquestions as $quest) {
62             // Second step : parse each question data into the intermediate
63             // rawquestion structure array.
64             // Warning : rawquestions are not Moodle questions.
65             $question = $this->create_raw_question($quest);
66             // Third step : convert a rawquestion into a Moodle question.
67             switch($question->qtype) {
68                 case "Matching":
69                     $this->process_matching($question, $questions);
70                     break;
71                 case "Multiple Choice":
72                     $this->process_mc($question, $questions);
73                     break;
74                 case "Essay":
75                     $this->process_essay($question, $questions);
76                     break;
77                 case "Multiple Answer":
78                     $this->process_ma($question, $questions);
79                     break;
80                 case "True/False":
81                     $this->process_tf($question, $questions);
82                     break;
83                 case 'Fill in the Blank':
84                     $this->process_fblank($question, $questions);
85                     break;
86                 case 'Short Response':
87                     $this->process_essay($question, $questions);
88                     break;
89                 default:
90                     $this->error(get_string('unknownorunhandledtype', 'qformat_blackboard_six', $question->qtype));
91                     break;
92             }
93         }
94         return $questions;
95     }
97     /**
98      * Creates a cleaner object to deal with for processing into Moodle.
99      * The object returned is NOT a moodle question object.
100      * @param array $quest XML <item> question  data
101      * @return object rawquestion
102      */
103     public function create_raw_question($quest) {
105         $rawquestion = new stdClass();
106         $rawquestion->qtype = $this->getpath($quest,
107                 array('#', 'itemmetadata', 0, '#', 'bbmd_questiontype', 0, '#'),
108                 '', true);
109         $rawquestion->id = $this->getpath($quest,
110                 array('#', 'itemmetadata', 0, '#', 'bbmd_asi_object_id', 0, '#'),
111                 '', true);
112         $presentation = new stdClass();
113         $presentation->blocks = $this->getpath($quest,
114                 array('#', 'presentation', 0, '#', 'flow', 0, '#', 'flow'),
115                 array(), false);
117         foreach ($presentation->blocks as $pblock) {
118             $block = new stdClass();
119             $block->type = $this->getpath($pblock,
120                     array('@', 'class'),
121                     '', true);
123             switch($block->type) {
124                 case 'QUESTION_BLOCK':
125                     $subblocks = $this->getpath($pblock,
126                             array('#', 'flow'),
127                             array(), false);
128                     foreach ($subblocks as $sblock) {
129                         $this->process_block($sblock, $block);
130                     }
131                     break;
133                 case 'RESPONSE_BLOCK':
134                     $choices = null;
135                     switch($rawquestion->qtype) {
136                         case 'Matching':
137                             $bbsubquestions = $this->getpath($pblock,
138                                     array('#', 'flow'),
139                                     array(), false);
140                             $sub_questions = array();
141                             foreach ($bbsubquestions as $bbsubquestion) {
142                                 $sub_question = new stdClass();
143                                 $sub_question->ident = $this->getpath($bbsubquestion,
144                                         array('#', 'response_lid', 0, '@', 'ident'),
145                                         '', true);
146                                 $this->process_block($this->getpath($bbsubquestion,
147                                         array('#', 'flow', 0),
148                                         false, false), $sub_question);
149                                 $bbchoices = $this->getpath($bbsubquestion,
150                                         array('#', 'response_lid', 0, '#', 'render_choice', 0,
151                                         '#', 'flow_label', 0, '#', 'response_label'),
152                                         array(), false);
153                                 $choices = array();
154                                 $this->process_choices($bbchoices, $choices);
155                                 $sub_question->choices = $choices;
156                                 if (!isset($block->subquestions)) {
157                                     $block->subquestions = array();
158                                 }
159                                 $block->subquestions[] = $sub_question;
160                             }
161                             break;
162                         case 'Multiple Answer':
163                             $bbchoices = $this->getpath($pblock,
164                                     array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
165                                     array(), false);
166                             $choices = array();
167                             $this->process_choices($bbchoices, $choices);
168                             $block->choices = $choices;
169                             break;
170                         case 'Essay':
171                             // Doesn't apply since the user responds with text input.
172                             break;
173                         case 'Multiple Choice':
174                             $mcchoices = $this->getpath($pblock,
175                                     array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
176                                     array(), false);
177                             foreach ($mcchoices as $mcchoice) {
178                                 $choices = new stdClass();
179                                 $choices = $this->process_block($mcchoice, $choices);
180                                 $block->choices[] = $choices;
181                             }
182                             break;
183                         case 'Short Response':
184                             // Do nothing?
185                             break;
186                         case 'Fill in the Blank':
187                             // Do nothing?
188                             break;
189                         default:
190                             $bbchoices = $this->getpath($pblock,
191                                     array('#', 'response_lid', 0, '#', 'render_choice', 0, '#',
192                                     'flow_label', 0, '#', 'response_label'),
193                                     array(), false);
194                             $choices = array();
195                             $this->process_choices($bbchoices, $choices);
196                             $block->choices = $choices;
197                     }
198                     break;
199                 case 'RIGHT_MATCH_BLOCK':
200                     $matchinganswerset = $this->getpath($pblock,
201                             array('#', 'flow'),
202                             false, false);
204                     $answerset = array();
205                     foreach ($matchinganswerset as $answer) {
206                         $bbanswer = new stdClass;
207                         $bbanswer->text =  $this->getpath($answer,
208                                 array('#', 'flow', 0, '#', 'material', 0, '#', 'mat_extension',
209                                 0, '#', 'mat_formattedtext', 0, '#'),
210                                 false, false);
211                         $answerset[] = $bbanswer;
212                     }
213                     $block->matchinganswerset = $answerset;
214                     break;
215                 default:
216                     $this->error(get_string('unhandledpresblock', 'qformat_blackboard_six'));
217                     break;
218             }
219             $rawquestion->{$block->type} = $block;
220         }
222         // Determine response processing.
223         // There is a section called 'outcomes' that I don't know what to do with.
224         $resprocessing = $this->getpath($quest,
225                 array('#', 'resprocessing'),
226                 array(), false);
228         $respconditions = $this->getpath($resprocessing[0],
229                 array('#', 'respcondition'),
230                 array(), false);
231         $responses = array();
232         if ($rawquestion->qtype == 'Matching') {
233             $this->process_matching_responses($respconditions, $responses);
234         } else {
235             $this->process_responses($respconditions, $responses);
236         }
237         $rawquestion->responses = $responses;
238         $feedbackset = $this->getpath($quest,
239                 array('#', 'itemfeedback'),
240                 array(), false);
242         $feedbacks = array();
243         $this->process_feedback($feedbackset, $feedbacks);
244         $rawquestion->feedback = $feedbacks;
245         return $rawquestion;
246     }
248     /**
249      * Helper function to process an XML block into an object.
250      * Can call himself recursively if necessary to parse this branch of the XML tree.
251      * @param array $curblock XML block to parse
252      * @return object $block parsed
253      */
254     public function process_block($curblock, $block) {
256         $curtype = $this->getpath($curblock,
257                 array('@', 'class'),
258                 '', true);
260         switch($curtype) {
261             case 'FORMATTED_TEXT_BLOCK':
262                 $text = $this->getpath($curblock,
263                         array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'),
264                         '', true);
265                 $block->text = $this->strip_applet_tags_get_mathml($text);
266                 break;
267             case 'FILE_BLOCK':
268                 $block->filename = $this->getpath($curblock,
269                         array('#', 'material', 0, '#'),
270                         '', true);
271                 if ($block->filename != '') {
272                     // TODO : determine what to do with the file's content.
273                     $this->error(get_string('filenothandled', 'qformat_blackboard_six', $block->filename));
274                 }
275                 break;
276             case 'Block':
277                 if ($this->getpath($curblock,
278                         array('#', 'material', 0, '#', 'mattext'),
279                         false, false)) {
280                     $block->text = $this->getpath($curblock,
281                             array('#', 'material', 0, '#', 'mattext', 0, '#'),
282                             '', true);
283                 } else if ($this->getpath($curblock,
284                         array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext'),
285                         false, false)) {
286                     $block->text = $this->getpath($curblock,
287                             array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'),
288                             '', true);
289                 } else if ($this->getpath($curblock,
290                         array('#', 'response_label'),
291                         false, false)) {
292                     // This is a response label block.
293                     $subblocks = $this->getpath($curblock,
294                             array('#', 'response_label', 0),
295                             array(), false);
296                     if (!isset($block->ident)) {
298                         if ($this->getpath($subblocks,
299                                 array('@', 'ident'), '', true)) {
300                             $block->ident = $this->getpath($subblocks,
301                                 array('@', 'ident'), '', true);
302                         }
303                     }
304                     foreach ($this->getpath($subblocks,
305                             array('#', 'flow_mat'), array(), false) as $subblock) {
306                         $this->process_block($subblock, $block);
307                     }
308                 } else {
309                     if ($this->getpath($curblock,
310                                 array('#', 'flow_mat'), false, false)
311                             || $this->getpath($curblock,
312                                 array('#', 'flow'), false, false)) {
313                         if ($this->getpath($curblock,
314                                 array('#', 'flow_mat'), false, false)) {
315                             $subblocks = $this->getpath($curblock,
316                                     array('#', 'flow_mat'), array(), false);
317                         } else if ($this->getpath($curblock,
318                                 array('#', 'flow'), false, false)) {
319                             $subblocks = $this->getpath($curblock,
320                                     array('#', 'flow'), array(), false);
321                         }
322                         foreach ($subblocks as $sblock) {
323                             // This will recursively grab the sub blocks which should be of one of the other types.
324                             $this->process_block($sblock, $block);
325                         }
326                     }
327                 }
328                 break;
329             case 'LINK_BLOCK':
330                 // Not sure how this should be included?
331                 $link = $this->getpath($curblock,
332                             array('#', 'material', 0, '#', 'mattext', 0, '@', 'uri'), '', true);
333                 if (!empty($link)) {
334                     $block->link = $link;
335                 } else {
336                     $block->link = '';
337                 }
338                 break;
339         }
340         return $block;
341     }
343     /**
344      * Preprocess XML blocks containing data for questions' choices.
345      * Called by {@link create_raw_question()}
346      * for matching, multichoice and fill in the blank questions.
347      * @param array $bbchoices XML block to parse
348      * @param array $choices array of choices suitable for a rawquestion.
349      */
350     protected function process_choices($bbchoices, &$choices) {
351         foreach ($bbchoices as $choice) {
352             if ($this->getpath($choice,
353                     array('@', 'ident'), '', true)) {
354                 $curchoice = $this->getpath($choice,
355                         array('@', 'ident'), '', true);
356             } else { // For multiple answers.
357                 $curchoice = $this->getpath($choice,
358                          array('#', 'response_label', 0), array(), false);
359             }
360             if ($this->getpath($choice,
361                     array('#', 'flow_mat', 0), false, false)) { // For multiple answers.
362                 $curblock = $this->getpath($choice,
363                     array('#', 'flow_mat', 0), false, false);
364                 // Reset $curchoice to new stdClass because process_block is expecting an object
365                 // for the second argument and not a string,
366                 // which is what is was set as originally - CT 8/7/06.
367                 $curchoice = new stdClass();
368                 $this->process_block($curblock, $curchoice);
369             } else if ($this->getpath($choice,
370                     array('#', 'response_label'), false, false)) {
371                 // Reset $curchoice to new stdClass because process_block is expecting an object
372                 // for the second argument and not a string,
373                 // which is what is was set as originally - CT 8/7/06.
374                 $curchoice = new stdClass();
375                 $this->process_block($choice, $curchoice);
376             }
377             $choices[] = $curchoice;
378         }
379     }
381     /**
382      * Preprocess XML blocks containing data for subanswers
383      * Called by {@link create_raw_question()}
384      * for matching questions only.
385      * @param array $bbresponses XML block to parse
386      * @param array $responses array of responses suitable for a matching rawquestion.
387      */
388     protected function process_matching_responses($bbresponses, &$responses) {
389         foreach ($bbresponses as $bbresponse) {
390             $response = new stdClass;
391             if ($this->getpath($bbresponse,
392                     array('#', 'conditionvar', 0, '#', 'varequal'), false, false)) {
393                 $response->correct = $this->getpath($bbresponse,
394                         array('#', 'conditionvar', 0, '#', 'varequal', 0, '#'), '', true);
395                 $response->ident = $this->getpath($bbresponse,
396                         array('#', 'conditionvar', 0, '#', 'varequal', 0, '@', 'respident'), '', true);
397             }
398             // Suppressed an else block because if the above if condition is false,
399             // the question is not necessary a broken one, most of the time it's an <other> tag.
401             $response->feedback = $this->getpath($bbresponse,
402                     array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
403             $responses[] = $response;
404         }
405     }
407     /**
408      * Preprocess XML blocks containing data for responses processing.
409      * Called by {@link create_raw_question()}
410      * for all questions types.
411      * @param array $bbresponses XML block to parse
412      * @param array $responses array of responses suitable for a rawquestion.
413      */
414     protected function process_responses($bbresponses, &$responses) {
415         foreach ($bbresponses as $bbresponse) {
416             $response = new stdClass();
417             if ($this->getpath($bbresponse,
418                     array('@', 'title'), '', true)) {
419                 $response->title = $this->getpath($bbresponse,
420                         array('@', 'title'), '', true);
421             } else {
422                 $response->title = $this->getpath($bbresponse,
423                         array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
424             }
425             $response->ident = array();
426             if ($this->getpath($bbresponse,
427                     array('#', 'conditionvar', 0, '#'), false, false)) {
428                 $response->ident[0] = $this->getpath($bbresponse,
429                         array('#', 'conditionvar', 0, '#'), array(), false);
430             } else if ($this->getpath($bbresponse,
431                     array('#', 'conditionvar', 0, '#', 'other', 0, '#'), false, false)) {
432                 $response->ident[0] = $this->getpath($bbresponse,
433                         array('#', 'conditionvar', 0, '#', 'other', 0, '#'), array(), false);
434             }
435             if ($this->getpath($bbresponse,
436                     array('#', 'conditionvar', 0, '#', 'and'), false, false)) {
437                 $responseset = $this->getpath($bbresponse,
438                     array('#', 'conditionvar', 0, '#', 'and'), array(), false);
439                 foreach ($responseset as $rs) {
440                     $response->ident[] = $this->getpath($rs, array('#'), array(), false);
441                     if (!isset($response->feedback) and $this->getpath($rs, array('@'), false, false)) {
442                         $response->feedback = $this->getpath($rs,
443                                 array('@', 'respident'), '', true);
444                     }
445                 }
446             } else {
447                 $response->feedback = $this->getpath($bbresponse,
448                         array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
449             }
451             // Determine what fraction to give response.
452             if ($this->getpath($bbresponse,
453                         array('#', 'setvar'), false, false)) {
454                 switch ($this->getpath($bbresponse,
455                         array('#', 'setvar', 0, '#'), false, false)) {
456                     case "SCORE.max":
457                         $response->fraction = 1;
458                         break;
459                     default:
460                         // I have only seen this being 0 or unset.
461                         // There are probably fractional values of SCORE.max, but I'm not sure what they look like.
462                         $response->fraction = 0;
463                         break;
464                 }
465             } else {
466                 // Just going to assume this is the case this is probably not correct.
467                 $response->fraction = 0;
468             }
470             $responses[] = $response;
471         }
472     }
474     /**
475      * Preprocess XML blocks containing data for responses feedbacks.
476      * Called by {@link create_raw_question()}
477      * for all questions types.
478      * @param array $feedbackset XML block to parse
479      * @param array $feedbacks array of feedbacks suitable for a rawquestion.
480      */
481     public function process_feedback($feedbackset, &$feedbacks) {
482         foreach ($feedbackset as $bb_feedback) {
483             $feedback = new stdClass();
484             $feedback->ident = $this->getpath($bb_feedback,
485                     array('@', 'ident'), '', true);
486             $feedback->text = '';
487             if ($this->getpath($bb_feedback,
488                     array('#', 'flow_mat', 0), false, false)) {
489                 $this->process_block($this->getpath($bb_feedback,
490                         array('#', 'flow_mat', 0), false, false), $feedback);
491             } else if ($this->getpath($bb_feedback,
492                     array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false)) {
493                 $this->process_block($this->getpath($bb_feedback,
494                         array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false), $feedback);
495             }
497             $feedbacks[$feedback->ident] = $feedback;
498         }
499     }
501     /**
502      * Create common parts of question
503      * @param object $quest rawquestion
504      * @return object Moodle question.
505      */
506     public function process_common($quest) {
507         $question = $this->defaultquestion();
508         $text = $quest->QUESTION_BLOCK->text;
509         $questiontext = $this->cleaned_text_field($text);
510         $question->questiontext = $questiontext['text'];
511         $question->questiontextformat = $questiontext['format']; // Needed because add_blank_combined_feedback uses it.
512         if (isset($questiontext['itemid'])) {
513             $question->questiontextitemid = $questiontext['itemid'];
514         }
515         $question->name = $this->create_default_question_name($question->questiontext,
516                 get_string('defaultname', 'qformat_blackboard_six' , $quest->id));
517         $question->generalfeedback = '';
518         $question->generalfeedbackformat = FORMAT_HTML;
519         $question->generalfeedbackfiles = array();
521         return $question;
522     }
524     /**
525      * Process True / False Questions
526      * Parse a truefalse rawquestion and add the result
527      * to the array of questions already parsed.
528      * @param object $quest rawquestion
529      * @param $questions array of Moodle questions already done.
530      */
531     protected function process_tf($quest, &$questions) {
532         $question = $this->process_common($quest);
534         $question->qtype = 'truefalse';
535         $question->single = 1; // Only one answer is allowed.
536         $question->penalty = 1; // Penalty = 1 for truefalse questions.
537         // 0th [response] is the correct answer.
538         $responses = $quest->responses;
539         $correctresponse = $this->getpath($responses[0]->ident[0],
540                 array('varequal', 0, '#'), '', true);
541         if ($correctresponse != 'false') {
542             $correct = true;
543         } else {
544             $correct = false;
545         }
546         $fback = new stdClass();
548         foreach ($quest->feedback as $fb) {
549             $fback->{$fb->ident} = $fb->text;
550         }
552         if ($correct) {  // True is correct.
553             $question->answer = 1;
554             $question->feedbacktrue = $this->cleaned_text_field($fback->correct);
555             $question->feedbackfalse = $this->cleaned_text_field($fback->incorrect);
556         } else {  // False is correct.
557             $question->answer = 0;
558             $question->feedbacktrue = $this->cleaned_text_field($fback->incorrect);
559             $question->feedbackfalse = $this->cleaned_text_field($fback->correct);
560         }
561         $question->correctanswer = $question->answer;
562         $questions[] = $question;
563     }
565     /**
566      * Process Fill in the Blank Questions
567      * Parse a fillintheblank rawquestion and add the result
568      * to the array of questions already parsed.
569      * @param object $quest rawquestion
570      * @param $questions array of Moodle questions already done.
571      */
572     protected function process_fblank($quest, &$questions) {
573         $question = $this->process_common($quest);
574         $question->qtype = 'shortanswer';
575         $question->usecase = 0; // Ignore case.
577         $answers = array();
578         $fractions = array();
579         $feedbacks = array();
581         // Extract the feedback.
582         $feedback = array();
583         foreach ($quest->feedback as $fback) {
584             if (isset($fback->ident)) {
585                 if ($fback->ident == 'correct' || $fback->ident == 'incorrect') {
586                     $feedback[$fback->ident] = $fback->text;
587                 }
588             }
589         }
591         foreach ($quest->responses as $response) {
592             if (isset($response->title)) {
593                 if ($this->getpath($response->ident[0],
594                         array('varequal', 0, '#'), false, false)) {
595                     // For BB Fill in the Blank, only interested in correct answers.
596                     if ($response->feedback = 'correct') {
597                         $answers[] = $this->getpath($response->ident[0],
598                                 array('varequal', 0, '#'), '', true);
599                         $fractions[] = 1;
600                         if (isset($feedback['correct'])) {
601                             $feedbacks[] = $this->cleaned_text_field($feedback['correct']);
602                         } else {
603                             $feedbacks[] = $this->text_field('');
604                         }
605                     }
606                 }
608             }
609         }
611         // Adding catchall to so that students can see feedback for incorrect answers when they enter something,
612         // the instructor did not enter.
613         $answers[] = '*';
614         $fractions[] = 0;
615         if (isset($feedback['incorrect'])) {
616             $feedbacks[] = $this->cleaned_text_field($feedback['incorrect']);
617         } else {
618             $feedbacks[] = $this->text_field('');
619         }
621         $question->answer = $answers;
622         $question->fraction = $fractions;
623         $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of.
625         if (!empty($question)) {
626             $questions[] = $question;
627         }
629     }
631     /**
632      * Process Multichoice Questions
633      * Parse a multichoice single answer rawquestion and add the result
634      * to the array of questions already parsed.
635      * @param object $quest rawquestion
636      * @param $questions array of Moodle questions already done.
637      */
638     protected function process_mc($quest, &$questions) {
639         $question = $this->process_common($quest);
640         $question->qtype = 'multichoice';
641         $question = $this->add_blank_combined_feedback($question);
642         $question->single = 1;
643         $feedback = array();
644         foreach ($quest->feedback as $fback) {
645             $feedback[$fback->ident] = $fback->text;
646         }
648         foreach ($quest->responses as $response) {
649             if (isset($response->title)) {
650                 if ($response->title == 'correct') {
651                     // Only one answer possible for this qtype so first index is correct answer.
652                     $correct = $this->getpath($response->ident[0],
653                             array('varequal', 0, '#'), '', true);
654                 }
655             } else {
656                 // Fallback method for when the title is not set.
657                 if ($response->feedback == 'correct') {
658                     // Only one answer possible for this qtype so first index is correct answer.
659                     $correct = $this->getpath($response->ident[0],
660                             array('varequal', 0, '#'), '', true);
661                 }
662             }
663         }
665         $i = 0;
666         foreach ($quest->RESPONSE_BLOCK->choices as $response) {
667             $question->answer[$i] = $this->cleaned_text_field($response->text);
668             if ($correct == $response->ident) {
669                 $question->fraction[$i] = 1;
670                 // This is a bit of a hack to catch the feedback... first we see if a  'specific'
671                 // feedback for this response exists, then if a 'correct' feedback exists.
673                 if (!empty($feedback[$response->ident]) ) {
674                     $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]);
675                 } else if (!empty($feedback['correct'])) {
676                     $question->feedback[$i] = $this->cleaned_text_field($feedback['correct']);
677                 } else if (!empty($feedback[$i])) {
678                     $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]);
679                 } else {
680                     $question->feedback[$i] = $this->cleaned_text_field(get_string('correct', 'question'));
681                 }
682             } else {
683                 $question->fraction[$i] = 0;
684                 if (!empty($feedback[$response->ident]) ) {
685                     $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]);
686                 } else if (!empty($feedback['incorrect'])) {
687                     $question->feedback[$i] = $this->cleaned_text_field($feedback['incorrect']);
688                 } else if (!empty($feedback[$i])) {
689                     $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]);
690                 } else {
691                     $question->feedback[$i] = $this->cleaned_text_field(get_string('incorrect', 'question'));
692                 }
693             }
694             $i++;
695         }
697         if (!empty($question)) {
698             $questions[] = $question;
699         }
700     }
702     /**
703      * Process Multiple Choice Questions With Multiple Answers.
704      * Parse a multichoice multianswer rawquestion and add the result
705      * to the array of questions already parsed.
706      * @param object $quest rawquestion
707      * @param $questions array of Moodle questions already done.
708      */
709     public function process_ma($quest, &$questions) {
710         $question = $this->process_common($quest);
711         $question->qtype = 'multichoice';
712         $question = $this->add_blank_combined_feedback($question);
713         $question->single = 0; // More than one answer allowed.
715         $answers = $quest->responses;
716         $correctanswers = array();
717         foreach ($answers as $answer) {
718             if ($answer->title == 'correct') {
719                 $answerset = $this->getpath($answer->ident[0],
720                         array('and', 0, '#', 'varequal'), array(), false);
721                 foreach ($answerset as $ans) {
722                     $correctanswers[] = $ans['#'];
723                 }
724             }
725         }
726         $feedback = new stdClass();
727         foreach ($quest->feedback as $fb) {
728             $feedback->{$fb->ident} = trim($fb->text);
729         }
731         $correctanswercount = count($correctanswers);
732         $fraction = 1/$correctanswercount;
733         $choiceset = $quest->RESPONSE_BLOCK->choices;
734         $i = 0;
735         foreach ($choiceset as $choice) {
736             $question->answer[$i] = $this->cleaned_text_field(trim($choice->text));
737             if (in_array($choice->ident, $correctanswers)) {
738                 // Correct answer.
739                 $question->fraction[$i] = $fraction;
740                 $question->feedback[$i] = $this->cleaned_text_field($feedback->correct);
741             } else {
742                 // Wrong answer.
743                 $question->fraction[$i] = 0;
744                 $question->feedback[$i] = $this->cleaned_text_field($feedback->incorrect);
745             }
746             $i++;
747         }
749         $questions[] = $question;
750     }
752     /**
753      * Process Essay Questions
754      * Parse an essay rawquestion and add the result
755      * to the array of questions already parsed.
756      * @param object $quest rawquestion
757      * @param $questions array of Moodle questions already done.
758      */
759     public function process_essay($quest, &$questions) {
761         $question = $this->process_common($quest);
762         $question->qtype = 'essay';
764         $question->feedback = array();
765         // Not sure where to get the correct answer from?
766         foreach ($quest->feedback as $feedback) {
767             // Added this code to put the possible solution that the
768             // instructor gives as the Moodle answer for an essay question.
769             if ($feedback->ident == 'solution') {
770                 $question->graderinfo = $this->cleaned_text_field($feedback->text);
771             }
772         }
773         // Added because essay/questiontype.php:save_question_option is expecting a
774         // fraction property - CT 8/10/06.
775         $question->fraction[] = 1;
776         $question->defaultmark = 1;
777         $question->responseformat = 'editor';
778         $question->responsefieldlines = 15;
779         $question->attachments = 0;
781         $questions[]=$question;
782     }
784     /**
785      * Process Matching Questions
786      * Parse a matching rawquestion and add the result
787      * to the array of questions already parsed.
788      * @param object $quest rawquestion
789      * @param $questions array of Moodle questions already done.
790      */
791     public function process_matching($quest, &$questions) {
793         // Blackboard matching questions can't be imported in core Moodle without a loss in data,
794         // as core match question don't allow HTML in subanswers. The contributed ddmatch
795         // question type support HTML in subanswers.
796         // The ddmatch question type is not part of core, so we need to check if it is defined.
797         $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch');
799         $question = $this->process_common($quest);
800         $question = $this->add_blank_combined_feedback($question);
801         $question->valid = true;
802         if ($ddmatchisinstalled) {
803             $question->qtype = 'ddmatch';
804         } else {
805             $question->qtype = 'match';
806         }
807         // Construction of the array holding mappings between subanswers and subquestions.
808         foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
809             foreach ($quest->responses as $rid => $resp) {
810                 if (isset($resp->ident) && $resp->ident == $subq->ident) {
811                     $correct = $resp->correct;
812                 }
813             }
815             foreach ($subq->choices as $cid => $choice) {
816                 if ($choice == $correct) {
817                     $mappings[$subq->ident] = $cid;
818                 }
819             }
820         }
822         foreach ($subq->choices as $choiceid => $choice) {
823             $subanswertext = $quest->RIGHT_MATCH_BLOCK->matchinganswerset[$choiceid]->text;
824             if ($ddmatchisinstalled) {
825                 $subanswer = $this->cleaned_text_field($subanswertext);
826             } else {
827                 $subanswertext = html_to_text($this->cleaninput($subanswertext), 0);
828                 $subanswer = $subanswertext;
829             }
831             if ($subanswertext != '') { // Only import non empty subanswers.
832                 $subquestion = '';
834                 $fiber = array_keys ($mappings, $choiceid);
835                 foreach ($fiber as $correctanswerid) {
836                     // We have found a correspondance for this subanswer so we need to take the associated subquestion.
837                     foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
838                         $currentsubqid = $subq->ident;
839                         if (strcmp ($currentsubqid, $correctanswerid) == 0) {
840                             $subquestion = $subq->text;
841                             break;
842                         }
843                     }
844                     $question->subquestions[] = $this->cleaned_text_field($subquestion);
845                     $question->subanswers[] = $subanswer;
846                 }
848                 if ($subquestion == '') { // Then in this case, $choice is a distractor.
849                     $question->subquestions[] = $this->text_field('');
850                     $question->subanswers[] = $subanswer;
851                 }
852             }
853         }
855         // Verify that this matching question has enough subquestions and subanswers.
856         $subquestioncount = 0;
857         $subanswercount = 0;
858         $subanswers = $question->subanswers;
859         foreach ($question->subquestions as $key => $subquestion) {
860             $subquestion = $subquestion['text'];
861             $subanswer = $subanswers[$key];
862             if ($subquestion != '') {
863                 $subquestioncount++;
864             }
865             $subanswercount++;
866         }
867         if ($subquestioncount < 2 || $subanswercount < 3) {
868                 $this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext));
869         } else {
870             $questions[] = $question;
871         }
872     }
874     /**
875      * Strip the applet tag used by Blackboard to render mathml formulas,
876      * keeping the mathml tag.
877      * @param string $string
878      * @return string
879      */
880     public function strip_applet_tags_get_mathml($string) {
881         if (stristr($string, '</APPLET>') === false) {
882             return $string;
883         } else {
884             // Strip all applet tags keeping stuff before/after and inbetween (if mathml) them.
885             while (stristr($string, '</APPLET>') !== false) {
886                 preg_match("/(.*)\<applet.*value=\"(\<math\>.*\<\/math\>)\".*\<\/applet\>(.*)/i", $string, $mathmls);
887                 $string = $mathmls[1].$mathmls[2].$mathmls[3];
888             }
889             return $string;
890         }
891     }