Files
saqel/backend/scripts/publish_grade10_textbooks.php
T

135 lines
5.7 KiB
PHP

<?php
declare(strict_types=1);
/**
* Link previously reviewed Grade 10 textbook PDFs to published bundles.
* Never creates a published bundle, grants rights, or approves an asset.
* Usage: php backend/scripts/publish_grade10_textbooks.php [--apply]
*/
require_once dirname(__DIR__) . '/app/bootstrap.php';
use App\Core\Database;
$apply = in_array('--apply', $argv, true);
$booksRoot = dirname(__DIR__, 2) . '/books';
$curriculumRoot = dirname(__DIR__) . '/storage/curriculum';
$subjectMap = [
'كيمياء' => 'chemistry_10', 'الرياضيات' => 'math_10', 'الفيزياء' => 'physics_10',
'الأحياء' => 'biology_10', 'حياتية' => 'biology_10', 'علوم الأرض' => 'earth_sciences_10',
'اللغة العربية' => 'arabic_10', 'العربية' => 'arabic_10',
'الإنجليزية' => 'english_10', 'الانجليزية' => 'english_10',
'التربية الإسلامية' => 'islamic_10', 'الإسلامية' => 'islamic_10',
'تاريخ' => 'history_10', 'جغرافيا' => 'geography_10',
'الوطنية' => 'civics_10', 'المالية' => 'financial_10',
'الرقمية' => 'digital_skills_10', 'حاسوب' => 'digital_skills_10',
];
if (!is_dir($booksRoot)) {
fwrite(STDERR, "Books directory is missing.\n");
exit(1);
}
$ready = [];
$blocked = [];
foreach (glob($booksRoot . '/*.pdf') ?: [] as $filePath) {
$filename = basename($filePath);
if (!str_contains($filename, 'كتاب الطالب')) {
echo "SKIP {$filename}: not a student textbook\n";
continue;
}
$subject = null;
foreach ($subjectMap as $keyword => $key) {
if (str_contains($filename, $keyword)) { $subject = $key; break; }
}
if ($subject === null) { $blocked[] = "{$filename}: unknown subject"; continue; }
if (str_contains($filename, 'الفصل الثاني')) {
$semester = 'semester_2';
} elseif (str_contains($filename, 'الفصل الأول')) {
$semester = 'semester_1';
} else {
$blocked[] = "{$filename}: explicit semester is missing";
continue;
}
$sha = hash_file('sha256', $filePath);
if ($sha === false) { $blocked[] = "{$filename}: unreadable source"; continue; }
$asset = Database::selectOne(
"SELECT id, storage_driver, storage_key, sha256, review_status, rights_status, source_reference
FROM content_assets WHERE sha256 = ? AND asset_type = 'textbook_pdf' LIMIT 1",
[$sha]
);
if (!$asset || $asset['review_status'] !== 'approved' || $asset['rights_status'] !== 'cleared' ||
trim((string)$asset['source_reference']) === '') {
$blocked[] = "{$filename}: reviewed textbook asset and rights evidence are missing";
continue;
}
if ($asset['storage_driver'] !== 'local') {
$blocked[] = "{$filename}: verify the remote asset with its storage provider before linking";
continue;
}
$storagePath = realpath($curriculumRoot . '/' . $asset['storage_key']);
$rootPath = realpath($curriculumRoot);
if ($storagePath === false || $rootPath === false || !str_starts_with($storagePath, $rootPath . DIRECTORY_SEPARATOR) ||
!hash_equals($sha, (string)hash_file('sha256', $storagePath))) {
$blocked[] = "{$filename}: approved stored file is missing or its checksum differs";
continue;
}
$bundle = Database::selectOne(
"SELECT pb.id FROM publication_bundles pb
JOIN curriculum_lessons cl ON cl.id = pb.curriculum_lesson_id
WHERE cl.grade_key = 'grade_10' AND cl.subject_key = ? AND cl.semester_key = ?
AND cl.source_status = 'approved' AND pb.status = 'published'
ORDER BY cl.unit_key, cl.lesson_key, pb.id LIMIT 1",
[$subject, $semester]
);
if (!$bundle) {
$blocked[] = "{$filename}: no published bundle for {$subject}/{$semester}";
continue;
}
$ready[] = ['name' => $filename, 'bundle_id' => (int)$bundle['id'], 'asset_id' => (int)$asset['id']];
}
foreach ($blocked as $reason) echo "BLOCKED {$reason}\n";
foreach ($ready as $row) echo "READY {$row['name']} => bundle {$row['bundle_id']}\n";
if (!$apply) {
echo "DRY RUN: " . count($ready) . " ready, " . count($blocked) . " blocked.\n";
exit($blocked === [] ? 0 : 2);
}
if ($blocked !== []) {
fwrite(STDERR, "REFUSE: resolve all textbook review and matching blockers before --apply.\n");
exit(2);
}
if ($ready === []) {
fwrite(STDERR, "REFUSE: no reviewed textbook to link.\n");
exit(2);
}
$pdo = Database::getConnection();
$pdo->beginTransaction();
try {
foreach ($ready as $row) {
$current = Database::selectOne(
"SELECT pb.status, cl.source_status, a.review_status, a.rights_status
FROM publication_bundles pb JOIN curriculum_lessons cl ON cl.id = pb.curriculum_lesson_id
JOIN content_assets a ON a.id = ? WHERE pb.id = ? FOR UPDATE",
[$row['asset_id'], $row['bundle_id']]
);
if (!$current || $current['status'] !== 'published' || $current['source_status'] !== 'approved' ||
$current['review_status'] !== 'approved' || $current['rights_status'] !== 'cleared') {
throw new RuntimeException('Approval changed while linking textbooks.');
}
Database::query(
"INSERT INTO publication_bundle_assets (publication_bundle_id, content_asset_id, role, sort_order)
VALUES (?, ?, 'textbook', 1) ON DUPLICATE KEY UPDATE sort_order = VALUES(sort_order)",
[$row['bundle_id'], $row['asset_id']]
);
}
$pdo->commit();
echo "Linked " . count($ready) . " reviewed textbooks.\n";
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
fwrite(STDERR, "FAILED: {$e->getMessage()}\n");
exit(1);
}