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:
@@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// ─── notification_broadcasts ────────────────────────────────────────────
|
||||
await queryInterface.addColumn('notification_broadcasts', 'show_in_sticky', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('notification_broadcasts', 'show_in_notifications', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
// ─── admin_notifications (admin bell) ───────────────────────────────────
|
||||
await queryInterface.addColumn('admin_notifications', 'show_in_notifications', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
// Not used by admin UI right now, but keeps filtering semantics symmetric.
|
||||
await queryInterface.addColumn('admin_notifications', 'show_in_sticky', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
// ─── user_notifications (client notifications + sticky banner) ────────
|
||||
await queryInterface.addColumn('user_notifications', 'show_in_notifications', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('user_notifications', 'show_in_sticky', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('user_notifications', 'show_in_sticky');
|
||||
await queryInterface.removeColumn('user_notifications', 'show_in_notifications');
|
||||
|
||||
await queryInterface.removeColumn('admin_notifications', 'show_in_sticky');
|
||||
await queryInterface.removeColumn('admin_notifications', 'show_in_notifications');
|
||||
|
||||
await queryInterface.removeColumn('notification_broadcasts', 'show_in_notifications');
|
||||
await queryInterface.removeColumn('notification_broadcasts', 'show_in_sticky');
|
||||
},
|
||||
};
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
// Part 1A — new requirement types (submit_text, pass_quiz) + review workflow.
|
||||
// `prompt` is submit_text's instructions field (mirrors link_label/reference_label
|
||||
// as the type-specific display field). `requires_review` opts any submission-based
|
||||
// requirement into admin approve/reject before it counts as complete.
|
||||
// task_requirements.type has no DB-level CHECK constraint (verified against the
|
||||
// live schema — Sequelize enforces the ENUM only at the application layer for
|
||||
// this table), so no constraint migration is needed to add the new type values.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('task_requirements', 'prompt', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
after: 'reference_label',
|
||||
});
|
||||
await queryInterface.addColumn('task_requirements', 'requires_review', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
after: 'prompt',
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('task_requirements', 'requires_review');
|
||||
await queryInterface.removeColumn('task_requirements', 'prompt');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
// Part 1A — completions gain a review workflow (submitted/approved/rejected)
|
||||
// and a response_text field for the new submit_text requirement type.
|
||||
// `status` is added as STRING + an explicit CHECK constraint, matching the
|
||||
// pattern task_progress.type already uses on this CockroachDB instance
|
||||
// (createTable auto-generates `check_type`; addColumn does not, so we add it
|
||||
// ourselves) rather than the ENUM-with-no-constraint gap task_requirements.type
|
||||
// happens to have today.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('task_completions', 'response_text', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
after: 'note',
|
||||
});
|
||||
await queryInterface.addColumn('task_completions', 'status', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'submitted',
|
||||
after: 'response_text',
|
||||
});
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE task_completions ADD CONSTRAINT check_status
|
||||
CHECK (status IN ('submitted', 'approved', 'rejected'))`
|
||||
);
|
||||
await queryInterface.addColumn('task_completions', 'reviewed_by', {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: true,
|
||||
after: 'status',
|
||||
});
|
||||
await queryInterface.addColumn('task_completions', 'reviewed_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
after: 'reviewed_by',
|
||||
});
|
||||
await queryInterface.addColumn('task_completions', 'review_note', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
after: 'reviewed_at',
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('task_completions', 'review_note');
|
||||
await queryInterface.removeColumn('task_completions', 'reviewed_at');
|
||||
await queryInterface.removeColumn('task_completions', 'reviewed_by');
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE task_completions DROP CONSTRAINT IF EXISTS check_status`
|
||||
);
|
||||
await queryInterface.removeColumn('task_completions', 'status');
|
||||
await queryInterface.removeColumn('task_completions', 'response_text');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
// Part 1A — task_progress.type gains 'pass_quiz' (reference_id becomes a
|
||||
// quiz_id for that type; completed is computed from QuizAttempt.passed the
|
||||
// same way unit-quiz has_passed already is). Same drop/recreate CHECK
|
||||
// constraint pattern as 20260101000058-add-exclusive-to-course-subscription.js.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE task_progress DROP CONSTRAINT IF EXISTS check_type`
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE task_progress ADD CONSTRAINT check_type
|
||||
CHECK (type IN ('read_course', 'read_unit', 'read_lesson', 'pass_quiz'))`
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE task_progress DROP CONSTRAINT IF EXISTS check_type`
|
||||
);
|
||||
await queryInterface.sequelize.query(
|
||||
`ALTER TABLE task_progress ADD CONSTRAINT check_type
|
||||
CHECK (type IN ('read_course', 'read_unit', 'read_lesson'))`
|
||||
);
|
||||
},
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
// Part 1A/1C — new learner-facing notification types for the task review
|
||||
// workflow, group assignment, and per-task completion. Seeded already-published
|
||||
// (status: 'sent') like every other is_system row in the original
|
||||
// 20260703000008 migration, so the trigger call sites work immediately without
|
||||
// requiring an admin to publish them first.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
const now = new Date();
|
||||
await queryInterface.bulkInsert('notification_templates', [
|
||||
{
|
||||
type: 'task_submission_reviewed', notify_type: 'task', scope: 'user', is_system: true,
|
||||
label: 'Task Submission Reviewed', status: 'sent',
|
||||
title: 'Submission Reviewed',
|
||||
message: 'Your submission for "{{taskName}}" was {{statusLabel}}.{{reviewNoteSuffix}}',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'task_assigned', notify_type: 'task', scope: 'user', is_system: true,
|
||||
label: 'Task List Assigned', status: 'sent',
|
||||
title: 'New Task Assigned',
|
||||
message: 'You have been assigned "{{taskListName}}" — {{taskCount}} task(s) to complete.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'task_completed', notify_type: 'task', scope: 'user', is_system: true,
|
||||
label: 'Task Completed', status: 'sent',
|
||||
title: 'Task Completed',
|
||||
message: 'You completed "{{taskName}}".',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.bulkDelete('notification_templates', {
|
||||
type: ['task_submission_reviewed', 'task_assigned', 'task_completed'],
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
// Part 1B — task_lists' tasks currently sort only by createdAt (no ordering
|
||||
// column exists). Add order_index (position within the list) and is_required
|
||||
// (mirrors unit_quizzes.is_required — an optional task doesn't block anything
|
||||
// after it in the sequencing lock).
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('tasks', 'order_index', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
after: 'deadline',
|
||||
});
|
||||
await queryInterface.addColumn('tasks', 'is_required', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
after: 'order_index',
|
||||
});
|
||||
|
||||
// Backfill existing rows with a stable order matching current createdAt
|
||||
// sort, so tasks don't all collapse to order_index 0 on first use.
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE tasks t SET order_index = sub.rn - 1
|
||||
FROM (
|
||||
SELECT task_id, ROW_NUMBER() OVER (PARTITION BY task_list_id ORDER BY "createdAt" ASC) AS rn
|
||||
FROM tasks
|
||||
) sub
|
||||
WHERE t.task_id = sub.task_id
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('tasks', 'is_required');
|
||||
await queryInterface.removeColumn('tasks', 'order_index');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
// Part 2A — admin-authored bullet list ("what's included") per plan, replacing
|
||||
// the client's hardcoded 4-item static perks list. Same JSONB-array-of-objects
|
||||
// pattern already used for plan_policies.access_rules / payment_policies.promo_rules.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('tier_plans', 'features', {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true,
|
||||
after: 'description',
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('tier_plans', 'features');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
// Part 2B — standalone (non-course) units currently can never be tier-gated:
|
||||
// canAccessUnit only ever checks course links, and an independent unit by
|
||||
// definition has none. Add a direct, optional subscription field so a
|
||||
// standalone unit can require a tier on its own. Deliberately no CHECK
|
||||
// constraint — mirrors 20260101000065-drop-tier-enum-check-user-tiers.js's
|
||||
// fix, since tier slugs are admin-defined (tier_categories.slug) and not a
|
||||
// fixed enum.
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('units', 'subscription', {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: true,
|
||||
after: 'title',
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('units', 'subscription');
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user