# -*- 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 CPL–CPMK & 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()