Harden published curriculum and student flows
This commit is contained in:
@@ -5,12 +5,12 @@ declare(strict_types=1);
|
||||
/**
|
||||
* Publication manager for Grade 10 curriculum bundles and assets.
|
||||
*
|
||||
* Transitions lessons and their assets from `draft`/`unverified` to `published`/`approved`.
|
||||
* Publishes bundles only after independent lesson and asset approval.
|
||||
*
|
||||
* Safety rules:
|
||||
* - Default mode is a read-only dry run. It writes nothing unless `--apply` is present.
|
||||
* - All database writes happen inside a single atomic transaction.
|
||||
* - Only assets belonging to the selected curriculum lessons are updated.
|
||||
* - This script never grants academic approval or clears rights.
|
||||
*
|
||||
* Usage:
|
||||
* php backend/scripts/publish_grade10_bundle.php --status
|
||||
@@ -93,7 +93,7 @@ if ($isPilot) {
|
||||
'grade_10/math_10/semester_1/unit_01/lesson_01.md',
|
||||
'grade_10/math_10/semester_1/unit_01/lesson_02.md',
|
||||
'grade_10/math_10/semester_1/unit_01/lesson_03.md',
|
||||
'grade_10/math_10/semester_1/unit_01/intro_and_project.md',
|
||||
'grade_10/physics_10/semester_1/unit_02/lesson_03.md',
|
||||
// Physics 10: Semester 1, Units 1 & 2
|
||||
'grade_10/physics_10/semester_1/unit_01/lesson_01.md',
|
||||
'grade_10/physics_10/semester_1/unit_01/lesson_02.md',
|
||||
@@ -140,12 +140,16 @@ if (empty($targetLessons)) {
|
||||
echo "No matching curriculum lessons found for the specified criteria.\n";
|
||||
exit(0);
|
||||
}
|
||||
if ($isPilot && count($targetLessons) !== 12) {
|
||||
fwrite(STDERR, "REFUSE: pilot must contain exactly 12 matching lessons; found " . count($targetLessons) . ".\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$lessonIds = array_column($targetLessons, 'id');
|
||||
$idList = implode(',', array_map('intval', $lessonIds));
|
||||
|
||||
// 3. Plan summary
|
||||
$targetAction = $isRevert ? 'REVERT TO DRAFT' : 'PUBLISH / APPROVE';
|
||||
$targetAction = $isRevert ? 'WITHDRAW PUBLISHED BUNDLES' : 'PUBLISH REVIEWED BUNDLES';
|
||||
echo sprintf("=== Grade 10 Publication Plan (%s) ===\n", $apply ? 'APPLY' : 'DRY-RUN');
|
||||
echo sprintf("Action: %s\n", $targetAction);
|
||||
echo sprintf("Matched Lessons: %d\n", count($targetLessons));
|
||||
@@ -161,8 +165,60 @@ foreach ($targetLessons as $idx => $l) {
|
||||
);
|
||||
}
|
||||
|
||||
function publicationBlockers(array $targetLessons, bool $lock): array
|
||||
{
|
||||
$blockers = [];
|
||||
foreach ($targetLessons as $lesson) {
|
||||
$id = (int)$lesson['id'];
|
||||
$path = $lesson['source_manifest_path'];
|
||||
$detail = Database::selectOne(
|
||||
'SELECT source_status, reviewed_by, reviewed_at FROM curriculum_lessons WHERE id = ?' . ($lock ? ' FOR UPDATE' : ''),
|
||||
[$id]
|
||||
);
|
||||
if (!$detail || $detail['source_status'] !== 'approved' || empty($detail['reviewed_by']) || empty($detail['reviewed_at'])) {
|
||||
$blockers[] = "{$path}: academic approval with reviewer and date is missing";
|
||||
}
|
||||
$bundles = Database::select(
|
||||
'SELECT id, status FROM publication_bundles WHERE curriculum_lesson_id = ?' . ($lock ? ' FOR UPDATE' : ''),
|
||||
[$id]
|
||||
);
|
||||
if (count($bundles) !== 1) {
|
||||
$blockers[] = "{$path}: expected exactly one bundle; found " . count($bundles);
|
||||
continue;
|
||||
}
|
||||
$bundle = $bundles[0];
|
||||
if (!in_array($bundle['status'], ['review', 'published'], true)) {
|
||||
$blockers[] = "{$path}: bundle must be in review before publication";
|
||||
}
|
||||
$assets = Database::select(
|
||||
'SELECT a.id, a.asset_type, a.review_status, a.rights_status, a.source_reference, pba.role
|
||||
FROM publication_bundle_assets pba JOIN content_assets a ON a.id = pba.content_asset_id
|
||||
WHERE pba.publication_bundle_id = ?' . ($lock ? ' FOR UPDATE' : ''),
|
||||
[(int)$bundle['id']]
|
||||
);
|
||||
if ($assets === []) {
|
||||
$blockers[] = "{$path}: bundle has no assets";
|
||||
}
|
||||
$hasPrimary = false;
|
||||
foreach ($assets as $asset) {
|
||||
if ($asset['role'] === 'primary_lesson' && $asset['asset_type'] === 'lesson_markdown') $hasPrimary = true;
|
||||
if ($asset['review_status'] !== 'approved' || $asset['rights_status'] !== 'cleared' || trim((string)$asset['source_reference']) === '') {
|
||||
$blockers[] = "{$path}: asset {$asset['id']} lacks review, rights clearance, or source";
|
||||
}
|
||||
}
|
||||
if (!$hasPrimary) $blockers[] = "{$path}: approved lesson Markdown is missing";
|
||||
}
|
||||
return $blockers;
|
||||
}
|
||||
|
||||
if (!$apply && !$isRevert) {
|
||||
$blockers = publicationBlockers($targetLessons, false);
|
||||
foreach ($blockers as $blocker) echo "BLOCKED {$blocker}\n";
|
||||
echo $blockers === [] ? "\n[Ready for reviewed publication; append --apply.]\n" : "\n[Publication blocked; resolve review evidence first.]\n";
|
||||
exit($blockers === [] ? 0 : 2);
|
||||
}
|
||||
if (!$apply) {
|
||||
echo "\n[Dry-run completed. To execute, append --apply to your command.]\n";
|
||||
echo "\n[Dry-run completed. To withdraw bundles, append --apply.]\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
@@ -172,34 +228,17 @@ $pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
if ($isRevert) {
|
||||
// Revert to draft
|
||||
// Withdrawal preserves academic decisions, rights records and history.
|
||||
Database::query(
|
||||
"UPDATE curriculum_lessons SET source_status = 'unverified' WHERE id IN ({$idList})"
|
||||
);
|
||||
Database::query(
|
||||
"UPDATE publication_bundles SET status = 'draft', published_at = NULL WHERE curriculum_lesson_id IN ({$idList})"
|
||||
);
|
||||
Database::query(
|
||||
"UPDATE content_assets a
|
||||
JOIN publication_bundle_assets pba ON pba.content_asset_id = a.id
|
||||
JOIN publication_bundles pb ON pb.id = pba.publication_bundle_id
|
||||
SET a.review_status = 'draft', a.rights_status = 'review_required'
|
||||
WHERE pb.curriculum_lesson_id IN ({$idList})"
|
||||
"UPDATE publication_bundles SET status = 'withdrawn', withdrawn_at = CURRENT_TIMESTAMP WHERE curriculum_lesson_id IN ({$idList}) AND status = 'published'"
|
||||
);
|
||||
} else {
|
||||
// Publish and approve
|
||||
$blockers = publicationBlockers($targetLessons, true);
|
||||
if ($blockers !== []) {
|
||||
throw new RuntimeException("Publication blocked:\n" . implode("\n", $blockers));
|
||||
}
|
||||
Database::query(
|
||||
"UPDATE curriculum_lessons SET source_status = 'approved', reviewed_at = CURRENT_TIMESTAMP WHERE id IN ({$idList})"
|
||||
);
|
||||
Database::query(
|
||||
"UPDATE publication_bundles SET status = 'published', published_at = CURRENT_TIMESTAMP WHERE curriculum_lesson_id IN ({$idList})"
|
||||
);
|
||||
Database::query(
|
||||
"UPDATE content_assets a
|
||||
JOIN publication_bundle_assets pba ON pba.content_asset_id = a.id
|
||||
JOIN publication_bundles pb ON pb.id = pba.publication_bundle_id
|
||||
SET a.review_status = 'approved', a.rights_status = 'cleared'
|
||||
WHERE pb.curriculum_lesson_id IN ({$idList})"
|
||||
"UPDATE publication_bundles SET status = 'published', published_at = CURRENT_TIMESTAMP WHERE curriculum_lesson_id IN ({$idList}) AND status = 'review'"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user