Files
OBE-Mapping/app/generator/rps_untan.py
T
Power BI Dev 374bdb5565 Perbaiki isi RPS unduhan yang nyasar ke baris salah (Peta CPL, Deskripsi, dst)
build_rps() mengisi blok naratif (Peta CPL-CPMK, Deskripsi Singkat MK,
Bahan Kajian, Pustaka, Media, Dosen Pengampu, MK Prasyarat) lewat
t.cell(18, 2) dst -- indeks baris tetap sesuai template asli (39 baris).
Tapi baris-baris itu dieksekusi SETELAH daftar CPL/CPMK/Sub-CPMK
digandakan & baris contohnya dibuang -- jumlah baris yang disisipkan
beda-beda tiap MK (tergantung berapa CPL/CPMK/Sub-CPMK-nya), jadi
t.cell(18, 2) di tabel yang sudah berubah itu tidak lagi menunjuk baris
"Peta CPL-CPMK" yang dimaksud. Teks jadi nyasar ke sel/baris lain,
termasuk kolom sempit -- terlihat sebagai puluhan halaman teks terpotong
huruf demi huruf.

Pola yang benar sudah dipakai untuk tabel mingguan (trs[31], trs[33],
dst -- referensi elemen XML baris asli yang dibekukan di awal, tak
ikut bergeser). Terapkan pola sama (helper cell()) untuk blok naratif
yang kelewat.

Diverifikasi: label & isi sekarang sejajar benar (baris "Peta CPL - CP
MK" berisi peta CPMK->CPL, bukan lagi nama dosen pengampu), dokumen
turun dari puluhan halaman rusak jadi 2 halaman rapi untuk bagian ini.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 21:42:01 +07:00

275 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Generator RPS format resmi UNTAN (standar Dikti, SN Dikti Permendikbud 3/2020).
Pendekatan: file template resmi (`templates/rps_untan.docx`) dimuat apa adanya,
sel-selnya diisi data, dan baris digandakan seperlunya — sehingga tata letak,
merge sel, dan logo dijamin identik dengan template. Ditambah satu lampiran
kisi-kisi asesmen (bukti constructive alignment) setelah tabel utama.
Peta baris tabel template (39 baris x 18 kolom):
0 kop (logo | identitas institusi | kode dokumen)
2/3 label & isian: MK, kode, rumpun, bobot T/P, semester, tgl
4/5 otorisasi: pengembang, koordinator, kaprodi
6..11 CPL (header + 5 baris contoh)
12..14 CPMK (header + 2 baris contoh)
15..17 Sub-CPMK (header + 2 baris contoh)
18..27 peta CPL-CPMK, deskripsi, bahan kajian, pustaka U/P, media,
pengampu, MK syarat
28..30 header tabel mingguan
31..37 baris contoh mingguan (31 normal, 33 UTS, 37 UAS)
38 total bobot
"""
import copy
import io
import os
import pathlib
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Pt
from docx.table import _Row
TEMPLATE = pathlib.Path(__file__).resolve().parent / "templates" / "rps_untan.docx"
FAKULTAS = os.environ.get("FAKULTAS", "FAKULTAS TEKNIK")
PRODI = os.environ.get("PRODI", "PROGRAM STUDI INFORMATIKA")
def _set_text(cell, text, align=None, bold=None):
"""Ganti isi sel; gaya paragraf/tabel template dipertahankan.
Beberapa sel template resmi (mis. contoh isian "peta CPL-CPMK") memuat
tabel kecil bersarang di antara paragraf petunjuknya. w:tc yang berakhir
dengan w:tbl tanpa w:p setelahnya melanggar skema OOXML — Word
menganggap dokumennya rusak dan mem-"perbaiki" (merapikan ulang) sendiri
saat dibuka, yang justru merusak tata letak. Buang tabel bersarang itu
sekalian saat sel ditimpa teks baru.
"""
for tbl in cell._tc.findall(qn("w:tbl")):
tbl.getparent().remove(tbl)
for p in cell.paragraphs[1:]:
p._p.getparent().remove(p._p)
p = cell.paragraphs[0]
for r in list(p.runs):
r._r.getparent().remove(r._r)
run = p.add_run()
for i, line in enumerate((text or "").split("\n")):
if i:
run.add_break()
run.add_text(line)
if bold is not None:
run.bold = bold
if align is not None:
p.alignment = align
def _unique_cells(row):
seen, out = None, []
for c in row.cells:
if c._tc is seen:
continue
seen = c._tc
out.append(c)
return out
def _clone_after(table, proto_tr, ref_tr):
"""Salin baris prototipe, kosongkan teksnya, sisipkan setelah ref_tr."""
new_tr = copy.deepcopy(proto_tr)
ref_tr.addnext(new_tr)
row = _Row(new_tr, table)
for c in _unique_cells(row):
_set_text(c, "")
return row
def _fmt(v, suffix=""):
return f"{v}{suffix}" if v not in (None, "") else ""
def _grid_borders(table):
"""Beri garis penuh pada tabel (template tidak punya style 'Table Grid')."""
tbl_pr = table._tbl.tblPr
borders = OxmlElement("w:tblBorders")
for sisi in ("top", "left", "bottom", "right", "insideH", "insideV"):
el = OxmlElement(f"w:{sisi}")
el.set(qn("w:val"), "single")
el.set(qn("w:sz"), "4")
el.set(qn("w:color"), "000000")
borders.append(el)
tbl_pr.append(borders)
def build_rps(d: dict) -> bytes:
doc = Document(str(TEMPLATE))
t = doc.tables[0]
ident = d.get("identitas", {})
# ── kop & identitas ──
_set_text(
t.cell(0, 3),
f"UNIVERSITAS TANJUNGPURA\n{FAKULTAS}\n{PRODI}",
align=WD_ALIGN_PARAGRAPH.CENTER, bold=True,
)
_set_text(t.cell(0, 16), "Kode Dokumen\n" + (ident.get("kode_dokumen") or "—"))
_set_text(t.cell(3, 0), ident.get("nama", ""), bold=True)
_set_text(t.cell(3, 5), ident.get("kode", ""))
_set_text(t.cell(3, 7), ident.get("rumpun", ""))
_set_text(t.cell(3, 10), "T = " + (ident.get("sks_teori") or "-"))
_set_text(t.cell(3, 11), "P = " + (ident.get("sks_praktikum") or "-"))
_set_text(t.cell(3, 13), ident.get("semester", ""))
_set_text(t.cell(3, 15), ident.get("tgl_penyusunan", ""))
_set_text(t.cell(5, 5), ident.get("pengembang") or ident.get("pengampu", ""))
_set_text(t.cell(5, 10), ident.get("koordinator", ""))
_set_text(t.cell(5, 13), ident.get("kaprodi", ""))
trs = [r._tr for r in t.rows] # indeks baris template asli
def cell(idx, col):
"""Ambil sel dari baris ASLI template lewat trs[idx] (referensi XML
beku), bukan t.cell(idx, col) yang ikut bergeser tiap kali baris
CPL/CPMK/Sub-CPMK digandakan/dibuang di atasnya — pakai t.cell()
langsung untuk baris di bawah blok itu akan salah sasaran."""
return _Row(trs[idx], t).cells[col]
def isi_daftar(proto_idx, hapus_idx, items, kolom_kode=2, kolom_isi=5):
"""Isi blok daftar (CPL/CPMK/Sub-CPMK): prototipe digandakan per item."""
proto = trs[proto_idx]
ref = proto
for kode, isi in items:
row = _clone_after(t, proto, ref)
_set_text(row.cells[kolom_kode], kode, bold=True)
_set_text(row.cells[kolom_isi], isi)
ref = row._tr
for i in hapus_idx:
trs[i].getparent().remove(trs[i])
proto.getparent().remove(proto)
# ── CPL / CPMK / Sub-CPMK (baris digandakan sesuai data) ──
isi_daftar(7, [8, 9, 10, 11], [(c["kode"], c["desk"]) for c in d.get("cpl", [])])
isi_daftar(13, [14], [(c["kode"], c["desk"]) for c in d.get("cpmk", [])])
isi_daftar(16, [17], [(s["kode"], s["uraian"]) for s in d.get("sub", [])])
# ── peta CPLCPMK & blok naratif ──
peta_txt = "\n".join(
f"{c['kode']}{', '.join(c.get('cpl', [])) or '-'}"
for c in d.get("cpmk", [])
)
_set_text(cell(18, 2), peta_txt)
_set_text(cell(19, 2), d.get("deskripsi", ""))
_set_text(cell(20, 2), d.get("bahan_kajian", ""))
utama = d.get("referensi", [])
pendukung = d.get("referensi_pendukung", [])
_set_text(cell(22, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(utama, 1)))
_set_text(cell(24, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(pendukung, 1)))
_set_text(cell(25, 2), d.get("media", ""))
_set_text(cell(26, 2), ident.get("pengampu", ""))
_set_text(cell(27, 2), ident.get("prasyarat") or "—")
# ── tabel mingguan ──
proto_norm, proto_uts, proto_uas = trs[31], trs[33], trs[37]
total = 0.0
ref = trs[30] # sisipkan setelah baris nomor kolom (1)(2)...(9)
for m in d.get("minggu", []):
w = m.get("ke")
try:
total += float(m.get("bobot") or 0)
except (TypeError, ValueError):
pass
if w in (8, 16):
row = _clone_after(t, proto_uts if w == 8 else proto_uas, ref)
cells = _unique_cells(row)
_set_text(cells[0], str(w), align=WD_ALIGN_PARAGRAPH.CENTER, bold=True)
nama_ujian = "Ujian Tengah Semester (UTS)" if w == 8 else "Ujian Akhir Semester (UAS)"
gab = [nama_ujian]
if m.get("sub"):
gab.append(f"Cakupan: {m['sub']}")
if m.get("indikator"):
gab.append(f"Indikator: {m['indikator']}")
if m.get("metode"):
gab.append(f"Bentuk: {m['metode']}")
_set_text(cells[1], "\n".join(gab))
_set_text(cells[2], _fmt(m.get("bobot")), align=WD_ALIGN_PARAGRAPH.CENTER)
else:
row = _clone_after(t, proto_norm, ref)
cells = _unique_cells(row)
_set_text(cells[0], str(w), align=WD_ALIGN_PARAGRAPH.CENTER)
_set_text(cells[1], m.get("sub", ""))
_set_text(cells[2], m.get("indikator", ""))
_set_text(cells[3], m.get("kriteria", ""))
_set_text(cells[4], m.get("metode", "")) # tatap muka
_set_text(cells[5], m.get("daring", ""))
_set_text(cells[6], m.get("kegiatan", ""))
_set_text(cells[7], m.get("materi", ""))
_set_text(cells[8], _fmt(m.get("bobot")), align=WD_ALIGN_PARAGRAPH.CENTER)
ref = row._tr
for i in (31, 32, 33, 34, 35, 36, 37): # buang seluruh baris contoh
trs[i].getparent().remove(trs[i])
_set_text(
_unique_cells(_Row(trs[38], t))[-1],
f"{total:g}", align=WD_ALIGN_PARAGRAPH.CENTER, bold=True,
)
# ── buang paragraf petunjuk pengisian (catatan SN Dikti) setelah tabel ──
body = doc.element.body
tbl_seen = False
for el in list(body):
if el.tag == qn("w:tbl"):
tbl_seen = True
continue
if tbl_seen and el.tag == qn("w:p"):
body.remove(el)
# ── lampiran kisi-kisi asesmen ──
doc.add_page_break()
p = doc.add_paragraph()
r = p.add_run("LAMPIRAN — KISI-KISI ASESMEN (Komponen Penilaian × Sub-CPMK)")
r.bold = True
r.font.size = Pt(11)
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)
_grid_borders(tk)
for ci, judul in enumerate(
["Komponen", "Sub-CPMK yang Dinilai", "Porsi per Sub-CPMK (%)", "Bobot (%)"]
):
_set_text(tk.cell(0, ci), judul, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
for i, k in enumerate(komponen, 1):
subs_k = k.get("subs", [])
try:
bobot = float(k.get("bobot") or 0)
except (TypeError, ValueError):
bobot = 0.0
_set_text(tk.cell(i, 0), k.get("nama", ""))
_set_text(tk.cell(i, 1), ", ".join(subs_k) or "—")
_set_text(
tk.cell(i, 2),
f"{bobot / len(subs_k):.1f} (bagi rata)" if subs_k else "—",
align=WD_ALIGN_PARAGRAPH.CENTER,
)
_set_text(tk.cell(i, 3), str(k.get("bobot", "")), align=WD_ALIGN_PARAGRAPH.CENTER)
p = doc.add_paragraph()
r = p.add_run(
"Porsi per Sub-CPMK dihitung bagi rata dari bobot komponen (kebijakan "
"default prodi); penyesuaian porsi per butir soal dicatat pada dokumen "
"portofolio mata kuliah."
)
r.italic = True
r.font.size = Pt(9)
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()