be4d43105e
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>
509 lines
20 KiB
JavaScript
509 lines
20 KiB
JavaScript
const db = require('../db/db');
|
|
|
|
/* ── helpers ──────────────────────────────────────────────────────────── */
|
|
|
|
// Reused SQL fragment: user's completed-lesson count for a course (param: user_id)
|
|
const DONE_COUNT_SUBQ = `(SELECT COUNT(*) FROM lesson_progress lp
|
|
JOIN lessons l2 ON lp.lesson_id = l2.id
|
|
WHERE l2.course_id = c.id AND lp.user_id = ? AND lp.completed = 1) AS done_count`;
|
|
|
|
function courseRow(row) {
|
|
return {
|
|
id: row.id,
|
|
subjectSlug: row.subject_slug,
|
|
title: row.title,
|
|
description: row.description || '',
|
|
coverEmoji: row.cover_emoji,
|
|
orderIndex: row.order_index,
|
|
isPublished: row.is_published === 1,
|
|
createdBy: row.created_by,
|
|
createdAt: row.created_at,
|
|
lessonCount: row.lesson_count ?? 0,
|
|
doneCount: row.done_count ?? 0,
|
|
};
|
|
}
|
|
|
|
function progressSubquery(role) {
|
|
return role === 'student' ? 'AND l.is_published = 1' : '';
|
|
}
|
|
|
|
/* ── GET /api/courses ─────────────────────────────────────────────────── */
|
|
function list(req, res) {
|
|
const { subject } = req.query;
|
|
const role = req.user.role;
|
|
const uid = req.user.id;
|
|
|
|
let where = role === 'student' ? 'WHERE c.is_published = 1' : 'WHERE 1=1';
|
|
const args = [];
|
|
if (subject) { where += ' AND c.subject_slug = ?'; args.push(subject); }
|
|
|
|
const rows = db.prepare(`
|
|
SELECT c.*,
|
|
(SELECT COUNT(*) FROM lessons l WHERE l.course_id = c.id ${progressSubquery(role)}) AS lesson_count,
|
|
${DONE_COUNT_SUBQ}
|
|
FROM courses c ${where}
|
|
ORDER BY c.subject_slug, c.order_index, c.id
|
|
`).all(uid, ...args);
|
|
|
|
res.json(rows.map(courseRow));
|
|
}
|
|
|
|
/* ── GET /api/courses/search?q=… ─────────────────────────────────────── */
|
|
function search(req, res) {
|
|
const q = (req.query.q || '').trim();
|
|
const role = req.user.role;
|
|
const uid = req.user.id;
|
|
if (!q) return res.json({ courses: [], lessons: [] });
|
|
|
|
const like = `%${q}%`;
|
|
const pubC = role === 'student' ? 'AND c.is_published = 1' : '';
|
|
const pubL = role === 'student' ? 'AND l.is_published = 1 AND c.is_published = 1' : '';
|
|
|
|
const courses = db.prepare(`
|
|
SELECT c.*,
|
|
(SELECT COUNT(*) FROM lessons l WHERE l.course_id = c.id ${progressSubquery(role)}) AS lesson_count,
|
|
${DONE_COUNT_SUBQ}
|
|
FROM courses c WHERE (c.title LIKE ? OR c.description LIKE ?) ${pubC}
|
|
ORDER BY c.subject_slug, c.order_index LIMIT 20
|
|
`).all(uid, like, like).map(courseRow);
|
|
|
|
const lessons = db.prepare(`
|
|
SELECT l.id, l.title, l.course_id, c.title AS course_title, c.subject_slug,
|
|
lp.completed
|
|
FROM lessons l
|
|
JOIN courses c ON l.course_id = c.id
|
|
LEFT JOIN lesson_progress lp ON lp.lesson_id = l.id AND lp.user_id = ?
|
|
WHERE l.title LIKE ? ${pubL}
|
|
ORDER BY c.subject_slug, l.order_index LIMIT 30
|
|
`).all(uid, like);
|
|
|
|
res.json({ courses, lessons });
|
|
}
|
|
|
|
/* ── GET /api/courses/continue ───────────────────────────────────────── */
|
|
// Returns the last-in-progress lesson across all courses
|
|
function continueLesson(req, res) {
|
|
const uid = req.user.id;
|
|
const role = req.user.role;
|
|
const pub = role === 'student' ? 'AND l.is_published = 1 AND c.is_published = 1' : '';
|
|
|
|
// 1. last started but not finished lesson (progress row exists, completed=0)
|
|
let row = db.prepare(`
|
|
SELECT l.id, l.title, l.course_id, c.title AS course_title, c.subject_slug, c.cover_emoji
|
|
FROM lesson_progress lp
|
|
JOIN lessons l ON lp.lesson_id = l.id
|
|
JOIN courses c ON l.course_id = c.id
|
|
WHERE lp.user_id = ? AND lp.completed = 0 ${pub}
|
|
ORDER BY lp.updated_at DESC LIMIT 1
|
|
`).get(uid);
|
|
|
|
// 2. fallback: first unprogressed lesson in courses that have any progress
|
|
if (!row) {
|
|
row = db.prepare(`
|
|
SELECT l.id, l.title, l.course_id, c.title AS course_title, c.subject_slug, c.cover_emoji
|
|
FROM lessons l
|
|
JOIN courses c ON l.course_id = c.id
|
|
WHERE c.id IN (
|
|
SELECT DISTINCT l2.course_id FROM lesson_progress lp2
|
|
JOIN lessons l2 ON lp2.lesson_id = l2.id WHERE lp2.user_id = ?
|
|
)
|
|
AND l.id NOT IN (SELECT lesson_id FROM lesson_progress WHERE user_id = ?)
|
|
${pub}
|
|
ORDER BY l.course_id, l.order_index LIMIT 1
|
|
`).get(uid, uid);
|
|
}
|
|
|
|
res.json(row ? {
|
|
lessonId: row.id,
|
|
lessonTitle: row.title,
|
|
courseId: row.course_id,
|
|
courseTitle: row.course_title,
|
|
subjectSlug: row.subject_slug,
|
|
coverEmoji: row.cover_emoji,
|
|
} : null);
|
|
}
|
|
|
|
/* ── GET /api/courses/:id ─────────────────────────────────────────────── */
|
|
function get(req, res) {
|
|
const role = req.user.role;
|
|
const uid = req.user.id;
|
|
|
|
const row = db.prepare(`
|
|
SELECT c.*,
|
|
(SELECT COUNT(*) FROM lessons l WHERE l.course_id = c.id ${progressSubquery(role)}) AS lesson_count,
|
|
${DONE_COUNT_SUBQ}
|
|
FROM courses c WHERE c.id = ?
|
|
`).get(uid, req.params.id);
|
|
|
|
if (!row) return res.status(404).json({ error: 'Course not found' });
|
|
if (role === 'student' && !row.is_published)
|
|
return res.status(403).json({ error: 'Course not published' });
|
|
|
|
// sections
|
|
const sections = db.prepare(
|
|
'SELECT * FROM course_sections WHERE course_id = ? ORDER BY order_index, id'
|
|
).all(row.id);
|
|
|
|
// lessons grouped
|
|
const pubWhere = role === 'student' ? 'AND l.is_published = 1' : '';
|
|
const lessons = db.prepare(`
|
|
SELECT l.id, l.title, l.order_index, l.is_published, l.section_id, l.read_time,
|
|
lp.completed
|
|
FROM lessons l
|
|
LEFT JOIN lesson_progress lp ON lp.lesson_id = l.id AND lp.user_id = ?
|
|
WHERE l.course_id = ? ${pubWhere}
|
|
ORDER BY l.order_index, l.id
|
|
`).all(uid, row.id);
|
|
|
|
res.json({
|
|
...courseRow(row),
|
|
sections: sections.map(s => ({ id: s.id, title: s.title, orderIndex: s.order_index })),
|
|
lessons: lessons.map(l => ({
|
|
id: l.id,
|
|
title: l.title,
|
|
orderIndex: l.order_index,
|
|
isPublished: l.is_published === 1,
|
|
sectionId: l.section_id,
|
|
readTime: l.read_time || 0,
|
|
completed: l.completed === 1,
|
|
})),
|
|
});
|
|
}
|
|
|
|
/* ── GET /api/courses/:id/stats?classId=X ────────────────────────────── */
|
|
function stats(req, res) {
|
|
const { classId } = req.query;
|
|
const course = db.prepare('SELECT id FROM courses WHERE id = ?').get(req.params.id);
|
|
if (!course) return res.status(404).json({ error: 'Course not found' });
|
|
|
|
// students in class (or all students who have progress)
|
|
let members;
|
|
if (classId) {
|
|
members = db.prepare(`
|
|
SELECT u.id FROM class_members cm JOIN users u ON cm.user_id = u.id
|
|
WHERE cm.class_id = ? AND u.role = 'student'
|
|
`).all(classId).map(r => r.id);
|
|
} else {
|
|
members = db.prepare(`
|
|
SELECT DISTINCT lp.user_id AS id FROM lesson_progress lp
|
|
JOIN lessons l ON lp.lesson_id = l.id WHERE l.course_id = ?
|
|
`).all(course.id).map(r => r.id);
|
|
}
|
|
const total = members.length || 1;
|
|
|
|
const lessons = db.prepare(`
|
|
SELECT l.id, l.title, l.order_index,
|
|
(SELECT COUNT(*) FROM lesson_progress lp
|
|
WHERE lp.lesson_id = l.id AND lp.completed = 1
|
|
AND lp.user_id IN (${members.map(() => '?').join(',') || 'NULL'})) AS done_count
|
|
FROM lessons l WHERE l.course_id = ? ORDER BY l.order_index, l.id
|
|
`).all(...members, course.id);
|
|
|
|
res.json({ total, lessons: lessons.map(l => ({
|
|
id: l.id, title: l.title,
|
|
doneCount: l.done_count,
|
|
pct: Math.round(l.done_count / total * 100),
|
|
}))});
|
|
}
|
|
|
|
/* ── GET /api/courses/:id/analytics?classId=X ───────────────────────── */
|
|
function analytics(req, res) {
|
|
const { classId } = req.query;
|
|
const course = db.prepare('SELECT id, title FROM courses WHERE id = ?').get(req.params.id);
|
|
if (!course) return res.status(404).json({ error: 'Course not found' });
|
|
|
|
// all lessons in course
|
|
const lessons = db.prepare(
|
|
'SELECT id, title, order_index FROM lessons WHERE course_id = ? ORDER BY order_index, id'
|
|
).all(course.id);
|
|
const lessonIds = lessons.map(l => l.id);
|
|
const totalLessons = lessonIds.length;
|
|
|
|
// students
|
|
let students;
|
|
if (classId) {
|
|
students = db.prepare(`
|
|
SELECT u.id, u.name, u.email FROM class_members cm
|
|
JOIN users u ON cm.user_id = u.id
|
|
WHERE cm.class_id = ? AND u.role = 'student'
|
|
ORDER BY u.name
|
|
`).all(classId);
|
|
} else {
|
|
students = db.prepare(`
|
|
SELECT DISTINCT u.id, u.name, u.email FROM lesson_progress lp
|
|
JOIN lessons l ON lp.lesson_id = l.id
|
|
JOIN users u ON lp.user_id = u.id
|
|
WHERE l.course_id = ?
|
|
ORDER BY u.name
|
|
`).all(course.id);
|
|
}
|
|
|
|
if (!students.length) {
|
|
return res.json({
|
|
totalStudents: 0, totalLessons, avgPct: 0,
|
|
lessons: lessons.map(l => ({ id: l.id, title: l.title, doneCount: 0, pct: 0 })),
|
|
students: [], stuckStudents: [],
|
|
});
|
|
}
|
|
|
|
// Batch-fetch ALL progress for ALL students in ONE query (eliminates N+1)
|
|
const studentIds = students.map(s => s.id);
|
|
const lpRows = lessonIds.length && studentIds.length
|
|
? db.prepare(`
|
|
SELECT user_id, lesson_id, completed, updated_at FROM lesson_progress
|
|
WHERE user_id IN (${studentIds.map(() => '?').join(',')})
|
|
AND lesson_id IN (${lessonIds.map(() => '?').join(',')})
|
|
`).all(...studentIds, ...lessonIds)
|
|
: [];
|
|
|
|
// Index progress by [user_id][lesson_id]
|
|
const progressByUser = {};
|
|
for (const r of lpRows) {
|
|
if (!progressByUser[r.user_id]) progressByUser[r.user_id] = {};
|
|
progressByUser[r.user_id][r.lesson_id] = r;
|
|
}
|
|
|
|
// Per-lesson done count from the same data
|
|
const lessonDoneCount = {};
|
|
for (const lid of lessonIds) lessonDoneCount[lid] = 0;
|
|
for (const r of lpRows) {
|
|
if (r.completed === 1) lessonDoneCount[r.lesson_id] = (lessonDoneCount[r.lesson_id] || 0) + 1;
|
|
}
|
|
|
|
const studentData = students.map(s => {
|
|
const progressMap = progressByUser[s.id] || {};
|
|
const progress = Object.values(progressMap);
|
|
const doneCnt = progress.filter(p => p.completed === 1).length;
|
|
const pct = totalLessons > 0 ? Math.round(doneCnt / totalLessons * 100) : 0;
|
|
|
|
// find first incomplete lesson
|
|
let firstIncompleteIdx = -1;
|
|
for (let i = 0; i < lessons.length; i++) {
|
|
const p = progressMap[lessons[i].id];
|
|
if (!p || p.completed !== 1) { firstIncompleteIdx = i; break; }
|
|
}
|
|
|
|
// "stuck" = started course (done > 0), not finished, and last activity > 3 days ago
|
|
let stuck = false;
|
|
let stuckLesson = null;
|
|
let lastActivity = null;
|
|
if (doneCnt > 0 && doneCnt < totalLessons) {
|
|
const dates = progress.map(p => p.updated_at).filter(Boolean);
|
|
if (dates.length) {
|
|
lastActivity = dates.sort().pop();
|
|
const daysSince = (Date.now() - new Date(lastActivity.replace(' ', 'T') + 'Z').getTime()) / 86400000;
|
|
if (daysSince > 3) {
|
|
stuck = true;
|
|
stuckLesson = firstIncompleteIdx >= 0 ? lessons[firstIncompleteIdx] : null;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: s.id, name: s.name, email: s.email,
|
|
doneCount: doneCnt, pct, stuck,
|
|
stuckLessonId: stuckLesson?.id || null,
|
|
stuckLessonTitle: stuckLesson?.title || null,
|
|
lastActivity,
|
|
};
|
|
});
|
|
|
|
// Per-lesson stats from pre-computed counts (no extra queries)
|
|
const lessonStats = lessons.map(l => {
|
|
const doneCount = lessonDoneCount[l.id] || 0;
|
|
return {
|
|
id: l.id, title: l.title,
|
|
doneCount,
|
|
pct: students.length > 0 ? Math.round(doneCount / students.length * 100) : 0,
|
|
};
|
|
});
|
|
|
|
const avgPct = studentData.length > 0
|
|
? Math.round(studentData.reduce((s, d) => s + d.pct, 0) / studentData.length)
|
|
: 0;
|
|
|
|
res.json({
|
|
totalStudents: students.length,
|
|
totalLessons,
|
|
avgPct,
|
|
lessons: lessonStats,
|
|
students: studentData,
|
|
stuckStudents: studentData.filter(s => s.stuck),
|
|
});
|
|
}
|
|
|
|
/* ── POST /api/courses ────────────────────────────────────────────────── */
|
|
function create(req, res) {
|
|
const { subjectSlug, title, description, coverEmoji, orderIndex } = req.body;
|
|
if (!subjectSlug || !title)
|
|
return res.status(400).json({ error: 'subjectSlug and title required' });
|
|
|
|
const result = db.prepare(`
|
|
INSERT INTO courses (subject_slug, title, description, cover_emoji, order_index, created_by)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`).run(subjectSlug, title.trim(), description || null, coverEmoji || '', orderIndex ?? 0, req.user.id);
|
|
res.status(201).json({ id: result.lastInsertRowid });
|
|
}
|
|
|
|
/* ── POST /api/courses/:id/duplicate ─────────────────────────────────── */
|
|
function duplicate(req, res) {
|
|
const src = db.prepare('SELECT * FROM courses WHERE id = ?').get(req.params.id);
|
|
if (!src) return res.status(404).json({ error: 'Course not found' });
|
|
|
|
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, ?)
|
|
`).run(src.subject_slug, src.title + ' (копия)', src.description, src.cover_emoji, src.order_index, req.user.id);
|
|
newCourseId = cr.lastInsertRowid;
|
|
|
|
// duplicate sections
|
|
const secMap = {};
|
|
const sections = db.prepare('SELECT * FROM course_sections WHERE course_id = ? ORDER BY order_index').all(src.id);
|
|
for (const s of sections) {
|
|
const sr = db.prepare('INSERT INTO course_sections (course_id, title, order_index) VALUES (?, ?, ?)').run(newCourseId, s.title, s.order_index);
|
|
secMap[s.id] = sr.lastInsertRowid;
|
|
}
|
|
|
|
// duplicate lessons + blocks
|
|
const lessons = db.prepare('SELECT * FROM lessons WHERE course_id = ? ORDER BY order_index').all(src.id);
|
|
for (const l of lessons) {
|
|
const lr = db.prepare(`
|
|
INSERT INTO lessons (course_id, title, order_index, section_id, read_time)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(newCourseId, l.title, l.order_index, l.section_id ? (secMap[l.section_id] || null) : null, l.read_time || 0);
|
|
const newLid = lr.lastInsertRowid;
|
|
|
|
const blocks = db.prepare('SELECT * FROM lesson_blocks WHERE lesson_id = ? ORDER BY order_index').all(l.id);
|
|
for (const b of blocks) {
|
|
db.prepare('INSERT INTO lesson_blocks (lesson_id, type, order_index, data) VALUES (?, ?, ?, ?)').run(newLid, b.type, b.order_index, b.data);
|
|
}
|
|
}
|
|
})();
|
|
|
|
res.status(201).json({ id: newCourseId });
|
|
}
|
|
|
|
/* ── PUT /api/courses/:id ─────────────────────────────────────────────── */
|
|
function update(req, res) {
|
|
const row = db.prepare('SELECT * FROM courses WHERE id = ?').get(req.params.id);
|
|
if (!row) return res.status(404).json({ error: 'Course not found' });
|
|
const { title, description, coverEmoji, orderIndex, isPublished, subjectSlug } = req.body;
|
|
|
|
db.prepare(`
|
|
UPDATE courses SET title=?,description=?,cover_emoji=?,order_index=?,is_published=?,subject_slug=? WHERE id=?
|
|
`).run(
|
|
title ?? row.title,
|
|
description !== undefined ? description : row.description,
|
|
coverEmoji ?? row.cover_emoji,
|
|
orderIndex ?? row.order_index,
|
|
isPublished !== undefined ? (isPublished ? 1 : 0) : row.is_published,
|
|
subjectSlug ?? row.subject_slug,
|
|
row.id
|
|
);
|
|
res.json({ ok: true });
|
|
}
|
|
|
|
/* ── DELETE /api/courses/:id ──────────────────────────────────────────── */
|
|
function remove(req, res) {
|
|
const row = db.prepare('SELECT id FROM courses WHERE id = ?').get(req.params.id);
|
|
if (!row) return res.status(404).json({ error: 'Course not found' });
|
|
db.prepare('DELETE FROM courses WHERE id = ?').run(row.id);
|
|
res.json({ ok: true });
|
|
}
|
|
|
|
/* ── SECTIONS ─────────────────────────────────────────────────────────── */
|
|
function listSections(req, res) {
|
|
const rows = db.prepare('SELECT * FROM course_sections WHERE course_id = ? ORDER BY order_index, id').all(req.params.id);
|
|
res.json(rows.map(s => ({ id: s.id, title: s.title, orderIndex: s.order_index })));
|
|
}
|
|
|
|
function createSection(req, res) {
|
|
const course = db.prepare('SELECT id FROM courses WHERE id = ?').get(req.params.id);
|
|
if (!course) return res.status(404).json({ error: 'Course not found' });
|
|
const { title, orderIndex } = req.body;
|
|
if (!title) return res.status(400).json({ error: 'title required' });
|
|
const r = db.prepare('INSERT INTO course_sections (course_id, title, order_index) VALUES (?, ?, ?)').run(course.id, title.trim(), orderIndex ?? 0);
|
|
res.status(201).json({ id: r.lastInsertRowid });
|
|
}
|
|
|
|
function updateSection(req, res) {
|
|
const s = db.prepare('SELECT * FROM course_sections WHERE id = ? AND course_id = ?').get(req.params.sid, req.params.id);
|
|
if (!s) return res.status(404).json({ error: 'Section not found' });
|
|
const { title, orderIndex } = req.body;
|
|
db.prepare('UPDATE course_sections SET title=?, order_index=? WHERE id=?').run(title ?? s.title, orderIndex ?? s.order_index, s.id);
|
|
res.json({ ok: true });
|
|
}
|
|
|
|
function deleteSection(req, res) {
|
|
const s = db.prepare('SELECT id FROM course_sections WHERE id = ? AND course_id = ?').get(req.params.sid, req.params.id);
|
|
if (!s) return res.status(404).json({ error: 'Section not found' });
|
|
// unlink lessons
|
|
db.prepare('UPDATE lessons SET section_id = NULL WHERE section_id = ?').run(s.id);
|
|
db.prepare('DELETE FROM course_sections WHERE id = ?').run(s.id);
|
|
res.json({ ok: true });
|
|
}
|
|
|
|
/* ── CLASS COURSES ────────────────────────────────────────────────────── */
|
|
function listClassCourses(req, res) {
|
|
const uid = req.user.id;
|
|
const role = req.user.role;
|
|
const pub = role === 'student' ? 'AND c.is_published = 1' : '';
|
|
const rows = db.prepare(`
|
|
SELECT c.*,
|
|
(SELECT COUNT(*) FROM lessons l WHERE l.course_id = c.id ${progressSubquery(role)}) AS lesson_count,
|
|
${DONE_COUNT_SUBQ},
|
|
cc.deadline
|
|
FROM class_courses cc
|
|
JOIN courses c ON cc.course_id = c.id
|
|
WHERE cc.class_id = ? ${pub}
|
|
ORDER BY cc.assigned_at
|
|
`).all(uid, req.params.classId);
|
|
res.json(rows.map(r => ({ ...courseRow(r), deadline: r.deadline })));
|
|
}
|
|
|
|
function assignCourseToClass(req, res) {
|
|
const { classId } = req.params;
|
|
const { courseId, deadline } = req.body;
|
|
if (!courseId) return res.status(400).json({ error: 'courseId required' });
|
|
try {
|
|
db.prepare(`
|
|
INSERT INTO class_courses (class_id, course_id, deadline, assigned_by)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT (class_id, course_id) DO UPDATE SET deadline=excluded.deadline
|
|
`).run(classId, courseId, deadline || null, req.user.id);
|
|
res.json({ ok: true });
|
|
} catch (e) { res.status(400).json({ error: e.message }); }
|
|
}
|
|
|
|
function unassignCourseFromClass(req, res) {
|
|
db.prepare('DELETE FROM class_courses WHERE class_id = ? AND course_id = ?').run(req.params.classId, req.params.courseId);
|
|
res.json({ ok: true });
|
|
}
|
|
|
|
/* ── PATCH /api/courses/:id/publish-all ──────────────────────────────── */
|
|
function publishAll(req, res) {
|
|
const course = db.prepare('SELECT id, created_by FROM courses WHERE id = ?').get(req.params.id);
|
|
if (!course) return res.status(404).json({ error: 'Course not found' });
|
|
if (req.user.role !== 'admin' && course.created_by !== req.user.id)
|
|
return res.status(403).json({ error: 'Forbidden' });
|
|
|
|
const publish = req.body.publish !== false; // default: publish=true
|
|
const { changes } = db.prepare(
|
|
'UPDATE lessons SET is_published = ? WHERE course_id = ?'
|
|
).run(publish ? 1 : 0, course.id);
|
|
|
|
// Also publish/unpublish the course itself
|
|
db.prepare('UPDATE courses SET is_published = ? WHERE id = ?').run(publish ? 1 : 0, course.id);
|
|
|
|
res.json({ ok: true, lessonsUpdated: changes });
|
|
}
|
|
|
|
module.exports = {
|
|
list, search, continueLesson, get, stats, analytics, create, duplicate, update, remove,
|
|
listSections, createSection, updateSection, deleteSection,
|
|
listClassCourses, assignCourseToClass, unassignCourseFromClass,
|
|
publishAll,
|
|
};
|