elemes/docs/02-backend-flask.md

99 lines
7.2 KiB
Markdown
Raw Normal View History

---
title: Backend (Flask API)
order: 2
category: backend
---
2026-05-14 16:33:45 +07:00
# 02. Backend (Flask API)
The backend is built with Flask, providing API endpoints for the SvelteKit frontend to fetch lessons, track progress, manage authentication, and proxy compilation requests.
## Application Factory (`elemes/app.py`)
- `def create_app():`
Initializes the Flask application, loads configuration from `elemes/config.py` (which reads from `.env`), and registers the following blueprints:
- `auth_bp` (`routes/auth.py`)
- `lessons_bp` (`routes/lessons.py`)
- `compile_bp` (`routes/compile.py`)
- `progress_bp` (`routes/progress.py`)
## Core API Routes
### Authentication (`routes/auth.py`)
- `def login():` (POST `/login`)
Receives `token` in JSON payload. Validates via `token_service.validate_token()`. On success, sets the `student_token` cookie. Rate-limited and includes a 1.5s tarpit for failures.
- `def logout():` (POST `/logout`)
Clears the `student_token` cookie.
- `def validate_token_route():` (POST `/validate-token`)
Checks if the current `student_token` cookie is valid.
### Lessons (`routes/lessons.py`)
- `def api_lessons():` (GET `/lessons`)
Returns a list of all lessons and the rendered `home.md` content via `lesson_service.get_ordered_lessons_with_learning_objectives()`.
- `def api_bab(folder):` (GET `/bab/<folder>`)
Returns parsed `sub-home.md` data for a folder (title, intro HTML, lesson list) via `lesson_service.get_sub_home_data()`. Returns 404 when the folder has no `sub-home.md`.
2026-05-14 16:33:45 +07:00
- `def api_lesson(filename):` (GET `/lesson/<slug>.json`)
Returns the fully parsed lesson data (content, initial code, circuits, key texts, active tabs) via `lesson_service.render_markdown_content(filepath)`. When the lesson lives in a folder that has a `sub-home.md`, `ordered_lessons` (sidebar) and prev/next navigation are scoped to that folder's lesson list; otherwise it falls back to the global `home.md` list.
2026-05-14 16:33:45 +07:00
- `def get_key_text(filename):` (GET `/get-key-text/<slug>`)
Returns only the required keywords for a specific lesson without exposing the full content logic.
### Compilation (`routes/compile.py`)
- `def compile_code():` (POST `/compile`)
Accepts `code` and `language`. Routes execution to the sandboxed worker using the `CompilerFactory`. Incorporates rate-limiting for anonymous users.
- `def velxio_compile():` (POST `/velxio-compile` mapped from `/api/velxio-compile`)
2026-05-14 16:33:45 +07:00
A proxy endpoint that forwards Arduino compilation requests to the Velxio container, enforcing rate limits for anonymous users.
### Progress Tracking (`routes/progress.py`)
- `def track_progress():` (POST `/track-progress`)
Accepts `lesson_name` and `status`. Persists progress to PostgreSQL via `token_service.update_student_progress()`.
feat(quiz): izinkan guru melanjutkan attempt terminated dari jawaban tersimpan Kebijakan termination tetap strict, tapi guru pegang override untuk kendala tak terduga (false-positive blur dari dialog OS, browser crash, refresh tak sengaja). Selain reset (siswa mengulang dari nol), guru kini bisa memberi izin resume — siswa melanjutkan dari jawaban yang sudah tersimpan. - Tambah kolom quiz_attempts.resumed_at (migrasi 0006) sebagai penanda izin guru melanjutkan attempt terminated - Tambah endpoint teacher-only POST /resume-quiz-attempt — kontrak keamanan sama dengan /reset-progress (guru cukup student_id anonim, tanpa token siswa) - Syarat: hanya attempt ber-status terminated yang bisa di-resume; submitted ditolak karena kuis sudah selesai normal - has_quiz_attempt meloloskan attempt ber-izin sehingga gate one-attempt terbuka selama sesi lanjutan - finalize_quiz_attempt menimpa row attempt sekali (status/skor/answers baru) lalu clear resumed_at; retry berikutnya kembali ke jalur idempotent normal - Sinyal ke frontend siswa lewat quiz_resumable pada data lesson; laporan guru menampilkan badge "Dilanjutkan" dan tombol "Lanjutkan" untuk attempt terminated - Frontend: restoreQuizSession() merekonstruksi sesi dari jawaban tersimpan per question_id, is_correct dihitung ulang dari source sebagai guard bila konten soal berubah; option_id tak dikenal dianggap belum dijawab - Frontend: firstUnansweredIndex() menentukan soal awal sesi lanjutan; resolveQuizTabView() memusatkan keputusan view agar transisi resume dapat diuji Vitest - UI siswa: panel "Lanjutkan Kuis" + snackbar mode lanjutan (sekali muncul). Attempt di-refetch segar sebelum sesi dimulai agar izin yang sudah dicabut guru tidak memulai sesi yang jawabannya akan terbuang - Test: 16 integrasi backend (services/tests/test_resume_quiz.py), 9 unit quiz-session, 10 unit quiz-view - Docs: alur resume + keterbatasan di 12-quiz-integrity.md, ringkasan di 07-quiz-authoring.md, kontrak endpoint di 02-backend-flask.md
2026-10-20 19:24:40 +07:00
- `def reset_progress():` (POST `/reset-progress`)
Resets a student's progress and deletes the quiz attempt (one-attempt) by anonymous `student_id`. Requires a teacher token.
- `def resume_quiz_attempt_route():` (POST `/resume-quiz-attempt`)
Grants a student permission to resume a `terminated` quiz attempt from saved answers, by anonymous `student_id`. The attempt row is kept (audit) and marked `resumed_at`; the one-attempt gate opens until the student finishes again. Requires a teacher token. Returns `success: false` when there is no terminated attempt.
2026-05-14 16:33:45 +07:00
- `def api_progress_report():` (GET `/progress-report.json`)
Returns a matrix of all student progress. Requires a teacher token.
- `def export_progress_csv():` (GET `/progress-report/export-csv`)
Exports progress data as a CSV download. Requires a teacher token.
## Services
### Token Service (`services/token_service.py`)
Facade over the single active storage backend — PostgreSQL, via
`services/storage/postgres_backend.py`. The legacy CSV backend and its helpers
(`initialize_tokens_file`, `_load_tokens_safely`) have been **removed**.
- `def validate_token(token):` Returns `{'student_name', 'is_teacher'}` or `None` for a valid token.
- `def is_teacher_token(token):` Returns `True` if the token belongs to the teacher account.
2026-05-14 16:33:45 +07:00
- `def get_student_progress(token):` Returns a dictionary of lesson progress for a specific token.
- `def update_student_progress(token, lesson_name, status="completed"):` Persists progress to PostgreSQL.
- `def get_teacher_token():` Returns the active teacher token (implemented in `postgres_backend`).
- `def reset_student_progress(student_id, lesson_name):` Resets progress by anonymous `student_id` (never the raw token).
feat(quiz): izinkan guru melanjutkan attempt terminated dari jawaban tersimpan Kebijakan termination tetap strict, tapi guru pegang override untuk kendala tak terduga (false-positive blur dari dialog OS, browser crash, refresh tak sengaja). Selain reset (siswa mengulang dari nol), guru kini bisa memberi izin resume — siswa melanjutkan dari jawaban yang sudah tersimpan. - Tambah kolom quiz_attempts.resumed_at (migrasi 0006) sebagai penanda izin guru melanjutkan attempt terminated - Tambah endpoint teacher-only POST /resume-quiz-attempt — kontrak keamanan sama dengan /reset-progress (guru cukup student_id anonim, tanpa token siswa) - Syarat: hanya attempt ber-status terminated yang bisa di-resume; submitted ditolak karena kuis sudah selesai normal - has_quiz_attempt meloloskan attempt ber-izin sehingga gate one-attempt terbuka selama sesi lanjutan - finalize_quiz_attempt menimpa row attempt sekali (status/skor/answers baru) lalu clear resumed_at; retry berikutnya kembali ke jalur idempotent normal - Sinyal ke frontend siswa lewat quiz_resumable pada data lesson; laporan guru menampilkan badge "Dilanjutkan" dan tombol "Lanjutkan" untuk attempt terminated - Frontend: restoreQuizSession() merekonstruksi sesi dari jawaban tersimpan per question_id, is_correct dihitung ulang dari source sebagai guard bila konten soal berubah; option_id tak dikenal dianggap belum dijawab - Frontend: firstUnansweredIndex() menentukan soal awal sesi lanjutan; resolveQuizTabView() memusatkan keputusan view agar transisi resume dapat diuji Vitest - UI siswa: panel "Lanjutkan Kuis" + snackbar mode lanjutan (sekali muncul). Attempt di-refetch segar sebelum sesi dimulai agar izin yang sudah dicabut guru tidak memulai sesi yang jawabannya akan terbuang - Test: 16 integrasi backend (services/tests/test_resume_quiz.py), 9 unit quiz-session, 10 unit quiz-view - Docs: alur resume + keterbatasan di 12-quiz-integrity.md, ringkasan di 07-quiz-authoring.md, kontrak endpoint di 02-backend-flask.md
2026-10-20 19:24:40 +07:00
- `def resume_quiz_attempt(student_id, lesson_name):` Grants a student permission to resume a `terminated` quiz attempt from saved answers (teacher-only; never the raw token). Opens the one-attempt gate (`resumed_at` set) without deleting the audit row.
- `def is_quiz_attempt_resumable(token, lesson_name):` Returns `True` when the student's attempt has been resumed by a teacher — surfaced to the lesson payload as `quiz_resumable`.
Student tokens are stored **only as an HMAC-SHA256 digest** in the
`access_tokens` table (peppered with `TOKEN_PEPPER` from `.env`) — raw tokens
are never persisted and cannot be recovered or exported.
2026-05-14 16:33:45 +07:00
### Lesson Service (`services/lesson_service.py`)
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.
2026-05-14 16:33:45 +07:00
- `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.
feat(quiz): taksonomi key quiz per jenis + MCQ berbobot (partial credit) Fitur B — setiap jenis soal kini punya key sendiri sehingga model penilaiannya eksplisit, tidak ditebak dari isi konten: - ---QUIZ_MCQ--- pilihan ganda biner (tepat satu [x]; larang [N]) - ---QUIZ_WEIGHTED--- pilihan ganda berbobot ([N] >= 1, minimal satu [ ], bobot tertinggi unik = kunci; poin parsial) - ---FLASHCARD--- kartu hafalan tanpa penilaian (larang opsi) - legacy ---QUIZ_FLASHCARD--- / ---INITIAL_QUIZ--- tetap dibaca tanpa perubahan perilaku (regex opsi legacy sengaja tidak berubah — baris '- [10]' di konten lama BUKAN opsi); dihapus dari docs/template - ::diagnostic tetap kategori di dalam QUIZ_MCQ & tetap masuk skor resmi Model skor: rata-rata persentase per jenis yang ada, Math.round, lulus >= 75. statusString: "X/Y" bila hanya MCQ/legacy, "P/100" bila ada weighted, "completed" bila hanya flashcard (auto-lulus). Satu sesi/ attempt untuk semua jenis; soal flat urut dokumen. Parser (backend & frontend mirror, ekstraksi multi-blok per key urut dokumen via loop marker-start paling awal; id q-N deterministik): - lesson_service.py: regex [ xX]?[0-9]* utk key baru, _iter_option_lines kind-aware, _validate_quiz_options per kind, _parse_flashcards(kind, id_offset), _extract_quiz_sections + guard loop maju (marker end hilang tidak lagi infinite loop), get_lesson_quiz_total -> 100 bila ada weighted (round-trip CSV "P/100") - parseFlashcards.ts / index.ts / detectActiveTabs.ts / extractSections.ts: mirror perilaku; heading soal <h3> kini fence-aware (fix fixture stale) Payload attempt baru: quiz_type, earned_points, question_max_points — opsional di validasi backend supaya klien lama tetap diterima; poin di-recompute dari konten saat resume/review. UI: QuizTab skor via statusString + alert berbasis percent, badge weighted (✓ 10 / 🟡 7/10 / ✗ 0) di review, preview guru badge bobot + "Berbobot (total N)", laporan guru badge MCQ/Bobot + tooltip, CSV progress 2 kolom baru per lesson (_quiz_mcq, _quiz_weighted), template editor konten QUIZ_MCQ/Quiz Bobot/Flashcard. Tooling & docs: scripts/convert_quiz_keys.py (konversi legacy -> key baru per run jenis, idempoten, verifikasi parse identik; examples/ content dikonversi), scanner mendukung semua key, docs/07 ditulis ulang, docs/15 direvisi ke desain final, README/documentation/docs 02/04/09 diperbarui. Tes: backend 444 lulus (+33: validasi ketat per key, urutan dokumen, multi-blok, zero regression, field selalu di-emit), frontend 249 lulus (+13 parser key baru, +11 model skor), parity fixture diregenerasi, svelte-check 0 error, vite build OK.
2026-10-25 20:16:56 +07:00
- `def _parse_flashcards(text, kind):` Parses quiz blocks (key `---QUIZ_MCQ---`, `---QUIZ_WEIGHTED---`, `---FLASHCARD---`, dan legacy `---QUIZ_FLASHCARD---`) into a structured JSON array for the frontend quiz component. `kind` menentukan model evaluasi & validasi per jenis key.
2026-05-14 16:33:45 +07:00
See `docs/13-content-sub-home.md` for the author-facing guide on writing `sub-home.md`.
2026-05-14 16:33:45 +07:00
## Compiler Framework (`compiler/`)
The compilation logic is abstracted via a factory pattern.
- `class CompilerFactory:` (`compiler/__init__.py`)
- `def get_compiler(self, language):` Returns the appropriate `BaseCompiler` instance (e.g., `CCompiler` or `PythonCompiler`).
- `class BaseCompiler(ABC):` (`compiler/base_compiler.py`)
- `def compile(self, code, timeout=10):` Abstract method.
- `def run(self, file_path, timeout=5):` Abstract method.
- `class CCompiler(BaseCompiler):` and `class PythonCompiler(BaseCompiler):`
Implementation wrappers that construct payloads and send HTTP requests to the `compiler-worker` container (`http://compiler-worker:8080/execute`).