TIF_E41211115_Genso_quiz_ba.../app/controllers/socket_conroller.py

404 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from flask_socketio import SocketIO, emit, join_room, leave_room
from flask import request
from app.services import SessionService
import threading
import time
import json
from redis import Redis
class RedisRepository:
"""Small helper wrapper to (de)serialize python objects to Redis."""
def __init__(self, redis: Redis):
self.redis = redis
def set_data(self, key: str, value):
self.redis.set(key, json.dumps(value))
def get_data(self, key: str):
data = self.redis.get(key)
return json.loads(data) if data else None
def delete_key(self, key: str):
self.redis.delete(key)
class SocketController:
def __init__(
self,
socketio: SocketIO,
redis: Redis,
session_service: SessionService,
):
self.socketio = socketio
self.session_service = session_service
self.redis_repo = RedisRepository(redis)
# Menyimpan SID admin untuk setiap session \u2192 {session_code: sid}
self.admin_sids: dict[str, str] = {}
self._register_events()
# ---------------------------------------------------------------------
# Helper utilities
# ---------------------------------------------------------------------
@staticmethod
def _is_correct(user_answer, question: dict) -> bool:
"""Bandingkan jawaban user dengan kunci jawaban."""
print("user answer", user_answer, "question_target", question["target_answer"])
if question["type"] == "fill_the_blank":
return (
str(user_answer).strip().lower()
== str(question["target_answer"]).strip().lower()
)
elif question["type"] == "true_false":
return bool(user_answer) == question["target_answer"]
elif question["type"] == "option":
# user_answer bisa dikirim sebagai index atau value teks option.
# Pastikan dicast ke int terlebih dahulu jika memungkinkan.
try:
return int(user_answer) == question["target_answer"]
except ValueError:
return (
str(user_answer).strip().lower()
== str(question["options"][question["target_answer"]])
.strip()
.lower()
)
return False
def _questions_key(self, session_code: str) -> str:
return f"session:{session_code}:questions"
def _answers_key(self, session_code: str) -> str:
return f"session:{session_code}:answers"
def _scores_key(self, session_code: str) -> str:
return f"session:{session_code}:scores"
# ---------------------------------------------------------------------
# Socket.IO event bindings
# ---------------------------------------------------------------------
def _register_events(self):
@self.socketio.on("connect")
def on_connect():
print(f"Client connected: {request.sid}")
emit("connection_response", {"status": "connected", "sid": request.sid})
@self.socketio.on("disconnect")
def on_disconnect():
print(f"Client disconnected: {request.sid}")
@self.socketio.on("join_room")
def handle_join_room(data):
session_code = data.get("session_code")
user_id = data.get("user_id")
if not session_code or not user_id:
emit("error", {"message": "session_code and user_id are required"})
return
session = self.session_service.join_session(
session_code=session_code, user_id=user_id
)
if session is None:
emit("error", {"message": "Failed to join session or session inactive"})
return
join_room(session_code)
# Kalau user ini admin, simpan SIDnya.
if session["is_admin"]:
self.admin_sids[session_code] = request.sid
message = "Admin has joined the room."
else:
message = f"User {session['username']} has joined the room."
print(message)
emit(
"room_message",
{
"type": "join",
"message": message,
"room": session_code,
"argument": "adm_update",
"data": session if not session["is_admin"] else None,
},
room=session_code,
)
@self.socketio.on("submit_answer")
def handle_submit_answer(data):
session_code = data.get("session_id")
user_id = data.get("user_id")
question_index = data.get("question_index")
user_answer = data.get("answer")
if not all(
[
session_code,
user_id,
question_index is not None,
user_answer is not None,
]
):
emit(
"error",
{
"message": "session_id, user_id, question_index, and answer are required"
},
)
return
# ----- 1. Ambil list pertanyaan untuk mendapatkan kunci jawaban ----------
questions = (
self.redis_repo.get_data(self._questions_key(session_code)) or []
)
question = next(
(q for q in questions if q["index"] == question_index), None
)
if question is None:
emit("error", {"message": "Question not found"})
return
is_correct = self._is_correct(user_answer, question)
print(
f"User {user_id} answered Q{question_index} with '{user_answer}' -> {'' if is_correct else ''}"
)
# ----- 2. Simpan jawaban ke Redis --------------------------------------
answers = self.redis_repo.get_data(self._answers_key(session_code)) or []
answers.append(
{
"user_id": user_id,
"question_index": question_index,
"answer": user_answer,
"correct": is_correct,
}
)
self.redis_repo.set_data(self._answers_key(session_code), answers)
# ----- 3. Update skor peruser -----------------------------------------
scores = self.redis_repo.get_data(self._scores_key(session_code)) or {}
user_score = scores.get(str(user_id), {"correct": 0, "incorrect": 0})
if is_correct:
user_score["correct"] += 1
else:
user_score["incorrect"] += 1
scores[str(user_id)] = user_score
self.redis_repo.set_data(self._scores_key(session_code), scores)
# ----- 4. Beri tahu user (ack) -----------------------------------------
emit(
"answer_submitted",
{
"user_id": user_id,
"question_index": question_index,
"answer": user_answer,
"correct": is_correct,
},
room=request.sid,
)
# ----- 5. Kirim update skor hanya ke admin -----------------------------
admin_sid = self.admin_sids.get(session_code)
if admin_sid:
emit("score_update", scores, room=admin_sid)
@self.socketio.on("leave_room")
def handle_leave_room(data):
session_code = data.get("session_id")
user_id = data.get("user_id")
username = data.get("username", "anonymous")
leave_room(session_code)
emit(
"room_message",
{
"type": "leave",
"message": f"{username} has left the room.",
"room": session_code,
"data": user_id,
},
room=session_code,
)
@self.socketio.on("send_message")
def on_send_message(data):
session_code = data.get("session_id")
message = data.get("message")
username = data.get("username", "anonymous")
emit(
"receive_message",
{"message": message, "from": username},
room=session_code,
)
@self.socketio.on("end_session")
def handle_end_session(data):
session_code = data.get("session_id")
user_id = data.get("user_id")
if not session_code or not user_id:
emit("error", {"message": "session_id and user_id required"})
return
# Validasi user berhak mengakhiri session
self.session_service.end_session(session_id=session_code, user_id=user_id)
answers = self.redis_repo.get_data(self._answers_key(session_code)) or []
scores = self.redis_repo.get_data(self._scores_key(session_code)) or {}
print("\n📦 Final Quiz Data for Session", session_code)
print("------------------------------------------------------------")
for entry in answers:
status = "" if entry["correct"] else ""
print(
f"User {entry['user_id']} - Q{entry['question_index']}: {entry['answer']} {status}"
)
print("\n🏁 Rekap Skor:")
for uid, sc in scores.items():
print(f"User {uid}: Benar {sc['correct']} | Salah {sc['incorrect']}")
# Bersihkan semua data session di Redis
for key in [
self._answers_key(session_code),
self._scores_key(session_code),
self._questions_key(session_code),
]:
self.redis_repo.delete_key(key)
emit(
"room_closed",
{"message": "Session has ended.", "room": session_code},
room=session_code,
)
@self.socketio.on("start_quiz")
def handle_start_quiz(data):
session_code = data.get("session_code")
if not session_code:
emit("error", {"message": "session_code is required"})
return
emit("quiz_started", {"message": "Quiz has started!"}, room=session_code)
threading.Thread(
target=self._simulate_quiz_flow, args=(session_code,), daemon=True
).start()
# ---------------------------------------------------------------------
# Quiz flow simulation -------------------------------------------------
# ---------------------------------------------------------------------
def _simulate_quiz_flow(self, session_code: str):
"""Mengirim list pertanyaan satu per satu secara otomatis (demo)."""
questions = [
{
"index": 1,
"question": "Kerajaan Hindu tertua di Indonesia adalah?",
"target_answer": "Kutai",
"duration": 30,
"type": "fill_the_blank",
"options": None,
},
{
"index": 2,
"question": "Apakah benar Majapahit mencapai puncak kejayaan pada masa Hayam Wuruk?",
"target_answer": True,
"duration": 30,
"type": "true_false",
"options": None,
},
{
"index": 3,
"question": "Kerajaan maritim terbesar di Asia Tenggara pada abad ke7 adalah?",
"target_answer": 2,
"duration": 30,
"type": "option",
"options": ["Majapahit", "Tarumanegara", "Sriwijaya", "Mataram Kuno"],
},
{
"index": 4,
"question": "Prasasti Yupa merupakan peninggalan dari kerajaan?",
"target_answer": "Kutai",
"duration": 30,
"type": "fill_the_blank",
"options": None,
},
{
"index": 5,
"question": "Apakah Tarumanegara terletak di wilayah Kalimantan Timur?",
"target_answer": False,
"duration": 30,
"type": "true_false",
"options": None,
},
{
"index": 6,
"question": "Kitab Negarakertagama ditulis oleh?",
"target_answer": 0,
"duration": 30,
"type": "option",
"options": [
"Empu Tantular",
"Empu Prapanca",
"Hayam Wuruk",
"Gajah Mada",
],
},
{
"index": 7,
"question": "Tokoh yang terkenal dengan Sumpah Palapa adalah?",
"target_answer": "Gajah Mada",
"duration": 30,
"type": "fill_the_blank",
"options": None,
},
{
"index": 8,
"question": "Candi Borobudur dibangun oleh kerajaan Hindu?",
"target_answer": False,
"duration": 30,
"type": "true_false",
"options": None,
},
{
"index": 9,
"question": "Raja terkenal dari Kerajaan Sriwijaya adalah?",
"target_answer": 1,
"duration": 30,
"type": "option",
"options": [
"Dapunta Hyang",
"Balaputradewa",
"Airlangga",
"Hayam Wuruk",
],
},
{
"index": 10,
"question": "Candi Prambanan merupakan peninggalan agama?",
"target_answer": "Hindu",
"duration": 30,
"type": "fill_the_blank",
"options": None,
},
]
# Simpan ke Redis agar bisa dipakai saat evaluasi jawaban
self.redis_repo.set_data(self._questions_key(session_code), questions)
# Beri sedikit jeda sebelum pertanyaan pertama
time.sleep(2)
for q in questions:
print(f"\n📢 Mengirim pertanyaan {q['index']} ke room {session_code}")
q.pop("target_answer")
self.socketio.emit("quiz_question", q, room=session_code)
time.sleep(q["duration"])
self.socketio.emit(
"quiz_done", {"message": "Quiz has ended!"}, room=session_code
)