afb9e72ba2
- Halaman nilai: tombol isi-cepat per komponen penilaian (bagian G RPS) yang menyalin nilai ke seluruh Sub-CPMK terkait; komponen berulang (Kuis, Tugas, Presentasi, Tes Lisan) mendukung banyak sesi dengan rata-rata otomatis, komponen sekali-jalan (UTS/UAS) mengedit sesi yang sama. Sub-CPMK yang diukur >1 komponen digulung rata tertimbang bobot (hitung.skor_dari_sesi). - Hapus jalur unggah Excel & mode "ketik rata-rata kelas" (agregat) dari input nilai — proses diseragamkan: hanya grid manual + tombol komponen, keduanya bersumber dari daftar mahasiswa impor kaprodi. - Dasbor Monitoring, Rekap CPL/CPMK, dan Riwayat kini khusus kaprodi (dosen tidak lagi melihat rollup lintas-MK yang bukan miliknya). - Penugasan dosen ke MK bisa dibatasi per kelas (kolom kelas baru di tabel penugasan, migrasi otomatis & aman untuk DB yang sudah berjalan) — dosen MK dengan banyak kelas paralel hanya melihat & menilai kelasnya sendiri; hapus nilai MK penuh dikunci untuk dosen yang dibatasi kelas.
112 lines
4.0 KiB
Python
112 lines
4.0 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: 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
|