2026-01-02 06:18:48 +07:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
|
|
|
|
C Programming Learning Management System
|
2026-03-25 09:39:51 +07:00
|
|
|
Application factory — assembles blueprints and startup tasks.
|
|
|
|
|
Flask serves as a JSON API consumed by the SvelteKit frontend.
|
2026-01-02 06:18:48 +07:00
|
|
|
"""
|
|
|
|
|
|
2026-01-12 12:03:23 +07:00
|
|
|
import logging
|
2026-01-02 06:18:48 +07:00
|
|
|
|
2026-03-25 09:39:51 +07:00
|
|
|
from flask import Flask
|
|
|
|
|
from flask_cors import CORS
|
2026-01-02 06:18:48 +07:00
|
|
|
|
2026-04-22 12:57:54 +07:00
|
|
|
from extensions import limiter
|
2026-01-12 12:03:23 +07:00
|
|
|
|
2026-03-25 09:39:51 +07:00
|
|
|
# Configure logging once at module level
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
2026-01-06 21:38:54 +07:00
|
|
|
)
|
2026-01-02 06:18:48 +07:00
|
|
|
|
2026-01-17 18:58:16 +07:00
|
|
|
|
2026-03-25 09:39:51 +07:00
|
|
|
def create_app():
|
|
|
|
|
"""Application factory."""
|
|
|
|
|
app = Flask(__name__)
|
2026-01-17 18:58:16 +07:00
|
|
|
|
2026-03-25 09:39:51 +07:00
|
|
|
# Allow cross-origin requests from the SvelteKit frontend
|
2026-04-21 15:13:16 +07:00
|
|
|
import os
|
|
|
|
|
allowed_origin = os.environ.get('ORIGIN', '*')
|
|
|
|
|
CORS(app, resources={r"/*": {"origins": allowed_origin}})
|
2026-01-17 18:58:16 +07:00
|
|
|
|
2026-04-22 12:57:54 +07:00
|
|
|
# Initialize extensions
|
|
|
|
|
limiter.init_app(app)
|
|
|
|
|
|
2026-03-25 09:39:51 +07:00
|
|
|
# ── Blueprints ────────────────────────────────────────────────────
|
|
|
|
|
from routes.auth import auth_bp
|
|
|
|
|
from routes.compile import compile_bp
|
|
|
|
|
from routes.lessons import lessons_bp
|
|
|
|
|
from routes.progress import progress_bp
|
2026-04-20 13:43:45 +07:00
|
|
|
from routes.help import help_bp
|
feat(student-management): kelola siswa round-trip CSV + bulk delete (PG)
- Export siswa terpilih/seluruh + progress ke CSV round-trip (token tidak
pernah diekspor; teacher tidak ikut; urutan deterministik)
- Import preview all-or-nothing: siswa existing di-restore (nama+progress,
token lama dipertahankan), siswa baru dibuat dari student_id kosong +
token; conflict tidak menyebut raw token/digest
- Bulk delete multi-seleksi dengan validasi ketat (teacher terlindungi,
zero-delete bila ada ID bermasalah)
- fix(import): deteksi delimiter berbasis schema — menerima titik koma
(;) maupun koma (,), ekspor tetap ';' + BOM; header tab/campuran ditolak
dengan pesan jelas (regresi: file Excel/Sheets kini bisa diimpor)
- refactor: progress_status.py — kontrak status legacy tunggal dipakai
storage PG, export, dan import; report siswa hanya role student
- UI /progress: dialog import/export + bulk delete, selection store,
respon import tanpa raw token; APP_VERSION 22 -> 26
- ops: ORIGIN env var di podman-compose.yml
- docs: README section 9 (workflow round-trip + delimiter) dan
docs/11-database-migration (kelola siswa via round-trip CSV)
- test: parser round-trip (45), repositories, route management (24,
PostgreSQL), frontend 91, fixtures CSV valid/invalid
2026-08-09 07:29:19 +07:00
|
|
|
from routes.student_management import student_management_bp
|
feat(quiz): strict focus-loss anti-cheat + audit attempt
Kebijakan strict: kehilangan fokus saat kuis aktif langsung mengakhiri kuis
dengan penalti, satu attempt per kuis tercatat audit, pembahasan disembunyikan
saat focus_lost, badge pelanggaran muncul di laporan guru.
Backend:
- tabel quiz_attempts (migration 0002) + endpoint POST /quiz-attempts/submit
idempotent dengan unique constraint (satu attempt per token+lesson, 409
untuk duplikat), bounded termination_reason
- models/repositories/storage: QuizAttempt + record/query pelanggaran untuk
laporan guru (progress)
Frontend:
- quiz-integrity.ts: policy hub (isFocusLossEvent, terminateQuiz,
shouldShowQuizReview) + attempt_id via crypto.randomUUID()
- lesson.svelte.ts: finalisasi idempotent; sendBeacon untuk exit path yang
bisa suspend (focus_lost/page_unload), fetch keepalive untuk reason lain
- QuizTab.svelte: summary menyembunyikan pembahasan saat focus_lost
- progress/+page.svelte: badge pelanggaran focus_lost di laporan guru
- APP_VERSION 26 -> 27
Tests & docs:
- 204 backend test pass (unit host + integrasi container, DB elemes_test)
- 107 frontend test pass, svelte-check 0 error
- docs/12-quiz-integrity.md (baru), docs/07-quiz-authoring.md + README FAQ
anti-cheat diperbarui
2026-08-11 20:55:39 +07:00
|
|
|
from routes.quiz_attempts import quiz_attempts_bp
|
2026-01-17 18:58:16 +07:00
|
|
|
|
2026-03-25 09:39:51 +07:00
|
|
|
app.register_blueprint(auth_bp)
|
|
|
|
|
app.register_blueprint(compile_bp)
|
|
|
|
|
app.register_blueprint(lessons_bp)
|
|
|
|
|
app.register_blueprint(progress_bp)
|
2026-04-20 13:43:45 +07:00
|
|
|
app.register_blueprint(help_bp)
|
feat(student-management): kelola siswa round-trip CSV + bulk delete (PG)
- Export siswa terpilih/seluruh + progress ke CSV round-trip (token tidak
pernah diekspor; teacher tidak ikut; urutan deterministik)
- Import preview all-or-nothing: siswa existing di-restore (nama+progress,
token lama dipertahankan), siswa baru dibuat dari student_id kosong +
token; conflict tidak menyebut raw token/digest
- Bulk delete multi-seleksi dengan validasi ketat (teacher terlindungi,
zero-delete bila ada ID bermasalah)
- fix(import): deteksi delimiter berbasis schema — menerima titik koma
(;) maupun koma (,), ekspor tetap ';' + BOM; header tab/campuran ditolak
dengan pesan jelas (regresi: file Excel/Sheets kini bisa diimpor)
- refactor: progress_status.py — kontrak status legacy tunggal dipakai
storage PG, export, dan import; report siswa hanya role student
- UI /progress: dialog import/export + bulk delete, selection store,
respon import tanpa raw token; APP_VERSION 22 -> 26
- ops: ORIGIN env var di podman-compose.yml
- docs: README section 9 (workflow round-trip + delimiter) dan
docs/11-database-migration (kelola siswa via round-trip CSV)
- test: parser round-trip (45), repositories, route management (24,
PostgreSQL), frontend 91, fixtures CSV valid/invalid
2026-08-09 07:29:19 +07:00
|
|
|
app.register_blueprint(student_management_bp)
|
feat(quiz): strict focus-loss anti-cheat + audit attempt
Kebijakan strict: kehilangan fokus saat kuis aktif langsung mengakhiri kuis
dengan penalti, satu attempt per kuis tercatat audit, pembahasan disembunyikan
saat focus_lost, badge pelanggaran muncul di laporan guru.
Backend:
- tabel quiz_attempts (migration 0002) + endpoint POST /quiz-attempts/submit
idempotent dengan unique constraint (satu attempt per token+lesson, 409
untuk duplikat), bounded termination_reason
- models/repositories/storage: QuizAttempt + record/query pelanggaran untuk
laporan guru (progress)
Frontend:
- quiz-integrity.ts: policy hub (isFocusLossEvent, terminateQuiz,
shouldShowQuizReview) + attempt_id via crypto.randomUUID()
- lesson.svelte.ts: finalisasi idempotent; sendBeacon untuk exit path yang
bisa suspend (focus_lost/page_unload), fetch keepalive untuk reason lain
- QuizTab.svelte: summary menyembunyikan pembahasan saat focus_lost
- progress/+page.svelte: badge pelanggaran focus_lost di laporan guru
- APP_VERSION 26 -> 27
Tests & docs:
- 204 backend test pass (unit host + integrasi container, DB elemes_test)
- 107 frontend test pass, svelte-check 0 error
- docs/12-quiz-integrity.md (baru), docs/07-quiz-authoring.md + README FAQ
anti-cheat diperbarui
2026-08-11 20:55:39 +07:00
|
|
|
app.register_blueprint(quiz_attempts_bp)
|
2026-01-17 18:58:16 +07:00
|
|
|
|
2026-03-25 09:39:51 +07:00
|
|
|
# ── Startup tasks ─────────────────────────────────────────────────
|
2026-08-08 11:57:08 +07:00
|
|
|
# Sync metadata lesson ke PostgreSQL (auto-skip bila DB belum aktif).
|
|
|
|
|
# Idempotent; kegagalan hanya di-log — tidak mematikan app.
|
|
|
|
|
from services.lesson_registry import maybe_sync_on_startup
|
|
|
|
|
|
|
|
|
|
maybe_sync_on_startup()
|
2026-01-02 08:45:32 +07:00
|
|
|
|
2026-03-25 09:39:51 +07:00
|
|
|
return app
|
2026-01-02 06:18:48 +07:00
|
|
|
|
2026-01-02 08:45:32 +07:00
|
|
|
|
2026-03-25 09:39:51 +07:00
|
|
|
# Gunicorn entry: gunicorn "app:create_app()"
|
|
|
|
|
# Dev entry: python app.py
|
2026-01-02 06:18:48 +07:00
|
|
|
if __name__ == '__main__':
|
2026-01-12 12:03:23 +07:00
|
|
|
import os
|
|
|
|
|
debug_mode = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
|
2026-03-25 09:39:51 +07:00
|
|
|
application = create_app()
|
|
|
|
|
application.run(host='0.0.0.0', port=5000, debug=debug_mode)
|