Files
formations-app/ops/export-quiz-bank.php
2026-06-11 00:04:24 +02:00

108 lines
4.1 KiB
PHP

<?php
// Read-only export of the Moodle quiz question bank as ONE JSON object on
// stdout, consumed by the Grist QuizBank push (SP3). Never writes to the DB.
// Run: docker cp ops/export-quiz-bank.php moodle:/tmp/ &&
// docker exec moodle php /tmp/export-quiz-bank.php > quizbank.json
//
// Moodle 4.5 schema chain (quiz_slots no longer carries questionid):
// quiz_slots.id -> question_references (component='mod_quiz',
// questionarea='slot', itemid=slot.id) -> questionbankentryid ->
// question_versions (latest version; qr.version NULL = "always latest")
// -> question. Multichoice flag in qtype_multichoice_options.single,
// choices in question_answers (fraction>0 = correct).
//
// Output shape:
// {courses: {"<courseid>": {quizzes: [{quiz_n, cmid, name, gradepass,
// questions: [{id, text, single, choices: [{id, text, correct,
// feedback}]}]}]}}}
//
// WARNING: the output contains the CORRECT flags and feedback — it must
// only ever land in Grist (API-key protected), NEVER in this public repo.
define('CLI_SCRIPT', true);
require('/var/www/html/config.php');
$quizmoduleid = $DB->get_field('modules', 'id', ['name' => 'quiz'], MUST_EXIST);
// Quizzes are named "Quiz N : ..." — name order is module order.
$quizzes = $DB->get_records_sql("
SELECT q.id, q.course, q.name, cm.id AS cmid
FROM {quiz} q
JOIN {course_modules} cm ON cm.instance = q.id AND cm.module = :mod
WHERE q.course > 1
ORDER BY q.course ASC, q.name ASC", ['mod' => $quizmoduleid]);
$out = ['courses' => []];
foreach ($quizzes as $quiz) {
$gradepass = $DB->get_field('grade_items', 'gradepass',
['itemmodule' => 'quiz', 'iteminstance' => $quiz->id]);
$gradepass = $gradepass ? (float)$gradepass : 0.0; // may be 0/NULL
$quizn = 0;
if (preg_match('/^Quiz\s+(\d+)/u', $quiz->name, $m)) {
$quizn = (int)$m[1];
}
$slotrows = $DB->get_records_sql("
SELECT qs.id AS slotid, qs.slot, qv.questionid
FROM {quiz_slots} qs
JOIN {question_references} qr
ON qr.component = 'mod_quiz'
AND qr.questionarea = 'slot'
AND qr.itemid = qs.id
JOIN {question_versions} qv
ON qv.questionbankentryid = qr.questionbankentryid
AND qv.version = (SELECT MAX(v.version)
FROM {question_versions} v
WHERE v.questionbankentryid
= qr.questionbankentryid)
WHERE qs.quizid = :quizid
ORDER BY qs.slot ASC", ['quizid' => $quiz->id]);
$questions = [];
foreach ($slotrows as $row) {
$question = $DB->get_record('question',
['id' => $row->questionid], '*', MUST_EXIST);
if ($question->qtype !== 'multichoice') {
fwrite(STDERR, "skip q{$question->id}: qtype "
. "{$question->qtype} (only multichoice supported)\n");
continue;
}
$opts = $DB->get_record('qtype_multichoice_options',
['questionid' => $question->id], '*', MUST_EXIST);
$answers = $DB->get_records('question_answers',
['question' => $question->id], 'id ASC');
$choices = [];
foreach ($answers as $a) {
$choices[] = [
'id' => (int)$a->id,
'text' => $a->answer,
'correct' => (float)$a->fraction > 0,
'feedback' => $a->feedback,
];
}
$questions[] = [
'id' => (int)$question->id,
'text' => $question->questiontext,
'single' => (int)$opts->single,
'choices' => $choices,
];
}
$courseid = (string)$quiz->course;
if (!isset($out['courses'][$courseid])) {
$out['courses'][$courseid] = ['quizzes' => []];
}
$out['courses'][$courseid]['quizzes'][] = [
'quiz_n' => $quizn,
'cmid' => (int)$quiz->cmid,
'name' => $quiz->name,
'gradepass' => $gradepass,
'questions' => $questions,
];
}
echo json_encode($out,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), "\n";