439 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;