Tambah halaman RTM (Rancangan Tugas Mahasiswa) tersendiri, terpisah dari Nilai

Dokumen RTM resmi sesuai template baku prodi (FORM RPS & RTM UNTAN), diisi
di halaman /rps/{kode}/rtm sendiri setelah RPS ditandai selesai — blok Tugas
bisa ditambah/dihapus dinamis, dan bisa diunduh sebagai .docx. Murni
dokumentasi, tak menyimpan atau memengaruhi nilai/skor mahasiswa sama sekali.
This commit is contained in:
Power BI Dev
2026-07-27 07:51:27 +07:00
parent b85c75bb31
commit af369883f3
8 changed files with 485 additions and 7 deletions
+114
View File
@@ -13,6 +13,7 @@ from fastapi.templating import Jinja2Templates
from . import auth, db, hitung, nilai_xlsx
from .generator.portofolio_docx import build_portofolio
from .generator.rps_untan import build_rps
from .generator.rtm_docx import build_rtm
BASE = pathlib.Path(__file__).resolve().parent
@@ -358,6 +359,43 @@ def form_kosong() -> dict:
}
# ── RTM (Rancangan Tugas Mahasiswa) — dokumen sendiri, terpisah dari Nilai ──
RTM_TUGAS_KOSONG = {
"judul": "", "subs": [], "bentuk_tugas": "", "deskripsi": "",
"metode_pengerjaan": "", "luaran": "", "indikator_bobot": "",
"alokasi_waktu": "", "tanggal": "", "lain_lain": "", "rujukan": "",
}
def _ambil_rtm(kode: str) -> tuple[list[dict], dict]:
"""Daftar entri Tugas RTM yang sudah tersimpan (bisa kosong — halaman RTM
menambah blok baru secara dinamis lewat JS, bukan slot tetap) — murni
dokumentasi, tak menyentuh data Nilai."""
tersimpan = db.ambil_dok("rtm", kode)
tugas = [dict(RTM_TUGAS_KOSONG, **t) for t in ((tersimpan or {}).get("data", {}).get("tugas") or [])]
meta = tersimpan or {"status": "", "updated_at": "", "updated_by": ""}
return tugas, meta
def _bahan_rtm(kode: str) -> dict | None:
peta = db.peta_mk(kode)
if not peta:
return None
rps = db.ambil_rps(kode)
ident = ((rps or {}).get("data") or {}).get("identitas") or {}
tersimpan = db.ambil_dok("rtm", kode)
tugas = [t for t in ((tersimpan or {}).get("data", {}).get("tugas") or [])
if t.get("judul") or t.get("bentuk_tugas") or t.get("deskripsi")]
return {
"tahun": db.tahun_aktif(),
"nama": peta["nama"], "kode": peta["kode"],
"sks": ident.get("sks", ""), "semester": ident.get("semester", ""),
"pengampu": ident.get("pengampu", ""),
"tugas": tugas,
}
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
@@ -594,6 +632,82 @@ def unduh_rps(kode: str):
)
@app.post("/rps/{kode}/rtm")
async def simpan_rtm(request: Request, kode: str):
"""RTM (Rancangan Tugas Mahasiswa) — dokumen sendiri, sekadar dokumentasi
rencana tugas per MK; tak ada kaitan dengan data Nilai/skor sama sekali."""
peta = db.peta_mk(kode)
if not peta:
return RedirectResponse("/", status_code=303)
if not boleh_rps(request.state.user, kode):
return RedirectResponse(
f"/rps/{kode}/rtm?pesan=Anda+tidak+memiliki+hak+edit+RPS+untuk+MK+ini.",
status_code=303)
form = await request.form()
# blok bisa ditambah/dihapus bebas di JS (lihat form_rtm.html) sehingga
# indeksnya boleh tidak berurutan — cari semua "judul_N" yang ada, bukan
# mengandalkan hitungan 0,1,2,... berurutan tanpa celah.
idxs = sorted({int(m.group(1)) for k in form.keys()
for m in [re.fullmatch(r"judul_(\d+)", k)] if m})
tugas = []
for i in idxs:
entri = {
"judul": (form.get(f"judul_{i}") or "").strip(),
"subs": form.getlist(f"subs_{i}"),
"bentuk_tugas": (form.get(f"bentuk_tugas_{i}") or "").strip(),
"deskripsi": (form.get(f"deskripsi_{i}") or "").strip(),
"metode_pengerjaan": (form.get(f"metode_pengerjaan_{i}") or "").strip(),
"luaran": (form.get(f"luaran_{i}") or "").strip(),
"indikator_bobot": (form.get(f"indikator_bobot_{i}") or "").strip(),
"alokasi_waktu": (form.get(f"alokasi_waktu_{i}") or "").strip(),
"tanggal": (form.get(f"tanggal_{i}") or "").strip(),
"lain_lain": (form.get(f"lain_lain_{i}") or "").strip(),
"rujukan": (form.get(f"rujukan_{i}") or "").strip(),
}
if entri["judul"] or entri["bentuk_tugas"] or entri["deskripsi"]:
tugas.append(entri)
db.simpan_dok("rtm", kode, {"tugas": tugas}, "selesai", request.state.user["nama"])
return RedirectResponse(
f"/rps/{kode}/rtm?pesan=RTM+tersimpan+-+{len(tugas)}+tugas+terisi.",
status_code=303)
@app.get("/rps/{kode}/rtm", response_class=HTMLResponse)
def form_rtm(request: Request, kode: str, pesan: str = ""):
peta = db.peta_mk(kode)
if not peta:
return RedirectResponse("/", status_code=303)
rps = db.ambil_rps(kode)
if not rps or rps["status"] != "selesai":
return RedirectResponse(
f"/rps/{kode}?pesan=RTM+hanya+bisa+diisi+setelah+RPS+ditandai+selesai.",
status_code=303)
rtm_tugas, rtm_meta = _ambil_rtm(kode)
return render(
request, "form_rtm.html",
{"peta": peta, "tahun": db.tahun_aktif(), "pesan": pesan,
"boleh_edit": boleh_rps(request.state.user, kode),
"rtm_tugas": rtm_tugas, "rtm_updated_at": rtm_meta.get("updated_at", ""),
"rtm_updated_by": rtm_meta.get("updated_by", "")},
)
@app.get("/rps/{kode}/rtm/unduh")
def unduh_rtm(kode: str):
bahan = _bahan_rtm(kode)
if not bahan or not bahan["tugas"]:
return RedirectResponse(
f"/rps/{kode}/rtm?pesan=Belum+ada+Tugas+RTM+yang+diisi+untuk+diunduh.",
status_code=303)
isi = build_rtm(bahan)
nama_file = re.sub(r"[^A-Za-z0-9_-]+", "_", f"RTM_{kode}_{bahan['nama']}") + ".docx"
return Response(
content=isi,
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
headers={"Content-Disposition": f'attachment; filename="{nama_file}"'},
)
# ═════════════════════════ TAHAP 2: NILAI & PENGUKURAN ═════════════════════════