Implement verified curriculum, video review, watch sessions, ledger safeguards, and remove demo data
This commit is contained in:
@@ -21,8 +21,11 @@ namespace App\Controllers;
|
||||
|
||||
use App\Core\Request;
|
||||
use App\Core\Response;
|
||||
use App\Core\Database;
|
||||
use App\Services\CurriculumService;
|
||||
use App\Services\CurriculumExtractorService;
|
||||
use App\Services\PublishedContentService;
|
||||
use App\Services\LearningPackageService;
|
||||
|
||||
class CurriculumController
|
||||
{
|
||||
@@ -164,16 +167,33 @@ class CurriculumController
|
||||
$response->json(['status' => 'success', 'log' => $logContent]);
|
||||
}
|
||||
|
||||
/** GET /api/curriculum/lessons/{lessonId}/english-package */
|
||||
public function getPublishedEnglishPackage(Request $request, Response $response): void
|
||||
{
|
||||
try {
|
||||
$result=LearningPackageService::publishedEnglish(trim((string)$request->getParam('lessonId','')),(int)$request->user_id,$request->getHeader('x-national-id'));
|
||||
$response->status((int)$result['http_status'])->json(array_diff_key($result,['http_status'=>true]));
|
||||
} catch (\Throwable $e) { error_log('English package read failed: '.$e->getMessage()); $response->status(503)->json(['status'=>'unavailable','message'=>'تعذر قراءة حزمة الإنجليزية.']); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Complete Live Tree
|
||||
*/
|
||||
|
||||
public function getTree(Request $request, Response $response): void
|
||||
{
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'data' => CurriculumService::getCurriculumTree()
|
||||
]);
|
||||
$tree = CurriculumService::getCurriculumTree();
|
||||
try {
|
||||
$published = Database::select("SELECT cl.uuid, cl.source_manifest_path, COUNT(vv.id) AS video_count FROM curriculum_lessons cl LEFT JOIN teacher_submissions ts ON ts.curriculum_lesson_id=cl.id AND ts.status='published' LEFT JOIN video_versions vv ON vv.id=ts.current_published_video_version_id AND vv.status='published' WHERE cl.source_status='approved' GROUP BY cl.id, cl.uuid, cl.source_manifest_path");
|
||||
$byPath=[];
|
||||
foreach ($published as $row) $byPath[(string)$row['source_manifest_path']]=['curriculum_lesson_id'=>$row['uuid'],'has_video'=>(int)$row['video_count']>0];
|
||||
foreach ($tree as &$grade) foreach (($grade['subjects'] ?? []) as &$subject) foreach (($subject['semesters'] ?? []) as &$semester) foreach (($semester['units'] ?? []) as &$unit) foreach (($unit['lessons'] ?? []) as &$lesson) {
|
||||
$path=(string)($lesson['file'] ?? '');
|
||||
if (isset($byPath[$path])) $lesson=array_merge($lesson,$byPath[$path]);
|
||||
}
|
||||
unset($grade,$subject,$semester,$unit,$lesson);
|
||||
} catch (\Throwable $e) { error_log('Published curriculum tree enrichment unavailable: '.$e->getMessage()); }
|
||||
$response->json(['status'=>'success','data'=>$tree]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -423,30 +443,18 @@ class CurriculumController
|
||||
$type = $params['type'] ?? 'textbook';
|
||||
|
||||
$storage = realpath(__DIR__ . '/../../storage/curriculum');
|
||||
$resolvedFile = null;
|
||||
|
||||
if (!empty($file) && !str_ends_with($file, '.pdf')) {
|
||||
$cleanFile = ltrim($file, '/');
|
||||
if (file_exists("{$storage}/{$cleanFile}")) {
|
||||
$resolvedFile = "{$storage}/{$cleanFile}";
|
||||
}
|
||||
if (!$storage || empty($file) || !str_ends_with(strtolower($file), '.md')) {
|
||||
$response->status(400)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'يجب تحديد ملف Markdown معتمد للوثيقة المطلوبة.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If not resolved by exact path, resolve by subject
|
||||
if (!$resolvedFile && !empty($subject)) {
|
||||
if (str_contains($subject, 'english') || str_contains($subject, 'إنجليز')) {
|
||||
$mds = glob("{$storage}/grade_10/english_10/semester_1/*/*.md");
|
||||
if (!empty($mds)) $resolvedFile = $mds[0];
|
||||
} elseif (str_contains($subject, 'physic') || str_contains($subject, 'فيزياء')) {
|
||||
$mds = glob("{$storage}/grade_10/physics_10/semester_1/*/*.md");
|
||||
if (!empty($mds)) $resolvedFile = $mds[1] ?? $mds[0];
|
||||
} elseif (str_contains($subject, 'math') || str_contains($subject, 'رياضيات')) {
|
||||
$mds = glob("{$storage}/grade_10/math_10/semester_1/*/*.md");
|
||||
if (!empty($mds)) $resolvedFile = $mds[1] ?? $mds[0];
|
||||
}
|
||||
}
|
||||
|
||||
if ($resolvedFile && file_exists($resolvedFile)) {
|
||||
$candidate = realpath($storage . '/' . ltrim($file, '/'));
|
||||
$storagePrefix = rtrim($storage, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||
if ($candidate && str_starts_with($candidate, $storagePrefix) && is_file($candidate)) {
|
||||
$resolvedFile = $candidate;
|
||||
$content = file_get_contents($resolvedFile);
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
@@ -458,26 +466,87 @@ class CurriculumController
|
||||
return;
|
||||
}
|
||||
|
||||
// Tailored subject fallback
|
||||
$content = self::getDefaultSubjectDocument($subject, $type);
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'subject' => $subject,
|
||||
'type' => $type,
|
||||
'content' => $content
|
||||
$response->status(404)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'ملف الوثيقة المعتمد غير موجود أو غير متاح حالياً.'
|
||||
]);
|
||||
}
|
||||
|
||||
private static function getDefaultSubjectDocument(string $subject, string $type): string
|
||||
/**
|
||||
* Serves an approved asset from a published curriculum bundle by UUID only.
|
||||
* GET /api/curriculum/assets/{assetId}
|
||||
*/
|
||||
public function getPublishedAsset(Request $request, Response $response): void
|
||||
{
|
||||
$s = strtolower($subject);
|
||||
if (str_contains($s, 'english') || str_contains($s, 'إنجليز')) {
|
||||
return "## Unit 01: Looking Good — Vocabulary & Reading\n\n### 1. Key Vocabulary & Word Formation\n- **Casual clothing:** Everyday informal garments (jeans, sneakers, hoodie).\n- **Traditional attire:** Cultural heritage wear (Jordanian Thobe and Keffiyeh).\n- **Subconscious influence:** The way external appearance affects cognitive confidence.\n\n### 2. Reading Text: The Power of First Impressions\nResearch shows that within the first seven seconds of meeting someone, people make subconscious judgments about character, competence, and reliability. In a famous experiment, doctors wearing clean white coats demonstrated higher diagnostic focus.\n\n### 3. Grammar Workshop: Articles (a, an, the, zero article)\n- Use **a / an** for non-specific singular countable nouns.\n- Use **the** when the listener knows which specific thing is meant.\n- Use **zero article** with plural or uncountable nouns spoken in general terms.";
|
||||
$assetId = trim((string)$request->getParam('assetId', ''));
|
||||
try {
|
||||
$asset = PublishedContentService::findPublishedAsset($assetId);
|
||||
} catch (\Throwable $e) {
|
||||
error_log('Published asset lookup failed: ' . $e->getMessage());
|
||||
$response->status(503)->json([
|
||||
'status' => 'unavailable',
|
||||
'message' => 'فهرس الأصول المنشورة غير متاح حالياً.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (str_contains($s, 'physic') || str_contains($s, 'فيزياء')) {
|
||||
return "## الوحدة الأولى: المتجهات والكميات الفيزيائية\n\n### 1. التمييز بين الكميات القياسية والمتجهة\n- **الكميات القياسية:** تُحدد بالمقدار ووحدة القياس فقط (الكتلة، الزمن، الطاقة، درجة الحرارة).\n- **الكميات المتجهة:** تُحدد بالمقدار والاتجاه ونقطة التأثير (القوة، السرعة المتجهة، التسارع، الإزاحة).\n\n### 2. جمع وتحليل المتجهات بيانياً وتحليلياً\n- **المركبة الأفقية:** A_x = A cos θ\n- **المركبة الرأسية:** A_y = A sin θ\n- **المحصلة:** R = √(R_x² + R_y²)\n- **زاوية الاتجاه:** tan θ = |R_y / R_x|";
|
||||
|
||||
if (!$asset) {
|
||||
$response->status(404)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'الأصل المطلوب غير منشور أو غير متاح لك.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
return "## ملخص الوحدة والنتاجات التعليمية المقررة\n\n### 1. الأهداف العامة للمادة\n- استيعاب المفاهيم والمصطلحات الأساسية وفق المنهاج الوزاري المعتمد.\n- التدرب على حل النماذج والتطبيقات العملية والأسئلة الوزارية.\n- التحقق من اكتساب المهارات من خلال بنك الأسئلة التكيفي والمختبر.";
|
||||
|
||||
$path = PublishedContentService::readLocalAsset($asset);
|
||||
if (!$path) {
|
||||
$response->status(404)->json([
|
||||
'status' => 'error',
|
||||
'message' => 'النسخة المنشورة من الأصل غير متاحة في التخزين.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_starts_with((string)$asset['mime_type'], 'text/')) {
|
||||
$content = file_get_contents($path);
|
||||
if ($content === false) {
|
||||
$response->status(503)->json(['status' => 'unavailable', 'message' => 'تعذر قراءة الأصل المنشور.']);
|
||||
return;
|
||||
}
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'asset' => self::assetMetadata($asset),
|
||||
'content' => $content,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$response->setHeader('Content-Type', (string)$asset['mime_type']);
|
||||
$response->setHeader('Content-Length', (string)$asset['byte_size']);
|
||||
$response->setHeader('Content-Disposition', 'inline; filename="' . basename($path) . '"');
|
||||
$response->sendHeaders();
|
||||
readfile($path);
|
||||
exit;
|
||||
}
|
||||
|
||||
private static function assetMetadata(array $asset): array
|
||||
{
|
||||
return [
|
||||
'asset_id' => $asset['uuid'],
|
||||
'asset_type' => $asset['asset_type'],
|
||||
'mime_type' => $asset['mime_type'],
|
||||
'byte_size' => (int)$asset['byte_size'],
|
||||
'sha256' => $asset['sha256'],
|
||||
'bundle_version' => $asset['bundle_version'],
|
||||
'curriculum_lesson_id' => $asset['curriculum_lesson_uuid'],
|
||||
'identity' => [
|
||||
'grade' => $asset['grade_key'],
|
||||
'subject' => $asset['subject_key'],
|
||||
'semester' => $asset['semester_key'],
|
||||
'unit' => $asset['unit_key'],
|
||||
'lesson' => $asset['lesson_key'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -593,4 +662,3 @@ class CurriculumController
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user