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:
Power BI Dev
2026-07-21 12:44:57 +07:00
parent a45b08b00e
commit afb9e72ba2
8 changed files with 490 additions and 274 deletions
+38 -7
View File
@@ -138,6 +138,15 @@ def _migrasi_role_pengguna(conn: sqlite3.Connection) -> None:
conn.execute("DROP TABLE pengguna_lama")
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
yang hanya mengampu kelas tertentu)."""
kolom = {r["name"] for r in conn.execute("PRAGMA table_info(penugasan)")}
if "kelas" not in kolom:
conn.execute("ALTER TABLE penugasan ADD COLUMN kelas TEXT NOT NULL DEFAULT ''")
def init_db() -> None:
with get_conn() as conn:
for tabel in ("rps", "portofolio", "nilai"):
@@ -195,6 +204,7 @@ def init_db() -> None:
PRIMARY KEY (user_id, mk_kode)
)"""
)
_migrasi_kelas_penugasan(conn)
conn.execute(
"""CREATE TABLE IF NOT EXISTS mahasiswa (
npm TEXT NOT NULL,
@@ -391,12 +401,15 @@ def daftar_pengguna() -> list[dict]:
"ORDER BY role, nama"
).fetchall()
tugas = conn.execute(
"SELECT user_id, mk_kode FROM penugasan ORDER BY mk_kode"
"SELECT user_id, mk_kode, kelas FROM penugasan ORDER BY mk_kode"
).fetchall()
per_user: dict[int, list[str]] = {}
per_user_kode: dict[int, list[str]] = {}
per_user_kelas: dict[int, dict[str, str]] = {}
for t in tugas:
per_user.setdefault(t["user_id"], []).append(t["mk_kode"])
return [{**dict(r), "mk": per_user.get(r["id"], [])} for r in rows]
per_user_kode.setdefault(t["user_id"], []).append(t["mk_kode"])
per_user_kelas.setdefault(t["user_id"], {})[t["mk_kode"]] = t["kelas"] or ""
return [{**dict(r), "mk": per_user_kode.get(r["id"], []),
"kelas_map": per_user_kelas.get(r["id"], {})} for r in rows]
def hapus_pengguna(user_id: int) -> None:
@@ -413,12 +426,14 @@ def ubah_password(user_id: int, pw_hash: str) -> None:
conn.execute("DELETE FROM sesi WHERE user_id=?", (user_id,))
def set_penugasan(user_id: int, kode_list: list[str]) -> None:
def set_penugasan(user_id: int, penugasan: list[tuple[str, str]]) -> None:
"""penugasan = [(mk_kode, kelas), ...] — kelas '' berarti seluruh kelas MK
ini; boleh CSV ('A,B') bila dosen mengampu beberapa kelas MK yang sama."""
with get_conn() as conn:
conn.execute("DELETE FROM penugasan WHERE user_id=?", (user_id,))
conn.executemany(
"INSERT OR IGNORE INTO penugasan (user_id, mk_kode) VALUES (?,?)",
[(user_id, k) for k in kode_list],
"INSERT OR IGNORE INTO penugasan (user_id, mk_kode, kelas) VALUES (?,?,?)",
[(user_id, k, kelas) for k, kelas in penugasan],
)
@@ -430,6 +445,22 @@ def mk_diampu(user_id: int) -> set[str]:
return {r["mk_kode"] for r in rows}
def kelas_diampu(user_id: int, mk_kode: str) -> set[str] | None:
"""Kelas yang diizinkan dosen ini untuk MK tsb. None = seluruh kelas
(tak dibatasi); set kosong = tidak ditugaskan sama sekali ke MK ini."""
with get_conn() as conn:
row = conn.execute(
"SELECT kelas FROM penugasan WHERE user_id=? AND mk_kode=?",
(user_id, mk_kode),
).fetchone()
if not row:
return set()
kelas = (row["kelas"] or "").strip()
if not kelas:
return None
return {k.strip() for k in kelas.split(",") if k.strip()}
def pengampu_per_mk() -> dict[str, list[str]]:
"""{mk_kode: [nama dosen, ...]} — untuk kolom pengampu di beranda."""
with get_conn() as conn: