5 Commits

Author SHA1 Message Date
Power BI Dev efc14dc7a7 Merge branch 'fitur/filter-kelas-input-nilai' into main 2026-08-09 18:36:25 +07:00
Power BI Dev 9c0fe8baa0 Tambah filter kelas di halaman Input Nilai
- /nilai/<kode>: dropdown "Tampilkan kelas" menyaring tabel Nilai per
  Komponen & Nilai per Mahasiswa sekaligus (tanpa reload); baris
  ringkasan (rata-rata/% lulus MK) ikut disembunyikan saat filter aktif
  karena angkanya dihitung dari seluruh MK, bukan per kelas
- /nilai/<kode>/komponen/<i>: filter serupa + kolom Kelas ditambahkan
  di tabel Nilai Mahasiswa (halaman paling sering dipakai dosen isi
  nilai per sesi/tugas)
- Muncul otomatis hanya bila MK punya >1 kelas

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 18:35:43 +07:00
Power BI Dev 38d309e7b1 Merge branch 'fix/rps-docx-corrupt-nested-table' into main 2026-08-09 17:37:22 +07:00
Power BI Dev 71858d974c Perbaiki dokumen RPS hasil unduh yang rusak/berantakan
Sel "Peta CPL-CPMK" pada template resmi UNTAN memuat tabel kecil bersarang
(contoh isian bawaan SN-Dikti). _set_text() cuma membuang paragraf tambahan
saat menimpa sel dengan teks generate, tak tahu ada tabel bersarang di
dalamnya -- hasilnya sel berakhir dengan w:tbl tanpa w:p setelahnya, yang
melanggar skema OOXML. Word membaca file itu sebagai "rusak" dan otomatis
memperbaikinya sendiri saat dibuka, dan perbaikan-otomatis itulah yang
mengacak tata letak dokumen.

- _set_text() sekarang membuang tabel bersarang tersisa sebelum menulis
  teks baru ke sel manapun (berlaku umum, bukan cuma sel yang saat ini
  kena masalah)
- Ganti templates/rps_untan.docx dengan versi yang sudah dirapikan
  formatnya (font/spacing) -- struktur baris & kolom identik (39x18),
  jadi tetap kompatibel dengan pemetaan baris di rps_untan.py
- Diverifikasi: tak ada lagi sel yang berakhir dengan tabel bersarang
  tanpa paragraf penutup di seluruh dokumen hasil generate

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 16:38:13 +07:00
Power BI Dev b9d5c22f33 Merge branch 'fitur/analisis-asesmen-riwayat-nilai' into main 2026-08-08 00:10:52 +07:00
4 changed files with 100 additions and 7 deletions
+11 -1
View File
@@ -39,7 +39,17 @@ 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."""
"""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]
Binary file not shown.
+49 -5
View File
@@ -89,6 +89,21 @@
</details>
{% endif %}
{% set ada_kelas = grid and grid|selectattr('kelas')|list|length > 0 %}
{% if ada_kelas %}
{% set semua_kelas = grid|map(attribute='kelas')|select|unique|sort|list %}
<div class="filter-baris">
<label for="filter-kelas">Tampilkan kelas:</label>
<select id="filter-kelas" style="width:auto">
<option value="">Semua</option>
{% for k in semua_kelas %}
<option value="{{ k }}">Kelas {{ k }}</option>
{% endfor %}
</select>
<span id="filter-kelas-info" class="keterangan" style="margin:0"></span>
</div>
{% endif %}
{% if grid and tabel_komponen and komponen_daftar %}
<h2 class="bagian">NILAI PER KOMPONEN <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai mahasiswa berdasarkan komponen</span></h2>
<p class="keterangan">Angka = rata-rata skor mentah tiap sesi/tugas komponen itu yang diikuti mahasiswa (bukan lewat Sub-CPMK). Ketik untuk menimpa —
@@ -109,7 +124,7 @@
<tbody>
{% for r in tabel_komponen %}
{% set ri = loop.index0 %}
<tr>
<tr data-kelas="{{ r.kelas or '' }}">
<td class="tengah">{{ loop.index }}
<input type="hidden" name="nim_{{ ri }}" value="{{ r.nim }}">
<input type="hidden" name="nama_{{ ri }}" value="{{ r.nama }}"></td>
@@ -142,7 +157,6 @@
<h2 class="bagian">NILAI PER MAHASISWA <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai mahasiswa berdasarkan Sub-CPMK</span></h2>
<form method="post" action="/nilai/{{ peta.kode }}">
<div class="gulir">
{% set ada_kelas = grid|selectattr('kelas')|list|length > 0 %}
{% set n_kiri = 4 if ada_kelas else 3 %}
<table class="grid">
<thead>
@@ -158,7 +172,7 @@
<tbody>
{% for m in grid %}
{% set i = loop.index0 %}
<tr>
<tr data-kelas="{{ m.kelas or '' }}">
<td class="tengah">{{ loop.index }}
<input type="hidden" name="nim_{{ i }}" value="{{ m.nim }}">
<input type="hidden" name="nama_{{ i }}" value="{{ m.nama }}"></td>
@@ -174,7 +188,7 @@
</tr>
{% endfor %}
{% if per_mhs and hasil %}
<tr class="total">
<tr class="total" data-kelas="__total__" title="Dihitung dari seluruh mahasiswa MK ini, tidak per kelas">
<td colspan="{{ n_kiri }}" style="text-align:right">Rata-rata kelas (tersimpan)</td>
{% for s in peta.subs %}
{% set info = hasil.sub.get(s.kode) %}
@@ -182,7 +196,7 @@
{% endfor %}
<td class="tengah">{{ '%.1f'|format(akhir.values()|sum / akhir|length) if akhir else '—' }}</td>
</tr>
<tr class="total">
<tr class="total" data-kelas="__total__" title="Dihitung dari seluruh mahasiswa MK ini, tidak per kelas">
<td colspan="{{ n_kiri }}" style="text-align:right">% lulus (≥ {{ ambang|round(0)|int }})</td>
{% for s in peta.subs %}
{% set info = hasil.sub.get(s.kode) %}
@@ -383,4 +397,34 @@
{% endif %}
</details>
{% endif %}
{% if ada_kelas %}
<script>
(function () {
var pilih = document.getElementById('filter-kelas');
var info = document.getElementById('filter-kelas-info');
var baris = document.querySelectorAll('table.grid tbody tr[data-kelas]');
function perbarui() {
var v = pilih.value, n = 0, total = 0;
baris.forEach(function (tr) {
var kelas = tr.dataset.kelas;
if (kelas === '__total__') {
// baris rata-rata/% lulus: dihitung dari seluruh MK, cuma masuk akal saat "Semua"
tr.style.display = v === '' ? '' : 'none';
return;
}
total++;
var tampil = v === '' || kelas === v;
tr.style.display = tampil ? '' : 'none';
if (tampil) n++;
});
info.textContent = v === '' ? '' : n + ' dari ' + total + ' mahasiswa ditampilkan';
}
pilih.addEventListener('change', perbarui);
perbarui();
})();
</script>
{% endif %}
{% endblock %}
+40 -1
View File
@@ -96,6 +96,20 @@
</label>
{% endif %}
<h2 class="bagian">NILAI MAHASISWA</h2>
{% set ada_kelas = grid|selectattr('kelas')|list|length > 0 %}
{% if ada_kelas %}
{% set semua_kelas = grid|map(attribute='kelas')|select|unique|sort|list %}
<div class="filter-baris">
<label for="filter-kelas">Tampilkan kelas:</label>
<select id="filter-kelas" style="width:auto">
<option value="">Semua</option>
{% for k in semua_kelas %}
<option value="{{ k }}">Kelas {{ k }}</option>
{% endfor %}
</select>
<span id="filter-kelas-info" class="keterangan" style="margin:0"></span>
</div>
{% endif %}
<div class="gulir" style="max-height:480px;overflow-y:auto">
<table class="grid">
<thead>
@@ -103,17 +117,19 @@
<th style="width:44px" class="tengah">No.</th>
<th style="width:112px">NIM</th>
<th style="min-width:150px">Nama</th>
{% if ada_kelas %}<th style="width:60px" class="tengah">Kelas</th>{% endif %}
<th class="tengah" style="width:140px">Nilai (0100)</th>
</tr>
</thead>
<tbody>
{% for m in grid %}
<tr>
<tr data-kelas="{{ m.kelas or '' }}">
<td class="tengah">{{ loop.index }}
<input type="hidden" name="nim_{{ loop.index0 }}" value="{{ m.nim }}">
<input type="hidden" name="nama_{{ loop.index0 }}" value="{{ m.nama }}"></td>
<td>{{ m.nim }}</td>
<td>{{ m.nama }}</td>
{% if ada_kelas %}<td class="tengah">{{ m.kelas or '—' }}</td>{% endif %}
<td class="isian"><input type="number" name="nilai_{{ loop.index0 }}"
min="0" max="100" step="any"
value="{{ '%g'|format(m.nilai_awal) if m.nilai_awal is not none else '' }}"></td>
@@ -129,6 +145,29 @@
</div>
</form>
{% if ada_kelas %}
<script>
(function () {
var pilih = document.getElementById('filter-kelas');
var info = document.getElementById('filter-kelas-info');
var baris = document.querySelectorAll('table.grid tbody tr[data-kelas]');
function perbarui() {
var v = pilih.value, n = 0;
baris.forEach(function (tr) {
var tampil = v === '' || tr.dataset.kelas === v;
tr.style.display = tampil ? '' : 'none';
if (tampil) n++;
});
info.textContent = v === '' ? '' : n + ' dari ' + baris.length + ' mahasiswa ditampilkan';
}
pilih.addEventListener('change', perbarui);
perbarui();
})();
</script>
{% endif %}
{% if komponen.tugas %}
<script>
const BENTUK_TEMPLATE = {