mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -16,6 +16,8 @@ const { getFieldValues } = require("../../utils/fieldValues.util");
|
|||||||
const logActivity = require('../../utils/logActivity.util');
|
const logActivity = require('../../utils/logActivity.util');
|
||||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||||
|
const CompletionRequirement = require('../../models/courses/completion_requirement.mdl');
|
||||||
|
const { VALID_ENTITY_TYPES } = require('../../utils/courses/completion_requirements.registry');
|
||||||
|
|
||||||
// ── Models ────────────────────────────────────────────────────────────────────
|
// ── Models ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -218,6 +220,23 @@ exports.createCourseFull = async (req, res) => {
|
|||||||
await t.rollback();
|
await t.rollback();
|
||||||
return R.error(res, "Each new unit needs a title.", 400);
|
return R.error(res, "Each new unit needs a title.", 400);
|
||||||
}
|
}
|
||||||
|
// Requirements only apply to units being created fresh here — units
|
||||||
|
// attached by unit_id already have their own configured set.
|
||||||
|
if (!unit.unit_id && unit.requirements?.length) {
|
||||||
|
const seenTypes = new Set();
|
||||||
|
for (const r of unit.requirements) {
|
||||||
|
const allowedEntityTypes = VALID_ENTITY_TYPES[r.type];
|
||||||
|
if (!allowedEntityTypes || !allowedEntityTypes.includes("unit")) {
|
||||||
|
await t.rollback();
|
||||||
|
return R.error(res, `"${r.type}" cannot be configured on a unit.`, 400);
|
||||||
|
}
|
||||||
|
if (seenTypes.has(r.type)) {
|
||||||
|
await t.rollback();
|
||||||
|
return R.error(res, `Duplicate "${r.type}" requirement — only one per unit is allowed.`, 400);
|
||||||
|
}
|
||||||
|
seenTypes.add(r.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const lesson of unit.lessons ?? []) {
|
for (const lesson of unit.lessons ?? []) {
|
||||||
if (!lesson.lesson_id && !lesson.title) {
|
if (!lesson.lesson_id && !lesson.title) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
@@ -301,6 +320,21 @@ exports.createCourseFull = async (req, res) => {
|
|||||||
duration_seconds: 0,
|
duration_seconds: 0,
|
||||||
createdBy: by,
|
createdBy: by,
|
||||||
}, { transaction: t });
|
}, { transaction: t });
|
||||||
|
|
||||||
|
if (unitInput.requirements?.length) {
|
||||||
|
const rows = unitInput.requirements.map((r, ri) => ({
|
||||||
|
entity_type: "unit",
|
||||||
|
entity_id: unit.unit_id,
|
||||||
|
type: r.type,
|
||||||
|
order: r.order ?? ri,
|
||||||
|
min_percent: r.type === "watch_percent" ? (r.min_percent ?? 100) : null,
|
||||||
|
button_label: r.type === "manual_complete" ? (r.button_label || null) : null,
|
||||||
|
is_required: r.is_required ?? true,
|
||||||
|
createdBy: by,
|
||||||
|
updatedBy: by,
|
||||||
|
}));
|
||||||
|
await CompletionRequirement.bulkCreate(rows, { transaction: t });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await CourseUnit.create({
|
await CourseUnit.create({
|
||||||
@@ -658,10 +692,15 @@ exports.syncPrerequisites = async (req, res) => {
|
|||||||
// ══════════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
// Literal fragments that resolve a unit's order within THIS course.
|
// Literal fragments that resolve a unit's order within THIS course.
|
||||||
|
// course_id is a 19-digit snowflake-style BIGINT — well past
|
||||||
|
// Number.MAX_SAFE_INTEGER, so Number(courseId) silently rounds to the wrong
|
||||||
|
// id and these subqueries match nothing. BigInt preserves full precision
|
||||||
|
// (and throws on non-numeric input, same effect as the old NaN would've had
|
||||||
|
// via the caller's try/catch).
|
||||||
const courseOrderLiteral = (courseId) =>
|
const courseOrderLiteral = (courseId) =>
|
||||||
`(SELECT cu.order_index FROM course_units cu WHERE cu.course_id = ${Number(courseId)} AND cu.unit_id = "Unit"."unit_id" LIMIT 1)`;
|
`(SELECT cu.order_index FROM course_units cu WHERE cu.course_id = ${BigInt(courseId)} AND cu.unit_id = "Unit"."unit_id" LIMIT 1)`;
|
||||||
const courseMembershipLiteral = (courseId) =>
|
const courseMembershipLiteral = (courseId) =>
|
||||||
`(SELECT cu.unit_id FROM course_units cu WHERE cu.course_id = ${Number(courseId)})`;
|
`(SELECT cu.unit_id FROM course_units cu WHERE cu.course_id = ${BigInt(courseId)})`;
|
||||||
|
|
||||||
exports.getUnits = async (req, res) => {
|
exports.getUnits = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -1197,10 +1236,12 @@ exports.getUnitPermanentDeleteImpact = async (req, res) => {
|
|||||||
// Entity-level archive/permanent-delete lives in /admin/lessons (the library).
|
// Entity-level archive/permanent-delete lives in /admin/lessons (the library).
|
||||||
// ══════════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// Same BigInt fix as courseOrderLiteral/courseMembershipLiteral above —
|
||||||
|
// unit_id is a 19-digit snowflake BIGINT, past Number.MAX_SAFE_INTEGER.
|
||||||
const unitOrderLiteral = (unitId) =>
|
const unitOrderLiteral = (unitId) =>
|
||||||
`(SELECT ul.order_index FROM unit_lessons ul WHERE ul.unit_id = ${Number(unitId)} AND ul.lesson_id = "Lesson"."lesson_id" LIMIT 1)`;
|
`(SELECT ul.order_index FROM unit_lessons ul WHERE ul.unit_id = ${BigInt(unitId)} AND ul.lesson_id = "Lesson"."lesson_id" LIMIT 1)`;
|
||||||
const unitMembershipLiteral = (unitId) =>
|
const unitMembershipLiteral = (unitId) =>
|
||||||
`(SELECT ul.lesson_id FROM unit_lessons ul WHERE ul.unit_id = ${Number(unitId)})`;
|
`(SELECT ul.lesson_id FROM unit_lessons ul WHERE ul.unit_id = ${BigInt(unitId)})`;
|
||||||
|
|
||||||
exports.getLessons = async (req, res) => {
|
exports.getLessons = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -73,8 +73,9 @@ const UNIT_LIST_COMPUTED = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "course_count",
|
key: "course_count",
|
||||||
label: "Used in courses",
|
label: "Affiliated",
|
||||||
type: "number",
|
type: "number",
|
||||||
|
order: 2, // 1: Title, 2: Affiliated, 3: Subscription, 4: Duration — see units.mdl.js
|
||||||
literal: `(
|
literal: `(
|
||||||
SELECT CAST(COUNT(*) AS INTEGER)
|
SELECT CAST(COUNT(*) AS INTEGER)
|
||||||
FROM course_units cu
|
FROM course_units cu
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ const Unit = sequelize.define("Unit", {
|
|||||||
unit_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, hidden: true, order: 0, filterable: false },
|
unit_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, hidden: true, order: 0, filterable: false },
|
||||||
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true, hidden: true, order: 0, filterable: false },
|
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true, hidden: true, order: 0, filterable: false },
|
||||||
title: { type: DataTypes.STRING(255), allowNull: false, label: "Title", hidden: false, order: 1, filterable: true },
|
title: { type: DataTypes.STRING(255), allowNull: false, label: "Title", hidden: false, order: 1, filterable: true },
|
||||||
subscription: { type: DataTypes.STRING(50), allowNull: true, label: "Subscription", hidden: false, order: 1.5, filterable: true, comment: "Optional direct tier gate for standalone units — null means open (or gated only via an attached course)." },
|
// order 2 is reserved for the computed "Affiliated" (course_count) column — see UNIT_LIST_COMPUTED in units.controller.js
|
||||||
|
subscription: { type: DataTypes.STRING(50), allowNull: true, label: "Subscription", hidden: false, order: 3, filterable: true, comment: "Optional direct tier gate for standalone units — null means open (or gated only via an attached course)." },
|
||||||
description: { type: DataTypes.TEXT, allowNull: true, label: "Description", hidden: true, order: 0, filterable: false },
|
description: { type: DataTypes.TEXT, allowNull: true, label: "Description", hidden: true, order: 0, filterable: false },
|
||||||
duration_seconds: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 0, hidden: false, order: 2, filterable: false },
|
duration_seconds: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 0, hidden: false, order: 4, filterable: false },
|
||||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, filterable: true },
|
createdBy: { type: DataTypes.BIGINT, allowNull: true, filterable: true },
|
||||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true, filterable: true },
|
updatedBy: { type: DataTypes.BIGINT, allowNull: true, filterable: true },
|
||||||
deletedBy: { type: DataTypes.BIGINT, allowNull: true, filterable: true },
|
deletedBy: { type: DataTypes.BIGINT, allowNull: true, filterable: true },
|
||||||
|
|||||||
Reference in New Issue
Block a user