Gabungkan aplikasi RPS & Portofolio OBE (dashboard jadi bagian aplikasi)
Dashboard pemetaan statis kini dilayani aplikasi di /dashboard (app/static/dashboard.html). Fitur lengkap Tahap 1-4 + dasbor monitoring, menu global, filter semester, input nilai langsung, penggabungan nilai team teaching, tabel bisa urut. Dockerfile berubah dari nginx statis ke uvicorn/FastAPI: healthcheck, proxy-headers, volume /data (SQLite). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Generator Portofolio Mata Kuliah (.docx).
|
||||
Struktur mengikuti `Contoh Portofolio MK 040126.xlsx`:
|
||||
Cover · Lembar Pengesahan · A CPL · B CPMK & Sub-CPMK · C Korelasi CPL–Sub-CPMK
|
||||
· D Rencana Penilaian & Tugas · E Realisasi Pembelajaran · F Penilaian
|
||||
· G Evaluasi Ketercapaian · H Simpulan & Tindak Lanjut.
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.section import WD_ORIENT
|
||||
from docx.enum.table import WD_ALIGN_VERTICAL
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Cm, Pt, RGBColor
|
||||
|
||||
DARK_BLUE = "1F4E79"
|
||||
LIGHT_BLUE = "D6E4F0"
|
||||
RED = "9A3324"
|
||||
WHITE = "FFFFFF"
|
||||
|
||||
LOGO = pathlib.Path(__file__).resolve().parent.parent / "static" / "logo-untan.png"
|
||||
FAKULTAS = os.environ.get("FAKULTAS", "FAKULTAS TEKNIK")
|
||||
PRODI = os.environ.get("PRODI", "PROGRAM STUDI INFORMATIKA")
|
||||
|
||||
|
||||
def _shd(cell, hex_color):
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
el = OxmlElement("w:shd")
|
||||
el.set(qn("w:val"), "clear")
|
||||
el.set(qn("w:color"), "auto")
|
||||
el.set(qn("w:fill"), hex_color)
|
||||
tc_pr.append(el)
|
||||
|
||||
|
||||
def _ct(cell, text, bold=False, italic=False, size=9, color=None,
|
||||
align=WD_ALIGN_PARAGRAPH.LEFT):
|
||||
p = cell.paragraphs[0]
|
||||
p.alignment = align
|
||||
p.paragraph_format.space_before = Pt(1)
|
||||
p.paragraph_format.space_after = Pt(1)
|
||||
run = p.add_run()
|
||||
for i, line in enumerate(str(text or "").split("\n")):
|
||||
if i:
|
||||
run.add_break()
|
||||
run.add_text(line)
|
||||
run.bold = bold
|
||||
run.italic = italic
|
||||
run.font.name = "Times New Roman"
|
||||
run.font.size = Pt(size)
|
||||
if color:
|
||||
run.font.color.rgb = RGBColor.from_string(color)
|
||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
||||
|
||||
|
||||
def _hdr(cell, text, size=9):
|
||||
_shd(cell, DARK_BLUE)
|
||||
_ct(cell, text, bold=True, color=WHITE, size=size, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
|
||||
|
||||
def _borders(table):
|
||||
tbl_pr = table._tbl.tblPr
|
||||
el = OxmlElement("w:tblBorders")
|
||||
for sisi in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
||||
b = OxmlElement(f"w:{sisi}")
|
||||
b.set(qn("w:val"), "single")
|
||||
b.set(qn("w:sz"), "4")
|
||||
b.set(qn("w:color"), "000000")
|
||||
el.append(b)
|
||||
tbl_pr.append(el)
|
||||
|
||||
|
||||
def _f(x, nd=2):
|
||||
return f"{x:.{nd}f}" if isinstance(x, (int, float)) else "—"
|
||||
|
||||
|
||||
def build_portofolio(d: dict) -> bytes:
|
||||
doc = Document()
|
||||
for sec in doc.sections:
|
||||
sec.orientation = WD_ORIENT.LANDSCAPE
|
||||
sec.page_width, sec.page_height = Cm(29.7), Cm(21.0)
|
||||
sec.top_margin = sec.bottom_margin = Cm(2)
|
||||
sec.left_margin = sec.right_margin = Cm(2)
|
||||
|
||||
ident = d.get("identitas", {})
|
||||
dok = d.get("dokumen", {})
|
||||
|
||||
def para(text, size=11, bold=False, italic=False, align=WD_ALIGN_PARAGRAPH.LEFT,
|
||||
before=6, after=3):
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = align
|
||||
p.paragraph_format.space_before = Pt(before)
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
r = p.add_run(text)
|
||||
r.bold, r.italic = bold, italic
|
||||
r.font.name = "Times New Roman"
|
||||
r.font.size = Pt(size)
|
||||
return p
|
||||
|
||||
def bagian(judul):
|
||||
para(judul, size=12, bold=True, before=14)
|
||||
|
||||
# ── COVER ──
|
||||
for _ in range(3):
|
||||
doc.add_paragraph()
|
||||
para("PORTOFOLIO MATA KULIAH", size=22, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
para(f"{ident.get('nama', '')} ({ident.get('kode', '')})", size=16, bold=True,
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
doc.add_paragraph()
|
||||
if LOGO.exists():
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.add_run().add_picture(str(LOGO), width=Cm(4.5))
|
||||
doc.add_paragraph()
|
||||
para(PRODI, size=13, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER, after=0)
|
||||
para(FAKULTAS, size=12, align=WD_ALIGN_PARAGRAPH.CENTER, before=0, after=0)
|
||||
para("UNIVERSITAS TANJUNGPURA", size=12, align=WD_ALIGN_PARAGRAPH.CENTER,
|
||||
before=0, after=0)
|
||||
para(f"TAHUN AJARAN {d.get('tahun', '')}", size=12, align=WD_ALIGN_PARAGRAPH.CENTER,
|
||||
before=0)
|
||||
doc.paragraphs[-1].runs[-1].add_break(WD_BREAK.PAGE)
|
||||
|
||||
# ── LEMBAR PENGESAHAN ──
|
||||
para("LEMBAR PENGESAHAN", size=14, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
para(f"PORTOFOLIO MATA KULIAH — KURIKULUM OBE {dok.get('kurikulum_tahun', '')}",
|
||||
size=11, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
doc.add_paragraph()
|
||||
ti = doc.add_table(rows=5, cols=3)
|
||||
_borders(ti)
|
||||
isian = [
|
||||
("Nama Dokumen", f"Portofolio MK {ident.get('nama', '')}"),
|
||||
("Nomor Dokumen", dok.get("nomor", "") or "—"),
|
||||
("No. Revisi", dok.get("revisi", "") or "—"),
|
||||
("Tanggal Terbit", dok.get("tgl_terbit", "") or "—"),
|
||||
("Status Dokumen", dok.get("status_dokumen", "Master")),
|
||||
]
|
||||
for i, (k, v) in enumerate(isian):
|
||||
_shd(ti.cell(i, 0), LIGHT_BLUE)
|
||||
_ct(ti.cell(i, 0), k, bold=True, size=10)
|
||||
_ct(ti.cell(i, 1), ":", size=10, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(ti.cell(i, 2), v, size=10)
|
||||
ti.rows[i].cells[0].width = Cm(5)
|
||||
ti.rows[i].cells[1].width = Cm(0.7)
|
||||
ti.rows[i].cells[2].width = Cm(12)
|
||||
doc.add_paragraph()
|
||||
tt = doc.add_table(rows=4, cols=3)
|
||||
_borders(tt)
|
||||
for ci, (peran, nama) in enumerate([
|
||||
("DISUSUN OLEH\nKoordinator Mata Kuliah", ident.get("koordinator", "")),
|
||||
("DIPERIKSA OLEH\nKetua Program Studi", ident.get("kaprodi", "")),
|
||||
("DISAHKAN OLEH\nDekan", dok.get("dekan", "")),
|
||||
]):
|
||||
_hdr(tt.cell(0, ci), peran, size=10)
|
||||
_ct(tt.cell(1, ci), "\n\n\n")
|
||||
_ct(tt.cell(2, ci), f"( {nama or '______________________'} )",
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER, size=10)
|
||||
_ct(tt.cell(3, ci), "NIP. ......................................",
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER, size=10)
|
||||
doc.paragraphs[-1].runs[-1].add_break(WD_BREAK.PAGE) if doc.paragraphs[-1].runs else doc.add_page_break()
|
||||
|
||||
# ── A. CPL ──
|
||||
bagian("A. CAPAIAN PEMBELAJARAN LULUSAN (CPL) YANG DIBEBANKAN PADA MK")
|
||||
cpls = d.get("cpl", [])
|
||||
ta = doc.add_table(rows=1 + len(cpls), cols=2)
|
||||
_borders(ta)
|
||||
_hdr(ta.cell(0, 0), "Kode CPL")
|
||||
_hdr(ta.cell(0, 1), "Uraian CPL")
|
||||
for i, c in enumerate(cpls, 1):
|
||||
_ct(ta.cell(i, 0), c["kode"], align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(ta.cell(i, 1), c["desk"])
|
||||
ta.rows[i].cells[0].width = Cm(3)
|
||||
ta.rows[i].cells[1].width = Cm(22)
|
||||
|
||||
# ── B. CPMK & Sub-CPMK ──
|
||||
bagian("B. CPMK DAN SUB-CPMK")
|
||||
cpmks = d.get("cpmk", [])
|
||||
tb = doc.add_table(rows=1 + len(cpmks), cols=3)
|
||||
_borders(tb)
|
||||
_hdr(tb.cell(0, 0), "Kode CPMK")
|
||||
_hdr(tb.cell(0, 1), "Uraian CPMK")
|
||||
_hdr(tb.cell(0, 2), "CPL Didukung")
|
||||
for i, c in enumerate(cpmks, 1):
|
||||
_ct(tb.cell(i, 0), c["kode"], align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tb.cell(i, 1), c["desk"])
|
||||
_ct(tb.cell(i, 2), ", ".join(c.get("cpl", [])), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
doc.add_paragraph()
|
||||
subs = d.get("sub", [])
|
||||
ts = doc.add_table(rows=1 + len(subs), cols=3)
|
||||
_borders(ts)
|
||||
_hdr(ts.cell(0, 0), "Kode Sub-CPMK")
|
||||
_hdr(ts.cell(0, 1), "Kemampuan Akhir Tiap Tahapan Belajar")
|
||||
_hdr(ts.cell(0, 2), "CPMK")
|
||||
for i, s in enumerate(subs, 1):
|
||||
_ct(ts.cell(i, 0), s["kode"], align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(ts.cell(i, 1), s["uraian"])
|
||||
_ct(ts.cell(i, 2), s.get("cpmk", ""), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
|
||||
# ── C. korelasi CPL–Sub-CPMK ──
|
||||
bagian("C. KORELASI CPL TERHADAP SUB-CPMK (BOBOT PENILAIAN, %)")
|
||||
korelasi = d.get("korelasi", {}) # {sub: {cpl: porsi}}
|
||||
bobot_sub = d.get("bobot_sub", {})
|
||||
minggu_sub = d.get("minggu_sub", {})
|
||||
kode_cpl = [c["kode"] for c in cpls]
|
||||
tc = doc.add_table(rows=1 + len(subs) + 1, cols=3 + len(kode_cpl))
|
||||
_borders(tc)
|
||||
_hdr(tc.cell(0, 0), "Sub-CPMK")
|
||||
for j, cp in enumerate(kode_cpl):
|
||||
_hdr(tc.cell(0, 1 + j), f"{cp}\n(%)")
|
||||
_hdr(tc.cell(0, 1 + len(kode_cpl)), "Bobot\nPenilaian (%)")
|
||||
_hdr(tc.cell(0, 2 + len(kode_cpl)), "Jumlah\nMinggu")
|
||||
tot_cpl = {cp: 0.0 for cp in kode_cpl}
|
||||
tot_bobot = 0.0
|
||||
for i, s in enumerate(subs, 1):
|
||||
_ct(tc.cell(i, 0), s["kode"], align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
for j, cp in enumerate(kode_cpl):
|
||||
v = korelasi.get(s["kode"], {}).get(cp)
|
||||
_ct(tc.cell(i, 1 + j), _f(v, 1) if v else "", align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
tot_cpl[cp] += v or 0
|
||||
b = bobot_sub.get(s["kode"])
|
||||
tot_bobot += b or 0
|
||||
_ct(tc.cell(i, 1 + len(kode_cpl)), _f(b, 1) if b else "—",
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tc.cell(i, 2 + len(kode_cpl)), str(minggu_sub.get(s["kode"], "")),
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
last = len(subs) + 1
|
||||
_shd(tc.cell(last, 0), LIGHT_BLUE)
|
||||
_ct(tc.cell(last, 0), "Total", bold=True, align=WD_ALIGN_PARAGRAPH.RIGHT)
|
||||
for j, cp in enumerate(kode_cpl):
|
||||
_shd(tc.cell(last, 1 + j), LIGHT_BLUE)
|
||||
_ct(tc.cell(last, 1 + j), _f(tot_cpl[cp], 1), bold=True,
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_shd(tc.cell(last, 1 + len(kode_cpl)), LIGHT_BLUE)
|
||||
_ct(tc.cell(last, 1 + len(kode_cpl)), _f(tot_bobot, 1), bold=True,
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_shd(tc.cell(last, 2 + len(kode_cpl)), LIGHT_BLUE)
|
||||
|
||||
# ── D. rencana penilaian & tugas (dari RPS) ──
|
||||
bagian("D. RENCANA PENILAIAN DAN RENCANA TUGAS (DARI RPS)")
|
||||
rencana = d.get("rencana", [])
|
||||
td = doc.add_table(rows=1 + max(len(rencana), 1), cols=4)
|
||||
_borders(td)
|
||||
for ci, h in enumerate(["Minggu ke-", "Sub-CPMK", "Bentuk Asesmen", "Bobot (%)"]):
|
||||
_hdr(td.cell(0, ci), h)
|
||||
for i, r in enumerate(rencana, 1):
|
||||
_ct(td.cell(i, 0), str(r["minggu"]), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(td.cell(i, 1), r["sub"])
|
||||
_ct(td.cell(i, 2), r["asesmen"])
|
||||
_ct(td.cell(i, 3), str(r["bobot"]), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
para("RPS lengkap diterbitkan sebagai dokumen terpisah dari aplikasi yang sama.",
|
||||
size=9, italic=True)
|
||||
|
||||
# ── E. realisasi ──
|
||||
bagian("E. REALISASI PEMBELAJARAN")
|
||||
te = doc.add_table(rows=1, cols=1)
|
||||
_borders(te)
|
||||
_ct(te.cell(0, 0), d.get("realisasi", "") or "—", size=10)
|
||||
|
||||
# ── F. penilaian ──
|
||||
bagian("F. PENILAIAN MATA KULIAH")
|
||||
nilai = d.get("nilai", {})
|
||||
skor_sub = d.get("skor_sub", {})
|
||||
if nilai.get("mode") == "per_mahasiswa":
|
||||
mhs = nilai.get("mahasiswa", [])
|
||||
akhir = {a["npm"]: a["akhir"] for a in d.get("akhir", [])}
|
||||
kol_sub = [s["kode"] for s in subs]
|
||||
tf = doc.add_table(rows=2 + len(mhs) + 3, cols=3 + len(kol_sub) + 1)
|
||||
_borders(tf)
|
||||
_hdr(tf.cell(0, 0), "No.")
|
||||
_hdr(tf.cell(0, 1), "NPM")
|
||||
_hdr(tf.cell(0, 2), "Nama")
|
||||
for j, sk in enumerate(kol_sub):
|
||||
_hdr(tf.cell(0, 3 + j), sk, size=7)
|
||||
_hdr(tf.cell(0, 3 + len(kol_sub)), "Nilai\nAkhir", size=7)
|
||||
# baris bobot
|
||||
_shd(tf.cell(1, 0), LIGHT_BLUE)
|
||||
_ct(tf.cell(1, 0), "Bobot", bold=True, size=8)
|
||||
for j, sk in enumerate(kol_sub):
|
||||
_shd(tf.cell(1, 3 + j), LIGHT_BLUE)
|
||||
b = d.get("bobot_sub", {}).get(sk)
|
||||
_ct(tf.cell(1, 3 + j), _f((b or 0) / 100, 2), size=8,
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
for i, m in enumerate(mhs, 2):
|
||||
_ct(tf.cell(i, 0), str(i - 1), size=8, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tf.cell(i, 1), m.get("npm", ""), size=8)
|
||||
_ct(tf.cell(i, 2), m.get("nama", ""), size=8)
|
||||
for j, sk in enumerate(kol_sub):
|
||||
v = (m.get("skor") or {}).get(sk)
|
||||
_ct(tf.cell(i, 3 + j), _f(v, 0) if v is not None else "",
|
||||
size=8, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tf.cell(i, 3 + len(kol_sub)), _f(akhir.get(m.get("npm", "")), 1),
|
||||
size=8, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
# baris statistik per sub
|
||||
base = 2 + len(mhs)
|
||||
for k, (label, kunci) in enumerate([("Rata-rata", "rata"),
|
||||
("Maksimum", "max"), ("Minimum", "min")]):
|
||||
_shd(tf.cell(base + k, 0), LIGHT_BLUE)
|
||||
_ct(tf.cell(base + k, 0), label, bold=True, size=8)
|
||||
for j, sk in enumerate(kol_sub):
|
||||
info = skor_sub.get(sk)
|
||||
if not info:
|
||||
continue
|
||||
if kunci == "rata":
|
||||
v = info["rata"]
|
||||
else:
|
||||
xs = [(m.get("skor") or {}).get(sk) for m in mhs]
|
||||
xs = [x for x in xs if x is not None]
|
||||
v = (max(xs) if kunci == "max" else min(xs)) if xs else None
|
||||
_ct(tf.cell(base + k, 3 + j), _f(v, 1), size=8,
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
else:
|
||||
para("Mode input: rata-rata kelas per Sub-CPMK (BUKTI AGREGAT — tidak "
|
||||
"memuat data per mahasiswa).", size=10, italic=True)
|
||||
tf = doc.add_table(rows=1 + len(subs), cols=2)
|
||||
_borders(tf)
|
||||
_hdr(tf.cell(0, 0), "Sub-CPMK")
|
||||
_hdr(tf.cell(0, 1), "Rata-rata Kelas")
|
||||
for i, s in enumerate(subs, 1):
|
||||
info = skor_sub.get(s["kode"])
|
||||
_ct(tf.cell(i, 0), s["kode"], align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tf.cell(i, 1), _f(info["rata"], 2) if info else "—",
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
|
||||
doc.add_paragraph()
|
||||
skala = d.get("skala", [])
|
||||
tsx = doc.add_table(rows=1 + len(skala), cols=3)
|
||||
_borders(tsx)
|
||||
_hdr(tsx.cell(0, 0), "Huruf")
|
||||
_hdr(tsx.cell(0, 1), "Rentang Nilai")
|
||||
_hdr(tsx.cell(0, 2), "Angka Mutu")
|
||||
for i, s in enumerate(skala, 1):
|
||||
_ct(tsx.cell(i, 0), s["huruf"], align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tsx.cell(i, 1), f"{s['lo']:g} – {s['hi']:g}", align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tsx.cell(i, 2), _f(s["angka"], 2) if s["angka"] is not None else "—",
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
|
||||
# ── G. evaluasi ketercapaian ──
|
||||
bagian("G. EVALUASI KETERCAPAIAN CPL DAN CPMK")
|
||||
rollup = d.get("rollup", {})
|
||||
ambang = d.get("ambang", 70)
|
||||
baris_g = []
|
||||
for cp in cpls:
|
||||
subs_cp = [s for s in subs
|
||||
if any(c["kode"] == s.get("cpmk") and cp["kode"] in c.get("cpl", [])
|
||||
for c in cpmks)]
|
||||
for k, s in enumerate(subs_cp):
|
||||
info = skor_sub.get(s["kode"])
|
||||
baris_g.append({
|
||||
"cpl": cp["kode"] if k == 0 else "",
|
||||
"sub": s["kode"],
|
||||
"rata_sub": info["rata"] if info else None,
|
||||
"rata_cpl": rollup.get("cpl", {}).get(cp["kode"], {}).get("skor")
|
||||
if k == 0 else None,
|
||||
"lulus": info["lulus_pct"] if info else None,
|
||||
"pertama": k == 0,
|
||||
"kode_cpl": cp["kode"],
|
||||
})
|
||||
tg = doc.add_table(rows=1 + max(len(baris_g), 1), cols=6)
|
||||
_borders(tg)
|
||||
for ci, h in enumerate(["CPL", "Sub-CPMK", "Rata-rata Nilai\nSub-CPMK",
|
||||
"Rata-rata\nNilai CPL", "% Kelulusan\n(≥ ambang)",
|
||||
"Kesimpulan"]):
|
||||
_hdr(tg.cell(0, ci), h)
|
||||
for i, b in enumerate(baris_g, 1):
|
||||
_ct(tg.cell(i, 0), b["cpl"], bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tg.cell(i, 1), b["sub"], align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tg.cell(i, 2), _f(b["rata_sub"]), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tg.cell(i, 3), _f(b["rata_cpl"]) if b["pertama"] else "",
|
||||
bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tg.cell(i, 4), _f(b["lulus"], 1) if b["lulus"] is not None else "—",
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
if b["pertama"]:
|
||||
skor = b["rata_cpl"]
|
||||
if skor is None:
|
||||
ket, warna = "Belum terukur", None
|
||||
elif skor >= ambang:
|
||||
ket, warna = "TERCAPAI", None
|
||||
else:
|
||||
ket, warna = "BELUM TERCAPAI", RED
|
||||
_ct(tg.cell(i, 5), ket, bold=True, color=warna,
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
para(f"Ambang ketercapaian: {ambang:g}. Metode: rata-rata sederhana (bagi rata) "
|
||||
"sesuai Metodologi Pengukuran prodi.", size=9, italic=True)
|
||||
|
||||
# ── H. simpulan ──
|
||||
bagian("H. SIMPULAN HASIL EVALUASI DAN TINDAK LANJUT")
|
||||
stat = d.get("statistik")
|
||||
if stat:
|
||||
th = doc.add_table(rows=7, cols=2)
|
||||
_borders(th)
|
||||
_hdr(th.cell(0, 0), "Statistik Deskriptif (Nilai Akhir)")
|
||||
_hdr(th.cell(0, 1), "Nilai")
|
||||
for i, (k, v) in enumerate([
|
||||
("Jumlah mahasiswa", stat["n"]), ("Mean", _f(stat["mean"])),
|
||||
("Median", _f(stat["median"])), ("Standar deviasi", _f(stat["stdev"])),
|
||||
("Minimum", _f(stat["min"])), ("Maksimum – rentang",
|
||||
f"{_f(stat['max'])} – {_f(stat['range'])}"),
|
||||
], 1):
|
||||
_ct(th.cell(i, 0), k)
|
||||
_ct(th.cell(i, 1), str(v), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
doc.add_paragraph()
|
||||
frek = stat["frekuensi"]
|
||||
tfr = doc.add_table(rows=2, cols=1 + len(frek))
|
||||
_borders(tfr)
|
||||
_hdr(tfr.cell(0, 0), "Huruf")
|
||||
_shd(tfr.cell(1, 0), LIGHT_BLUE)
|
||||
_ct(tfr.cell(1, 0), "Frekuensi", bold=True)
|
||||
for j, (h, n) in enumerate(frek.items(), 1):
|
||||
_hdr(tfr.cell(0, j), h)
|
||||
_ct(tfr.cell(1, j), str(n), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
else:
|
||||
para("Statistik nilai akhir tidak tersedia (mode bukti agregat).",
|
||||
size=10, italic=True)
|
||||
|
||||
doc.add_paragraph()
|
||||
kendala = [k for k in d.get("kendala", []) if k.get("kendala")]
|
||||
tk = doc.add_table(rows=1 + max(len(kendala), 1), cols=3)
|
||||
_borders(tk)
|
||||
_hdr(tk.cell(0, 0), "No.")
|
||||
_hdr(tk.cell(0, 1), "Kendala")
|
||||
_hdr(tk.cell(0, 2), "Tindak Lanjut")
|
||||
for i, k in enumerate(kendala, 1):
|
||||
_ct(tk.cell(i, 0), str(i), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tk.cell(i, 1), k.get("kendala", ""))
|
||||
_ct(tk.cell(i, 2), k.get("tindak", ""))
|
||||
|
||||
if d.get("simpulan"):
|
||||
doc.add_paragraph()
|
||||
ts2 = doc.add_table(rows=1, cols=1)
|
||||
_borders(ts2)
|
||||
_ct(ts2.cell(0, 0), d["simpulan"], size=10)
|
||||
|
||||
buf = io.BytesIO()
|
||||
doc.save(buf)
|
||||
return buf.getvalue()
|
||||
@@ -0,0 +1,335 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Generator RPS Format APTIKOM 2024 — Prodi Informatika S1 UNTAN.
|
||||
Diadaptasi dari buat_template_rps.py; gaya visual dipertahankan persis,
|
||||
placeholder diganti data isian dosen.
|
||||
"""
|
||||
import io
|
||||
import pathlib
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.table import WD_ALIGN_VERTICAL
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Cm, Pt, RGBColor
|
||||
|
||||
DARK_BLUE = "1F4E79"
|
||||
MID_BLUE = "2E75B6"
|
||||
LIGHT_BLUE = "D6E4F0"
|
||||
GREEN_LITE = "E2EFDA"
|
||||
YELLOW = "FFF2CC"
|
||||
WHITE = "FFFFFF"
|
||||
|
||||
LOGO = pathlib.Path(__file__).resolve().parent.parent / "static" / "logo-untan.png"
|
||||
|
||||
|
||||
def _shd(cell, hex_color):
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
el = OxmlElement("w:shd")
|
||||
el.set(qn("w:val"), "clear")
|
||||
el.set(qn("w:color"), "auto")
|
||||
el.set(qn("w:fill"), hex_color)
|
||||
tc_pr.append(el)
|
||||
|
||||
|
||||
def _ct(cell, text, bold=False, italic=False, size=10, color=None,
|
||||
align=WD_ALIGN_PARAGRAPH.LEFT):
|
||||
p = cell.paragraphs[0]
|
||||
p.alignment = align
|
||||
p.paragraph_format.space_before = Pt(1)
|
||||
p.paragraph_format.space_after = Pt(1)
|
||||
run = p.add_run(text or "")
|
||||
run.bold = bold
|
||||
run.italic = italic
|
||||
run.font.name = "Times New Roman"
|
||||
run.font.size = Pt(size)
|
||||
if color:
|
||||
run.font.color.rgb = RGBColor.from_string(color)
|
||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
||||
|
||||
|
||||
def _hdr(cell, text, bg=DARK_BLUE, fg=WHITE):
|
||||
_shd(cell, bg)
|
||||
_ct(cell, text, bold=True, color=fg, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
|
||||
|
||||
def _widths(table, cms):
|
||||
for row in table.rows:
|
||||
for ci, w in enumerate(cms):
|
||||
try:
|
||||
row.cells[ci].width = Cm(w)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
def build_rps(d: dict) -> bytes:
|
||||
"""d = data RPS tersimpan (lihat main.py: bentuk_data_form)."""
|
||||
doc = Document()
|
||||
for sec in doc.sections:
|
||||
sec.top_margin = Cm(2.5)
|
||||
sec.bottom_margin = Cm(2.5)
|
||||
sec.left_margin = Cm(3)
|
||||
sec.right_margin = Cm(2)
|
||||
|
||||
def section_head(text):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(8)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
r = p.add_run(text)
|
||||
r.bold = True
|
||||
r.font.name = "Times New Roman"
|
||||
r.font.size = Pt(11)
|
||||
|
||||
ident = d.get("identitas", {})
|
||||
|
||||
# ── logo UNTAN (dari RPS Template.docx resmi) ──
|
||||
if LOGO.exists():
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
p.add_run().add_picture(str(LOGO), width=Cm(2.6))
|
||||
|
||||
# ── judul ──
|
||||
for txt, sz, bd in [
|
||||
("RENCANA PEMBELAJARAN SEMESTER (RPS)", 14, True),
|
||||
("Program Studi Informatika", 12, True),
|
||||
("Fakultas Matematika dan Ilmu Pengetahuan Alam", 11, False),
|
||||
("Universitas Tanjungpura", 11, False),
|
||||
]:
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p.paragraph_format.space_before = Pt(0)
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
r = p.add_run(txt)
|
||||
r.bold = bd
|
||||
r.font.name = "Times New Roman"
|
||||
r.font.size = Pt(sz)
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── A. identitas ──
|
||||
section_head("A. IDENTITAS MATA KULIAH")
|
||||
sks = ident.get("sks", "")
|
||||
sks_t = ident.get("sks_teori", "")
|
||||
sks_p = ident.get("sks_praktikum", "")
|
||||
sks_txt = f"{sks} SKS"
|
||||
if sks_t or sks_p:
|
||||
sks_txt += f" (Teori: {sks_t or '-'} SKS | Praktikum: {sks_p or '-'} SKS)"
|
||||
rows_id = [
|
||||
("Nama Mata Kuliah", ident.get("nama", "")),
|
||||
("Kode Mata Kuliah", ident.get("kode", "")),
|
||||
("Bobot SKS", sks_txt),
|
||||
("Semester", ident.get("semester", "")),
|
||||
("MK Prasyarat", ident.get("prasyarat", "") or "—"),
|
||||
("Dosen Pengampu", ident.get("pengampu", "")),
|
||||
("Koordinator MK", ident.get("koordinator", "")),
|
||||
("Ketua Program Studi", ident.get("kaprodi", "")),
|
||||
("Tahun Ajaran", d.get("tahun", "")),
|
||||
]
|
||||
ta = doc.add_table(rows=len(rows_id), cols=3)
|
||||
ta.style = "Table Grid"
|
||||
for i, (lbl, val) in enumerate(rows_id):
|
||||
c = ta.rows[i].cells
|
||||
_shd(c[0], LIGHT_BLUE)
|
||||
_ct(c[0], lbl, bold=True)
|
||||
_ct(c[1], ":", align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(c[2], val)
|
||||
_widths(ta, [5.5, 0.5, 9.5])
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── B. CPL ──
|
||||
section_head("B. CAPAIAN PEMBELAJARAN LULUSAN (CPL) YANG DIBEBANKAN PADA MK")
|
||||
cpls = d.get("cpl", [])
|
||||
tb = doc.add_table(rows=1 + max(len(cpls), 1), cols=2)
|
||||
tb.style = "Table Grid"
|
||||
_hdr(tb.cell(0, 0), "Kode CPL")
|
||||
_hdr(tb.cell(0, 1), "Deskripsi CPL")
|
||||
for i, cpl in enumerate(cpls, 1):
|
||||
_ct(tb.cell(i, 0), cpl["kode"], align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tb.cell(i, 1), cpl["desk"])
|
||||
_widths(tb, [2.5, 13])
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── C. CPMK ──
|
||||
section_head("C. CAPAIAN PEMBELAJARAN MATA KULIAH (CPMK)")
|
||||
cpmks = d.get("cpmk", [])
|
||||
tc = doc.add_table(rows=1 + max(len(cpmks), 1), cols=3)
|
||||
tc.style = "Table Grid"
|
||||
_hdr(tc.cell(0, 0), "Kode CPMK")
|
||||
_hdr(tc.cell(0, 1), "Deskripsi CPMK")
|
||||
_hdr(tc.cell(0, 2), "CPL yang Didukung")
|
||||
for i, c in enumerate(cpmks, 1):
|
||||
_ct(tc.cell(i, 0), c["kode"], align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tc.cell(i, 1), c["desk"])
|
||||
_ct(tc.cell(i, 2), ", ".join(c.get("cpl", [])), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_widths(tc, [2.5, 9, 4])
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── D. Sub-CPMK ──
|
||||
section_head("D. SUB-CPMK (Kemampuan Akhir Tiap Tahapan Belajar)")
|
||||
subs = d.get("sub", [])
|
||||
td = doc.add_table(rows=1 + max(len(subs), 1), cols=3)
|
||||
td.style = "Table Grid"
|
||||
_hdr(td.cell(0, 0), "Kode Sub-CPMK")
|
||||
_hdr(td.cell(0, 1), "Uraian Sub-CPMK")
|
||||
_hdr(td.cell(0, 2), "CPMK yang\nDidukung")
|
||||
for i, s in enumerate(subs, 1):
|
||||
_ct(td.cell(i, 0), s["kode"], align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(td.cell(i, 1), s["uraian"])
|
||||
_ct(td.cell(i, 2), s.get("cpmk", ""), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_widths(td, [3, 9, 3.5])
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── E. deskripsi ──
|
||||
section_head("E. DESKRIPSI SINGKAT MATA KULIAH")
|
||||
te = doc.add_table(rows=1, cols=1)
|
||||
te.style = "Table Grid"
|
||||
_ct(te.cell(0, 0), d.get("deskripsi", ""))
|
||||
te.cell(0, 0).width = Cm(15.5)
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── F. tabel mingguan ──
|
||||
section_head("F. RENCANA PEMBELAJARAN SEMESTER — Tabel F APTIKOM 2024")
|
||||
minggu = d.get("minggu", [])
|
||||
nrow = 2 + len(minggu) + 1
|
||||
tf = doc.add_table(rows=nrow, cols=7)
|
||||
tf.style = "Table Grid"
|
||||
_hdr(tf.cell(0, 0), "Minggu\nke-")
|
||||
_hdr(tf.cell(0, 1), "Sub-CPMK\n(Kemampuan Akhir\nyang Direncanakan)")
|
||||
_hdr(tf.cell(0, 2), "Penilaian")
|
||||
_hdr(tf.cell(0, 4), "Bentuk & Metode\nPembelajaran\n[Estimasi Waktu]")
|
||||
_hdr(tf.cell(0, 5), "Materi Pembelajaran\n[Bahan Kajian]\n[Referensi]")
|
||||
_hdr(tf.cell(0, 6), "Bobot\n(%)")
|
||||
tf.cell(0, 2).merge(tf.cell(0, 3))
|
||||
_hdr(tf.cell(1, 2), "Indikator\nPencapaian", bg=MID_BLUE)
|
||||
_hdr(tf.cell(1, 3), "Kriteria &\nBentuk Penilaian", bg=MID_BLUE)
|
||||
for ci in (0, 1, 4, 5, 6):
|
||||
tf.cell(0, ci).merge(tf.cell(1, ci))
|
||||
|
||||
total_bobot = 0.0
|
||||
for idx, m in enumerate(minggu):
|
||||
ri = idx + 2
|
||||
w = m.get("ke", idx + 1)
|
||||
cells = [tf.cell(ri, ci) for ci in range(7)]
|
||||
if w == 8:
|
||||
for c in cells:
|
||||
_shd(c, GREEN_LITE)
|
||||
elif w == 16:
|
||||
for c in cells:
|
||||
_shd(c, YELLOW)
|
||||
_ct(cells[0], str(w), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(cells[1], m.get("sub", ""))
|
||||
_ct(cells[2], m.get("indikator", ""))
|
||||
_ct(cells[3], m.get("kriteria", ""))
|
||||
_ct(cells[4], m.get("metode", ""))
|
||||
_ct(cells[5], m.get("materi", ""))
|
||||
bobot = m.get("bobot", "")
|
||||
_ct(cells[6], str(bobot) if bobot != "" else "", align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
try:
|
||||
total_bobot += float(bobot)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
last = nrow - 1
|
||||
tf.cell(last, 0).merge(tf.cell(last, 5))
|
||||
_shd(tf.cell(last, 0), LIGHT_BLUE)
|
||||
_ct(tf.cell(last, 0), "Total Bobot Penilaian", bold=True, align=WD_ALIGN_PARAGRAPH.RIGHT)
|
||||
_shd(tf.cell(last, 6), LIGHT_BLUE)
|
||||
total_txt = f"{total_bobot:g}%"
|
||||
_ct(tf.cell(last, 6), total_txt, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_widths(tf, [1.0, 3.0, 2.5, 2.5, 3.0, 3.0, 1.0])
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── G. rekap bobot ──
|
||||
section_head("G. REKAP BOBOT PENILAIAN")
|
||||
komponen = d.get("komponen", [])
|
||||
tg = doc.add_table(rows=1 + len(komponen) + 1, cols=3)
|
||||
tg.style = "Table Grid"
|
||||
_hdr(tg.cell(0, 0), "Komponen Penilaian")
|
||||
_hdr(tg.cell(0, 1), "Sub-CPMK yang Dinilai")
|
||||
_hdr(tg.cell(0, 2), "Bobot (%)")
|
||||
tot_komp = 0.0
|
||||
for i, k in enumerate(komponen, 1):
|
||||
_ct(tg.cell(i, 0), k.get("nama", ""))
|
||||
_ct(tg.cell(i, 1), ", ".join(k.get("subs", [])))
|
||||
b = k.get("bobot", "")
|
||||
_ct(tg.cell(i, 2), str(b), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
try:
|
||||
tot_komp += float(b)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
li = len(komponen) + 1
|
||||
for ci in range(3):
|
||||
_shd(tg.cell(li, ci), LIGHT_BLUE)
|
||||
_ct(tg.cell(li, 0), "Total", bold=True)
|
||||
_ct(tg.cell(li, 2), f"{tot_komp:g}", bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_widths(tg, [4, 9, 2.5])
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── H. referensi ──
|
||||
section_head("H. DAFTAR REFERENSI")
|
||||
refs = d.get("referensi", [])
|
||||
th = doc.add_table(rows=1 + max(len(refs), 1), cols=2)
|
||||
th.style = "Table Grid"
|
||||
_hdr(th.cell(0, 0), "No.")
|
||||
_hdr(th.cell(0, 1), "Referensi (Format APA)")
|
||||
for i, ref in enumerate(refs, 1):
|
||||
_ct(th.cell(i, 0), str(i), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(th.cell(i, 1), ref)
|
||||
_widths(th, [1, 14.5])
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── I. otorisasi ──
|
||||
section_head("I. OTORISASI")
|
||||
ti = doc.add_table(rows=4, cols=2)
|
||||
ti.style = "Table Grid"
|
||||
_shd(ti.cell(0, 0), LIGHT_BLUE)
|
||||
_shd(ti.cell(0, 1), LIGHT_BLUE)
|
||||
_ct(ti.cell(0, 0), "Koordinator Mata Kuliah", bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(ti.cell(0, 1), "Ketua Program Studi", bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(ti.cell(1, 0), "\n\n\n\n")
|
||||
_ct(ti.cell(1, 1), "\n\n\n\n")
|
||||
koor = ident.get("koordinator", "").strip()
|
||||
kapr = ident.get("kaprodi", "").strip()
|
||||
_ct(ti.cell(2, 0), f"( {koor or '_____________________________'} )",
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(ti.cell(2, 1), f"( {kapr or '_____________________________'} )",
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(ti.cell(3, 0), "NIP. ....................................", align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(ti.cell(3, 1), "NIP. ....................................", align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── J. lampiran kisi-kisi (bukti constructive alignment) ──
|
||||
section_head("J. LAMPIRAN — KISI-KISI ASESMEN (Komponen × Sub-CPMK)")
|
||||
tj = doc.add_table(rows=1 + max(len(komponen), 1), cols=4)
|
||||
tj.style = "Table Grid"
|
||||
_hdr(tj.cell(0, 0), "Komponen")
|
||||
_hdr(tj.cell(0, 1), "Sub-CPMK yang Dinilai")
|
||||
_hdr(tj.cell(0, 2), "Porsi per Sub-CPMK (%)")
|
||||
_hdr(tj.cell(0, 3), "Bobot Komponen (%)")
|
||||
for i, k in enumerate(komponen, 1):
|
||||
subs_k = k.get("subs", [])
|
||||
try:
|
||||
bobot = float(k.get("bobot", 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
bobot = 0.0
|
||||
porsi = f"{bobot / len(subs_k):.1f}" if subs_k else "—"
|
||||
_ct(tj.cell(i, 0), k.get("nama", ""))
|
||||
_ct(tj.cell(i, 1), ", ".join(subs_k) or "—")
|
||||
_ct(tj.cell(i, 2), f"{porsi} (bagi rata)" if subs_k else "—",
|
||||
align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_ct(tj.cell(i, 3), str(k.get("bobot", "")), align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
_widths(tj, [3.5, 7, 3, 2])
|
||||
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.name = "Times New Roman"
|
||||
r.font.size = Pt(9)
|
||||
|
||||
buf = io.BytesIO()
|
||||
doc.save(buf)
|
||||
return buf.getvalue()
|
||||
@@ -0,0 +1,254 @@
|
||||
# -*- 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."""
|
||||
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 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(t.cell(18, 2), peta_txt)
|
||||
_set_text(t.cell(19, 2), d.get("deskripsi", ""))
|
||||
_set_text(t.cell(20, 2), d.get("bahan_kajian", ""))
|
||||
|
||||
utama = d.get("referensi", [])
|
||||
pendukung = d.get("referensi_pendukung", [])
|
||||
_set_text(t.cell(22, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(utama, 1)))
|
||||
_set_text(t.cell(24, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(pendukung, 1)))
|
||||
_set_text(t.cell(25, 2), d.get("media", ""))
|
||||
_set_text(t.cell(26, 2), ident.get("pengampu", ""))
|
||||
_set_text(t.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 = d.get("komponen", [])
|
||||
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()
|
||||
Binary file not shown.
Reference in New Issue
Block a user