Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
require_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
 */
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 $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 = moodle_array_keys_filter($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
}