Files
OBE-Mapping/app/nilai_xlsx.py
T
Power BI Dev 9be6bd03e3 Gabungkan aplikasi RPS & Portofolio OBE (dashboard jadi bagian aplikasi)
Dashboard pemetaan statis kini dilayani aplikasi di /dashboard
(app/static/dashboard.html). Fitur lengkap Tahap 1-4 + dasbor monitoring,
menu global, filter semester, input nilai langsung, penggabungan nilai
team teaching, tabel bisa urut. Dockerfile berubah dari nginx statis ke
uvicorn/FastAPI: healthcheck, proxy-headers, volume /data (SQLite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:05:28 +07:00

208 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Template & parser Excel nilai per mahasiswa per Sub-CPMK."""
import io
import openpyxl
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter
BIRU = "1F4E79"
BIRU_MUDA = "D6E4F0"
def buat_template(peta: dict, tahun: str, mahasiswa: list[dict] | None = None) -> bytes:
"""Template xlsx: kolom NPM, Nama, Kelas, lalu satu kolom per Sub-CPMK.
Bila daftar mahasiswa (impor kaprodi) tersedia, baris NPM/Nama/Kelas
sudah terisi — dosen tinggal mengisi skornya.
"""
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Nilai"
subs = peta["subs"]
ws.append([f"NILAI PER SUB-CPMK — {peta['nama']} ({peta['kode']}) — {tahun}"])
ws.append(["Petunjuk: isi skor 0100 per mahasiswa per Sub-CPMK. "
"Jangan mengubah baris kode Sub-CPMK."])
ws.append([])
header = ["NPM", "Nama Mahasiswa", "Kelas"] + [s["kode"] for s in subs]
ws.append(header)
uraian = ["", "", ""] + [s["uraian"][:80] for s in subs]
ws.append(uraian)
for m in mahasiswa or []:
ws.append([m["npm"], m["nama"], m.get("kelas", "")])
for ci, _ in enumerate(header, 1):
c = ws.cell(row=4, column=ci)
c.font = Font(bold=True, color="FFFFFF")
c.fill = PatternFill("solid", fgColor=BIRU)
c.alignment = Alignment(horizontal="center")
u = ws.cell(row=5, column=ci)
u.font = Font(italic=True, size=8)
u.alignment = Alignment(wrap_text=True, vertical="top")
ws.column_dimensions[get_column_letter(ci)].width = (
14 if ci == 1 else 28 if ci == 2 else 9 if ci == 3 else 14)
ws.freeze_panes = "D6"
buf = io.BytesIO()
wb.save(buf)
return buf.getvalue()
def template_mahasiswa(daftar_mk: list[dict], tahun: str) -> bytes:
"""Template impor daftar mahasiswa: NPM | Nama | Kode MK | Kelas."""
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Mahasiswa"
ws.append([f"DAFTAR MAHASISWA MENGONTRAK MK — {tahun}"])
ws.append(["Petunjuk: satu baris per kontrak (mahasiswa yang mengambil 5 MK "
"berarti 5 baris). Kode MK harus sesuai sheet 'Kode MK'."])
ws.append([])
ws.append(["NPM", "Nama Mahasiswa", "Kode MK", "Kelas"])
for ci, lebar in enumerate((16, 30, 14, 10), 1):
c = ws.cell(row=4, column=ci)
c.font = Font(bold=True, color="FFFFFF")
c.fill = PatternFill("solid", fgColor=BIRU)
c.alignment = Alignment(horizontal="center")
ws.column_dimensions[get_column_letter(ci)].width = lebar
ws.freeze_panes = "A5"
rujukan = wb.create_sheet("Kode MK")
rujukan.append(["Kode MK", "Nama Mata Kuliah"])
for ci in (1, 2):
c = rujukan.cell(row=1, column=ci)
c.font = Font(bold=True, color="FFFFFF")
c.fill = PatternFill("solid", fgColor=BIRU)
for mk in daftar_mk:
rujukan.append([mk["kode"], mk["nama"]])
rujukan.column_dimensions["A"].width = 14
rujukan.column_dimensions["B"].width = 46
buf = io.BytesIO()
wb.save(buf)
return buf.getvalue()
def parse_mahasiswa(isi: bytes, kode_mk_valid: set[str]) -> tuple[list[dict], list[str]]:
"""Baca xlsx daftar mahasiswa → (baris kontrak, peringatan).
Header dicari otomatis (baris yang memuat 'NPM'); kolom dikenali dari
judulnya: NPM, Nama, Kode MK, Kelas.
"""
wb = openpyxl.load_workbook(io.BytesIO(isi), read_only=True, data_only=True)
ws = wb["Mahasiswa"] if "Mahasiswa" in wb.sheetnames else wb.active
rows = list(ws.iter_rows(values_only=True))
kol = {}
header_i = None
for i, row in enumerate(rows):
vals = [str(v).strip().lower() if v is not None else "" for v in row]
if "npm" in vals:
header_i = i
for ci, v in enumerate(vals):
if v == "npm":
kol["npm"] = ci
elif "nama" in v:
kol.setdefault("nama", ci)
elif "kode" in v or v == "mk":
kol.setdefault("mk", ci)
elif "kelas" in v:
kol.setdefault("kelas", ci)
break
if header_i is None or "npm" not in kol or "mk" not in kol:
return [], ["Header tidak dikenali: pastikan ada kolom NPM, Nama, "
"Kode MK, dan Kelas (pakai template dari aplikasi)."]
def ambil(vals, kunci):
ci = kol.get(kunci)
if ci is None or ci >= len(vals) or vals[ci] is None:
return ""
v = vals[ci]
# NPM/kelas numerik dari Excel → buang '.0'
if isinstance(v, float) and v.is_integer():
v = int(v)
return str(v).strip()
baris, peringatan, tak_dikenal = [], [], set()
terlihat = set()
for row in rows[header_i + 1:]:
vals = list(row)
npm = ambil(vals, "npm")
mk = ambil(vals, "mk").upper()
if not npm or not mk:
continue
if mk not in kode_mk_valid:
tak_dikenal.add(mk)
continue
if (npm, mk) in terlihat:
continue
terlihat.add((npm, mk))
baris.append({"npm": npm, "nama": ambil(vals, "nama"),
"mk_kode": mk, "kelas": ambil(vals, "kelas")})
if tak_dikenal:
peringatan.append(
f"{len(tak_dikenal)} kode MK tidak dikenal dan dilewati: "
+ ", ".join(sorted(tak_dikenal)[:6])
+ ("…" if len(tak_dikenal) > 6 else ""))
if not baris:
peringatan.append("Tidak ada baris kontrak yang terbaca.")
return baris, peringatan
def parse_unggahan(isi: bytes, kode_sub_valid: set[str]) -> tuple[list[dict], list[str]]:
"""Baca xlsx nilai → (mahasiswa, peringatan).
Baris header dicari otomatis: baris yang memuat 'NPM'. Kolom Sub-CPMK
dicocokkan dengan kode yang valid untuk MK tsb.
"""
wb = openpyxl.load_workbook(io.BytesIO(isi), read_only=True, data_only=True)
ws = wb.active
rows = list(ws.iter_rows(values_only=True))
header_i, kolom_sub = None, {}
for i, row in enumerate(rows):
vals = [str(v).strip() if v is not None else "" for v in row]
if "NPM" in vals:
header_i = i
for ci, v in enumerate(vals):
if v in kode_sub_valid:
kolom_sub[ci] = v
npm_i = vals.index("NPM")
nama_i = npm_i + 1
break
if header_i is None or not kolom_sub:
return [], ["Header tidak dikenali: pastikan memakai template dari aplikasi "
"(ada kolom NPM dan kode Sub-CPMK)."]
mahasiswa, peringatan = [], []
for row in rows[header_i + 1:]:
vals = list(row)
npm = str(vals[npm_i]).strip() if npm_i < len(vals) and vals[npm_i] is not None else ""
nama = str(vals[nama_i]).strip() if nama_i < len(vals) and vals[nama_i] is not None else ""
skor = {}
for ci, sub in kolom_sub.items():
v = vals[ci] if ci < len(vals) else None
if v is None or v == "":
continue
try:
x = float(v)
except (TypeError, ValueError):
continue
if 0 <= x <= 100:
skor[sub] = x
else:
peringatan.append(f"{npm or nama}: nilai {sub} = {x} di luar 0100, dilewati.")
if skor and (npm or nama):
mahasiswa.append({"npm": npm, "nama": nama, "skor": skor})
if not mahasiswa:
peringatan.append("Tidak ada baris nilai yang terbaca.")
hilang = kode_sub_valid - {s for m in mahasiswa for s in m["skor"]}
if mahasiswa and hilang:
peringatan.append(
f"{len(hilang)} Sub-CPMK tanpa nilai sama sekali: "
+ ", ".join(sorted(hilang)[:8]) + ("…" if len(hilang) > 8 else "")
)
return mahasiswa, peringatan