From 5f615aa7e8e9ca25519deec9f694ba479156900d Mon Sep 17 00:00:00 2001 From: Power BI Dev Date: Wed, 22 Jul 2026 08:37:54 +0700 Subject: [PATCH] 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
untuk dosen tanpa hak edit -- seluruh field benar-benar terkunci (bukan cuma tombol simpan hilang). --- app/db.py | 20 ++-- app/generator/portofolio_docx.py | 8 +- app/hitung.py | 24 ++--- app/main.py | 122 ++++++++++++++++++------- app/nilai_xlsx.py | 30 +++--- app/static/tabel.js | 17 ++++ app/templates/admin.html | 17 ++-- app/templates/base.html | 1 + app/templates/form_nilai.html | 8 +- app/templates/form_nilai_komponen.html | 6 +- app/templates/form_rps.html | 8 +- app/templates/ganti_password.html | 25 +++++ 12 files changed, 199 insertions(+), 87 deletions(-) create mode 100644 app/templates/ganti_password.html diff --git a/app/db.py b/app/db.py index 12b11a8..750abbf 100644 --- a/app/db.py +++ b/app/db.py @@ -138,6 +138,13 @@ def _migrasi_role_pengguna(conn: sqlite3.Connection) -> None: conn.execute("DROP TABLE pengguna_lama") +def _migrasi_nim_mahasiswa(conn: sqlite3.Connection) -> None: + """Kolom lama bernama 'npm' — istilah baku adalah NIM, bukan NPM.""" + kolom = {r["name"] for r in conn.execute("PRAGMA table_info(mahasiswa)")} + if "npm" in kolom and "nim" not in kolom: + conn.execute("ALTER TABLE mahasiswa RENAME COLUMN npm TO nim") + + def _migrasi_kelas_penugasan(conn: sqlite3.Connection) -> None: """Skema lama penugasan hanya (user_id, mk_kode) — tambah kolom kelas (kosong = seluruh kelas MK ini, dipakai dosen_rps_mk; terisi = dosen_mk @@ -207,14 +214,15 @@ def init_db() -> None: _migrasi_kelas_penugasan(conn) conn.execute( """CREATE TABLE IF NOT EXISTS mahasiswa ( - npm TEXT NOT NULL, + nim TEXT NOT NULL, nama TEXT NOT NULL, mk_kode TEXT NOT NULL, kelas TEXT NOT NULL DEFAULT '', tahun TEXT NOT NULL, - PRIMARY KEY (npm, mk_kode, tahun) + PRIMARY KEY (nim, mk_kode, tahun) )""" ) + _migrasi_nim_mahasiswa(conn) # ── pengaturan prodi ───────────────────────────────────────────────────────── @@ -507,9 +515,9 @@ def ganti_mahasiswa(baris: list[dict], tahun: str | None = None) -> None: with get_conn() as conn: conn.execute("DELETE FROM mahasiswa WHERE tahun=?", (tahun,)) conn.executemany( - "INSERT OR REPLACE INTO mahasiswa (npm, nama, mk_kode, kelas, tahun) " + "INSERT OR REPLACE INTO mahasiswa (nim, nama, mk_kode, kelas, tahun) " "VALUES (?,?,?,?,?)", - [(b["npm"], b["nama"], b["mk_kode"], b.get("kelas", ""), tahun) + [(b["nim"], b["nama"], b["mk_kode"], b.get("kelas", ""), tahun) for b in baris], ) @@ -518,8 +526,8 @@ def mahasiswa_mk(mk_kode: str, tahun: str | None = None) -> list[dict]: tahun = tahun or tahun_aktif() with get_conn() as conn: rows = conn.execute( - "SELECT npm, nama, kelas FROM mahasiswa WHERE mk_kode=? AND tahun=? " - "ORDER BY kelas, npm", + "SELECT nim, nama, kelas FROM mahasiswa WHERE mk_kode=? AND tahun=? " + "ORDER BY kelas, nim", (mk_kode, tahun), ).fetchall() return [dict(r) for r in rows] diff --git a/app/generator/portofolio_docx.py b/app/generator/portofolio_docx.py index d4dfeef..cac1041 100644 --- a/app/generator/portofolio_docx.py +++ b/app/generator/portofolio_docx.py @@ -265,12 +265,12 @@ def build_portofolio(d: dict) -> bytes: skor_sub = d.get("skor_sub", {}) if nilai.get("mode") == "per_mahasiswa": mhs = nilai.get("mahasiswa", []) - akhir = {a["npm"]: a["akhir"] for a in d.get("akhir", [])} + akhir = {a["nim"]: a["akhir"] for a in d.get("akhir", [])} kol_sub = [s["kode"] for s in subs] tf = doc.add_table(rows=2 + len(mhs) + 3, cols=3 + len(kol_sub) + 1) _borders(tf) _hdr(tf.cell(0, 0), "No.") - _hdr(tf.cell(0, 1), "NPM") + _hdr(tf.cell(0, 1), "NIM") _hdr(tf.cell(0, 2), "Nama") for j, sk in enumerate(kol_sub): _hdr(tf.cell(0, 3 + j), sk, size=7) @@ -285,13 +285,13 @@ def build_portofolio(d: dict) -> bytes: align=WD_ALIGN_PARAGRAPH.CENTER) for i, m in enumerate(mhs, 2): _ct(tf.cell(i, 0), str(i - 1), size=8, align=WD_ALIGN_PARAGRAPH.CENTER) - _ct(tf.cell(i, 1), m.get("npm", ""), size=8) + _ct(tf.cell(i, 1), m.get("nim", ""), size=8) _ct(tf.cell(i, 2), m.get("nama", ""), size=8) for j, sk in enumerate(kol_sub): v = (m.get("skor") or {}).get(sk) _ct(tf.cell(i, 3 + j), _f(v, 0) if v is not None else "", size=8, align=WD_ALIGN_PARAGRAPH.CENTER) - _ct(tf.cell(i, 3 + len(kol_sub)), _f(akhir.get(m.get("npm", "")), 1), + _ct(tf.cell(i, 3 + len(kol_sub)), _f(akhir.get(m.get("nim", "")), 1), size=8, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER) # baris statistik per sub base = 2 + len(mhs) diff --git a/app/hitung.py b/app/hitung.py index bf5f92b..7acbf52 100644 --- a/app/hitung.py +++ b/app/hitung.py @@ -50,7 +50,7 @@ def skor_sub(nilai_data: dict, ambang: float) -> dict: """Skor per Sub-CPMK dari data nilai tersimpan. nilai_data = {"mode": "per_mahasiswa"|"agregat", - "mahasiswa": [{"npm","nama","skor":{sub: nilai}}] | + "mahasiswa": [{"nim","nama","skor":{sub: nilai}}] | "agregat": {sub: rata}} → {sub: {"rata": x, "n": jumlah_mhs, "lulus_pct": y|None}} """ @@ -92,7 +92,7 @@ def bobot_sub_dari_rps(rps_data: dict | None) -> dict[str, float]: def skor_dari_sesi(komponen_daftar: list[dict], sesi: dict) -> dict[str, dict[str, float]]: - """npm -> {sub: skor}, dihitung dari log sesi input per-komponen (tombol + """nim -> {sub: skor}, dihitung dari log sesi input per-komponen (tombol 'isi per komponen' di halaman nilai): rata sederhana antar sesi dalam komponen yang sama (mis. beberapa kali Kuis), lalu rata tertimbang bobot antar-komponen bila satu Sub-CPMK diukur oleh >1 komponen (mis. Kuis dan @@ -108,12 +108,12 @@ def skor_dari_sesi(komponen_daftar: list[dict], sesi: dict) -> dict[str, dict[st continue if i not in komponen_by_i or not daftar_sesi: continue - per_npm: dict[str, list[float]] = {} + per_nim: dict[str, list[float]] = {} for s in daftar_sesi: - for npm, v in (s.get("nilai") or {}).items(): + for nim, v in (s.get("nilai") or {}).items(): if v is not None: - per_npm.setdefault(npm, []).append(float(v)) - per_komponen_avg[i] = {npm: _rata(xs) for npm, xs in per_npm.items()} + per_nim.setdefault(nim, []).append(float(v)) + per_komponen_avg[i] = {nim: _rata(xs) for nim, xs in per_nim.items()} kontribusi_per_sub: dict[str, list[tuple[float, dict]]] = {} for i, avg_map in per_komponen_avg.items(): @@ -129,15 +129,15 @@ def skor_dari_sesi(komponen_daftar: list[dict], sesi: dict) -> dict[str, dict[st out: dict[str, dict[str, float]] = {} for sub, kontribusi in kontribusi_per_sub.items(): - npms: set[str] = set() + nims: set[str] = set() for _, avg_map in kontribusi: - npms |= avg_map.keys() - for npm in npms: - pasangan = [(w, avg_map[npm]) for w, avg_map in kontribusi if npm in avg_map] + nims |= avg_map.keys() + for nim in nims: + pasangan = [(w, avg_map[nim]) for w, avg_map in kontribusi if nim in avg_map] total_w = sum(w for w, _ in pasangan) nilai = (sum(w * v for w, v in pasangan) / total_w) if total_w \ else _rata([v for _, v in pasangan]) - out.setdefault(npm, {})[sub] = nilai + out.setdefault(nim, {})[sub] = nilai return out @@ -154,7 +154,7 @@ def nilai_akhir_mahasiswa(nilai_data: dict, bobot_sub: dict[str, float]) -> list else: # RPS belum mengisi bobot → rata-rata sederhana akhir = _rata([v for _, v in pasangan]) if akhir is not None: - out.append({"npm": m.get("npm", ""), "nama": m.get("nama", ""), + out.append({"nim": m.get("nim", ""), "nama": m.get("nama", ""), "akhir": akhir}) return out diff --git a/app/main.py b/app/main.py index b4b7255..67c874a 100644 --- a/app/main.py +++ b/app/main.py @@ -74,17 +74,17 @@ def _kelas_dosen(user: dict, kode: str) -> set[str] | None: return db.kelas_diampu(user["id"], kode) -def _npm_diizinkan(kode: str, kelas_dosen: set[str] | None) -> set[str] | None: - """NPM yang boleh ditulis dosen ini. None = tak dibatasi (semua NPM MK ini).""" +def _nim_diizinkan(kode: str, kelas_dosen: set[str] | None) -> set[str] | None: + """NIM yang boleh ditulis dosen ini. None = tak dibatasi (semua NIM MK ini).""" if kelas_dosen is None: return None - return {m["npm"] for m in db.mahasiswa_mk(kode) if m.get("kelas") in kelas_dosen} + return {m["nim"] for m in db.mahasiswa_mk(kode) if m.get("kelas") in kelas_dosen} -def _masuk(user_id: int) -> RedirectResponse: +def _masuk(user_id: int, tujuan: str = "/") -> RedirectResponse: token = auth.token_baru() db.buat_sesi(user_id, token) - r = RedirectResponse("/", status_code=303) + r = RedirectResponse(tujuan, status_code=303) r.set_cookie("sesi", token, httponly=True, samesite="lax", max_age=60 * 60 * 24 * 30) return r @@ -148,6 +148,30 @@ def keluar(request: Request): return r +@app.get("/akun", response_class=HTMLResponse) +def ganti_password_form(request: Request, pesan: str = "", error: str = ""): + return render(request, "ganti_password.html", {"pesan": pesan, "error": error}) + + +@app.post("/akun") +async def ganti_password_simpan(request: Request): + form = await request.form() + lama = form.get("lama") or "" + baru = form.get("baru") or "" + ulang = form.get("ulang") or "" + user = db.pengguna_by_id(request.state.user["id"]) + if not user or not auth.verifikasi_password(lama, user["pw_hash"]): + return render(request, "ganti_password.html", {"error": "Password lama salah."}) + if len(baru) < 4: + return render(request, "ganti_password.html", + {"error": "Password baru minimal 4 karakter."}) + if baru != ulang: + return render(request, "ganti_password.html", + {"error": "Konfirmasi password baru tidak cocok dengan password baru."}) + db.ubah_password(user["id"], auth.hash_password(baru)) + return _masuk(user["id"], "/akun?pesan=Password+berhasil+diubah.") + + # ── admin kaprodi: akun, penugasan MK, daftar mahasiswa ───────────────────── @@ -330,9 +354,27 @@ def form_kosong() -> dict: "komponen": [dict(k) for k in KOMPONEN_DEFAULT], "referensi_teks": "", "referensi_pendukung_teks": "", + "sub_uraian_custom": {}, } +def _terapkan_override_sub(peta: dict, rps_data: dict | None) -> dict: + """Salinan `peta` dengan uraian Sub-CPMK ditimpa override tersimpan di RPS + MK ini (bila ada) — dipakai konsisten di form RPS, unduhan RPS, dan + portofolio. Kode, CPMK/CPL yang didukung, dan struktur pemetaan kurikulum + TIDAK ikut berubah — dosen hanya boleh menyesuaikan redaksi kalimatnya, + bukan struktur/kaitannya (itu tetap kewenangan kurikulum prodi).""" + override = (rps_data or {}).get("sub_uraian_custom") or {} + if not override: + return peta + peta = dict(peta) + peta["subs"] = [ + {**s, "uraian": override[s["kode"]]} if override.get(s["kode"]) else s + for s in peta["subs"] + ] + return peta + + @app.get("/", response_class=HTMLResponse) def beranda(request: Request, semua: int = 0): daftar = db.daftar_mk() @@ -385,6 +427,7 @@ def form_rps(request: Request, kode: str, pesan: str = ""): if info.get("semester"): f["identitas"]["semester"] = str(info["semester"]) ctx_status = {"status": "", "updated_at": "", "updated_by": ""} + peta = _terapkan_override_sub(peta, f) return render( request, "form_rps.html", {"peta": peta, "f": f, "status": ctx_status["status"], @@ -461,6 +504,12 @@ async def simpan_rps(request: Request, kode: str): pendukung_teks = (form.get("referensi_pendukung") or "").strip() pendukung = [r.strip() for r in pendukung_teks.splitlines() if r.strip()] + sub_uraian_custom = {} + for s in peta["subs"]: + teks = (form.get(f"sub_uraian_{s['kode']}") or "").strip() + if teks and teks != s["uraian"]: + sub_uraian_custom[s["kode"]] = teks + data = { "identitas": identitas, "deskripsi": (form.get("deskripsi") or "").strip(), @@ -472,6 +521,7 @@ async def simpan_rps(request: Request, kode: str): "referensi_teks": referensi_teks, "referensi_pendukung": pendukung, "referensi_pendukung_teks": pendukung_teks, + "sub_uraian_custom": sub_uraian_custom, } aksi = form.get("aksi") or "draft" status = "selesai" if aksi == "selesai" else "draft" @@ -486,6 +536,7 @@ def unduh_rps(kode: str): if not peta or not tersimpan: return RedirectResponse(f"/rps/{kode}", status_code=303) f = tersimpan["data"] + peta = _terapkan_override_sub(peta, f) # resolusi kode Sub-CPMK → teks uraian untuk dokumen uraian = {s["kode"]: s["uraian"] for s in peta["subs"]} @@ -535,16 +586,16 @@ def _grid_mahasiswa(tersimpan: dict | None, terdaftar: list[dict], skor_map, nama_map = {}, {} if tersimpan and tersimpan["data"].get("mode") == "per_mahasiswa": for m in tersimpan["data"]["mahasiswa"]: - skor_map[m.get("npm", "")] = m.get("skor") or {} - nama_map[m.get("npm", "")] = m.get("nama", "") + skor_map[m.get("nim", "")] = m.get("skor") or {} + nama_map[m.get("nim", "")] = m.get("nama", "") grid, ada = [], set() for m in terdaftar: - grid.append({"npm": m["npm"], "nama": m["nama"], "kelas": m.get("kelas") or "", - "skor": skor_map.get(m["npm"], {})}) - ada.add(m["npm"]) - for npm, sk in skor_map.items(): - if npm not in ada: - grid.append({"npm": npm, "nama": nama_map.get(npm, ""), "kelas": "", + grid.append({"nim": m["nim"], "nama": m["nama"], "kelas": m.get("kelas") or "", + "skor": skor_map.get(m["nim"], {})}) + ada.add(m["nim"]) + for nim, sk in skor_map.items(): + if nim not in ada: + grid.append({"nim": nim, "nama": nama_map.get(nim, ""), "kelas": "", "skor": sk}) if kelas_dosen is not None: grid = [g for g in grid if g["kelas"] in kelas_dosen] @@ -596,7 +647,7 @@ def _konteks_nilai(request, kode, pesan="", peringatan=None): if tersimpan["data"].get("mode") == "per_mahasiswa": rps = db.ambil_rps(kode) bobot_sub = hitung.bobot_sub_dari_rps(rps["data"] if rps else None) - akhir = {a["npm"]: a["akhir"] + akhir = {a["nim"]: a["akhir"] for a in hitung.nilai_akhir_mahasiswa(tersimpan["data"], bobot_sub)} kelas_dosen = _kelas_dosen(request.state.user, kode) terdaftar = db.mahasiswa_mk(kode) @@ -656,7 +707,7 @@ def form_nilai_komponen(request: Request, kode: str, i: int, sesi_id: int = -1, kelas_dosen = _kelas_dosen(request.state.user, kode) grid = _grid_mahasiswa(tersimpan, db.mahasiswa_mk(kode), kelas_dosen) for m in grid: - m["nilai_awal"] = nilai_awal.get(m["npm"]) + m["nilai_awal"] = nilai_awal.get(m["nim"]) return render( request, "form_nilai_komponen.html", @@ -686,19 +737,19 @@ async def simpan_nilai_komponen(request: Request, kode: str, i: int): except ValueError: edit_id = -1 - npm_diizinkan = _npm_diizinkan(kode, _kelas_dosen(request.state.user, kode)) + nim_diizinkan = _nim_diizinkan(kode, _kelas_dosen(request.state.user, kode)) nilai = {} j = 0 - while f"npm_{j}" in form: - npm = (form.get(f"npm_{j}") or "").strip() + while f"nim_{j}" in form: + nim = (form.get(f"nim_{j}") or "").strip() v = (form.get(f"nilai_{j}") or "").strip() - if npm and v and (npm_diizinkan is None or npm in npm_diizinkan): + if nim and v and (nim_diizinkan is None or nim in nim_diizinkan): try: x = float(v) except ValueError: x = None if x is not None and 0 <= x <= 100: - nilai[npm] = x + nilai[nim] = x j += 1 if not nilai: @@ -724,16 +775,16 @@ async def simpan_nilai_komponen(request: Request, kode: str, i: int): komponen_daftar = _komponen_daftar(kode) skor_terhitung = hitung.skor_dari_sesi(komponen_daftar, sesi_semua) - nama_map = {npm: m["nama"] for m in db.mahasiswa_mk(kode) for npm in [m["npm"]]} + nama_map = {nim: m["nama"] for m in db.mahasiswa_mk(kode) for nim in [m["nim"]]} urut = [dict(m, skor=dict(m.get("skor") or {})) for m in lama_data.get("mahasiswa", [])] - per_npm = {m["npm"]: m for m in urut if m.get("npm")} - for npm, subskor in skor_terhitung.items(): - m = per_npm.get(npm) + per_nim = {m["nim"]: m for m in urut if m.get("nim")} + for nim, subskor in skor_terhitung.items(): + m = per_nim.get(nim) if m is None: - m = {"npm": npm, "nama": nama_map.get(npm, ""), "skor": {}} + m = {"nim": nim, "nama": nama_map.get(nim, ""), "skor": {}} urut.append(m) - per_npm[npm] = m + per_nim[nim] = m m["skor"].update(subskor) riwayat = lama_data.get("sumber") or [] @@ -772,13 +823,13 @@ async def simpan_nilai(request: Request, kode: str): if aksi == "grid": kelas_dosen = _kelas_dosen(request.state.user, kode) - npm_diizinkan = _npm_diizinkan(kode, kelas_dosen) + nim_diizinkan = _nim_diizinkan(kode, kelas_dosen) mahasiswa = [] i = 0 - while f"npm_{i}" in form: - npm = (form.get(f"npm_{i}") or "").strip() + while f"nim_{i}" in form: + nim = (form.get(f"nim_{i}") or "").strip() nama = (form.get(f"nama_{i}") or "").strip() - if npm_diizinkan is not None and npm not in npm_diizinkan: + if nim_diizinkan is not None and nim not in nim_diizinkan: i += 1 continue skor = {} @@ -792,8 +843,8 @@ async def simpan_nilai(request: Request, kode: str): continue if 0 <= x <= 100: skor[s["kode"]] = x - if skor and (npm or nama): - mahasiswa.append({"npm": npm, "nama": nama, "skor": skor}) + if skor and (nim or nama): + mahasiswa.append({"nim": nim, "nama": nama, "skor": skor}) i += 1 if not mahasiswa: return _konteks_nilai(request, kode, @@ -813,12 +864,12 @@ async def simpan_nilai(request: Request, kode: str): # ke data lama, jangan sampai menghapus nilai kelas lain urut = [dict(m, skor=dict(m.get("skor") or {})) for m in lama_data.get("mahasiswa", [])] - per_npm = {m["npm"]: m for m in urut if m.get("npm")} + per_nim = {m["nim"]: m for m in urut if m.get("nim")} for m in mahasiswa: - ada = per_npm.get(m["npm"]) + ada = per_nim.get(m["nim"]) if ada is None: urut.append(m) - per_npm[m["npm"]] = m + per_nim[m["nim"]] = m else: ada["nama"] = m["nama"] or ada.get("nama", "") ada["skor"] = m["skor"] @@ -916,6 +967,7 @@ def _bahan_porto(kode: str) -> dict | None: f = porto["data"] if porto else PORTO_KOSONG rps_data = rps["data"] if rps else None + peta = _terapkan_override_sub(peta, rps_data) nilai_data = nilai["data"] if nilai else {"mode": "agregat", "agregat": {}} bobot_sub = hitung.bobot_sub_dari_rps(rps_data) diff --git a/app/nilai_xlsx.py b/app/nilai_xlsx.py index a89c3c1..c7f4be5 100644 --- a/app/nilai_xlsx.py +++ b/app/nilai_xlsx.py @@ -11,7 +11,7 @@ BIRU_MUDA = "D6E4F0" def template_mahasiswa(daftar_mk: list[dict], tahun: str) -> bytes: - """Template impor daftar mahasiswa: NPM | Nama | Kode MK | Kelas.""" + """Template impor daftar mahasiswa: NIM | Nama | Kode MK | Kelas.""" wb = openpyxl.Workbook() ws = wb.active ws.title = "Mahasiswa" @@ -19,7 +19,7 @@ def template_mahasiswa(daftar_mk: list[dict], tahun: str) -> bytes: 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"]) + 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") @@ -47,8 +47,8 @@ def template_mahasiswa(daftar_mk: list[dict], tahun: str) -> bytes: 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. + 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 @@ -58,11 +58,11 @@ def parse_mahasiswa(isi: bytes, kode_mk_valid: set[str]) -> tuple[list[dict], li 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: + if "nim" in vals or "npm" in vals: header_i = i for ci, v in enumerate(vals): - if v == "npm": - kol["npm"] = ci + 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": @@ -70,8 +70,8 @@ def parse_mahasiswa(isi: bytes, kode_mk_valid: set[str]) -> tuple[list[dict], li 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, " + 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): @@ -79,7 +79,7 @@ def parse_mahasiswa(isi: bytes, kode_mk_valid: set[str]) -> tuple[list[dict], li if ci is None or ci >= len(vals) or vals[ci] is None: return "" v = vals[ci] - # NPM/kelas numerik dari Excel → buang '.0' + # NIM/kelas numerik dari Excel → buang '.0' if isinstance(v, float) and v.is_integer(): v = int(v) return str(v).strip() @@ -88,17 +88,17 @@ def parse_mahasiswa(isi: bytes, kode_mk_valid: set[str]) -> tuple[list[dict], li terlihat = set() for row in rows[header_i + 1:]: vals = list(row) - npm = ambil(vals, "npm") + nim = ambil(vals, "nim") mk = ambil(vals, "mk").upper() - if not npm or not mk: + if not nim or not mk: continue if mk not in kode_mk_valid: tak_dikenal.add(mk) continue - if (npm, mk) in terlihat: + if (nim, mk) in terlihat: continue - terlihat.add((npm, mk)) - baris.append({"npm": npm, "nama": ambil(vals, "nama"), + terlihat.add((nim, mk)) + baris.append({"nim": nim, "nama": ambil(vals, "nama"), "mk_kode": mk, "kelas": ambil(vals, "kelas")}) if tak_dikenal: diff --git a/app/static/tabel.js b/app/static/tabel.js index 5ac4ee2..023140f 100644 --- a/app/static/tabel.js +++ b/app/static/tabel.js @@ -55,4 +55,21 @@ if (!t.closest("form")) pasang(t); }); }); + + /* Filter daftar MK saat penugasan dosen (/admin): ketik di .cari-mk + menyaring .baris-mk berdasarkan kode/nama MK (data-cari). */ + document.addEventListener("input", function (ev) { + if (!ev.target.classList.contains("cari-mk")) return; + var kata = ev.target.value.trim().toLowerCase(); + var wadah = ev.target.nextElementSibling; // .pilih-sub tepat setelah input cari + if (!wadah) return; + var ada = false; + wadah.querySelectorAll(".baris-mk").forEach(function (baris) { + var cocok = !kata || baris.dataset.cari.indexOf(kata) !== -1; + baris.style.display = cocok ? "" : "none"; + if (cocok) ada = true; + }); + var kosong = wadah.querySelector(".cari-kosong"); + if (kosong) kosong.style.display = ada ? "none" : ""; + }); })(); diff --git a/app/templates/admin.html b/app/templates/admin.html index 3a9fca0..f80f83e 100644 --- a/app/templates/admin.html +++ b/app/templates/admin.html @@ -39,13 +39,18 @@
Ubah penugasan…
-
+ +
{% for mk in daftar %} - - +
+ + +
{% endfor %} +
@@ -91,7 +96,7 @@

C. Daftar Mahasiswa Mengontrak MK — {{ tahun }}

Unggah satu file Excel berisi seluruh kontrak semester ini - (kolom: NPM, Nama, Kode MK, Kelas — satu baris per kontrak). Impor baru + (kolom: NIM, Nama, Kode MK, Kelas — satu baris per kontrak). Impor baru menggantikan seluruh daftar tahun ajaran aktif. Daftar ini dipakai untuk mengisi otomatis template nilai tiap MK.

diff --git a/app/templates/base.html b/app/templates/base.html index 9c119ed..daa5238 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -39,6 +39,7 @@ {{ user.nama }} — {{ {'kaprodi': 'Kaprodi', 'dosen_rps_mk': 'Dosen RPS + MK', 'dosen_mk': 'Dosen MK'}[user.role] }} + · Ganti Password · Keluar diff --git a/app/templates/form_nilai.html b/app/templates/form_nilai.html index 2f52d67..8ee36ee 100644 --- a/app/templates/form_nilai.html +++ b/app/templates/form_nilai.html @@ -92,7 +92,7 @@ No. - NPM + NIM Nama {% if ada_kelas %}Kelas{% endif %} {% for s in peta.subs %}{{ s.kode }}{% endfor %} @@ -104,9 +104,9 @@ {% set i = loop.index0 %} {{ loop.index }} - + - {{ m.npm }} + {{ m.nim }} {{ m.nama }} {% if ada_kelas %}{{ m.kelas or '—' }}{% endif %} {% for s in peta.subs %} @@ -114,7 +114,7 @@ min="0" max="100" step="0.01" value="{{ '%g'|format(m.skor[s.kode]) if s.kode in m.skor else '' }}"> {% endfor %} - {{ '%.1f'|format(akhir.get(m.npm)) if akhir.get(m.npm) is not none else '—' }} + {{ '%.1f'|format(akhir.get(m.nim)) if akhir.get(m.nim) is not none else '—' }} {% endfor %} {% if per_mhs and hasil %} diff --git a/app/templates/form_nilai_komponen.html b/app/templates/form_nilai_komponen.html index 2619a05..4f1bfaf 100644 --- a/app/templates/form_nilai_komponen.html +++ b/app/templates/form_nilai_komponen.html @@ -57,7 +57,7 @@ No. - NPM + NIM Nama Nilai (0–100) @@ -66,9 +66,9 @@ {% for m in grid %} {{ loop.index }} - + - {{ m.npm }} + {{ m.nim }} {{ m.nama }} +

A. IDENTITAS MATA KULIAH

@@ -77,13 +78,15 @@
-

D. SUB-CPMK — otomatis dari pemetaan kurikulum ({{ peta.subs|length }} butir)

+

D. SUB-CPMK — kode & kaitan CPMK otomatis dari pemetaan kurikulum ({{ peta.subs|length }} butir); redaksi kalimat boleh disesuaikan bila perlu

{% for s in peta.subs %} - + + + {% endfor %}
KodeUraian Sub-CPMKCPMK
{{ s.kode }}{{ s.uraian }}{{ s.cpmk }}
{{ s.kode }}{{ s.cpmk }}
@@ -206,6 +209,7 @@

H2. PUSTAKA PENDUKUNG

+
{% if boleh_edit %} Disimpan atas nama {{ user.nama }} diff --git a/app/templates/ganti_password.html b/app/templates/ganti_password.html new file mode 100644 index 0000000..b8626e2 --- /dev/null +++ b/app/templates/ganti_password.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block judul %}Ganti Password — RPS & Portofolio UNTAN{% endblock %} +{% block isi %} +

Ganti Password

+

Berlaku untuk akun Anda sendiri ({{ user.nama }}). + Setelah berhasil, sesi login lain dari akun ini akan keluar otomatis dan + Anda perlu memakai password baru pada login berikutnya.

+ + {% if pesan %}
{{ pesan }}
{% endif %} + {% if error %}
{{ error }}
{% endif %} + +
+
+ + + + + + +
+ +
+
+
+{% endblock %}