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,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()
|
||||
Reference in New Issue
Block a user