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
|
|
|
"""
|
2026-08-30 04:12:23 +07:00
|
|
|
Parsing/format status progress.
|
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
|
|
|
|
2026-08-30 04:12:23 +07:00
|
|
|
Kontrak legacy (dipakai UI live app, laporan guru, dan endpoint attempt kuis):
|
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
|
|
|
- belum mulai : kosong ("") atau "not_started"
|
|
|
|
|
- selesai : "completed"
|
|
|
|
|
- skor : "<earned>/<total>", misalnya "3/4"
|
|
|
|
|
|
2026-08-30 04:12:23 +07:00
|
|
|
Grammar round-trip CSV (dipakai export/import siswa via parse_roundtrip_cell / format_roundtrip_cell):
|
|
|
|
|
- belum mulai/diamkan : kosong ("") atau "not_started" -> None (merge semantics)
|
|
|
|
|
- reset lesson : "RESET" (case-insensitive) -> hapus progress lesson utk siswa ybs
|
|
|
|
|
- selesai : "completed"
|
|
|
|
|
- skor kuis : "<earned>/<total>"
|
|
|
|
|
- composite : "done:<ex>:<earned>/<total>" (<ex>: "1" / "0" / "")
|
|
|
|
|
|
|
|
|
|
Catatan pemakaian spreadsheet (Excel / Google Sheets):
|
|
|
|
|
Kolom lesson berisi "<earned>/<total>" atau "done:..." sebaiknya diformat sebagai
|
|
|
|
|
**Text** di aplikasi spreadsheet sebelum diedit manual agar format tidak otomatis
|
|
|
|
|
dikonversi menjadi tanggal.
|
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 dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ParsedProgress:
|
|
|
|
|
"""Status progress hasil parse (hanya state non-not_started yang dipertahankan)."""
|
|
|
|
|
|
2026-08-30 04:12:23 +07:00
|
|
|
state: str # 'completed' | 'scored' | 'done' | 'reset'
|
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
|
|
|
score_earned: int | None = None
|
|
|
|
|
score_total: int | None = None
|
2026-08-30 04:12:23 +07:00
|
|
|
exercise_passed: bool | None = None
|
|
|
|
|
quiz_earned: int | None = None
|
|
|
|
|
quiz_total: int | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_roundtrip_cell(value: str) -> ParsedProgress | None:
|
|
|
|
|
"""Grammar sel round-trip (single source of truth untuk import & export):
|
|
|
|
|
- "" / "not_started" → None (tidak diubah — merge semantics)
|
|
|
|
|
- "RESET" (case-insensitive) → ParsedProgress(state="reset")
|
|
|
|
|
— hapus progress lesson ini utk siswa ybs
|
|
|
|
|
- "completed" → ParsedProgress(state="completed")
|
|
|
|
|
- "<earned>/<total>" → ParsedProgress(state="scored", score_earned=..., score_total=...)
|
|
|
|
|
- "done:<ex>:<earned>/<total>" → ParsedProgress(state="done", exercise_passed=..., quiz_earned=..., quiz_total=...)
|
|
|
|
|
<ex>: "1"→True, "0"→False, ""→None (lesson tanpa komponen exercise)
|
|
|
|
|
bagian kuis boleh kosong (lesson tanpa komponen quiz); jika diisi,
|
|
|
|
|
validasi sama seperti "scored" (int, int, total>0, 0<=earned<=total)
|
|
|
|
|
- selain itu → raise ValueError
|
|
|
|
|
"""
|
|
|
|
|
v = (value or "").strip()
|
|
|
|
|
if v in ("", "not_started"):
|
|
|
|
|
return None
|
|
|
|
|
if v.upper() == "RESET":
|
|
|
|
|
return ParsedProgress(state="reset")
|
|
|
|
|
if v == "completed":
|
|
|
|
|
return ParsedProgress(state="completed")
|
|
|
|
|
|
|
|
|
|
if v.startswith("done:"):
|
|
|
|
|
parts = v.split(":")
|
|
|
|
|
if len(parts) != 3:
|
|
|
|
|
raise ValueError(f"format cell done tidak valid: {v!r}")
|
|
|
|
|
|
|
|
|
|
ex_raw = parts[1].strip()
|
|
|
|
|
if ex_raw == "":
|
|
|
|
|
exercise_passed = None
|
|
|
|
|
elif ex_raw == "1":
|
|
|
|
|
exercise_passed = True
|
|
|
|
|
elif ex_raw == "0":
|
|
|
|
|
exercise_passed = False
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"komponen exercise tidak valid (harus 1, 0, atau kosong): {ex_raw!r}")
|
|
|
|
|
|
|
|
|
|
quiz_raw = parts[2].strip()
|
|
|
|
|
if quiz_raw == "":
|
|
|
|
|
quiz_earned = None
|
|
|
|
|
quiz_total = None
|
|
|
|
|
elif "/" in quiz_raw:
|
|
|
|
|
qparts = quiz_raw.split("/")
|
|
|
|
|
if len(qparts) != 2:
|
|
|
|
|
raise ValueError(f"komponen quiz tidak valid: {quiz_raw!r}")
|
|
|
|
|
try:
|
|
|
|
|
q_earned, q_total = int(qparts[0]), int(qparts[1])
|
|
|
|
|
except ValueError:
|
|
|
|
|
raise ValueError(f"skor quiz bukan angka: {quiz_raw!r}")
|
|
|
|
|
if q_total <= 0 or not (0 <= q_earned <= q_total):
|
|
|
|
|
raise ValueError(f"skor quiz tidak valid ({q_earned}/{q_total}): {quiz_raw!r}")
|
|
|
|
|
quiz_earned, quiz_total = q_earned, q_total
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"format quiz tidak valid: {quiz_raw!r}")
|
|
|
|
|
|
|
|
|
|
return ParsedProgress(
|
|
|
|
|
state="done",
|
|
|
|
|
exercise_passed=exercise_passed,
|
|
|
|
|
quiz_earned=quiz_earned,
|
|
|
|
|
quiz_total=quiz_total,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if "/" in v:
|
|
|
|
|
parts = v.split("/")
|
|
|
|
|
if len(parts) == 2:
|
|
|
|
|
try:
|
|
|
|
|
earned, total = int(parts[0]), int(parts[1])
|
|
|
|
|
except ValueError:
|
|
|
|
|
raise ValueError(f"status progress tidak dikenal: {v!r}")
|
|
|
|
|
if total > 0 and 0 <= earned <= total:
|
|
|
|
|
return ParsedProgress(state="scored", score_earned=earned, score_total=total)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"skor tidak valid ({earned}/{total}): {v!r}")
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"status progress tidak dikenal: {v!r}")
|
|
|
|
|
|
|
|
|
|
raise ValueError(f"status progress tidak dikenal: {v!r}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_roundtrip_cell(parsed: ParsedProgress | None) -> str:
|
|
|
|
|
"""Kebalikan dari parse_roundtrip_cell (hanya dipakai EXPORT, tidak pernah
|
|
|
|
|
menghasilkan 'RESET' — itu murni instruksi import).
|
|
|
|
|
state='done' → 'done:<ex>:<earned>/<total>'.
|
|
|
|
|
"""
|
|
|
|
|
if parsed is None:
|
|
|
|
|
return ""
|
|
|
|
|
if parsed.state == "completed":
|
|
|
|
|
return "completed"
|
|
|
|
|
if parsed.state == "scored":
|
|
|
|
|
return f"{parsed.score_earned}/{parsed.score_total}"
|
|
|
|
|
if parsed.state == "done":
|
|
|
|
|
ex_str = ""
|
|
|
|
|
if parsed.exercise_passed is True:
|
|
|
|
|
ex_str = "1"
|
|
|
|
|
elif parsed.exercise_passed is False:
|
|
|
|
|
ex_str = "0"
|
|
|
|
|
|
|
|
|
|
quiz_str = ""
|
|
|
|
|
if parsed.quiz_earned is not None and parsed.quiz_total is not None:
|
|
|
|
|
quiz_str = f"{parsed.quiz_earned}/{parsed.quiz_total}"
|
|
|
|
|
|
|
|
|
|
return f"done:{ex_str}:{quiz_str}"
|
|
|
|
|
if parsed.state in ("not_started", ""):
|
|
|
|
|
return ""
|
|
|
|
|
return parsed.state
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_progress_status(value: str) -> tuple[str, int | None, int | None]:
|
|
|
|
|
"""Parse status legacy → (state, score_earned, score_total).
|
|
|
|
|
|
|
|
|
|
- "" / "not_started" → ("not_started", None, None)
|
|
|
|
|
- "completed" → ("completed", None, None)
|
2026-08-29 00:17:48 +07:00
|
|
|
- "done" → ("done", None, None)
|
|
|
|
|
- "in_progress" → ("in_progress", None, None)
|
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
|
|
|
- "<earned>/<total>" → ("scored", earned, total) bila total > 0 dan
|
|
|
|
|
0 <= earned <= total
|
|
|
|
|
- selain itu → raise ValueError (status tidak dikenal)
|
|
|
|
|
"""
|
|
|
|
|
v = (value or "").strip()
|
|
|
|
|
if v in ("", "not_started"):
|
|
|
|
|
return ("not_started", None, None)
|
|
|
|
|
if v == "completed":
|
|
|
|
|
return ("completed", None, None)
|
2026-08-29 00:17:48 +07:00
|
|
|
if v == "done":
|
|
|
|
|
return ("done", None, None)
|
|
|
|
|
if v == "in_progress":
|
|
|
|
|
return ("in_progress", None, None)
|
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
|
|
|
if "/" in v:
|
|
|
|
|
parts = v.split("/")
|
|
|
|
|
if len(parts) == 2:
|
|
|
|
|
try:
|
|
|
|
|
earned, total = int(parts[0]), int(parts[1])
|
|
|
|
|
except ValueError:
|
|
|
|
|
earned = total = None
|
|
|
|
|
if (
|
|
|
|
|
earned is not None
|
|
|
|
|
and total is not None
|
|
|
|
|
and total > 0
|
|
|
|
|
and 0 <= earned <= total
|
|
|
|
|
):
|
|
|
|
|
return ("scored", earned, total)
|
|
|
|
|
raise ValueError(f"status progress tidak dikenal: {v!r}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_progress_status(progress) -> str:
|
|
|
|
|
"""Render status ke string kontrak legacy.
|
|
|
|
|
|
|
|
|
|
Menerima None, ParsedProgress, tuple (state, earned, total), atau objek
|
|
|
|
|
dengan atribut state/score_earned/score_total (mis. model StudentProgress).
|
|
|
|
|
|
|
|
|
|
- None → ""
|
2026-08-29 00:17:48 +07:00
|
|
|
- done → persen komposit dibulatkan bila ada, else "done"
|
|
|
|
|
- in_progress → "" (harus kosong: tidak dihitung prasyarat lengkap)
|
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
|
|
|
- scored → "<earned>/<total>"
|
|
|
|
|
- completed → "completed"
|
|
|
|
|
- not_started → "not_started"
|
2026-08-29 00:17:48 +07:00
|
|
|
- lainnya → state itu sendiri
|
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
|
|
|
"""
|
|
|
|
|
if progress is None:
|
|
|
|
|
return ""
|
|
|
|
|
if isinstance(progress, tuple):
|
|
|
|
|
state, earned, total = progress
|
|
|
|
|
else:
|
|
|
|
|
state = progress.state
|
|
|
|
|
earned = getattr(progress, "score_earned", None)
|
|
|
|
|
total = getattr(progress, "score_total", None)
|
2026-08-29 00:17:48 +07:00
|
|
|
if state == "in_progress":
|
|
|
|
|
return ""
|
|
|
|
|
if state == "done":
|
|
|
|
|
composite = getattr(progress, "composite_percent", None)
|
|
|
|
|
if composite is not None:
|
|
|
|
|
return f"{round(composite)}"
|
|
|
|
|
return "done"
|
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
|
|
|
if state == "scored":
|
|
|
|
|
return f"{earned}/{total}"
|
|
|
|
|
return state
|