Tambah peran Dosen MK/RPS+MK, komponen penilaian baku, dan bantu isi RPS
- Peran dosen dipecah jadi dosen_mk (nilai & portofolio saja, RPS baca-only) dan dosen_rps_mk (bisa edit RPS juga); migrasi otomatis dari skema lama. - Rekap Bobot Penilaian RPS pakai 6 komponen teknik asesmen baku prodi; total wajib tepat 100%, komponen individual boleh dikosongkan. - Dropdown & daftar Sub-CPMK tak lagi terpotong teksnya. - Bagian F (Rencana Mingguan): tombol "Isi Otomatis" per baris (termasuk usulan Bobot) yang muncul setelah Sub-CPMK dipilih, plus panel contoh teks bantu per kolom. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,39 @@ def get_conn() -> sqlite3.Connection:
|
|||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def _migrasi_role_pengguna(conn: sqlite3.Connection) -> None:
|
||||||
|
"""Skema lama hanya kenal role 'dosen'; pecah jadi dosen_mk/dosen_rps_mk.
|
||||||
|
|
||||||
|
Akun 'dosen' lama dipetakan ke 'dosen_rps_mk' agar hak akses (termasuk
|
||||||
|
edit RPS) yang sudah mereka miliki tidak berkurang oleh migrasi ini.
|
||||||
|
"""
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='pengguna'"
|
||||||
|
).fetchone()
|
||||||
|
if not row or "dosen_rps_mk" in row["sql"]:
|
||||||
|
return
|
||||||
|
conn.execute("ALTER TABLE pengguna RENAME TO pengguna_lama")
|
||||||
|
conn.execute(
|
||||||
|
"""CREATE TABLE pengguna (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
nama TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL
|
||||||
|
CHECK (role IN ('kaprodi','dosen_mk','dosen_rps_mk')),
|
||||||
|
pw_hash TEXT NOT NULL,
|
||||||
|
dibuat TEXT NOT NULL
|
||||||
|
)"""
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO pengguna (id, username, nama, role, pw_hash, dibuat)
|
||||||
|
SELECT id, username, nama,
|
||||||
|
CASE WHEN role='dosen' THEN 'dosen_rps_mk' ELSE role END,
|
||||||
|
pw_hash, dibuat
|
||||||
|
FROM pengguna_lama"""
|
||||||
|
)
|
||||||
|
conn.execute("DROP TABLE pengguna_lama")
|
||||||
|
|
||||||
|
|
||||||
def init_db() -> None:
|
def init_db() -> None:
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
for tabel in ("rps", "portofolio", "nilai"):
|
for tabel in ("rps", "portofolio", "nilai"):
|
||||||
@@ -141,11 +174,13 @@ def init_db() -> None:
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT NOT NULL UNIQUE,
|
username TEXT NOT NULL UNIQUE,
|
||||||
nama TEXT NOT NULL,
|
nama TEXT NOT NULL,
|
||||||
role TEXT NOT NULL CHECK (role IN ('kaprodi','dosen')),
|
role TEXT NOT NULL
|
||||||
|
CHECK (role IN ('kaprodi','dosen_mk','dosen_rps_mk')),
|
||||||
pw_hash TEXT NOT NULL,
|
pw_hash TEXT NOT NULL,
|
||||||
dibuat TEXT NOT NULL
|
dibuat TEXT NOT NULL
|
||||||
)"""
|
)"""
|
||||||
)
|
)
|
||||||
|
_migrasi_role_pengguna(conn)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""CREATE TABLE IF NOT EXISTS sesi (
|
"""CREATE TABLE IF NOT EXISTS sesi (
|
||||||
token TEXT PRIMARY KEY,
|
token TEXT PRIMARY KEY,
|
||||||
|
|||||||
@@ -218,7 +218,10 @@ def build_rps(d: dict) -> bytes:
|
|||||||
r.bold = True
|
r.bold = True
|
||||||
r.font.size = Pt(11)
|
r.font.size = Pt(11)
|
||||||
|
|
||||||
komponen = d.get("komponen", [])
|
komponen = [
|
||||||
|
k for k in d.get("komponen", [])
|
||||||
|
if k.get("subs") or str(k.get("bobot") or "").strip() not in ("", "0", "0.0")
|
||||||
|
]
|
||||||
tk = doc.add_table(rows=1 + max(len(komponen), 1), cols=4)
|
tk = doc.add_table(rows=1 + max(len(komponen), 1), cols=4)
|
||||||
_grid_borders(tk)
|
_grid_borders(tk)
|
||||||
for ci, judul in enumerate(
|
for ci, judul in enumerate(
|
||||||
|
|||||||
+61
-27
@@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Aplikasi Penyusunan RPS & Portofolio — Prodi Informatika UNTAN."""
|
"""Aplikasi Penyusunan RPS & Portofolio — Prodi Informatika UNTAN."""
|
||||||
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -17,6 +18,9 @@ BASE = pathlib.Path(__file__).resolve().parent
|
|||||||
app = FastAPI(title="RPS & Portofolio — Informatika UNTAN")
|
app = FastAPI(title="RPS & Portofolio — Informatika UNTAN")
|
||||||
app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static")
|
app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static")
|
||||||
templates = Jinja2Templates(directory=str(BASE / "templates"))
|
templates = Jinja2Templates(directory=str(BASE / "templates"))
|
||||||
|
templates.env.filters["tojson"] = (
|
||||||
|
lambda v: json.dumps(v, ensure_ascii=False).replace("</", "<\\/")
|
||||||
|
)
|
||||||
|
|
||||||
db.init_db()
|
db.init_db()
|
||||||
db.kurikulum()
|
db.kurikulum()
|
||||||
@@ -46,9 +50,19 @@ def render(request: Request, nama: str, ctx: dict):
|
|||||||
return templates.TemplateResponse(request, nama, ctx)
|
return templates.TemplateResponse(request, nama, ctx)
|
||||||
|
|
||||||
|
|
||||||
def boleh_kelola(user: dict, kode: str) -> bool:
|
def boleh_rps(user: dict, kode: str) -> bool:
|
||||||
"""Pengampu MK ini atau kaprodi — syarat edit & akses nilai."""
|
"""Kaprodi, atau dosen RPS+MK yang ditugaskan ke MK ini — syarat edit RPS."""
|
||||||
return user["role"] == "kaprodi" or kode in db.mk_diampu(user["id"])
|
if user["role"] == "kaprodi":
|
||||||
|
return True
|
||||||
|
return user["role"] == "dosen_rps_mk" and kode in db.mk_diampu(user["id"])
|
||||||
|
|
||||||
|
|
||||||
|
def boleh_mk(user: dict, kode: str) -> bool:
|
||||||
|
"""Kaprodi, atau dosen (MK / RPS+MK) yang ditugaskan ke MK ini —
|
||||||
|
syarat edit nilai & portofolio."""
|
||||||
|
if user["role"] == "kaprodi":
|
||||||
|
return True
|
||||||
|
return user["role"] in ("dosen_mk", "dosen_rps_mk") and kode in db.mk_diampu(user["id"])
|
||||||
|
|
||||||
|
|
||||||
def _masuk(user_id: int) -> RedirectResponse:
|
def _masuk(user_id: int) -> RedirectResponse:
|
||||||
@@ -147,7 +161,8 @@ async def admin_buat_pengguna(request: Request):
|
|||||||
form = await request.form()
|
form = await request.form()
|
||||||
nip = _rapikan_nip(form.get("nip") or "")
|
nip = _rapikan_nip(form.get("nip") or "")
|
||||||
nama = (form.get("nama") or "").strip()
|
nama = (form.get("nama") or "").strip()
|
||||||
role = "kaprodi" if form.get("role") == "kaprodi" else "dosen"
|
role_in = form.get("role")
|
||||||
|
role = role_in if role_in in ("kaprodi", "dosen_mk", "dosen_rps_mk") else "dosen_mk"
|
||||||
if not nip or not nama:
|
if not nip or not nama:
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
"/admin?pesan=Nama+dan+NIP+wajib+diisi.", status_code=303)
|
"/admin?pesan=Nama+dan+NIP+wajib+diisi.", status_code=303)
|
||||||
@@ -243,15 +258,33 @@ async def admin_impor_mahasiswa(request: Request):
|
|||||||
return RedirectResponse(f"/admin?pesan={pesan}", status_code=303)
|
return RedirectResponse(f"/admin?pesan={pesan}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
# bobot default komponen — dari sheet `Asessement` draft kurikulum (Tabel 25)
|
# komponen penilaian tetap — teknik asesmen baku prodi; bobot boleh 0 (opsional,
|
||||||
|
# tak dipakai MK ybs) tapi total seluruh komponen tak boleh melebihi 100%.
|
||||||
KOMPONEN_DEFAULT = [
|
KOMPONEN_DEFAULT = [
|
||||||
{"nama": "Kuis", "subs": [], "bobot": 15},
|
{"nama": "Partisipasi (Kuis)", "subs": [], "bobot": 10},
|
||||||
{"nama": "Ujian Tengah Semester (UTS)", "subs": [], "bobot": 20},
|
{"nama": "Observasi (Praktik/Tugas)", "subs": [], "bobot": 20},
|
||||||
{"nama": "Ujian Akhir Semester (UAS)", "subs": [], "bobot": 30},
|
{"nama": "Unjuk Kerja (Presentasi)", "subs": [], "bobot": 10},
|
||||||
{"nama": "Tugas / Praktikum / Proyek", "subs": [], "bobot": 35},
|
{"nama": "Tes Tulis (UTS)", "subs": [], "bobot": 25},
|
||||||
|
{"nama": "Tes Tulis (UAS)", "subs": [], "bobot": 25},
|
||||||
|
{"nama": "Tes Lisan (Tugas Kelompok)", "subs": [], "bobot": 10},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalisasi_komponen(tersimpan: list[dict]) -> list[dict]:
|
||||||
|
"""Selaraskan komponen tersimpan ke struktur baku KOMPONEN_DEFAULT (cocokkan
|
||||||
|
berdasar nama; RPS lama dengan nama komponen berbeda kehilangan isian itu)."""
|
||||||
|
by_nama = {k.get("nama"): k for k in tersimpan}
|
||||||
|
out = []
|
||||||
|
for default in KOMPONEN_DEFAULT:
|
||||||
|
ada = by_nama.get(default["nama"])
|
||||||
|
out.append({
|
||||||
|
"nama": default["nama"],
|
||||||
|
"subs": ada.get("subs", []) if ada else [],
|
||||||
|
"bobot": ada.get("bobot", "") if ada else "",
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def minggu_default() -> list[dict]:
|
def minggu_default() -> list[dict]:
|
||||||
rows = []
|
rows = []
|
||||||
for w in range(1, 17):
|
for w in range(1, 17):
|
||||||
@@ -289,7 +322,7 @@ def beranda(request: Request, semua: int = 0):
|
|||||||
n_total = len(daftar)
|
n_total = len(daftar)
|
||||||
mk_saya = db.mk_diampu(request.state.user["id"])
|
mk_saya = db.mk_diampu(request.state.user["id"])
|
||||||
# dosen: bawaan hanya MK yang diampu; ?semua=1 menampilkan seluruh MK
|
# dosen: bawaan hanya MK yang diampu; ?semua=1 menampilkan seluruh MK
|
||||||
hanya_saya = (request.state.user["role"] == "dosen"
|
hanya_saya = (request.state.user["role"] != "kaprodi"
|
||||||
and bool(mk_saya) and not semua)
|
and bool(mk_saya) and not semua)
|
||||||
if hanya_saya:
|
if hanya_saya:
|
||||||
daftar = [m for m in daftar if m["kode"] in mk_saya]
|
daftar = [m for m in daftar if m["kode"] in mk_saya]
|
||||||
@@ -324,6 +357,7 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
|
|||||||
if tersimpan:
|
if tersimpan:
|
||||||
f = tersimpan["data"]
|
f = tersimpan["data"]
|
||||||
f.setdefault("referensi_teks", "\n".join(f.get("referensi", [])))
|
f.setdefault("referensi_teks", "\n".join(f.get("referensi", [])))
|
||||||
|
f["komponen"] = _normalisasi_komponen(f.get("komponen", []))
|
||||||
ctx_status = tersimpan
|
ctx_status = tersimpan
|
||||||
else:
|
else:
|
||||||
f = form_kosong()
|
f = form_kosong()
|
||||||
@@ -339,17 +373,19 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
|
|||||||
{"peta": peta, "f": f, "status": ctx_status["status"],
|
{"peta": peta, "f": f, "status": ctx_status["status"],
|
||||||
"updated_at": ctx_status["updated_at"], "updated_by": ctx_status["updated_by"],
|
"updated_at": ctx_status["updated_at"], "updated_by": ctx_status["updated_by"],
|
||||||
"tahun": db.tahun_aktif(), "pesan": pesan,
|
"tahun": db.tahun_aktif(), "pesan": pesan,
|
||||||
"boleh_edit": boleh_kelola(request.state.user, kode),
|
"boleh_edit": boleh_rps(request.state.user, kode),
|
||||||
"tahun_lain": db.tahun_rps_lain(kode) if not tersimpan else []},
|
"tahun_lain": db.tahun_rps_lain(kode) if not tersimpan else [],
|
||||||
|
"sub_uraian": {s["kode"]: s["uraian"] for s in peta["subs"]}},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/rps/{kode}/salin")
|
@app.post("/rps/{kode}/salin")
|
||||||
async def salin_rps(request: Request, kode: str):
|
async def salin_rps(request: Request, kode: str):
|
||||||
"""Duplikasi RPS dari tahun ajaran lain ke tahun aktif (status: draft)."""
|
"""Duplikasi RPS dari tahun ajaran lain ke tahun aktif (status: draft)."""
|
||||||
if not boleh_kelola(request.state.user, kode):
|
if not boleh_rps(request.state.user, kode):
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/rps/{kode}?pesan=Anda+bukan+pengampu+MK+ini.", status_code=303)
|
f"/rps/{kode}?pesan=Anda+tidak+memiliki+hak+edit+RPS+untuk+MK+ini.",
|
||||||
|
status_code=303)
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
dari = form.get("dari") or ""
|
dari = form.get("dari") or ""
|
||||||
sumber = db.ambil_rps(kode, tahun=dari) if dari else None
|
sumber = db.ambil_rps(kode, tahun=dari) if dari else None
|
||||||
@@ -371,9 +407,10 @@ async def simpan_rps(request: Request, kode: str):
|
|||||||
peta = db.peta_mk(kode)
|
peta = db.peta_mk(kode)
|
||||||
if not peta:
|
if not peta:
|
||||||
return RedirectResponse("/", status_code=303)
|
return RedirectResponse("/", status_code=303)
|
||||||
if not boleh_kelola(request.state.user, kode):
|
if not boleh_rps(request.state.user, kode):
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/rps/{kode}?pesan=Anda+bukan+pengampu+MK+ini.", status_code=303)
|
f"/rps/{kode}?pesan=Anda+tidak+memiliki+hak+edit+RPS+untuk+MK+ini.",
|
||||||
|
status_code=303)
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
|
|
||||||
identitas = {
|
identitas = {
|
||||||
@@ -396,12 +433,9 @@ async def simpan_rps(request: Request, kode: str):
|
|||||||
"bobot": (form.get(f"m{w}_bobot") or "").strip(),
|
"bobot": (form.get(f"m{w}_bobot") or "").strip(),
|
||||||
})
|
})
|
||||||
komponen = []
|
komponen = []
|
||||||
for i in range(1, len(KOMPONEN_DEFAULT) + 1):
|
for i, default in enumerate(KOMPONEN_DEFAULT, 1):
|
||||||
nama = (form.get(f"k{i}_nama") or "").strip()
|
|
||||||
if not nama:
|
|
||||||
continue
|
|
||||||
komponen.append({
|
komponen.append({
|
||||||
"nama": nama,
|
"nama": default["nama"],
|
||||||
"subs": [s for s in form.getlist(f"k{i}_subs")],
|
"subs": [s for s in form.getlist(f"k{i}_subs")],
|
||||||
"bobot": (form.get(f"k{i}_bobot") or "").strip(),
|
"bobot": (form.get(f"k{i}_bobot") or "").strip(),
|
||||||
})
|
})
|
||||||
@@ -540,7 +574,7 @@ def _gabung_nilai(lama: dict, baru: list[dict], sumber: str, oleh: str) -> dict:
|
|||||||
def form_nilai(request: Request, kode: str, pesan: str = ""):
|
def form_nilai(request: Request, kode: str, pesan: str = ""):
|
||||||
if not db.peta_mk(kode):
|
if not db.peta_mk(kode):
|
||||||
return RedirectResponse("/", status_code=303)
|
return RedirectResponse("/", status_code=303)
|
||||||
if not boleh_kelola(request.state.user, kode):
|
if not boleh_mk(request.state.user, kode):
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/rps/{kode}?pesan=Nilai+mahasiswa+hanya+dibuka+untuk+pengampu+MK+dan+kaprodi.",
|
f"/rps/{kode}?pesan=Nilai+mahasiswa+hanya+dibuka+untuk+pengampu+MK+dan+kaprodi.",
|
||||||
status_code=303)
|
status_code=303)
|
||||||
@@ -552,7 +586,7 @@ def template_nilai(request: Request, kode: str):
|
|||||||
peta = db.peta_mk(kode)
|
peta = db.peta_mk(kode)
|
||||||
if not peta:
|
if not peta:
|
||||||
return RedirectResponse("/", status_code=303)
|
return RedirectResponse("/", status_code=303)
|
||||||
if not boleh_kelola(request.state.user, kode):
|
if not boleh_mk(request.state.user, kode):
|
||||||
return RedirectResponse(f"/rps/{kode}", status_code=303)
|
return RedirectResponse(f"/rps/{kode}", status_code=303)
|
||||||
isi = nilai_xlsx.buat_template(peta, db.tahun_aktif(), db.mahasiswa_mk(kode))
|
isi = nilai_xlsx.buat_template(peta, db.tahun_aktif(), db.mahasiswa_mk(kode))
|
||||||
return Response(
|
return Response(
|
||||||
@@ -568,7 +602,7 @@ async def simpan_nilai(request: Request, kode: str):
|
|||||||
peta = db.peta_mk(kode)
|
peta = db.peta_mk(kode)
|
||||||
if not peta:
|
if not peta:
|
||||||
return RedirectResponse("/", status_code=303)
|
return RedirectResponse("/", status_code=303)
|
||||||
if not boleh_kelola(request.state.user, kode):
|
if not boleh_mk(request.state.user, kode):
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/rps/{kode}?pesan=Nilai+mahasiswa+hanya+dibuka+untuk+pengampu+MK+dan+kaprodi.",
|
f"/rps/{kode}?pesan=Nilai+mahasiswa+hanya+dibuka+untuk+pengampu+MK+dan+kaprodi.",
|
||||||
status_code=303)
|
status_code=303)
|
||||||
@@ -705,7 +739,7 @@ def form_porto(request: Request, kode: str, pesan: str = ""):
|
|||||||
"status": tersimpan["status"] if tersimpan else "",
|
"status": tersimpan["status"] if tersimpan else "",
|
||||||
"updated_at": tersimpan["updated_at"] if tersimpan else "",
|
"updated_at": tersimpan["updated_at"] if tersimpan else "",
|
||||||
"updated_by": tersimpan["updated_by"] if tersimpan else "",
|
"updated_by": tersimpan["updated_by"] if tersimpan else "",
|
||||||
"boleh_edit": boleh_kelola(request.state.user, kode),
|
"boleh_edit": boleh_mk(request.state.user, kode),
|
||||||
"ada_rps": bool(rps), "ada_nilai": bool(nilai)},
|
"ada_rps": bool(rps), "ada_nilai": bool(nilai)},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -714,7 +748,7 @@ def form_porto(request: Request, kode: str, pesan: str = ""):
|
|||||||
async def simpan_porto(request: Request, kode: str):
|
async def simpan_porto(request: Request, kode: str):
|
||||||
if not db.peta_mk(kode):
|
if not db.peta_mk(kode):
|
||||||
return RedirectResponse("/", status_code=303)
|
return RedirectResponse("/", status_code=303)
|
||||||
if not boleh_kelola(request.state.user, kode):
|
if not boleh_mk(request.state.user, kode):
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/portofolio/{kode}?pesan=Anda+bukan+pengampu+MK+ini.", status_code=303)
|
f"/portofolio/{kode}?pesan=Anda+bukan+pengampu+MK+ini.", status_code=303)
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
@@ -810,7 +844,7 @@ def _bahan_porto(kode: str) -> dict | None:
|
|||||||
@app.get("/portofolio/{kode}/unduh")
|
@app.get("/portofolio/{kode}/unduh")
|
||||||
def unduh_porto(request: Request, kode: str):
|
def unduh_porto(request: Request, kode: str):
|
||||||
# dokumen memuat matriks nilai per mahasiswa — batasi pengampu + kaprodi
|
# dokumen memuat matriks nilai per mahasiswa — batasi pengampu + kaprodi
|
||||||
if not boleh_kelola(request.state.user, kode):
|
if not boleh_mk(request.state.user, kode):
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
f"/portofolio/{kode}?pesan=Unduhan+portofolio+memuat+nilai+mahasiswa+-+hanya+pengampu+dan+kaprodi.",
|
f"/portofolio/{kode}?pesan=Unduhan+portofolio+memuat+nilai+mahasiswa+-+hanya+pengampu+dan+kaprodi.",
|
||||||
status_code=303)
|
status_code=303)
|
||||||
|
|||||||
@@ -5,15 +5,17 @@
|
|||||||
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
|
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
|
||||||
|
|
||||||
<h2 class="bagian">A. Akun Pengguna & Penugasan Mata Kuliah</h2>
|
<h2 class="bagian">A. Akun Pengguna & Penugasan Mata Kuliah</h2>
|
||||||
<p class="keterangan">Dosen hanya dapat mengedit RPS/portofolio dan melihat nilai
|
<p class="keterangan"><b>Dosen RPS + MK</b> dapat mengedit RPS, nilai, dan portofolio
|
||||||
mahasiswa pada MK yang ditugaskan kepadanya. Kaprodi dapat mengakses semuanya.</p>
|
pada MK yang ditugaskan. <b>Dosen MK</b> hanya dapat mengedit nilai & portofolio
|
||||||
|
pada MK yang ditugaskan — halaman RPS terbuka hanya untuk dibaca. Kaprodi dapat
|
||||||
|
mengakses semuanya.</p>
|
||||||
<div class="gulir">
|
<div class="gulir">
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:170px">Nama</th>
|
<th style="width:170px">Nama</th>
|
||||||
<th style="width:160px">NIP</th>
|
<th style="width:160px">NIP</th>
|
||||||
<th style="width:90px">Peran</th>
|
<th style="width:130px">Peran</th>
|
||||||
<th>MK yang Diampu</th>
|
<th>MK yang Diampu</th>
|
||||||
<th style="width:340px">Tindakan</th>
|
<th style="width:340px">Tindakan</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ p.nama }}</td>
|
<td>{{ p.nama }}</td>
|
||||||
<td>{{ p.username }}</td>
|
<td>{{ p.username }}</td>
|
||||||
<td>{{ 'Kaprodi' if p.role == 'kaprodi' else 'Dosen' }}</td>
|
<td>{{ {'kaprodi': 'Kaprodi', 'dosen_rps_mk': 'Dosen RPS + MK', 'dosen_mk': 'Dosen MK'}[p.role] }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if p.role == 'kaprodi' %}
|
{% if p.role == 'kaprodi' %}
|
||||||
<i style="color:var(--grey)">semua MK</i>
|
<i style="color:var(--grey)">semua MK</i>
|
||||||
@@ -71,8 +73,9 @@
|
|||||||
<label>Nama lengkap <input type="text" name="nama" required style="width:230px"></label>
|
<label>Nama lengkap <input type="text" name="nama" required style="width:230px"></label>
|
||||||
<label>NIP <input type="text" name="nip" required style="width:180px"></label>
|
<label>NIP <input type="text" name="nip" required style="width:180px"></label>
|
||||||
<label>Peran
|
<label>Peran
|
||||||
<select name="role" style="width:110px">
|
<select name="role" style="width:150px">
|
||||||
<option value="dosen" selected>Dosen</option>
|
<option value="dosen_mk" selected>Dosen MK</option>
|
||||||
|
<option value="dosen_rps_mk">Dosen RPS + MK</option>
|
||||||
<option value="kaprodi">Kaprodi</option>
|
<option value="kaprodi">Kaprodi</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
{% if user.role == 'kaprodi' %}{{ tautan('/admin', 'Kelola Akun & Mahasiswa') }}{% endif %}
|
{% if user.role == 'kaprodi' %}{{ tautan('/admin', 'Kelola Akun & Mahasiswa') }}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<span class="menu-kanan">
|
<span class="menu-kanan">
|
||||||
<b>{{ user.nama }}</b> — {{ 'Kaprodi' if user.role == 'kaprodi' else 'Dosen' }}
|
<b>{{ user.nama }}</b> — {{ {'kaprodi': 'Kaprodi', 'dosen_rps_mk': 'Dosen RPS + MK', 'dosen_mk': 'Dosen MK'}[user.role] }}
|
||||||
· <a href="/keluar">Keluar</a>
|
· <a href="/keluar">Keluar</a>
|
||||||
</span>
|
</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div><b id="kpi-porto">{{ st_porto.values()|selectattr('status', 'equalto', 'selesai')|list|length }}</b> <span>portofolio selesai</span> <span class="kpi-pct" id="pct-porto"></span></div>
|
<div><b id="kpi-porto">{{ st_porto.values()|selectattr('status', 'equalto', 'selesai')|list|length }}</b> <span>portofolio selesai</span> <span class="kpi-pct" id="pct-porto"></span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user.role == 'dosen' %}
|
{% if user.role != 'kaprodi' %}
|
||||||
{% if hanya_saya %}
|
{% if hanya_saya %}
|
||||||
<div class="info" style="margin-top:0">Menampilkan <b>{{ daftar|length }} MK yang Anda ampu</b>.
|
<div class="info" style="margin-top:0">Menampilkan <b>{{ daftar|length }} MK yang Anda ampu</b>.
|
||||||
RPS mata kuliah lain tetap terbuka baca — <a href="/?semua=1">tampilkan seluruh {{ n_total }} MK</a>.</div>
|
RPS mata kuliah lain tetap terbuka baca — <a href="/?semua=1">tampilkan seluruh {{ n_total }} MK</a>.</div>
|
||||||
|
|||||||
@@ -16,8 +16,10 @@
|
|||||||
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
|
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
|
||||||
|
|
||||||
{% if not boleh_edit %}
|
{% if not boleh_edit %}
|
||||||
<div class="info">Anda bukan pengampu MK ini — form terbuka <b>hanya untuk dibaca</b>.
|
<div class="info">Anda tidak memiliki hak edit RPS untuk MK ini — form terbuka
|
||||||
Hubungi kaprodi bila penugasan MK Anda perlu diubah.</div>
|
<b>hanya untuk dibaca</b>. Ini karena Anda bukan pengampu MK ini, atau peran Anda
|
||||||
|
<b>Dosen MK</b> (hanya mengelola nilai & portofolio). Hubungi kaprodi bila ini
|
||||||
|
perlu diubah.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if tahun_lain and boleh_edit %}
|
{% if tahun_lain and boleh_edit %}
|
||||||
@@ -97,7 +99,23 @@
|
|||||||
<textarea name="media" rows="2" placeholder="mis. LMS (e-learning UNTAN), LCD proyektor, perangkat lunak pendukung.">{{ f.media }}</textarea>
|
<textarea name="media" rows="2" placeholder="mis. LMS (e-learning UNTAN), LCD proyektor, perangkat lunak pendukung.">{{ f.media }}</textarea>
|
||||||
|
|
||||||
<h2 class="bagian">F. RENCANA MINGGUAN <span id="total-bobot-f" style="font-weight:normal">— total bobot: <b>0</b>%</span></h2>
|
<h2 class="bagian">F. RENCANA MINGGUAN <span id="total-bobot-f" style="font-weight:normal">— total bobot: <b>0</b>%</span></h2>
|
||||||
<p class="keterangan">Minggu 8 = UTS, minggu 16 = UAS. Total bobot seluruh minggu harus 100 %.</p>
|
<p class="keterangan">Minggu 8 = UTS, minggu 16 = UAS. Total bobot seluruh minggu harus 100 %.
|
||||||
|
{% if boleh_edit %}Pilih Sub-CPMK lalu klik <b>Isi Otomatis</b> untuk mengisi kolom kosong dengan teks
|
||||||
|
bantu (termasuk usulan Bobot — UTS 20%, UAS 30%, minggu biasa 3,5% — sesuaikan lagi agar totalnya
|
||||||
|
pas 100%).{% endif %}</p>
|
||||||
|
{% if boleh_edit %}
|
||||||
|
<details style="margin:0 0 10px">
|
||||||
|
<summary style="cursor:pointer;color:var(--blue);font-size:13px">Lihat contoh teks bantu per kolom</summary>
|
||||||
|
<div class="info" style="margin-top:6px;line-height:1.7">
|
||||||
|
<b>Indikator</b> — turunan Sub-CPMK, mis. "Mahasiswa mampu menjelaskan …"; "Ketepatan dan kelengkapan dalam …"<br>
|
||||||
|
<b>Kriteria & Teknik</b> — mis. "Rubrik holistik; tes tertulis" / "Ketepatan jawaban; unjuk kerja" / "Kelengkapan tugas; observasi"<br>
|
||||||
|
<b>Tatap Muka</b> — mis. "Ceramah, diskusi, tanya jawab [TM: 2x50 menit]" / "Studi kasus & presentasi kelompok [TM: 2x50 menit]"<br>
|
||||||
|
<b>Daring</b> — mis. "Membaca modul & kuis di LMS [BT+BM: 2x60 menit]" / "Diskusi forum LMS [BT+BM: 2x60 menit]"<br>
|
||||||
|
<b>Kegiatan Pembelajaran</b> — mis. "Mahasiswa menyimak paparan dosen, berdiskusi, dan mengerjakan latihan terkait materi minggu ini."<br>
|
||||||
|
<b>Materi [Pustaka]</b> — rujuk nomor pustaka di Bagian H, mis. "Bab 3 (Pustaka Utama #1)"
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
<div class="gulir">
|
<div class="gulir">
|
||||||
<table class="grid" id="tabel-f">
|
<table class="grid" id="tabel-f">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -122,14 +140,20 @@
|
|||||||
<tr class="{% if m.ke == 8 %}uts{% elif m.ke == 16 %}uas{% endif %}">
|
<tr class="{% if m.ke == 8 %}uts{% elif m.ke == 16 %}uas{% endif %}">
|
||||||
<td class="tengah">{{ m.ke }}</td>
|
<td class="tengah">{{ m.ke }}</td>
|
||||||
<td class="isian">
|
<td class="isian">
|
||||||
<select name="m{{ m.ke }}_sub">
|
<select name="m{{ m.ke }}_sub" {% if boleh_edit %}onchange="document.getElementById('btn-isi-m{{ m.ke }}').style.display = this.value ? '' : 'none'"{% endif %}>
|
||||||
<option value="">— pilih —</option>
|
<option value="">— pilih —</option>
|
||||||
{% for s in peta.subs %}
|
{% for s in peta.subs %}
|
||||||
<option value="{{ s.kode }}" {% if m.sub == s.kode %}selected{% endif %}>{{ s.kode }} — {{ s.uraian[:60] }}{% if s.uraian|length > 60 %}…{% endif %}</option>
|
<option value="{{ s.kode }}" {% if m.sub == s.kode %}selected{% endif %}>{{ s.kode }} — {{ s.uraian }}</option>
|
||||||
|
<option disabled>──────────────────────</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<option value="__uts__" {% if m.sub == '__uts__' %}selected{% endif %}>Sub-CPMK minggu 1–7 (UTS)</option>
|
<option value="__uts__" {% if m.sub == '__uts__' %}selected{% endif %}>Sub-CPMK minggu 1–7 (UTS)</option>
|
||||||
<option value="__uas__" {% if m.sub == '__uas__' %}selected{% endif %}>Sub-CPMK minggu 9–15 (UAS)</option>
|
<option value="__uas__" {% if m.sub == '__uas__' %}selected{% endif %}>Sub-CPMK minggu 9–15 (UAS)</option>
|
||||||
</select>
|
</select>
|
||||||
|
{% if boleh_edit %}
|
||||||
|
<button type="button" id="btn-isi-m{{ m.ke }}" class="tombol garis"
|
||||||
|
style="padding:1px 6px;font-size:10px;margin-top:4px;{% if not m.sub %}display:none{% endif %}"
|
||||||
|
onclick="isiOtomatis({{ m.ke }})" title="Isi kolom kosong dengan teks bantu dari Sub-CPMK terpilih">Isi Otomatis</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="isian"><textarea name="m{{ m.ke }}_indikator" rows="2">{{ m.indikator }}</textarea></td>
|
<td class="isian"><textarea name="m{{ m.ke }}_indikator" rows="2">{{ m.indikator }}</textarea></td>
|
||||||
<td class="isian"><textarea name="m{{ m.ke }}_kriteria" rows="2">{{ m.kriteria }}</textarea></td>
|
<td class="isian"><textarea name="m{{ m.ke }}_kriteria" rows="2">{{ m.kriteria }}</textarea></td>
|
||||||
@@ -145,7 +169,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="bagian">G. REKAP BOBOT PENILAIAN <span id="total-bobot-g" style="font-weight:normal">— total: <b>0</b>%</span></h2>
|
<h2 class="bagian">G. REKAP BOBOT PENILAIAN <span id="total-bobot-g" style="font-weight:normal">— total: <b>0</b>%</span></h2>
|
||||||
<p class="keterangan">Centang Sub-CPMK yang dinilai tiap komponen. Setiap Sub-CPMK harus tercakup minimal satu komponen agar CPMK/CPL-nya terukur.</p>
|
<p class="keterangan">Enam komponen teknik asesmen baku prodi. Bobot boleh dikosongkan untuk
|
||||||
|
komponen yang tidak dipakai MK ini (opsional), tapi total seluruh komponen yang diisi harus
|
||||||
|
tepat 100%. Centang Sub-CPMK yang dinilai tiap komponen; setiap Sub-CPMK harus tercakup minimal
|
||||||
|
satu komponen agar CPMK/CPL-nya terukur.</p>
|
||||||
<div class="gulir">
|
<div class="gulir">
|
||||||
<table class="grid">
|
<table class="grid">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -155,11 +182,11 @@
|
|||||||
{% for k in f.komponen %}
|
{% for k in f.komponen %}
|
||||||
{% set ki = loop.index %}
|
{% set ki = loop.index %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="isian"><input type="text" name="k{{ ki }}_nama" value="{{ k.nama }}"></td>
|
<td>{{ k.nama }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="pilih-sub">
|
<div class="pilih-sub">
|
||||||
{% for s in peta.subs %}
|
{% for s in peta.subs %}
|
||||||
<label><input type="checkbox" class="sub-k" name="k{{ ki }}_subs" value="{{ s.kode }}" {% if s.kode in k.subs %}checked{% endif %}>{{ s.kode }} — {{ s.uraian[:70] }}{% if s.uraian|length > 70 %}…{% endif %}</label>
|
<label><input type="checkbox" class="sub-k" name="k{{ ki }}_subs" value="{{ s.kode }}" {% if s.kode in k.subs %}checked{% endif %}>{{ s.kode }} — {{ s.uraian }}</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -194,6 +221,34 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const SUB_URAIAN = {{ sub_uraian | tojson | safe }};
|
||||||
|
|
||||||
|
function isiOtomatis(mg) {
|
||||||
|
const sel = document.querySelector(`select[name="m${mg}_sub"]`);
|
||||||
|
const nilai = sel ? sel.value : '';
|
||||||
|
const isi = (nama, teks) => {
|
||||||
|
const el = document.querySelector(`[name="m${mg}_${nama}"]`);
|
||||||
|
if (el && !el.value.trim()) el.value = teks;
|
||||||
|
};
|
||||||
|
if (SUB_URAIAN[nilai]) {
|
||||||
|
const uraian = SUB_URAIAN[nilai];
|
||||||
|
isi('indikator', 'Mahasiswa ' + uraian.charAt(0).toLowerCase() + uraian.slice(1));
|
||||||
|
} else if (nilai === '__uts__') {
|
||||||
|
isi('indikator', 'Mahasiswa menguasai capaian Sub-CPMK minggu 1–7.');
|
||||||
|
} else if (nilai === '__uas__') {
|
||||||
|
isi('indikator', 'Mahasiswa menguasai capaian Sub-CPMK minggu 9–15.');
|
||||||
|
} else {
|
||||||
|
return; // belum pilih Sub-CPMK — tak ada dasar untuk mengisi
|
||||||
|
}
|
||||||
|
isi('kriteria', 'Ketepatan dan kelengkapan jawaban/tugas sesuai rubrik penilaian.');
|
||||||
|
isi('metode', 'Ceramah, diskusi, dan tanya jawab [TM: 2x50 menit]');
|
||||||
|
isi('daring', 'Membaca materi & berdiskusi di LMS [BT+BM: 2x60 menit]');
|
||||||
|
isi('kegiatan', 'Mahasiswa menyimak penjelasan dosen, berdiskusi, dan mengerjakan latihan terkait materi minggu ini.');
|
||||||
|
// bobot: usulan awal (UTS/UAS lazim lebih besar) — total tetap harus disesuaikan ke 100%
|
||||||
|
isi('bobot', mg == 8 ? '20' : mg == 16 ? '30' : '3.5');
|
||||||
|
segarkan();
|
||||||
|
}
|
||||||
|
|
||||||
function hitungTotal(kelas, idLabel) {
|
function hitungTotal(kelas, idLabel) {
|
||||||
let t = 0;
|
let t = 0;
|
||||||
document.querySelectorAll(kelas).forEach(el => { t += parseFloat(el.value) || 0; });
|
document.querySelectorAll(kelas).forEach(el => { t += parseFloat(el.value) || 0; });
|
||||||
|
|||||||
Reference in New Issue
Block a user