Add quiz support: parse *_quiz_solved.txt files, take and grade quizzes

Scanner indexes quiz files found next to videos ([&] questions, "- "
options, "-- Correct" markers; _notsolved twins ignored). Quizzes appear
in the folder content list interleaved with videos by filename order.
New api/quiz.php serves questions without answers and grades attempts
server-side; per-user best/last score and attempts stored in
quiz_progress. Folder/course progress now aggregates videos + quizzes
with partial credit for partial scores.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Harwinder Sekhon
2026-07-11 11:24:37 -04:00
parent aba6ef4734
commit deda7c3c18
7 changed files with 467 additions and 28 deletions
+16
View File
@@ -24,6 +24,7 @@ function videoSummary(array $v): array
return [
'id' => (int)$v['id'],
'name' => $v['name'],
'filename' => $v['filename'],
'duration' => $duration !== null ? (float)$duration : null,
'position' => $position,
'completed' => (bool)($v['completed'] ?? false),
@@ -31,6 +32,19 @@ function videoSummary(array $v): array
];
}
function quizSummary(array $q): array
{
return [
'id' => (int)$q['id'],
'name' => $q['name'],
'filename' => $q['filename'],
'questions' => (int)$q['num_questions'],
'attempts' => (int)($q['attempts'] ?? 0),
'best_score' => (int)($q['best_score'] ?? 0),
'completed' => (bool)($q['completed'] ?? false),
];
}
function crumbList(array $folder): array
{
return array_map(
@@ -70,6 +84,8 @@ switch ($view) {
childFolders((int)$folder['id'])),
'videos' => array_map('videoSummary',
folderVideos((int)$folder['id'], $userId)),
'quizzes' => array_map('quizSummary',
folderQuizzes((int)$folder['id'], $userId)),
]);
break;
+122
View File
@@ -0,0 +1,122 @@
<?php
// GET api/quiz.php?id=N -> quiz questions + options (no answers) + user's results
// POST api/quiz.php -> grade an attempt, save the result
// JSON body: {quiz_id, answers: [[optIdx,...], ...]} (one array per question)
// Response includes the correct options so the UI can mark the attempt.
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/functions.php';
header('Content-Type: application/json');
$userId = requireLoginApi();
function quizProgressRow(int $userId, int $quizId): array
{
$stmt = db()->prepare(
'SELECT best_score, last_score, total_questions, attempts, completed
FROM quiz_progress WHERE user_id = ? AND quiz_id = ?'
);
$stmt->execute([$userId, $quizId]);
return $stmt->fetch()
?: ['best_score' => 0, 'last_score' => 0, 'total_questions' => 0,
'attempts' => 0, 'completed' => 0];
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$quiz = getQuiz((int)($_GET['id'] ?? 0));
if (!$quiz) {
http_response_code(404);
echo json_encode(['error' => 'Quiz not found']);
exit;
}
$file = quizFilePath($quiz);
$questions = $file ? parseQuizFile($file) : [];
if (!$questions) {
http_response_code(404);
echo json_encode(['error' => 'Quiz file is missing — rescan the library']);
exit;
}
$folder = getFolder((int)$quiz['folder_id']);
$p = quizProgressRow($userId, (int)$quiz['id']);
echo json_encode([
'quiz' => ['id' => (int)$quiz['id'], 'name' => $quiz['name']],
'folder' => ['id' => (int)$folder['id'], 'name' => $folder['name']],
'breadcrumbs' => array_map(
fn($c) => ['id' => (int)$c['id'], 'name' => $c['name']],
folderBreadcrumbs($folder)
),
'questions' => array_map(fn($q) => [
'text' => $q['text'],
'multi' => $q['multi'],
'options' => $q['options'],
], $questions),
'progress' => [
'attempts' => (int)$p['attempts'],
'best_score' => (int)$p['best_score'],
'last_score' => (int)$p['last_score'],
'total' => count($questions),
'completed' => (bool)$p['completed'],
],
]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
$data = json_decode(file_get_contents('php://input'), true) ?: [];
$quiz = getQuiz((int)($data['quiz_id'] ?? 0));
$answers = $data['answers'] ?? null;
if (!$quiz || !is_array($answers)) {
http_response_code(400);
echo json_encode(['error' => 'Bad request']);
exit;
}
$file = quizFilePath($quiz);
$questions = $file ? parseQuizFile($file) : [];
if (!$questions) {
http_response_code(404);
echo json_encode(['error' => 'Quiz file is missing — rescan the library']);
exit;
}
$score = 0;
$results = [];
foreach ($questions as $i => $q) {
$selected = array_values(array_unique(array_map('intval', (array)($answers[$i] ?? []))));
sort($selected);
$correct = $q['correct'];
sort($correct);
$isCorrect = $selected === $correct;
if ($isCorrect) $score++;
$results[] = [
'correct' => $isCorrect,
'correct_options' => $correct,
'selected' => $selected,
];
}
$total = count($questions);
$completed = ($score === $total) ? 1 : 0;
db()->prepare(
'INSERT INTO quiz_progress
(user_id, quiz_id, best_score, last_score, total_questions, attempts, completed)
VALUES (?, ?, ?, ?, ?, 1, ?)
ON DUPLICATE KEY UPDATE
last_score = VALUES(last_score),
best_score = GREATEST(best_score, VALUES(best_score)),
total_questions = VALUES(total_questions),
attempts = attempts + 1,
completed = GREATEST(completed, VALUES(completed))'
)->execute([$userId, (int)$quiz['id'], $score, $score, $total, $completed]);
echo json_encode([
'score' => $score,
'total' => $total,
'completed' => (bool)$completed,
'results' => $results,
]);