diff --git a/app/controllers/socket_conroller.py b/app/controllers/socket_conroller.py index 8fac996..358d8c4 100644 --- a/app/controllers/socket_conroller.py +++ b/app/controllers/socket_conroller.py @@ -2,7 +2,6 @@ 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 @@ -17,49 +16,6 @@ class SocketController: self.session_service = session_service self._register_events() - @staticmethod - def _is_correct(user_answer, question: dict) -> bool: - """Advanced answer validation method.""" - print(f"Validating answer: user={user_answer}, question={question}") - - try: - 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": - # Handle both index and text-based answers - try: - # First try numeric index comparison - return int(user_answer) == question["target_answer"] - except ValueError: - # If not an index, compare text - return ( - str(user_answer).strip().lower() - == str(question["options"][question["target_answer"]]) - .strip() - .lower() - ) - return False - except Exception as e: - print(f"Error in answer validation: {e}") - 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(): @@ -127,84 +83,61 @@ class SocketController: skip_sid=request.sid, ) - @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") + # @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 + # 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 + # quiz_service = QuizService(self.redis_repo) + # question = quiz_service.get_question(session_code, question_index) - is_correct = self._is_correct(user_answer, question) + # if question is None: + # emit("error", {"message": "Question not found"}) + # return - print( - f"User {user_id} answered Q{question_index} with '{user_answer}' -> {'✔' if is_correct else '✖'}" - ) + # is_correct = quiz_service.is_correct(user_answer, question) - # ----- 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) + # print( + # f"User {user_id} answered Q{question_index} with '{user_answer}' -> {'✔' if is_correct else '✖'}" + # ) - # ----- 3. Update skor per‑user ----------------------------------------- - 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) + # quiz_service.save_answer( + # session_code, user_id, question_index, user_answer, is_correct + # ) + # scores = quiz_service.update_score(session_code, user_id, is_correct) - # ----- 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, - ) + # 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) + # 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): @@ -283,175 +216,14 @@ class SocketController: @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"}) + session_id = data.get("session_id") + if not session_id: + emit("error", {"message": "session_id is required"}) return - emit("quiz_started", {"message": "Quiz has started!"}, room=session_code) + emit("quiz_started", {"message": "Quiz has started!"}, room=session_id) threading.Thread( - target=self._simulate_quiz_flow, args=(session_code,), daemon=True + target=self.session_service.simulate_quiz_flow, + args=(session_id, self.socketio), + daemon=True, ).start() - - def _simulate_quiz_flow(self, session_code: str): - """Enhanced quiz flow with better question management.""" - questions = [ - { - "index": 1, - "question": "Kerajaan Hindu tertua di Indonesia adalah?", - "target_answer": "Kutai", - "duration": 30, - "type": "fill_the_blank", - "points": 10, - "options": None, - }, - { - "index": 2, - "question": "Apakah benar Majapahit mencapai puncak kejayaan pada masa Hayam Wuruk?", - "target_answer": True, - "duration": 30, - "type": "true_false", - "points": 10, - "options": None, - }, - { - "index": 3, - "question": "Kerajaan maritim terbesar di Asia Tenggara pada abad ke‑7 adalah?", - "target_answer": 2, - "duration": 30, - "type": "option", - "points": 10, - "options": ["Majapahit", "Tarumanegara", "Sriwijaya", "Mataram Kuno"], - }, - ] - - # Simpan ke Redis - self.redis_repo.set_data(self._questions_key(session_code), questions) - - # Inisialisasi skor untuk semua peserta - scores = self.redis_repo.get_data(self._scores_key(session_code)) or {} - self.redis_repo.set_data(self._scores_key(session_code), scores) - - # Beri jeda sebelum pertanyaan pertama - time.sleep(2) - - for q in questions: - print(f"\n📢 Mengirim pertanyaan {q['index']} ke room {session_code}") - - # Kirim pertanyaan tanpa jawaban yang benar - question_to_send = q.copy() - question_to_send.pop("target_answer", None) - - self.socketio.emit("quiz_question", question_to_send, room=session_code) - - # Tunggu durasi pertanyaan - time.sleep(q["duration"]) - - # Generate dan kirim rekap kuis - self._generate_quiz_recap(session_code) - - def _generate_quiz_recap(self, session_code: str): - """Comprehensive quiz recap generation.""" - try: - # Ambil data dari Redis - answers = self.redis_repo.get_data(self._answers_key(session_code)) or [] - scores = self.redis_repo.get_data(self._scores_key(session_code)) or {} - questions = ( - self.redis_repo.get_data(self._questions_key(session_code)) or [] - ) - - # Persiapkan data rekap - recap_data = { - "session_code": session_code, - "total_questions": len(questions), - "answers": [], - "scores": [], - "questions": [], - } - - # Tambahkan detail pertanyaan - for q in questions: - question_recap = { - "index": q["index"], - "question": q["question"], - "type": q["type"], - "points": q.get("points", 10), - } - recap_data["questions"].append(question_recap) - - # Tambahkan detail jawaban - for entry in answers: - # Temukan pertanyaan terkait - related_question = next( - (q for q in questions if q["index"] == entry["question_index"]), - None, - ) - - answer_recap = { - "user_id": entry["user_id"], - "question_index": entry["question_index"], - "answer": entry["answer"], - "correct": entry["correct"], - "question": ( - related_question["question"] - if related_question - else "Pertanyaan tidak ditemukan" - ), - } - recap_data["answers"].append(answer_recap) - - # Tambahkan skor per pengguna - for uid, sc in scores.items(): - score_recap = { - "user_id": uid, - "correct": sc.get("correct", 0), - "incorrect": sc.get("incorrect", 0), - "total_score": sc.get("correct", 0) - * 10, # 10 poin per jawaban benar - } - recap_data["scores"].append(score_recap) - - # Urutkan skor dari tertinggi ke terendah - recap_data["scores"].sort(key=lambda x: x["total_score"], reverse=True) - - # Kirim rekap ke semua peserta - self.socketio.emit( - "quiz_recap", - { - "message": "Kuis telah selesai. Berikut adalah rekap lengkap.", - "recap": recap_data, - }, - room=session_code, - ) - - print(recap_data) - - # Cetak rekap di konsol server - print("\n🏁 Rekap Kuis Lengkap") - print("=" * 50) - print(f"Kode Sesi: {session_code}") - print(f"Total Pertanyaan: {len(questions)}") - print("\nPeringkat Peserta:") - for i, score in enumerate(recap_data["scores"], 1): - print( - f"{i}. User {score['user_id']}: Skor {score['total_score']} (Benar: {score['correct']}, Salah: {score['incorrect']})" - ) - - # Kirim tanda kuis telah berakhir - self.socketio.emit( - "quiz_done", - {"message": "Kuis telah berakhir.", "session_code": session_code}, - room=session_code, - ) - - except Exception as e: - print(f"Error generating quiz recap: {e}") - # Kirim pesan error jika gagal membuat rekap - self.socketio.emit( - "quiz_error", - { - "message": "Terjadi kesalahan saat membuat rekap kuis.", - "error": str(e), - }, - room=session_code, - ) diff --git a/app/repositories/session_memory_repository.py b/app/repositories/session_memory_repository.py index 8930842..8883682 100644 --- a/app/repositories/session_memory_repository.py +++ b/app/repositories/session_memory_repository.py @@ -169,6 +169,7 @@ class SessionMemoryRepository: Retrieve quiz questions for a session. """ data = self.get_data(f"session:{session_id}:quiz") + print(data) data["date"] = DatetimeUtil.from_string(data["date"]) return QuizEntity(**data) @@ -177,3 +178,71 @@ class SessionMemoryRepository: Delete quiz data for a session. """ self.delete_key(f"session:{session_id}:quiz") + + def save_user_answer( + self, + session_id: str, + user_id: str, + question_index: int, + answer: Any, + correct: bool, + ): + """ + Save user answer for a session. + """ + key = f"session:{session_id}:answers" + answers = self.get_data(key) or [] + + answers.append( + { + "user_id": user_id, + "question_index": question_index, + "answer": answer, + "correct": correct, + } + ) + + self.set_data(key, answers) + + def get_all_answers(self, session_id: str) -> List[Dict[str, Any]]: + """ + Retrieve all answers for a session. + """ + return self.get_data(f"session:{session_id}:answers") or [] + + def delete_all_answers(self, session_id: str): + """ + Delete all answers for a session. + """ + self.delete_key(f"session:{session_id}:answers") + + def update_user_score( + self, session_id: str, user_id: str, correct: bool + ) -> Dict[str, Dict[str, int]]: + """ + Update the user's score based on the latest answer. + """ + key = f"session:{session_id}:scores" + scores = self.get_data(key) or {} + + user_score = scores.get(str(user_id), {"correct": 0, "incorrect": 0}) + if correct: + user_score["correct"] += 1 + else: + user_score["incorrect"] += 1 + + scores[str(user_id)] = user_score + self.set_data(key, scores) + return scores + + def get_scores(self, session_id: str) -> Dict[str, Dict[str, int]]: + """ + Retrieve all user scores for a session. + """ + return self.get_data(f"session:{session_id}:scores") or {} + + def delete_scores(self, session_id: str): + """ + Delete all scores for a session. + """ + self.delete_key(f"session:{session_id}:scores") diff --git a/app/services/session_service.py b/app/services/session_service.py index 82427b4..6f288ae 100644 --- a/app/services/session_service.py +++ b/app/services/session_service.py @@ -9,6 +9,8 @@ from app.repositories import ( ) from app.models.entities import SessionEntity from app.helpers import DatetimeUtil +from flask_socketio import SocketIO +import time class SessionService: @@ -149,3 +151,102 @@ class SessionService: def get_session(self, session_id: str) -> Optional[SessionEntity]: session = self.repository.find_by_session_id(session_id) return SessionEntity(**session) if session else None + + def simulate_quiz_flow(self, session_id: str, socketio: SocketIO): + quiz = self.repository_redis.get_quiz_for_session(session_id) + questions = quiz.question_listings + time.sleep(2) + + for q in questions: + print(f"\nMengirim pertanyaan {q.index} ke room {session_id}") + + question_to_send = q.model_dump(exclude={"target_answer"}) + + socketio.emit("quiz_question", question_to_send, room=session_id) + + time.sleep(q.duration) + + def generate_quiz_recap(self, session_id: str, socketio: SocketIO): + try: + + answers = self.repository_redis.get_all_answers(session_id) + scores = self.repository_redis.get_scores(session_id) + quiz = self.repository_redis.get_quiz_for_session(session_id) + questions = quiz.question_listings + + recap_data = { + "session_id": session_id, + "total_questions": len(questions), + "answers": [], + "scores": [], + "questions": [], + } + + recap_data["questions"] = [ + { + "index": q["index"], + "question": q["question"], + "type": q["type"], + } + for q in questions + ] + + for entry in answers: + related_question = next( + (q for q in questions if q["index"] == entry["question_index"]), + None, + ) + recap_data["answers"].append( + { + "user_id": entry["user_id"], + "question_index": entry["question_index"], + "answer": entry["answer"], + "correct": entry["correct"], + "question": ( + related_question["question"] + if related_question + else "Pertanyaan tidak ditemukan" + ), + } + ) + + for uid, sc in scores.items(): + recap_data["scores"].append( + { + "user_id": uid, + "correct": sc.get("correct", 0), + "incorrect": sc.get("incorrect", 0), + "total_score": sc.get("correct", 0) * 10, + } + ) + + # Urutkan skor tertinggi ke terendah + recap_data["scores"].sort(key=lambda x: x["total_score"], reverse=True) + + # Emit recap data ke semua peserta + socketio.emit( + "quiz_recap", + { + "message": "Kuis telah selesai. Berikut adalah rekap lengkap.", + "recap": recap_data, + }, + room=session_id, + ) + + # Emit informasi bahwa kuis telah berakhir + socketio.emit( + "quiz_done", + {"message": "Kuis telah berakhir.", "session_id": session_id}, + room=session_id, + ) + + except Exception as e: + # Tangani error dan informasikan ke peserta + socketio.emit( + "quiz_error", + { + "message": "Terjadi kesalahan saat membuat rekap kuis.", + "error": str(e), + }, + room=session_id, + )