392 lines
9.7 KiB
JavaScript
392 lines
9.7 KiB
JavaScript
const router = require('express').Router();
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const timer = (time = 300) => (req, res, next) => setTimeout(next, time);
|
|
|
|
// Helper functions
|
|
const loadJSON = (filename) => {
|
|
const filePath = path.join(__dirname, 'data', filename);
|
|
const data = fs.readFileSync(filePath, 'utf8');
|
|
return JSON.parse(data);
|
|
};
|
|
|
|
const respond = (res, data) => {
|
|
res.json({ error: null, data });
|
|
};
|
|
|
|
const respondError = (res, message, statusCode = 400) => {
|
|
res.status(statusCode).json({
|
|
error: { message },
|
|
data: null
|
|
});
|
|
};
|
|
|
|
// In-memory storage (resets on server restart)
|
|
let tasksCache = null;
|
|
let chainsCache = null;
|
|
let usersCache = null;
|
|
let submissionsCache = null;
|
|
let statsCache = null;
|
|
|
|
const getTasks = () => {
|
|
if (!tasksCache) tasksCache = loadJSON('tasks.json');
|
|
return tasksCache;
|
|
};
|
|
|
|
const getChains = () => {
|
|
if (!chainsCache) chainsCache = loadJSON('chains.json');
|
|
return chainsCache;
|
|
};
|
|
|
|
const getUsers = () => {
|
|
if (!usersCache) usersCache = loadJSON('users.json');
|
|
return usersCache;
|
|
};
|
|
|
|
const getSubmissions = () => {
|
|
if (!submissionsCache) submissionsCache = loadJSON('submissions.json');
|
|
return submissionsCache;
|
|
};
|
|
|
|
const getStats = () => {
|
|
if (!statsCache) statsCache = loadJSON('stats.json');
|
|
return statsCache;
|
|
};
|
|
|
|
router.use(timer());
|
|
|
|
// ============= TASKS =============
|
|
|
|
// GET /api/challenge/tasks
|
|
router.get('/challenge/tasks', (req, res) => {
|
|
const tasks = getTasks();
|
|
respond(res, tasks);
|
|
});
|
|
|
|
// GET /api/challenge/task/:id
|
|
router.get('/challenge/task/:id', (req, res) => {
|
|
const tasks = getTasks();
|
|
const task = tasks.find(t => t.id === req.params.id);
|
|
|
|
if (!task) {
|
|
return respondError(res, 'Task not found', 404);
|
|
}
|
|
|
|
respond(res, task);
|
|
});
|
|
|
|
// POST /api/challenge/task
|
|
router.post('/challenge/task', (req, res) => {
|
|
const { title, description, hiddenInstructions } = req.body;
|
|
|
|
if (!title || !description) {
|
|
return respondError(res, 'Title and description are required');
|
|
}
|
|
|
|
const tasks = getTasks();
|
|
const newTask = {
|
|
_id: `task_${Date.now()}`,
|
|
id: `task_${Date.now()}`,
|
|
title,
|
|
description,
|
|
hiddenInstructions: hiddenInstructions || undefined,
|
|
creator: {
|
|
sub: 'teacher-123',
|
|
preferred_username: 'current_teacher',
|
|
email: 'teacher@example.com'
|
|
},
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString()
|
|
};
|
|
|
|
tasks.push(newTask);
|
|
|
|
// Update stats
|
|
const stats = getStats();
|
|
stats.tasks = tasks.length;
|
|
|
|
respond(res, newTask);
|
|
});
|
|
|
|
// PUT /api/challenge/task/:id
|
|
router.put('/challenge/task/:id', (req, res) => {
|
|
const tasks = getTasks();
|
|
const taskIndex = tasks.findIndex(t => t.id === req.params.id);
|
|
|
|
if (taskIndex === -1) {
|
|
return respondError(res, 'Task not found', 404);
|
|
}
|
|
|
|
const { title, description, hiddenInstructions } = req.body;
|
|
const task = tasks[taskIndex];
|
|
|
|
if (title) task.title = title;
|
|
if (description) task.description = description;
|
|
if (hiddenInstructions !== undefined) {
|
|
task.hiddenInstructions = hiddenInstructions || undefined;
|
|
}
|
|
task.updatedAt = new Date().toISOString();
|
|
|
|
respond(res, task);
|
|
});
|
|
|
|
// DELETE /api/challenge/task/:id
|
|
router.delete('/challenge/task/:id', (req, res) => {
|
|
const tasks = getTasks();
|
|
const taskIndex = tasks.findIndex(t => t.id === req.params.id);
|
|
|
|
if (taskIndex === -1) {
|
|
return respondError(res, 'Task not found', 404);
|
|
}
|
|
|
|
tasks.splice(taskIndex, 1);
|
|
|
|
// Update stats
|
|
const stats = getStats();
|
|
stats.tasks = tasks.length;
|
|
|
|
respond(res, { success: true });
|
|
});
|
|
|
|
// ============= CHAINS =============
|
|
|
|
// GET /api/challenge/chains
|
|
router.get('/challenge/chains', (req, res) => {
|
|
const chains = getChains();
|
|
respond(res, chains);
|
|
});
|
|
|
|
// GET /api/challenge/chain/:id
|
|
router.get('/challenge/chain/:id', (req, res) => {
|
|
const chains = getChains();
|
|
const chain = chains.find(c => c.id === req.params.id);
|
|
|
|
if (!chain) {
|
|
return respondError(res, 'Chain not found', 404);
|
|
}
|
|
|
|
respond(res, chain);
|
|
});
|
|
|
|
// POST /api/challenge/chain
|
|
router.post('/challenge/chain', (req, res) => {
|
|
const { name, tasks } = req.body;
|
|
|
|
if (!name || !tasks || !Array.isArray(tasks)) {
|
|
return respondError(res, 'Name and tasks array are required');
|
|
}
|
|
|
|
const chains = getChains();
|
|
const allTasks = getTasks();
|
|
|
|
// Populate tasks
|
|
const populatedTasks = tasks.map(taskId => {
|
|
const task = allTasks.find(t => t.id === taskId);
|
|
return task ? {
|
|
_id: task._id,
|
|
id: task.id,
|
|
title: task.title,
|
|
description: task.description,
|
|
createdAt: task.createdAt,
|
|
updatedAt: task.updatedAt
|
|
} : null;
|
|
}).filter(t => t !== null);
|
|
|
|
const newChain = {
|
|
_id: `chain_${Date.now()}`,
|
|
id: `chain_${Date.now()}`,
|
|
name,
|
|
tasks: populatedTasks,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString()
|
|
};
|
|
|
|
chains.push(newChain);
|
|
|
|
// Update stats
|
|
const stats = getStats();
|
|
stats.chains = chains.length;
|
|
|
|
respond(res, newChain);
|
|
});
|
|
|
|
// PUT /api/challenge/chain/:id
|
|
router.put('/challenge/chain/:id', (req, res) => {
|
|
const chains = getChains();
|
|
const chainIndex = chains.findIndex(c => c.id === req.params.id);
|
|
|
|
if (chainIndex === -1) {
|
|
return respondError(res, 'Chain not found', 404);
|
|
}
|
|
|
|
const { name, tasks } = req.body;
|
|
const chain = chains[chainIndex];
|
|
|
|
if (name) chain.name = name;
|
|
|
|
if (tasks && Array.isArray(tasks)) {
|
|
const allTasks = getTasks();
|
|
const populatedTasks = tasks.map(taskId => {
|
|
const task = allTasks.find(t => t.id === taskId);
|
|
return task ? {
|
|
_id: task._id,
|
|
id: task.id,
|
|
title: task.title,
|
|
description: task.description,
|
|
createdAt: task.createdAt,
|
|
updatedAt: task.updatedAt
|
|
} : null;
|
|
}).filter(t => t !== null);
|
|
|
|
chain.tasks = populatedTasks;
|
|
}
|
|
|
|
chain.updatedAt = new Date().toISOString();
|
|
|
|
respond(res, chain);
|
|
});
|
|
|
|
// DELETE /api/challenge/chain/:id
|
|
router.delete('/challenge/chain/:id', (req, res) => {
|
|
const chains = getChains();
|
|
const chainIndex = chains.findIndex(c => c.id === req.params.id);
|
|
|
|
if (chainIndex === -1) {
|
|
return respondError(res, 'Chain not found', 404);
|
|
}
|
|
|
|
chains.splice(chainIndex, 1);
|
|
|
|
// Update stats
|
|
const stats = getStats();
|
|
stats.chains = chains.length;
|
|
|
|
respond(res, { success: true });
|
|
});
|
|
|
|
// ============= USERS =============
|
|
|
|
// GET /api/challenge/users
|
|
router.get('/challenge/users', (req, res) => {
|
|
const users = getUsers();
|
|
respond(res, users);
|
|
});
|
|
|
|
// ============= STATS =============
|
|
|
|
// GET /api/challenge/stats
|
|
router.get('/challenge/stats', (req, res) => {
|
|
const stats = getStats();
|
|
respond(res, stats);
|
|
});
|
|
|
|
// GET /api/challenge/user/:userId/stats
|
|
router.get('/challenge/user/:userId/stats', (req, res) => {
|
|
const users = getUsers();
|
|
const submissions = getSubmissions();
|
|
const chains = getChains();
|
|
|
|
const user = users.find(u => u.id === req.params.userId);
|
|
|
|
if (!user) {
|
|
return respondError(res, 'User not found', 404);
|
|
}
|
|
|
|
const userSubmissions = submissions.filter(s => s.user.id === req.params.userId);
|
|
|
|
// Calculate stats
|
|
const completedTasks = new Set();
|
|
const taskStats = {};
|
|
|
|
userSubmissions.forEach(sub => {
|
|
const taskId = sub.task.id;
|
|
|
|
if (!taskStats[taskId]) {
|
|
taskStats[taskId] = {
|
|
taskId: taskId,
|
|
taskTitle: sub.task.title,
|
|
attempts: [],
|
|
totalAttempts: 0,
|
|
status: 'not_attempted',
|
|
lastAttemptAt: null
|
|
};
|
|
}
|
|
|
|
taskStats[taskId].attempts.push({
|
|
attemptNumber: sub.attemptNumber,
|
|
status: sub.status,
|
|
submittedAt: sub.submittedAt,
|
|
checkedAt: sub.checkedAt,
|
|
feedback: sub.feedback
|
|
});
|
|
|
|
taskStats[taskId].totalAttempts++;
|
|
taskStats[taskId].status = sub.status;
|
|
taskStats[taskId].lastAttemptAt = sub.submittedAt;
|
|
|
|
if (sub.status === 'accepted') {
|
|
completedTasks.add(taskId);
|
|
}
|
|
});
|
|
|
|
const taskStatsArray = Object.values(taskStats);
|
|
|
|
// Chain stats
|
|
const chainStats = chains.map(chain => {
|
|
const completedInChain = chain.tasks.filter(t => completedTasks.has(t.id)).length;
|
|
return {
|
|
chainId: chain.id,
|
|
chainName: chain.name,
|
|
totalTasks: chain.tasks.length,
|
|
completedTasks: completedInChain,
|
|
progress: chain.tasks.length > 0 ? (completedInChain / chain.tasks.length * 100) : 0
|
|
};
|
|
});
|
|
|
|
const totalCheckTime = userSubmissions
|
|
.filter(s => s.checkedAt)
|
|
.reduce((sum, s) => {
|
|
const submitted = new Date(s.submittedAt).getTime();
|
|
const checked = new Date(s.checkedAt).getTime();
|
|
return sum + (checked - submitted);
|
|
}, 0);
|
|
|
|
const userStats = {
|
|
totalTasksAttempted: taskStatsArray.length,
|
|
completedTasks: completedTasks.size,
|
|
inProgressTasks: taskStatsArray.filter(t => t.status === 'in_progress').length,
|
|
needsRevisionTasks: taskStatsArray.filter(t => t.status === 'needs_revision').length,
|
|
totalSubmissions: userSubmissions.length,
|
|
averageCheckTimeMs: userSubmissions.length > 0 ? totalCheckTime / userSubmissions.length : 0,
|
|
taskStats: taskStatsArray,
|
|
chainStats: chainStats
|
|
};
|
|
|
|
respond(res, userStats);
|
|
});
|
|
|
|
// ============= SUBMISSIONS =============
|
|
|
|
// GET /api/challenge/submissions
|
|
router.get('/challenge/submissions', (req, res) => {
|
|
const submissions = getSubmissions();
|
|
respond(res, submissions);
|
|
});
|
|
|
|
// GET /api/challenge/user/:userId/submissions
|
|
router.get('/challenge/user/:userId/submissions', (req, res) => {
|
|
const submissions = getSubmissions();
|
|
const taskId = req.query.taskId;
|
|
|
|
let filtered = submissions.filter(s => s.user.id === req.params.userId);
|
|
|
|
if (taskId) {
|
|
filtered = filtered.filter(s => s.task.id === taskId);
|
|
}
|
|
|
|
respond(res, filtered);
|
|
});
|
|
|
|
module.exports = router;
|