From 79edcd15968c6c85841cdd4ea38bfb753aef0d21 Mon Sep 17 00:00:00 2001 From: a2nr Date: Fri, 14 Aug 2026 07:07:25 +0700 Subject: [PATCH] fix: sync lesson registry termasuk sub-bab (sub-home.md) agar kolom materi muncul di CSV guru MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lesson_specs() hanya memindai home.md root sehingga lesson di bab//sub-home.md tidak pernah masuk tabel lessons. Akibatnya kolom materi sub-bab hilang saat guru import/export siswa CSV. - services/lesson_service.py: tambah find_all_sub_home_folders() (enumerate level-1 folder di CONTENT_DIR berisi sub-home.md) - services/lesson_registry.py: lesson_specs() gabungkan union root + sub-bab, dedupe slug (root priority) — satu lesson entitas - services/tests/test_lesson_registry.py: test unit + DB integration Verifikasi: 3 passed, 2 skipped (unit); test_sync_includes_sub_bab_lessons passed (DB); 58 passed total termasuk test_repositories + student_management tanpa regresi. --- services/lesson_registry.py | 38 +++++++++--- services/lesson_service.py | 22 +++++++ services/tests/test_lesson_registry.py | 83 ++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 7 deletions(-) diff --git a/services/lesson_registry.py b/services/lesson_registry.py index ae14d14..c36468f 100644 --- a/services/lesson_registry.py +++ b/services/lesson_registry.py @@ -17,19 +17,43 @@ logger = logging.getLogger(__name__) def lesson_specs() -> list[tuple[str, str, int]]: - """Daftar (slug, title, order_index) dari home.md — fresh, tanpa cache. + """Daftar (slug, title, order_index) dari **seluruh** lesson aktif — - Urutan mengikuti daftar Available_Lessons di home.md. + root home.md ditambah setiap sub-home.md (bab//sub-home.md). + + Urutan: root home.md dulu (indeks lokal), lalu sub-bab (indeks lokal). + Slug didedupe (root selalu menang atas sub-bab bila collision) sehingga + satu lesson ≡ satu baris di tabel `lessons`, tak tergantung dari halaman + mana siswa/guru mengakses materi tersebut. + + Tidak pakai cache — fresh setiap panggilan agar perubahan home.md/sub-home.md + yang belum trigger restart tetap terpantau pada sync berikutnya. """ lesson_service.get_lessons.cache_clear() - lessons = lesson_service.get_lessons() - specs = [] - for idx, lesson in enumerate(lessons): + specs: dict[str, tuple[str, str, int]] = {} + + # 1) Root home.md — prioritas utama, urutan index-nya menjadi acuan. + root_lessons = lesson_service.get_lessons() + for idx, lesson in enumerate(root_lessons): slug = lesson["filename"] if slug.endswith(".md"): slug = slug[:-3] - specs.append((slug, lesson["title"], idx)) - return specs + specs[slug] = (slug, lesson["title"], idx) + + # 2) Tiap folder level-1 yang punya sub-home.md. + for folder in lesson_service.find_all_sub_home_folders(): + path = lesson_service.get_sub_home_path(folder) + if not path: + continue + sub_lessons = lesson_service.get_lessons(source_path=path) + for idx, lesson in enumerate(sub_lessons): + slug = lesson["filename"] + if slug.endswith(".md"): + slug = slug[:-3] + # root selamat: setdefault agar urutan root tidak tertimpa. + specs.setdefault(slug, (slug, lesson["title"], idx)) + + return list(specs.values()) def sync_lesson_registry(db: Session) -> dict: diff --git a/services/lesson_service.py b/services/lesson_service.py index 08a15f0..8c4cdbb 100644 --- a/services/lesson_service.py +++ b/services/lesson_service.py @@ -511,6 +511,28 @@ def get_sub_home_data(folder_name): return data +def find_all_sub_home_folders(): + """Return list nama folder level-1 di CONTENT_DIR yang berisi sub-home.md. + + Digunakan lesson_registry.py untuk menemukan semua materi sub-bab agar + ter-sync ke tabel `lessons` (bukan cuma home.md root). Hanya memindai + langsung satu level di bawah CONTENT_DIR — sub-bab Elemes memang strukturnya + flat (bab//sub-home.md). + """ + folders = [] + if not os.path.isdir(CONTENT_DIR): + return folders + try: + for entry in os.scandir(CONTENT_DIR): + if entry.is_dir(follow_symlinks=False) and os.path.exists( + os.path.join(entry.path, 'sub-home.md') + ): + folders.append(entry.name) + except OSError: + pass + return folders + + # --------------------------------------------------------------------------- # Markdown rendering # --------------------------------------------------------------------------- diff --git a/services/tests/test_lesson_registry.py b/services/tests/test_lesson_registry.py index 76bb2d7..6cd9224 100644 --- a/services/tests/test_lesson_registry.py +++ b/services/tests/test_lesson_registry.py @@ -81,3 +81,86 @@ def test_sync_creates_and_deactivates_lessons(tmp_path, monkeypatch): assert third["total"] == 2 finally: db.close() + + +@pytest.mark.skipif(not DB_REQUIRED, reason="butuh DATABASE_URL (PostgreSQL nyata)") +def test_sync_includes_sub_bab_lessons(tmp_path, monkeypatch): + """Lesson yang hanya ada di sub-home.md (bukan di home.md root) tetap + ter-sync ke DB sebagai lesson aktif — agar kolom CSV guru/sub-bab lengkap. + + Regression test untuk bug: sync_lesson_registry hanya membaca home.md root + sehingga materi sub-bab tidak muncul sebagai kolom saat guru import siswa. + """ + from services.database import SessionLocal + from services import repositories + + (tmp_path / "home.md").write_text( + "# Home\n\n---Available_Lessons---\n- [Hello World](hello_world.md)\n", + encoding="utf-8", + ) + (tmp_path / "hello_world.md").write_text("# Hello World\nisi", encoding="utf-8") + + # Buat folder sub-bab dengan sub-home.md yang merujuk lesson unik + # (sub_bab_only) yang TIDAK ada di home.md root. + bab = tmp_path / "dasar" + bab.mkdir() + (bab / "sub-home.md").write_text( + "# Dasar\n\n---Available_Lessons---\n- [Sub Bab Only](sub_bab_only.md)\n", + encoding="utf-8", + ) + (bab / "sub_bab_only.md").write_text("# Sub Bab Only\nisi", encoding="utf-8") + + monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path)) + lesson_service.get_lessons.cache_clear() + + db = SessionLocal() + try: + result = sync_lesson_registry(db) + assert result["specs"] == 2 # hello_world (root) + sub_bab_only (sub-bab) + + active_slugs = {lesson.slug for lesson in repositories.list_active_lessons(db)} + assert "hello_world" in active_slugs + assert "sub_bab_only" in active_slugs # <-- regression assertion + finally: + # Bersihkan: non-aktifkan lesson yang mungkin tertinggal (sub_bab_only) + # agar test idempotent & tidak mengganggu test lain. + repositories.deactivate_missing_lessons(db, {"hello_world", "sub_bab_only"}) + db.commit() + db.close() + + +def test_specs_union_root_and_sub_bab_no_db(tmp_path, monkeypatch): + """Unit (host, tanpa DB): lesson_specs() mengembalikan union root + sub-bab + dengan dedupe slug (root menang atas sub-bab bila collision).""" + (tmp_path / "home.md").write_text( + "# Home\n\n---Available_Lessons---\n" + "- [Root Only](root_only.md)\n" + "- [Shared](shared.md)\n", + encoding="utf-8", + ) + for slug, title in [("root_only", "Root Only"), ("shared", "Shared")]: + (tmp_path / f"{slug}.md").write_text(f"# {title}\nisi", encoding="utf-8") + + bab = tmp_path / "dasar" + bab.mkdir() + (bab / "sub-home.md").write_text( + "# Dasar\n\n---Available_Lessons---\n" + "- [Shared](shared.md)\n" # collision — root priority\n" + "- [Sub Only](sub_only.md)\n", # hanya ada di sub-bab\n" + encoding="utf-8", + ) + (bab / "sub_only.md").write_text("# Sub Only\nisi", encoding="utf-8") + + monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path)) + lesson_service.get_lessons.cache_clear() + + specs = lesson_specs() + slugs = [s for s, _, _ in specs] + + # Tiga lesson unik: root_only, shared, sub_only + assert slugs == ["root_only", "shared", "sub_only"] + + # Slug collision: root title (Shared) menang, bukan sub-bab title + title_by_slug = {s: t for s, t, _ in specs} + assert title_by_slug["shared"] == "Shared" + assert title_by_slug["sub_only"] == "Sub Only"