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  2003 Scott Elliott
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 pool question importer class.
31
 *
32
 * @package    qformat_blackboard_six
33
 * @copyright  2003 Scott Elliott
34
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
class qformat_blackboard_six_pool extends qformat_blackboard_six_base {
37
    /**
38
     * @var bool Is the current question's question text escaped HTML
39
     * (true for most if not all Blackboard files).
40
     */
41
    public $ishtml = true;
42
 
43
    /**
44
     * Parse the xml document into an array of questions
45
     *
46
     * This *could* burn memory - but it won't happen that much
47
     * so fingers crossed!
48
     *
49
     * @param array $text array of lines from the input file.
50
     * @return array (of objects) questions objects.
51
     */
52
    protected function readquestions($text) {
53
 
54
        // This converts xml to big nasty data structure,
55
        // the 0 means keep white space as it is.
56
        try {
57
            $xml = xmlize($text, 0, 'UTF-8', true);
58
        } catch (xml_format_exception $e) {
59
            $this->error($e->getMessage(), '');
60
            return false;
61
        }
62
 
63
        $questions = array();
64
 
65
        $this->process_category($xml, $questions);
66
 
67
        $this->process_tf($xml, $questions);
68
        $this->process_mc($xml, $questions);
69
        $this->process_ma($xml, $questions);
70
        $this->process_fib($xml, $questions);
71
        $this->process_matching($xml, $questions);
72
        $this->process_essay($xml, $questions);
73
 
74
        return $questions;
75
    }
76
 
77
    /**
78
     * Do question import processing common to every qtype.
79
     *
80
     * @param array $questiondata the xml tree related to the current question
81
     * @return object initialized question object.
82
     */
83
    public function process_common($questiondata) {
84
 
85
        // This routine initialises the question object.
86
        $question = $this->defaultquestion();
87
 
88
        // Determine if the question is already escaped html.
89
        $this->ishtml = $this->getpath($questiondata,
90
                array('#', 'BODY', 0, '#', 'FLAGS', 0, '#', 'ISHTML', 0, '@', 'value'),
91
                false, false);
92
 
93
        // Put questiontext in question object.
94
        $text = $this->getpath($questiondata,
95
                array('#', 'BODY', 0, '#', 'TEXT', 0, '#'),
96
                '', true, get_string('importnotext', 'qformat_blackboard_six'));
97
 
98
        $questiontext = $this->cleaned_text_field($text);
99
        $question->questiontext = $questiontext['text'];
100
        $question->questiontextformat = $questiontext['format']; // Needed because add_blank_combined_feedback uses it.
101
        if (isset($questiontext['itemid'])) {
102
            $question->questiontextitemid = $questiontext['itemid'];
103
        }
104
 
105
        // Put name in question object. We must ensure it is not empty and it is less than 250 chars.
106
        $id = $this->getpath($questiondata, array('@', 'id'), '',  true);
107
        $question->name = $this->create_default_question_name($question->questiontext,
108
                get_string('defaultname', 'qformat_blackboard_six' , $id));
109
 
110
        $question->generalfeedback = '';
111
        $question->generalfeedbackformat = FORMAT_HTML;
112
        $question->generalfeedbackfiles = array();
113
 
114
        // TODO : read the mark from the POOL TITLE QUESTIONLIST section.
115
        $question->defaultmark = 1;
116
        return $question;
117
    }
118
 
119
    /**
120
     * Add a category question entry based on the pool file title
121
     * @param array $xml the xml tree
122
     * @param array $questions the questions already parsed
123
     */
124
    public function process_category($xml, &$questions) {
125
        $title = $this->getpath($xml, array('POOL', '#', 'TITLE', 0, '@', 'value'), '', true);
126
 
127
        $dummyquestion = new stdClass();
128
        $dummyquestion->qtype = 'category';
129
        $dummyquestion->category = $this->cleaninput($this->clean_question_name($title));
130
 
131
        $questions[] = $dummyquestion;
132
    }
133
 
134
    /**
135
     * Process Essay Questions
136
     * @param array $xml the xml tree
137
     * @param array $questions the questions already parsed
138
     */
139
    public function process_essay($xml, &$questions) {
140
 
141
        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_ESSAY'), false, false)) {
142
            $essayquestions = $this->getpath($xml,
143
                    array('POOL', '#', 'QUESTION_ESSAY'), false, false);
144
        } else {
145
            return;
146
        }
147
 
148
        foreach ($essayquestions as $thisquestion) {
149
 
150
            $question = $this->process_common($thisquestion);
151
 
152
            $question->qtype = 'essay';
153
 
154
            $question->answer = '';
155
            $answer = $this->getpath($thisquestion,
156
                    array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true);
157
            $question->graderinfo = $this->cleaned_text_field($answer);
158
            $question->responsetemplate = $this->text_field('');
159
            $question->feedback = '';
160
            $question->responseformat = 'editor';
161
            $question->responserequired = 1;
162
            $question->responsefieldlines = 15;
163
            $question->attachments = 0;
164
            $question->attachmentsrequired = 0;
165
            $question->fraction = 0;
166
 
167
            $questions[] = $question;
168
        }
169
    }
170
 
171
    /**
172
     * Process True / False Questions
173
     * @param array $xml the xml tree
174
     * @param array $questions the questions already parsed
175
     */
176
    public function process_tf($xml, &$questions) {
177
 
178
        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false)) {
179
            $tfquestions = $this->getpath($xml,
180
                    array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false);
181
        } else {
182
            return;
183
        }
184
 
185
        foreach ($tfquestions as $thisquestion) {
186
 
187
            $question = $this->process_common($thisquestion);
188
 
189
            $question->qtype = 'truefalse';
190
            $question->single = 1; // Only one answer is allowed.
191
 
192
            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false);
193
 
194
            $correctanswer = $this->getpath($thisquestion,
195
                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
196
                    '', true);
197
 
198
            // First choice is true, second is false.
199
            $id = $this->getpath($choices[0], array('@', 'id'), '', true);
200
            $correctfeedback = $this->getpath($thisquestion,
201
                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
202
                    '', true);
203
            $incorrectfeedback = $this->getpath($thisquestion,
204
                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
205
                    '', true);
206
            if (strcmp($id,  $correctanswer) == 0) {  // True is correct.
207
                $question->answer = 1;
208
                $question->feedbacktrue = $this->cleaned_text_field($correctfeedback);
209
                $question->feedbackfalse = $this->cleaned_text_field($incorrectfeedback);
210
            } else {  // False is correct.
211
                $question->answer = 0;
212
                $question->feedbacktrue = $this->cleaned_text_field($incorrectfeedback);
213
                $question->feedbackfalse = $this->cleaned_text_field($correctfeedback);
214
            }
215
            $question->correctanswer = $question->answer;
216
            $questions[] = $question;
217
        }
218
    }
219
 
220
    /**
221
     * Process Multiple Choice Questions with single answer
222
     * @param array $xml the xml tree
223
     * @param array $questions the questions already parsed
224
     */
225
    public function process_mc($xml, &$questions) {
226
 
227
        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false)) {
228
            $mcquestions = $this->getpath($xml,
229
                    array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false);
230
        } else {
231
            return;
232
        }
233
 
234
        foreach ($mcquestions as $thisquestion) {
235
 
236
            $question = $this->process_common($thisquestion);
237
 
238
            $correctfeedback = $this->getpath($thisquestion,
239
                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
240
                    '', true);
241
            $incorrectfeedback = $this->getpath($thisquestion,
242
                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
243
                    '', true);
244
            $question->correctfeedback = $this->cleaned_text_field($correctfeedback);
245
            $question->partiallycorrectfeedback = $this->text_field('');
246
            $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
247
 
248
            $question->qtype = 'multichoice';
249
            $question->single = 1; // Only one answer is allowed.
250
 
251
            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
252
            $correctanswerid = $this->getpath($thisquestion,
253
                        array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
254
                        '', true);
255
            foreach ($choices as $choice) {
256
                $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
257
                // Put this choice in the question object.
258
                $question->answer[] = $this->cleaned_text_field($choicetext);
259
 
260
                $choiceid = $this->getpath($choice, array('@', 'id'), '', true);
261
                // If choice is the right answer, give 100% mark, otherwise give 0%.
262
                if (strcmp ($choiceid, $correctanswerid) == 0) {
263
                    $question->fraction[] = 1;
264
                } else {
265
                    $question->fraction[] = 0;
266
                }
267
                // There is never feedback specific to each choice.
268
                $question->feedback[] = $this->text_field('');
269
            }
270
            $questions[] = $question;
271
        }
272
    }
273
 
274
    /**
275
     * Process Multiple Choice Questions With Multiple Answers
276
     * @param array $xml the xml tree
277
     * @param array $questions the questions already parsed
278
     */
279
    public function process_ma($xml, &$questions) {
280
        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false)) {
281
            $maquestions = $this->getpath($xml,
282
                    array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false);
283
        } else {
284
            return;
285
        }
286
 
287
        foreach ($maquestions as $thisquestion) {
288
            $question = $this->process_common($thisquestion);
289
 
290
            $correctfeedback = $this->getpath($thisquestion,
291
                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
292
                    '', true);
293
            $incorrectfeedback = $this->getpath($thisquestion,
294
                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
295
                    '', true);
296
            $question->correctfeedback = $this->cleaned_text_field($correctfeedback);
297
            // As there is no partially correct feedback we use incorrect one.
298
            $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
299
            $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
300
 
301
            $question->qtype = 'multichoice';
302
            $question->defaultmark = 1;
303
            $question->single = 0; // More than one answers allowed.
304
 
305
            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
306
            $correctanswerids = array();
307
            foreach ($this->getpath($thisquestion,
308
                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) {
309
                if ($correctanswer) {
310
                    $correctanswerids[] = $this->getpath($correctanswer,
311
                            array('@', 'answer_id'),
312
                            '', true);
313
                }
314
            }
315
            $fraction = 1 / count($correctanswerids);
316
 
317
            foreach ($choices as $choice) {
318
                $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
319
                // Put this choice in the question object.
320
                $question->answer[] = $this->cleaned_text_field($choicetext);
321
 
322
                $choiceid = $this->getpath($choice, array('@', 'id'), '', true);
323
 
324
                $iscorrect = in_array($choiceid, $correctanswerids);
325
 
326
                if ($iscorrect) {
327
                    $question->fraction[] = $fraction;
328
                } else {
329
                    $question->fraction[] = 0;
330
                }
331
                // There is never feedback specific to each choice.
332
                $question->feedback[] = $this->text_field('');
333
            }
334
            $questions[] = $question;
335
        }
336
    }
337
 
338
    /**
339
     * Process Fill in the Blank Questions
340
     * @param array $xml the xml tree
341
     * @param array $questions the questions already parsed
342
     */
343
    public function process_fib($xml, &$questions) {
344
        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false)) {
345
            $fibquestions = $this->getpath($xml,
346
                    array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false);
347
        } else {
348
            return;
349
        }
350
 
351
        foreach ($fibquestions as $thisquestion) {
352
 
353
            $question = $this->process_common($thisquestion);
354
 
355
            $question->qtype = 'shortanswer';
356
            $question->usecase = 0; // Ignore case.
357
 
358
            $correctfeedback = $this->getpath($thisquestion,
359
                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
360
                    '', true);
361
            $incorrectfeedback = $this->getpath($thisquestion,
362
                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
363
                    '', true);
364
            $answers = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
365
            foreach ($answers as $answer) {
366
                $question->answer[] = $this->getpath($answer,
367
                        array('#', 'TEXT', 0, '#'), '', true);
368
                $question->fraction[] = 1;
369
                $question->feedback[] = $this->cleaned_text_field($correctfeedback);
370
            }
371
            $question->answer[] = '*';
372
            $question->fraction[] = 0;
373
            $question->feedback[] = $this->cleaned_text_field($incorrectfeedback);
374
 
375
            $questions[] = $question;
376
        }
377
    }
378
 
379
    /**
380
     * Process Matching Questions
381
     * @param array $xml the xml tree
382
     * @param array $questions the questions already parsed
383
     */
384
    public function process_matching($xml, &$questions) {
385
        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MATCH'), false, false)) {
386
            $matchquestions = $this->getpath($xml,
387
                    array('POOL', '#', 'QUESTION_MATCH'), false, false);
388
        } else {
389
            return;
390
        }
391
        // Blackboard questions can't be imported in core Moodle without a loss in data,
392
        // as core match question don't allow HTML in subanswers. The contributed ddmatch
393
        // question type support HTML in subanswers.
394
        // The ddmatch question type is not part of core, so we need to check if it is defined.
395
        $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch');
396
 
397
        foreach ($matchquestions as $thisquestion) {
398
 
399
            $question = $this->process_common($thisquestion);
400
            if ($ddmatchisinstalled) {
401
                $question->qtype = 'ddmatch';
402
            } else {
403
                $question->qtype = 'match';
404
            }
405
 
406
            $correctfeedback = $this->getpath($thisquestion,
407
                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
408
                    '', true);
409
            $incorrectfeedback = $this->getpath($thisquestion,
410
                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
411
                    '', true);
412
            $question->correctfeedback = $this->cleaned_text_field($correctfeedback);
413
            // As there is no partially correct feedback we use incorrect one.
414
            $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
415
            $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
416
 
417
            $choices = $this->getpath($thisquestion,
418
                    array('#', 'CHOICE'), false, false); // Blackboard "choices" are Moodle subanswers.
419
            $answers = $this->getpath($thisquestion,
420
                    array('#', 'ANSWER'), false, false); // Blackboard "answers" are Moodle subquestions.
421
            $correctanswers = $this->getpath($thisquestion,
422
                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false); // Mapping between choices and answers.
423
            $mappings = array();
424
            foreach ($correctanswers as $correctanswer) {
425
                if ($correctanswer) {
426
                    $correctchoiceid = $this->getpath($correctanswer,
427
                                array('@', 'choice_id'), '', true);
428
                    $correctanswerid = $this->getpath($correctanswer,
429
                            array('@', 'answer_id'),
430
                            '', true);
431
                    $mappings[$correctanswerid] = $correctchoiceid;
432
                }
433
            }
434
 
435
            foreach ($choices as $choice) {
436
                if ($ddmatchisinstalled) {
437
                    $choicetext = $this->cleaned_text_field($this->getpath($choice,
438
                            array('#', 'TEXT', 0, '#'), '', true));
439
                } else {
440
                    $choicetext = trim(strip_tags($this->getpath($choice,
441
                            array('#', 'TEXT', 0, '#'), '', true)));
442
                }
443
 
444
                if ($choicetext != '') { // Only import non empty subanswers.
445
                    $subquestion = '';
446
                    $choiceid = $this->getpath($choice,
447
                            array('@', 'id'), '', true);
448
                    $fiber = array_search($choiceid, $mappings);
449
                    $fiber = moodle_array_keys_filter($mappings, $choiceid);
450
                    foreach ($fiber as $correctanswerid) {
451
                        // We have found a correspondance for this choice so we need to take the associated answer.
452
                        foreach ($answers as $answer) {
453
                            $currentanswerid = $this->getpath($answer,
454
                                    array('@', 'id'), '', true);
455
                            if (strcmp ($currentanswerid, $correctanswerid) == 0) {
456
                                $subquestion = $this->getpath($answer,
457
                                        array('#', 'TEXT', 0, '#'), '', true);
458
                                break;
459
                            }
460
                        }
461
                        $question->subquestions[] = $this->cleaned_text_field($subquestion);
462
                        $question->subanswers[] = $choicetext;
463
                    }
464
 
465
                    if ($subquestion == '') { // Then in this case, $choice is a distractor.
466
                        $question->subquestions[] = $this->text_field('');
467
                        $question->subanswers[] = $choicetext;
468
                    }
469
                }
470
            }
471
 
472
            // Verify that this matching question has enough subquestions and subanswers.
473
            $subquestioncount = 0;
474
            $subanswercount = 0;
475
            $subanswers = $question->subanswers;
476
            foreach ($question->subquestions as $key => $subquestion) {
477
                $subquestion = $subquestion['text'];
478
                $subanswer = $subanswers[$key];
479
                if ($subquestion != '') {
480
                    $subquestioncount++;
481
                }
482
                $subanswercount++;
483
            }
484
            if ($subquestioncount < 2 || $subanswercount < 3) {
485
                    $this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext));
486
            } else {
487
                $questions[] = $question;
488
            }
489
 
490
        }
491
    }
492
}