mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
course,tasklist,task and completed validation
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
const { Op } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
|
||||
const { Task, TaskList, TaskRequirement, TaskListGroup } = require('../../models/task/task.mdl');
|
||||
const { Task, TaskList, TaskRequirement, TaskListGroup, TaskPrerequisite } = require('../../models/task/task.mdl');
|
||||
const { TaskCompletion, TaskCompletionFile } = require('../../models/task/task_completion.mdl');
|
||||
const { TaskLinkVisit, TaskProgress } = require('../../models/task/task_progress.mdl');
|
||||
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
|
||||
@@ -207,13 +207,44 @@ const isRequirementDone = (r, signals) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Helper: resolve which sibling task(s) are blocking a locked task ───────
|
||||
// Prerequisites are always siblings within the same task list (enforced by
|
||||
// admin's syncTaskPrerequisites), so names can always be resolved from `arr`.
|
||||
// Mirrors the locked boolean rule above: explicit prereqIds win when present,
|
||||
// otherwise falls back to earlier is_required tasks in order_index order.
|
||||
const resolveLockedBy = (task, i, arr, prereqsByTask, completedById) => {
|
||||
const prereqIds = prereqsByTask.get(task.task_id);
|
||||
const blockers = (prereqIds && prereqIds.length)
|
||||
? arr.filter((t) => prereqIds.includes(t.task_id) && completedById.get(t.task_id) !== true)
|
||||
: arr.slice(0, i).filter((prev) => prev.is_required && !completedById.get(prev.task_id));
|
||||
|
||||
return blockers.map((t) => ({ task_id: t.task_id, name: t.name }));
|
||||
};
|
||||
|
||||
// ─── Helper: explicit prerequisite gate ─────────────────────────────────────
|
||||
// Returns true/false when `taskId` has explicit task_prerequisites rows —
|
||||
// ALL of them must be completed by this user. Returns null when the task has
|
||||
// no explicit prerequisites configured, signaling the caller to fall back to
|
||||
// the default order_index/is_required linear sequencing below.
|
||||
const checkPrerequisitesUnlocked = async (userId, taskId) => {
|
||||
const prereqRows = await TaskPrerequisite.findAll({ where: { task_id: taskId } });
|
||||
if (!prereqRows.length) return null;
|
||||
const results = await Promise.all(prereqRows.map((r) => checkTaskCompletion(userId, r.prerequisite_task_id)));
|
||||
return results.every(Boolean);
|
||||
};
|
||||
|
||||
// ─── Helper: server-side sequencing gate ───────────────────────────────────
|
||||
// Rejects a completion write if any earlier *required* task in the same list
|
||||
// isn't complete yet — the enforcement piece the unit-quiz sequencing
|
||||
// precedent (UnitList.jsx) does NOT have (that one is client-lock-only).
|
||||
// Shared by this file's submitTask and task_progress.controller.js's
|
||||
// visitLink/updateProgress.
|
||||
const assertTaskUnlocked = async (userId, taskListId, orderIndex) => {
|
||||
// Prefers a task's explicit prerequisites (see checkPrerequisitesUnlocked)
|
||||
// when configured; otherwise falls back to the original rule — rejects a
|
||||
// completion write if any earlier *required* task in the same list isn't
|
||||
// complete yet. Shared by this file's submitTask and
|
||||
// task_progress.controller.js's visitLink/updateProgress.
|
||||
const assertTaskUnlocked = async (userId, taskListId, orderIndex, taskId) => {
|
||||
if (taskId) {
|
||||
const prereqResult = await checkPrerequisitesUnlocked(userId, taskId);
|
||||
if (prereqResult !== null) return prereqResult;
|
||||
}
|
||||
|
||||
const earlierRequired = await Task.findAll({
|
||||
where: { task_list_id: taskListId, order_index: { [Op.lt]: orderIndex }, is_required: true },
|
||||
include: [{ model: TaskRequirement, as: 'requirements' }],
|
||||
@@ -274,6 +305,7 @@ const fireTaskCompletedEvent = async (userId, taskId) => {
|
||||
exports.getTaskCompletionSignals = getTaskCompletionSignals;
|
||||
exports.isRequirementDone = isRequirementDone;
|
||||
exports.assertTaskUnlocked = assertTaskUnlocked;
|
||||
exports.checkPrerequisitesUnlocked = checkPrerequisitesUnlocked;
|
||||
exports.checkTaskCompletion = checkTaskCompletion;
|
||||
exports.fireTaskCompletedEvent = fireTaskCompletedEvent;
|
||||
|
||||
@@ -357,14 +389,28 @@ exports.getGroupTaskList = async (req, res) => {
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// ── Bucket each task by per-requirement completion ──────────────────────
|
||||
const bucketedTasks = tasks.map((task) => {
|
||||
// ── has_completed per task (needed up-front — both the bucket AND the
|
||||
// locked computation below depend on sibling tasks' completion) ────────
|
||||
const completedById = new Map(tasks.map((task) => {
|
||||
const requirements = task.requirements ?? [];
|
||||
return [task.task_id, requirements.length > 0 && requirements.every((r) => isRequirementDone(r, signals))];
|
||||
}));
|
||||
|
||||
const allRequirementsDone = requirements.length > 0 &&
|
||||
requirements.every((r) => isRequirementDone(r, signals));
|
||||
// ── Explicit prerequisite edges for these tasks ─────────────────────────
|
||||
const prereqEdges = taskIds.length
|
||||
? await TaskPrerequisite.findAll({ where: { task_id: { [Op.in]: taskIds } }, attributes: ['task_id', 'prerequisite_task_id'] })
|
||||
: [];
|
||||
const prereqsByTask = new Map();
|
||||
for (const { task_id, prerequisite_task_id } of prereqEdges) {
|
||||
if (!prereqsByTask.has(task_id)) prereqsByTask.set(task_id, []);
|
||||
prereqsByTask.get(task_id).push(prerequisite_task_id);
|
||||
}
|
||||
|
||||
const has_completed = allRequirementsDone;
|
||||
// ── Bucket + lock each task — a task with explicit prerequisites is
|
||||
// locked until ALL of them are done; otherwise fall back to the linear
|
||||
// order_index/is_required rule (same rule assertTaskUnlocked enforces).
|
||||
const bucketedTasks = tasks.map((task, i, arr) => {
|
||||
const has_completed = completedById.get(task.task_id);
|
||||
|
||||
let bucket;
|
||||
if (has_completed) {
|
||||
@@ -375,7 +421,10 @@ exports.getGroupTaskList = async (req, res) => {
|
||||
bucket = 'ongoing';
|
||||
}
|
||||
|
||||
return { ...task, has_completed, _bucket: bucket };
|
||||
const lockedBy = resolveLockedBy(task, i, arr, prereqsByTask, completedById);
|
||||
const locked = lockedBy.length > 0;
|
||||
|
||||
return { ...task, has_completed, locked, lockedBy, _bucket: bucket };
|
||||
});
|
||||
|
||||
// ── Filter by requested status, strip internal _bucket field ──────────
|
||||
@@ -479,6 +528,16 @@ exports.getGroupTaskLists = async (req, res) => {
|
||||
return requirements.every((r) => isRequirementDone(r, signals));
|
||||
};
|
||||
|
||||
// ── Explicit prerequisite edges for these tasks ─────────────────────────
|
||||
const prereqEdges = taskIds.length
|
||||
? await TaskPrerequisite.findAll({ where: { task_id: { [Op.in]: taskIds } }, attributes: ['task_id', 'prerequisite_task_id'] })
|
||||
: [];
|
||||
const prereqsByTask = new Map();
|
||||
for (const { task_id, prerequisite_task_id } of prereqEdges) {
|
||||
if (!prereqsByTask.has(task_id)) prereqsByTask.set(task_id, []);
|
||||
prereqsByTask.get(task_id).push(prerequisite_task_id);
|
||||
}
|
||||
|
||||
// ── Bucket each task list based on per-task has_completed ───────────────
|
||||
const bucketed = taskLists.map((tl) => {
|
||||
const json = tl.toJSON();
|
||||
@@ -488,6 +547,14 @@ exports.getGroupTaskLists = async (req, res) => {
|
||||
task.has_completed = computeHasCompleted(task);
|
||||
});
|
||||
|
||||
// has_completed lookup scoped to THIS list's tasks (order_index
|
||||
// fallback only ever looks at siblings within the same list).
|
||||
const completedById = new Map(tasks.map((task) => [task.task_id, task.has_completed]));
|
||||
tasks.forEach((task, i, arr) => {
|
||||
task.lockedBy = resolveLockedBy(task, i, arr, prereqsByTask, completedById);
|
||||
task.locked = task.lockedBy.length > 0;
|
||||
});
|
||||
|
||||
let bucket;
|
||||
if (tasks.length === 0) {
|
||||
bucket = 'ongoing';
|
||||
@@ -588,6 +655,7 @@ exports.getTask = async (req, res) => {
|
||||
const data = task.toJSON();
|
||||
data.latest_completion = data.completions?.[0] ?? null;
|
||||
delete data.completions;
|
||||
data.locked = !(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index, taskId));
|
||||
|
||||
return R.success(res, 'Task retrieved.', data);
|
||||
} catch (err) {
|
||||
@@ -655,7 +723,7 @@ exports.submitTask = async (req, res) => {
|
||||
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId }, transaction: t });
|
||||
if (!task) { await t.rollback(); return R.error(res, 'Task not found.', 404); }
|
||||
|
||||
if (!(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index))) {
|
||||
if (!(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index, taskId))) {
|
||||
await t.rollback();
|
||||
return R.error(res, 'Complete the earlier required tasks in this list first.', 409);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user