439 lines
11 KiB
JavaScript
439 lines
11 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, body) => {
|
||
res.json({ success: true, body });
|
||
};
|
||
|
||
const respondError = (res, message, statusCode = 400) => {
|
||
res.status(statusCode).json({
|
||
success: false,
|
||
body: null,
|
||
error: { message }
|
||
});
|
||
};
|
||
|
||
// In-memory storage (resets on server restart)
|
||
let tasksCache = null;
|
||
let chainsCache = null;
|
||
let usersCache = null;
|
||
let submissionsCache = null;
|
||
let statsCache = null;
|
||
let statsV2Cache = 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;
|
||
};
|
||
|
||
const getStatsV2 = () => {
|
||
if (!statsV2Cache) statsV2Cache = loadJSON('stats-v2.json');
|
||
return statsV2Cache;
|
||
};
|
||
|
||
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/stats/v2
|
||
router.get('/challenge/stats/v2', (req, res) => {
|
||
const statsV2 = getStatsV2();
|
||
const chainId = req.query.chainId;
|
||
|
||
// Если chainId не передан, возвращаем все данные
|
||
if (!chainId) {
|
||
respond(res, statsV2);
|
||
return;
|
||
}
|
||
|
||
// Фильтруем данные по выбранной цепочке
|
||
const filteredChain = statsV2.chainsDetailed.find(c => c.chainId === chainId);
|
||
|
||
if (!filteredChain) {
|
||
return respondError(res, 'Chain not found', 404);
|
||
}
|
||
|
||
// Фильтруем tasksTable - только задания из этой цепочки
|
||
const chainTaskIds = new Set(filteredChain.tasks.map(t => t.taskId));
|
||
const filteredTasksTable = statsV2.tasksTable.filter(t => chainTaskIds.has(t.taskId));
|
||
|
||
// Фильтруем activeParticipants - только участники с попытками в этой цепочке
|
||
const participantIds = new Set(filteredChain.participantProgress.map(p => p.userId));
|
||
const filteredParticipants = statsV2.activeParticipants
|
||
.filter(p => participantIds.has(p.userId))
|
||
.map(p => ({
|
||
...p,
|
||
chainProgress: p.chainProgress.filter(cp => cp.chainId === chainId)
|
||
}));
|
||
|
||
// Возвращаем отфильтрованные данные
|
||
respond(res, {
|
||
...statsV2,
|
||
tasksTable: filteredTasksTable,
|
||
activeParticipants: filteredParticipants,
|
||
chainsDetailed: [filteredChain]
|
||
});
|
||
});
|
||
|
||
// 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;
|