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;
}
}