From b85c75bb314f77dc4a3a91b1af66bb83f6a0e98f Mon Sep 17 00:00:00 2001 From: Power BI Dev Date: Mon, 27 Jul 2026 07:16:44 +0700 Subject: [PATCH] Tambah tabel nilai skala poin, penanda Nilai Akhir belum lengkap, dan peringatan katrol manual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Halaman Nilai kini punya tabel per-mahasiswa dalam skala poin (bukan 0-100) supaya terlihat berapa poin riil yang sudah didapat dari total 100 per Sub-CPMK. Nilai Akhir juga ditandai * kalau sebagian Sub-CPMK belum ternilai (angkanya sementara), dan sel di grid Sub-CPMK ditandai ✎ kalau skornya pernah dikoreksi manual dan berisiko tertimpa balik saat sesi komponen terkait disimpan ulang. --- app/main.py | 88 ++++++++++++++++++++++++- app/templates/form_nilai.html | 119 +++++++++++++++++++++++++++++++++- 2 files changed, 203 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index edd24bb..530ff89 100644 --- a/app/main.py +++ b/app/main.py @@ -703,6 +703,61 @@ def _tabel_komponen(grid: list[dict], komponen_daftar: list[dict], return out +def _rincian_bobot(kode: str) -> tuple[list[dict], dict[str, list[dict]]]: + """Rincian pembagian bobot komponen (RPS bagian G) ke tiap Sub-CPMK — + bobot_komponen ÷ jumlah Sub-CPMK komponen itu, dijumlah per Sub-CPMK bila + diukur >1 komponen. Murni dari konfigurasi RPS (tak bergantung nilai + mahasiswa) — dipakai untuk menjelaskan metodologi bobot di halaman nilai.""" + rps = db.ambil_rps(kode) + per_komponen: list[dict] = [] + per_sub: dict[str, list[dict]] = {} + if not rps: + return per_komponen, per_sub + for k in rps["data"].get("komponen", []): + subs = k.get("subs") or [] + if not subs: + continue + try: + bobot = float(k.get("bobot") or 0) + except (TypeError, ValueError): + bobot = 0 + if not bobot: + continue + poin = bobot / len(subs) + per_komponen.append({"nama": k["nama"], "bobot": bobot, + "n_subs": len(subs), "poin": poin}) + for s in subs: + per_sub.setdefault(s, []).append({"nama": k["nama"], "poin": poin}) + return per_komponen, per_sub + + +def _tabel_poin(grid: list[dict], bobot_sub_rincian: dict[str, list[dict]]) -> list[dict]: + """Nilai per mahasiswa dalam skala POIN (bukan 0-100) — tiap Sub-CPMK + ditimbang sesuai kontribusi poinnya ke total 100 (lihat _rincian_bobot). + Poin diperoleh = skor/100 × poin maks Sub-CPMK itu. Kolom Total adalah + jumlah poin yang SUDAH didapat mahasiswa dari Sub-CPMK yang sudah + ternilai — bukan nilai akhir yang direskalakan ke bobot yang ternilai + saja, jadi Total ini bisa lebih kecil dari Nilai Akhir bila masih ada + Sub-CPMK yang belum dinilai.""" + poin_maks = {s: sum(c["poin"] for c in kontribusi) + for s, kontribusi in bobot_sub_rincian.items()} + out = [] + for m in grid: + skor = m.get("skor") or {} + poin, total = {}, 0.0 + for s, maks in poin_maks.items(): + v = skor.get(s) + if v is None: + poin[s] = None + else: + dapat = v / 100 * maks + poin[s] = {"dapat": dapat, "maks": maks} + total += dapat + out.append({"nim": m["nim"], "nama": m["nama"], "kelas": m.get("kelas"), + "poin": poin, "total": total}) + return out + + def _konteks_nilai(request, kode, pesan="", peringatan=None): peta = db.peta_mk(kode) tersimpan = db.ambil_dok("nilai", kode) @@ -721,13 +776,44 @@ def _konteks_nilai(request, kode, pesan="", peringatan=None): grid = _grid_mahasiswa(tersimpan, terdaftar, kelas_dosen) komponen_daftar = _komponen_daftar(kode) sesi_semua = (tersimpan["data"].get("sesi") or {}) if tersimpan else {} + bobot_komponen, bobot_sub_rincian = _rincian_bobot(kode) + # Nilai Akhir dihitung hanya dari Sub-CPMK yang sudah ternilai (lihat + # nilai_akhir_mahasiswa) — kalau belum semua Sub-CPMK MK ini ternilai, + # angkanya cuma perkiraan sementara dan bisa berubah; ditandai bintang + # di template supaya tak disalahsangka sudah final. + total_subs = len(peta.get("subs") or []) + tidak_lengkap = set() + if total_subs: + for m in grid: + skor = m.get("skor") or {} + n_terisi = sum(1 for s in peta["subs"] if skor.get(s["kode"]) is not None) + if n_terisi < total_subs: + tidak_lengkap.add(m["nim"]) + # Sub-CPMK yang skor tersimpannya beda dari hasil hitung ulang sesi — + # berarti pernah "dikatrol" manual lewat grid Nilai per Mahasiswa. Nilai + # itu akan tertimpa balik kalau sesi/komponen yang mengukur Sub-CPMK + # tsb disimpan lagi (lihat aksi="komponen" di simpan_nilai), jadi + # ditandai di template supaya dosen tak kaget kalau koreksinya hilang. + dikatrol = set() + if komponen_daftar: + skor_hitung_sesi = hitung.skor_dari_sesi(komponen_daftar, sesi_semua) + for m in grid: + terhitung = skor_hitung_sesi.get(m["nim"]) or {} + tersimpan_skor = m.get("skor") or {} + for sub_kode, v_terhitung in terhitung.items(): + v_tersimpan = tersimpan_skor.get(sub_kode) + if v_tersimpan is not None and abs(v_tersimpan - v_terhitung) > 0.05: + dikatrol.add((m["nim"], sub_kode)) return render( request, "form_nilai.html", {"peta": peta, "tersimpan": tersimpan, "hasil": hasil, "ambang": ambang, "akhir": akhir, "tahun": db.tahun_aktif(), "pesan": pesan, - "grid": grid, "kelas_dosen": kelas_dosen, + "grid": grid, "kelas_dosen": kelas_dosen, "tidak_lengkap": tidak_lengkap, + "dikatrol": dikatrol, "komponen_daftar": komponen_daftar, "tabel_komponen": _tabel_komponen(grid, komponen_daftar, sesi_semua) if komponen_daftar else [], + "bobot_komponen": bobot_komponen, "bobot_sub_rincian": bobot_sub_rincian, + "tabel_poin": _tabel_poin(grid, bobot_sub_rincian) if bobot_sub_rincian else [], "peringatan": peringatan or []}, ) diff --git a/app/templates/form_nilai.html b/app/templates/form_nilai.html index 5e869b2..009036e 100644 --- a/app/templates/form_nilai.html +++ b/app/templates/form_nilai.html @@ -99,12 +99,15 @@ min="0" max="100" step="any" value="{{ '%g'|format(r.nilai[k.i]) if r.nilai[k.i] is not none else '' }}"> {% endfor %} - {{ '%.1f'|format(akhir.get(r.nim)) if akhir.get(r.nim) is not none else '—' }} + {{ '%.1f'|format(akhir.get(r.nim)) if akhir.get(r.nim) is not none else '—' }}{% if r.nim in tidak_lengkap and akhir.get(r.nim) is not none %}*{% endif %} {% endfor %} + {% if tidak_lengkap %} +

* Sebagian Sub-CPMK mahasiswa ini belum ternilai — Nilai Akhir dihitung hanya dari yang sudah ternilai dan bisa berubah setelah sisanya diisi.

+ {% endif %}
Disimpan atas nama {{ user.nama }} @@ -144,9 +147,9 @@ {% for s in peta.subs %} + value="{{ '%g'|format(m.skor[s.kode]) if s.kode in m.skor else '' }}">{% if (m.nim, s.kode) in dikatrol %}{% endif %} {% endfor %} - {{ '%.1f'|format(akhir.get(m.nim)) if akhir.get(m.nim) is not none else '—' }} + {{ '%.1f'|format(akhir.get(m.nim)) if akhir.get(m.nim) is not none else '—' }}{% if m.nim in tidak_lengkap and akhir.get(m.nim) is not none %}*{% endif %} {% endfor %} {% if per_mhs and hasil %} @@ -170,6 +173,12 @@
+ {% if tidak_lengkap %} +

* Sebagian Sub-CPMK mahasiswa ini belum ternilai — Nilai Akhir dihitung hanya dari yang sudah ternilai dan bisa berubah setelah sisanya diisi.

+ {% endif %} + {% if dikatrol %} +

✎ = nilai sudah dikoreksi manual di sini (beda dari hasil hitung sesi/tugas komponennya) — hati-hati, koreksi ini akan tertimpa balik kalau sesi/komponen yang mengukur Sub-CPMK itu diisi/disimpan lagi.

+ {% endif %}
Disimpan atas nama {{ user.nama }} @@ -178,7 +187,111 @@ {% endif %} + {% if tabel_poin %} + {% set ada_kelas_p = tabel_poin|selectattr('kelas')|list|length > 0 %} + {% set n_kiri_p = 4 if ada_kelas_p else 3 %} +

NILAI PER MAHASISWA — SKALA POIN — nilai yang sama, ditampilkan sebagai poin yang didapat dari total 100 (lihat rincian bobot di bawah); hanya tampilan, bukan isian

+
+ + + + + + + {% if ada_kelas_p %}{% endif %} + {% for s in peta.subs %}{% endfor %} + + + + + {% for m in tabel_poin %} + + + + + {% if ada_kelas_p %}{% endif %} + {% for s in peta.subs %} + {% set p = m.poin.get(s.kode) %} + + {% endfor %} + + + {% endfor %} + +
No.NIMNamaKelas{{ s.kode }}Total
{{ loop.index }}{{ m.nim }}{{ m.nama }}{{ m.kelas or '—' }}{% if p %}{{ '%.2f'|format(p.dapat) }}/{{ '%.2f'|format(p.maks) }}{% else %}—{% endif %}{{ '%.2f'|format(m.total) }}
+
+ {% endif %} + + {% if bobot_komponen %} +

RINCIAN BOBOT PENILAIAN — bobot komponen (RPS bagian G) dibagi rata ke Sub-CPMK-nya; murni dari RPS, tak bergantung nilai

+
+ + + + {% for k in bobot_komponen %} + + + + + + + + {% endfor %} + + + + + +
KomponenBobotJumlah Sub-CPMKPoin per Sub-CPMKVerifikasi (Jumlah × Poin)
{{ k.nama }}{{ '%g'|format(k.bobot) }}%{{ k.n_subs }}{{ '%.2f'|format(k.poin) }}%{{ k.n_subs }} × {{ '%.2f'|format(k.poin) }}% = {{ '%.2f'|format(k.n_subs * k.poin) }}%
Total{{ '%.2f'|format(bobot_komponen|sum(attribute='bobot')) }}%
+
+

Dijumlah per Sub-CPMK (karena sebagian Sub-CPMK diukur lebih dari satu komponen):

+
+ + + + {% for s in peta.subs %} + {% set kontribusi = bobot_sub_rincian.get(s.kode) %} + {% if kontribusi %} + + + + + + {% endif %} + {% endfor %} + + + + + + +
Sub-CPMKTotal PoinRincian
{{ s.kode }}{{ '%.2f'|format(kontribusi|sum(attribute='poin')) }}{% for c in kontribusi %}{{ c.nama }} {{ '%.2f'|format(c.poin) }}{{ ' + ' if not loop.last }}{% endfor %}
Total{{ '%.2f'|format(bobot_komponen|sum(attribute='bobot')) }}
+
+ {% endif %} + {% if hasil %} +

HASIL KETERCAPAIAN SUB-CPMK (ambang {{ ambang|round(0)|int }})

+
+ + + + {% for s in peta.subs %} + {% set info = hasil.sub.get(s.kode) %} + + + + + + + {% endfor %} + +
Sub-CPMKSkorMaksStatus
{{ s.kode }}{% if info and info.rata is not none %}{{ '%.1f'|format(info.rata) }}{% else %}—{% endif %}100 + {% if not info or info.rata is none %}BELUM TERUKUR + {% elif info.rata >= ambang %}TERCAPAI + {% else %}DI BAWAH AMBANG{% endif %} +
+
+

HASIL KETERCAPAIAN MK (ambang {{ ambang|round(0)|int }})