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.
- dqlab_grader-0.1.0/LICENSE +21 -0
- dqlab_grader-0.1.0/PKG-INFO +108 -0
- dqlab_grader-0.1.0/README.md +87 -0
- dqlab_grader-0.1.0/dqcheck/__init__.py +296 -0
- dqlab_grader-0.1.0/dqcheck/admin.py +176 -0
- dqlab_grader-0.1.0/dqcheck/analisis.py +491 -0
- dqlab_grader-0.1.0/dqcheck/config.py +18 -0
- dqlab_grader-0.1.0/dqcheck/soal/__init__.py +50 -0
- dqlab_grader-0.1.0/dqcheck/soal/sesi06.py +239 -0
- dqlab_grader-0.1.0/dqcheck/soal/sesi07.py +237 -0
- dqlab_grader-0.1.0/dqcheck/soal/sesi08.py +234 -0
- dqlab_grader-0.1.0/dqcheck/soal/sesi09.py +283 -0
- dqlab_grader-0.1.0/dqcheck/soal/sesi10.py +238 -0
- dqlab_grader-0.1.0/dqcheck/soal/sesi11.py +204 -0
- dqlab_grader-0.1.0/dqcheck/soal/sesi12.py +179 -0
- dqlab_grader-0.1.0/dqcheck/storage.py +144 -0
- dqlab_grader-0.1.0/dqcheck/tampilan.py +98 -0
- dqlab_grader-0.1.0/dqlab_grader.egg-info/PKG-INFO +108 -0
- dqlab_grader-0.1.0/dqlab_grader.egg-info/SOURCES.txt +22 -0
- dqlab_grader-0.1.0/dqlab_grader.egg-info/dependency_links.txt +1 -0
- dqlab_grader-0.1.0/dqlab_grader.egg-info/requires.txt +4 -0
- dqlab_grader-0.1.0/dqlab_grader.egg-info/top_level.txt +1 -0
- dqlab_grader-0.1.0/pyproject.toml +31 -0
- dqlab_grader-0.1.0/setup.cfg +4 -0
|
@@ -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()}")
|