Passed
Pull Request — master (#6894)
by
unknown
09:02
created

QuizExport   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 528
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 334
c 1
b 0
f 0
dl 0
loc 528
rs 3.6
wmc 60

15 Methods

Rating   Name   Duplication   Size   Complexity  
A exportMultichoiceQuestion() 0 24 2
A getAnswersForQuestion() 0 21 5
A getQuestionsForQuiz() 0 22 4
A getFeedbacksForQuiz() 0 16 3
B mapQuestionType() 0 16 7
B exportQuestion() 0 53 7
A exportShortAnswerQuestion() 0 14 2
A exportMatchQuestion() 0 47 3
A getDefaultCategoryId() 0 3 1
A exportMultichoiceNosingleQuestion() 0 3 1
B createQuizXml() 0 106 4
B getData() 0 66 4
A export() 0 17 1
A exportAnswer() 0 9 1
C exportTrueFalseQuestion() 0 49 15

How to fix   Complexity   

Complex Class

Complex classes like QuizExport often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QuizExport, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CourseBundle\Component\CourseCopy\Moodle\Activities;
8
9
use Exception;
10
use FillBlanks;
11
12
use const PHP_EOL;
13
14
/**
15
 * Handles the export of quizzes within a course.
16
 */
17
class QuizExport extends ActivityExport
18
{
19
    /**
20
     * Export a quiz to the specified directory.
21
     *
22
     * @param int    $activityId the ID of the quiz
23
     * @param string $exportDir  the directory where the quiz will be exported
24
     * @param int    $moduleId   the ID of the module
25
     * @param int    $sectionId  the ID of the section
26
     */
27
    public function export($activityId, $exportDir, $moduleId, $sectionId): void
28
    {
29
        $quizDir = $this->prepareActivityDirectory($exportDir, 'quiz', (int) $moduleId);
30
31
        $quizData = $this->getData((int) $activityId, (int) $sectionId);
32
33
        $this->createQuizXml($quizData, $quizDir);
34
        $this->createModuleXml($quizData, $quizDir);
35
        $this->createGradesXml($quizData, $quizDir);
36
        $this->createCompletionXml($quizData, $quizDir);
37
        $this->createCommentsXml($quizData, $quizDir);
38
        $this->createCompetenciesXml($quizData, $quizDir);
39
        $this->createFiltersXml($quizData, $quizDir);
40
        $this->createGradeHistoryXml($quizData, $quizDir);
41
        $this->createInforefXml($quizData, $quizDir);
42
        $this->createRolesXml($quizData, $quizDir);
43
        $this->createCalendarXml($quizData, $quizDir);
44
    }
45
46
    /**
47
     * Retrieves the quiz data.
48
     *
49
     * @return array<string,mixed>
50
     */
51
    public function getData(int $quizId, int $sectionId): array
52
    {
53
        $quizResources = $this->course->resources[RESOURCE_QUIZ] ?? [];
54
55
        foreach ($quizResources as $quiz) {
56
            if (-1 == $quiz->obj->iid) {
57
                continue;
58
            }
59
60
            if ((int) $quiz->obj->iid === $quizId) {
61
                $contextid = (int) ($quiz->obj->c_id ?? $this->course->info['real_id'] ?? 0);
62
63
                return [
64
                    'id' => (int) $quiz->obj->iid,
65
                    'name' => (string) $quiz->obj->title,
66
                    'intro' => (string) ($quiz->obj->description ?? ''),
67
                    'timeopen' => (int) ($quiz->obj->start_time ?? 0),
68
                    'timeclose' => (int) ($quiz->obj->end_time ?? 0),
69
                    'timelimit' => (int) ($quiz->obj->timelimit ?? 0),
70
                    'grademethod' => (int) ($quiz->obj->grademethod ?? 1),
71
                    'decimalpoints' => (int) ($quiz->obj->decimalpoints ?? 2),
72
                    'sumgrades' => (float) ($quiz->obj->sumgrades ?? 0),
73
                    'grade' => (float) ($quiz->obj->grade ?? 0),
74
                    'questionsperpage' => (int) ($quiz->obj->questionsperpage ?? 1),
75
                    'preferredbehaviour' => (string) ($quiz->obj->preferredbehaviour ?? 'deferredfeedback'),
76
                    'navmethod' => (string) ($quiz->obj->navmethod ?? 'free'),
77
                    'shuffleanswers' => (int) ($quiz->obj->shuffleanswers ?? 1),
78
                    'questions' => $this->getQuestionsForQuiz($quizId),
79
                    'feedbacks' => $this->getFeedbacksForQuiz($quizId),
80
                    'sectionid' => $sectionId,
81
                    'moduleid' => (int) ($quiz->obj->iid ?? 0),
82
                    'modulename' => 'quiz',
83
                    'contextid' => $contextid,
84
                    'overduehandling' => (string) ($quiz->obj->overduehandling ?? 'autosubmit'),
85
                    'graceperiod' => (int) ($quiz->obj->graceperiod ?? 0),
86
                    'canredoquestions' => (int) ($quiz->obj->canredoquestions ?? 0),
87
                    'attempts_number' => (int) ($quiz->obj->attempts_number ?? 0),
88
                    'attemptonlast' => (int) ($quiz->obj->attemptonlast ?? 0),
89
                    'questiondecimalpoints' => (int) ($quiz->obj->questiondecimalpoints ?? 2),
90
                    'reviewattempt' => (int) ($quiz->obj->reviewattempt ?? 69888),
91
                    'reviewcorrectness' => (int) ($quiz->obj->reviewcorrectness ?? 4352),
92
                    'reviewmarks' => (int) ($quiz->obj->reviewmarks ?? 4352),
93
                    'reviewspecificfeedback' => (int) ($quiz->obj->reviewspecificfeedback ?? 4352),
94
                    'reviewgeneralfeedback' => (int) ($quiz->obj->reviewgeneralfeedback ?? 4352),
95
                    'reviewrightanswer' => (int) ($quiz->obj->reviewrightanswer ?? 4352),
96
                    'reviewoverallfeedback' => (int) ($quiz->obj->reviewoverallfeedback ?? 4352),
97
                    'timecreated' => (int) ($quiz->obj->insert_date ?? time()),
98
                    'timemodified' => (int) ($quiz->obj->lastedit_date ?? time()),
99
                    'password' => (string) ($quiz->obj->password ?? ''),
100
                    'subnet' => (string) ($quiz->obj->subnet ?? ''),
101
                    'browsersecurity' => (string) ($quiz->obj->browsersecurity ?? '-'),
102
                    'delay1' => (int) ($quiz->obj->delay1 ?? 0),
103
                    'delay2' => (int) ($quiz->obj->delay2 ?? 0),
104
                    'showuserpicture' => (int) ($quiz->obj->showuserpicture ?? 0),
105
                    'showblocks' => (int) ($quiz->obj->showblocks ?? 0),
106
                    'completionattemptsexhausted' => (int) ($quiz->obj->completionattemptsexhausted ?? 0),
107
                    'completionpass' => (int) ($quiz->obj->completionpass ?? 0),
108
                    'completionminattempts' => (int) ($quiz->obj->completionminattempts ?? 0),
109
                    'allowofflineattempts' => (int) ($quiz->obj->allowofflineattempts ?? 0),
110
                    'users' => [],
111
                    'files' => [],
112
                ];
113
            }
114
        }
115
116
        return [];
117
    }
118
119
    /**
120
     * Export one question (bank) entry in XML format.
121
     */
122
    public function exportQuestion(array $question): string
123
    {
124
        $qtype = (string) ($question['qtype'] ?? 'unknown');
125
126
        $xmlContent = '      <question id="'.(int) ($question['id'] ?? 0).'">'.PHP_EOL;
127
        $xmlContent .= '        <parent>0</parent>'.PHP_EOL;
128
        $xmlContent .= '        <name>'.htmlspecialchars((string) ($question['questiontext'] ?? 'No question text')).'</name>'.PHP_EOL;
129
        $xmlContent .= '        <questiontext>'.htmlspecialchars((string) ($question['questiontext'] ?? 'No question text')).'</questiontext>'.PHP_EOL;
130
        $xmlContent .= '        <questiontextformat>1</questiontextformat>'.PHP_EOL;
131
        $xmlContent .= '        <generalfeedback></generalfeedback>'.PHP_EOL;
132
        $xmlContent .= '        <generalfeedbackformat>1</generalfeedbackformat>'.PHP_EOL;
133
        $xmlContent .= '        <defaultmark>'.(float) ($question['maxmark'] ?? 0).'</defaultmark>'.PHP_EOL;
134
        $xmlContent .= '        <penalty>0.3333333</penalty>'.PHP_EOL;
135
        $xmlContent .= '        <qtype>'.htmlspecialchars(str_replace('_nosingle', '', $qtype) ?: 'unknown').'</qtype>'.PHP_EOL;
136
        $xmlContent .= '        <length>1</length>'.PHP_EOL;
137
        $xmlContent .= '        <stamp>moodle+'.time().'+QUESTIONSTAMP</stamp>'.PHP_EOL;
138
        $xmlContent .= '        <version>moodle+'.time().'+VERSIONSTAMP</version>'.PHP_EOL;
139
        $xmlContent .= '        <hidden>0</hidden>'.PHP_EOL;
140
        $xmlContent .= '        <timecreated>'.time().'</timecreated>'.PHP_EOL;
141
        $xmlContent .= '        <timemodified>'.time().'</timemodified>'.PHP_EOL;
142
        $xmlContent .= '        <createdby>2</createdby>'.PHP_EOL;
143
        $xmlContent .= '        <modifiedby>2</modifiedby>'.PHP_EOL;
144
145
        switch ($qtype) {
146
            case 'multichoice':
147
                $xmlContent .= $this->exportMultichoiceQuestion($question);
148
149
                break;
150
151
            case 'multichoice_nosingle':
152
                $xmlContent .= $this->exportMultichoiceNosingleQuestion($question);
153
154
                break;
155
156
            case 'truefalse':
157
                $xmlContent .= $this->exportTrueFalseQuestion($question);
158
159
                break;
160
161
            case 'shortanswer':
162
                $xmlContent .= $this->exportShortAnswerQuestion($question);
163
164
                break;
165
166
            case 'match':
167
                $xmlContent .= $this->exportMatchQuestion($question);
168
169
                break;
170
        }
171
172
        $xmlContent .= '      </question>'.PHP_EOL;
173
174
        return $xmlContent;
175
    }
176
177
    /**
178
     * @return array<int,array<string,mixed>>
179
     */
180
    private function getQuestionsForQuiz(int $quizId): array
181
    {
182
        $questions = [];
183
        $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? [];
184
185
        foreach ($quizResources as $questionId => $questionData) {
186
            if (\in_array($questionId, $this->course->resources[RESOURCE_QUIZ][$quizId]->obj->question_ids ?? [], true)) {
187
                $categoryId = (int) ($questionData->question_category ?? 0);
188
                $categoryId = $categoryId > 0 ? $categoryId : $this->getDefaultCategoryId();
189
190
                $questions[] = [
191
                    'id' => (int) $questionData->source_id,
192
                    'questiontext' => (string) $questionData->question,
193
                    'qtype' => $this->mapQuestionType((string) $questionData->quiz_type),
194
                    'questioncategoryid' => $categoryId,
195
                    'answers' => $this->getAnswersForQuestion((int) $questionData->source_id),
196
                    'maxmark' => (float) ($questionData->ponderation ?? 1),
197
                ];
198
            }
199
        }
200
201
        return $questions;
202
    }
203
204
    private function mapQuestionType(string $quizType): string
205
    {
206
        switch ($quizType) {
207
            case UNIQUE_ANSWER:     return 'multichoice';
208
209
            case MULTIPLE_ANSWER:   return 'multichoice_nosingle';
210
211
            case FILL_IN_BLANKS:    return 'match';
212
213
            case FREE_ANSWER:       return 'shortanswer';
214
215
            case CALCULATED_ANSWER: return 'calculated';
216
217
            case UPLOAD_ANSWER:     return 'fileupload';
218
219
            default:                return 'unknown';
220
        }
221
    }
222
223
    /**
224
     * @return array<int,array<string,mixed>>
225
     */
226
    private function getAnswersForQuestion(int $questionId): array
227
    {
228
        static $globalCounter = 0;
229
        $answers = [];
230
        $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? [];
231
232
        foreach ($quizResources as $questionData) {
233
            if ((int) $questionData->source_id === $questionId) {
234
                foreach ($questionData->answers as $answer) {
235
                    $globalCounter++;
236
                    $answers[] = [
237
                        'id' => $questionId * 1000 + $globalCounter,
238
                        'text' => (string) $answer['answer'],
239
                        'fraction' => (int) ('1' == $answer['correct'] ? 100 : 0),
240
                        'feedback' => (string) ($answer['comment'] ?? ''),
241
                    ];
242
                }
243
            }
244
        }
245
246
        return $answers;
247
    }
248
249
    /**
250
     * @return array<int,array<string,mixed>>
251
     */
252
    private function getFeedbacksForQuiz(int $quizId): array
253
    {
254
        $feedbacks = [];
255
        $quizResources = $this->course->resources[RESOURCE_QUIZ] ?? [];
256
257
        foreach ($quizResources as $quiz) {
258
            if ((int) $quiz->obj->iid === $quizId) {
259
                $feedbacks[] = [
260
                    'feedbacktext' => (string) ($quiz->obj->description ?? ''),
261
                    'mingrade' => 0.00000,
262
                    'maxgrade' => (float) ($quiz->obj->grade ?? 10.00000),
263
                ];
264
            }
265
        }
266
267
        return $feedbacks;
268
    }
269
270
    private function getDefaultCategoryId(): int
271
    {
272
        return 1;
273
    }
274
275
    /**
276
     * Creates the quiz.xml file.
277
     *
278
     * @param array<string,mixed> $quizData
279
     */
280
    private function createQuizXml(array $quizData, string $destinationDir): void
281
    {
282
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
283
        $xmlContent .= '<activity id="'.$quizData['id'].'" moduleid="'.$quizData['moduleid'].'" modulename="quiz" contextid="'.$quizData['contextid'].'">'.PHP_EOL;
284
        $xmlContent .= '  <quiz id="'.$quizData['id'].'">'.PHP_EOL;
285
        $xmlContent .= '    <name>'.htmlspecialchars((string) $quizData['name']).'</name>'.PHP_EOL;
286
        $xmlContent .= '    <intro>'.htmlspecialchars((string) $quizData['intro']).'</intro>'.PHP_EOL;
287
        $xmlContent .= '    <introformat>1</introformat>'.PHP_EOL;
288
        $xmlContent .= '    <timeopen>'.(int) ($quizData['timeopen'] ?? 0).'</timeopen>'.PHP_EOL;
289
        $xmlContent .= '    <timeclose>'.(int) ($quizData['timeclose'] ?? 0).'</timeclose>'.PHP_EOL;
290
        $xmlContent .= '    <timelimit>'.(int) ($quizData['timelimit'] ?? 0).'</timelimit>'.PHP_EOL;
291
        $xmlContent .= '    <overduehandling>'.htmlspecialchars((string) ($quizData['overduehandling'] ?? 'autosubmit')).'</overduehandling>'.PHP_EOL;
292
        $xmlContent .= '    <graceperiod>'.(int) ($quizData['graceperiod'] ?? 0).'</graceperiod>'.PHP_EOL;
293
        $xmlContent .= '    <preferredbehaviour>'.htmlspecialchars((string) $quizData['preferredbehaviour']).'</preferredbehaviour>'.PHP_EOL;
294
        $xmlContent .= '    <canredoquestions>'.(int) ($quizData['canredoquestions'] ?? 0).'</canredoquestions>'.PHP_EOL;
295
        $xmlContent .= '    <attempts_number>'.(int) ($quizData['attempts_number'] ?? 0).'</attempts_number>'.PHP_EOL;
296
        $xmlContent .= '    <attemptonlast>'.(int) ($quizData['attemptonlast'] ?? 0).'</attemptonlast>'.PHP_EOL;
297
        $xmlContent .= '    <grademethod>'.(int) $quizData['grademethod'].'</grademethod>'.PHP_EOL;
298
        $xmlContent .= '    <decimalpoints>'.(int) $quizData['decimalpoints'].'</decimalpoints>'.PHP_EOL;
299
        $xmlContent .= '    <questiondecimalpoints>'.(int) ($quizData['questiondecimalpoints'] ?? -1).'</questiondecimalpoints>'.PHP_EOL;
300
301
        // Review options
302
        $xmlContent .= '    <reviewattempt>'.(int) ($quizData['reviewattempt'] ?? 69888).'</reviewattempt>'.PHP_EOL;
303
        $xmlContent .= '    <reviewcorrectness>'.(int) ($quizData['reviewcorrectness'] ?? 4352).'</reviewcorrectness>'.PHP_EOL;
304
        $xmlContent .= '    <reviewmarks>'.(int) ($quizData['reviewmarks'] ?? 4352).'</reviewmarks>'.PHP_EOL;
305
        $xmlContent .= '    <reviewspecificfeedback>'.(int) ($quizData['reviewspecificfeedback'] ?? 4352).'</reviewspecificfeedback>'.PHP_EOL;
306
        $xmlContent .= '    <reviewgeneralfeedback>'.(int) ($quizData['reviewgeneralfeedback'] ?? 4352).'</reviewgeneralfeedback>'.PHP_EOL;
307
        $xmlContent .= '    <reviewrightanswer>'.(int) ($quizData['reviewrightanswer'] ?? 4352).'</reviewrightanswer>'.PHP_EOL;
308
        $xmlContent .= '    <reviewoverallfeedback>'.(int) ($quizData['reviewoverallfeedback'] ?? 4352).'</reviewoverallfeedback>'.PHP_EOL;
309
310
        // Navigation and presentation
311
        $xmlContent .= '    <questionsperpage>'.(int) $quizData['questionsperpage'].'</questionsperpage>'.PHP_EOL;
312
        $xmlContent .= '    <navmethod>'.htmlspecialchars((string) $quizData['navmethod']).'</navmethod>'.PHP_EOL;
313
        $xmlContent .= '    <shuffleanswers>'.(int) $quizData['shuffleanswers'].'</shuffleanswers>'.PHP_EOL;
314
        $xmlContent .= '    <sumgrades>'.(float) $quizData['sumgrades'].'</sumgrades>'.PHP_EOL;
315
        $xmlContent .= '    <grade>'.(float) $quizData['grade'].'</grade>'.PHP_EOL;
316
317
        // Timing and security
318
        $xmlContent .= '    <timecreated>'.(int) ($quizData['timecreated'] ?? time()).'</timecreated>'.PHP_EOL;
319
        $xmlContent .= '    <timemodified>'.(int) ($quizData['timemodified'] ?? time()).'</timemodified>'.PHP_EOL;
320
        $xmlContent .= '    <password>'.htmlspecialchars((string) ($quizData['password'] ?? '')).'</password>'.PHP_EOL;
321
        $xmlContent .= '    <subnet>'.htmlspecialchars((string) ($quizData['subnet'] ?? '')).'</subnet>'.PHP_EOL;
322
        $xmlContent .= '    <browsersecurity>'.htmlspecialchars((string) ($quizData['browsersecurity'] ?? '-')).'</browsersecurity>'.PHP_EOL;
323
        $xmlContent .= '    <delay1>'.(int) ($quizData['delay1'] ?? 0).'</delay1>'.PHP_EOL;
324
        $xmlContent .= '    <delay2>'.(int) ($quizData['delay2'] ?? 0).'</delay2>'.PHP_EOL;
325
326
        // Additional options
327
        $xmlContent .= '    <showuserpicture>'.(int) ($quizData['showuserpicture'] ?? 0).'</showuserpicture>'.PHP_EOL;
328
        $xmlContent .= '    <showblocks>'.(int) ($quizData['showblocks'] ?? 0).'</showblocks>'.PHP_EOL;
329
        $xmlContent .= '    <completionattemptsexhausted>'.(int) ($quizData['completionattemptsexhausted'] ?? 0).'</completionattemptsexhausted>'.PHP_EOL;
330
        $xmlContent .= '    <completionpass>'.(int) ($quizData['completionpass'] ?? 0).'</completionpass>'.PHP_EOL;
331
        $xmlContent .= '    <completionminattempts>'.(int) ($quizData['completionminattempts'] ?? 0).'</completionminattempts>'.PHP_EOL;
332
        $xmlContent .= '    <allowofflineattempts>'.(int) ($quizData['allowofflineattempts'] ?? 0).'</allowofflineattempts>'.PHP_EOL;
333
334
        // Subplugin placeholder
335
        $xmlContent .= '    <subplugin_quizaccess_seb_quiz>'.PHP_EOL;
336
        $xmlContent .= '    </subplugin_quizaccess_seb_quiz>'.PHP_EOL;
337
338
        // Question instances
339
        $xmlContent .= '    <question_instances>'.PHP_EOL;
340
        $slotIndex = 1;
341
        foreach (($quizData['questions'] ?? []) as $question) {
342
            $xmlContent .= '      <question_instance id="'.(int) ($question['id'] ?? 0).'">'.PHP_EOL;
343
            $xmlContent .= '        <slot>'.$slotIndex.'</slot>'.PHP_EOL;
344
            $xmlContent .= '        <page>1</page>'.PHP_EOL;
345
            $xmlContent .= '        <requireprevious>0</requireprevious>'.PHP_EOL;
346
            $xmlContent .= '        <questionid>'.(int) ($question['id'] ?? 0).'</questionid>'.PHP_EOL;
347
            $xmlContent .= '        <questioncategoryid>'.(int) ($question['questioncategoryid'] ?? 0).'</questioncategoryid>'.PHP_EOL;
348
            $xmlContent .= '        <includingsubcategories>$@NULL@$</includingsubcategories>'.PHP_EOL;
349
            $xmlContent .= '        <maxmark>'.(float) ($question['maxmark'] ?? 0).'</maxmark>'.PHP_EOL;
350
            $xmlContent .= '      </question_instance>'.PHP_EOL;
351
            $slotIndex++;
352
        }
353
        $xmlContent .= '    </question_instances>'.PHP_EOL;
354
355
        // Sections
356
        $xmlContent .= '    <sections>'.PHP_EOL;
357
        $xmlContent .= '      <section id="'.(int) ($quizData['id'] ?? 0).'">'.PHP_EOL;
358
        $xmlContent .= '        <firstslot>1</firstslot>'.PHP_EOL;
359
        $xmlContent .= '        <shufflequestions>0</shufflequestions>'.PHP_EOL;
360
        $xmlContent .= '      </section>'.PHP_EOL;
361
        $xmlContent .= '    </sections>'.PHP_EOL;
362
363
        // Feedbacks
364
        $xmlContent .= '    <feedbacks>'.PHP_EOL;
365
        foreach (($quizData['feedbacks'] ?? []) as $feedback) {
366
            $xmlContent .= '      <feedback id="'.(int) ($quizData['id'] ?? 0).'">'.PHP_EOL;
367
            $xmlContent .= '        <feedbacktext>'.htmlspecialchars((string) ($feedback['feedbacktext'] ?? '')).'</feedbacktext>'.PHP_EOL;
368
            $xmlContent .= '        <feedbacktextformat>1</feedbacktextformat>'.PHP_EOL;
369
            $xmlContent .= '        <mingrade>'.(float) ($feedback['mingrade'] ?? 0).'</mingrade>'.PHP_EOL;
370
            $xmlContent .= '        <maxgrade>'.(float) ($feedback['maxgrade'] ?? 0).'</maxgrade>'.PHP_EOL;
371
            $xmlContent .= '      </feedback>'.PHP_EOL;
372
        }
373
        $xmlContent .= '    </feedbacks>'.PHP_EOL;
374
375
        // Placeholders
376
        $xmlContent .= '    <overrides></overrides>'.PHP_EOL;
377
        $xmlContent .= '    <grades></grades>'.PHP_EOL;
378
        $xmlContent .= '    <attempts></attempts>'.PHP_EOL;
379
380
        $xmlContent .= '  </quiz>'.PHP_EOL;
381
        $xmlContent .= '</activity>'.PHP_EOL;
382
383
        $xmlFile = $destinationDir.'/quiz.xml';
384
        if (false === file_put_contents($xmlFile, $xmlContent)) {
385
            throw new Exception(get_lang('ErrorCreatingQuizXml'));
386
        }
387
    }
388
389
    private function exportMultichoiceQuestion(array $question): string
390
    {
391
        $xmlContent = '        <plugin_qtype_multichoice_question>'.PHP_EOL;
392
        $xmlContent .= '          <answers>'.PHP_EOL;
393
        foreach (($question['answers'] ?? []) as $answer) {
394
            $xmlContent .= $this->exportAnswer($answer);
395
        }
396
        $xmlContent .= '          </answers>'.PHP_EOL;
397
        $xmlContent .= '          <multichoice id="'.(int) ($question['id'] ?? 0).'">'.PHP_EOL;
398
        $xmlContent .= '            <layout>0</layout>'.PHP_EOL;
399
        $xmlContent .= '            <single>1</single>'.PHP_EOL;
400
        $xmlContent .= '            <shuffleanswers>1</shuffleanswers>'.PHP_EOL;
401
        $xmlContent .= '            <correctfeedback>Your answer is correct.</correctfeedback>'.PHP_EOL;
402
        $xmlContent .= '            <correctfeedbackformat>1</correctfeedbackformat>'.PHP_EOL;
403
        $xmlContent .= '            <partiallycorrectfeedback>Your answer is partially correct.</partiallycorrectfeedback>'.PHP_EOL;
404
        $xmlContent .= '            <partiallycorrectfeedbackformat>1</partiallycorrectfeedbackformat>'.PHP_EOL;
405
        $xmlContent .= '            <incorrectfeedback>Your answer is incorrect.</incorrectfeedback>'.PHP_EOL;
406
        $xmlContent .= '            <incorrectfeedbackformat>1</incorrectfeedbackformat>'.PHP_EOL;
407
        $xmlContent .= '            <answernumbering>abc</answernumbering>'.PHP_EOL;
408
        $xmlContent .= '            <shownumcorrect>1</shownumcorrect>'.PHP_EOL;
409
        $xmlContent .= '          </multichoice>'.PHP_EOL;
410
        $xmlContent .= '        </plugin_qtype_multichoice_question>'.PHP_EOL;
411
412
        return $xmlContent;
413
    }
414
415
    private function exportMultichoiceNosingleQuestion(array $question): string
416
    {
417
        return str_replace('<single>1</single>', '<single>0</single>', $this->exportMultichoiceQuestion($question));
418
    }
419
420
    private function exportTrueFalseQuestion(array $question): string
421
    {
422
        $xmlContent = '        <plugin_qtype_truefalse_question>'.PHP_EOL;
423
        $xmlContent .= '          <answers>'.PHP_EOL;
424
425
        // Normalize array to avoid non-sequential indexes
426
        $answers = array_values($question['answers'] ?? []);
427
428
        foreach ($answers as $answer) {
429
            $xmlContent .= $this->exportAnswer($answer);
430
        }
431
        $xmlContent .= '          </answers>'.PHP_EOL;
432
433
        // Robust mapping: determine true/false ids without assuming positions
434
        $trueId = 0;
435
        $falseId = 0;
436
437
        foreach ($answers as $ans) {
438
            $id = (int) ($ans['id'] ?? 0);
439
            $fraction = (int) ($ans['fraction'] ?? 0); // 100 means correct (true), 0 means false
440
            if ($fraction > 0 && 0 === $trueId) {
441
                $trueId = $id;
442
            } elseif ($fraction <= 0 && 0 === $falseId) {
443
                $falseId = $id;
444
            }
445
        }
446
447
        // Fallbacks to avoid undefined references in odd data cases
448
        if (0 === $trueId && isset($answers[0]['id'])) {
449
            $trueId = (int) $answers[0]['id'];
450
        }
451
        if (0 === $falseId && isset($answers[1]['id'])) {
452
            $falseId = (int) $answers[1]['id'];
453
        }
454
        // As last resort, mirror one id so XML stays valid
455
        if (0 === $trueId && $falseId > 0) {
456
            $trueId = $falseId;
457
        }
458
        if (0 === $falseId && $trueId > 0) {
459
            $falseId = $trueId;
460
        }
461
462
        $xmlContent .= '          <truefalse id="'.(int) ($question['id'] ?? 0).'">'.PHP_EOL;
463
        $xmlContent .= '            <trueanswer>'.$trueId.'</trueanswer>'.PHP_EOL;
464
        $xmlContent .= '            <falseanswer>'.$falseId.'</falseanswer>'.PHP_EOL;
465
        $xmlContent .= '          </truefalse>'.PHP_EOL;
466
        $xmlContent .= '        </plugin_qtype_truefalse_question>'.PHP_EOL;
467
468
        return $xmlContent;
469
    }
470
471
    private function exportShortAnswerQuestion(array $question): string
472
    {
473
        $xmlContent = '        <plugin_qtype_shortanswer_question>'.PHP_EOL;
474
        $xmlContent .= '          <answers>'.PHP_EOL;
475
        foreach (($question['answers'] ?? []) as $answer) {
476
            $xmlContent .= $this->exportAnswer($answer);
477
        }
478
        $xmlContent .= '          </answers>'.PHP_EOL;
479
        $xmlContent .= '          <shortanswer id="'.(int) ($question['id'] ?? 0).'">'.PHP_EOL;
480
        $xmlContent .= '            <usecase>0</usecase>'.PHP_EOL;
481
        $xmlContent .= '          </shortanswer>'.PHP_EOL;
482
        $xmlContent .= '        </plugin_qtype_shortanswer_question>'.PHP_EOL;
483
484
        return $xmlContent;
485
    }
486
487
    private function exportMatchQuestion(array $question): string
488
    {
489
        $xmlContent = '        <plugin_qtype_match_question>'.PHP_EOL;
490
        $xmlContent .= '          <matchoptions id="'.htmlspecialchars((string) ($question['id'] ?? '0')).'">'.PHP_EOL;
491
        $xmlContent .= '            <shuffleanswers>1</shuffleanswers>'.PHP_EOL;
492
        $xmlContent .= '            <correctfeedback>'.htmlspecialchars((string) ($question['correctfeedback'] ?? '')).'</correctfeedback>'.PHP_EOL;
493
        $xmlContent .= '            <correctfeedbackformat>0</correctfeedbackformat>'.PHP_EOL;
494
        $xmlContent .= '            <partiallycorrectfeedback>'.htmlspecialchars((string) ($question['partiallycorrectfeedback'] ?? '')).'</partiallycorrectfeedback>'.PHP_EOL;
495
        $xmlContent .= '            <partiallycorrectfeedbackformat>0</partiallycorrectfeedbackformat>'.PHP_EOL;
496
        $xmlContent .= '            <incorrectfeedback>'.htmlspecialchars((string) ($question['incorrectfeedback'] ?? '')).'</incorrectfeedback>'.PHP_EOL;
497
        $xmlContent .= '            <incorrectfeedbackformat>0</incorrectfeedbackformat>'.PHP_EOL;
498
        $xmlContent .= '            <shownumcorrect>0</shownumcorrect>'.PHP_EOL;
499
        $xmlContent .= '          </matchoptions>'.PHP_EOL;
500
        $xmlContent .= '          <matches>'.PHP_EOL;
501
502
        // Be defensive: not all datasets will have answers[0]['text'] populated
503
        $answers = array_values($question['answers'] ?? []);
504
        $firstText = (string) ($answers[0]['text'] ?? '');
505
506
        $res = FillBlanks::getAnswerInfo($firstText);
507
        $words = (array) ($res['words'] ?? []);
508
        $common = (array) ($res['common_words'] ?? []);
509
510
        // Use the shortest length to avoid "Undefined array key" warnings
511
        $limit = min(\count($common), \count($words));
512
513
        for ($i = 0; $i < $limit; $i++) {
514
            $leftRaw = (string) ($common[$i] ?? '');
515
            $left = htmlspecialchars(trim(strip_tags($leftRaw)));
516
            if ('' === $left) {
517
                continue;
518
            }
519
520
            $pair = explode('|', (string) ($words[$i] ?? ''));
521
            $right = htmlspecialchars((string) ($pair[0] ?? ''));
522
523
            $xmlContent .= '            <match id="'.($i + 1).'">'.PHP_EOL;
524
            $xmlContent .= '              <questiontext>'.$left.'</questiontext>'.PHP_EOL;
525
            $xmlContent .= '              <questiontextformat>0</questiontextformat>'.PHP_EOL;
526
            $xmlContent .= '              <answertext>'.$right.'</answertext>'.PHP_EOL;
527
            $xmlContent .= '            </match>'.PHP_EOL;
528
        }
529
530
        $xmlContent .= '          </matches>'.PHP_EOL;
531
        $xmlContent .= '        </plugin_qtype_match_question>'.PHP_EOL;
532
533
        return $xmlContent;
534
    }
535
536
    private function exportAnswer(array $answer): string
537
    {
538
        return '            <answer id="'.(int) ($answer['id'] ?? 0).'">'.PHP_EOL
539
            .'              <answertext>'.htmlspecialchars((string) ($answer['text'] ?? 'No answer text')).'</answertext>'.PHP_EOL
540
            .'              <answerformat>1</answerformat>'.PHP_EOL
541
            .'              <fraction>'.(int) ($answer['fraction'] ?? 0).'</fraction>'.PHP_EOL
542
            .'              <feedback>'.htmlspecialchars((string) ($answer['feedback'] ?? '')).'</feedback>'.PHP_EOL
543
            .'              <feedbackformat>1</feedbackformat>'.PHP_EOL
544
            .'            </answer>'.PHP_EOL;
545
    }
546
}
547