Files
Learn_System/backend/src/controllers/templateController.js
T
Maxim Dolgolyov be4d43105e LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend.
Features: real-time collaborative whiteboard (SSE), multi-page support,
LaTeX formulas, shapes/connectors, coordinate systems, number lines,
compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with
rotation & resize controls, minimap navigation overlay, auto-measurements,
multi-page thumbnails sidebar, PNG export, page templates.
Student/teacher workflows: classes, assignments, library, dashboard.
Mobile responsive. SQLite (better-sqlite3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:10:37 +03:00

318 lines
11 KiB
JavaScript

const db = require('../db/db');
/* ══════════════════════════════════════════════════════════════════════
COURSE TEMPLATES
══════════════════════════════════════════════════════════════════════ */
/* ── GET /api/templates/courses ──────────────────────────────────────── */
function listCourseTemplates(req, res) {
const uid = req.user.id;
const { my, subject } = req.query;
let where;
const args = [];
if (my) {
where = 'WHERE ct.created_by = ?';
args.push(uid);
} else if (subject) {
where = 'WHERE (ct.is_public = 1 OR ct.created_by = ?) AND ct.subject_slug = ?';
args.push(uid, subject);
} else {
where = 'WHERE ct.is_public = 1 OR ct.created_by = ?';
args.push(uid);
}
const rows = db.prepare(`
SELECT ct.*, u.name AS creator_name
FROM course_templates ct
LEFT JOIN users u ON ct.created_by = u.id
${where}
ORDER BY ct.created_at DESC
`).all(...args);
res.json(rows.map(r => ({
id: r.id,
title: r.title,
description: r.description || '',
category: r.category,
subjectSlug: r.subject_slug,
structure: safeJSON(r.structure, {}),
isPublic: r.is_public === 1,
createdBy: r.created_by,
creatorName: r.creator_name || '',
createdAt: r.created_at,
})));
}
/* ── POST /api/templates/courses ─────────────────────────────────────── */
function saveCourseTemplate(req, res) {
const uid = req.user.id;
const { title, description, category, subject_slug, courseId } = req.body;
if (!title) return res.status(400).json({ error: 'title required' });
let structure = {};
if (courseId) {
// Snapshot course structure
const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(courseId);
if (!course) return res.status(404).json({ error: 'Course not found' });
const sections = db.prepare(
'SELECT * FROM course_sections WHERE course_id = ? ORDER BY order_index, id'
).all(courseId);
const lessons = db.prepare(
'SELECT * FROM lessons WHERE course_id = ? ORDER BY order_index, id'
).all(courseId);
const sectionArr = sections.map(s => {
const sectionLessons = lessons.filter(l => l.section_id === s.id);
return {
title: s.title,
lessons: sectionLessons.map(l => ({
title: l.title,
blocks: db.prepare(
'SELECT type, data FROM lesson_blocks WHERE lesson_id = ? ORDER BY order_index, id'
).all(l.id).map(b => ({ type: b.type, data: safeJSON(b.data, {}) })),
})),
};
});
// Lessons without a section
const unsectioned = lessons.filter(l => !l.section_id);
if (unsectioned.length) {
sectionArr.unshift({
title: null,
lessons: unsectioned.map(l => ({
title: l.title,
blocks: db.prepare(
'SELECT type, data FROM lesson_blocks WHERE lesson_id = ? ORDER BY order_index, id'
).all(l.id).map(b => ({ type: b.type, data: safeJSON(b.data, {}) })),
})),
});
}
structure = { sections: sectionArr };
}
const r = db.prepare(`
INSERT INTO course_templates (title, description, category, subject_slug, structure, is_public, created_by)
VALUES (?, ?, ?, ?, ?, 1, ?)
`).run(
title.trim(),
description || null,
category || 'general',
subject_slug || null,
JSON.stringify(structure),
uid
);
res.status(201).json({ id: r.lastInsertRowid });
}
/* ── POST /api/templates/courses/:id/create ──────────────────────────── */
function createFromCourseTemplate(req, res) {
const tpl = db.prepare('SELECT * FROM course_templates WHERE id = ?').get(req.params.id);
if (!tpl) return res.status(404).json({ error: 'Template not found' });
const { title, subjectSlug } = req.body;
const structure = safeJSON(tpl.structure, {});
const sections = structure.sections || [];
let newCourseId;
db.transaction(() => {
const cr = db.prepare(`
INSERT INTO courses (subject_slug, title, description, cover_emoji, order_index, is_published, created_by)
VALUES (?, ?, ?, '', 0, 0, ?)
`).run(
subjectSlug || tpl.subject_slug || 'other',
title || tpl.title,
tpl.description || null,
req.user.id
);
newCourseId = cr.lastInsertRowid;
let lessonOrder = 0;
for (const sec of sections) {
let sectionId = null;
if (sec.title) {
const sr = db.prepare(
'INSERT INTO course_sections (course_id, title, order_index) VALUES (?, ?, ?)'
).run(newCourseId, sec.title, lessonOrder);
sectionId = sr.lastInsertRowid;
}
for (const lesson of (sec.lessons || [])) {
const lr = db.prepare(
'INSERT INTO lessons (course_id, title, order_index, section_id) VALUES (?, ?, ?, ?)'
).run(newCourseId, lesson.title, lessonOrder++, sectionId);
const newLid = lr.lastInsertRowid;
(lesson.blocks || []).forEach((b, i) => {
db.prepare(
'INSERT INTO lesson_blocks (lesson_id, type, order_index, data) VALUES (?, ?, ?, ?)'
).run(newLid, b.type, i, JSON.stringify(b.data || {}));
});
}
}
})();
res.status(201).json({ id: newCourseId });
}
/* ── DELETE /api/templates/courses/:id ────────────────────────────────── */
function deleteCourseTemplate(req, res) {
const tpl = db.prepare('SELECT * FROM course_templates WHERE id = ?').get(req.params.id);
if (!tpl) return res.status(404).json({ error: 'Template not found' });
if (tpl.created_by !== req.user.id && req.user.role !== 'admin')
return res.status(403).json({ error: 'Forbidden' });
db.prepare('DELETE FROM course_templates WHERE id = ?').run(tpl.id);
res.json({ ok: true });
}
/* ══════════════════════════════════════════════════════════════════════
LESSON TEMPLATES
══════════════════════════════════════════════════════════════════════ */
/* ── GET /api/templates/lessons ──────────────────────────────────────── */
function listLessonTemplates(req, res) {
const uid = req.user.id;
const { my, category } = req.query;
let where;
const args = [];
if (my) {
where = 'WHERE lt.created_by = ?';
args.push(uid);
} else if (category) {
where = 'WHERE (lt.is_public = 1 OR lt.created_by = ?) AND lt.category = ?';
args.push(uid, category);
} else {
where = 'WHERE lt.is_public = 1 OR lt.created_by = ?';
args.push(uid);
}
const rows = db.prepare(`
SELECT lt.*, u.name AS creator_name
FROM lesson_templates lt
LEFT JOIN users u ON lt.created_by = u.id
${where}
ORDER BY lt.created_at DESC
`).all(...args);
res.json(rows.map(r => ({
id: r.id,
title: r.title,
category: r.category,
subjectSlug: r.subject_slug,
blocks: safeJSON(r.blocks, []),
isPublic: r.is_public === 1,
createdBy: r.created_by,
creatorName: r.creator_name || '',
createdAt: r.created_at,
})));
}
/* ── POST /api/templates/lessons ─────────────────────────────────────── */
function saveLessonTemplate(req, res) {
const uid = req.user.id;
const { title, category, subject_slug, lessonId } = req.body;
if (!title) return res.status(400).json({ error: 'title required' });
let blocksJSON = '[]';
if (lessonId) {
const lesson = db.prepare('SELECT id FROM lessons WHERE id = ?').get(lessonId);
if (!lesson) return res.status(404).json({ error: 'Lesson not found' });
const rawBlocks = db.prepare(
'SELECT type, data FROM lesson_blocks WHERE lesson_id = ? ORDER BY order_index, id'
).all(lesson.id);
blocksJSON = JSON.stringify(rawBlocks.map(b => ({
type: b.type,
data: safeJSON(b.data, {}),
})));
}
const r = db.prepare(`
INSERT INTO lesson_templates (title, category, subject_slug, blocks, is_public, created_by)
VALUES (?, ?, ?, ?, 1, ?)
`).run(
title.trim(),
category || 'general',
subject_slug || null,
blocksJSON,
uid
);
res.status(201).json({ id: r.lastInsertRowid });
}
/* ── POST /api/templates/lessons/:id/create ──────────────────────────── */
function createFromLessonTemplate(req, res) {
const tpl = db.prepare('SELECT * FROM lesson_templates WHERE id = ?').get(req.params.id);
if (!tpl) return res.status(404).json({ error: 'Template not found' });
const { courseId, sectionId, title } = req.body;
if (!courseId) return res.status(400).json({ error: 'courseId required' });
const course = db.prepare('SELECT id FROM courses WHERE id = ?').get(courseId);
if (!course) return res.status(404).json({ error: 'Course not found' });
const tplBlocks = safeJSON(tpl.blocks, []);
let newLessonId;
db.transaction(() => {
// Get max order_index
const maxOrd = db.prepare(
'SELECT MAX(order_index) AS mx FROM lessons WHERE course_id = ?'
).get(courseId);
const lr = db.prepare(
'INSERT INTO lessons (course_id, title, order_index, section_id) VALUES (?, ?, ?, ?)'
).run(courseId, title || tpl.title, (maxOrd?.mx ?? -1) + 1, sectionId || null);
newLessonId = lr.lastInsertRowid;
tplBlocks.forEach((b, i) => {
db.prepare(
'INSERT INTO lesson_blocks (lesson_id, type, order_index, data) VALUES (?, ?, ?, ?)'
).run(newLessonId, b.type, i, JSON.stringify(b.data || {}));
});
})();
res.status(201).json({ id: newLessonId });
}
/* ── DELETE /api/templates/lessons/:id ────────────────────────────────── */
function deleteLessonTemplate(req, res) {
const tpl = db.prepare('SELECT * FROM lesson_templates WHERE id = ?').get(req.params.id);
if (!tpl) return res.status(404).json({ error: 'Template not found' });
if (tpl.created_by !== req.user.id && req.user.role !== 'admin')
return res.status(403).json({ error: 'Forbidden' });
db.prepare('DELETE FROM lesson_templates WHERE id = ?').run(tpl.id);
res.json({ ok: true });
}
/* ── helpers ─────────────────────────────────────────────────────────── */
function safeJSON(str, fallback) {
try { return JSON.parse(str); } catch { return fallback; }
}
module.exports = {
listCourseTemplates,
saveCourseTemplate,
createFromCourseTemplate,
deleteCourseTemplate,
listLessonTemplates,
saveLessonTemplate,
createFromLessonTemplate,
deleteLessonTemplate,
};