dqlab-grader 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dqcheck/__init__.py +296 -0
- dqcheck/admin.py +176 -0
- dqcheck/analisis.py +491 -0
- dqcheck/config.py +18 -0
- dqcheck/soal/__init__.py +50 -0
- dqcheck/soal/sesi06.py +239 -0
- dqcheck/soal/sesi07.py +237 -0
- dqcheck/soal/sesi08.py +234 -0
- dqcheck/soal/sesi09.py +283 -0
- dqcheck/soal/sesi10.py +238 -0
- dqcheck/soal/sesi11.py +204 -0
- dqcheck/soal/sesi12.py +179 -0
- dqcheck/storage.py +144 -0
- dqcheck/tampilan.py +98 -0
- dqlab_grader-0.1.0.dist-info/METADATA +108 -0
- dqlab_grader-0.1.0.dist-info/RECORD +19 -0
- dqlab_grader-0.1.0.dist-info/WHEEL +5 -0
- dqlab_grader-0.1.0.dist-info/licenses/LICENSE +21 -0
- dqlab_grader-0.1.0.dist-info/top_level.txt +1 -0
dqcheck/__init__.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""dqcheck — pengecek jawaban latihan Data Analyst with Python.
|
|
2
|
+
|
|
3
|
+
Pemakaian singkat (di Colab / Jupyter)::
|
|
4
|
+
|
|
5
|
+
import dqcheck as dq
|
|
6
|
+
dq.mulai(nama="Budi Santoso", id_peserta="DQ-001", sesi=6)
|
|
7
|
+
... # kerjakan soal
|
|
8
|
+
dq.cek("2.1") # cek jawaban soal 2.1
|
|
9
|
+
dq.daftar_soal() # soal mana yang belum selesai
|
|
10
|
+
dq.status() # rekap jawabanmu
|
|
11
|
+
|
|
12
|
+
Ketik ``dq.bantuan()`` untuk panduan lengkap.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
import datetime as _dt
|
|
18
|
+
import inspect as _inspect
|
|
19
|
+
import re as _re
|
|
20
|
+
import uuid as _uuid
|
|
21
|
+
|
|
22
|
+
from . import analisis as _analisis
|
|
23
|
+
from . import soal as _soal
|
|
24
|
+
from . import storage as _storage
|
|
25
|
+
from .tampilan import kartu as _kartu, tabel as _tabel
|
|
26
|
+
|
|
27
|
+
__all__ = ["mulai", "ganti_sesi", "cek", "status", "daftar_soal", "bantuan", "kirim_ulang"]
|
|
28
|
+
|
|
29
|
+
_POLA_ID = _re.compile(r"^[A-Za-z0-9_.\-]{2,40}$")
|
|
30
|
+
_peserta = {}
|
|
31
|
+
_sesi_aktif = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ------------------------------------------------------------------ identitas
|
|
35
|
+
|
|
36
|
+
def mulai(nama=None, id_peserta=None, sesi=None):
|
|
37
|
+
"""Isi identitas peserta (wajib sebelum ``cek``). Sesi opsional."""
|
|
38
|
+
global _sesi_aktif
|
|
39
|
+
nama = (nama or input("Nama lengkap : ")).strip()
|
|
40
|
+
id_peserta = (id_peserta or input("ID peserta : ")).strip()
|
|
41
|
+
nama = " ".join(nama.split())
|
|
42
|
+
|
|
43
|
+
salah = []
|
|
44
|
+
if not 2 <= len(nama) <= 80:
|
|
45
|
+
salah.append("Nama wajib diisi (2–80 karakter).")
|
|
46
|
+
if not _POLA_ID.match(id_peserta):
|
|
47
|
+
salah.append("ID peserta 2–40 karakter, hanya huruf, angka, titik, `-` atau `_` (tanpa spasi).")
|
|
48
|
+
if sesi is not None and int(sesi) not in _soal.daftar_sesi():
|
|
49
|
+
salah.append(f"Sesi {sesi} belum tersedia. Pilihan: {_soal.daftar_sesi()}.")
|
|
50
|
+
if salah:
|
|
51
|
+
_kartu("salah", "Identitas belum valid", bagian=[("Perbaiki:", salah)],
|
|
52
|
+
catatan='Contoh: `dq.mulai(nama="Budi Santoso", id_peserta="DQ-001", sesi=6)`')
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
_peserta.update(nama=nama, id_peserta=id_peserta)
|
|
56
|
+
if sesi is not None:
|
|
57
|
+
_sesi_aktif = int(sesi)
|
|
58
|
+
|
|
59
|
+
mode = _storage.mode()
|
|
60
|
+
info = [f"Nama: {nama}", f"ID peserta: `{id_peserta}`"]
|
|
61
|
+
if _sesi_aktif:
|
|
62
|
+
info.append(f"Sesi aktif: {_sesi_aktif} — {_soal.judul_sesi(_sesi_aktif)} "
|
|
63
|
+
f"({len(_soal.semua_soal(_sesi_aktif))} soal)")
|
|
64
|
+
info.append("Penyimpanan: server (jawaban tercatat ke instruktur)" if mode == "server"
|
|
65
|
+
else "Penyimpanan: LOKAL saja (belum terhubung ke server instruktur)")
|
|
66
|
+
langkah = [
|
|
67
|
+
"Kerjakan soal di sel jawabanmu, lalu jalankan sel tersebut.",
|
|
68
|
+
"Cek jawaban: `dq.cek(\"2.1\")`" + ("" if _sesi_aktif else " (atau `dq.cek(\"2.1\", sesi=6)`)"),
|
|
69
|
+
"Lihat soal yang belum selesai: `dq.daftar_soal()`",
|
|
70
|
+
"Lihat rekap jawabanmu: `dq.status()`",
|
|
71
|
+
]
|
|
72
|
+
_kartu("info" if mode == "server" else "peringatan", f"Halo, {nama}! Siap mengecek jawaban.",
|
|
73
|
+
bagian=[("Identitas", info), ("Langkah berikutnya", langkah)],
|
|
74
|
+
catatan="Salah itu wajar — setiap percobaan tercatat dan kamu boleh mencoba lagi sampai benar.")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def ganti_sesi(sesi):
|
|
78
|
+
"""Ganti sesi aktif, misal ``dq.ganti_sesi(7)``."""
|
|
79
|
+
global _sesi_aktif
|
|
80
|
+
if int(sesi) not in _soal.daftar_sesi():
|
|
81
|
+
_kartu("salah", f"Sesi {sesi} belum tersedia", sub=f"Pilihan: {_soal.daftar_sesi()}")
|
|
82
|
+
return
|
|
83
|
+
_sesi_aktif = int(sesi)
|
|
84
|
+
_kartu("info", f"Sesi aktif: {sesi} — {_soal.judul_sesi(sesi)}",
|
|
85
|
+
sub=f"{len(_soal.semua_soal(sesi))} soal. Lihat daftarnya: dq.daftar_soal()")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _butuh_login():
|
|
89
|
+
if _peserta:
|
|
90
|
+
return False
|
|
91
|
+
_kartu("peringatan", "Isi identitas dulu sebelum mengecek jawaban",
|
|
92
|
+
bagian=[("Jalankan:", ['`dq.mulai(nama="Nama Lengkapmu", id_peserta="ID-dari-instruktur", sesi=6)`'])])
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _tentukan_sesi(sesi):
|
|
97
|
+
s = sesi if sesi is not None else _sesi_aktif
|
|
98
|
+
if s is None:
|
|
99
|
+
_kartu("peringatan", "Sesi belum ditentukan",
|
|
100
|
+
bagian=[("Pilih salah satu:", ["`dq.cek(\"2.1\", sesi=6)`", "`dq.ganti_sesi(6)` lalu `dq.cek(\"2.1\")`"])])
|
|
101
|
+
return None
|
|
102
|
+
if int(s) not in _soal.daftar_sesi():
|
|
103
|
+
_kartu("salah", f"Sesi {s} belum tersedia", sub=f"Pilihan: {_soal.daftar_sesi()}")
|
|
104
|
+
return None
|
|
105
|
+
return int(s)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------- cek
|
|
109
|
+
|
|
110
|
+
def cek(soal, sesi=None):
|
|
111
|
+
"""Cek jawaban satu soal, misal ``dq.cek("2.1")`` atau ``dq.cek("Capstone", sesi=8)``."""
|
|
112
|
+
if _butuh_login():
|
|
113
|
+
return
|
|
114
|
+
s = _tentukan_sesi(sesi)
|
|
115
|
+
if s is None:
|
|
116
|
+
return
|
|
117
|
+
try:
|
|
118
|
+
sid, kode = _soal.cari_soal(s, soal)
|
|
119
|
+
except KeyError:
|
|
120
|
+
_kartu("salah", f"Soal \"{soal}\" tidak ada di Sesi {s}",
|
|
121
|
+
bagian=[("Soal yang tersedia:", ", ".join(_soal.semua_soal(s)))],
|
|
122
|
+
catatan="Perhatikan penulisan, contoh: `dq.cek(\"2.1\")`, `dq.cek(\"Capstone A\")`.")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
ns = _inspect.currentframe().f_back.f_globals
|
|
126
|
+
hasil = _analisis.jalankan(kode, ns, f"sesi{s} {sid}")
|
|
127
|
+
|
|
128
|
+
rec = {
|
|
129
|
+
"waktu": _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
130
|
+
"id_peserta": _peserta["id_peserta"],
|
|
131
|
+
"nama": _peserta["nama"],
|
|
132
|
+
"sesi": s,
|
|
133
|
+
"soal": sid,
|
|
134
|
+
"status": "BENAR" if hasil.benar else "SALAH",
|
|
135
|
+
"pesan": hasil.ringkas,
|
|
136
|
+
"uid": _uuid.uuid4().hex,
|
|
137
|
+
"versi": __version__,
|
|
138
|
+
}
|
|
139
|
+
simpan = _storage.simpan(rec)
|
|
140
|
+
ke = simpan["percobaan_ke"]
|
|
141
|
+
sub = f"Sesi {s} · Soal {sid} · {_peserta['nama']} ({_peserta['id_peserta']})" + (f" · percobaan ke-{ke}" if ke else "")
|
|
142
|
+
|
|
143
|
+
if simpan["tempat"] == "server":
|
|
144
|
+
catatan = "Tersimpan di server instruktur ✓"
|
|
145
|
+
elif simpan["tempat"] == "lokal":
|
|
146
|
+
catatan = f"Tersimpan lokal di `{_storage.config.LOCAL_CSV}` (mode lokal)."
|
|
147
|
+
elif simpan["tempat"] == "antrean":
|
|
148
|
+
catatan = ("⚠️ Server tidak terjangkau — hasil disimpan di antrean dan akan dikirim otomatis "
|
|
149
|
+
"saat kamu mengecek lagi (atau jalankan `dq.kirim_ulang()`).")
|
|
150
|
+
else:
|
|
151
|
+
catatan = f"⚠️ Hasil TIDAK tersimpan: {simpan['error']}"
|
|
152
|
+
|
|
153
|
+
if hasil.benar:
|
|
154
|
+
_kartu("benar", "BENAR! Jawabanmu lolos semua pengecekan", sub=sub,
|
|
155
|
+
bagian=[("Output", hasil.output)] if hasil.output.strip() else [], catatan=catatan)
|
|
156
|
+
else:
|
|
157
|
+
bagian = [
|
|
158
|
+
("Pengecekan yang gagal", f"{hasil.masalah}" if hasil.masalah else ""),
|
|
159
|
+
("Apa yang belum tepat", hasil.detail),
|
|
160
|
+
("Nilai variabelmu saat ini", hasil.konteks),
|
|
161
|
+
("Cara memperbaiki", hasil.saran + [f"Setelah diperbaiki, jalankan ulang sel jawabanmu lalu `dq.cek(\"{sid}\")` lagi."]),
|
|
162
|
+
]
|
|
163
|
+
_kartu("salah", "Belum tepat — ayo coba lagi!", sub=sub, bagian=bagian, catatan=catatan)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def kirim_ulang():
|
|
167
|
+
"""Kirim ulang hasil yang tertunda karena koneksi putus."""
|
|
168
|
+
sisa = _storage.kirim_antrean()
|
|
169
|
+
if sisa:
|
|
170
|
+
_kartu("peringatan", f"Masih ada {sisa} hasil tertunda", sub="Cek koneksi internet lalu coba lagi.")
|
|
171
|
+
else:
|
|
172
|
+
_kartu("benar", "Tidak ada hasil tertunda — semua sudah terkirim.")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ------------------------------------------------------------------- status
|
|
176
|
+
|
|
177
|
+
def _rekap(rows):
|
|
178
|
+
"""Kelompokkan riwayat per (sesi, soal)."""
|
|
179
|
+
rows = sorted(rows, key=lambda r: r["waktu"]) # stabil: urutan asli dipertahankan bila waktu sama
|
|
180
|
+
rek = {}
|
|
181
|
+
for r in rows:
|
|
182
|
+
k = (int(r["sesi"]), r["soal"])
|
|
183
|
+
d = rek.setdefault(k, {"percobaan": 0, "benar_di": None, "terakhir": None, "waktu": None})
|
|
184
|
+
d["percobaan"] += 1
|
|
185
|
+
d["terakhir"] = r["status"]
|
|
186
|
+
d["waktu"] = r["waktu"]
|
|
187
|
+
if r["status"] == "BENAR" and d["benar_di"] is None:
|
|
188
|
+
d["benar_di"] = d["percobaan"]
|
|
189
|
+
return rek
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _ambil(id_peserta, sesi=None):
|
|
193
|
+
try:
|
|
194
|
+
return _storage.ambil_riwayat(id_peserta, sesi)
|
|
195
|
+
except _storage.ServerError as e:
|
|
196
|
+
_kartu("salah", "Gagal mengambil status dari server", sub=str(e))
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def status(id_peserta=None, sesi=None, kembalikan=False):
|
|
201
|
+
"""Rekap jawaban (benar/salah + jumlah percobaan) milik peserta.
|
|
202
|
+
|
|
203
|
+
``dq.status()`` untuk peserta yang sedang login, atau ``dq.status("DQ-001")``
|
|
204
|
+
untuk mengecek ID tertentu tanpa login. Tambahkan ``sesi=6`` untuk satu sesi.
|
|
205
|
+
"""
|
|
206
|
+
id_peserta = id_peserta or _peserta.get("id_peserta")
|
|
207
|
+
if not id_peserta:
|
|
208
|
+
_kartu("peringatan", "Masukkan ID peserta", sub='Contoh: dq.status("DQ-001") atau login dulu dengan dq.mulai(...)')
|
|
209
|
+
return None
|
|
210
|
+
rows = _ambil(id_peserta, sesi)
|
|
211
|
+
if rows is None:
|
|
212
|
+
return None
|
|
213
|
+
if not rows:
|
|
214
|
+
_kartu("info", f"Belum ada jawaban tercatat untuk ID `{id_peserta}`" + (f" di Sesi {sesi}" if sesi else ""),
|
|
215
|
+
sub="Pastikan ID-nya benar. Mulai mengecek dengan dq.cek(\"nomor_soal\").")
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
rek = _rekap(rows)
|
|
219
|
+
nama = sorted(rows, key=lambda r: r["waktu"])[-1]["nama"]
|
|
220
|
+
ringkas = []
|
|
221
|
+
for s in sorted({k[0] for k in rek}):
|
|
222
|
+
total = len(_soal.semua_soal(s)) if s in _soal.daftar_sesi() else "?"
|
|
223
|
+
benar = sum(1 for (ss, _), d in rek.items() if ss == s and d["benar_di"])
|
|
224
|
+
ringkas.append(f"Sesi {s}: {benar}/{total} soal benar")
|
|
225
|
+
_kartu("info", f"Status jawaban {nama} ({id_peserta})", bagian=[("Ringkasan", ringkas)],
|
|
226
|
+
catatan="✅ = sudah pernah benar · ❌ = belum benar. Soal yang belum dikerjakan: dq.daftar_soal()")
|
|
227
|
+
|
|
228
|
+
data = []
|
|
229
|
+
for (s, sid), d in sorted(rek.items(), key=lambda kv: (kv[0][0], _soal.urutan(kv[0][0], kv[0][1]))):
|
|
230
|
+
data.append({
|
|
231
|
+
"Sesi": s, "Soal": sid,
|
|
232
|
+
"Status": "✅ Benar" if d["benar_di"] else "❌ Salah",
|
|
233
|
+
"Jumlah percobaan": d["percobaan"],
|
|
234
|
+
"Benar di percobaan ke-": d["benar_di"] or "-",
|
|
235
|
+
"Hasil terakhir": d["terakhir"],
|
|
236
|
+
"Terakhir dicek (UTC)": d["waktu"].replace("T", " ").replace("Z", ""),
|
|
237
|
+
})
|
|
238
|
+
df = _tabel(data, list(data[0].keys()))
|
|
239
|
+
return df if kembalikan else None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def daftar_soal(sesi=None, hanya_belum=False, kembalikan=False):
|
|
243
|
+
"""Daftar soal satu sesi beserta statusmu: ✅ benar, ❌ belum benar, ⬜ belum dikerjakan."""
|
|
244
|
+
s = _tentukan_sesi(sesi)
|
|
245
|
+
if s is None:
|
|
246
|
+
return None
|
|
247
|
+
rek = {}
|
|
248
|
+
if _peserta:
|
|
249
|
+
rows = _ambil(_peserta["id_peserta"], s)
|
|
250
|
+
rek = _rekap(rows or [])
|
|
251
|
+
|
|
252
|
+
data = []
|
|
253
|
+
for sid in _soal.semua_soal(s):
|
|
254
|
+
d = rek.get((s, sid))
|
|
255
|
+
if d and d["benar_di"]:
|
|
256
|
+
st = "✅ Benar"
|
|
257
|
+
elif d:
|
|
258
|
+
st = f"❌ Belum benar ({d['percobaan']}x)"
|
|
259
|
+
else:
|
|
260
|
+
st = "⬜ Belum dikerjakan"
|
|
261
|
+
if hanya_belum and st.startswith("✅"):
|
|
262
|
+
continue
|
|
263
|
+
data.append({"Soal": sid, "Status": st, "Perintah cek": f'dq.cek("{sid}", sesi={s})'})
|
|
264
|
+
|
|
265
|
+
total = len(_soal.semua_soal(s))
|
|
266
|
+
benar = sum(1 for sid in _soal.semua_soal(s) if rek.get((s, sid), {}).get("benar_di"))
|
|
267
|
+
sub = (f"Progres: {benar}/{total} benar · sisa {total - benar} soal" if _peserta
|
|
268
|
+
else "Login dengan dq.mulai(...) untuk melihat statusmu per soal.")
|
|
269
|
+
_kartu("info", f"Sesi {s} — {_soal.judul_sesi(s)}", sub=sub)
|
|
270
|
+
if not data:
|
|
271
|
+
_kartu("benar", "Semua soal sesi ini sudah benar. Mantap! 🏆")
|
|
272
|
+
return None
|
|
273
|
+
df = _tabel(data, ["Soal", "Status", "Perintah cek"])
|
|
274
|
+
return df if kembalikan else None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ------------------------------------------------------------------ bantuan
|
|
278
|
+
|
|
279
|
+
def bantuan():
|
|
280
|
+
"""Tampilkan panduan singkat."""
|
|
281
|
+
_kartu("info", f"Panduan dqcheck v{__version__}", bagian=[
|
|
282
|
+
("1. Isi identitas (wajib, sekali per notebook)",
|
|
283
|
+
['`dq.mulai(nama="Budi Santoso", id_peserta="DQ-001", sesi=6)`']),
|
|
284
|
+
("2. Kerjakan soal, jalankan selnya, lalu cek",
|
|
285
|
+
['`dq.cek("2.1")` — cek soal 2.1 di sesi aktif',
|
|
286
|
+
'`dq.cek("Capstone A", sesi=7)` — cek soal di sesi lain',
|
|
287
|
+
'`dq.ganti_sesi(8)` — pindah sesi aktif']),
|
|
288
|
+
("3. Pantau progres",
|
|
289
|
+
['`dq.daftar_soal()` — semua soal sesi aktif + statusnya',
|
|
290
|
+
'`dq.daftar_soal(hanya_belum=True)` — hanya yang belum benar',
|
|
291
|
+
'`dq.status()` — rekap benar/salah & jumlah percobaan',
|
|
292
|
+
'`dq.status("DQ-001")` — cek status ID tertentu']),
|
|
293
|
+
("Tips", ["Variabel/function harus bernama persis seperti di soal.",
|
|
294
|
+
"Jalankan ulang sel jawaban setiap kali kamu mengubah kode, baru cek lagi.",
|
|
295
|
+
"Setiap percobaan tercatat; yang dihitung adalah apakah kamu pernah benar."]),
|
|
296
|
+
], catatan=f"Sesi tersedia: {_soal.daftar_sesi()} · Mode penyimpanan: {_storage.mode()}")
|
dqcheck/admin.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import glob
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import urllib.request
|
|
6
|
+
|
|
7
|
+
from . import soal as _soal
|
|
8
|
+
|
|
9
|
+
KOLOM_WAJIB = {"waktu", "id_peserta", "nama", "sesi", "soal", "status"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _pd():
|
|
13
|
+
try:
|
|
14
|
+
import pandas as pd
|
|
15
|
+
return pd
|
|
16
|
+
except ImportError:
|
|
17
|
+
raise SystemExit("Modul admin butuh pandas: pip install pandas openpyxl") from None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def muat(sumber=None, endpoint=None, admin_key=None, sesi=None):
|
|
21
|
+
"""Baca semua percobaan menjadi satu DataFrame.
|
|
22
|
+
|
|
23
|
+
sumber : folder (dicari rekursif file ``percobaan.csv`` / CSV dqcheck) atau satu file CSV
|
|
24
|
+
endpoint : URL Worker; butuh ``admin_key``
|
|
25
|
+
"""
|
|
26
|
+
pd = _pd()
|
|
27
|
+
if endpoint:
|
|
28
|
+
url = endpoint.rstrip("/") + "/export" + (f"?sesi={int(sesi)}" if sesi else "")
|
|
29
|
+
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {admin_key}",
|
|
30
|
+
"User-Agent": "dqcheck-admin"})
|
|
31
|
+
with urllib.request.urlopen(req, timeout=60) as r:
|
|
32
|
+
df = pd.read_csv(io.StringIO(r.read().decode("utf-8")), dtype={"id_peserta": str, "soal": str})
|
|
33
|
+
elif sumber and os.path.isdir(sumber):
|
|
34
|
+
files = sorted(set(glob.glob(os.path.join(sumber, "**", "percobaan.csv"), recursive=True)
|
|
35
|
+
+ glob.glob(os.path.join(sumber, "**", "jawaban_dqcheck*.csv"), recursive=True)))
|
|
36
|
+
if not files:
|
|
37
|
+
raise SystemExit(f"Tidak ada file percobaan.csv di {sumber}")
|
|
38
|
+
df = pd.concat([pd.read_csv(f, dtype={"id_peserta": str, "soal": str}) for f in files], ignore_index=True)
|
|
39
|
+
elif sumber:
|
|
40
|
+
df = pd.read_csv(sumber, dtype={"id_peserta": str, "soal": str})
|
|
41
|
+
else:
|
|
42
|
+
raise SystemExit("Isi salah satu: sumber=... atau endpoint=... + admin_key=...")
|
|
43
|
+
|
|
44
|
+
kurang = KOLOM_WAJIB - set(df.columns)
|
|
45
|
+
if kurang:
|
|
46
|
+
raise SystemExit(f"Kolom wajib tidak ada: {sorted(kurang)}")
|
|
47
|
+
if "uid" in df.columns:
|
|
48
|
+
df = df.drop_duplicates("uid")
|
|
49
|
+
df["sesi"] = df["sesi"].astype(int)
|
|
50
|
+
if sesi:
|
|
51
|
+
df = df[df["sesi"] == int(sesi)]
|
|
52
|
+
df = df.sort_values("waktu", kind="stable").reset_index(drop=True)
|
|
53
|
+
df["percobaan_ke"] = df.groupby(["id_peserta", "sesi", "soal"]).cumcount() + 1
|
|
54
|
+
return df
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _urut(sesi, soal):
|
|
58
|
+
try:
|
|
59
|
+
return _soal.urutan(sesi, soal)
|
|
60
|
+
except KeyError:
|
|
61
|
+
return 999
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def rekap(df):
|
|
65
|
+
"""Hasilkan dict DataFrame: ringkasan, detail, per_soal, matriks per sesi, dan log mentah."""
|
|
66
|
+
pd = _pd()
|
|
67
|
+
nama_terakhir = df.groupby("id_peserta")["nama"].last()
|
|
68
|
+
|
|
69
|
+
g = df.groupby(["id_peserta", "sesi", "soal"], sort=False)
|
|
70
|
+
detail = g.agg(
|
|
71
|
+
jumlah_percobaan=("status", "size"),
|
|
72
|
+
jumlah_benar=("status", lambda s: int((s == "BENAR").sum())),
|
|
73
|
+
hasil_terakhir=("status", "last"),
|
|
74
|
+
pertama=("waktu", "min"),
|
|
75
|
+
terakhir=("waktu", "max"),
|
|
76
|
+
).reset_index()
|
|
77
|
+
benar_di = (df[df["status"] == "BENAR"].groupby(["id_peserta", "sesi", "soal"])["percobaan_ke"].min()
|
|
78
|
+
.rename("benar_di_percobaan"))
|
|
79
|
+
detail = detail.merge(benar_di, on=["id_peserta", "sesi", "soal"], how="left")
|
|
80
|
+
detail["status"] = detail["benar_di_percobaan"].notna().map({True: "BENAR", False: "SALAH"})
|
|
81
|
+
detail.insert(1, "nama", detail["id_peserta"].map(nama_terakhir))
|
|
82
|
+
detail["_u"] = [_urut(s, q) for s, q in zip(detail["sesi"], detail["soal"])]
|
|
83
|
+
detail = detail.sort_values(["sesi", "id_peserta", "_u"]).drop(columns="_u")
|
|
84
|
+
detail = detail[["id_peserta", "nama", "sesi", "soal", "status", "jumlah_percobaan",
|
|
85
|
+
"benar_di_percobaan", "jumlah_benar", "hasil_terakhir", "pertama", "terakhir"]]
|
|
86
|
+
detail["benar_di_percobaan"] = detail["benar_di_percobaan"].astype("Int64")
|
|
87
|
+
|
|
88
|
+
def _total(s):
|
|
89
|
+
return len(_soal.semua_soal(s)) if s in _soal.daftar_sesi() else None
|
|
90
|
+
|
|
91
|
+
ring = detail.groupby(["id_peserta", "sesi"]).agg(
|
|
92
|
+
dikerjakan=("soal", "size"),
|
|
93
|
+
benar=("status", lambda s: int((s == "BENAR").sum())),
|
|
94
|
+
total_percobaan=("jumlah_percobaan", "sum"),
|
|
95
|
+
rata2_percobaan_sampai_benar=("benar_di_percobaan", "mean"),
|
|
96
|
+
terakhir=("terakhir", "max"),
|
|
97
|
+
).reset_index()
|
|
98
|
+
ring.insert(1, "nama", ring["id_peserta"].map(nama_terakhir))
|
|
99
|
+
ring["total_soal"] = ring["sesi"].map(_total)
|
|
100
|
+
ring["belum_benar"] = ring["dikerjakan"] - ring["benar"]
|
|
101
|
+
ring["belum_dikerjakan"] = ring["total_soal"] - ring["dikerjakan"]
|
|
102
|
+
ring["persen_benar"] = (100 * ring["benar"] / ring["total_soal"]).round(1)
|
|
103
|
+
ring["rata2_percobaan_sampai_benar"] = ring["rata2_percobaan_sampai_benar"].astype(float).round(2)
|
|
104
|
+
ring = ring[["id_peserta", "nama", "sesi", "total_soal", "benar", "belum_benar", "belum_dikerjakan",
|
|
105
|
+
"persen_benar", "total_percobaan", "rata2_percobaan_sampai_benar", "terakhir"]]
|
|
106
|
+
ring = ring.sort_values(["sesi", "persen_benar", "id_peserta"], ascending=[True, False, True])
|
|
107
|
+
|
|
108
|
+
per_soal = detail.groupby(["sesi", "soal"]).agg(
|
|
109
|
+
peserta_mencoba=("id_peserta", "nunique"),
|
|
110
|
+
peserta_benar=("status", lambda s: int((s == "BENAR").sum())),
|
|
111
|
+
total_percobaan=("jumlah_percobaan", "sum"),
|
|
112
|
+
rata2_percobaan_sampai_benar=("benar_di_percobaan", "mean"),
|
|
113
|
+
).reset_index()
|
|
114
|
+
per_soal["persen_benar"] = (100 * per_soal["peserta_benar"] / per_soal["peserta_mencoba"]).round(1)
|
|
115
|
+
per_soal["rata2_percobaan_sampai_benar"] = per_soal["rata2_percobaan_sampai_benar"].astype(float).round(2)
|
|
116
|
+
per_soal["_u"] = [_urut(s, q) for s, q in zip(per_soal["sesi"], per_soal["soal"])]
|
|
117
|
+
per_soal = per_soal.sort_values(["sesi", "_u"]).drop(columns="_u")
|
|
118
|
+
|
|
119
|
+
hasil = {"ringkasan": ring, "detail": detail, "per_soal": per_soal}
|
|
120
|
+
|
|
121
|
+
# matriks per sesi: baris = peserta, kolom = soal, isi = ✔/✘ (jumlah percobaan)
|
|
122
|
+
for s in sorted(detail["sesi"].unique()):
|
|
123
|
+
d = detail[detail["sesi"] == s].copy()
|
|
124
|
+
d["sel"] = [("✔" if st == "BENAR" else "✘") + f" ({n})"
|
|
125
|
+
for st, n in zip(d["status"], d["jumlah_percobaan"])]
|
|
126
|
+
m = d.pivot_table(index=["id_peserta", "nama"], columns="soal", values="sel", aggfunc="first")
|
|
127
|
+
urutan_kolom = list(_soal.semua_soal(s)) if s in _soal.daftar_sesi() else sorted(m.columns)
|
|
128
|
+
m = m.reindex(columns=[c for c in urutan_kolom] + [c for c in m.columns if c not in urutan_kolom])
|
|
129
|
+
m = m.fillna("").reset_index()
|
|
130
|
+
m.columns.name = None
|
|
131
|
+
hasil[f"sesi-{int(s):02d}"] = m
|
|
132
|
+
|
|
133
|
+
hasil["log_percobaan"] = df
|
|
134
|
+
return hasil
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def ekspor(hasil, keluar="rekap_dqcheck.xlsx"):
|
|
138
|
+
"""Simpan hasil ``rekap`` ke Excel (satu sheet per tabel) atau folder CSV."""
|
|
139
|
+
if keluar.lower().endswith(".xlsx"):
|
|
140
|
+
pd = _pd()
|
|
141
|
+
try:
|
|
142
|
+
with pd.ExcelWriter(keluar) as w:
|
|
143
|
+
for nama, tabel in hasil.items():
|
|
144
|
+
tabel.to_excel(w, sheet_name=nama[:31], index=False)
|
|
145
|
+
return keluar
|
|
146
|
+
except ImportError:
|
|
147
|
+
print("openpyxl tidak ada, beralih ke CSV. (pip install openpyxl untuk Excel)")
|
|
148
|
+
keluar = keluar[:-5]
|
|
149
|
+
os.makedirs(keluar, exist_ok=True)
|
|
150
|
+
for nama, tabel in hasil.items():
|
|
151
|
+
tabel.to_csv(os.path.join(keluar, f"{nama}.csv"), index=False)
|
|
152
|
+
return keluar
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main(argv=None):
|
|
156
|
+
p = argparse.ArgumentParser(prog="python -m dqcheck.admin",
|
|
157
|
+
description="Rekap & export jawaban peserta dqcheck.")
|
|
158
|
+
p.add_argument("--sumber", help="folder data hasil sinkron GitHub, atau satu file CSV")
|
|
159
|
+
p.add_argument("--endpoint", help="URL Worker (ambil langsung dari server)")
|
|
160
|
+
p.add_argument("--admin-key", default=os.environ.get("DQCHECK_ADMIN_KEY"),
|
|
161
|
+
help="ADMIN_KEY Worker (atau env DQCHECK_ADMIN_KEY)")
|
|
162
|
+
p.add_argument("--sesi", type=int, help="hanya sesi tertentu")
|
|
163
|
+
p.add_argument("--keluar", default="rekap_dqcheck.xlsx", help="file .xlsx atau nama folder CSV")
|
|
164
|
+
a = p.parse_args(argv)
|
|
165
|
+
|
|
166
|
+
df = muat(a.sumber, a.endpoint, a.admin_key, a.sesi)
|
|
167
|
+
hasil = rekap(df)
|
|
168
|
+
lokasi = ekspor(hasil, a.keluar)
|
|
169
|
+
r = hasil["ringkasan"]
|
|
170
|
+
print(f"{len(df)} percobaan dari {df['id_peserta'].nunique()} peserta, sesi {sorted(int(s) for s in df['sesi'].unique())}")
|
|
171
|
+
print(r.to_string(index=False, max_rows=30))
|
|
172
|
+
print(f"\nTersimpan: {lokasi}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
main()
|