Tambah asesmen per komponen, batasi Dasbor/Rekap ke kaprodi, penugasan per kelas

- Halaman nilai: tombol isi-cepat per komponen penilaian (bagian G RPS) yang
  menyalin nilai ke seluruh Sub-CPMK terkait; komponen berulang (Kuis, Tugas,
  Presentasi, Tes Lisan) mendukung banyak sesi dengan rata-rata otomatis,
  komponen sekali-jalan (UTS/UAS) mengedit sesi yang sama. Sub-CPMK yang
  diukur >1 komponen digulung rata tertimbang bobot (hitung.skor_dari_sesi).
- Hapus jalur unggah Excel & mode "ketik rata-rata kelas" (agregat) dari
  input nilai — proses diseragamkan: hanya grid manual + tombol komponen,
  keduanya bersumber dari daftar mahasiswa impor kaprodi.
- Dasbor Monitoring, Rekap CPL/CPMK, dan Riwayat kini khusus kaprodi (dosen
  tidak lagi melihat rollup lintas-MK yang bukan miliknya).
- Penugasan dosen ke MK bisa dibatasi per kelas (kolom kelas baru di tabel
  penugasan, migrasi otomatis & aman untuk DB yang sudah berjalan) — dosen
  MK dengan banyak kelas paralel hanya melihat & menilai kelasnya sendiri;
  hapus nilai MK penuh dikunci untuk dosen yang dibatasi kelas.
This commit is contained in:
Power BI Dev
2026-07-21 12:44:57 +07:00
parent a45b08b00e
commit afb9e72ba2
8 changed files with 490 additions and 274 deletions
+50
View File
@@ -91,6 +91,56 @@ def bobot_sub_dari_rps(rps_data: dict | None) -> dict[str, float]:
return bobot
def skor_dari_sesi(komponen_daftar: list[dict], sesi: dict) -> dict[str, dict[str, float]]:
"""npm -> {sub: skor}, dihitung dari log sesi input per-komponen (tombol
'isi per komponen' di halaman nilai): rata sederhana antar sesi dalam
komponen yang sama (mis. beberapa kali Kuis), lalu rata tertimbang bobot
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).
"""
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_npm: dict[str, list[float]] = {}
for s in daftar_sesi:
for npm, v in (s.get("nilai") or {}).items():
if v is not None:
per_npm.setdefault(npm, []).append(float(v))
per_komponen_avg[i] = {npm: _rata(xs) for npm, xs in per_npm.items()}
kontribusi_per_sub: dict[str, list[tuple[float, dict]]] = {}
for i, avg_map in per_komponen_avg.items():
k = komponen_by_i[i]
try:
bobot = float(k.get("bobot") or 0)
except (TypeError, ValueError):
bobot = 0
n_subs = len(k["subs"]) or 1
w = bobot / n_subs if bobot else 1.0
for sub in k["subs"]:
kontribusi_per_sub.setdefault(sub, []).append((w, avg_map))
out: dict[str, dict[str, float]] = {}
for sub, kontribusi in kontribusi_per_sub.items():
npms: set[str] = set()
for _, avg_map in kontribusi:
npms |= avg_map.keys()
for npm in npms:
pasangan = [(w, avg_map[npm]) for w, avg_map in kontribusi if npm in avg_map]
total_w = sum(w for w, _ in pasangan)
nilai = (sum(w * v for w, v in pasangan) / total_w) if total_w \
else _rata([v for _, v in pasangan])
out.setdefault(npm, {})[sub] = nilai
return out
def nilai_akhir_mahasiswa(nilai_data: dict, bobot_sub: dict[str, float]) -> list[dict]:
"""Nilai akhir tertimbang bobot Sub-CPMK per mahasiswa (mode per_mahasiswa)."""
out = []