course,tasklist,task and completed validation

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-17 13:04:27 +08:00
parent 612805acaf
commit d49e3be4d2
26 changed files with 1174 additions and 69 deletions
@@ -30,6 +30,7 @@ const { Op } = require('sequelize');
const R = require('../../utils/response.util');
const logActivity = require('../../utils/logActivity.util');
const { recomputeCascade, recordWatchProgress, recordManualComplete } = require('../../services/completion_requirements.service');
const { recordPlaybackPosition } = require('../../services/playback_position.service');
const CourseReadingProgress = require('../../models/courses/course_reading_progress.mdl');
const Certificate = require('../../models/courses/certificate.mdl');
const {
@@ -355,6 +356,116 @@ exports.getCourseTaskContext = async (req, res) => {
}
};
// =============================================================================
// ── TASK CONTEXT FOR A STANDALONE LESSON / UNIT ───────────────────────────────
// =============================================================================
//
// Same idea as getCourseTaskContext, but scoped to a single lesson/unit UUID
// rather than a whole course tree — the fallback source for LessonDetails.jsx/
// UnitReader.jsx (the standalone/library readers reached via /lessons/:uuid and
// /units/:uuid/read) when the page is opened directly rather than navigated to
// from a task's requirement card, so the "Task mode" banner still shows up.
// GET /client/courses/lesson/uuid/:uuid/task-context
exports.getLessonTaskContext = async (req, res) => {
try {
const { uuid } = req.params;
const userId = req.user.user_id;
const lesson = await Lesson.findOne({ where: { uuid, ...notDeleted }, attributes: ['lesson_id', 'uuid'] });
if (!lesson) return R.error(res, 'Lesson not found.', 404);
const { taskListIds, taskListToGroup } = await getAccessibleTaskListIds(userId);
if (!taskListIds.length) return R.success(res, 'Task context retrieved.', { has_task: false, contexts: [] });
const requirements = await TaskRequirement.findAll({
where: { type: 'read_lesson', reference_id: uuid, deletedAt: null },
include: [{
model: Task,
as: 'task',
where: { task_list_id: { [Op.in]: taskListIds }, deletedAt: null },
required: true,
attributes: ['task_id', 'name', 'task_list_id'],
}],
attributes: ['requirement_id', 'task_id', 'reference_id'],
});
if (!requirements.length) return R.success(res, 'Task context retrieved.', { has_task: false, contexts: [] });
const taskIds = [...new Set(requirements.map((r) => r.task_id))];
const doneProgress = await TaskProgress.findAll({
where: { task_id: { [Op.in]: taskIds }, user_id: userId, completed: true },
attributes: ['requirement_id', 'reference_id'],
});
const doneSet = new Set(doneProgress.map((p) => `${p.requirement_id}:${p.reference_id}`));
const contexts = requirements.map((req) => ({
task_id: req.task_id,
task_name: req.task.name,
task_list_id: req.task.task_list_id,
group_id: taskListToGroup[req.task.task_list_id],
requirement_id: req.requirement_id,
already_completed: doneSet.has(`${req.requirement_id}:${req.reference_id}`),
}));
return R.success(res, 'Task context retrieved.', { has_task: true, contexts });
} catch (err) {
console.error('[CLIENT][LESSON TASK CONTEXT]', err);
return R.error(res, 'Could not retrieve task context.', 500);
}
};
// GET /client/courses/unit/uuid/:uuid/task-context
exports.getUnitTaskContext = async (req, res) => {
try {
const { uuid } = req.params;
const userId = req.user.user_id;
const unit = await Unit.findOne({ where: { uuid, ...notDeleted }, attributes: ['unit_id', 'uuid'] });
if (!unit) return R.error(res, 'Unit not found.', 404);
const { taskListIds, taskListToGroup } = await getAccessibleTaskListIds(userId);
if (!taskListIds.length) return R.success(res, 'Task context retrieved.', { has_task: false, contexts: [] });
const requirements = await TaskRequirement.findAll({
where: { type: 'read_unit', reference_id: uuid, deletedAt: null },
include: [{
model: Task,
as: 'task',
where: { task_list_id: { [Op.in]: taskListIds }, deletedAt: null },
required: true,
attributes: ['task_id', 'name', 'task_list_id'],
}],
attributes: ['requirement_id', 'task_id', 'reference_id'],
});
if (!requirements.length) return R.success(res, 'Task context retrieved.', { has_task: false, contexts: [] });
const taskIds = [...new Set(requirements.map((r) => r.task_id))];
const doneProgress = await TaskProgress.findAll({
where: { task_id: { [Op.in]: taskIds }, user_id: userId, completed: true },
attributes: ['requirement_id', 'reference_id'],
});
const doneSet = new Set(doneProgress.map((p) => `${p.requirement_id}:${p.reference_id}`));
const contexts = requirements.map((req) => ({
task_id: req.task_id,
task_name: req.task.name,
task_list_id: req.task.task_list_id,
group_id: taskListToGroup[req.task.task_list_id],
requirement_id: req.requirement_id,
already_completed: doneSet.has(`${req.requirement_id}:${req.reference_id}`),
}));
return R.success(res, 'Task context retrieved.', { has_task: true, contexts });
} catch (err) {
console.error('[CLIENT][UNIT TASK CONTEXT]', err);
return R.error(res, 'Could not retrieve task context.', 500);
}
};
// =============================================================================
// ── UPSERT LESSON PROGRESS ────────────────────────────────────────────────────
// =============================================================================
@@ -453,6 +564,11 @@ exports.upsertWatchProgress = async (req, res) => {
if (!unit || !courseLink) return R.error(res, 'Unit not found.', 404);
if (!lesson || !lessonLink) return R.error(res, 'Lesson not found.', 404);
// Resume-position tracking is unconditional — every block gets it regardless of
// whether a completion requirement is configured. recordWatchProgress, below, is
// the anti-cheat-validated path and stays a no-op when nothing's configured.
await recordPlaybackPosition(userId, { lessonId: lesson.lesson_id, blockId, percent });
const result = await recordWatchProgress(userId, {
lessonId: lesson.lesson_id, lessonUuid: lesson.uuid,
unitId: unit.unit_id, unitUuid: unit.uuid,