fix: show lesson progress on sub-home /bab pages
The /bab/<folder> endpoint returned raw get_sub_home_data() output, so lessons had no completed/locked fields and green checkmarks never appeared. api_bab now reads the student token, injects progress via get_ordered_lessons_with_learning_objectives (scoped to the folder's sub-home.md), and computes lock status from prerequisites — matching /lessons. The bab page load also forwards the student token, and tests cover progress injection and prerequisite locking.
This commit is contained in:
parent
67ed58a327
commit
432a8c2eca
|
|
@ -67,6 +67,7 @@ Parses Markdown files to extract content and configuration.
|
|||
- `def get_lessons(source_path=None):` Returns lessons listed in the `Available_Lessons` section of `home.md` (or of `source_path` when given, e.g. a `sub-home.md`).
|
||||
- `def get_ordered_lessons_with_learning_objectives(progress=None, source_path=None):` Returns lessons ordered as they appear in `home.md` (or `source_path`), optionally injected with user progress status.
|
||||
- `def find_sub_home_for_lesson(file_path):` Returns `(sub_home_path, folder_name)` when the lesson's folder (one level inside `content/`) has a `sub-home.md`, else `(None, None)`.
|
||||
- `def get_sub_home_path(folder_name):` Returns the absolute path to a folder's `sub-home.md` (or `None`), using the same `CONTENT_DIR` as `get_sub_home_data`.
|
||||
- `def get_sub_home_data(folder_name):` Parses a folder's `sub-home.md` (title, intro HTML, lesson list) with an mtime-based cache so edits to the file are picked up without restart.
|
||||
- `def render_markdown_content(file_path):` The core parsing function. Uses regex to extract markers like `---INITIAL_CODE---`, `---VELXIO_CIRCUIT---`, etc. It identifies the `active_tabs` needed for the frontend.
|
||||
- `def _parse_flashcards(text):` Specifically parses `---QUIZ_FLASHCARD---` blocks into a structured JSON array for the frontend MCQ/Flashcard component.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import type { Lesson } from '$types/lesson';
|
|||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageLoad = async ({ params, fetch }) => {
|
||||
const res = await fetch(`/api/bab/${params.folder}`);
|
||||
const token = typeof window !== 'undefined'
|
||||
? localStorage.getItem('student_token') ?? ''
|
||||
: '';
|
||||
const query = token ? `?token=${encodeURIComponent(token)}` : '';
|
||||
const res = await fetch(`/api/bab/${params.folder}${query}`);
|
||||
if (!res.ok) {
|
||||
throw error(404, 'Bab not found');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ from flask import Blueprint, request, jsonify, send_from_directory
|
|||
from werkzeug.utils import secure_filename
|
||||
|
||||
from compiler import compiler_factory
|
||||
from config import CONTENT_DIR, ASSETS_DIR
|
||||
from config import ASSETS_DIR
|
||||
from services.lesson_service import (
|
||||
get_ordered_lessons_with_learning_objectives,
|
||||
get_sub_home_data,
|
||||
get_sub_home_path,
|
||||
find_sub_home_for_lesson,
|
||||
render_markdown_content,
|
||||
render_home_content,
|
||||
|
|
@ -58,6 +59,30 @@ def api_bab(folder):
|
|||
data = get_sub_home_data(folder)
|
||||
if not data:
|
||||
return jsonify({'error': 'Bab not found'}), 404
|
||||
|
||||
# Suntik progress & lock status per lesson (sama seperti /lessons) agar
|
||||
# centang hijau & ikon kunci muncul di halaman bab.
|
||||
token = request.args.get('token', '') or request.cookies.get('student_token', '')
|
||||
progress = None
|
||||
if token:
|
||||
progress = get_student_progress(token)
|
||||
|
||||
source_path = get_sub_home_path(folder)
|
||||
ordered = get_ordered_lessons_with_learning_objectives(
|
||||
progress,
|
||||
source_path=source_path if source_path else None,
|
||||
)
|
||||
for lesson in ordered:
|
||||
prereqs = lesson.get('prerequisites', [])
|
||||
is_locked = False
|
||||
if prereqs:
|
||||
for p_slug in prereqs:
|
||||
if not progress or progress.get(p_slug) != 'completed':
|
||||
is_locked = True
|
||||
break
|
||||
lesson['locked'] = is_locked
|
||||
data['lessons'] = ordered
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -384,6 +384,20 @@ def find_sub_home_for_lesson(file_path):
|
|||
return sub_home_path, folder_name
|
||||
|
||||
|
||||
def get_sub_home_path(folder_name):
|
||||
"""Absolute path to a folder's sub-home.md, or None if missing.
|
||||
|
||||
Uses the same CONTENT_DIR as get_sub_home_data() so callers can feed the
|
||||
result back into get_ordered_lessons_with_learning_objectives(source_path=...)
|
||||
without re-deriving the path (and without mismatched CONTENT_DIR copies).
|
||||
"""
|
||||
folder_path = os.path.join(CONTENT_DIR, folder_name)
|
||||
sub_home_path = os.path.join(folder_path, 'sub-home.md')
|
||||
if os.path.isdir(folder_path) and os.path.exists(sub_home_path):
|
||||
return sub_home_path
|
||||
return None
|
||||
|
||||
|
||||
def get_sub_home_data(folder_name):
|
||||
"""Return parsed sub-home data for a given folder name (mtime-cached).
|
||||
|
||||
|
|
|
|||
|
|
@ -106,3 +106,79 @@ def test_lesson_prev_next_within_sub_home(client, content_dir):
|
|||
data = resp.get_json()
|
||||
assert data["prev_lesson"]["filename"] == "hello_world.md"
|
||||
assert data["next_lesson"]["filename"] == "percabangan.md"
|
||||
|
||||
|
||||
def test_bab_lessons_have_progress_fields(client, content_dir, monkeypatch):
|
||||
from routes import lessons as lessons_routes
|
||||
|
||||
# Tanpa token → semua lesson punya field completed/locked, belum ada yang selesai
|
||||
resp = client.get("/bab/bab1")
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
for lesson in data["lessons"]:
|
||||
assert "completed" in lesson
|
||||
assert "locked" in lesson
|
||||
assert lesson["completed"] is False
|
||||
|
||||
# Dengan progress → completed dihitung dari status siswa
|
||||
monkeypatch.setattr(
|
||||
lessons_routes,
|
||||
"get_student_progress",
|
||||
lambda token: {"hello_world": "completed"},
|
||||
)
|
||||
resp = client.get("/bab/bab1?token=siswa1")
|
||||
data = resp.get_json()
|
||||
by_name = {l["filename"]: l for l in data["lessons"]}
|
||||
assert by_name["hello_world.md"]["completed"] is True
|
||||
assert by_name["variabel.md"]["completed"] is False
|
||||
assert by_name["percabangan.md"]["completed"] is False
|
||||
|
||||
|
||||
def test_bab_lesson_locked_by_prerequisites(client, tmp_path, monkeypatch):
|
||||
bab = tmp_path / "bab1"
|
||||
bab.mkdir()
|
||||
(bab / "sub-home.md").write_text(
|
||||
"# Bab Satu\n\n----Available_Lessons----\n"
|
||||
"1. [Dasar](lesson/dasar.md)\n"
|
||||
"2. [Lanjutan](lesson/lanjutan.md)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(bab / "dasar.md").write_text("# Dasar\nMateri.\n", encoding="utf-8")
|
||||
(bab / "lanjutan.md").write_text(
|
||||
"# Lanjutan\n\n---LESSON_INFO---\n**Prerequisites:**\n- [Dasar](lesson/dasar.md)\n"
|
||||
"---END_LESSON_INFO---\n\nMateri lanjutan.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "home.md").write_text("# Home\n\n----Available_Lessons----\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr("services.lesson_service.CONTENT_DIR", str(tmp_path))
|
||||
lesson_service.find_lesson_file.cache_clear()
|
||||
lesson_service.get_lessons.cache_clear()
|
||||
lesson_service.get_lesson_names.cache_clear()
|
||||
lesson_service.get_lessons_with_learning_objectives.cache_clear()
|
||||
|
||||
from routes import lessons as lessons_routes
|
||||
|
||||
# Prasyarat belum selesai → Lanjutan terkunci
|
||||
monkeypatch.setattr(lessons_routes, "get_student_progress", lambda token: {})
|
||||
resp = client.get("/bab/bab1?token=siswa1")
|
||||
data = resp.get_json()
|
||||
by_name = {l["filename"]: l for l in data["lessons"]}
|
||||
assert by_name["dasar.md"]["completed"] is False
|
||||
assert by_name["dasar.md"]["locked"] is False
|
||||
assert by_name["lanjutan.md"]["completed"] is False
|
||||
assert by_name["lanjutan.md"]["locked"] is True
|
||||
|
||||
# Prasyarat selesai → Lanjutan tidak terkunci lagi
|
||||
monkeypatch.setattr(
|
||||
lessons_routes,
|
||||
"get_student_progress",
|
||||
lambda token: {"dasar": "completed"},
|
||||
)
|
||||
resp = client.get("/bab/bab1?token=siswa1")
|
||||
data = resp.get_json()
|
||||
by_name = {l["filename"]: l for l in data["lessons"]}
|
||||
assert by_name["dasar.md"]["completed"] is True
|
||||
assert by_name["dasar.md"]["locked"] is False
|
||||
assert by_name["lanjutan.md"]["completed"] is False
|
||||
assert by_name["lanjutan.md"]["locked"] is False
|
||||
|
|
|
|||
Loading…
Reference in New Issue