Compare commits
16
Commits
d6469a01dc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e31f8e57dd | ||
|
|
374bdb5565 | ||
|
|
de65ca99e6 | ||
|
|
adf2918e85 | ||
|
|
3c9482ee23 | ||
|
|
e5d45cdcdd | ||
|
|
a3efa9fef3 | ||
|
|
6375bc88f4 | ||
|
|
efc14dc7a7 | ||
|
|
9c0fe8baa0 | ||
|
|
38d309e7b1 | ||
|
|
71858d974c | ||
|
|
b9d5c22f33 | ||
|
|
5d7ab16733 | ||
|
|
af369883f3 | ||
|
|
b85c75bb31 |
@@ -30,6 +30,12 @@ def kurikulum() -> dict:
|
|||||||
global _kur
|
global _kur
|
||||||
if _kur is None:
|
if _kur is None:
|
||||||
_kur = json.loads(SEED.read_text(encoding="utf-8"))
|
_kur = json.loads(SEED.read_text(encoding="utf-8"))
|
||||||
|
# seed lama menyimpan 'cpl' CPMK sebagai string tunggal ("CPL-03")
|
||||||
|
# alih-alih list — semua pemakai (rollup, template |join) mengasumsikan
|
||||||
|
# list; tanpa ini string ikut ter-iterasi per huruf ("C, P, L, -, 0, 3").
|
||||||
|
for v in _kur.get("cpmk", {}).values():
|
||||||
|
if isinstance(v.get("cpl"), str):
|
||||||
|
v["cpl"] = [v["cpl"]]
|
||||||
return _kur
|
return _kur
|
||||||
|
|
||||||
|
|
||||||
@@ -156,7 +162,7 @@ def _migrasi_kelas_penugasan(conn: sqlite3.Connection) -> None:
|
|||||||
|
|
||||||
def init_db() -> None:
|
def init_db() -> None:
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
for tabel in ("rps", "portofolio", "nilai"):
|
for tabel in ("rps", "portofolio", "nilai", "rtm"):
|
||||||
conn.execute(
|
conn.execute(
|
||||||
f"""CREATE TABLE IF NOT EXISTS {tabel} (
|
f"""CREATE TABLE IF NOT EXISTS {tabel} (
|
||||||
mk_kode TEXT NOT NULL,
|
mk_kode TEXT NOT NULL,
|
||||||
@@ -179,6 +185,16 @@ def init_db() -> None:
|
|||||||
updated_by TEXT NOT NULL
|
updated_by TEXT NOT NULL
|
||||||
)"""
|
)"""
|
||||||
)
|
)
|
||||||
|
conn.execute(
|
||||||
|
"""CREATE TABLE IF NOT EXISTS nilai_arsip_hapus (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mk_kode TEXT NOT NULL,
|
||||||
|
tahun TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
dihapus_at TEXT NOT NULL,
|
||||||
|
dihapus_oleh TEXT NOT NULL
|
||||||
|
)"""
|
||||||
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""CREATE TABLE IF NOT EXISTS pengaturan (
|
"""CREATE TABLE IF NOT EXISTS pengaturan (
|
||||||
kunci TEXT PRIMARY KEY,
|
kunci TEXT PRIMARY KEY,
|
||||||
@@ -259,7 +275,7 @@ def simpan_pengaturan(d: dict) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
_TABEL_DOK = ("rps", "portofolio", "nilai")
|
_TABEL_DOK = ("rps", "portofolio", "nilai", "rtm")
|
||||||
|
|
||||||
|
|
||||||
def simpan_dok(tabel: str, mk_kode: str, data: dict, status: str, updated_by: str,
|
def simpan_dok(tabel: str, mk_kode: str, data: dict, status: str, updated_by: str,
|
||||||
@@ -286,6 +302,19 @@ def simpan_dok(tabel: str, mk_kode: str, data: dict, status: str, updated_by: st
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def arsipkan_nilai_hapus(mk_kode: str, tahun: str, data: dict, dihapus_oleh: str) -> None:
|
||||||
|
"""Simpan salinan nilai sebelum dihapus (jaring pengaman) — tak ada UI
|
||||||
|
pemulihan otomatis, tapi baris ini tetap ikut ke-backup lewat /cadangan.db
|
||||||
|
dan bisa diambil manual bila suatu saat perlu dipulihkan."""
|
||||||
|
now = datetime.now(WIB).strftime("%Y-%m-%d %H:%M")
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO nilai_arsip_hapus (mk_kode, tahun, data, dihapus_at, dihapus_oleh)
|
||||||
|
VALUES (?,?,?,?,?)""",
|
||||||
|
(mk_kode, tahun, json.dumps(data, ensure_ascii=False), now, dihapus_oleh),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def hapus_dok(tabel: str, mk_kode: str, tahun: str | None = None) -> None:
|
def hapus_dok(tabel: str, mk_kode: str, tahun: str | None = None) -> None:
|
||||||
assert tabel in _TABEL_DOK
|
assert tabel in _TABEL_DOK
|
||||||
tahun = tahun or tahun_aktif()
|
tahun = tahun or tahun_aktif()
|
||||||
@@ -356,11 +385,11 @@ def dump_semua() -> dict:
|
|||||||
"""Seluruh isi database sebagai dict (untuk cadangan JSON)."""
|
"""Seluruh isi database sebagai dict (untuk cadangan JSON)."""
|
||||||
out = {}
|
out = {}
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
for tabel in (*_TABEL_DOK, "rps_arsip", "pengaturan",
|
for tabel in (*_TABEL_DOK, "rps_arsip", "nilai_arsip_hapus", "pengaturan",
|
||||||
"pengguna", "penugasan", "mahasiswa"):
|
"pengguna", "penugasan", "mahasiswa"):
|
||||||
rows = conn.execute(f"SELECT * FROM {tabel}").fetchall()
|
rows = conn.execute(f"SELECT * FROM {tabel}").fetchall()
|
||||||
out[tabel] = [dict(r) for r in rows]
|
out[tabel] = [dict(r) for r in rows]
|
||||||
for tabel in _TABEL_DOK + ("rps_arsip",):
|
for tabel in _TABEL_DOK + ("rps_arsip", "nilai_arsip_hapus"):
|
||||||
for r in out.get(tabel, []):
|
for r in out.get(tabel, []):
|
||||||
r["data"] = json.loads(r["data"])
|
r["data"] = json.loads(r["data"])
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -39,7 +39,17 @@ PRODI = os.environ.get("PRODI", "PROGRAM STUDI INFORMATIKA")
|
|||||||
|
|
||||||
|
|
||||||
def _set_text(cell, text, align=None, bold=None):
|
def _set_text(cell, text, align=None, bold=None):
|
||||||
"""Ganti isi sel; gaya paragraf/tabel template dipertahankan."""
|
"""Ganti isi sel; gaya paragraf/tabel template dipertahankan.
|
||||||
|
|
||||||
|
Beberapa sel template resmi (mis. contoh isian "peta CPL-CPMK") memuat
|
||||||
|
tabel kecil bersarang di antara paragraf petunjuknya. w:tc yang berakhir
|
||||||
|
dengan w:tbl tanpa w:p setelahnya melanggar skema OOXML — Word
|
||||||
|
menganggap dokumennya rusak dan mem-"perbaiki" (merapikan ulang) sendiri
|
||||||
|
saat dibuka, yang justru merusak tata letak. Buang tabel bersarang itu
|
||||||
|
sekalian saat sel ditimpa teks baru.
|
||||||
|
"""
|
||||||
|
for tbl in cell._tc.findall(qn("w:tbl")):
|
||||||
|
tbl.getparent().remove(tbl)
|
||||||
for p in cell.paragraphs[1:]:
|
for p in cell.paragraphs[1:]:
|
||||||
p._p.getparent().remove(p._p)
|
p._p.getparent().remove(p._p)
|
||||||
p = cell.paragraphs[0]
|
p = cell.paragraphs[0]
|
||||||
@@ -120,6 +130,13 @@ def build_rps(d: dict) -> bytes:
|
|||||||
|
|
||||||
trs = [r._tr for r in t.rows] # indeks baris template asli
|
trs = [r._tr for r in t.rows] # indeks baris template asli
|
||||||
|
|
||||||
|
def cell(idx, col):
|
||||||
|
"""Ambil sel dari baris ASLI template lewat trs[idx] (referensi XML
|
||||||
|
beku), bukan t.cell(idx, col) yang ikut bergeser tiap kali baris
|
||||||
|
CPL/CPMK/Sub-CPMK digandakan/dibuang di atasnya — pakai t.cell()
|
||||||
|
langsung untuk baris di bawah blok itu akan salah sasaran."""
|
||||||
|
return _Row(trs[idx], t).cells[col]
|
||||||
|
|
||||||
def isi_daftar(proto_idx, hapus_idx, items, kolom_kode=2, kolom_isi=5):
|
def isi_daftar(proto_idx, hapus_idx, items, kolom_kode=2, kolom_isi=5):
|
||||||
"""Isi blok daftar (CPL/CPMK/Sub-CPMK): prototipe digandakan per item."""
|
"""Isi blok daftar (CPL/CPMK/Sub-CPMK): prototipe digandakan per item."""
|
||||||
proto = trs[proto_idx]
|
proto = trs[proto_idx]
|
||||||
@@ -143,17 +160,17 @@ def build_rps(d: dict) -> bytes:
|
|||||||
f"{c['kode']} → {', '.join(c.get('cpl', [])) or '-'}"
|
f"{c['kode']} → {', '.join(c.get('cpl', [])) or '-'}"
|
||||||
for c in d.get("cpmk", [])
|
for c in d.get("cpmk", [])
|
||||||
)
|
)
|
||||||
_set_text(t.cell(18, 2), peta_txt)
|
_set_text(cell(18, 2), peta_txt)
|
||||||
_set_text(t.cell(19, 2), d.get("deskripsi", ""))
|
_set_text(cell(19, 2), d.get("deskripsi", ""))
|
||||||
_set_text(t.cell(20, 2), d.get("bahan_kajian", ""))
|
_set_text(cell(20, 2), d.get("bahan_kajian", ""))
|
||||||
|
|
||||||
utama = d.get("referensi", [])
|
utama = d.get("referensi", [])
|
||||||
pendukung = d.get("referensi_pendukung", [])
|
pendukung = d.get("referensi_pendukung", [])
|
||||||
_set_text(t.cell(22, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(utama, 1)))
|
_set_text(cell(22, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(utama, 1)))
|
||||||
_set_text(t.cell(24, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(pendukung, 1)))
|
_set_text(cell(24, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(pendukung, 1)))
|
||||||
_set_text(t.cell(25, 2), d.get("media", ""))
|
_set_text(cell(25, 2), d.get("media", ""))
|
||||||
_set_text(t.cell(26, 2), ident.get("pengampu", ""))
|
_set_text(cell(26, 2), ident.get("pengampu", ""))
|
||||||
_set_text(t.cell(27, 2), ident.get("prasyarat") or "—")
|
_set_text(cell(27, 2), ident.get("prasyarat") or "—")
|
||||||
|
|
||||||
# ── tabel mingguan ──
|
# ── tabel mingguan ──
|
||||||
proto_norm, proto_uts, proto_uas = trs[31], trs[33], trs[37]
|
proto_norm, proto_uts, proto_uas = trs[31], trs[33], trs[37]
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Generator Rancangan Tugas Mahasiswa (RTM) (.docx).
|
||||||
|
Mengikuti format resmi `FORM RPS & RTM UNTAN (draft).docx`: tabel identitas
|
||||||
|
(Mata Kuliah/Kode MK/Tugas ke/SKS/Dosen Pengampu/Semester) diikuti tabel
|
||||||
|
RANCANGAN TUGAS MAHASISWA per tugas (Bentuk, Judul, Sub-CPMK, Deskripsi,
|
||||||
|
Metode Pengerjaan, Bentuk & Format Luaran, Indikator/Kriteria/Bobot,
|
||||||
|
Jadwal Pelaksanaan, Lain-lain, Daftar Rujukan). Dokumen ini berdiri sendiri,
|
||||||
|
tak memuat data nilai/skor mahasiswa sama sekali.
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
|
||||||
|
from docx import Document
|
||||||
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||||
|
from docx.oxml import OxmlElement
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
from docx.shared import Cm, Pt, RGBColor
|
||||||
|
|
||||||
|
DARK_BLUE = "1F4E79"
|
||||||
|
LIGHT_BLUE = "D6E4F0"
|
||||||
|
WHITE = "FFFFFF"
|
||||||
|
|
||||||
|
FAKULTAS = os.environ.get("FAKULTAS", "FAKULTAS TEKNIK")
|
||||||
|
PRODI = os.environ.get("PRODI", "PROGRAM STUDI INFORMATIKA")
|
||||||
|
|
||||||
|
|
||||||
|
def _shd(cell, hex_color):
|
||||||
|
tc_pr = cell._tc.get_or_add_tcPr()
|
||||||
|
el = OxmlElement("w:shd")
|
||||||
|
el.set(qn("w:val"), "clear")
|
||||||
|
el.set(qn("w:color"), "auto")
|
||||||
|
el.set(qn("w:fill"), hex_color)
|
||||||
|
tc_pr.append(el)
|
||||||
|
|
||||||
|
|
||||||
|
def _ct(cell, text, bold=False, italic=False, size=10, color=None,
|
||||||
|
align=WD_ALIGN_PARAGRAPH.LEFT):
|
||||||
|
p = cell.paragraphs[0]
|
||||||
|
p.alignment = align
|
||||||
|
p.paragraph_format.space_before = Pt(2)
|
||||||
|
p.paragraph_format.space_after = Pt(2)
|
||||||
|
run = p.add_run()
|
||||||
|
for i, line in enumerate(str(text or "").split("\n")):
|
||||||
|
if i:
|
||||||
|
run.add_break()
|
||||||
|
run.add_text(line)
|
||||||
|
run.bold = bold
|
||||||
|
run.italic = italic
|
||||||
|
run.font.name = "Times New Roman"
|
||||||
|
run.font.size = Pt(size)
|
||||||
|
if color:
|
||||||
|
run.font.color.rgb = RGBColor.from_string(color)
|
||||||
|
|
||||||
|
|
||||||
|
def _hdr(cell, text, size=10):
|
||||||
|
_shd(cell, DARK_BLUE)
|
||||||
|
_ct(cell, text, bold=True, color=WHITE, size=size, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||||
|
|
||||||
|
|
||||||
|
def _borders(table):
|
||||||
|
tbl_pr = table._tbl.tblPr
|
||||||
|
el = OxmlElement("w:tblBorders")
|
||||||
|
for sisi in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
||||||
|
b = OxmlElement(f"w:{sisi}")
|
||||||
|
b.set(qn("w:val"), "single")
|
||||||
|
b.set(qn("w:sz"), "4")
|
||||||
|
b.set(qn("w:color"), "000000")
|
||||||
|
el.append(b)
|
||||||
|
tbl_pr.append(el)
|
||||||
|
|
||||||
|
|
||||||
|
def build_rtm(d: dict) -> bytes:
|
||||||
|
doc = Document()
|
||||||
|
for sec in doc.sections:
|
||||||
|
sec.top_margin = sec.bottom_margin = Cm(2.5)
|
||||||
|
sec.left_margin = sec.right_margin = Cm(2.5)
|
||||||
|
|
||||||
|
def para(text, size=12, bold=False, align=WD_ALIGN_PARAGRAPH.LEFT, before=6, after=3):
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
p.alignment = align
|
||||||
|
p.paragraph_format.space_before = Pt(before)
|
||||||
|
p.paragraph_format.space_after = Pt(after)
|
||||||
|
r = p.add_run(text)
|
||||||
|
r.bold = bold
|
||||||
|
r.font.name = "Times New Roman"
|
||||||
|
r.font.size = Pt(size)
|
||||||
|
return p
|
||||||
|
|
||||||
|
para("RANCANGAN TUGAS MAHASISWA (RTM)", size=16, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||||
|
para(PRODI, size=12, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER, before=0)
|
||||||
|
para(f"{FAKULTAS} — UNIVERSITAS TANJUNGPURA", size=11, align=WD_ALIGN_PARAGRAPH.CENTER, before=0)
|
||||||
|
para(f"Tahun Ajaran {d.get('tahun', '')}", size=11, align=WD_ALIGN_PARAGRAPH.CENTER,
|
||||||
|
before=0, after=10)
|
||||||
|
|
||||||
|
daftar_tugas = d.get("tugas", [])
|
||||||
|
if not daftar_tugas:
|
||||||
|
para("Belum ada Tugas yang diisi untuk RTM ini.", after=6)
|
||||||
|
|
||||||
|
for idx, t in enumerate(daftar_tugas, 1):
|
||||||
|
if idx > 1:
|
||||||
|
doc.add_page_break()
|
||||||
|
|
||||||
|
ti = doc.add_table(rows=3, cols=4)
|
||||||
|
_borders(ti)
|
||||||
|
baris = [
|
||||||
|
("Mata Kuliah", d.get("nama", ""), "Kode MK", d.get("kode", "")),
|
||||||
|
("Tugas ke", str(idx), "SKS", str(d.get("sks", "") or "—")),
|
||||||
|
("Dosen Pengampu", d.get("pengampu", "") or "—", "Semester",
|
||||||
|
str(d.get("semester", "") or "—")),
|
||||||
|
]
|
||||||
|
for ri, (l1, v1, l2, v2) in enumerate(baris):
|
||||||
|
_shd(ti.cell(ri, 0), LIGHT_BLUE)
|
||||||
|
_ct(ti.cell(ri, 0), l1, bold=True)
|
||||||
|
_ct(ti.cell(ri, 1), v1)
|
||||||
|
_shd(ti.cell(ri, 2), LIGHT_BLUE)
|
||||||
|
_ct(ti.cell(ri, 2), l2, bold=True)
|
||||||
|
_ct(ti.cell(ri, 3), v2)
|
||||||
|
doc.add_paragraph()
|
||||||
|
|
||||||
|
subs_teks = ", ".join(t.get("subs") or []) or "—"
|
||||||
|
jadwal = "; ".join(x for x in (
|
||||||
|
f"Alokasi waktu: {t['alokasi_waktu']}" if t.get("alokasi_waktu") else "",
|
||||||
|
f"Tanggal: {t['tanggal']}" if t.get("tanggal") else "",
|
||||||
|
) if x) or "—"
|
||||||
|
rows = [
|
||||||
|
("BENTUK TUGAS", t.get("bentuk_tugas", "") or "—"),
|
||||||
|
("JUDUL TUGAS", t.get("judul", "") or "—"),
|
||||||
|
("SUB-CPMK YANG DICAPAI", subs_teks),
|
||||||
|
("DESKRIPSI TUGAS", t.get("deskripsi", "") or "—"),
|
||||||
|
("METODE PENGERJAAN TUGAS", t.get("metode_pengerjaan", "") or "—"),
|
||||||
|
("BENTUK DAN FORMAT LUARAN", t.get("luaran", "") or "—"),
|
||||||
|
("INDIKATOR, KRITERIA DAN BOBOT PENILAIAN", t.get("indikator_bobot", "") or "—"),
|
||||||
|
("JADWAL PELAKSANAAN", jadwal),
|
||||||
|
("LAIN-LAIN", t.get("lain_lain", "") or "—"),
|
||||||
|
("DAFTAR RUJUKAN", t.get("rujukan", "") or "—"),
|
||||||
|
]
|
||||||
|
tt = doc.add_table(rows=1 + len(rows), cols=2)
|
||||||
|
_borders(tt)
|
||||||
|
_hdr(tt.cell(0, 0), "RANCANGAN TUGAS MAHASISWA")
|
||||||
|
tt.cell(0, 0).merge(tt.cell(0, 1))
|
||||||
|
for ri, (label, isi) in enumerate(rows, 1):
|
||||||
|
_shd(tt.cell(ri, 0), LIGHT_BLUE)
|
||||||
|
_ct(tt.cell(ri, 0), label, bold=True, size=9)
|
||||||
|
_ct(tt.cell(ri, 1), isi, size=10)
|
||||||
|
tt.rows[ri].cells[0].width = Cm(5.5)
|
||||||
|
tt.rows[ri].cells[1].width = Cm(11)
|
||||||
|
|
||||||
|
buf = io.BytesIO()
|
||||||
|
doc.save(buf)
|
||||||
|
return buf.getvalue()
|
||||||
Binary file not shown.
+140
@@ -205,6 +205,146 @@ def rollup_mk(peta: dict, nilai_data: dict, ambang: float) -> dict:
|
|||||||
"mode": nilai_data.get("mode", "agregat")}
|
"mode": nilai_data.get("mode", "agregat")}
|
||||||
|
|
||||||
|
|
||||||
|
def sub_terurut(peta: dict, rollup_sub: dict) -> list[dict]:
|
||||||
|
"""Sub-CPMK ternilai, terurut naik dari yang terlemah — bahan analisis
|
||||||
|
proses asesmen di Portofolio (mana yang paling perlu perhatian)."""
|
||||||
|
out = []
|
||||||
|
for s in peta["subs"]:
|
||||||
|
info = rollup_sub.get(s["kode"])
|
||||||
|
if info and info["rata"] is not None:
|
||||||
|
out.append({"kode": s["kode"], "uraian": s["uraian"],
|
||||||
|
"rata": info["rata"], "lulus_pct": info["lulus_pct"]})
|
||||||
|
return sorted(out, key=lambda s: s["rata"])
|
||||||
|
|
||||||
|
|
||||||
|
def kesenjangan_rencana_realisasi(bobot_sub: dict[str, float],
|
||||||
|
rollup_sub: dict) -> list[str]:
|
||||||
|
"""Sub-CPMK yang diberi bobot penilaian di RPS tapi belum ada nilai
|
||||||
|
masuk sama sekali — kesenjangan antara rencana dan realisasi asesmen."""
|
||||||
|
return sorted(
|
||||||
|
s for s in bobot_sub
|
||||||
|
if bobot_sub[s] and (s not in rollup_sub or rollup_sub[s]["rata"] is None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mahasiswa_berisiko(nilai_data: dict, akhir: list[dict], ambang: float,
|
||||||
|
min_sub_dibawah: int = 2) -> list[dict]:
|
||||||
|
"""Mahasiswa dengan >= min_sub_dibawah Sub-CPMK di bawah ambang, atau
|
||||||
|
nilai akhir tertimbangnya sendiri di bawah ambang (early-warning list)."""
|
||||||
|
if nilai_data.get("mode") != "per_mahasiswa":
|
||||||
|
return []
|
||||||
|
akhir_by_nim = {a["nim"]: a["akhir"] for a in akhir}
|
||||||
|
out = []
|
||||||
|
for m in nilai_data.get("mahasiswa", []):
|
||||||
|
skor = {s: v for s, v in (m.get("skor") or {}).items() if v is not None}
|
||||||
|
if not skor:
|
||||||
|
continue
|
||||||
|
n_dibawah = sum(1 for v in skor.values() if v < ambang)
|
||||||
|
nim = m.get("nim", "")
|
||||||
|
nilai_akhir = akhir_by_nim.get(nim)
|
||||||
|
if n_dibawah >= min_sub_dibawah or (nilai_akhir is not None and nilai_akhir < ambang):
|
||||||
|
out.append({"nim": nim, "nama": m.get("nama", ""),
|
||||||
|
"n_dibawah": n_dibawah, "total_sub": len(skor),
|
||||||
|
"akhir": nilai_akhir})
|
||||||
|
return sorted(out, key=lambda m: (-m["n_dibawah"], m["akhir"] if m["akhir"] is not None else 999))
|
||||||
|
|
||||||
|
|
||||||
|
def tren_mk(kode: str, daftar_tahun: list[str], ambang: float) -> list[dict]:
|
||||||
|
"""Rata-rata nilai akhir & % kelulusan MK ini per tahun ajaran yang
|
||||||
|
berdata — untuk melihat apakah capaian membaik atau tidak antar tahun."""
|
||||||
|
out = []
|
||||||
|
for t in daftar_tahun:
|
||||||
|
nilai_t = db.ambil_dok("nilai", kode, t)
|
||||||
|
if not nilai_t:
|
||||||
|
continue
|
||||||
|
nilai_data_t = nilai_t["data"]
|
||||||
|
rps_t = db.ambil_rps(kode, t)
|
||||||
|
bobot_t = bobot_sub_dari_rps(rps_t["data"] if rps_t else None)
|
||||||
|
if nilai_data_t.get("mode") == "per_mahasiswa":
|
||||||
|
akhir_t = nilai_akhir_mahasiswa(nilai_data_t, bobot_t)
|
||||||
|
if akhir_t:
|
||||||
|
xs = [a["akhir"] for a in akhir_t]
|
||||||
|
out.append({"tahun": t, "mean": _rata(xs),
|
||||||
|
"lulus_pct": 100.0 * sum(1 for x in xs if x >= ambang) / len(xs),
|
||||||
|
"n": len(xs)})
|
||||||
|
continue
|
||||||
|
sub_t = skor_sub(nilai_data_t, ambang)
|
||||||
|
nilai_ada = [v["rata"] for v in sub_t.values() if v["rata"] is not None]
|
||||||
|
if nilai_ada:
|
||||||
|
out.append({"tahun": t, "mean": _rata(nilai_ada), "lulus_pct": None, "n": None})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def kelompok_capaian(akhir: list[dict], statistik: dict | None) -> dict | None:
|
||||||
|
"""Kelompokkan mahasiswa jadi Atas/Tengah/Bawah berdasarkan mean ± 1 SD
|
||||||
|
(acuan norma, lazim dipakai dalam evaluasi pendidikan) — pelengkap
|
||||||
|
'mahasiswa berisiko' yang acuannya ambang kelulusan tetap, bukan sebaran
|
||||||
|
kelas ini."""
|
||||||
|
if not statistik or not akhir:
|
||||||
|
return None
|
||||||
|
batas_bawah = statistik["mean"] - statistik["stdev"]
|
||||||
|
batas_atas = statistik["mean"] + statistik["stdev"]
|
||||||
|
atas, tengah, bawah = [], [], []
|
||||||
|
for a in akhir:
|
||||||
|
if a["akhir"] > batas_atas:
|
||||||
|
atas.append(a)
|
||||||
|
elif a["akhir"] < batas_bawah:
|
||||||
|
bawah.append(a)
|
||||||
|
else:
|
||||||
|
tengah.append(a)
|
||||||
|
return {
|
||||||
|
"batas_bawah": batas_bawah, "batas_atas": batas_atas,
|
||||||
|
"atas": sorted(atas, key=lambda a: -a["akhir"]),
|
||||||
|
"tengah": sorted(tengah, key=lambda a: -a["akhir"]),
|
||||||
|
"bawah": sorted(bawah, key=lambda a: -a["akhir"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def saran_simpulan(akhir: list[dict], statistik: dict | None, sub_urut: list[dict],
|
||||||
|
kesenjangan: list[str], berisiko: list[dict],
|
||||||
|
tren: list[dict], ambang: float) -> str:
|
||||||
|
"""Rangkai draf narasi Simpulan Hasil Evaluasi dari hasil Analisis Proses
|
||||||
|
Asesmen — dipakai sebagai placeholder awal, dosen tetap yang menulis versi
|
||||||
|
finalnya (termasuk simpulan ketercapaian CPL/CPMK dan tindak lanjut)."""
|
||||||
|
bagian = []
|
||||||
|
if statistik:
|
||||||
|
lulus_pct = 100.0 * sum(1 for a in akhir if a["akhir"] >= ambang) / len(akhir)
|
||||||
|
bagian.append(
|
||||||
|
f"Rata-rata nilai akhir {statistik['mean']:.1f} (median {statistik['median']:.1f}) "
|
||||||
|
f"dari {statistik['n']} mahasiswa; {lulus_pct:.0f}% mencapai ambang {ambang:.0f}."
|
||||||
|
)
|
||||||
|
if sub_urut:
|
||||||
|
terlemah, terkuat = sub_urut[0], sub_urut[-1]
|
||||||
|
if terlemah["kode"] != terkuat["kode"]:
|
||||||
|
bagian.append(
|
||||||
|
f"Capaian terendah pada {terlemah['kode']} (skor {terlemah['rata']:.1f}), "
|
||||||
|
f"tertinggi pada {terkuat['kode']} (skor {terkuat['rata']:.1f})."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
bagian.append(f"Satu-satunya Sub-CPMK ternilai: {terlemah['kode']} (skor {terlemah['rata']:.1f}).")
|
||||||
|
if kesenjangan:
|
||||||
|
bagian.append(
|
||||||
|
f"{len(kesenjangan)} Sub-CPMK direncanakan di RPS namun belum ternilai: "
|
||||||
|
f"{', '.join(kesenjangan)}."
|
||||||
|
)
|
||||||
|
if berisiko:
|
||||||
|
bagian.append(f"{len(berisiko)} mahasiswa teridentifikasi berisiko (nilai rendah di beberapa Sub-CPMK).")
|
||||||
|
if len(tren) > 1:
|
||||||
|
sekarang, sebelum = tren[-1], tren[-2]
|
||||||
|
if sekarang["mean"] is not None and sebelum["mean"] is not None:
|
||||||
|
selisih = sekarang["mean"] - sebelum["mean"]
|
||||||
|
arah = "naik" if selisih > 0 else ("turun" if selisih < 0 else "stabil")
|
||||||
|
bagian.append(
|
||||||
|
f"Dibanding {sebelum['tahun']} ({sebelum['mean']:.1f}), rata-rata {arah} "
|
||||||
|
f"{abs(selisih):.1f} poin di {sekarang['tahun']}."
|
||||||
|
)
|
||||||
|
bagian.append(
|
||||||
|
"Lengkapi dengan simpulan ketercapaian CPL/CPMK terkait dan rencana tindak lanjut "
|
||||||
|
"untuk siklus berikutnya."
|
||||||
|
)
|
||||||
|
return " ".join(bagian)
|
||||||
|
|
||||||
|
|
||||||
def rollup_prodi(tahun: str | None = None) -> dict:
|
def rollup_prodi(tahun: str | None = None) -> dict:
|
||||||
"""Ketercapaian tingkat prodi + kelengkapan bukti per CPL/CPMK."""
|
"""Ketercapaian tingkat prodi + kelengkapan bukti per CPL/CPMK."""
|
||||||
tahun = tahun or db.tahun_aktif()
|
tahun = tahun or db.tahun_aktif()
|
||||||
|
|||||||
+358
-24
@@ -13,6 +13,7 @@ from fastapi.templating import Jinja2Templates
|
|||||||
from . import auth, db, hitung, nilai_xlsx
|
from . import auth, db, hitung, nilai_xlsx
|
||||||
from .generator.portofolio_docx import build_portofolio
|
from .generator.portofolio_docx import build_portofolio
|
||||||
from .generator.rps_untan import build_rps
|
from .generator.rps_untan import build_rps
|
||||||
|
from .generator.rtm_docx import build_rtm
|
||||||
|
|
||||||
BASE = pathlib.Path(__file__).resolve().parent
|
BASE = pathlib.Path(__file__).resolve().parent
|
||||||
|
|
||||||
@@ -326,6 +327,19 @@ def _normalisasi_komponen(tersimpan: list[dict]) -> list[dict]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _komponen_tanpa_sub(rps_data: dict) -> list[str]:
|
||||||
|
"""Nama komponen RPS bagian G yang bobotnya sudah diisi tapi belum ada
|
||||||
|
Sub-CPMK dicentang. Komponen begini masih ikut dihitung ke total 100% di
|
||||||
|
bagian G (jadi mudah lolos tanpa disadari), tapi disaring habis oleh
|
||||||
|
_komponen_daftar() — tidak akan pernah muncul di halaman Nilai."""
|
||||||
|
out = []
|
||||||
|
for k in _normalisasi_komponen(rps_data.get("komponen", [])):
|
||||||
|
bobot_terisi = str(k.get("bobot") or "").strip() not in ("", "0", "0.0")
|
||||||
|
if bobot_terisi and not k.get("subs"):
|
||||||
|
out.append(k["nama"])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def minggu_default() -> list[dict]:
|
def minggu_default() -> list[dict]:
|
||||||
rows = []
|
rows = []
|
||||||
for w in range(1, 17):
|
for w in range(1, 17):
|
||||||
@@ -358,6 +372,43 @@ def form_kosong() -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── RTM (Rancangan Tugas Mahasiswa) — dokumen sendiri, terpisah dari Nilai ──
|
||||||
|
|
||||||
|
RTM_TUGAS_KOSONG = {
|
||||||
|
"judul": "", "subs": [], "bentuk_tugas": "", "deskripsi": "",
|
||||||
|
"metode_pengerjaan": "", "luaran": "", "indikator_bobot": "",
|
||||||
|
"alokasi_waktu": "", "tanggal": "", "lain_lain": "", "rujukan": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _ambil_rtm(kode: str) -> tuple[list[dict], dict]:
|
||||||
|
"""Daftar entri Tugas RTM yang sudah tersimpan (bisa kosong — halaman RTM
|
||||||
|
menambah blok baru secara dinamis lewat JS, bukan slot tetap) — murni
|
||||||
|
dokumentasi, tak menyentuh data Nilai."""
|
||||||
|
tersimpan = db.ambil_dok("rtm", kode)
|
||||||
|
tugas = [dict(RTM_TUGAS_KOSONG, **t) for t in ((tersimpan or {}).get("data", {}).get("tugas") or [])]
|
||||||
|
meta = tersimpan or {"status": "", "updated_at": "", "updated_by": ""}
|
||||||
|
return tugas, meta
|
||||||
|
|
||||||
|
|
||||||
|
def _bahan_rtm(kode: str) -> dict | None:
|
||||||
|
peta = db.peta_mk(kode)
|
||||||
|
if not peta:
|
||||||
|
return None
|
||||||
|
rps = db.ambil_rps(kode)
|
||||||
|
ident = ((rps or {}).get("data") or {}).get("identitas") or {}
|
||||||
|
tersimpan = db.ambil_dok("rtm", kode)
|
||||||
|
tugas = [t for t in ((tersimpan or {}).get("data", {}).get("tugas") or [])
|
||||||
|
if t.get("judul") or t.get("bentuk_tugas") or t.get("deskripsi")]
|
||||||
|
return {
|
||||||
|
"tahun": db.tahun_aktif(),
|
||||||
|
"nama": peta["nama"], "kode": peta["kode"],
|
||||||
|
"sks": ident.get("sks", ""), "semester": ident.get("semester", ""),
|
||||||
|
"pengampu": ident.get("pengampu", ""),
|
||||||
|
"tugas": tugas,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _terapkan_override_sub(peta: dict, rps_data: dict | None) -> dict:
|
def _terapkan_override_sub(peta: dict, rps_data: dict | None) -> dict:
|
||||||
"""Salinan `peta` dengan uraian Sub-CPMK ditimpa override tersimpan di RPS
|
"""Salinan `peta` dengan uraian Sub-CPMK ditimpa override tersimpan di RPS
|
||||||
MK ini (bila ada) — dipakai konsisten di form RPS, unduhan RPS, dan
|
MK ini (bila ada) — dipakai konsisten di form RPS, unduhan RPS, dan
|
||||||
@@ -418,6 +469,7 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
|
|||||||
f.setdefault("referensi_teks", "\n".join(f.get("referensi", [])))
|
f.setdefault("referensi_teks", "\n".join(f.get("referensi", [])))
|
||||||
f["komponen"] = _normalisasi_komponen(f.get("komponen", []))
|
f["komponen"] = _normalisasi_komponen(f.get("komponen", []))
|
||||||
ctx_status = tersimpan
|
ctx_status = tersimpan
|
||||||
|
komponen_tanpa_sub = _komponen_tanpa_sub(f)
|
||||||
else:
|
else:
|
||||||
f = form_kosong()
|
f = form_kosong()
|
||||||
# prefill SKS & semester dari data kurikulum (sheet '14. Susunan MK')
|
# prefill SKS & semester dari data kurikulum (sheet '14. Susunan MK')
|
||||||
@@ -427,6 +479,7 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
|
|||||||
if info.get("semester"):
|
if info.get("semester"):
|
||||||
f["identitas"]["semester"] = str(info["semester"])
|
f["identitas"]["semester"] = str(info["semester"])
|
||||||
ctx_status = {"status": "", "updated_at": "", "updated_by": ""}
|
ctx_status = {"status": "", "updated_at": "", "updated_by": ""}
|
||||||
|
komponen_tanpa_sub = [] # form belum pernah disimpan — bobot masih bawaan default
|
||||||
peta = _terapkan_override_sub(peta, f)
|
peta = _terapkan_override_sub(peta, f)
|
||||||
return render(
|
return render(
|
||||||
request, "form_rps.html",
|
request, "form_rps.html",
|
||||||
@@ -435,6 +488,7 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
|
|||||||
"tahun": db.tahun_aktif(), "pesan": pesan,
|
"tahun": db.tahun_aktif(), "pesan": pesan,
|
||||||
"boleh_edit": boleh_rps(request.state.user, kode),
|
"boleh_edit": boleh_rps(request.state.user, kode),
|
||||||
"tahun_lain": db.tahun_rps_lain(kode) if not tersimpan else [],
|
"tahun_lain": db.tahun_rps_lain(kode) if not tersimpan else [],
|
||||||
|
"komponen_tanpa_sub": komponen_tanpa_sub,
|
||||||
"sub_uraian": {s["kode"]: s["uraian"] for s in peta["subs"]}},
|
"sub_uraian": {s["kode"]: s["uraian"] for s in peta["subs"]}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -594,6 +648,82 @@ def unduh_rps(kode: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/rps/{kode}/rtm")
|
||||||
|
async def simpan_rtm(request: Request, kode: str):
|
||||||
|
"""RTM (Rancangan Tugas Mahasiswa) — dokumen sendiri, sekadar dokumentasi
|
||||||
|
rencana tugas per MK; tak ada kaitan dengan data Nilai/skor sama sekali."""
|
||||||
|
peta = db.peta_mk(kode)
|
||||||
|
if not peta:
|
||||||
|
return RedirectResponse("/", status_code=303)
|
||||||
|
if not boleh_rps(request.state.user, kode):
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/rps/{kode}/rtm?pesan=Anda+tidak+memiliki+hak+edit+RPS+untuk+MK+ini.",
|
||||||
|
status_code=303)
|
||||||
|
form = await request.form()
|
||||||
|
# blok bisa ditambah/dihapus bebas di JS (lihat form_rtm.html) sehingga
|
||||||
|
# indeksnya boleh tidak berurutan — cari semua "judul_N" yang ada, bukan
|
||||||
|
# mengandalkan hitungan 0,1,2,... berurutan tanpa celah.
|
||||||
|
idxs = sorted({int(m.group(1)) for k in form.keys()
|
||||||
|
for m in [re.fullmatch(r"judul_(\d+)", k)] if m})
|
||||||
|
tugas = []
|
||||||
|
for i in idxs:
|
||||||
|
entri = {
|
||||||
|
"judul": (form.get(f"judul_{i}") or "").strip(),
|
||||||
|
"subs": form.getlist(f"subs_{i}"),
|
||||||
|
"bentuk_tugas": (form.get(f"bentuk_tugas_{i}") or "").strip(),
|
||||||
|
"deskripsi": (form.get(f"deskripsi_{i}") or "").strip(),
|
||||||
|
"metode_pengerjaan": (form.get(f"metode_pengerjaan_{i}") or "").strip(),
|
||||||
|
"luaran": (form.get(f"luaran_{i}") or "").strip(),
|
||||||
|
"indikator_bobot": (form.get(f"indikator_bobot_{i}") or "").strip(),
|
||||||
|
"alokasi_waktu": (form.get(f"alokasi_waktu_{i}") or "").strip(),
|
||||||
|
"tanggal": (form.get(f"tanggal_{i}") or "").strip(),
|
||||||
|
"lain_lain": (form.get(f"lain_lain_{i}") or "").strip(),
|
||||||
|
"rujukan": (form.get(f"rujukan_{i}") or "").strip(),
|
||||||
|
}
|
||||||
|
if entri["judul"] or entri["bentuk_tugas"] or entri["deskripsi"]:
|
||||||
|
tugas.append(entri)
|
||||||
|
db.simpan_dok("rtm", kode, {"tugas": tugas}, "selesai", request.state.user["nama"])
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/rps/{kode}/rtm?pesan=RTM+tersimpan+-+{len(tugas)}+tugas+terisi.",
|
||||||
|
status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/rps/{kode}/rtm", response_class=HTMLResponse)
|
||||||
|
def form_rtm(request: Request, kode: str, pesan: str = ""):
|
||||||
|
peta = db.peta_mk(kode)
|
||||||
|
if not peta:
|
||||||
|
return RedirectResponse("/", status_code=303)
|
||||||
|
rps = db.ambil_rps(kode)
|
||||||
|
if not rps or rps["status"] != "selesai":
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/rps/{kode}?pesan=RTM+hanya+bisa+diisi+setelah+RPS+ditandai+selesai.",
|
||||||
|
status_code=303)
|
||||||
|
rtm_tugas, rtm_meta = _ambil_rtm(kode)
|
||||||
|
return render(
|
||||||
|
request, "form_rtm.html",
|
||||||
|
{"peta": peta, "tahun": db.tahun_aktif(), "pesan": pesan,
|
||||||
|
"boleh_edit": boleh_rps(request.state.user, kode),
|
||||||
|
"rtm_tugas": rtm_tugas, "rtm_updated_at": rtm_meta.get("updated_at", ""),
|
||||||
|
"rtm_updated_by": rtm_meta.get("updated_by", "")},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/rps/{kode}/rtm/unduh")
|
||||||
|
def unduh_rtm(kode: str):
|
||||||
|
bahan = _bahan_rtm(kode)
|
||||||
|
if not bahan or not bahan["tugas"]:
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/rps/{kode}/rtm?pesan=Belum+ada+Tugas+RTM+yang+diisi+untuk+diunduh.",
|
||||||
|
status_code=303)
|
||||||
|
isi = build_rtm(bahan)
|
||||||
|
nama_file = re.sub(r"[^A-Za-z0-9_-]+", "_", f"RTM_{kode}_{bahan['nama']}") + ".docx"
|
||||||
|
return Response(
|
||||||
|
content=isi,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="{nama_file}"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ═════════════════════════ TAHAP 2: NILAI & PENGUKURAN ═════════════════════════
|
# ═════════════════════════ TAHAP 2: NILAI & PENGUKURAN ═════════════════════════
|
||||||
|
|
||||||
|
|
||||||
@@ -703,31 +833,180 @@ def _tabel_komponen(grid: list[dict], komponen_daftar: list[dict],
|
|||||||
return out
|
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 _catat_riwayat(riwayat_lama, updated_by: str, aksi: str, komponen: str | None = None) -> list:
|
||||||
|
"""Tambah satu entri riwayat pengisian nilai (siapa, kapan, apa) — dipakai
|
||||||
|
di tiap jalur simpan nilai (grid, input cepat, isi per sesi). Disimpan
|
||||||
|
penuh (tak dipangkas) untuk akuntabilitas team teaching."""
|
||||||
|
riwayat = list(riwayat_lama or [])
|
||||||
|
riwayat.append({
|
||||||
|
"at": datetime.now(db.WIB).strftime("%Y-%m-%d %H:%M"),
|
||||||
|
"by": updated_by, "aksi": aksi, "komponen": komponen,
|
||||||
|
})
|
||||||
|
return riwayat
|
||||||
|
|
||||||
|
|
||||||
|
_POLA_RIWAYAT_LAMA = re.compile(r"^(.*)\s\(([^()]+)\)$")
|
||||||
|
|
||||||
|
|
||||||
|
def _riwayat_terstruktur(sumber) -> list[dict]:
|
||||||
|
"""Normalisasi 'sumber' tersimpan jadi list dict seragam, terbaru duluan
|
||||||
|
— entri lama (sebelum fitur log terstruktur ini) berupa string polos
|
||||||
|
'aksi (Nama Dosen)', nama tetap diambil lewat pola itu; tanpa jam/komponen
|
||||||
|
karena data lama tidak menyimpannya."""
|
||||||
|
if not sumber:
|
||||||
|
return []
|
||||||
|
if isinstance(sumber, str):
|
||||||
|
sumber = [sumber]
|
||||||
|
out = []
|
||||||
|
for r in sumber:
|
||||||
|
if isinstance(r, dict):
|
||||||
|
out.append(r)
|
||||||
|
continue
|
||||||
|
teks = str(r)
|
||||||
|
m = _POLA_RIWAYAT_LAMA.match(teks)
|
||||||
|
if m:
|
||||||
|
out.append({"at": None, "by": m.group(2), "aksi": m.group(1), "komponen": None})
|
||||||
|
else:
|
||||||
|
out.append({"at": None, "by": "", "aksi": teks, "komponen": None})
|
||||||
|
return list(reversed(out))
|
||||||
|
|
||||||
|
|
||||||
|
def _ringkas_riwayat(riwayat: list[dict]) -> list[dict]:
|
||||||
|
"""Rekap 'siapa mengisi apa berapa kali' per komponen, dari log lengkap —
|
||||||
|
ringkasan cepat sebelum buka Log Lengkap."""
|
||||||
|
per_komponen: dict[str, dict[str, int]] = {}
|
||||||
|
for r in riwayat:
|
||||||
|
k = r.get("komponen") or "Umum"
|
||||||
|
by = r.get("by") or "?"
|
||||||
|
per_komponen.setdefault(k, {})
|
||||||
|
per_komponen[k][by] = per_komponen[k].get(by, 0) + 1
|
||||||
|
return [
|
||||||
|
{"komponen": k, "per_dosen": sorted(
|
||||||
|
({"nama": n, "n": c} for n, c in v.items()), key=lambda x: -x["n"])}
|
||||||
|
for k, v in per_komponen.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _konteks_nilai(request, kode, pesan="", peringatan=None):
|
def _konteks_nilai(request, kode, pesan="", peringatan=None):
|
||||||
peta = db.peta_mk(kode)
|
peta = db.peta_mk(kode)
|
||||||
tersimpan = db.ambil_dok("nilai", kode)
|
tersimpan = db.ambil_dok("nilai", kode)
|
||||||
pengaturan = db.ambil_pengaturan()
|
pengaturan = db.ambil_pengaturan()
|
||||||
ambang = float(pengaturan["ambang"])
|
ambang = float(pengaturan["ambang"])
|
||||||
hasil, akhir = None, {}
|
hasil, akhir, kelompok = None, {}, None
|
||||||
if tersimpan:
|
if tersimpan:
|
||||||
hasil = hitung.rollup_mk(peta, tersimpan["data"], ambang)
|
hasil = hitung.rollup_mk(peta, tersimpan["data"], ambang)
|
||||||
if tersimpan["data"].get("mode") == "per_mahasiswa":
|
if tersimpan["data"].get("mode") == "per_mahasiswa":
|
||||||
rps = db.ambil_rps(kode)
|
rps = db.ambil_rps(kode)
|
||||||
bobot_sub = hitung.bobot_sub_dari_rps(rps["data"] if rps else None)
|
bobot_sub = hitung.bobot_sub_dari_rps(rps["data"] if rps else None)
|
||||||
akhir = {a["nim"]: a["akhir"]
|
akhir_list = hitung.nilai_akhir_mahasiswa(tersimpan["data"], bobot_sub)
|
||||||
for a in hitung.nilai_akhir_mahasiswa(tersimpan["data"], bobot_sub)}
|
akhir = {a["nim"]: a["akhir"] for a in akhir_list}
|
||||||
|
skala = hitung.parse_skala(pengaturan["skala_huruf"])
|
||||||
|
statistik = hitung.statistik_akhir(akhir_list, skala)
|
||||||
|
kelompok = hitung.kelompok_capaian(akhir_list, statistik)
|
||||||
kelas_dosen = _kelas_dosen(request.state.user, kode)
|
kelas_dosen = _kelas_dosen(request.state.user, kode)
|
||||||
terdaftar = db.mahasiswa_mk(kode)
|
terdaftar = db.mahasiswa_mk(kode)
|
||||||
grid = _grid_mahasiswa(tersimpan, terdaftar, kelas_dosen)
|
grid = _grid_mahasiswa(tersimpan, terdaftar, kelas_dosen)
|
||||||
komponen_daftar = _komponen_daftar(kode)
|
komponen_daftar = _komponen_daftar(kode)
|
||||||
|
rps_cek = db.ambil_rps(kode)
|
||||||
|
komponen_tanpa_sub = _komponen_tanpa_sub(rps_cek["data"]) if rps_cek else []
|
||||||
sesi_semua = (tersimpan["data"].get("sesi") or {}) if tersimpan else {}
|
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))
|
||||||
|
riwayat = _riwayat_terstruktur(tersimpan["data"].get("sumber")) if tersimpan else []
|
||||||
return render(
|
return render(
|
||||||
request, "form_nilai.html",
|
request, "form_nilai.html",
|
||||||
{"peta": peta, "tersimpan": tersimpan, "hasil": hasil, "ambang": ambang,
|
{"peta": peta, "tersimpan": tersimpan, "hasil": hasil, "ambang": ambang,
|
||||||
"akhir": akhir, "tahun": db.tahun_aktif(), "pesan": pesan,
|
"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,
|
"komponen_daftar": komponen_daftar,
|
||||||
|
"komponen_tanpa_sub": komponen_tanpa_sub,
|
||||||
"tabel_komponen": _tabel_komponen(grid, komponen_daftar, sesi_semua) if komponen_daftar else [],
|
"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 [],
|
||||||
|
"kelompok": kelompok,
|
||||||
|
"riwayat": riwayat, "riwayat_ringkas": _ringkas_riwayat(riwayat),
|
||||||
"peringatan": peringatan or []},
|
"peringatan": peringatan or []},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -878,12 +1157,10 @@ async def simpan_nilai_komponen(request: Request, kode: str, i: str):
|
|||||||
per_nim[nim] = m
|
per_nim[nim] = m
|
||||||
m["skor"].update(subskor)
|
m["skor"].update(subskor)
|
||||||
|
|
||||||
riwayat = lama_data.get("sumber") or []
|
riwayat = _catat_riwayat(lama_data.get("sumber"), updated_by,
|
||||||
if isinstance(riwayat, str):
|
f"Isi Sesi — {label}", komponen["nama"])
|
||||||
riwayat = [riwayat]
|
|
||||||
riwayat.append(f"komponen {komponen['nama']} — {label} ({updated_by})")
|
|
||||||
data = {"mode": "per_mahasiswa", "mahasiswa": urut, "sesi": sesi_semua,
|
data = {"mode": "per_mahasiswa", "mahasiswa": urut, "sesi": sesi_semua,
|
||||||
"sumber": riwayat[-8:]}
|
"sumber": riwayat}
|
||||||
db.simpan_dok("nilai", kode, data, "selesai", updated_by)
|
db.simpan_dok("nilai", kode, data, "selesai", updated_by)
|
||||||
pesan = f"Nilai+{label.replace(' ', '+')}+tersimpan+untuk+{len(nilai)}+mahasiswa."
|
pesan = f"Nilai+{label.replace(' ', '+')}+tersimpan+untuk+{len(nilai)}+mahasiswa."
|
||||||
return RedirectResponse(f"/nilai/{kode}?pesan={pesan}", status_code=303)
|
return RedirectResponse(f"/nilai/{kode}?pesan={pesan}", status_code=303)
|
||||||
@@ -907,10 +1184,19 @@ async def simpan_nilai(request: Request, kode: str):
|
|||||||
return _konteks_nilai(request, kode, peringatan=[
|
return _konteks_nilai(request, kode, peringatan=[
|
||||||
"Anda hanya diampukan sebagian kelas MK ini — menghapus seluruh nilai "
|
"Anda hanya diampukan sebagian kelas MK ini — menghapus seluruh nilai "
|
||||||
"hanya bisa dilakukan kaprodi atau dosen yang mengampu seluruh kelas."])
|
"hanya bisa dilakukan kaprodi atau dosen yang mengampu seluruh kelas."])
|
||||||
|
akun = db.pengguna_by_id(request.state.user["id"])
|
||||||
|
password = (form.get("password") or "").strip()
|
||||||
|
if not akun or not auth.verifikasi_password(password, akun["pw_hash"]):
|
||||||
|
return _konteks_nilai(request, kode, peringatan=[
|
||||||
|
"Password salah — nilai TIDAK jadi dihapus."])
|
||||||
|
lama = db.ambil_dok("nilai", kode)
|
||||||
|
if lama:
|
||||||
|
db.arsipkan_nilai_hapus(kode, db.tahun_aktif(), lama["data"], updated_by)
|
||||||
db.hapus_dok("nilai", kode)
|
db.hapus_dok("nilai", kode)
|
||||||
return _konteks_nilai(
|
return _konteks_nilai(
|
||||||
request, kode,
|
request, kode,
|
||||||
pesan="Seluruh nilai tersimpan MK ini dihapus — silakan mulai dari awal.")
|
pesan="Seluruh nilai tersimpan MK ini dihapus (salinan sebelumnya "
|
||||||
|
"tersimpan otomatis sebagai arsip) — silakan mulai dari awal.")
|
||||||
|
|
||||||
if aksi == "komponen":
|
if aksi == "komponen":
|
||||||
kelas_dosen = _kelas_dosen(request.state.user, kode)
|
kelas_dosen = _kelas_dosen(request.state.user, kode)
|
||||||
@@ -965,12 +1251,9 @@ async def simpan_nilai(request: Request, kode: str):
|
|||||||
per_nim[nim] = mm
|
per_nim[nim] = mm
|
||||||
mm["skor"].update(subskor)
|
mm["skor"].update(subskor)
|
||||||
|
|
||||||
riwayat = lama_data.get("sumber") or []
|
riwayat = _catat_riwayat(lama_data.get("sumber"), updated_by, "Input Cepat per Komponen")
|
||||||
if isinstance(riwayat, str):
|
|
||||||
riwayat = [riwayat]
|
|
||||||
riwayat.append(f"input per komponen ({updated_by})")
|
|
||||||
data = {"mode": "per_mahasiswa", "mahasiswa": urut, "sesi": sesi_semua,
|
data = {"mode": "per_mahasiswa", "mahasiswa": urut, "sesi": sesi_semua,
|
||||||
"sumber": riwayat[-8:]}
|
"sumber": riwayat}
|
||||||
db.simpan_dok("nilai", kode, data, "selesai", updated_by)
|
db.simpan_dok("nilai", kode, data, "selesai", updated_by)
|
||||||
return _konteks_nilai(
|
return _konteks_nilai(
|
||||||
request, kode,
|
request, kode,
|
||||||
@@ -1006,14 +1289,11 @@ async def simpan_nilai(request: Request, kode: str):
|
|||||||
peringatan=["Tidak ada nilai yang diisi."])
|
peringatan=["Tidak ada nilai yang diisi."])
|
||||||
lama = db.ambil_dok("nilai", kode)
|
lama = db.ambil_dok("nilai", kode)
|
||||||
lama_data = lama["data"] if lama else {}
|
lama_data = lama["data"] if lama else {}
|
||||||
riwayat = lama_data.get("sumber") or []
|
riwayat = _catat_riwayat(lama_data.get("sumber"), updated_by, "Input Langsung (grid Sub-CPMK)")
|
||||||
if isinstance(riwayat, str):
|
|
||||||
riwayat = [riwayat]
|
|
||||||
riwayat.append(f"input langsung ({updated_by})")
|
|
||||||
if kelas_dosen is None:
|
if kelas_dosen is None:
|
||||||
# dosen tak dibatasi kelas: tabel yang tampil = seluruh MK,
|
# dosen tak dibatasi kelas: tabel yang tampil = seluruh MK,
|
||||||
# jadi menyimpan persis isi tabel memang dimaksudkan sebagai pengganti
|
# jadi menyimpan persis isi tabel memang dimaksudkan sebagai pengganti
|
||||||
data = {"mode": "per_mahasiswa", "mahasiswa": mahasiswa, "sumber": riwayat[-8:]}
|
data = {"mode": "per_mahasiswa", "mahasiswa": mahasiswa, "sumber": riwayat}
|
||||||
else:
|
else:
|
||||||
# dosen dibatasi kelas: tabel yang tampil cuma kelasnya — gabungkan
|
# dosen dibatasi kelas: tabel yang tampil cuma kelasnya — gabungkan
|
||||||
# ke data lama, jangan sampai menghapus nilai kelas lain
|
# ke data lama, jangan sampai menghapus nilai kelas lain
|
||||||
@@ -1028,7 +1308,7 @@ async def simpan_nilai(request: Request, kode: str):
|
|||||||
else:
|
else:
|
||||||
ada["nama"] = m["nama"] or ada.get("nama", "")
|
ada["nama"] = m["nama"] or ada.get("nama", "")
|
||||||
ada["skor"] = m["skor"]
|
ada["skor"] = m["skor"]
|
||||||
data = {"mode": "per_mahasiswa", "mahasiswa": urut, "sumber": riwayat[-8:]}
|
data = {"mode": "per_mahasiswa", "mahasiswa": urut, "sumber": riwayat}
|
||||||
if lama_data.get("sesi"):
|
if lama_data.get("sesi"):
|
||||||
# pertahankan log sesi per-komponen — grid ini hanya koreksi manual
|
# pertahankan log sesi per-komponen — grid ini hanya koreksi manual
|
||||||
# di atasnya, bukan pengganti sumber datanya
|
# di atasnya, bukan pengganti sumber datanya
|
||||||
@@ -1052,6 +1332,23 @@ PORTO_KOSONG = {
|
|||||||
"simpulan": "",
|
"simpulan": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_BULAN_IND = ("", "Januari", "Februari", "Maret", "April", "Mei", "Juni",
|
||||||
|
"Juli", "Agustus", "September", "Oktober", "November", "Desember")
|
||||||
|
|
||||||
|
|
||||||
|
def _tanggal_indonesia(d: datetime) -> str:
|
||||||
|
return f"{d.day} {_BULAN_IND[d.month]} {d.year}"
|
||||||
|
|
||||||
|
|
||||||
|
def _nomor_porto_otomatis(kode: str) -> str:
|
||||||
|
"""PORTO/INF/<tahun>/<no urut MK di kurikulum> — bisa diedit dosen."""
|
||||||
|
try:
|
||||||
|
no_mk = int(db.kurikulum()["mk"].get(kode, {}).get("no", 0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
no_mk = 0
|
||||||
|
tahun_awal = db.tahun_aktif().split("/")[0]
|
||||||
|
return f"PORTO/INF/{tahun_awal}/{no_mk:03d}"
|
||||||
|
|
||||||
|
|
||||||
@app.get("/portofolio/{kode}", response_class=HTMLResponse)
|
@app.get("/portofolio/{kode}", response_class=HTMLResponse)
|
||||||
def form_porto(request: Request, kode: str, pesan: str = ""):
|
def form_porto(request: Request, kode: str, pesan: str = ""):
|
||||||
@@ -1062,13 +1359,46 @@ def form_porto(request: Request, kode: str, pesan: str = ""):
|
|||||||
if tersimpan:
|
if tersimpan:
|
||||||
f = tersimpan["data"]
|
f = tersimpan["data"]
|
||||||
else:
|
else:
|
||||||
f = {"dokumen": dict(PORTO_KOSONG["dokumen"]),
|
f = {"dokumen": {**PORTO_KOSONG["dokumen"],
|
||||||
|
"nomor": _nomor_porto_otomatis(kode),
|
||||||
|
"tgl_terbit": _tanggal_indonesia(datetime.now())},
|
||||||
"realisasi": "", "simpulan": "",
|
"realisasi": "", "simpulan": "",
|
||||||
"kendala": [{"kendala": "", "tindak": ""} for _ in range(5)]}
|
"kendala": [{"kendala": "", "tindak": ""} for _ in range(5)]}
|
||||||
while len(f.get("kendala", [])) < 5:
|
while len(f.get("kendala", [])) < 5:
|
||||||
f.setdefault("kendala", []).append({"kendala": "", "tindak": ""})
|
f.setdefault("kendala", []).append({"kendala": "", "tindak": ""})
|
||||||
rps = db.ambil_rps(kode)
|
rps = db.ambil_rps(kode)
|
||||||
nilai = db.ambil_dok("nilai", kode)
|
nilai = db.ambil_dok("nilai", kode)
|
||||||
|
|
||||||
|
analisis = None
|
||||||
|
if nilai:
|
||||||
|
pengaturan = db.ambil_pengaturan()
|
||||||
|
ambang = float(pengaturan["ambang"])
|
||||||
|
skala = hitung.parse_skala(pengaturan["skala_huruf"])
|
||||||
|
rps_data = rps["data"] if rps else None
|
||||||
|
peta_analisis = _terapkan_override_sub(peta, rps_data)
|
||||||
|
nilai_data = nilai["data"]
|
||||||
|
bobot_sub = hitung.bobot_sub_dari_rps(rps_data)
|
||||||
|
rollup = hitung.rollup_mk(peta_analisis, nilai_data, ambang)
|
||||||
|
akhir = (hitung.nilai_akhir_mahasiswa(nilai_data, bobot_sub)
|
||||||
|
if nilai_data.get("mode") == "per_mahasiswa" else [])
|
||||||
|
statistik = hitung.statistik_akhir(akhir, skala) if akhir else None
|
||||||
|
tren = hitung.tren_mk(kode, db.daftar_tahun(), ambang)
|
||||||
|
sub_urut = hitung.sub_terurut(peta_analisis, rollup["sub"])
|
||||||
|
kesenjangan = hitung.kesenjangan_rencana_realisasi(bobot_sub, rollup["sub"])
|
||||||
|
berisiko = hitung.mahasiswa_berisiko(nilai_data, akhir, ambang)
|
||||||
|
tren = tren if len(tren) > 1 else []
|
||||||
|
analisis = {
|
||||||
|
"ambang": ambang,
|
||||||
|
"statistik": statistik,
|
||||||
|
"sub_urut": sub_urut,
|
||||||
|
"kesenjangan": kesenjangan,
|
||||||
|
"berisiko": berisiko,
|
||||||
|
"tren": tren,
|
||||||
|
"kelompok": hitung.kelompok_capaian(akhir, statistik),
|
||||||
|
"simpulan_saran": hitung.saran_simpulan(
|
||||||
|
akhir, statistik, sub_urut, kesenjangan, berisiko, tren, ambang),
|
||||||
|
}
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request, "form_porto.html",
|
request, "form_porto.html",
|
||||||
{"peta": peta, "f": f, "tahun": db.tahun_aktif(), "pesan": pesan,
|
{"peta": peta, "f": f, "tahun": db.tahun_aktif(), "pesan": pesan,
|
||||||
@@ -1076,7 +1406,7 @@ def form_porto(request: Request, kode: str, pesan: str = ""):
|
|||||||
"updated_at": tersimpan["updated_at"] if tersimpan else "",
|
"updated_at": tersimpan["updated_at"] if tersimpan else "",
|
||||||
"updated_by": tersimpan["updated_by"] if tersimpan else "",
|
"updated_by": tersimpan["updated_by"] if tersimpan else "",
|
||||||
"boleh_edit": boleh_mk(request.state.user, kode),
|
"boleh_edit": boleh_mk(request.state.user, kode),
|
||||||
"ada_rps": bool(rps), "ada_nilai": bool(nilai)},
|
"ada_rps": bool(rps), "ada_nilai": bool(nilai), "analisis": analisis},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1210,10 +1540,14 @@ def rekap(request: Request):
|
|||||||
cpl_urut = sorted(hasil["cpl"].values(), key=lambda c: c["kode"])
|
cpl_urut = sorted(hasil["cpl"].values(), key=lambda c: c["kode"])
|
||||||
cpmk_urut = sorted(hasil["cpmk"].values(), key=lambda c: c["kode"])
|
cpmk_urut = sorted(hasil["cpmk"].values(), key=lambda c: c["kode"])
|
||||||
nama_mk = {m["kode"]: m["nama"] for m in db.daftar_mk()}
|
nama_mk = {m["kode"]: m["nama"] for m in db.daftar_mk()}
|
||||||
|
k = db.kurikulum()
|
||||||
|
desk_cpmk = {info["kode"]: info["desk"] for info in k["cpmk"].values()}
|
||||||
|
desk_cpl = {info["kode"]: info["desk"] for info in k["cpl"].values()}
|
||||||
return render(
|
return render(
|
||||||
request, "rekap.html",
|
request, "rekap.html",
|
||||||
{"hasil": hasil, "cpl_urut": cpl_urut, "cpmk_urut": cpmk_urut,
|
{"hasil": hasil, "cpl_urut": cpl_urut, "cpmk_urut": cpmk_urut,
|
||||||
"nama_mk": nama_mk, "tahun": db.tahun_aktif()},
|
"nama_mk": nama_mk, "tahun": db.tahun_aktif(),
|
||||||
|
"desk_cpmk": desk_cpmk, "desk_cpl": desk_cpl},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -113,9 +113,15 @@ table.grid td.tengah, table.grid th.tengah { text-align: center; }
|
|||||||
table.grid td.label { background: var(--blue-fill); font-weight: bold; width: 220px; }
|
table.grid td.label { background: var(--blue-fill); font-weight: bold; width: 220px; }
|
||||||
table.grid tr.uts td { background: var(--green-fill); }
|
table.grid tr.uts td { background: var(--green-fill); }
|
||||||
table.grid tr.uas td { background: var(--yellow-fill); }
|
table.grid tr.uas td { background: var(--yellow-fill); }
|
||||||
|
table.grid tr.kelompok-atas td { background: var(--green-fill); }
|
||||||
|
table.grid tr.kelompok-tengah td { background: var(--yellow-fill); }
|
||||||
|
table.grid tr.kelompok-bawah td { background: #f6ddd8; }
|
||||||
table.grid tr.total td { background: var(--blue-fill); font-weight: bold; }
|
table.grid tr.total td { background: var(--blue-fill); font-weight: bold; }
|
||||||
.gulir { overflow-x: auto; }
|
.gulir { overflow-x: auto; }
|
||||||
|
|
||||||
|
/* kode CPL/CPMK/Sub-CPMK dengan deskripsi lengkap di tooltip (title) */
|
||||||
|
.kode-hover { cursor: help; border-bottom: 1px dotted var(--grey); }
|
||||||
|
|
||||||
/* header tabel yang bisa diurut (tabel.js) */
|
/* header tabel yang bisa diurut (tabel.js) */
|
||||||
table.grid th.bisa-urut { cursor: pointer; user-select: none; }
|
table.grid th.bisa-urut { cursor: pointer; user-select: none; }
|
||||||
table.grid th.bisa-urut:hover { background: #2e75b6; }
|
table.grid th.bisa-urut:hover { background: #2e75b6; }
|
||||||
|
|||||||
+256
-19
@@ -2,7 +2,7 @@
|
|||||||
{% block judul %}Nilai {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
{% block judul %}Nilai {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
||||||
{% block isi %}
|
{% block isi %}
|
||||||
<div class="nav-baris">
|
<div class="nav-baris">
|
||||||
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <b>Input Nilai</b> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <a href="/rps/{{ peta.kode }}/rtm">RTM</a> · <b>Input Nilai</b> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Input Nilai — {{ peta.nama }} <span style="color:var(--grey)">({{ peta.kode }})</span></h1>
|
<h1>Input Nilai — {{ peta.nama }} <span style="color:var(--grey)">({{ peta.kode }})</span></h1>
|
||||||
@@ -17,6 +17,13 @@
|
|||||||
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
|
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
|
||||||
{% for w in peringatan %}<div class="peringatan">{{ w }}</div>{% endfor %}
|
{% for w in peringatan %}<div class="peringatan">{{ w }}</div>{% endfor %}
|
||||||
|
|
||||||
|
{% if komponen_tanpa_sub %}
|
||||||
|
<div class="peringatan"><b>Sub-CPMK belum ditautkan ke komponen penilaian</b> untuk
|
||||||
|
{{ komponen_tanpa_sub|join(', ') }} — segera isi di RPS bagian G. Selama belum ditautkan,
|
||||||
|
komponen itu {% if not komponen_daftar %}tidak muncul sama sekali{% else %}belum ikut muncul{% endif %}
|
||||||
|
di "Isi per Komponen" di bawah. <a href="/rps/{{ peta.kode }}">Buka RPS →</a> lalu centang Sub-CPMK-nya.</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if komponen_daftar %}
|
{% if komponen_daftar %}
|
||||||
<h2 class="bagian">ISI PER KOMPONEN</h2>
|
<h2 class="bagian">ISI PER KOMPONEN</h2>
|
||||||
<table class="grid" style="max-width:760px">
|
<table class="grid" style="max-width:760px">
|
||||||
@@ -54,27 +61,69 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if tersimpan %}
|
{% if tersimpan %}
|
||||||
|
{% set n_mhs = tersimpan.data.mahasiswa|length if tersimpan.data.mode == 'per_mahasiswa' else none %}
|
||||||
<div class="info">
|
<div class="info">
|
||||||
Data tersimpan: mode
|
Data tersimpan: mode
|
||||||
<b>{% if tersimpan.data.mode == 'per_mahasiswa' %}PER MAHASISWA ({{ tersimpan.data.mahasiswa|length }} mahasiswa){% else %}BUKTI AGREGAT (rata-rata kelas){% endif %}</b>
|
<b>{% if tersimpan.data.mode == 'per_mahasiswa' %}PER MAHASISWA ({{ n_mhs }} mahasiswa){% else %}BUKTI AGREGAT (rata-rata kelas){% endif %}</b>
|
||||||
— terakhir disimpan {{ tersimpan.updated_at }}{% if tersimpan.updated_by %} oleh {{ tersimpan.updated_by }}{% endif %}.
|
— terakhir disimpan {{ tersimpan.updated_at }}{% if tersimpan.updated_by %} oleh {{ tersimpan.updated_by }}{% endif %}.
|
||||||
{% if tersimpan.data.sumber %}<br>Riwayat perubahan:
|
{% if riwayat_ringkas %}<br>Ringkasan pengisian — team teaching:
|
||||||
{{ tersimpan.data.sumber if tersimpan.data.sumber is string else tersimpan.data.sumber|join(' · ') }}.{% endif %}
|
{% for k in riwayat_ringkas %}{{ k.komponen }}: {% for d in k.per_dosen %}{{ d.n }}× {{ d.nama }}{{ ', ' if not loop.last }}{% endfor %}{{ ' · ' if not loop.last }}{% endfor %}.{% endif %}
|
||||||
<form method="post" action="/nilai/{{ peta.kode }}" style="display:inline;margin-left:8px"
|
<form method="post" action="/nilai/{{ peta.kode }}" style="display:flex;align-items:center;gap:8px;margin-top:8px;flex-wrap:wrap"
|
||||||
onsubmit="return confirm('Hapus SELURUH nilai tersimpan MK ini untuk {{ tahun }}? Tindakan ini tidak bisa dibatalkan.')">
|
onsubmit="return confirm('Hapus SELURUH nilai{% if n_mhs %} {{ n_mhs }} mahasiswa{% endif %} tersimpan MK ini untuk {{ tahun }}?\n\nSalinan sebelumnya akan diarsipkan otomatis, tapi tidak ada tombol pulihkan di aplikasi — hubungi kaprodi/admin bila perlu dikembalikan.\n\nTindakan ini tidak bisa dibatalkan sendiri. Lanjutkan?')">
|
||||||
<button class="tombol garis" type="submit" name="aksi" value="hapus"
|
<input type="hidden" name="aksi" value="hapus">
|
||||||
|
<label style="font-size:13px;color:var(--grey)">Konfirmasi password Anda:
|
||||||
|
<input type="password" name="password" required autocomplete="current-password"
|
||||||
|
style="display:inline-block;width:auto;padding:3px 6px;font-size:13px;margin-left:4px">
|
||||||
|
</label>
|
||||||
|
<button class="tombol garis" type="submit"
|
||||||
style="color:var(--red);border-color:var(--red);padding:2px 10px;font-size:13px">Mulai Ulang (hapus semua nilai)</button>
|
style="color:var(--red);border-color:var(--red);padding:2px 10px;font-size:13px">Mulai Ulang (hapus semua nilai)</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if riwayat %}
|
||||||
|
<details class="cara-alt" style="margin:16px 0">
|
||||||
|
<summary style="font-size:13px">Log Lengkap Pengisian Nilai ({{ riwayat|length }} entri)</summary>
|
||||||
|
<div class="gulir" style="margin-top:8px;max-height:320px;overflow-y:auto">
|
||||||
|
<table class="grid">
|
||||||
|
<thead><tr><th style="width:140px">Waktu</th><th>Dosen</th><th>Aksi</th><th style="width:140px">Komponen</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for r in riwayat %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ r.at or '—' }}</td>
|
||||||
|
<td>{{ r.by or '—' }}</td>
|
||||||
|
<td>{{ r.aksi }}</td>
|
||||||
|
<td>{{ r.komponen or '—' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set ada_kelas = grid and grid|selectattr('kelas')|list|length > 0 %}
|
||||||
|
|
||||||
{% if grid and tabel_komponen and komponen_daftar %}
|
{% if grid and tabel_komponen and komponen_daftar %}
|
||||||
<h2 class="bagian">NILAI PER KOMPONEN <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai mahasiswa berdasarkan komponen</span></h2>
|
<h2 class="bagian">NILAI PER KOMPONEN <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai mahasiswa berdasarkan komponen</span></h2>
|
||||||
<p class="keterangan">Angka = rata-rata skor mentah tiap sesi/tugas komponen itu yang diikuti mahasiswa (bukan lewat Sub-CPMK). Ketik untuk menimpa —
|
<p class="keterangan">Angka = rata-rata skor mentah tiap sesi/tugas komponen itu yang diikuti mahasiswa (bukan lewat Sub-CPMK). Ketik untuk menimpa —
|
||||||
ditulis rata ke semua Sub-CPMK komponen itu; rincian per Sub-CPMK tetap presisi di grid bawah.</p>
|
ditulis rata ke semua Sub-CPMK komponen itu; rincian per Sub-CPMK tetap presisi di grid bawah.</p>
|
||||||
|
{% if ada_kelas %}
|
||||||
|
{% set semua_kelas = grid|map(attribute='kelas')|select|unique|sort|list %}
|
||||||
|
<div class="filter-baris">
|
||||||
|
<label for="filter-kelas">Tampilkan kelas:</label>
|
||||||
|
<select id="filter-kelas" style="width:auto">
|
||||||
|
<option value="">Semua</option>
|
||||||
|
{% for k in semua_kelas %}
|
||||||
|
<option value="{{ k }}">Kelas {{ k }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<span id="filter-kelas-info" class="keterangan" style="margin:0"></span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<form method="post" action="/nilai/{{ peta.kode }}">
|
<form method="post" action="/nilai/{{ peta.kode }}">
|
||||||
<input type="hidden" name="aksi" value="komponen">
|
<input type="hidden" name="aksi" value="komponen">
|
||||||
<div class="gulir" style="max-height:480px;overflow-y:auto">
|
<div class="gulir">
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -88,7 +137,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for r in tabel_komponen %}
|
{% for r in tabel_komponen %}
|
||||||
{% set ri = loop.index0 %}
|
{% set ri = loop.index0 %}
|
||||||
<tr>
|
<tr data-kelas="{{ r.kelas or '' }}">
|
||||||
<td class="tengah">{{ loop.index }}
|
<td class="tengah">{{ loop.index }}
|
||||||
<input type="hidden" name="nim_{{ ri }}" value="{{ r.nim }}">
|
<input type="hidden" name="nim_{{ ri }}" value="{{ r.nim }}">
|
||||||
<input type="hidden" name="nama_{{ ri }}" value="{{ r.nama }}"></td>
|
<input type="hidden" name="nama_{{ ri }}" value="{{ r.nama }}"></td>
|
||||||
@@ -99,12 +148,15 @@
|
|||||||
min="0" max="100" step="any"
|
min="0" max="100" step="any"
|
||||||
value="{{ '%g'|format(r.nilai[k.i]) if r.nilai[k.i] is not none else '' }}"></td>
|
value="{{ '%g'|format(r.nilai[k.i]) if r.nilai[k.i] is not none else '' }}"></td>
|
||||||
{% endfor %}
|
{% 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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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">
|
<div class="aksi-baris">
|
||||||
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
@@ -116,9 +168,21 @@
|
|||||||
{% if grid %}
|
{% if grid %}
|
||||||
{% set per_mhs = tersimpan and tersimpan.data.mode == 'per_mahasiswa' %}
|
{% set per_mhs = tersimpan and tersimpan.data.mode == 'per_mahasiswa' %}
|
||||||
<h2 class="bagian">NILAI PER MAHASISWA <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai mahasiswa berdasarkan Sub-CPMK</span></h2>
|
<h2 class="bagian">NILAI PER MAHASISWA <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai mahasiswa berdasarkan Sub-CPMK</span></h2>
|
||||||
|
{% if ada_kelas and not (tabel_komponen and komponen_daftar) %}
|
||||||
|
{% set semua_kelas = grid|map(attribute='kelas')|select|unique|sort|list %}
|
||||||
|
<div class="filter-baris">
|
||||||
|
<label for="filter-kelas">Tampilkan kelas:</label>
|
||||||
|
<select id="filter-kelas" style="width:auto">
|
||||||
|
<option value="">Semua</option>
|
||||||
|
{% for k in semua_kelas %}
|
||||||
|
<option value="{{ k }}">Kelas {{ k }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<span id="filter-kelas-info" class="keterangan" style="margin:0"></span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<form method="post" action="/nilai/{{ peta.kode }}">
|
<form method="post" action="/nilai/{{ peta.kode }}">
|
||||||
<div class="gulir" style="max-height:480px;overflow-y:auto">
|
<div class="gulir">
|
||||||
{% set ada_kelas = grid|selectattr('kelas')|list|length > 0 %}
|
|
||||||
{% set n_kiri = 4 if ada_kelas else 3 %}
|
{% set n_kiri = 4 if ada_kelas else 3 %}
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -134,7 +198,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for m in grid %}
|
{% for m in grid %}
|
||||||
{% set i = loop.index0 %}
|
{% set i = loop.index0 %}
|
||||||
<tr>
|
<tr data-kelas="{{ m.kelas or '' }}">
|
||||||
<td class="tengah">{{ loop.index }}
|
<td class="tengah">{{ loop.index }}
|
||||||
<input type="hidden" name="nim_{{ i }}" value="{{ m.nim }}">
|
<input type="hidden" name="nim_{{ i }}" value="{{ m.nim }}">
|
||||||
<input type="hidden" name="nama_{{ i }}" value="{{ m.nama }}"></td>
|
<input type="hidden" name="nama_{{ i }}" value="{{ m.nama }}"></td>
|
||||||
@@ -144,13 +208,13 @@
|
|||||||
{% for s in peta.subs %}
|
{% for s in peta.subs %}
|
||||||
<td class="isian"><input type="number" name="skor_{{ i }}_{{ s.kode }}"
|
<td class="isian"><input type="number" name="skor_{{ i }}_{{ s.kode }}"
|
||||||
min="0" max="100" step="any"
|
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 %}
|
{% 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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if per_mhs and hasil %}
|
{% if per_mhs and hasil %}
|
||||||
<tr class="total">
|
<tr class="total" data-kelas="__total__" title="Dihitung dari seluruh mahasiswa MK ini, tidak per kelas">
|
||||||
<td colspan="{{ n_kiri }}" style="text-align:right">Rata-rata kelas (tersimpan)</td>
|
<td colspan="{{ n_kiri }}" style="text-align:right">Rata-rata kelas (tersimpan)</td>
|
||||||
{% for s in peta.subs %}
|
{% for s in peta.subs %}
|
||||||
{% set info = hasil.sub.get(s.kode) %}
|
{% set info = hasil.sub.get(s.kode) %}
|
||||||
@@ -158,7 +222,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<td class="tengah">{{ '%.1f'|format(akhir.values()|sum / akhir|length) if akhir else '—' }}</td>
|
<td class="tengah">{{ '%.1f'|format(akhir.values()|sum / akhir|length) if akhir else '—' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="total">
|
<tr class="total" data-kelas="__total__" title="Dihitung dari seluruh mahasiswa MK ini, tidak per kelas">
|
||||||
<td colspan="{{ n_kiri }}" style="text-align:right">% lulus (≥ {{ ambang|round(0)|int }})</td>
|
<td colspan="{{ n_kiri }}" style="text-align:right">% lulus (≥ {{ ambang|round(0)|int }})</td>
|
||||||
{% for s in peta.subs %}
|
{% for s in peta.subs %}
|
||||||
{% set info = hasil.sub.get(s.kode) %}
|
{% set info = hasil.sub.get(s.kode) %}
|
||||||
@@ -170,6 +234,12 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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">
|
<div class="aksi-baris">
|
||||||
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
@@ -178,15 +248,149 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if kelompok or tabel_poin or bobot_komponen or hasil %}
|
||||||
|
<details class="cara-alt" style="margin-top:26px;border:1px solid #c9c9c9;padding:10px 14px">
|
||||||
|
<summary>DETAIL TAMBAHAN — Kelompok Capaian, Skala Poin, Rincian Bobot & Hasil Ketercapaian</summary>
|
||||||
|
|
||||||
|
{% if kelompok %}
|
||||||
|
<h2 class="bagian">KELOMPOK CAPAIAN — ATAS / TENGAH / BAWAH <span style="font-weight:normal;font-style:italic;color:var(--grey)">(acuan norma: mean ± SD kelas ini, bukan ambang kelulusan)</span></h2>
|
||||||
|
<p class="keterangan">Batas: Atas > {{ '%.1f'|format(kelompok.batas_atas) }} · Bawah < {{ '%.1f'|format(kelompok.batas_bawah) }} — untuk melihat sebaran capaian relatif dalam kelas ini, pelengkap tabel Hasil Ketercapaian (acuan ambang tetap) di bawah.</p>
|
||||||
|
<div class="gulir">
|
||||||
|
<table class="grid" style="max-width:540px">
|
||||||
|
<thead><tr><th style="width:44px" class="tengah">No.</th><th style="width:112px">NIM</th><th>Nama</th><th class="tengah" style="width:90px">Nilai Akhir</th><th class="tengah" style="width:80px">Kelompok</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% set ns = namespace(no=0) %}
|
||||||
|
{% for label, kelas, warna, daftar in [('Atas', 'kelompok-atas', '#205020', kelompok.atas), ('Tengah', 'kelompok-tengah', '#7a5c00', kelompok.tengah), ('Bawah', 'kelompok-bawah', 'var(--red)', kelompok.bawah)] %}
|
||||||
|
{% for m in daftar %}
|
||||||
|
{% set ns.no = ns.no + 1 %}
|
||||||
|
<tr class="{{ kelas }}">
|
||||||
|
<td class="tengah">{{ ns.no }}</td>
|
||||||
|
<td>{{ m.nim }}</td>
|
||||||
|
<td>{{ m.nama }}</td>
|
||||||
|
<td class="tengah">{{ '%.1f'|format(m.akhir) }}</td>
|
||||||
|
<td class="tengah" style="color:{{ warna }};font-weight:bold">{{ label }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% 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">
|
||||||
|
<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 %}
|
{% 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><span class="kode-hover" title="{{ s.uraian }}">{{ s.kode }}</span></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>
|
<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">
|
<div class="gulir">
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
<thead><tr><th>CPMK</th><th class="tengah">Skor</th><th class="tengah">Sub Ternilai</th><th class="tengah">% Ternilai</th><th>CPL Didukung</th></tr></thead>
|
<thead><tr><th>CPMK</th><th class="tengah">Skor</th><th class="tengah">Sub Ternilai</th><th class="tengah">% Ternilai</th><th>CPL Didukung</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for kode_c, v in hasil.cpmk.items() %}
|
{% for kode_c, v in hasil.cpmk.items() %}
|
||||||
|
{% set cm = peta.cpmks|selectattr('kode', 'equalto', kode_c)|first %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ kode_c }}</td>
|
<td><span class="kode-hover" title="{{ cm.desk if cm else '' }}">{{ kode_c }}</span></td>
|
||||||
<td class="tengah">{% if v.skor is not none %}<b>{{ '%.2f'|format(v.skor) }}</b>{% else %}—{% endif %}</td>
|
<td class="tengah">{% if v.skor is not none %}<b>{{ '%.2f'|format(v.skor) }}</b>{% else %}—{% endif %}</td>
|
||||||
<td class="tengah">{{ v.n_ternilai }}/{{ v.n_sub }}</td>
|
<td class="tengah">{{ v.n_ternilai }}/{{ v.n_sub }}</td>
|
||||||
<td class="tengah">{{ '%.0f'|format(100 * v.n_ternilai / v.n_sub) ~ '%' if v.n_sub else '—' }}</td>
|
<td class="tengah">{{ '%.0f'|format(100 * v.n_ternilai / v.n_sub) ~ '%' if v.n_sub else '—' }}</td>
|
||||||
@@ -201,8 +405,9 @@
|
|||||||
<thead><tr><th>CPL</th><th class="tengah">Skor</th><th class="tengah">% CPMK Ternilai</th><th class="tengah">Status</th></tr></thead>
|
<thead><tr><th>CPL</th><th class="tengah">Skor</th><th class="tengah">% CPMK Ternilai</th><th class="tengah">Status</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for kode_c, v in hasil.cpl.items() %}
|
{% for kode_c, v in hasil.cpl.items() %}
|
||||||
|
{% set cp = peta.cpls|selectattr('kode', 'equalto', kode_c)|first %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ kode_c }}</td>
|
<td><span class="kode-hover" title="{{ cp.desk if cp else '' }}">{{ kode_c }}</span></td>
|
||||||
<td class="tengah">{% if v.skor is not none %}<b>{{ '%.2f'|format(v.skor) }}</b>{% else %}—{% endif %}</td>
|
<td class="tengah">{% if v.skor is not none %}<b>{{ '%.2f'|format(v.skor) }}</b>{% else %}—{% endif %}</td>
|
||||||
<td class="tengah">{{ '%.0f'|format(100 * v.n_cpmk_ternilai / v.n_cpmk) ~ '%' if v.n_cpmk else '—' }}</td>
|
<td class="tengah">{{ '%.0f'|format(100 * v.n_cpmk_ternilai / v.n_cpmk) ~ '%' if v.n_cpmk else '—' }}</td>
|
||||||
<td class="tengah">
|
<td class="tengah">
|
||||||
@@ -216,4 +421,36 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if ada_kelas %}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var pilih = document.getElementById('filter-kelas');
|
||||||
|
var info = document.getElementById('filter-kelas-info');
|
||||||
|
var baris = document.querySelectorAll('table.grid tbody tr[data-kelas]');
|
||||||
|
|
||||||
|
function perbarui() {
|
||||||
|
var v = pilih.value, n = 0, total = 0;
|
||||||
|
baris.forEach(function (tr) {
|
||||||
|
var kelas = tr.dataset.kelas;
|
||||||
|
if (kelas === '__total__') {
|
||||||
|
// baris rata-rata/% lulus: dihitung dari seluruh MK, cuma masuk akal saat "Semua"
|
||||||
|
tr.style.display = v === '' ? '' : 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
total++;
|
||||||
|
var tampil = v === '' || kelas === v;
|
||||||
|
tr.style.display = tampil ? '' : 'none';
|
||||||
|
if (tampil) n++;
|
||||||
|
});
|
||||||
|
info.textContent = v === '' ? '' : n + ' dari ' + total + ' mahasiswa ditampilkan';
|
||||||
|
}
|
||||||
|
|
||||||
|
pilih.addEventListener('change', perbarui);
|
||||||
|
perbarui();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block judul %}Nilai {{ komponen.nama }} — {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
{% block judul %}Nilai {{ komponen.nama }} — {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
||||||
{% block isi %}
|
{% block isi %}
|
||||||
<div class="nav-baris">
|
<div class="nav-baris">
|
||||||
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <b>{{ komponen.nama }}</b> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <a href="/rps/{{ peta.kode }}/rtm">RTM</a> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <b>{{ komponen.nama }}</b> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Isi Nilai — {{ komponen.nama }} <span style="color:var(--grey)">({{ peta.nama }})</span></h1>
|
<h1>Isi Nilai — {{ komponen.nama }} <span style="color:var(--grey)">({{ peta.nama }})</span></h1>
|
||||||
@@ -96,6 +96,20 @@
|
|||||||
</label>
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h2 class="bagian">NILAI MAHASISWA</h2>
|
<h2 class="bagian">NILAI MAHASISWA</h2>
|
||||||
|
{% set ada_kelas = grid|selectattr('kelas')|list|length > 0 %}
|
||||||
|
{% if ada_kelas %}
|
||||||
|
{% set semua_kelas = grid|map(attribute='kelas')|select|unique|sort|list %}
|
||||||
|
<div class="filter-baris">
|
||||||
|
<label for="filter-kelas">Tampilkan kelas:</label>
|
||||||
|
<select id="filter-kelas" style="width:auto">
|
||||||
|
<option value="">Semua</option>
|
||||||
|
{% for k in semua_kelas %}
|
||||||
|
<option value="{{ k }}">Kelas {{ k }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<span id="filter-kelas-info" class="keterangan" style="margin:0"></span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="gulir" style="max-height:480px;overflow-y:auto">
|
<div class="gulir" style="max-height:480px;overflow-y:auto">
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -103,17 +117,19 @@
|
|||||||
<th style="width:44px" class="tengah">No.</th>
|
<th style="width:44px" class="tengah">No.</th>
|
||||||
<th style="width:112px">NIM</th>
|
<th style="width:112px">NIM</th>
|
||||||
<th style="min-width:150px">Nama</th>
|
<th style="min-width:150px">Nama</th>
|
||||||
|
{% if ada_kelas %}<th style="width:60px" class="tengah">Kelas</th>{% endif %}
|
||||||
<th class="tengah" style="width:140px">Nilai (0–100)</th>
|
<th class="tengah" style="width:140px">Nilai (0–100)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for m in grid %}
|
{% for m in grid %}
|
||||||
<tr>
|
<tr data-kelas="{{ m.kelas or '' }}">
|
||||||
<td class="tengah">{{ loop.index }}
|
<td class="tengah">{{ loop.index }}
|
||||||
<input type="hidden" name="nim_{{ loop.index0 }}" value="{{ m.nim }}">
|
<input type="hidden" name="nim_{{ loop.index0 }}" value="{{ m.nim }}">
|
||||||
<input type="hidden" name="nama_{{ loop.index0 }}" value="{{ m.nama }}"></td>
|
<input type="hidden" name="nama_{{ loop.index0 }}" value="{{ m.nama }}"></td>
|
||||||
<td>{{ m.nim }}</td>
|
<td>{{ m.nim }}</td>
|
||||||
<td>{{ m.nama }}</td>
|
<td>{{ m.nama }}</td>
|
||||||
|
{% if ada_kelas %}<td class="tengah">{{ m.kelas or '—' }}</td>{% endif %}
|
||||||
<td class="isian"><input type="number" name="nilai_{{ loop.index0 }}"
|
<td class="isian"><input type="number" name="nilai_{{ loop.index0 }}"
|
||||||
min="0" max="100" step="any"
|
min="0" max="100" step="any"
|
||||||
value="{{ '%g'|format(m.nilai_awal) if m.nilai_awal is not none else '' }}"></td>
|
value="{{ '%g'|format(m.nilai_awal) if m.nilai_awal is not none else '' }}"></td>
|
||||||
@@ -129,6 +145,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% if ada_kelas %}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var pilih = document.getElementById('filter-kelas');
|
||||||
|
var info = document.getElementById('filter-kelas-info');
|
||||||
|
var baris = document.querySelectorAll('table.grid tbody tr[data-kelas]');
|
||||||
|
|
||||||
|
function perbarui() {
|
||||||
|
var v = pilih.value, n = 0;
|
||||||
|
baris.forEach(function (tr) {
|
||||||
|
var tampil = v === '' || tr.dataset.kelas === v;
|
||||||
|
tr.style.display = tampil ? '' : 'none';
|
||||||
|
if (tampil) n++;
|
||||||
|
});
|
||||||
|
info.textContent = v === '' ? '' : n + ' dari ' + baris.length + ' mahasiswa ditampilkan';
|
||||||
|
}
|
||||||
|
|
||||||
|
pilih.addEventListener('change', perbarui);
|
||||||
|
perbarui();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if komponen.tugas %}
|
{% if komponen.tugas %}
|
||||||
<script>
|
<script>
|
||||||
const BENTUK_TEMPLATE = {
|
const BENTUK_TEMPLATE = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block judul %}Portofolio {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
{% block judul %}Portofolio {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
||||||
{% block isi %}
|
{% block isi %}
|
||||||
<div class="nav-baris">
|
<div class="nav-baris">
|
||||||
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <b>Portofolio</b></div>
|
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <a href="/rps/{{ peta.kode }}/rtm">RTM</a> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <b>Portofolio</b></div>
|
||||||
<div>
|
<div>
|
||||||
{% if status == 'selesai' %}<span class="stamp stamp-ok">SELESAI</span>
|
{% if status == 'selesai' %}<span class="stamp stamp-ok">SELESAI</span>
|
||||||
{% elif status == 'draft' %}<span class="stamp stamp-draft">DRAFT</span>
|
{% elif status == 'draft' %}<span class="stamp stamp-draft">DRAFT</span>
|
||||||
@@ -30,6 +30,90 @@
|
|||||||
<div class="info">RPS ✓ dan Nilai ✓ tersedia — tabel korelasi, penilaian, evaluasi ketercapaian, dan statistik akan diisi otomatis di dokumen. Anda hanya melengkapi bagian di bawah.</div>
|
<div class="info">RPS ✓ dan Nilai ✓ tersedia — tabel korelasi, penilaian, evaluasi ketercapaian, dan statistik akan diisi otomatis di dokumen. Anda hanya melengkapi bagian di bawah.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if analisis %}
|
||||||
|
<details class="cara-alt" style="margin-top:26px;border:1px solid #c9c9c9;padding:10px 14px">
|
||||||
|
<summary>ANALISIS PROSES ASESMEN {% if analisis.berisiko %}<span style="color:var(--red);font-weight:normal;font-size:13px">— {{ analisis.berisiko|length }} mahasiswa berisiko</span>{% endif %}</summary>
|
||||||
|
<p class="keterangan">Dihitung otomatis dari RPS + Nilai tersimpan — bahan diskusi sebelum menulis Realisasi & Simpulan di bawah. Hanya tampil di layar, tidak ikut ke dokumen unduhan.</p>
|
||||||
|
|
||||||
|
<div class="perhatian-kisi">
|
||||||
|
<div>
|
||||||
|
<p class="keterangan" style="margin-top:0">Distribusi huruf mutu{% if analisis.statistik %} ({{ analisis.statistik.n }} mahasiswa){% endif %}</p>
|
||||||
|
{% if analisis.statistik %}
|
||||||
|
<table class="grid">
|
||||||
|
<thead><tr><th>Huruf</th><th class="tengah">Jumlah</th><th class="tengah">%</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for h, n in analisis.statistik.frekuensi.items() %}
|
||||||
|
<tr><td>{{ h }}</td><td class="tengah">{{ n }}</td>
|
||||||
|
<td class="tengah">{{ '%.0f'|format(100 * n / analisis.statistik.n) }}%</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="keterangan">Rata-rata {{ '%.1f'|format(analisis.statistik.mean) }} · median {{ '%.1f'|format(analisis.statistik.median) }} · rentang {{ '%.1f'|format(analisis.statistik.min) }}–{{ '%.1f'|format(analisis.statistik.max) }}</p>
|
||||||
|
{% if analisis.kelompok %}
|
||||||
|
<p class="keterangan">Kelompok capaian (mean ± SD, batas {{ '%.1f'|format(analisis.kelompok.batas_bawah) }}–{{ '%.1f'|format(analisis.kelompok.batas_atas) }}):
|
||||||
|
<b style="color:#205020">Atas {{ analisis.kelompok.atas|length }}</b> ·
|
||||||
|
Tengah {{ analisis.kelompok.tengah|length }} ·
|
||||||
|
<b style="color:var(--red)">Bawah {{ analisis.kelompok.bawah|length }}</b>{% if analisis.kelompok.bawah %} ({{ analisis.kelompok.bawah|map(attribute='nama')|join(', ') }}){% endif %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p class="keterangan">Nilai masih mode agregat (rata-rata kelas) — distribusi huruf per mahasiswa tidak bisa dihitung.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="keterangan" style="margin-top:0">Sub-CPMK terlemah & terkuat</p>
|
||||||
|
{% if analisis.sub_urut %}
|
||||||
|
<table class="grid">
|
||||||
|
<thead><tr><th>Sub-CPMK</th><th class="tengah">Skor</th><th class="tengah">Lulus %</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for s in analisis.sub_urut %}
|
||||||
|
<tr><td>{{ s.kode }} — {{ s.uraian }}</td>
|
||||||
|
<td class="tengah"{% if s.rata < analisis.ambang %} style="color:var(--red)"{% endif %}>{{ '%.1f'|format(s.rata) }}</td>
|
||||||
|
<td class="tengah">{{ '%.0f'|format(s.lulus_pct) if s.lulus_pct is not none else '—' }}{% if s.lulus_pct is not none %}%{% endif %}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="keterangan">Belum ada Sub-CPMK ternilai.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if analisis.kesenjangan %}
|
||||||
|
<p class="keterangan" style="margin-top:14px"><b>Kesenjangan rencana vs realisasi:</b> {{ analisis.kesenjangan|length }} Sub-CPMK diberi bobot di RPS tapi belum ada nilai masuk — {{ analisis.kesenjangan|join(', ') }}.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if analisis.berisiko %}
|
||||||
|
<p class="keterangan" style="margin-top:14px">Mahasiswa berisiko — ≥2 Sub-CPMK di bawah ambang ({{ analisis.ambang|int }}), atau nilai akhir sendiri di bawahnya</p>
|
||||||
|
<table class="grid">
|
||||||
|
<thead><tr><th>NIM</th><th>Nama</th><th class="tengah">Sub-CPMK di bawah ambang</th><th class="tengah">Nilai Akhir</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for m in analisis.berisiko %}
|
||||||
|
<tr><td>{{ m.nim }}</td><td>{{ m.nama }}</td>
|
||||||
|
<td class="tengah">{{ m.n_dibawah }}/{{ m.total_sub }}</td>
|
||||||
|
<td class="tengah" style="color:var(--red)">{{ '%.1f'|format(m.akhir) if m.akhir is not none else '—' }}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if analisis.tren %}
|
||||||
|
<p class="keterangan" style="margin-top:14px">Tren MK ini lintas tahun ajaran — apakah capaian membaik atau tidak</p>
|
||||||
|
<table class="grid">
|
||||||
|
<thead><tr><th>Tahun Ajaran</th><th class="tengah">Rata-rata</th><th class="tengah">% Kelulusan</th><th class="tengah">n Mahasiswa</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for t in analisis.tren %}
|
||||||
|
<tr{% if t.tahun == tahun %} class="total"{% endif %}>
|
||||||
|
<td>{{ t.tahun }}</td>
|
||||||
|
<td class="tengah">{{ '%.1f'|format(t.mean) if t.mean is not none else '—' }}</td>
|
||||||
|
<td class="tengah">{{ '%.0f'|format(t.lulus_pct) if t.lulus_pct is not none else '—' }}{% if t.lulus_pct is not none %}%{% endif %}</td>
|
||||||
|
<td class="tengah">{{ t.n if t.n is not none else '—' }}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" action="/portofolio/{{ peta.kode }}">
|
<form method="post" action="/portofolio/{{ peta.kode }}">
|
||||||
<h2 class="bagian">IDENTITAS DOKUMEN (LEMBAR PENGESAHAN)</h2>
|
<h2 class="bagian">IDENTITAS DOKUMEN (LEMBAR PENGESAHAN)</h2>
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
@@ -49,24 +133,34 @@
|
|||||||
|
|
||||||
<h2 class="bagian">REALISASI PEMBELAJARAN</h2>
|
<h2 class="bagian">REALISASI PEMBELAJARAN</h2>
|
||||||
<p class="keterangan">Ceritakan pelaksanaan nyata semester ini: penyesuaian jadwal/materi/metode terhadap rencana di RPS, kehadiran, dan hal penting lain.</p>
|
<p class="keterangan">Ceritakan pelaksanaan nyata semester ini: penyesuaian jadwal/materi/metode terhadap rencana di RPS, kehadiran, dan hal penting lain.</p>
|
||||||
<textarea name="realisasi" rows="6">{{ f.realisasi }}</textarea>
|
<textarea name="realisasi" rows="6" placeholder="Contoh: - Materi minggu 5–6 (rekursi) tertunda 1 minggu karena libur nasional, digabung jadi 1 pertemuan - Praktikum awalnya direncanakan luring, dialihkan ke daring minggu 3–4 karena lab dalam perbaikan - Rata-rata kehadiran mahasiswa, jumlah pertemuan efektif dari 16 minggu rencana - Metode yang berubah di lapangan (mis. tugas kelompok jadi individu karena jumlah mahasiswa sedikit)">{{ f.realisasi }}</textarea>
|
||||||
|
|
||||||
<h2 class="bagian">KENDALA & TINDAK LANJUT</h2>
|
<h2 class="bagian">KENDALA & TINDAK LANJUT</h2>
|
||||||
|
{% set contoh_kendala = [
|
||||||
|
"Banyak mahasiswa kesulitan di konsep rekursi (Sub-CPMK X)",
|
||||||
|
"Partisipasi diskusi daring rendah",
|
||||||
|
"Jadwal praktikum bentrok dengan MK lain"] %}
|
||||||
|
{% set contoh_tindak = [
|
||||||
|
"Ditambah sesi responsi/tutorial tambahan minggu 9",
|
||||||
|
"Kuis singkat wajib tiap sesi untuk dorong keterlibatan",
|
||||||
|
"Diusulkan penyesuaian jadwal ke prodi semester depan"] %}
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
<thead><tr><th style="width:50px" class="tengah">No.</th><th>Kendala</th><th>Tindak Lanjut</th></tr></thead>
|
<thead><tr><th style="width:50px" class="tengah">No.</th><th>Kendala</th><th>Tindak Lanjut</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for k in f.kendala[:5] %}
|
{% for k in f.kendala[:5] %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="tengah">{{ loop.index }}</td>
|
<td class="tengah">{{ loop.index }}</td>
|
||||||
<td class="isian"><textarea name="kendala_{{ loop.index }}" rows="2">{{ k.kendala }}</textarea></td>
|
<td class="isian"><textarea name="kendala_{{ loop.index }}" rows="2" placeholder="{{ contoh_kendala[loop.index0] if loop.index0 < contoh_kendala|length else '' }}">{{ k.kendala }}</textarea></td>
|
||||||
<td class="isian"><textarea name="tindak_{{ loop.index }}" rows="2">{{ k.tindak }}</textarea></td>
|
<td class="isian"><textarea name="tindak_{{ loop.index }}" rows="2" placeholder="{{ contoh_tindak[loop.index0] if loop.index0 < contoh_tindak|length else '' }}">{{ k.tindak }}</textarea></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2 class="bagian">SIMPULAN HASIL EVALUASI</h2>
|
<h2 class="bagian">SIMPULAN HASIL EVALUASI</h2>
|
||||||
<p class="keterangan">Statistik nilai, frekuensi huruf mutu, dan tabel ketercapaian CPL/CPMK dihitung dan dicetak otomatis; tuliskan simpulan naratifnya di sini.</p>
|
<p class="keterangan">Statistik nilai, frekuensi huruf mutu, dan tabel ketercapaian CPL/CPMK dihitung dan dicetak otomatis; tuliskan simpulan naratifnya di sini.
|
||||||
|
{% if boleh_edit %}<button type="button" class="tombol garis" style="padding:2px 10px;font-size:12px" onclick="isiSimpulanOtomatis()">Isi Otomatis</button>
|
||||||
|
merangkai draf dari Analisis Proses Asesmen, Realisasi Pembelajaran, dan Kendala & Tindak Lanjut di atas — cek & lengkapi sebelum menyimpan.{% endif %}</p>
|
||||||
<textarea name="simpulan" rows="4">{{ f.simpulan }}</textarea>
|
<textarea name="simpulan" rows="4">{{ f.simpulan }}</textarea>
|
||||||
|
|
||||||
<div class="aksi-baris">
|
<div class="aksi-baris">
|
||||||
@@ -80,4 +174,45 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const SIMPULAN_SARAN = {% if analisis %}{{ analisis.simpulan_saran | tojson | safe }}{% else %}null{% endif %};
|
||||||
|
|
||||||
|
// ambil kalimat pertama (maks `maks` karakter) dari teks bebas — dipakai
|
||||||
|
// untuk mengutip cuplikan Realisasi/Kendala ke dalam draf Simpulan
|
||||||
|
function ringkas(teks, maks) {
|
||||||
|
maks = maks || 160;
|
||||||
|
const kalimat = teks.match(/^.*?[.!?](?=\s|$)/);
|
||||||
|
let dasar = kalimat ? kalimat[0].trim() : teks.trim();
|
||||||
|
if (dasar.length > maks) dasar = teks.trim().slice(0, maks).trim() + '…';
|
||||||
|
return /[.!?…]$/.test(dasar) ? dasar : dasar + '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isiSimpulanOtomatis() {
|
||||||
|
const bagian = [];
|
||||||
|
if (SIMPULAN_SARAN) bagian.push(SIMPULAN_SARAN);
|
||||||
|
|
||||||
|
const elRealisasi = document.querySelector('[name="realisasi"]');
|
||||||
|
const realisasi = elRealisasi ? elRealisasi.value.trim() : '';
|
||||||
|
if (realisasi) bagian.push('Dari sisi pelaksanaan pembelajaran: ' + ringkas(realisasi));
|
||||||
|
|
||||||
|
const kendalaTeks = [];
|
||||||
|
document.querySelectorAll('[name^="kendala_"]').forEach(el => {
|
||||||
|
const v = el.value.trim();
|
||||||
|
if (v) kendalaTeks.push(v);
|
||||||
|
});
|
||||||
|
if (kendalaTeks.length) {
|
||||||
|
const label = kendalaTeks.length > 1
|
||||||
|
? kendalaTeks.length + ' kendala tercatat, utamanya'
|
||||||
|
: 'Kendala utama yang tercatat';
|
||||||
|
bagian.push(label + ': ' + ringkas(kendalaTeks[0])
|
||||||
|
+ ' Tindak lanjutnya sudah direncanakan pada tabel Kendala & Tindak Lanjut di atas.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bagian.length) {
|
||||||
|
bagian.push('Belum ada data Analisis Proses Asesmen, Realisasi Pembelajaran, atau Kendala untuk dirangkai — isi bagian-bagian tersebut dulu, lalu klik Isi Otomatis lagi.');
|
||||||
|
}
|
||||||
|
document.querySelector('textarea[name="simpulan"]').value = bagian.join(' ');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block judul %}RPS {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
{% block judul %}RPS {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
||||||
{% block isi %}
|
{% block isi %}
|
||||||
<div class="nav-baris">
|
<div class="nav-baris">
|
||||||
<div>MK ini: <b>RPS</b> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
<div>MK ini: <b>RPS</b> · <a href="/rps/{{ peta.kode }}/rtm">RTM</a> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
||||||
<div>
|
<div>
|
||||||
<span id="status-stamp">
|
<span id="status-stamp">
|
||||||
{% if status == 'selesai' %}<span class="stamp stamp-ok">SELESAI</span>
|
{% if status == 'selesai' %}<span class="stamp stamp-ok">SELESAI</span>
|
||||||
@@ -178,6 +178,12 @@
|
|||||||
komponen yang tidak dipakai MK ini (opsional), tapi total seluruh komponen yang diisi harus
|
komponen yang tidak dipakai MK ini (opsional), tapi total seluruh komponen yang diisi harus
|
||||||
tepat 100%. Centang Sub-CPMK yang dinilai tiap komponen; setiap Sub-CPMK harus tercakup minimal
|
tepat 100%. Centang Sub-CPMK yang dinilai tiap komponen; setiap Sub-CPMK harus tercakup minimal
|
||||||
satu komponen agar CPMK/CPL-nya terukur.</p>
|
satu komponen agar CPMK/CPL-nya terukur.</p>
|
||||||
|
{% if komponen_tanpa_sub %}
|
||||||
|
<div class="peringatan"><b>Sub-CPMK belum ditautkan ke komponen penilaian</b> — segera centang
|
||||||
|
minimal satu Sub-CPMK untuk: {{ komponen_tanpa_sub|join(', ') }}. Tanpa tautan ini, komponen
|
||||||
|
tersebut <b>tidak akan muncul</b> di halaman Input Nilai meski bobotnya sudah dihitung ke
|
||||||
|
total 100% di bawah.</div>
|
||||||
|
{% endif %}
|
||||||
<div class="gulir">
|
<div class="gulir">
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -342,7 +348,7 @@
|
|||||||
clearTimeout(timerAutosave);
|
clearTimeout(timerAutosave);
|
||||||
timerAutosave = setTimeout(simpanOtomatis, 3000);
|
timerAutosave = setTimeout(simpanOtomatis, 3000);
|
||||||
}
|
}
|
||||||
document.addEventListener('input', jadwalkanAutosave);
|
document.getElementById('form-rps').addEventListener('input', jadwalkanAutosave);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block judul %}RTM {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
||||||
|
{% block isi %}
|
||||||
|
<div class="nav-baris">
|
||||||
|
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <b>RTM</b> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Rencana Tugas Mandiri — {{ peta.nama }} <span style="color:var(--grey)">({{ peta.kode }})</span></h1>
|
||||||
|
|
||||||
|
<p class="keterangan">Dokumen RTM (Rancangan Tugas Mahasiswa) resmi sesuai format baku prodi —
|
||||||
|
<b>berdiri sendiri, tak ada kaitan apa pun dengan Nilai</b> (tidak memuat skor/nilai mahasiswa
|
||||||
|
sama sekali, murni dokumentasi rencana tugas MK ini). Tambah blok Tugas sesuai kebutuhan,
|
||||||
|
isi, lalu unduh sebagai satu berkas .docx.</p>
|
||||||
|
{% if rtm_updated_at %}<p class="keterangan" style="margin-top:-6px">Terakhir disimpan {{ rtm_updated_at }}{% if rtm_updated_by %} oleh {{ rtm_updated_by }}{% endif %}.</p>{% endif %}
|
||||||
|
|
||||||
|
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
|
||||||
|
|
||||||
|
{% if not boleh_edit %}
|
||||||
|
<div class="info">Anda tidak memiliki hak edit RPS untuk MK ini — form terbuka
|
||||||
|
<b>hanya untuk dibaca</b>.</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/rps/{{ peta.kode }}/rtm" id="form-rtm">
|
||||||
|
<fieldset {% if not boleh_edit %}disabled{% endif %} style="border:none;padding:0;margin:0">
|
||||||
|
<div id="daftar-tugas-rtm"></div>
|
||||||
|
<button type="button" class="tombol garis" onclick="tambahTugasRtm()">+ Tambah Tugas</button>
|
||||||
|
</fieldset>
|
||||||
|
<div class="aksi-baris">
|
||||||
|
{% if boleh_edit %}
|
||||||
|
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<button class="tombol" type="submit">Simpan RTM</button>
|
||||||
|
{% else %}
|
||||||
|
<span class="spacer"></span>
|
||||||
|
{% endif %}
|
||||||
|
<a class="tombol garis" href="/rps/{{ peta.kode }}/rtm/unduh">Unduh RTM (.docx)</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="text-align:center;margin-top:16px">
|
||||||
|
<button type="button" class="tombol garis" onclick="window.scrollTo({top:0, behavior:'smooth'})">▲ Ke Atas</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script id="tpl-tugas-rtm" type="text/template">
|
||||||
|
<details style="margin-bottom:12px;border:1px solid var(--border);border-radius:6px;padding:10px 14px">
|
||||||
|
<summary style="cursor:pointer;font-weight:bold"><span class="nomor-tugas-rtm">Tugas</span>
|
||||||
|
<button type="button" class="tombol garis" style="padding:0 8px;font-size:11px;color:var(--red);border-color:var(--red);float:right"
|
||||||
|
onclick="event.preventDefault(); hapusTugasRtm(this)">Hapus Tugas Ini</button>
|
||||||
|
</summary>
|
||||||
|
<div style="margin-top:12px">
|
||||||
|
<label style="display:block;margin-bottom:10px">Judul Tugas
|
||||||
|
<input type="text" name="judul___IDX__" style="display:block;margin-top:4px;max-width:480px">
|
||||||
|
</label>
|
||||||
|
<div style="margin-bottom:10px">
|
||||||
|
<div>Sub Capaian Pembelajaran Mata Kuliah yang Dicapai <span style="font-weight:normal;color:var(--grey)">(opsional — untuk keterlacakan dokumen saja, tak memengaruhi nilai)</span></div>
|
||||||
|
<div class="pilih-sub">
|
||||||
|
{% for s in peta.subs %}
|
||||||
|
<label><input type="checkbox" name="subs___IDX__" value="{{ s.kode }}">{{ s.kode }} — {{ s.uraian }}</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label style="display:block;margin-bottom:10px">Bentuk Tugas
|
||||||
|
<span style="display:flex;align-items:center;gap:8px;margin-top:4px">
|
||||||
|
<select name="bentuk_tugas___IDX__" id="bentuk_tugas___IDX__" onchange="toggleIsiOtomatisRtm('__IDX__')" style="max-width:280px">
|
||||||
|
<option value="">— pilih —</option>
|
||||||
|
{% for opsi in ["Diskusi", "PjBL", "CBL", "Esai", "Tes"] %}
|
||||||
|
<option value="{{ opsi }}">{{ opsi }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button type="button" class="tombol garis" id="tombol_isi_otomatis___IDX__"
|
||||||
|
onclick="isiOtomatisRtm('__IDX__')" hidden>Isi Otomatis</button>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label style="display:block;margin-bottom:10px">Deskripsi Tugas
|
||||||
|
<textarea name="deskripsi___IDX__" id="rtm_deskripsi___IDX__" rows="4" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||||
|
</label>
|
||||||
|
<label style="display:block;margin-bottom:10px">Metode Pengerjaan Tugas
|
||||||
|
<textarea name="metode_pengerjaan___IDX__" id="rtm_metode___IDX__" rows="3" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||||
|
</label>
|
||||||
|
<label style="display:block;margin-bottom:10px">Bentuk dan Format Luaran
|
||||||
|
<textarea name="luaran___IDX__" id="rtm_luaran___IDX__" rows="2" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||||
|
</label>
|
||||||
|
<label style="display:block;margin-bottom:10px">Indikator, Kriteria dan Bobot Penilaian
|
||||||
|
<textarea name="indikator_bobot___IDX__" id="rtm_indikator___IDX__" rows="4" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||||
|
</label>
|
||||||
|
<label style="display:block;margin-bottom:10px">Jadwal Pelaksanaan
|
||||||
|
<span style="display:flex;gap:16px;margin-top:4px;flex-wrap:wrap">
|
||||||
|
<span>Alokasi waktu<input type="text" name="alokasi_waktu___IDX__" id="rtm_alokasi___IDX__" placeholder="mis. 2 minggu" style="display:block;margin-top:2px;width:200px"></span>
|
||||||
|
<span>Tanggal pelaksanaan<input type="text" name="tanggal___IDX__" placeholder="mis. minggu ke-5 s.d. 6" style="display:block;margin-top:2px;width:220px"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label style="display:block;margin-bottom:10px">Lain-lain
|
||||||
|
<textarea name="lain_lain___IDX__" rows="2" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||||
|
</label>
|
||||||
|
<label style="display:block;margin-bottom:0">Daftar Rujukan
|
||||||
|
<textarea name="rujukan___IDX__" rows="2" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const RTM_AWAL = {{ rtm_tugas | tojson | safe }};
|
||||||
|
let rtmCounter = 0;
|
||||||
|
|
||||||
|
const BENTUK_TEMPLATE_RTM = {
|
||||||
|
Diskusi: {
|
||||||
|
deskripsi: 'Diskusikan {SUBCPMK} melalui studi kasus/topik terkait materi ini. Kelompok kecil (3-5 orang) menganalisis permasalahan dan menyusun rekomendasi.',
|
||||||
|
metode: '1) Baca materi & bahan diskusi;\n2) Diskusi kelompok;\n3) Susun poin-poin kesimpulan;\n4) Presentasikan hasil diskusi.',
|
||||||
|
luaran: 'Notulen/rangkuman diskusi kelompok dalam bentuk dokumen tertulis (maks. 3 halaman).',
|
||||||
|
indikator: 'Ketepatan analisis (30%)\nKeaktifan berdiskusi (30%)\nKejelasan kesimpulan (20%)\nKerja sama kelompok (20%)\nBobot: 100%',
|
||||||
|
jadwal: '1 minggu',
|
||||||
|
},
|
||||||
|
PjBL: {
|
||||||
|
deskripsi: 'Kerjakan proyek yang menghasilkan solusi nyata terkait {SUBCPMK}, mulai dari perencanaan hingga penyerahan hasil akhir.',
|
||||||
|
metode: '1) Identifikasi masalah;\n2) Rencanakan solusi;\n3) Kerjakan proyek secara bertahap;\n4) Uji & perbaiki;\n5) Serahkan hasil akhir beserta laporan.',
|
||||||
|
luaran: 'Produk/prototipe beserta laporan tertulis yang menjelaskan proses pengerjaan.',
|
||||||
|
indikator: 'Perencanaan (20%)\nProses pengerjaan (30%)\nKualitas hasil akhir (30%)\nLaporan (20%)\nBobot: 100%',
|
||||||
|
jadwal: '2-3 minggu',
|
||||||
|
},
|
||||||
|
CBL: {
|
||||||
|
deskripsi: 'Analisis studi kasus terkait {SUBCPMK}; identifikasi permasalahan dan usulkan solusi berbasis konsep yang dipelajari.',
|
||||||
|
metode: '1) Baca & pahami kasus;\n2) Identifikasi masalah inti;\n3) Terapkan konsep untuk analisis;\n4) Rumuskan solusi;\n5) Tulis laporan.',
|
||||||
|
luaran: 'Laporan analisis kasus tertulis, memuat identifikasi masalah, analisis, dan solusi yang diusulkan.',
|
||||||
|
indikator: 'Ketepatan identifikasi masalah (25%)\nKedalaman analisis (35%)\nKetepatan solusi (25%)\nSistematika laporan (15%)\nBobot: 100%',
|
||||||
|
jadwal: '1-2 minggu',
|
||||||
|
},
|
||||||
|
Esai: {
|
||||||
|
deskripsi: 'Tulis esai mengenai {SUBCPMK} sepanjang 800-1200 kata, disertai argumen dan referensi yang relevan.',
|
||||||
|
metode: '1) Tentukan tesis/argumen utama;\n2) Kumpulkan referensi pendukung;\n3) Susun kerangka esai;\n4) Tulis draf;\n5) Revisi & finalisasi.',
|
||||||
|
luaran: 'Esai tertulis (800-1200 kata) dalam format dokumen (PDF/Word).',
|
||||||
|
indikator: 'Ketepatan argumen (30%)\nKedalaman analisis (30%)\nSistematika penulisan (20%)\nPenggunaan referensi (20%)\nBobot: 100%',
|
||||||
|
jadwal: '1 minggu',
|
||||||
|
},
|
||||||
|
Tes: {
|
||||||
|
deskripsi: 'Kerjakan soal-soal tes tertulis terkait {SUBCPMK} sesuai instruksi yang diberikan.',
|
||||||
|
metode: '1) Baca instruksi & soal dengan teliti;\n2) Kerjakan sesuai alokasi waktu;\n3) Periksa kembali jawaban sebelum dikumpulkan.',
|
||||||
|
luaran: 'Lembar jawaban tes (tertulis/daring).',
|
||||||
|
indikator: 'Ketepatan jawaban per butir soal, dijumlah sesuai bobot masing-masing.\nBobot: 100%',
|
||||||
|
jadwal: '1x pertemuan (sesuai jadwal tes)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function buatBlokTugasRtm(data) {
|
||||||
|
data = data || {};
|
||||||
|
const idx = rtmCounter++;
|
||||||
|
const html = document.getElementById('tpl-tugas-rtm').innerHTML.split('__IDX__').join(idx);
|
||||||
|
const bungkus = document.createElement('div');
|
||||||
|
bungkus.innerHTML = html.trim();
|
||||||
|
const det = bungkus.firstElementChild;
|
||||||
|
det.querySelector(`[name="judul_${idx}"]`).value = data.judul || '';
|
||||||
|
(data.subs || []).forEach(kode => {
|
||||||
|
const cb = Array.from(det.querySelectorAll(`input[name="subs_${idx}"]`)).find(el => el.value === kode);
|
||||||
|
if (cb) cb.checked = true;
|
||||||
|
});
|
||||||
|
det.querySelector(`#bentuk_tugas_${idx}`).value = data.bentuk_tugas || '';
|
||||||
|
det.querySelector(`#tombol_isi_otomatis_${idx}`).hidden = !data.bentuk_tugas;
|
||||||
|
det.querySelector(`[name="deskripsi_${idx}"]`).value = data.deskripsi || '';
|
||||||
|
det.querySelector(`[name="metode_pengerjaan_${idx}"]`).value = data.metode_pengerjaan || '';
|
||||||
|
det.querySelector(`[name="luaran_${idx}"]`).value = data.luaran || '';
|
||||||
|
det.querySelector(`[name="indikator_bobot_${idx}"]`).value = data.indikator_bobot || '';
|
||||||
|
det.querySelector(`[name="alokasi_waktu_${idx}"]`).value = data.alokasi_waktu || '';
|
||||||
|
det.querySelector(`[name="tanggal_${idx}"]`).value = data.tanggal || '';
|
||||||
|
det.querySelector(`[name="lain_lain_${idx}"]`).value = data.lain_lain || '';
|
||||||
|
det.querySelector(`[name="rujukan_${idx}"]`).value = data.rujukan || '';
|
||||||
|
if (data.judul || data.bentuk_tugas) det.open = true;
|
||||||
|
document.getElementById('daftar-tugas-rtm').appendChild(det);
|
||||||
|
renumberTugasRtm();
|
||||||
|
return det;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tambahTugasRtm() {
|
||||||
|
const det = buatBlokTugasRtm();
|
||||||
|
det.open = true;
|
||||||
|
det.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function hapusTugasRtm(btn) {
|
||||||
|
if (!confirm('Hapus blok Tugas ini?')) return;
|
||||||
|
btn.closest('details').remove();
|
||||||
|
renumberTugasRtm();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renumberTugasRtm() {
|
||||||
|
document.querySelectorAll('#daftar-tugas-rtm > details').forEach((det, i) => {
|
||||||
|
det.querySelector('.nomor-tugas-rtm').textContent = 'Tugas ' + (i + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isiOtomatisRtm(idx) {
|
||||||
|
const bentuk = document.getElementById('bentuk_tugas_' + idx).value;
|
||||||
|
const tmpl = BENTUK_TEMPLATE_RTM[bentuk];
|
||||||
|
if (!tmpl) return;
|
||||||
|
const checked = Array.from(document.querySelectorAll(`input[name="subs_${idx}"]:checked`)).map(cb => cb.value);
|
||||||
|
const subcpmk = checked.length ? checked.join(', ') : 'Sub-CPMK terkait';
|
||||||
|
const isi = (id, teks) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el && !el.value.trim()) el.value = teks.replace('{SUBCPMK}', subcpmk);
|
||||||
|
};
|
||||||
|
isi('rtm_deskripsi_' + idx, tmpl.deskripsi);
|
||||||
|
isi('rtm_metode_' + idx, tmpl.metode);
|
||||||
|
isi('rtm_luaran_' + idx, tmpl.luaran);
|
||||||
|
isi('rtm_indikator_' + idx, tmpl.indikator);
|
||||||
|
isi('rtm_alokasi_' + idx, tmpl.jadwal);
|
||||||
|
}
|
||||||
|
function toggleIsiOtomatisRtm(idx) {
|
||||||
|
const bentuk = document.getElementById('bentuk_tugas_' + idx).value;
|
||||||
|
document.getElementById('tombol_isi_otomatis_' + idx).hidden = !bentuk;
|
||||||
|
}
|
||||||
|
|
||||||
|
(RTM_AWAL.length ? RTM_AWAL : [{}]).forEach(buatBlokTugasRtm);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for c in cpl_urut %}
|
{% for c in cpl_urut %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="tengah"><b>{{ c.kode }}</b></td>
|
<td class="tengah"><b class="kode-hover" title="{{ c.desk }}">{{ c.kode }}</b></td>
|
||||||
<td>{{ c.desk[:130] }}{% if c.desk|length > 130 %}…{% endif %}</td>
|
<td>{{ c.desk[:130] }}{% if c.desk|length > 130 %}…{% endif %}</td>
|
||||||
<td class="tengah">{% if c.skor is not none %}<b>{{ '%.2f'|format(c.skor) }}</b>{% else %}—{% endif %}</td>
|
<td class="tengah">{% if c.skor is not none %}<b>{{ '%.2f'|format(c.skor) }}</b>{% else %}—{% endif %}</td>
|
||||||
<td class="tengah">
|
<td class="tengah">
|
||||||
@@ -66,8 +66,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for c in cpmk_urut %}
|
{% for c in cpmk_urut %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="tengah">{{ c.kode }}</td>
|
<td class="tengah"><span class="kode-hover" title="{{ desk_cpmk.get(c.kode, '') }}">{{ c.kode }}</span></td>
|
||||||
<td class="tengah">{{ c.cpl|join(', ') }}</td>
|
<td class="tengah">{% for cp in c.cpl %}<span class="kode-hover" title="{{ desk_cpl.get(cp, '') }}">{{ cp }}</span>{{ ', ' if not loop.last }}{% endfor %}</td>
|
||||||
<td class="tengah">{% if c.skor is not none %}<b>{{ '%.2f'|format(c.skor) }}</b>{% else %}—{% endif %}</td>
|
<td class="tengah">{% if c.skor is not none %}<b>{{ '%.2f'|format(c.skor) }}</b>{% else %}—{% endif %}</td>
|
||||||
<td class="tengah">{{ c.n_setor }}/{{ c.n_pemikul }}</td>
|
<td class="tengah">{{ c.n_setor }}/{{ c.n_pemikul }}</td>
|
||||||
<td>{% if c.mk_terlemah %}{{ nama_mk.get(c.mk_terlemah[0], c.mk_terlemah[0]) }} ({{ '%.1f'|format(c.mk_terlemah[1]) }}){% else %}—{% endif %}</td>
|
<td>{% if c.mk_terlemah %}{{ nama_mk.get(c.mk_terlemah[0], c.mk_terlemah[0]) }} ({{ '%.1f'|format(c.mk_terlemah[1]) }}){% else %}—{% endif %}</td>
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ def main() -> None:
|
|||||||
raise SystemExit(f"Blok JSON kurikulum tidak ditemukan di {src}")
|
raise SystemExit(f"Blok JSON kurikulum tidak ditemukan di {src}")
|
||||||
data, _ = json.JSONDecoder().raw_decode(html[i:])
|
data, _ = json.JSONDecoder().raw_decode(html[i:])
|
||||||
|
|
||||||
|
# sumber dashboard menyimpan 'cpl' CPMK sebagai string tunggal ("CPL-03")
|
||||||
|
# untuk CPMK yang cuma mendukung 1 CPL — normalisasi jadi list di sini
|
||||||
|
# supaya konsisten dengan pemakai lain (rollup, template |join).
|
||||||
|
for v in data.get("cpmk", {}).values():
|
||||||
|
if isinstance(v.get("cpl"), str):
|
||||||
|
v["cpl"] = [v["cpl"]]
|
||||||
|
|
||||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
OUT.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
|
OUT.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user