Files
OBE-Mapping/app/nilai_xlsx.py
Power BI Dev 5f615aa7e8 Rapikan tampilan nilai, ganti NPM->NIM, fitur ganti password, kunci RPS view-only
- Tampilan "Isi Per Komponen Penilaian" diganti dari tombol bertumpuk teks
  jadi tabel (Komponen/Sub-CPMK/Bobot/Status/Aksi) dengan status berupa
  stempel, lebih mudah dipindai.
- Filter pencarian MK (ketik kode/nama) di kotak penugasan dosen /admin.
- Istilah NPM diseragamkan jadi NIM di seluruh aplikasi (label, header,
  template Excel, dokumen portofolio), termasuk migrasi kolom database
  mahasiswa.npm -> nim yang aman (ALTER TABLE RENAME COLUMN, otomatis saat
  startup, tanpa kehilangan data; parser Excel tetap terima file lama
  berheader NPM).
- Fitur ganti password mandiri (/akun): verifikasi password lama, sesi lain
  otomatis keluar setelah berhasil, sesi yang dipakai tetap aktif.
- RPS: dosen RPS+MK kini bisa menyesuaikan redaksi kalimat Sub-CPMK (bagian
  D) untuk MK yang diampu -- kode & kaitan CPMK/CPL tetap dari kurikulum,
  hanya teksnya yang bisa disesuaikan. Penyesuaian otomatis konsisten di
  dropdown mingguan (F), checklist komponen (G), serta unduhan RPS &
  portofolio, tanpa menyentuh data kurikulum induk atau halaman nilai/
  dashboard prodi.
- Form RPS dibungkus <fieldset disabled> untuk dosen tanpa hak edit --
  seluruh field benar-benar terkunci (bukan cuma tombol simpan hilang).
2026-07-22 08:37:54 +07:00

112 lines
4.1 KiB
Python

# -*- 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 template_mahasiswa(daftar_mk: list[dict], tahun: str) -> bytes:
"""Template impor daftar mahasiswa: NIM | 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(["NIM", "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 'NIM'); kolom dikenali dari
judulnya: NIM, 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 "nim" in vals or "npm" in vals:
header_i = i
for ci, v in enumerate(vals):
if v in ("nim", "npm"): # 'npm' — kompatibilitas file lama
kol["nim"] = 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 "nim" not in kol or "mk" not in kol:
return [], ["Header tidak dikenali: pastikan ada kolom NIM, 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]
# NIM/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)
nim = ambil(vals, "nim")
mk = ambil(vals, "mk").upper()
if not nim or not mk:
continue
if mk not in kode_mk_valid:
tak_dikenal.add(mk)
continue
if (nim, mk) in terlihat:
continue
terlihat.add((nim, mk))
baris.append({"nim": nim, "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