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