prepareActivityDirectory($exportDir, 'quiz', $moduleId); // Retrieve quiz data $quizData = $this->getData($activityId, $sectionId); // Generate XML files $this->createQuizXml($quizData, $quizDir); $this->createModuleXml($quizData, $quizDir); $this->createGradesXml($quizData, $quizDir); $this->createCompletionXml($quizData, $quizDir); $this->createCommentsXml($quizData, $quizDir); $this->createCompetenciesXml($quizData, $quizDir); $this->createFiltersXml($quizData, $quizDir); $this->createGradeHistoryXml($quizData, $quizDir); $this->createInforefXml($quizData, $quizDir); $this->createRolesXml($quizData, $quizDir); $this->createCalendarXml($quizData, $quizDir); } /** * Retrieves the quiz data. */ public function getData(int $quizId, int $sectionId): array { $quizResources = $this->course->resources[RESOURCE_QUIZ]; foreach ($quizResources as $quiz) { if ($quiz->obj->iid == -1) { continue; } if ($quiz->obj->iid == $quizId) { $contextid = $quiz->obj->c_id; return [ 'id' => $quiz->obj->iid, 'name' => $quiz->obj->title, 'intro' => $quiz->obj->description, 'timeopen' => $quiz->obj->start_time ?? 0, 'timeclose' => $quiz->obj->end_time ?? 0, 'timelimit' => $quiz->obj->timelimit ?? 0, 'grademethod' => $quiz->obj->grademethod ?? 1, 'decimalpoints' => $quiz->obj->decimalpoints ?? 2, 'sumgrades' => $quiz->obj->sumgrades ?? 0, 'grade' => $quiz->obj->grade ?? 0, 'questionsperpage' => $quiz->obj->questionsperpage ?? 1, 'preferredbehaviour' => $quiz->obj->preferredbehaviour ?? 'deferredfeedback', 'shuffleanswers' => $quiz->obj->shuffleanswers ?? 1, 'questions' => $this->getQuestionsForQuiz($quizId), 'feedbacks' => $this->getFeedbacksForQuiz($quizId), 'sectionid' => $sectionId, 'moduleid' => $quiz->obj->iid ?? 0, 'modulename' => 'quiz', 'contextid' => $contextid, 'overduehandling' => $quiz->obj->overduehandling ?? 'autosubmit', 'graceperiod' => $quiz->obj->graceperiod ?? 0, 'canredoquestions' => $quiz->obj->canredoquestions ?? 0, 'attempts_number' => $quiz->obj->attempts_number ?? 0, 'attemptonlast' => $quiz->obj->attemptonlast ?? 0, 'questiondecimalpoints' => $quiz->obj->questiondecimalpoints ?? 2, 'reviewattempt' => $quiz->obj->reviewattempt ?? 0, 'reviewcorrectness' => $quiz->obj->reviewcorrectness ?? 0, 'reviewmarks' => $quiz->obj->reviewmarks ?? 0, 'reviewspecificfeedback' => $quiz->obj->reviewspecificfeedback ?? 0, 'reviewgeneralfeedback' => $quiz->obj->reviewgeneralfeedback ?? 0, 'reviewrightanswer' => $quiz->obj->reviewrightanswer ?? 0, 'reviewoverallfeedback' => $quiz->obj->reviewoverallfeedback ?? 0, 'timecreated' => $quiz->obj->insert_date ?? time(), 'timemodified' => $quiz->obj->lastedit_date ?? time(), 'password' => $quiz->obj->password ?? '', 'subnet' => $quiz->obj->subnet ?? '', 'browsersecurity' => $quiz->obj->browsersecurity ?? '-', 'delay1' => $quiz->obj->delay1 ?? 0, 'delay2' => $quiz->obj->delay2 ?? 0, 'showuserpicture' => $quiz->obj->showuserpicture ?? 0, 'showblocks' => $quiz->obj->showblocks ?? 0, 'completionattemptsexhausted' => $quiz->obj->completionattemptsexhausted ?? 0, 'completionpass' => $quiz->obj->completionpass ?? 0, 'completionminattempts' => $quiz->obj->completionminattempts ?? 0, 'allowofflineattempts' => $quiz->obj->allowofflineattempts ?? 0, 'users' => [], 'files' => [], ]; } } return []; } /** * Exports a question in XML format. */ public function exportQuestion(array $question): string { $xmlContent = ' '.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= ' '.htmlspecialchars($question['questiontext'] ?? 'No question text').''.PHP_EOL; $xmlContent .= ' '.htmlspecialchars($question['questiontext'] ?? 'No question text').''.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' '.($question['maxmark'] ?? '0').''.PHP_EOL; $xmlContent .= ' 0.3333333'.PHP_EOL; $xmlContent .= ' '.htmlspecialchars(str_replace('_nosingle', '', $question['qtype']) ?? 'unknown').''.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' moodle+'.time().'+QUESTIONSTAMP'.PHP_EOL; $xmlContent .= ' moodle+'.time().'+VERSIONSTAMP'.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= ' '.time().''.PHP_EOL; $xmlContent .= ' '.time().''.PHP_EOL; $xmlContent .= ' 2'.PHP_EOL; $xmlContent .= ' 2'.PHP_EOL; // Add question type-specific content switch ($question['qtype']) { case 'multichoice': $xmlContent .= $this->exportMultichoiceQuestion($question); break; case 'multichoice_nosingle': $xmlContent .= $this->exportMultichoiceNosingleQuestion($question); break; case 'truefalse': $xmlContent .= $this->exportTrueFalseQuestion($question); break; case 'shortanswer': $xmlContent .= $this->exportShortAnswerQuestion($question); break; case 'match': $xmlContent .= $this->exportMatchQuestion($question); break; } $xmlContent .= ' '.PHP_EOL; return $xmlContent; } /** * Retrieves the questions for a specific quiz. */ private function getQuestionsForQuiz(int $quizId): array { $questions = []; $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? []; foreach ($quizResources as $questionId => $questionData) { if (in_array($questionId, $this->course->resources[RESOURCE_QUIZ][$quizId]->obj->question_ids)) { $categoryId = $questionData->question_category ?? 0; $categoryId = $categoryId > 0 ? $categoryId : $this->getDefaultCategoryId(); $questions[] = [ 'id' => $questionData->source_id, 'questiontext' => $questionData->question, 'qtype' => $this->mapQuestionType($questionData->quiz_type), 'questioncategoryid' => $categoryId, 'answers' => $this->getAnswersForQuestion($questionData->source_id), 'maxmark' => $questionData->ponderation ?? 1, ]; } } return $questions; } /** * Maps the quiz type code to a descriptive string. */ private function mapQuestionType(string $quizType): string { switch ($quizType) { case UNIQUE_ANSWER: return 'multichoice'; case MULTIPLE_ANSWER: return 'multichoice_nosingle'; case FILL_IN_BLANKS: return 'match'; case FREE_ANSWER: return 'shortanswer'; case CALCULATED_ANSWER: return 'calculated'; case UPLOAD_ANSWER: return 'fileupload'; default: return 'unknown'; } } /** * Retrieves the answers for a specific question ID. */ private function getAnswersForQuestion(int $questionId): array { static $globalCounter = 0; $answers = []; $quizResources = $this->course->resources[RESOURCE_QUIZQUESTION] ?? []; foreach ($quizResources as $questionData) { if ($questionData->source_id == $questionId) { foreach ($questionData->answers as $answer) { $globalCounter++; $answers[] = [ 'id' => $questionId * 1000 + $globalCounter, 'text' => $answer['answer'], 'fraction' => $answer['correct'] == '1' ? 100 : 0, 'feedback' => $answer['comment'], ]; } } } return $answers; } /** * Retrieves feedbacks for a specific quiz. */ private function getFeedbacksForQuiz(int $quizId): array { $feedbacks = []; $quizResources = $this->course->resources[RESOURCE_QUIZ] ?? []; foreach ($quizResources as $quiz) { if ($quiz->obj->iid == $quizId) { $feedbacks[] = [ 'feedbacktext' => $quiz->obj->description ?? '', 'mingrade' => 0.00000, 'maxgrade' => $quiz->obj->grade ?? 10.00000, ]; } } return $feedbacks; } private function getDefaultCategoryId(): int { return 1; } /** * Creates the quiz.xml file. */ private function createQuizXml(array $quizData, string $destinationDir): void { $xmlContent = ''.PHP_EOL; $xmlContent .= ''.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.htmlspecialchars($quizData['name']).''.PHP_EOL; $xmlContent .= ' '.htmlspecialchars($quizData['intro']).''.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' '.($quizData['timeopen'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['timeclose'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['timelimit'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['overduehandling'] ?? 'autosubmit').''.PHP_EOL; $xmlContent .= ' '.($quizData['graceperiod'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.htmlspecialchars($quizData['preferredbehaviour']).''.PHP_EOL; $xmlContent .= ' '.($quizData['canredoquestions'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['attempts_number'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['attemptonlast'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.$quizData['grademethod'].''.PHP_EOL; $xmlContent .= ' '.$quizData['decimalpoints'].''.PHP_EOL; $xmlContent .= ' '.($quizData['questiondecimalpoints'] ?? -1).''.PHP_EOL; // Review options $xmlContent .= ' '.($quizData['reviewattempt'] ?? 69888).''.PHP_EOL; $xmlContent .= ' '.($quizData['reviewcorrectness'] ?? 4352).''.PHP_EOL; $xmlContent .= ' '.($quizData['reviewmarks'] ?? 4352).''.PHP_EOL; $xmlContent .= ' '.($quizData['reviewspecificfeedback'] ?? 4352).''.PHP_EOL; $xmlContent .= ' '.($quizData['reviewgeneralfeedback'] ?? 4352).''.PHP_EOL; $xmlContent .= ' '.($quizData['reviewrightanswer'] ?? 4352).''.PHP_EOL; $xmlContent .= ' '.($quizData['reviewoverallfeedback'] ?? 4352).''.PHP_EOL; // Navigation and presentation settings $xmlContent .= ' '.$quizData['questionsperpage'].''.PHP_EOL; $xmlContent .= ' '.htmlspecialchars($quizData['navmethod']).''.PHP_EOL; $xmlContent .= ' '.$quizData['shuffleanswers'].''.PHP_EOL; $xmlContent .= ' '.$quizData['sumgrades'].''.PHP_EOL; $xmlContent .= ' '.$quizData['grade'].''.PHP_EOL; // Timing and security $xmlContent .= ' '.($quizData['timecreated'] ?? time()).''.PHP_EOL; $xmlContent .= ' '.($quizData['timemodified'] ?? time()).''.PHP_EOL; $xmlContent .= ' '.(isset($quizData['password']) ? htmlspecialchars($quizData['password']) : '').''.PHP_EOL; $xmlContent .= ' '.(isset($quizData['subnet']) ? htmlspecialchars($quizData['subnet']) : '').''.PHP_EOL; $xmlContent .= ' '.(isset($quizData['browsersecurity']) ? htmlspecialchars($quizData['browsersecurity']) : '-').''.PHP_EOL; $xmlContent .= ' '.($quizData['delay1'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['delay2'] ?? 0).''.PHP_EOL; // Additional options $xmlContent .= ' '.($quizData['showuserpicture'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['showblocks'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['completionattemptsexhausted'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['completionpass'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['completionminattempts'] ?? 0).''.PHP_EOL; $xmlContent .= ' '.($quizData['allowofflineattempts'] ?? 0).''.PHP_EOL; // Subplugin, if applicable $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; // Add question instances $xmlContent .= ' '.PHP_EOL; $slotIndex = 1; foreach ($quizData['questions'] as $question) { $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.$slotIndex.''.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= ' '.$question['id'].''.PHP_EOL; $xmlContent .= ' '.$question['questioncategoryid'].''.PHP_EOL; $xmlContent .= ' $@NULL@$'.PHP_EOL; $xmlContent .= ' '.$question['maxmark'].''.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $slotIndex++; } $xmlContent .= ' '.PHP_EOL; // Quiz sections $xmlContent .= ' '.PHP_EOL; $xmlContent .= '
'.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= '
'.PHP_EOL; $xmlContent .= '
'.PHP_EOL; // Add feedbacks $xmlContent .= ' '.PHP_EOL; foreach ($quizData['feedbacks'] as $feedback) { $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.htmlspecialchars($feedback['feedbacktext']).''.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' '.$feedback['mingrade'].''.PHP_EOL; $xmlContent .= ' '.$feedback['maxgrade'].''.PHP_EOL; $xmlContent .= ' '.PHP_EOL; } $xmlContent .= ' '.PHP_EOL; // Complete with placeholders for attempts and grades $xmlContent .= ' '.PHP_EOL.' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL.' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL.' '.PHP_EOL; // Close the activity tag $xmlContent .= '
'.PHP_EOL; $xmlContent .= '
'.PHP_EOL; // Save the XML file $xmlFile = $destinationDir.'/quiz.xml'; if (file_put_contents($xmlFile, $xmlContent) === false) { throw new Exception(get_lang('ErrorCreatingQuizXml')); } } /** * Exports a multiple-choice question in XML format. */ private function exportMultichoiceQuestion(array $question): string { $xmlContent = ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; foreach ($question['answers'] as $answer) { $xmlContent .= $this->exportAnswer($answer); } $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' Your answer is correct.'.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' Your answer is partially correct.'.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' Your answer is incorrect.'.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' abc'.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; return $xmlContent; } /** * Exports a multiple-choice question with single=0 in XML format. */ private function exportMultichoiceNosingleQuestion(array $question): string { // Similar structure to exportMultichoiceQuestion, but with single=0 $xmlContent = str_replace('1', '0', $this->exportMultichoiceQuestion($question)); return $xmlContent; } /** * Exports a true/false question in XML format. */ private function exportTrueFalseQuestion(array $question): string { $xmlContent = ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; foreach ($question['answers'] as $answer) { $xmlContent .= $this->exportAnswer($answer); } $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $trueId = $question['answers'][0]['id'] ?? 0; $falseId = $question['answers'][1]['id'] ?? 0; $xmlContent .= ' '.$trueId.''.PHP_EOL; $xmlContent .= ' '.$falseId.''.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; return $xmlContent; } /** * Exports a short answer question in XML format. */ private function exportShortAnswerQuestion(array $question): string { $xmlContent = ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; foreach ($question['answers'] as $answer) { $xmlContent .= $this->exportAnswer($answer); } $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; return $xmlContent; } /** * Exports a matching question in XML format. */ private function exportMatchQuestion(array $question): string { $xmlContent = ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' 1'.PHP_EOL; $xmlContent .= ' '.htmlspecialchars($question['correctfeedback'] ?? '').''.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= ' '.htmlspecialchars($question['partiallycorrectfeedback'] ?? '').''.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= ' '.htmlspecialchars($question['incorrectfeedback'] ?? '').''.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; $res = FillBlanks::getAnswerInfo($question['answers'][0]['text']); $words = $res['words']; $common_words = $res['common_words']; for ($i = 0; $i < count($common_words); $i++) { $answer = htmlspecialchars(trim(strip_tags($common_words[$i]))); if (!empty(trim($answer))) { $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.$answer.''.PHP_EOL; $xmlContent .= ' 0'.PHP_EOL; $xmlContent .= ' '.htmlspecialchars(explode('|', $words[$i])[0]).''.PHP_EOL; $xmlContent .= ' '.PHP_EOL; } } $xmlContent .= ' '.PHP_EOL; $xmlContent .= ' '.PHP_EOL; return $xmlContent; } /** * Exports an answer in XML format. */ private function exportAnswer(array $answer): string { return ' '.PHP_EOL. ' '.htmlspecialchars($answer['text'] ?? 'No answer text').''.PHP_EOL. ' 1'.PHP_EOL. ' '.($answer['fraction'] ?? '0').''.PHP_EOL. ' '.htmlspecialchars($answer['feedback'] ?? '').''.PHP_EOL. ' 1'.PHP_EOL. ' '.PHP_EOL; } }