init + api use
This commit is contained in:
391
stubs/api/index.js
Normal file
391
stubs/api/index.js
Normal file
@@ -0,0 +1,391 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user