mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
change query
This commit is contained in:
@@ -62,7 +62,7 @@ async function getTaskListMembers(taskListId) {
|
|||||||
|
|
||||||
// ─── Allowed filter/sort fields ───────────────────────────────────────────────
|
// ─── Allowed filter/sort fields ───────────────────────────────────────────────
|
||||||
const TASK_LIST_FIELDS = ['name', 'description', 'createdAt', 'updatedAt', 'deletedAt'];
|
const TASK_LIST_FIELDS = ['name', 'description', 'createdAt', 'updatedAt', 'deletedAt'];
|
||||||
const TASK_FIELDS = ['name', 'description', 'deadline', 'order_index', 'is_required', 'status', 'createdAt', 'updatedAt', 'deletedAt'];
|
const TASK_FIELDS = ['name', 'description', 'deadline', 'order_index', 'status', 'createdAt', 'updatedAt', 'deletedAt'];
|
||||||
const FILTERABLE_MODELS = {
|
const FILTERABLE_MODELS = {
|
||||||
TaskList: TaskList,
|
TaskList: TaskList,
|
||||||
Task: Task,
|
Task: Task,
|
||||||
@@ -816,7 +816,7 @@ exports.createTask = async (req, res) => {
|
|||||||
const t = await sequelize.transaction();
|
const t = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const { taskListId } = req.params;
|
const { taskListId } = req.params;
|
||||||
const { name, description, deadline, is_required, requirements = [], prerequisite_task_ids = [] } = req.body;
|
const { name, description, deadline, requirements = [], prerequisite_task_ids = [] } = req.body;
|
||||||
|
|
||||||
if (!name) return R.error(res, 'Task name is required.', 400);
|
if (!name) return R.error(res, 'Task name is required.', 400);
|
||||||
|
|
||||||
@@ -835,7 +835,6 @@ exports.createTask = async (req, res) => {
|
|||||||
description,
|
description,
|
||||||
deadline: deadline || null,
|
deadline: deadline || null,
|
||||||
order_index,
|
order_index,
|
||||||
is_required: is_required ?? true,
|
|
||||||
createdBy: req.user.user_id,
|
createdBy: req.user.user_id,
|
||||||
updatedBy: req.user.user_id,
|
updatedBy: req.user.user_id,
|
||||||
},
|
},
|
||||||
@@ -899,7 +898,7 @@ exports.updateTask = async (req, res) => {
|
|||||||
return R.error(res, 'Task not found.', 404);
|
return R.error(res, 'Task not found.', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, description, deadline, status, is_required, accepts_submissions, requirements, prerequisite_task_ids } = req.body;
|
const { name, description, deadline, status, accepts_submissions, requirements, prerequisite_task_ids } = req.body;
|
||||||
|
|
||||||
const wasAccepting = task.accepts_submissions;
|
const wasAccepting = task.accepts_submissions;
|
||||||
|
|
||||||
@@ -908,7 +907,6 @@ exports.updateTask = async (req, res) => {
|
|||||||
if (description !== undefined) updates.description = description;
|
if (description !== undefined) updates.description = description;
|
||||||
if (deadline !== undefined) updates.deadline = deadline || null;
|
if (deadline !== undefined) updates.deadline = deadline || null;
|
||||||
if (status !== undefined) updates.status = status;
|
if (status !== undefined) updates.status = status;
|
||||||
if (is_required !== undefined) updates.is_required = is_required;
|
|
||||||
if (accepts_submissions !== undefined) updates.accepts_submissions = accepts_submissions;
|
if (accepts_submissions !== undefined) updates.accepts_submissions = accepts_submissions;
|
||||||
|
|
||||||
await task.update(updates, { transaction: t });
|
await task.update(updates, { transaction: t });
|
||||||
|
|||||||
@@ -210,56 +210,31 @@ const isRequirementDone = (r, signals) => {
|
|||||||
// ─── Helper: resolve which sibling task(s) are blocking a locked task ───────
|
// ─── Helper: resolve which sibling task(s) are blocking a locked task ───────
|
||||||
// Prerequisites are always siblings within the same task list (enforced by
|
// Prerequisites are always siblings within the same task list (enforced by
|
||||||
// admin's syncTaskPrerequisites), so names can always be resolved from `arr`.
|
// admin's syncTaskPrerequisites), so names can always be resolved from `arr`.
|
||||||
// Mirrors the locked boolean rule above: explicit prereqIds win when present,
|
// A task with no explicit task_prerequisites rows is never locked.
|
||||||
// otherwise falls back to earlier is_required tasks in order_index order.
|
|
||||||
const resolveLockedBy = (task, i, arr, prereqsByTask, completedById) => {
|
const resolveLockedBy = (task, i, arr, prereqsByTask, completedById) => {
|
||||||
const prereqIds = prereqsByTask.get(task.task_id);
|
const prereqIds = prereqsByTask.get(task.task_id);
|
||||||
const blockers = (prereqIds && prereqIds.length)
|
if (!prereqIds || !prereqIds.length) return [];
|
||||||
? 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));
|
|
||||||
|
|
||||||
|
const blockers = arr.filter((t) => prereqIds.includes(t.task_id) && completedById.get(t.task_id) !== true);
|
||||||
return blockers.map((t) => ({ task_id: t.task_id, name: t.name }));
|
return blockers.map((t) => ({ task_id: t.task_id, name: t.name }));
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Helper: explicit prerequisite gate ─────────────────────────────────────
|
// ─── Helper: explicit prerequisite gate ─────────────────────────────────────
|
||||||
// Returns true/false when `taskId` has explicit task_prerequisites rows —
|
// 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
|
// ALL of them must be completed by this user. Returns true (unlocked) when
|
||||||
// no explicit prerequisites configured, signaling the caller to fall back to
|
// the task has no explicit prerequisites configured.
|
||||||
// the default order_index/is_required linear sequencing below.
|
|
||||||
const checkPrerequisitesUnlocked = async (userId, taskId) => {
|
const checkPrerequisitesUnlocked = async (userId, taskId) => {
|
||||||
const prereqRows = await TaskPrerequisite.findAll({ where: { task_id: taskId } });
|
const prereqRows = await TaskPrerequisite.findAll({ where: { task_id: taskId } });
|
||||||
if (!prereqRows.length) return null;
|
if (!prereqRows.length) return true;
|
||||||
const results = await Promise.all(prereqRows.map((r) => checkTaskCompletion(userId, r.prerequisite_task_id)));
|
const results = await Promise.all(prereqRows.map((r) => checkTaskCompletion(userId, r.prerequisite_task_id)));
|
||||||
return results.every(Boolean);
|
return results.every(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Helper: server-side sequencing gate ───────────────────────────────────
|
// ─── Helper: server-side sequencing gate ───────────────────────────────────
|
||||||
// Prefers a task's explicit prerequisites (see checkPrerequisitesUnlocked)
|
// A task is locked only by its own explicit task_prerequisites rows — no
|
||||||
// when configured; otherwise falls back to the original rule — rejects a
|
// implicit locking based on list position. Shared by this file's submitTask
|
||||||
// completion write if any earlier *required* task in the same list isn't
|
// and task_progress.controller.js's visitLink/updateProgress.
|
||||||
// complete yet. Shared by this file's submitTask and
|
const assertTaskUnlocked = async (userId, taskId) => checkPrerequisitesUnlocked(userId, taskId);
|
||||||
// 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' }],
|
|
||||||
});
|
|
||||||
if (!earlierRequired.length) return true;
|
|
||||||
|
|
||||||
const taskIds = earlierRequired.map((t) => t.task_id);
|
|
||||||
const allRequirements = earlierRequired.flatMap((t) => (t.requirements ?? []).map((r) => r.toJSON()));
|
|
||||||
const signals = await getTaskCompletionSignals(userId, taskIds, allRequirements);
|
|
||||||
|
|
||||||
return earlierRequired.every((t) => {
|
|
||||||
const reqs = (t.requirements ?? []);
|
|
||||||
return reqs.length > 0 && reqs.every((r) => isRequirementDone(r.toJSON ? r.toJSON() : r, signals));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Helper: is this one task fully done for this user, right now? ─────────
|
// ─── Helper: is this one task fully done for this user, right now? ─────────
|
||||||
const checkTaskCompletion = async (userId, taskId) => {
|
const checkTaskCompletion = async (userId, taskId) => {
|
||||||
@@ -407,8 +382,8 @@ exports.getGroupTaskList = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Bucket + lock each task — a task with explicit prerequisites is
|
// ── Bucket + lock each task — a task with explicit prerequisites is
|
||||||
// locked until ALL of them are done; otherwise fall back to the linear
|
// locked until ALL of them are done; a task with none is never locked
|
||||||
// order_index/is_required rule (same rule assertTaskUnlocked enforces).
|
// (same rule assertTaskUnlocked enforces).
|
||||||
const bucketedTasks = tasks.map((task, i, arr) => {
|
const bucketedTasks = tasks.map((task, i, arr) => {
|
||||||
const has_completed = completedById.get(task.task_id);
|
const has_completed = completedById.get(task.task_id);
|
||||||
|
|
||||||
@@ -547,8 +522,8 @@ exports.getGroupTaskLists = async (req, res) => {
|
|||||||
task.has_completed = computeHasCompleted(task);
|
task.has_completed = computeHasCompleted(task);
|
||||||
});
|
});
|
||||||
|
|
||||||
// has_completed lookup scoped to THIS list's tasks (order_index
|
// has_completed lookup scoped to THIS list's tasks (prerequisite
|
||||||
// fallback only ever looks at siblings within the same list).
|
// edges only ever point at siblings within the same list).
|
||||||
const completedById = new Map(tasks.map((task) => [task.task_id, task.has_completed]));
|
const completedById = new Map(tasks.map((task) => [task.task_id, task.has_completed]));
|
||||||
tasks.forEach((task, i, arr) => {
|
tasks.forEach((task, i, arr) => {
|
||||||
task.lockedBy = resolveLockedBy(task, i, arr, prereqsByTask, completedById);
|
task.lockedBy = resolveLockedBy(task, i, arr, prereqsByTask, completedById);
|
||||||
@@ -655,7 +630,7 @@ exports.getTask = async (req, res) => {
|
|||||||
const data = task.toJSON();
|
const data = task.toJSON();
|
||||||
data.latest_completion = data.completions?.[0] ?? null;
|
data.latest_completion = data.completions?.[0] ?? null;
|
||||||
delete data.completions;
|
delete data.completions;
|
||||||
data.locked = !(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index, taskId));
|
data.locked = !(await assertTaskUnlocked(req.user.user_id, taskId));
|
||||||
|
|
||||||
return R.success(res, 'Task retrieved.', data);
|
return R.success(res, 'Task retrieved.', data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -728,9 +703,9 @@ exports.submitTask = async (req, res) => {
|
|||||||
return R.error(res, 'This task no longer accepts submissions.', 409);
|
return R.error(res, 'This task no longer accepts submissions.', 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index, taskId))) {
|
if (!(await assertTaskUnlocked(req.user.user_id, taskId))) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
return R.error(res, 'Complete the earlier required tasks in this list first.', 409);
|
return R.error(res, 'Complete this task\'s prerequisite tasks first.', 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasComplete = await checkTaskCompletion(req.user.user_id, taskId);
|
const wasComplete = await checkTaskCompletion(req.user.user_id, taskId);
|
||||||
|
|||||||
@@ -218,9 +218,9 @@ exports.visitLink = async (req, res) => {
|
|||||||
|
|
||||||
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId }, transaction: t });
|
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 (!task) { await t.rollback(); return R.error(res, 'Task not found.', 404); }
|
||||||
if (!(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index, taskId))) {
|
if (!(await assertTaskUnlocked(req.user.user_id, taskId))) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
return R.error(res, 'Complete the earlier required tasks in this list first.', 409);
|
return R.error(res, 'Complete this task\'s prerequisite tasks first.', 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasComplete = await checkTaskCompletion(req.user.user_id, taskId);
|
const wasComplete = await checkTaskCompletion(req.user.user_id, taskId);
|
||||||
@@ -349,9 +349,9 @@ exports.updateProgress = async (req, res) => {
|
|||||||
|
|
||||||
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId }, transaction: t });
|
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 (!task) { await t.rollback(); return R.error(res, 'Task not found.', 404); }
|
||||||
if (!(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index, taskId))) {
|
if (!(await assertTaskUnlocked(req.user.user_id, taskId))) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
return R.error(res, 'Complete the earlier required tasks in this list first.', 409);
|
return R.error(res, 'Complete this task\'s prerequisite tasks first.', 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// The order_index/is_required linear sequencing lock (added in
|
||||||
|
// 20260709000005-add-order-index-and-is-required-to-tasks) is redundant now
|
||||||
|
// that tasks have an explicit prerequisite graph (task_prerequisites, added
|
||||||
|
// in 20260717000001-create-task-prerequisites). Locking a task is now purely
|
||||||
|
// a function of its task_prerequisites rows — a task with none is never
|
||||||
|
// implicitly locked by its position in the list. order_index is kept as a
|
||||||
|
// display-order-only column.
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.removeColumn('tasks', 'is_required');
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.addColumn('tasks', 'is_required', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
after: 'order_index',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -41,8 +41,7 @@ const Task = sequelize.define('Task', {
|
|||||||
name: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: true }, order: 1, filterable: true },
|
name: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: true }, order: 1, filterable: true },
|
||||||
description: { type: DataTypes.TEXT, allowNull: true, hidden: true, filterable: false },
|
description: { type: DataTypes.TEXT, allowNull: true, hidden: true, filterable: false },
|
||||||
deadline: { type: DataTypes.DATE, allowNull: true, order: 2, filterable: true },
|
deadline: { type: DataTypes.DATE, allowNull: true, order: 2, filterable: true },
|
||||||
order_index: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, order: 2.1, filterable: true, comment: 'Position within the task list — drives sequencing lock.' },
|
order_index: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, order: 2.1, filterable: true, comment: 'Position within the task list — display order only. Locking is driven solely by explicit task_prerequisites.' },
|
||||||
is_required: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, order: 2.2, filterable: true, comment: 'Optional tasks do not block later tasks in the sequencing lock.' },
|
|
||||||
accepts_submissions: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, order: 2.25, filterable: true, comment: 'When false, no new TaskCompletion submissions are accepted for this task (existing completions are unaffected).' },
|
accepts_submissions: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, order: 2.25, filterable: true, comment: 'When false, no new TaskCompletion submissions are accepted for this task (existing completions are unaffected).' },
|
||||||
status: { type: DataTypes.ENUM('pending', 'in_progress', 'completed', 'overdue'), defaultValue: 'pending', allowNull: false, filterable: true },
|
status: { type: DataTypes.ENUM('pending', 'in_progress', 'completed', 'overdue'), defaultValue: 'pending', allowNull: false, filterable: true },
|
||||||
auto_marked_at: { type: DataTypes.DATE, allowNull: true, filterable: true, comment: 'Set only by the taskOverdue cron sweep when it auto-flips status; never touched by user-driven completion.' },
|
auto_marked_at: { type: DataTypes.DATE, allowNull: true, filterable: true, comment: 'Set only by the taskOverdue cron sweep when it auto-flips status; never touched by user-driven completion.' },
|
||||||
|
|||||||
Reference in New Issue
Block a user