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:
+114
@@ -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 ═════════════════════════
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user