dqlab-grader 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abel Kristanto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: dqlab-grader
3
+ Version: 0.1.0
4
+ Summary: Pengecek jawaban latihan Data Analyst with Python (DQLab) + pencatatan progres peserta
5
+ Author: Abel Kristanto
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/AbelKristanto/DQLab-dataanalystusingpython
8
+ Keywords: dqlab,data analyst,python,latihan,autograder,jupyter,colab
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Natural Language :: Indonesian
11
+ Classifier: Framework :: Jupyter
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Topic :: Education
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: admin
18
+ Requires-Dist: pandas; extra == "admin"
19
+ Requires-Dist: openpyxl; extra == "admin"
20
+ Dynamic: license-file
21
+
22
+ # dqlab-grader
23
+
24
+ Alat untuk **mengecek jawaban latihan** kelas Data Analyst with Python (DQLab).
25
+
26
+ Kamu cukup mengerjakan soal seperti biasa. Alat ini akan memberi tahu apakah jawabanmu sudah benar. Kalau belum, alat ini menjelaskan bagian yang salah dan cara memperbaikinya.
27
+
28
+ ---
29
+
30
+ ## Langkah 1: Pasang (sekali saja)
31
+
32
+ Tulis ini di sel paling atas notebook (Google Colab), lalu jalankan:
33
+
34
+ ```python
35
+ !pip install -q dqlab-grader
36
+ ```
37
+
38
+ ## Langkah 2: Isi nama dan ID
39
+
40
+ Jalankan ini sekali setiap kali membuka notebook. Perhatikan: namanya saat di-install `dqlab-grader`, tapi saat dipakai tetap `import dqcheck`.
41
+
42
+ ```python
43
+ import dqcheck as dq
44
+
45
+ dq.mulai(nama="Budi Santoso", id_peserta="DQ-001", sesi=6)
46
+ ```
47
+
48
+ - Ganti `nama` dengan nama lengkapmu.
49
+ - Ganti `id_peserta` dengan ID dari instruktur. Selalu pakai ID yang **sama**.
50
+ - Ganti `sesi` dengan sesi yang sedang kamu kerjakan.
51
+
52
+ ## Langkah 3: Kerjakan soal
53
+
54
+ Tulis jawabanmu di sel, lalu **jalankan selnya**.
55
+
56
+ ```python
57
+ nama_menu = "Kopi Susu Gula Aren"
58
+ harga = 18000
59
+ ```
60
+
61
+ ## Langkah 4: Cek jawaban
62
+
63
+ Di sel baru, tulis nomor soalnya:
64
+
65
+ ```python
66
+ dq.cek("2.1")
67
+ ```
68
+
69
+ Hasilnya:
70
+
71
+ - ✅ **BENAR**: lanjut ke soal berikutnya.
72
+ - ❌ **Belum tepat**: baca penjelasannya, perbaiki jawaban, **jalankan ulang sel jawabanmu**, lalu cek lagi.
73
+
74
+ Tidak apa-apa salah. Kamu boleh mencoba berkali-kali sampai benar.
75
+
76
+ ---
77
+
78
+ ## Perintah lain
79
+
80
+ | Mau apa? | Tulis ini |
81
+ |---|---|
82
+ | Cek soal nomor 2.1 | `dq.cek("2.1")` |
83
+ | Cek soal Capstone | `dq.cek("Capstone")` |
84
+ | Pindah ke sesi 7 | `dq.ganti_sesi(7)` |
85
+ | Lihat semua soal dan mana yang sudah benar | `dq.daftar_soal()` |
86
+ | Lihat soal yang belum benar saja | `dq.daftar_soal(hanya_belum=True)` |
87
+ | Lihat rekap jawabanku | `dq.status()` |
88
+ | Lihat bantuan | `dq.bantuan()` |
89
+
90
+ Arti tanda di daftar soal:
91
+
92
+ - ✅ sudah benar
93
+ - ❌ sudah dicoba tapi belum benar
94
+ - ⬜ belum dikerjakan
95
+
96
+ ---
97
+
98
+ ## Ada masalah?
99
+
100
+ | Pesan yang muncul | Yang harus dilakukan |
101
+ |---|---|
102
+ | "Isi identitas dulu" | Jalankan `dq.mulai(...)` lagi (Langkah 2). |
103
+ | "Variabel ... belum ada" | Jalankan dulu sel jawabanmu. Pastikan nama variabelnya sama persis dengan soal. |
104
+ | Sudah diperbaiki tapi masih salah | Kamu lupa menjalankan ulang sel jawaban. Jalankan dulu, baru cek lagi. |
105
+ | "Soal ... tidak ada" | Cek nomor soal dan sesinya. Lihat daftarnya dengan `dq.daftar_soal()`. |
106
+ | "Server tidak terjangkau" | Internetmu putus. Hasilmu tetap disimpan dan akan dikirim otomatis nanti. |
107
+
108
+ Kalau notebook di-restart, ulangi dari **Langkah 2**.
@@ -0,0 +1,87 @@
1
+ # dqlab-grader
2
+
3
+ Alat untuk **mengecek jawaban latihan** kelas Data Analyst with Python (DQLab).
4
+
5
+ Kamu cukup mengerjakan soal seperti biasa. Alat ini akan memberi tahu apakah jawabanmu sudah benar. Kalau belum, alat ini menjelaskan bagian yang salah dan cara memperbaikinya.
6
+
7
+ ---
8
+
9
+ ## Langkah 1: Pasang (sekali saja)
10
+
11
+ Tulis ini di sel paling atas notebook (Google Colab), lalu jalankan:
12
+
13
+ ```python
14
+ !pip install -q dqlab-grader
15
+ ```
16
+
17
+ ## Langkah 2: Isi nama dan ID
18
+
19
+ Jalankan ini sekali setiap kali membuka notebook. Perhatikan: namanya saat di-install `dqlab-grader`, tapi saat dipakai tetap `import dqcheck`.
20
+
21
+ ```python
22
+ import dqcheck as dq
23
+
24
+ dq.mulai(nama="Budi Santoso", id_peserta="DQ-001", sesi=6)
25
+ ```
26
+
27
+ - Ganti `nama` dengan nama lengkapmu.
28
+ - Ganti `id_peserta` dengan ID dari instruktur. Selalu pakai ID yang **sama**.
29
+ - Ganti `sesi` dengan sesi yang sedang kamu kerjakan.
30
+
31
+ ## Langkah 3: Kerjakan soal
32
+
33
+ Tulis jawabanmu di sel, lalu **jalankan selnya**.
34
+
35
+ ```python
36
+ nama_menu = "Kopi Susu Gula Aren"
37
+ harga = 18000
38
+ ```
39
+
40
+ ## Langkah 4: Cek jawaban
41
+
42
+ Di sel baru, tulis nomor soalnya:
43
+
44
+ ```python
45
+ dq.cek("2.1")
46
+ ```
47
+
48
+ Hasilnya:
49
+
50
+ - ✅ **BENAR**: lanjut ke soal berikutnya.
51
+ - ❌ **Belum tepat**: baca penjelasannya, perbaiki jawaban, **jalankan ulang sel jawabanmu**, lalu cek lagi.
52
+
53
+ Tidak apa-apa salah. Kamu boleh mencoba berkali-kali sampai benar.
54
+
55
+ ---
56
+
57
+ ## Perintah lain
58
+
59
+ | Mau apa? | Tulis ini |
60
+ |---|---|
61
+ | Cek soal nomor 2.1 | `dq.cek("2.1")` |
62
+ | Cek soal Capstone | `dq.cek("Capstone")` |
63
+ | Pindah ke sesi 7 | `dq.ganti_sesi(7)` |
64
+ | Lihat semua soal dan mana yang sudah benar | `dq.daftar_soal()` |
65
+ | Lihat soal yang belum benar saja | `dq.daftar_soal(hanya_belum=True)` |
66
+ | Lihat rekap jawabanku | `dq.status()` |
67
+ | Lihat bantuan | `dq.bantuan()` |
68
+
69
+ Arti tanda di daftar soal:
70
+
71
+ - ✅ sudah benar
72
+ - ❌ sudah dicoba tapi belum benar
73
+ - ⬜ belum dikerjakan
74
+
75
+ ---
76
+
77
+ ## Ada masalah?
78
+
79
+ | Pesan yang muncul | Yang harus dilakukan |
80
+ |---|---|
81
+ | "Isi identitas dulu" | Jalankan `dq.mulai(...)` lagi (Langkah 2). |
82
+ | "Variabel ... belum ada" | Jalankan dulu sel jawabanmu. Pastikan nama variabelnya sama persis dengan soal. |
83
+ | Sudah diperbaiki tapi masih salah | Kamu lupa menjalankan ulang sel jawaban. Jalankan dulu, baru cek lagi. |
84
+ | "Soal ... tidak ada" | Cek nomor soal dan sesinya. Lihat daftarnya dengan `dq.daftar_soal()`. |
85
+ | "Server tidak terjangkau" | Internetmu putus. Hasilmu tetap disimpan dan akan dikirim otomatis nanti. |
86
+
87
+ Kalau notebook di-restart, ulangi dari **Langkah 2**.
@@ -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()}")