Tambah asesmen per komponen, batasi Dasbor/Rekap ke kaprodi, penugasan per kelas
- 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.
This commit is contained in:
@@ -10,45 +10,6 @@ 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()
|
||||
@@ -148,60 +109,3 @@ def parse_mahasiswa(isi: bytes, kode_mk_valid: set[str]) -> tuple[list[dict], li
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user