mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
units,lesson as standalone
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
* Description: Standalone Unit / Lesson consumption — the junction revamp lets
|
||||
* learners run Units and Lessons outside any Course:
|
||||
*
|
||||
* GET /client/units → all units w/ lesson counts + is_locked
|
||||
* GET /client/units → INDEPENDENT units only (no course affiliation)
|
||||
* GET /client/lessons → INDEPENDENT lessons only (no unit is course-affiliated)
|
||||
* GET /client/units/:uuid → unit metadata (shared handler)
|
||||
* GET /client/units/:uuid/lessons → unit + ALL lesson data (shared handler)
|
||||
* GET /client/units/:uuid/quiz → the unit's quiz, no course context
|
||||
@@ -16,6 +17,13 @@
|
||||
* be able to access at least one attached course. Lessons resolve through
|
||||
* their parent units the same way.
|
||||
*
|
||||
* Discovery rule (getUnits/getLessons only): only course-free content is
|
||||
* listed at all — a unit with any course affiliation, or a lesson with any
|
||||
* unit that has a course affiliation, is excluded outright rather than
|
||||
* listed-but-locked. This does not affect the single-item endpoints above
|
||||
* (:uuid) — those still enforce access normally for direct links, and
|
||||
* course-scoped consumption runs through a separate controller entirely.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 7, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
@@ -53,11 +61,17 @@ function sanitizeQuestions(questions = []) {
|
||||
|
||||
// ─── UNIT LIBRARY (learner view) ──────────────────────────────────────────────
|
||||
|
||||
// Client-side Units/Lessons browsing only ever shows INDEPENDENT content —
|
||||
// anything affiliated with a course (directly, or for a lesson, through any
|
||||
// of its attached units) is excluded from these listings entirely, not just
|
||||
// flagged locked. This does not affect course-scoped consumption (which runs
|
||||
// through ClientCoursesContext/getCourse, a separate path) or direct-link
|
||||
// access to UnitDetails/LessonDetails, which still enforce access normally.
|
||||
exports.getUnits = async (req, res) => {
|
||||
try {
|
||||
const rows = await sequelize.query(`
|
||||
SELECT
|
||||
u.unit_id, u.uuid, u.title, u.description, u.duration_seconds,
|
||||
u.unit_id, u.uuid, u.title, u.subscription, u.description, u.duration_seconds,
|
||||
(SELECT CAST(COUNT(*) AS INTEGER) FROM unit_lessons ul
|
||||
JOIN lessons l ON l.lesson_id = ul.lesson_id AND l."deletedAt" IS NULL
|
||||
WHERE ul.unit_id = u.unit_id) AS lesson_count,
|
||||
@@ -68,6 +82,11 @@ exports.getUnits = async (req, res) => {
|
||||
WHERE q.unit_id = u.unit_id AND q."deletedAt" IS NULL LIMIT 1) AS quiz_id
|
||||
FROM units u
|
||||
WHERE u."deletedAt" IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM course_units cu
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL
|
||||
WHERE cu.unit_id = u.unit_id
|
||||
)
|
||||
ORDER BY u.title ASC
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
@@ -89,11 +108,12 @@ exports.getUnits = async (req, res) => {
|
||||
coursesByUnit.set(row.unit_id, list);
|
||||
}
|
||||
|
||||
// is_locked mirrors canAccessUnit: standalone units are open, attached units
|
||||
// need at least one accessible course.
|
||||
// is_locked mirrors canAccessUnit: a unit with its own subscription or at
|
||||
// least one attached course needs an access check; a fully open standalone
|
||||
// unit (no subscription, no course links) is never locked.
|
||||
const result = [];
|
||||
for (const row of rows) {
|
||||
const is_locked = Number(row.course_count) > 0
|
||||
const is_locked = (row.subscription || Number(row.course_count) > 0)
|
||||
? !(await coursesCtrl.canAccessUnit(req.user.user_id, row.unit_id))
|
||||
: false;
|
||||
result.push({ ...row, courses: coursesByUnit.get(row.unit_id) ?? [], is_locked });
|
||||
@@ -106,6 +126,66 @@ exports.getUnits = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ─── LESSON LIBRARY (learner view) ────────────────────────────────────────────
|
||||
// Mirrors getUnits above — a Lesson may sit in several Units (each possibly in
|
||||
// different courses), so is_locked/courses are resolved across ALL attached
|
||||
// units rather than a single direct course link.
|
||||
|
||||
exports.getLessons = async (req, res) => {
|
||||
try {
|
||||
const rows = await sequelize.query(`
|
||||
SELECT
|
||||
l.lesson_id, l.uuid, l.title, l.description, l.duration_seconds,
|
||||
(SELECT CAST(COUNT(*) AS INTEGER) FROM unit_lessons ul
|
||||
JOIN units u ON u.unit_id = ul.unit_id AND u."deletedAt" IS NULL
|
||||
WHERE ul.lesson_id = l.lesson_id) AS unit_count
|
||||
FROM lessons l
|
||||
WHERE l."deletedAt" IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM unit_lessons ul
|
||||
JOIN course_units cu ON cu.unit_id = ul.unit_id
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL
|
||||
WHERE ul.lesson_id = l.lesson_id
|
||||
)
|
||||
ORDER BY l.title ASC
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
// Batch-fetch every course reachable through any attached unit, for every
|
||||
// returned lesson, in one query — same batching style as getUnits.
|
||||
const lessonIds = rows.map((r) => r.lesson_id);
|
||||
const courseLinkRows = lessonIds.length ? await sequelize.query(`
|
||||
SELECT DISTINCT ul.lesson_id, c.course_id, c.uuid, c.title, c.subscription
|
||||
FROM unit_lessons ul
|
||||
JOIN course_units cu ON cu.unit_id = ul.unit_id
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL
|
||||
WHERE ul.lesson_id IN (:lessonIds)
|
||||
`, { replacements: { lessonIds }, type: sequelize.QueryTypes.SELECT }) : [];
|
||||
|
||||
const coursesByLesson = new Map();
|
||||
for (const row of courseLinkRows) {
|
||||
const list = coursesByLesson.get(row.lesson_id) ?? [];
|
||||
list.push({ course_id: row.course_id, uuid: row.uuid, title: row.title, subscription: row.subscription });
|
||||
coursesByLesson.set(row.lesson_id, list);
|
||||
}
|
||||
|
||||
// is_locked mirrors canAccessLesson: standalone/unattached lessons are
|
||||
// open, attached lessons need at least one accessible course through
|
||||
// any attached unit.
|
||||
const result = [];
|
||||
for (const row of rows) {
|
||||
const is_locked = Number(row.unit_count) > 0
|
||||
? !(await coursesCtrl.canAccessLesson(req.user.user_id, row.lesson_id))
|
||||
: false;
|
||||
result.push({ ...row, courses: coursesByLesson.get(row.lesson_id) ?? [], is_locked });
|
||||
}
|
||||
|
||||
return R.success(res, "Lessons retrieved.", result);
|
||||
} catch (err) {
|
||||
console.error("[CLIENT][LESSONS][GET ALL]", err);
|
||||
return R.error(res, "Could not retrieve lessons.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── STANDALONE UNIT QUIZ ─────────────────────────────────────────────────────
|
||||
|
||||
exports.getUnitQuiz = async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user