Tambah tabel nilai skala poin, penanda Nilai Akhir belum lengkap, dan peringatan katrol manual
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.
This commit is contained in:
+87
-1
@@ -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 []},
|
||||
)
|
||||
|
||||
|
||||
@@ -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 '' }}"></td>
|
||||
{% endfor %}
|
||||
<td class="tengah"><b>{{ '%.1f'|format(akhir.get(r.nim)) if akhir.get(r.nim) is not none else '—' }}</b></td>
|
||||
<td class="tengah"><b>{{ '%.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 %}<span title="Belum semua Sub-CPMK ternilai — angka ini sementara">*</span>{% endif %}</b></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if tidak_lengkap %}
|
||||
<p class="keterangan" style="margin-top:6px">* Sebagian Sub-CPMK mahasiswa ini belum ternilai — Nilai Akhir dihitung hanya dari yang sudah ternilai dan bisa berubah setelah sisanya diisi.</p>
|
||||
{% endif %}
|
||||
<div class="aksi-baris">
|
||||
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
||||
<span class="spacer"></span>
|
||||
@@ -144,9 +147,9 @@
|
||||
{% for s in peta.subs %}
|
||||
<td class="isian"><input type="number" name="skor_{{ i }}_{{ s.kode }}"
|
||||
min="0" max="100" step="any"
|
||||
value="{{ '%g'|format(m.skor[s.kode]) if s.kode in m.skor else '' }}"></td>
|
||||
value="{{ '%g'|format(m.skor[s.kode]) if s.kode in m.skor else '' }}">{% if (m.nim, s.kode) in dikatrol %}<span title="Nilai ini dikoreksi manual (beda dari hasil hitung sesi/tugas komponen) — bisa tertimpa balik kalau sesi/komponen yang mengukur Sub-CPMK ini disimpan lagi">✎</span>{% endif %}</td>
|
||||
{% endfor %}
|
||||
<td class="tengah"><b>{{ '%.1f'|format(akhir.get(m.nim)) if akhir.get(m.nim) is not none else '—' }}</b></td>
|
||||
<td class="tengah"><b>{{ '%.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 %}<span title="Belum semua Sub-CPMK ternilai — angka ini sementara">*</span>{% endif %}</b></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if per_mhs and hasil %}
|
||||
@@ -170,6 +173,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if tidak_lengkap %}
|
||||
<p class="keterangan" style="margin-top:6px">* Sebagian Sub-CPMK mahasiswa ini belum ternilai — Nilai Akhir dihitung hanya dari yang sudah ternilai dan bisa berubah setelah sisanya diisi.</p>
|
||||
{% endif %}
|
||||
{% if dikatrol %}
|
||||
<p class="keterangan" style="margin-top:6px">✎ = 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.</p>
|
||||
{% endif %}
|
||||
<div class="aksi-baris">
|
||||
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
||||
<span class="spacer"></span>
|
||||
@@ -178,7 +187,111 @@
|
||||
</form>
|
||||
{% 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 %}
|
||||
<h2 class="bagian">NILAI PER MAHASISWA — SKALA POIN <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai yang sama, ditampilkan sebagai poin yang didapat dari total 100 (lihat rincian bobot di bawah); hanya tampilan, bukan isian</span></h2>
|
||||
<div class="gulir" style="max-height:480px;overflow-y:auto">
|
||||
<table class="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:44px" class="tengah">No.</th>
|
||||
<th style="width:112px">NIM</th>
|
||||
<th style="min-width:150px">Nama</th>
|
||||
{% if ada_kelas_p %}<th style="width:60px" class="tengah">Kelas</th>{% endif %}
|
||||
{% for s in peta.subs %}<th class="tengah" style="font-size:11px;min-width:70px">{{ s.kode }}</th>{% endfor %}
|
||||
<th class="tengah" style="width:90px">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in tabel_poin %}
|
||||
<tr>
|
||||
<td class="tengah">{{ loop.index }}</td>
|
||||
<td>{{ m.nim }}</td>
|
||||
<td>{{ m.nama }}</td>
|
||||
{% if ada_kelas_p %}<td class="tengah">{{ m.kelas or '—' }}</td>{% endif %}
|
||||
{% for s in peta.subs %}
|
||||
{% set p = m.poin.get(s.kode) %}
|
||||
<td class="tengah" style="font-size:12px">{% if p %}{{ '%.2f'|format(p.dapat) }}/{{ '%.2f'|format(p.maks) }}{% else %}—{% endif %}</td>
|
||||
{% endfor %}
|
||||
<td class="tengah"><b>{{ '%.2f'|format(m.total) }}</b></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if bobot_komponen %}
|
||||
<h2 class="bagian">RINCIAN BOBOT PENILAIAN <span style="font-weight:normal;font-style:italic;color:var(--grey)">— bobot komponen (RPS bagian G) dibagi rata ke Sub-CPMK-nya; murni dari RPS, tak bergantung nilai</span></h2>
|
||||
<div class="gulir">
|
||||
<table class="grid" style="max-width:820px">
|
||||
<thead><tr><th>Komponen</th><th class="tengah">Bobot</th><th class="tengah">Jumlah Sub-CPMK</th><th class="tengah">Poin per Sub-CPMK</th><th class="tengah">Verifikasi (Jumlah × Poin)</th></tr></thead>
|
||||
<tbody>
|
||||
{% for k in bobot_komponen %}
|
||||
<tr>
|
||||
<td>{{ k.nama }}</td>
|
||||
<td class="tengah">{{ '%g'|format(k.bobot) }}%</td>
|
||||
<td class="tengah">{{ k.n_subs }}</td>
|
||||
<td class="tengah">{{ '%.2f'|format(k.poin) }}%</td>
|
||||
<td class="tengah">{{ k.n_subs }} × {{ '%.2f'|format(k.poin) }}% = <b>{{ '%.2f'|format(k.n_subs * k.poin) }}%</b></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total">
|
||||
<td colspan="4" style="text-align:right"><b>Total</b></td>
|
||||
<td class="tengah"><b>{{ '%.2f'|format(bobot_komponen|sum(attribute='bobot')) }}%</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="keterangan" style="margin-top:10px">Dijumlah per Sub-CPMK (karena sebagian Sub-CPMK diukur lebih dari satu komponen):</p>
|
||||
<div class="gulir">
|
||||
<table class="grid">
|
||||
<thead><tr><th>Sub-CPMK</th><th class="tengah">Total Poin</th><th>Rincian</th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in peta.subs %}
|
||||
{% set kontribusi = bobot_sub_rincian.get(s.kode) %}
|
||||
{% if kontribusi %}
|
||||
<tr>
|
||||
<td>{{ s.kode }}</td>
|
||||
<td class="tengah"><b>{{ '%.2f'|format(kontribusi|sum(attribute='poin')) }}</b></td>
|
||||
<td>{% for c in kontribusi %}{{ c.nama }} {{ '%.2f'|format(c.poin) }}{{ ' + ' if not loop.last }}{% endfor %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<tr class="total">
|
||||
<td style="text-align:right"><b>Total</b></td>
|
||||
<td class="tengah"><b>{{ '%.2f'|format(bobot_komponen|sum(attribute='bobot')) }}</b></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if hasil %}
|
||||
<h2 class="bagian">HASIL KETERCAPAIAN SUB-CPMK <span style="font-weight:normal;font-style:italic;color:var(--grey)">(ambang {{ ambang|round(0)|int }})</span></h2>
|
||||
<div class="gulir">
|
||||
<table class="grid">
|
||||
<thead><tr><th>Sub-CPMK</th><th class="tengah">Skor</th><th class="tengah">Maks</th><th class="tengah">Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in peta.subs %}
|
||||
{% set info = hasil.sub.get(s.kode) %}
|
||||
<tr>
|
||||
<td>{{ s.kode }}</td>
|
||||
<td class="tengah">{% if info and info.rata is not none %}<b>{{ '%.1f'|format(info.rata) }}</b>{% else %}—{% endif %}</td>
|
||||
<td class="tengah">100</td>
|
||||
<td class="tengah">
|
||||
{% if not info or info.rata is none %}<span class="stamp stamp-draft">BELUM TERUKUR</span>
|
||||
{% elif info.rata >= ambang %}<span class="stamp stamp-ok">TERCAPAI</span>
|
||||
{% else %}<span class="stamp stamp-no">DI BAWAH AMBANG</span>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 class="bagian">HASIL KETERCAPAIAN MK <span style="font-weight:normal;font-style:italic;color:var(--grey)">(ambang {{ ambang|round(0)|int }})</span></h2>
|
||||
<div class="gulir">
|
||||
<table class="grid">
|
||||
|
||||
Reference in New Issue
Block a user