fix: sync lesson registry termasuk sub-bab (sub-home.md) agar kolom materi muncul di CSV guru
lesson_specs() hanya memindai home.md root sehingga lesson di bab/<folder>/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.
This commit is contained in:
parent
432a8c2eca
commit
79edcd1596
|
|
@ -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/<folder>/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:
|
||||
|
|
|
|||
|
|
@ -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/<folder>/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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue