Tambah ringkasan nilai per komponen, rincian tugas ala RTM, auto-save RPS

- Halaman Nilai: tabel "Nilai per Komponen" (rata-rata skor mentah tiap
  sesi, bisa diedit langsung) berdampingan dengan grid Sub-CPMK yang tetap
  presisi; kolom persentase ternilai di Hasil Ketercapaian.
- Komponen Tugas: sesi bisa menyasar subset Sub-CPMK (RPS tetap batasannya),
  lengkap dengan field ala RTM (bentuk tugas, deskripsi, metode, luaran,
  indikator/bobot, jadwal) dan tombol isi-otomatis per bentuk tugas.
- RPS: auto-save berkala sebagai draft, tombol ke atas, validasi total
  bobot komponen 100% saat ditandai selesai, komponen berbobot kosong
  disembunyikan dari halaman Nilai.
- Admin: tampilkan kelas yang tersedia dari data mahasiswa saat mengatur
  penugasan dosen per kelas.
- Perbaikan step="any" pada input nilai (mencegah validasi HTML5 menolak
  nilai desimal hasil roll-up).
- Tambah METODOLOGI_ASESMEN_KOMPONEN.md: dokumentasi metodologi Sub-CPMK
  vs komponen, termasuk skenario tugas menyasar sebagian Sub-CPMK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Power BI Dev
2026-07-23 23:28:46 +07:00
parent bdaacf1c8f
commit d5edbe2134
7 changed files with 668 additions and 67 deletions
+25 -20
View File
@@ -98,26 +98,20 @@ def skor_dari_sesi(komponen_daftar: list[dict], sesi: dict) -> dict[str, dict[st
antar-komponen bila satu Sub-CPMK diukur oleh >1 komponen (mis. Kuis dan
Presentasi sama-sama menilai satu Sub-CPMK) — bobot mengikuti alokasi
yang sama dengan bobot_sub_dari_rps (bobot komponen dibagi rata ke subnya).
Tiap sesi boleh punya "subs" sendiri (subset Sub-CPMK komponen itu — dipakai
komponen Observasi/Tugas untuk rincian ala RTM per tugas bernama); sesi
tanpa "subs" (atau kosong) dianggap menyasar semua Sub-CPMK komponen itu
(perilaku lama). Dengan begitu satu komponen boleh diisi lewat beberapa
sesi yang masing-masing hanya menyasar sebagian Sub-CPMK, tanpa
menyamaratakan nilainya ke Sub-CPMK lain yang tak disasar sesi itu.
"""
komponen_by_i = {k["i"]: k for k in komponen_daftar}
per_komponen_avg: dict[int, dict[str, float]] = {}
for i_str, daftar_sesi in (sesi or {}).items():
try:
i = int(i_str)
except (TypeError, ValueError):
continue
if i not in komponen_by_i or not daftar_sesi:
continue
per_nim: dict[str, list[float]] = {}
for s in daftar_sesi:
for nim, v in (s.get("nilai") or {}).items():
if v is not None:
per_nim.setdefault(nim, []).append(float(v))
per_komponen_avg[i] = {nim: _rata(xs) for nim, xs in per_nim.items()}
kontribusi_per_sub: dict[str, list[tuple[float, dict]]] = {}
for i, avg_map in per_komponen_avg.items():
k = komponen_by_i[i]
for i, k in komponen_by_i.items():
daftar_sesi = (sesi or {}).get(i) or []
if not daftar_sesi:
continue
try:
bobot = float(k.get("bobot") or 0)
except (TypeError, ValueError):
@@ -125,6 +119,15 @@ def skor_dari_sesi(komponen_daftar: list[dict], sesi: dict) -> dict[str, dict[st
n_subs = len(k["subs"]) or 1
w = bobot / n_subs if bobot else 1.0
for sub in k["subs"]:
relevan = [s for s in daftar_sesi if sub in (s.get("subs") or k["subs"])]
if not relevan:
continue
per_nim: dict[str, list[float]] = {}
for s in relevan:
for nim, v in (s.get("nilai") or {}).items():
if v is not None:
per_nim.setdefault(nim, []).append(float(v))
avg_map = {nim: _rata(xs) for nim, xs in per_nim.items()}
kontribusi_per_sub.setdefault(sub, []).append((w, avg_map))
out: dict[str, dict[str, float]] = {}
@@ -192,9 +195,11 @@ def rollup_mk(peta: dict, nilai_data: dict, ambang: float) -> dict:
cpl: dict[str, dict] = {}
for cp in peta["cpls"]:
skor = _rata([v["skor"] for v in cpmk.values()
if cp["kode"] in v["cpl"] and v["skor"] is not None])
cpl[cp["kode"]] = {"skor": skor}
pendukung = [v for v in cpmk.values() if cp["kode"] in v["cpl"]]
ternilai = [v for v in pendukung if v["skor"] is not None]
skor = _rata([v["skor"] for v in ternilai])
cpl[cp["kode"]] = {"skor": skor, "n_cpmk": len(pendukung),
"n_cpmk_ternilai": len(ternilai)}
return {"sub": sub, "cpmk": cpmk, "cpl": cpl,
"mode": nilai_data.get("mode", "agregat")}