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).
This commit is contained in:
+87
-35
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user