# -*- 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 0–100 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 0–100, 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