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:
@@ -138,6 +138,13 @@ def _migrasi_role_pengguna(conn: sqlite3.Connection) -> None:
|
|||||||
conn.execute("DROP TABLE pengguna_lama")
|
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:
|
def _migrasi_kelas_penugasan(conn: sqlite3.Connection) -> None:
|
||||||
"""Skema lama penugasan hanya (user_id, mk_kode) — tambah kolom kelas
|
"""Skema lama penugasan hanya (user_id, mk_kode) — tambah kolom kelas
|
||||||
(kosong = seluruh kelas MK ini, dipakai dosen_rps_mk; terisi = dosen_mk
|
(kosong = seluruh kelas MK ini, dipakai dosen_rps_mk; terisi = dosen_mk
|
||||||
@@ -207,14 +214,15 @@ def init_db() -> None:
|
|||||||
_migrasi_kelas_penugasan(conn)
|
_migrasi_kelas_penugasan(conn)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""CREATE TABLE IF NOT EXISTS mahasiswa (
|
"""CREATE TABLE IF NOT EXISTS mahasiswa (
|
||||||
npm TEXT NOT NULL,
|
nim TEXT NOT NULL,
|
||||||
nama TEXT NOT NULL,
|
nama TEXT NOT NULL,
|
||||||
mk_kode TEXT NOT NULL,
|
mk_kode TEXT NOT NULL,
|
||||||
kelas TEXT NOT NULL DEFAULT '',
|
kelas TEXT NOT NULL DEFAULT '',
|
||||||
tahun TEXT NOT NULL,
|
tahun TEXT NOT NULL,
|
||||||
PRIMARY KEY (npm, mk_kode, tahun)
|
PRIMARY KEY (nim, mk_kode, tahun)
|
||||||
)"""
|
)"""
|
||||||
)
|
)
|
||||||
|
_migrasi_nim_mahasiswa(conn)
|
||||||
|
|
||||||
|
|
||||||
# ── pengaturan prodi ─────────────────────────────────────────────────────────
|
# ── pengaturan prodi ─────────────────────────────────────────────────────────
|
||||||
@@ -507,9 +515,9 @@ def ganti_mahasiswa(baris: list[dict], tahun: str | None = None) -> None:
|
|||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
conn.execute("DELETE FROM mahasiswa WHERE tahun=?", (tahun,))
|
conn.execute("DELETE FROM mahasiswa WHERE tahun=?", (tahun,))
|
||||||
conn.executemany(
|
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 (?,?,?,?,?)",
|
"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],
|
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()
|
tahun = tahun or tahun_aktif()
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT npm, nama, kelas FROM mahasiswa WHERE mk_kode=? AND tahun=? "
|
"SELECT nim, nama, kelas FROM mahasiswa WHERE mk_kode=? AND tahun=? "
|
||||||
"ORDER BY kelas, npm",
|
"ORDER BY kelas, nim",
|
||||||
(mk_kode, tahun),
|
(mk_kode, tahun),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|||||||
@@ -265,12 +265,12 @@ def build_portofolio(d: dict) -> bytes:
|
|||||||
skor_sub = d.get("skor_sub", {})
|
skor_sub = d.get("skor_sub", {})
|
||||||
if nilai.get("mode") == "per_mahasiswa":
|
if nilai.get("mode") == "per_mahasiswa":
|
||||||
mhs = nilai.get("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]
|
kol_sub = [s["kode"] for s in subs]
|
||||||
tf = doc.add_table(rows=2 + len(mhs) + 3, cols=3 + len(kol_sub) + 1)
|
tf = doc.add_table(rows=2 + len(mhs) + 3, cols=3 + len(kol_sub) + 1)
|
||||||
_borders(tf)
|
_borders(tf)
|
||||||
_hdr(tf.cell(0, 0), "No.")
|
_hdr(tf.cell(0, 0), "No.")
|
||||||
_hdr(tf.cell(0, 1), "NPM")
|
_hdr(tf.cell(0, 1), "NIM")
|
||||||
_hdr(tf.cell(0, 2), "Nama")
|
_hdr(tf.cell(0, 2), "Nama")
|
||||||
for j, sk in enumerate(kol_sub):
|
for j, sk in enumerate(kol_sub):
|
||||||
_hdr(tf.cell(0, 3 + j), sk, size=7)
|
_hdr(tf.cell(0, 3 + j), sk, size=7)
|
||||||
@@ -285,13 +285,13 @@ def build_portofolio(d: dict) -> bytes:
|
|||||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||||
for i, m in enumerate(mhs, 2):
|
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, 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)
|
_ct(tf.cell(i, 2), m.get("nama", ""), size=8)
|
||||||
for j, sk in enumerate(kol_sub):
|
for j, sk in enumerate(kol_sub):
|
||||||
v = (m.get("skor") or {}).get(sk)
|
v = (m.get("skor") or {}).get(sk)
|
||||||
_ct(tf.cell(i, 3 + j), _f(v, 0) if v is not None else "",
|
_ct(tf.cell(i, 3 + j), _f(v, 0) if v is not None else "",
|
||||||
size=8, align=WD_ALIGN_PARAGRAPH.CENTER)
|
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)
|
size=8, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||||
# baris statistik per sub
|
# baris statistik per sub
|
||||||
base = 2 + len(mhs)
|
base = 2 + len(mhs)
|
||||||
|
|||||||
+12
-12
@@ -50,7 +50,7 @@ def skor_sub(nilai_data: dict, ambang: float) -> dict:
|
|||||||
"""Skor per Sub-CPMK dari data nilai tersimpan.
|
"""Skor per Sub-CPMK dari data nilai tersimpan.
|
||||||
|
|
||||||
nilai_data = {"mode": "per_mahasiswa"|"agregat",
|
nilai_data = {"mode": "per_mahasiswa"|"agregat",
|
||||||
"mahasiswa": [{"npm","nama","skor":{sub: nilai}}] |
|
"mahasiswa": [{"nim","nama","skor":{sub: nilai}}] |
|
||||||
"agregat": {sub: rata}}
|
"agregat": {sub: rata}}
|
||||||
→ {sub: {"rata": x, "n": jumlah_mhs, "lulus_pct": y|None}}
|
→ {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]]:
|
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
|
'isi per komponen' di halaman nilai): rata sederhana antar sesi dalam
|
||||||
komponen yang sama (mis. beberapa kali Kuis), lalu rata tertimbang bobot
|
komponen yang sama (mis. beberapa kali Kuis), lalu rata tertimbang bobot
|
||||||
antar-komponen bila satu Sub-CPMK diukur oleh >1 komponen (mis. Kuis dan
|
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
|
continue
|
||||||
if i not in komponen_by_i or not daftar_sesi:
|
if i not in komponen_by_i or not daftar_sesi:
|
||||||
continue
|
continue
|
||||||
per_npm: dict[str, list[float]] = {}
|
per_nim: dict[str, list[float]] = {}
|
||||||
for s in daftar_sesi:
|
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:
|
if v is not None:
|
||||||
per_npm.setdefault(npm, []).append(float(v))
|
per_nim.setdefault(nim, []).append(float(v))
|
||||||
per_komponen_avg[i] = {npm: _rata(xs) for npm, xs in per_npm.items()}
|
per_komponen_avg[i] = {nim: _rata(xs) for nim, xs in per_nim.items()}
|
||||||
|
|
||||||
kontribusi_per_sub: dict[str, list[tuple[float, dict]]] = {}
|
kontribusi_per_sub: dict[str, list[tuple[float, dict]]] = {}
|
||||||
for i, avg_map in per_komponen_avg.items():
|
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]] = {}
|
out: dict[str, dict[str, float]] = {}
|
||||||
for sub, kontribusi in kontribusi_per_sub.items():
|
for sub, kontribusi in kontribusi_per_sub.items():
|
||||||
npms: set[str] = set()
|
nims: set[str] = set()
|
||||||
for _, avg_map in kontribusi:
|
for _, avg_map in kontribusi:
|
||||||
npms |= avg_map.keys()
|
nims |= avg_map.keys()
|
||||||
for npm in npms:
|
for nim in nims:
|
||||||
pasangan = [(w, avg_map[npm]) for w, avg_map in kontribusi if npm in avg_map]
|
pasangan = [(w, avg_map[nim]) for w, avg_map in kontribusi if nim in avg_map]
|
||||||
total_w = sum(w for w, _ in pasangan)
|
total_w = sum(w for w, _ in pasangan)
|
||||||
nilai = (sum(w * v for w, v in pasangan) / total_w) if total_w \
|
nilai = (sum(w * v for w, v in pasangan) / total_w) if total_w \
|
||||||
else _rata([v for _, v in pasangan])
|
else _rata([v for _, v in pasangan])
|
||||||
out.setdefault(npm, {})[sub] = nilai
|
out.setdefault(nim, {})[sub] = nilai
|
||||||
return out
|
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
|
else: # RPS belum mengisi bobot → rata-rata sederhana
|
||||||
akhir = _rata([v for _, v in pasangan])
|
akhir = _rata([v for _, v in pasangan])
|
||||||
if akhir is not None:
|
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})
|
"akhir": akhir})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
+87
-35
@@ -74,17 +74,17 @@ def _kelas_dosen(user: dict, kode: str) -> set[str] | None:
|
|||||||
return db.kelas_diampu(user["id"], kode)
|
return db.kelas_diampu(user["id"], kode)
|
||||||
|
|
||||||
|
|
||||||
def _npm_diizinkan(kode: str, kelas_dosen: set[str] | None) -> set[str] | None:
|
def _nim_diizinkan(kode: str, kelas_dosen: set[str] | None) -> set[str] | None:
|
||||||
"""NPM yang boleh ditulis dosen ini. None = tak dibatasi (semua NPM MK ini)."""
|
"""NIM yang boleh ditulis dosen ini. None = tak dibatasi (semua NIM MK ini)."""
|
||||||
if kelas_dosen is None:
|
if kelas_dosen is None:
|
||||||
return 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()
|
token = auth.token_baru()
|
||||||
db.buat_sesi(user_id, token)
|
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",
|
r.set_cookie("sesi", token, httponly=True, samesite="lax",
|
||||||
max_age=60 * 60 * 24 * 30)
|
max_age=60 * 60 * 24 * 30)
|
||||||
return r
|
return r
|
||||||
@@ -148,6 +148,30 @@ def keluar(request: Request):
|
|||||||
return r
|
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 ─────────────────────
|
# ── admin kaprodi: akun, penugasan MK, daftar mahasiswa ─────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -330,9 +354,27 @@ def form_kosong() -> dict:
|
|||||||
"komponen": [dict(k) for k in KOMPONEN_DEFAULT],
|
"komponen": [dict(k) for k in KOMPONEN_DEFAULT],
|
||||||
"referensi_teks": "",
|
"referensi_teks": "",
|
||||||
"referensi_pendukung_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)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
def beranda(request: Request, semua: int = 0):
|
def beranda(request: Request, semua: int = 0):
|
||||||
daftar = db.daftar_mk()
|
daftar = db.daftar_mk()
|
||||||
@@ -385,6 +427,7 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
|
|||||||
if info.get("semester"):
|
if info.get("semester"):
|
||||||
f["identitas"]["semester"] = str(info["semester"])
|
f["identitas"]["semester"] = str(info["semester"])
|
||||||
ctx_status = {"status": "", "updated_at": "", "updated_by": ""}
|
ctx_status = {"status": "", "updated_at": "", "updated_by": ""}
|
||||||
|
peta = _terapkan_override_sub(peta, f)
|
||||||
return render(
|
return render(
|
||||||
request, "form_rps.html",
|
request, "form_rps.html",
|
||||||
{"peta": peta, "f": f, "status": ctx_status["status"],
|
{"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_teks = (form.get("referensi_pendukung") or "").strip()
|
||||||
pendukung = [r.strip() for r in pendukung_teks.splitlines() if r.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 = {
|
data = {
|
||||||
"identitas": identitas,
|
"identitas": identitas,
|
||||||
"deskripsi": (form.get("deskripsi") or "").strip(),
|
"deskripsi": (form.get("deskripsi") or "").strip(),
|
||||||
@@ -472,6 +521,7 @@ async def simpan_rps(request: Request, kode: str):
|
|||||||
"referensi_teks": referensi_teks,
|
"referensi_teks": referensi_teks,
|
||||||
"referensi_pendukung": pendukung,
|
"referensi_pendukung": pendukung,
|
||||||
"referensi_pendukung_teks": pendukung_teks,
|
"referensi_pendukung_teks": pendukung_teks,
|
||||||
|
"sub_uraian_custom": sub_uraian_custom,
|
||||||
}
|
}
|
||||||
aksi = form.get("aksi") or "draft"
|
aksi = form.get("aksi") or "draft"
|
||||||
status = "selesai" if aksi == "selesai" else "draft"
|
status = "selesai" if aksi == "selesai" else "draft"
|
||||||
@@ -486,6 +536,7 @@ def unduh_rps(kode: str):
|
|||||||
if not peta or not tersimpan:
|
if not peta or not tersimpan:
|
||||||
return RedirectResponse(f"/rps/{kode}", status_code=303)
|
return RedirectResponse(f"/rps/{kode}", status_code=303)
|
||||||
f = tersimpan["data"]
|
f = tersimpan["data"]
|
||||||
|
peta = _terapkan_override_sub(peta, f)
|
||||||
|
|
||||||
# resolusi kode Sub-CPMK → teks uraian untuk dokumen
|
# resolusi kode Sub-CPMK → teks uraian untuk dokumen
|
||||||
uraian = {s["kode"]: s["uraian"] for s in peta["subs"]}
|
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 = {}, {}
|
skor_map, nama_map = {}, {}
|
||||||
if tersimpan and tersimpan["data"].get("mode") == "per_mahasiswa":
|
if tersimpan and tersimpan["data"].get("mode") == "per_mahasiswa":
|
||||||
for m in tersimpan["data"]["mahasiswa"]:
|
for m in tersimpan["data"]["mahasiswa"]:
|
||||||
skor_map[m.get("npm", "")] = m.get("skor") or {}
|
skor_map[m.get("nim", "")] = m.get("skor") or {}
|
||||||
nama_map[m.get("npm", "")] = m.get("nama", "")
|
nama_map[m.get("nim", "")] = m.get("nama", "")
|
||||||
grid, ada = [], set()
|
grid, ada = [], set()
|
||||||
for m in terdaftar:
|
for m in terdaftar:
|
||||||
grid.append({"npm": m["npm"], "nama": m["nama"], "kelas": m.get("kelas") or "",
|
grid.append({"nim": m["nim"], "nama": m["nama"], "kelas": m.get("kelas") or "",
|
||||||
"skor": skor_map.get(m["npm"], {})})
|
"skor": skor_map.get(m["nim"], {})})
|
||||||
ada.add(m["npm"])
|
ada.add(m["nim"])
|
||||||
for npm, sk in skor_map.items():
|
for nim, sk in skor_map.items():
|
||||||
if npm not in ada:
|
if nim not in ada:
|
||||||
grid.append({"npm": npm, "nama": nama_map.get(npm, ""), "kelas": "",
|
grid.append({"nim": nim, "nama": nama_map.get(nim, ""), "kelas": "",
|
||||||
"skor": sk})
|
"skor": sk})
|
||||||
if kelas_dosen is not None:
|
if kelas_dosen is not None:
|
||||||
grid = [g for g in grid if g["kelas"] in kelas_dosen]
|
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":
|
if tersimpan["data"].get("mode") == "per_mahasiswa":
|
||||||
rps = db.ambil_rps(kode)
|
rps = db.ambil_rps(kode)
|
||||||
bobot_sub = hitung.bobot_sub_dari_rps(rps["data"] if rps else None)
|
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)}
|
for a in hitung.nilai_akhir_mahasiswa(tersimpan["data"], bobot_sub)}
|
||||||
kelas_dosen = _kelas_dosen(request.state.user, kode)
|
kelas_dosen = _kelas_dosen(request.state.user, kode)
|
||||||
terdaftar = db.mahasiswa_mk(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)
|
kelas_dosen = _kelas_dosen(request.state.user, kode)
|
||||||
grid = _grid_mahasiswa(tersimpan, db.mahasiswa_mk(kode), kelas_dosen)
|
grid = _grid_mahasiswa(tersimpan, db.mahasiswa_mk(kode), kelas_dosen)
|
||||||
for m in grid:
|
for m in grid:
|
||||||
m["nilai_awal"] = nilai_awal.get(m["npm"])
|
m["nilai_awal"] = nilai_awal.get(m["nim"])
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request, "form_nilai_komponen.html",
|
request, "form_nilai_komponen.html",
|
||||||
@@ -686,19 +737,19 @@ async def simpan_nilai_komponen(request: Request, kode: str, i: int):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
edit_id = -1
|
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 = {}
|
nilai = {}
|
||||||
j = 0
|
j = 0
|
||||||
while f"npm_{j}" in form:
|
while f"nim_{j}" in form:
|
||||||
npm = (form.get(f"npm_{j}") or "").strip()
|
nim = (form.get(f"nim_{j}") or "").strip()
|
||||||
v = (form.get(f"nilai_{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:
|
try:
|
||||||
x = float(v)
|
x = float(v)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
x = None
|
x = None
|
||||||
if x is not None and 0 <= x <= 100:
|
if x is not None and 0 <= x <= 100:
|
||||||
nilai[npm] = x
|
nilai[nim] = x
|
||||||
j += 1
|
j += 1
|
||||||
|
|
||||||
if not nilai:
|
if not nilai:
|
||||||
@@ -724,16 +775,16 @@ async def simpan_nilai_komponen(request: Request, kode: str, i: int):
|
|||||||
|
|
||||||
komponen_daftar = _komponen_daftar(kode)
|
komponen_daftar = _komponen_daftar(kode)
|
||||||
skor_terhitung = hitung.skor_dari_sesi(komponen_daftar, sesi_semua)
|
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", [])]
|
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 npm, subskor in skor_terhitung.items():
|
for nim, subskor in skor_terhitung.items():
|
||||||
m = per_npm.get(npm)
|
m = per_nim.get(nim)
|
||||||
if m is None:
|
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)
|
urut.append(m)
|
||||||
per_npm[npm] = m
|
per_nim[nim] = m
|
||||||
m["skor"].update(subskor)
|
m["skor"].update(subskor)
|
||||||
|
|
||||||
riwayat = lama_data.get("sumber") or []
|
riwayat = lama_data.get("sumber") or []
|
||||||
@@ -772,13 +823,13 @@ async def simpan_nilai(request: Request, kode: str):
|
|||||||
|
|
||||||
if aksi == "grid":
|
if aksi == "grid":
|
||||||
kelas_dosen = _kelas_dosen(request.state.user, kode)
|
kelas_dosen = _kelas_dosen(request.state.user, kode)
|
||||||
npm_diizinkan = _npm_diizinkan(kode, kelas_dosen)
|
nim_diizinkan = _nim_diizinkan(kode, kelas_dosen)
|
||||||
mahasiswa = []
|
mahasiswa = []
|
||||||
i = 0
|
i = 0
|
||||||
while f"npm_{i}" in form:
|
while f"nim_{i}" in form:
|
||||||
npm = (form.get(f"npm_{i}") or "").strip()
|
nim = (form.get(f"nim_{i}") or "").strip()
|
||||||
nama = (form.get(f"nama_{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
|
i += 1
|
||||||
continue
|
continue
|
||||||
skor = {}
|
skor = {}
|
||||||
@@ -792,8 +843,8 @@ async def simpan_nilai(request: Request, kode: str):
|
|||||||
continue
|
continue
|
||||||
if 0 <= x <= 100:
|
if 0 <= x <= 100:
|
||||||
skor[s["kode"]] = x
|
skor[s["kode"]] = x
|
||||||
if skor and (npm or nama):
|
if skor and (nim or nama):
|
||||||
mahasiswa.append({"npm": npm, "nama": nama, "skor": skor})
|
mahasiswa.append({"nim": nim, "nama": nama, "skor": skor})
|
||||||
i += 1
|
i += 1
|
||||||
if not mahasiswa:
|
if not mahasiswa:
|
||||||
return _konteks_nilai(request, kode,
|
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
|
# ke data lama, jangan sampai menghapus nilai kelas lain
|
||||||
urut = [dict(m, skor=dict(m.get("skor") or {}))
|
urut = [dict(m, skor=dict(m.get("skor") or {}))
|
||||||
for m in lama_data.get("mahasiswa", [])]
|
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:
|
for m in mahasiswa:
|
||||||
ada = per_npm.get(m["npm"])
|
ada = per_nim.get(m["nim"])
|
||||||
if ada is None:
|
if ada is None:
|
||||||
urut.append(m)
|
urut.append(m)
|
||||||
per_npm[m["npm"]] = m
|
per_nim[m["nim"]] = m
|
||||||
else:
|
else:
|
||||||
ada["nama"] = m["nama"] or ada.get("nama", "")
|
ada["nama"] = m["nama"] or ada.get("nama", "")
|
||||||
ada["skor"] = m["skor"]
|
ada["skor"] = m["skor"]
|
||||||
@@ -916,6 +967,7 @@ def _bahan_porto(kode: str) -> dict | None:
|
|||||||
|
|
||||||
f = porto["data"] if porto else PORTO_KOSONG
|
f = porto["data"] if porto else PORTO_KOSONG
|
||||||
rps_data = rps["data"] if rps else None
|
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": {}}
|
nilai_data = nilai["data"] if nilai else {"mode": "agregat", "agregat": {}}
|
||||||
|
|
||||||
bobot_sub = hitung.bobot_sub_dari_rps(rps_data)
|
bobot_sub = hitung.bobot_sub_dari_rps(rps_data)
|
||||||
|
|||||||
+15
-15
@@ -11,7 +11,7 @@ BIRU_MUDA = "D6E4F0"
|
|||||||
|
|
||||||
|
|
||||||
def template_mahasiswa(daftar_mk: list[dict], tahun: str) -> bytes:
|
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()
|
wb = openpyxl.Workbook()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = "Mahasiswa"
|
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 "
|
ws.append(["Petunjuk: satu baris per kontrak (mahasiswa yang mengambil 5 MK "
|
||||||
"berarti 5 baris). Kode MK harus sesuai sheet 'Kode MK'."])
|
"berarti 5 baris). Kode MK harus sesuai sheet 'Kode MK'."])
|
||||||
ws.append([])
|
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):
|
for ci, lebar in enumerate((16, 30, 14, 10), 1):
|
||||||
c = ws.cell(row=4, column=ci)
|
c = ws.cell(row=4, column=ci)
|
||||||
c.font = Font(bold=True, color="FFFFFF")
|
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]]:
|
def parse_mahasiswa(isi: bytes, kode_mk_valid: set[str]) -> tuple[list[dict], list[str]]:
|
||||||
"""Baca xlsx daftar mahasiswa → (baris kontrak, peringatan).
|
"""Baca xlsx daftar mahasiswa → (baris kontrak, peringatan).
|
||||||
|
|
||||||
Header dicari otomatis (baris yang memuat 'NPM'); kolom dikenali dari
|
Header dicari otomatis (baris yang memuat 'NIM'); kolom dikenali dari
|
||||||
judulnya: NPM, Nama, Kode MK, Kelas.
|
judulnya: NIM, Nama, Kode MK, Kelas.
|
||||||
"""
|
"""
|
||||||
wb = openpyxl.load_workbook(io.BytesIO(isi), read_only=True, data_only=True)
|
wb = openpyxl.load_workbook(io.BytesIO(isi), read_only=True, data_only=True)
|
||||||
ws = wb["Mahasiswa"] if "Mahasiswa" in wb.sheetnames else wb.active
|
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
|
header_i = None
|
||||||
for i, row in enumerate(rows):
|
for i, row in enumerate(rows):
|
||||||
vals = [str(v).strip().lower() if v is not None else "" for v in row]
|
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
|
header_i = i
|
||||||
for ci, v in enumerate(vals):
|
for ci, v in enumerate(vals):
|
||||||
if v == "npm":
|
if v in ("nim", "npm"): # 'npm' — kompatibilitas file lama
|
||||||
kol["npm"] = ci
|
kol["nim"] = ci
|
||||||
elif "nama" in v:
|
elif "nama" in v:
|
||||||
kol.setdefault("nama", ci)
|
kol.setdefault("nama", ci)
|
||||||
elif "kode" in v or v == "mk":
|
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:
|
elif "kelas" in v:
|
||||||
kol.setdefault("kelas", ci)
|
kol.setdefault("kelas", ci)
|
||||||
break
|
break
|
||||||
if header_i is None or "npm" not in kol or "mk" not in kol:
|
if header_i is None or "nim" not in kol or "mk" not in kol:
|
||||||
return [], ["Header tidak dikenali: pastikan ada kolom NPM, Nama, "
|
return [], ["Header tidak dikenali: pastikan ada kolom NIM, Nama, "
|
||||||
"Kode MK, dan Kelas (pakai template dari aplikasi)."]
|
"Kode MK, dan Kelas (pakai template dari aplikasi)."]
|
||||||
|
|
||||||
def ambil(vals, kunci):
|
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:
|
if ci is None or ci >= len(vals) or vals[ci] is None:
|
||||||
return ""
|
return ""
|
||||||
v = vals[ci]
|
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():
|
if isinstance(v, float) and v.is_integer():
|
||||||
v = int(v)
|
v = int(v)
|
||||||
return str(v).strip()
|
return str(v).strip()
|
||||||
@@ -88,17 +88,17 @@ def parse_mahasiswa(isi: bytes, kode_mk_valid: set[str]) -> tuple[list[dict], li
|
|||||||
terlihat = set()
|
terlihat = set()
|
||||||
for row in rows[header_i + 1:]:
|
for row in rows[header_i + 1:]:
|
||||||
vals = list(row)
|
vals = list(row)
|
||||||
npm = ambil(vals, "npm")
|
nim = ambil(vals, "nim")
|
||||||
mk = ambil(vals, "mk").upper()
|
mk = ambil(vals, "mk").upper()
|
||||||
if not npm or not mk:
|
if not nim or not mk:
|
||||||
continue
|
continue
|
||||||
if mk not in kode_mk_valid:
|
if mk not in kode_mk_valid:
|
||||||
tak_dikenal.add(mk)
|
tak_dikenal.add(mk)
|
||||||
continue
|
continue
|
||||||
if (npm, mk) in terlihat:
|
if (nim, mk) in terlihat:
|
||||||
continue
|
continue
|
||||||
terlihat.add((npm, mk))
|
terlihat.add((nim, mk))
|
||||||
baris.append({"npm": npm, "nama": ambil(vals, "nama"),
|
baris.append({"nim": nim, "nama": ambil(vals, "nama"),
|
||||||
"mk_kode": mk, "kelas": ambil(vals, "kelas")})
|
"mk_kode": mk, "kelas": ambil(vals, "kelas")})
|
||||||
|
|
||||||
if tak_dikenal:
|
if tak_dikenal:
|
||||||
|
|||||||
@@ -55,4 +55,21 @@
|
|||||||
if (!t.closest("form")) pasang(t);
|
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" : "";
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -39,13 +39,18 @@
|
|||||||
<details style="margin-top:6px">
|
<details style="margin-top:6px">
|
||||||
<summary style="cursor:pointer;color:var(--blue);font-size:13px">Ubah penugasan…</summary>
|
<summary style="cursor:pointer;color:var(--blue);font-size:13px">Ubah penugasan…</summary>
|
||||||
<form method="post" action="/admin/pengguna/{{ p.id }}/tugas">
|
<form method="post" action="/admin/pengguna/{{ p.id }}/tugas">
|
||||||
<div class="pilih-sub" style="max-height:220px;margin:6px 0">
|
<input type="text" class="cari-mk" placeholder="Cari kode/nama MK…"
|
||||||
|
style="width:260px;margin:6px 0 4px;font-size:13px">
|
||||||
|
<div class="pilih-sub" style="max-height:220px;margin:0 0 6px">
|
||||||
{% for mk in daftar %}
|
{% for mk in daftar %}
|
||||||
<label><input type="checkbox" name="mk" value="{{ mk.kode }}"
|
<div class="baris-mk" data-cari="{{ (mk.kode ~ ' ' ~ mk.nama)|lower }}">
|
||||||
{% if mk.kode in p.mk %}checked{% endif %}> {{ mk.kode }} — {{ mk.nama }}</label>
|
<label><input type="checkbox" name="mk" value="{{ mk.kode }}"
|
||||||
<input type="text" name="kelas_{{ mk.kode }}" placeholder="kelas (kosong = semua)"
|
{% if mk.kode in p.mk %}checked{% endif %}> {{ mk.kode }} — {{ mk.nama }}</label>
|
||||||
value="{{ p.kelas_map.get(mk.kode, '') }}" style="width:150px;margin:0 0 4px 22px;font-size:12px">
|
<input type="text" name="kelas_{{ mk.kode }}" placeholder="kelas (kosong = semua)"
|
||||||
|
value="{{ p.kelas_map.get(mk.kode, '') }}" style="width:150px;margin:0 0 4px 22px;font-size:12px">
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<p class="cari-kosong" style="display:none;color:var(--grey);font-size:13px;margin:4px 0">Tidak ada MK yang cocok.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="tombol garis" type="submit" style="padding:4px 12px;font-size:13px">Simpan Penugasan</button>
|
<button class="tombol garis" type="submit" style="padding:4px 12px;font-size:13px">Simpan Penugasan</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -91,7 +96,7 @@
|
|||||||
|
|
||||||
<h2 class="bagian">C. Daftar Mahasiswa Mengontrak MK — {{ tahun }}</h2>
|
<h2 class="bagian">C. Daftar Mahasiswa Mengontrak MK — {{ tahun }}</h2>
|
||||||
<p class="keterangan">Unggah satu file Excel berisi seluruh kontrak semester ini
|
<p class="keterangan">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
|
||||||
<b>menggantikan seluruh daftar</b> tahun ajaran aktif. Daftar ini dipakai untuk
|
<b>menggantikan seluruh daftar</b> tahun ajaran aktif. Daftar ini dipakai untuk
|
||||||
mengisi otomatis template nilai tiap MK.</p>
|
mengisi otomatis template nilai tiap MK.</p>
|
||||||
<div class="aksi-baris" style="border-top:none;padding-top:0;margin-top:8px">
|
<div class="aksi-baris" style="border-top:none;padding-top:0;margin-top:8px">
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="menu-kanan">
|
<span class="menu-kanan">
|
||||||
<b>{{ user.nama }}</b> — {{ {'kaprodi': 'Kaprodi', 'dosen_rps_mk': 'Dosen RPS + MK', 'dosen_mk': 'Dosen MK'}[user.role] }}
|
<b>{{ user.nama }}</b> — {{ {'kaprodi': 'Kaprodi', 'dosen_rps_mk': 'Dosen RPS + MK', 'dosen_mk': 'Dosen MK'}[user.role] }}
|
||||||
|
· <a href="/akun">Ganti Password</a>
|
||||||
· <a href="/keluar">Keluar</a>
|
· <a href="/keluar">Keluar</a>
|
||||||
</span>
|
</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:44px" class="tengah">No.</th>
|
<th style="width:44px" class="tengah">No.</th>
|
||||||
<th style="width:112px">NPM</th>
|
<th style="width:112px">NIM</th>
|
||||||
<th style="min-width:150px">Nama</th>
|
<th style="min-width:150px">Nama</th>
|
||||||
{% if ada_kelas %}<th style="width:60px" class="tengah">Kelas</th>{% endif %}
|
{% if ada_kelas %}<th style="width:60px" class="tengah">Kelas</th>{% endif %}
|
||||||
{% for s in peta.subs %}<th class="tengah" style="font-size:11px;min-width:62px">{{ s.kode }}</th>{% endfor %}
|
{% for s in peta.subs %}<th class="tengah" style="font-size:11px;min-width:62px">{{ s.kode }}</th>{% endfor %}
|
||||||
@@ -104,9 +104,9 @@
|
|||||||
{% set i = loop.index0 %}
|
{% set i = loop.index0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="tengah">{{ loop.index }}
|
<td class="tengah">{{ loop.index }}
|
||||||
<input type="hidden" name="npm_{{ i }}" value="{{ m.npm }}">
|
<input type="hidden" name="nim_{{ i }}" value="{{ m.nim }}">
|
||||||
<input type="hidden" name="nama_{{ i }}" value="{{ m.nama }}"></td>
|
<input type="hidden" name="nama_{{ i }}" value="{{ m.nama }}"></td>
|
||||||
<td>{{ m.npm }}</td>
|
<td>{{ m.nim }}</td>
|
||||||
<td>{{ m.nama }}</td>
|
<td>{{ m.nama }}</td>
|
||||||
{% if ada_kelas %}<td class="tengah">{{ m.kelas or '—' }}</td>{% endif %}
|
{% if ada_kelas %}<td class="tengah">{{ m.kelas or '—' }}</td>{% endif %}
|
||||||
{% for s in peta.subs %}
|
{% for s in peta.subs %}
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
min="0" max="100" step="0.01"
|
min="0" max="100" step="0.01"
|
||||||
value="{{ '%g'|format(m.skor[s.kode]) if s.kode in m.skor else '' }}"></td>
|
value="{{ '%g'|format(m.skor[s.kode]) if s.kode in m.skor else '' }}"></td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<td class="tengah"><b>{{ '%.1f'|format(akhir.get(m.npm)) if akhir.get(m.npm) is not none else '—' }}</b></td>
|
<td class="tengah"><b>{{ '%.1f'|format(akhir.get(m.nim)) if akhir.get(m.nim) is not none else '—' }}</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if per_mhs and hasil %}
|
{% if per_mhs and hasil %}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:44px" class="tengah">No.</th>
|
<th style="width:44px" class="tengah">No.</th>
|
||||||
<th style="width:112px">NPM</th>
|
<th style="width:112px">NIM</th>
|
||||||
<th style="min-width:150px">Nama</th>
|
<th style="min-width:150px">Nama</th>
|
||||||
<th class="tengah" style="width:140px">Nilai (0–100)</th>
|
<th class="tengah" style="width:140px">Nilai (0–100)</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -66,9 +66,9 @@
|
|||||||
{% for m in grid %}
|
{% for m in grid %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="tengah">{{ loop.index }}
|
<td class="tengah">{{ loop.index }}
|
||||||
<input type="hidden" name="npm_{{ loop.index0 }}" value="{{ m.npm }}">
|
<input type="hidden" name="nim_{{ loop.index0 }}" value="{{ m.nim }}">
|
||||||
<input type="hidden" name="nama_{{ loop.index0 }}" value="{{ m.nama }}"></td>
|
<input type="hidden" name="nama_{{ loop.index0 }}" value="{{ m.nama }}"></td>
|
||||||
<td>{{ m.npm }}</td>
|
<td>{{ m.nim }}</td>
|
||||||
<td>{{ m.nama }}</td>
|
<td>{{ m.nama }}</td>
|
||||||
<td class="isian"><input type="number" name="nilai_{{ loop.index0 }}"
|
<td class="isian"><input type="number" name="nilai_{{ loop.index0 }}"
|
||||||
min="0" max="100" step="0.01"
|
min="0" max="100" step="0.01"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" action="/rps/{{ peta.kode }}">
|
<form method="post" action="/rps/{{ peta.kode }}">
|
||||||
|
<fieldset {% if not boleh_edit %}disabled{% endif %} style="border:none;padding:0;margin:0">
|
||||||
|
|
||||||
<h2 class="bagian">A. IDENTITAS MATA KULIAH</h2>
|
<h2 class="bagian">A. IDENTITAS MATA KULIAH</h2>
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
@@ -77,13 +78,15 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2 class="bagian">D. SUB-CPMK <span style="font-weight:normal;font-style:italic;color:var(--grey)">— otomatis dari pemetaan kurikulum ({{ peta.subs|length }} butir)</span></h2>
|
<h2 class="bagian">D. SUB-CPMK <span style="font-weight:normal;font-style:italic;color:var(--grey)">— kode & kaitan CPMK otomatis dari pemetaan kurikulum ({{ peta.subs|length }} butir); redaksi kalimat boleh disesuaikan bila perlu</span></h2>
|
||||||
<div class="gulir" style="max-height:320px;overflow-y:auto">
|
<div class="gulir" style="max-height:320px;overflow-y:auto">
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
<thead><tr><th style="width:110px">Kode</th><th>Uraian Sub-CPMK</th><th style="width:110px">CPMK</th></tr></thead>
|
<thead><tr><th style="width:110px">Kode</th><th>Uraian Sub-CPMK</th><th style="width:110px">CPMK</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for s in peta.subs %}
|
{% for s in peta.subs %}
|
||||||
<tr><td class="tengah">{{ s.kode }}</td><td>{{ s.uraian }}</td><td class="tengah">{{ s.cpmk }}</td></tr>
|
<tr><td class="tengah">{{ s.kode }}</td>
|
||||||
|
<td class="isian"><textarea name="sub_uraian_{{ s.kode }}" rows="2">{{ s.uraian }}</textarea></td>
|
||||||
|
<td class="tengah">{{ s.cpmk }}</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -206,6 +209,7 @@
|
|||||||
<h2 class="bagian">H2. PUSTAKA PENDUKUNG</h2>
|
<h2 class="bagian">H2. PUSTAKA PENDUKUNG</h2>
|
||||||
<textarea name="referensi_pendukung" rows="3" placeholder="Satu referensi per baris; boleh kosong.">{{ f.referensi_pendukung_teks }}</textarea>
|
<textarea name="referensi_pendukung" rows="3" placeholder="Satu referensi per baris; boleh kosong.">{{ f.referensi_pendukung_teks }}</textarea>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
<div class="aksi-baris">
|
<div class="aksi-baris">
|
||||||
{% if boleh_edit %}
|
{% if boleh_edit %}
|
||||||
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block judul %}Ganti Password — RPS & Portofolio UNTAN{% endblock %}
|
||||||
|
{% block isi %}
|
||||||
|
<h1>Ganti Password</h1>
|
||||||
|
<p class="keterangan">Berlaku untuk akun Anda sendiri (<b>{{ user.nama }}</b>).
|
||||||
|
Setelah berhasil, sesi login lain dari akun ini akan keluar otomatis dan
|
||||||
|
Anda perlu memakai password baru pada login berikutnya.</p>
|
||||||
|
|
||||||
|
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
|
||||||
|
{% if error %}<div class="peringatan">{{ error }}</div>{% endif %}
|
||||||
|
|
||||||
|
<div style="max-width:360px">
|
||||||
|
<form method="post" action="/akun">
|
||||||
|
<label for="lama" style="display:block;font-size:14px;margin:12px 0 3px">Password lama</label>
|
||||||
|
<input type="password" id="lama" name="lama" autocomplete="current-password" required autofocus>
|
||||||
|
<label for="baru" style="display:block;font-size:14px;margin:12px 0 3px">Password baru <span style="color:var(--grey);font-weight:normal">(minimal 4 karakter)</span></label>
|
||||||
|
<input type="password" id="baru" name="baru" autocomplete="new-password" minlength="4" required>
|
||||||
|
<label for="ulang" style="display:block;font-size:14px;margin:12px 0 3px">Ulangi password baru</label>
|
||||||
|
<input type="password" id="ulang" name="ulang" autocomplete="new-password" minlength="4" required>
|
||||||
|
<div class="aksi-baris" style="border-top:none;padding-top:10px">
|
||||||
|
<button class="tombol" type="submit">Simpan Password Baru</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user