fix: logic quiz multiplayer

This commit is contained in:
akhdanre 2025-05-17 21:25:24 +07:00
parent bd8353b6d6
commit a6ef847403
12 changed files with 361 additions and 365 deletions

View File

@ -75,7 +75,7 @@ class QuizController:
def get_answer(self, quiz_id, user_id, session_id): def get_answer(self, quiz_id, user_id, session_id):
try: try:
# self.answer_service. # self.answer_service.
print("yps") pass
except Exception as e: except Exception as e:
return make_error_response(e) return make_error_response(e)

View File

@ -83,62 +83,6 @@ class SocketController:
skip_sid=request.sid, 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")
# 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
# quiz_service = QuizService(self.redis_repo)
# question = quiz_service.get_question(session_code, question_index)
# if question is None:
# emit("error", {"message": "Question not found"})
# return
# is_correct = quiz_service.is_correct(user_answer, question)
# print(
# f"User {user_id} answered Q{question_index} with '{user_answer}' -> {'✔' if is_correct else '✖'}"
# )
# 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)
# emit(
# "answer_submitted",
# {
# "user_id": user_id,
# "question_index": question_index,
# "answer": user_answer,
# "correct": is_correct,
# },
# room=request.sid,
# )
# admin_sid = self.admin_sids.get(session_code)
# if admin_sid:
# emit("score_update", scores, room=admin_sid)
@self.socketio.on("leave_room") @self.socketio.on("leave_room")
def handle_leave_room(data): def handle_leave_room(data):
session_id = data.get("session_id") session_id = data.get("session_id")
@ -223,7 +167,63 @@ class SocketController:
emit("quiz_started", {"message": "Quiz has started!"}, room=session_id) emit("quiz_started", {"message": "Quiz has started!"}, room=session_id)
threading.Thread( threading.Thread(
target=self.session_service.simulate_quiz_flow, target=self.session_service.run_quiz_flow,
args=(session_id, self.socketio), args=(session_id, self.socketio),
daemon=True, daemon=True,
).start() ).start()
@self.socketio.on("submit_answer")
def handle_submit_answer(data):
session_id = data.get("session_id")
user_id = data.get("user_id")
question_index = data.get("question_index")
user_answer = data.get("answer")
time_spent = data.get("time_spent")
if not all(
[
session_id,
user_id,
question_index is not None,
user_answer is not None,
time_spent is not None,
]
):
emit(
"error",
{
"message": "session_id, user_id, question_index, and answer are required"
},
)
return
try:
result = self.session_service.submit_answer(
session_id=session_id,
user_id=user_id,
question_index=question_index,
answer=user_answer,
time_spent=time_spent,
)
except ValueError as exc:
emit("error", {"message": str(exc)})
return
emit(
"answer_submitted",
{
"question_index": result["question_index"],
"answer": result["answer"],
"correct": result["correct"],
"score": result["scores"],
},
to=request.sid,
)
emit(
"score_update",
{
"scores": self.session_service.get_ranked_scores(session_id),
},
room=session_id,
)

View File

@ -7,6 +7,9 @@ from app.repositories import (
SessionRepository, SessionRepository,
NERSRLRepository, NERSRLRepository,
SessionMemoryRepository, SessionMemoryRepository,
QuizMemoryRepository,
AnswerMemoryRepository,
ScoreMemoryRepository,
) )
from app.services import ( from app.services import (
@ -46,6 +49,9 @@ class Container(containers.DeclarativeContainer):
session_repository = providers.Factory(SessionRepository, mongo.provided.db) session_repository = providers.Factory(SessionRepository, mongo.provided.db)
ner_srl_repository = providers.Factory(NERSRLRepository) ner_srl_repository = providers.Factory(NERSRLRepository)
session_memory_repository = providers.Factory(SessionMemoryRepository, redis) session_memory_repository = providers.Factory(SessionMemoryRepository, redis)
quiz_memory_repository = providers.Factory(QuizMemoryRepository, redis)
answer_memory_repository = providers.Factory(AnswerMemoryRepository, redis)
score_memory_repository = providers.Factory(ScoreMemoryRepository, redis)
# services # services
auth_service = providers.Factory( auth_service = providers.Factory(
@ -86,6 +92,9 @@ class Container(containers.DeclarativeContainer):
SessionService, SessionService,
session_repository, session_repository,
session_memory_repository, session_memory_repository,
quiz_memory_repository,
answer_memory_repository,
score_memory_repository,
user_repository, user_repository,
quiz_repository, quiz_repository,
answer_repository, answer_repository,

View File

@ -5,6 +5,9 @@ from .subject_repository import SubjectRepository
from .session_repostory import SessionRepository from .session_repostory import SessionRepository
from .ner_srl_repository import NERSRLRepository from .ner_srl_repository import NERSRLRepository
from .session_memory_repository import SessionMemoryRepository from .session_memory_repository import SessionMemoryRepository
from .quiz_memory_repository import QuizMemoryRepository
from .answer_memory_repository import AnswerMemoryRepository
from .score_memory_repository import ScoreMemoryRepository
__all__ = [ __all__ = [
"UserRepository", "UserRepository",
@ -14,4 +17,7 @@ __all__ = [
"SessionRepository", "SessionRepository",
"NERSRLRepository", "NERSRLRepository",
"SessionMemoryRepository", "SessionMemoryRepository",
"QuizMemoryRepository",
"AnswerMemoryRepository",
"ScoreMemoryRepository",
] ]

View File

@ -0,0 +1,106 @@
import json
from typing import Any, Dict, List
from redis import Redis
class AnswerMemoryRepository:
KEY_TEMPLATE = "answer:{session_id}:{user_id}"
KEY_PATTERN_TEMPLATE = "answer:{session_id}:*"
def __init__(self, redis: Redis):
self.redis = redis
def _build_key(self, session_id: str, user_id: str) -> str:
return self.KEY_TEMPLATE.format(session_id=session_id, user_id=user_id)
def _build_pattern_key(self, session_id: str) -> str:
return self.KEY_PATTERN_TEMPLATE.format(session_id=session_id)
def save_user_answer(
self,
session_id: str,
user_id: str,
question_index: int,
answer: Any,
correct: bool,
time_spent: float,
):
key = self._build_key(session_id, user_id)
answers = self.get_data(key) or []
for ans in answers:
if ans["question_index"] == question_index:
ans.update(
{
"answer": answer,
"is_true": correct,
"time_spent": time_spent,
}
)
break
else:
answers.append(
{
"question_index": question_index,
"answer": answer,
"is_true": correct,
"time_spent": time_spent,
}
)
self.set_data(key, answers)
def get_user_answers(self, session_id: str, user_id: str) -> List[Dict[str, Any]]:
key = self._build_key(session_id, user_id)
return self.get_data(key) or []
def get_all_user_answers(self, session_id: str) -> Dict[str, List[Dict[str, Any]]]:
pattern = self._build_pattern_key(session_id)
keys = self.redis.keys(pattern)
all_answers = {}
for key in keys:
user_id = key.decode().split(":")[-1]
all_answers[user_id] = self.get_data(key) or []
return all_answers
def delete_all_answers(self, session_id: str):
pattern = self._build_pattern_key(session_id)
keys = self.redis.keys(pattern)
for key in keys:
self.redis.delete(key)
def set_data(self, key: str, value: Any):
self.redis.set(key, json.dumps(value))
def get_data(self, key: str) -> Any:
data = self.redis.get(key)
return json.loads(data) if data else None
def auto_fill_incorrect_answers(
self,
session_id: str,
question_index: int,
default_time_spent: float = 0.0,
):
pattern = self._build_pattern_key(session_id)
keys = self.redis.keys(pattern)
for key in keys:
answers = self.get_data(key) or []
has_answered = any(
ans["question_index"] == question_index for ans in answers
)
if not has_answered:
answers.append(
{
"question_index": question_index,
"answer": "",
"is_true": False,
"time_spent": default_time_spent,
}
)
self.set_data(key, answers)

View File

@ -0,0 +1,32 @@
import json
from typing import Dict, Any, Optional
from redis import Redis
from app.helpers import DatetimeUtil
from app.models.entities import QuizEntity
class QuizMemoryRepository:
KEY_TEMPLATE = "quiz:{session_id}"
def __init__(self, redis: Redis):
self.redis = redis
def _build_key(self, session_id: str) -> str:
return self.KEY_TEMPLATE.format(session_id=session_id)
def set_quiz_for_session(self, session_id: str, quiz_data: QuizEntity):
data = quiz_data.model_dump()
data["id"] = str(data["id"])
data["date"] = DatetimeUtil.to_string(data["date"])
self.redis.set(self._build_key(session_id), json.dumps(data))
def get_quiz_for_session(self, session_id: str) -> Optional[QuizEntity]:
data = self.redis.get(self._build_key(session_id))
if data:
data = json.loads(data)
data["date"] = DatetimeUtil.from_string(data["date"])
return QuizEntity(**data)
return None
def delete_quiz_for_session(self, session_id: str):
self.redis.delete(self._build_key(session_id))

View File

@ -86,7 +86,7 @@ class QuizRepository:
object_ids = [ObjectId(qid) for qid in quiz_ids] object_ids = [ObjectId(qid) for qid in quiz_ids]
cursor = self.collection.find({"_id": {"$in": object_ids}}) cursor = self.collection.find({"_id": {"$in": object_ids}})
datas = list(cursor) datas = list(cursor)
print(datas)
if not datas: if not datas:
return None return None

View File

@ -0,0 +1,28 @@
from typing import Dict
from redis import Redis
class ScoreMemoryRepository:
KEY_TEMPLATE = "score:{session_id}"
def __init__(self, redis: Redis):
self.redis = redis
def _build_key(self, session_id: str) -> str:
return self.KEY_TEMPLATE.format(session_id=session_id)
def update_user_score(self, session_id: str, user_id: str, correct: bool):
hkey = self._build_key(session_id)
field = f"{user_id}:{'correct' if correct else 'incorrect'}"
self.redis.hincrby(hkey, field, 1)
def get_scores(self, session_id: str) -> Dict[str, Dict[str, int]]:
raw = self.redis.hgetall(self._build_key(session_id))
scores = {}
for k, v in raw.items():
uid, category = k.decode().split(":")
scores.setdefault(uid, {"correct": 0, "incorrect": 0})[category] = int(v)
return scores
def delete_scores(self, session_id: str):
self.redis.delete(self._build_key(session_id))

View File

@ -1,114 +1,60 @@
import json import json
from typing import Dict, Any, List, Optional from typing import Dict, Any, Optional
from redis import Redis from redis import Redis
from app.helpers import DatetimeUtil from app.helpers import DatetimeUtil
from app.models.entities import SessionEntity, QuizEntity from app.models.entities import SessionEntity
class SessionMemoryRepository: class SessionMemoryRepository:
KEY_TEMPLATE = "session:{session_id}"
KEY_PATTERN = "session:*"
def __init__(self, redis: Redis): def __init__(self, redis: Redis):
self.redis = redis self.redis = redis
def _build_key(self, session_id: str) -> str:
return self.KEY_TEMPLATE.format(session_id=session_id)
def set_data(self, key: str, value: Any): def set_data(self, key: str, value: Any):
"""
Set data in Redis
:param key: Redis key
:param value: Value to store
"""
self.redis.set(key, json.dumps(value)) self.redis.set(key, json.dumps(value))
def get_data(self, key: str) -> Optional[Any]: def get_data(self, key: str) -> Optional[Any]:
"""
Get data from Redis
:param key: Redis key
:return: Decoded JSON data or None
"""
data = self.redis.get(key) data = self.redis.get(key)
return json.loads(data) if data else None return json.loads(data) if data else None
def delete_key(self, key: str): def delete_key(self, key: str):
"""
Delete a key from Redis
:param key: Redis key to delete
"""
self.redis.delete(key) self.redis.delete(key)
def create_session(self, session_id: str, initial_data: SessionEntity) -> str: def create_session(self, session_id: str, initial_data: SessionEntity) -> str:
"""
Create a new session
:param session_id: ID for the session
:param initial_data: Initial data for the session
:return: Session ID
"""
data = initial_data.model_dump() data = initial_data.model_dump()
data["id"] = data["id"] data["id"] = data["id"]
data["created_at"] = str(data["created_at"]) data["created_at"] = str(data["created_at"])
self.set_data(self._build_key(session_id), data)
self.set_data(f"session:{session_id}", data)
return session_id return session_id
def get_session(self, session_id: str) -> Optional[Dict[str, Any]]: def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
""" return self.get_data(self._build_key(session_id))
Retrieve a session by its ID
:param session_id: ID of the session
:return: Session data or None if not found
"""
return self.get_data(f"session:{session_id}")
def close_session( def close_session(self, session_id: str) -> Optional[Dict[str, Any]]:
self, session_id: str, additional_data: Optional[Dict[str, Any]] = None session = self.get_data(self._build_key(session_id))
) -> Optional[Dict[str, Any]]:
"""
Close/end a session and return its data before deleting
:param session_id: ID of the session to close
:param additional_data: Optional extra data to add when closing
:return: Session data if found and closed, None otherwise
"""
# Ambil data session
session = self.get_data(f"session:{session_id}")
if not session: if not session:
return None return None
session["status"] = "closed" session["status"] = "closed"
session["closed_at"] = DatetimeUtil.now_iso session["closed_at"] = DatetimeUtil.now_iso()
self.delete_key(self._build_key(session_id))
if additional_data: return session
session.update(additional_data)
try:
self.delete_key(f"session:{session_id}")
return session
except Exception as e:
print(f"Error closing session {session_id}: {e}")
return session
def find_session_by_code(self, session_code: str) -> Optional[Dict[str, Any]]: def find_session_by_code(self, session_code: str) -> Optional[Dict[str, Any]]:
""" session_keys = self.redis.keys(self.KEY_PATTERN)
Find a session by its session code.
:param session_code: Session code to search for.
:return: Session data if found, None otherwise.
"""
# Dapatkan semua session keys
session_keys = self.redis.keys("session:*")
for key in session_keys: for key in session_keys:
session_data = self.get_data(key) session_data = self.get_data(key)
if session_data and session_data.get("session_code") == session_code: if session_data and session_data.get("session_code") == session_code:
return session_data return session_data
return None return None
def add_user_to_session( def add_user_to_session(
self, session_id: str, user_data: Dict[str, Any] = None self, session_id: str, user_data: Dict[str, Any] = None
) -> bool: ) -> bool:
"""
Add a user to an existing session.
:param session_id: ID of the session.
:param user_id: ID of the user to add.
:param user_data: Optional additional data about the user in the session.
:return: True if user was added successfully, False if session doesn't exist.
"""
session = self.get_session(session_id) session = self.get_session(session_id)
if not session: if not session:
return False return False
@ -118,131 +64,29 @@ class SessionMemoryRepository:
"joined_at": DatetimeUtil.now_iso(), "joined_at": DatetimeUtil.now_iso(),
} }
print("Final user_entry:", user_entry)
existing_users = session.get("participants", []) existing_users = session.get("participants", [])
existing_users.append(user_entry) existing_users.append(user_entry)
session["participants"] = existing_users session["participants"] = existing_users
self.set_data(f"session:{session_id}", session) self.set_data(self._build_key(session_id), session)
return True return True
def get_user_in_session(self, session_id: str): def get_user_in_session(self, session_id: str):
session = self.get_session(session_id) session = self.get_session(session_id)
if not session: if not session:
return [] return []
existing_users = session.get("participants", []) return session.get("participants", [])
return existing_users
def remove_user_from_session(self, session_id: str, user_id: str) -> bool: def remove_user_from_session(self, session_id: str, user_id: str) -> bool:
"""
Remove a user from a session
:param session_id: ID of the session
:param user_id: ID of the user to remove
:return: True if user was removed, False if session doesn't exist or user not in session
"""
session = self.get_session(session_id) session = self.get_session(session_id)
if not session: if not session:
return False return False
session["participants"] = [ session["participants"] = [
user for user in session.get("participants", []) if user["id"] != user_id user
for user in session.get("participants", [])
if user.get("id") != user_id
] ]
self.set_data(f"session:{session_id}", session) self.set_data(self._build_key(session_id), session)
return True return True
def set_quiz_for_session(self, session_id: str, quiz_data: QuizEntity):
"""
Store quiz questions for a session.
"""
data = quiz_data.model_dump()
data["id"] = str(data["id"]) ## objectId
data["date"] = DatetimeUtil.to_string(data["date"])
self.set_data(f"session:{session_id}:quiz", data)
def get_quiz_for_session(self, session_id: str) -> QuizEntity:
"""
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)
def delete_quiz_for_session(self, session_id: str):
"""
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")

View File

@ -23,7 +23,7 @@ class HistoryService:
quiz_ids = [asn.quiz_id for asn in answer_data] quiz_ids = [asn.quiz_id for asn in answer_data]
quiz_data = self.quiz_repository.get_by_ids(quiz_ids) quiz_data = self.quiz_repository.get_by_ids(quiz_ids)
quiz_map = {str(quiz.id): quiz for quiz in quiz_data} quiz_map = {str(quiz.id): quiz for quiz in quiz_data}
print(quiz_map)
result = [] result = []
for answer in answer_data: for answer in answer_data:
quiz = quiz_map.get(answer.quiz_id) quiz = quiz_map.get(answer.quiz_id)

View File

@ -63,7 +63,7 @@ class QuizService:
total_user_quiz = self.quiz_repository.count_by_user_id(user_id) total_user_quiz = self.quiz_repository.count_by_user_id(user_id)
print(total_user_quiz)
user = self.user_repostory.get_user_by_id(user_id) user = self.user_repostory.get_user_by_id(user_id)

View File

@ -1,4 +1,4 @@
from typing import Optional from typing import Any, Dict, Optional
from uuid import uuid4 from uuid import uuid4
from app.repositories import ( from app.repositories import (
SessionRepository, SessionRepository,
@ -6,6 +6,9 @@ from app.repositories import (
SessionMemoryRepository, SessionMemoryRepository,
QuizRepository, QuizRepository,
UserAnswerRepository, UserAnswerRepository,
QuizMemoryRepository,
AnswerMemoryRepository,
ScoreMemoryRepository,
) )
from app.models.entities import SessionEntity from app.models.entities import SessionEntity
from app.helpers import DatetimeUtil from app.helpers import DatetimeUtil
@ -16,14 +19,20 @@ import time
class SessionService: class SessionService:
def __init__( def __init__(
self, self,
repository_mongo: SessionRepository, session_mongo_repository: SessionRepository,
repository_redis: SessionMemoryRepository, session_redis_repository: SessionMemoryRepository,
quiz_redis_repository: QuizMemoryRepository,
answer_redis_repository: AnswerMemoryRepository,
score_redis_repostory: ScoreMemoryRepository,
user_repository: UserRepository, user_repository: UserRepository,
quiz_repository: QuizRepository, quiz_repository: QuizRepository,
answer_repository: UserAnswerRepository, answer_repository: UserAnswerRepository,
): ):
self.repository_mongo = repository_mongo self.session_mongo_repository = session_mongo_repository
self.repository_redis = repository_redis self.session_redis_repository = session_redis_repository
self.quiz_redis_repository = quiz_redis_repository
self.answer_redis_repository = answer_redis_repository
self.score_redis_repository = score_redis_repostory
self.user_repository = user_repository self.user_repository = user_repository
self.quiz_repository = quiz_repository self.quiz_repository = quiz_repository
self.answer_repository = answer_repository self.answer_repository = answer_repository
@ -40,11 +49,11 @@ class SessionService:
current_question_index=0, current_question_index=0,
is_active=True, is_active=True,
) )
session_id = self.repository_mongo.insert(session) session_id = self.session_mongo_repository.insert(session)
session.id = session_id session.id = session_id
self.repository_redis.create_session(session_id, session) self.session_redis_repository.create_session(session_id, session)
data = self.quiz_repository.get_by_id(quiz_id=quiz_id) data = self.quiz_repository.get_by_id(quiz_id=quiz_id)
self.repository_redis.set_quiz_for_session(session_id, data) self.quiz_redis_repository.set_quiz_for_session(session_id, data)
return { return {
"session_id": session_id, "session_id": session_id,
"session_code": generateed_code, "session_code": generateed_code,
@ -52,7 +61,7 @@ class SessionService:
def join_session(self, session_code: str, user_id: str) -> dict: def join_session(self, session_code: str, user_id: str) -> dict:
user = self.user_repository.get_user_by_id(user_id) user = self.user_repository.get_user_by_id(user_id)
session = self.repository_redis.find_session_by_code(session_code) session = self.session_redis_repository.find_session_by_code(session_code)
if session is None: if session is None:
return None return None
@ -62,7 +71,7 @@ class SessionService:
u["id"] == user_id for u in session.get("participants", []) u["id"] == user_id for u in session.get("participants", [])
) )
session_quiz = self.repository_redis.get_quiz_for_session(session["id"]) session_quiz = self.quiz_redis_repository.get_quiz_for_session(session["id"])
quiz_info = { quiz_info = {
"title": session_quiz.title, "title": session_quiz.title,
@ -91,7 +100,7 @@ class SessionService:
"quiz_info": quiz_info, "quiz_info": quiz_info,
"new_user": not is_existing_user, "new_user": not is_existing_user,
} }
self.repository_redis.add_user_to_session( self.session_redis_repository.add_user_to_session(
session_id=session["id"], session_id=session["id"],
user_data={ user_data={
"id": str(user.id), "id": str(user.id),
@ -99,7 +108,7 @@ class SessionService:
"user_pic": user.pic_url, "user_pic": user.pic_url,
}, },
) )
session = self.repository_redis.get_session(session["id"]) session = self.session_redis_repository.get_session(session["id"])
response = { response = {
"session_id": session_id, "session_id": session_id,
@ -114,46 +123,20 @@ class SessionService:
return response return response
def leave_session(self, session_id: str, user_id: str) -> dict: def leave_session(self, session_id: str, user_id: str) -> dict:
is_success = self.repository_redis.remove_user_from_session(session_id, user_id) is_success = self.session_redis_repository.remove_user_from_session(
session_id, user_id
)
if is_success: if is_success:
participant_left = self.repository_redis.get_user_in_session(session_id) participant_left = self.session_redis_repository.get_user_in_session(
session_id
)
return {"is_success": True, "participants": participant_left} return {"is_success": True, "participants": participant_left}
return {"is_success": False} return {"is_success": False}
def start_session(self, session_id: str) -> bool: def run_quiz_flow(self, session_id: str, socketio: SocketIO):
now = DatetimeUtil.now_iso() quiz = self.quiz_redis_repository.get_quiz_for_session(session_id)
return self.repository.update(session_id, {"started_at": now})
def end_session(self, session_id: str, user_id: str):
session = self.repository.find_by_session_id(session_id)
if session and session.host_id == user_id:
session.is_active = False
self.repository.update(session_id, {"is_active": False})
def advance_question(self, session_id: str) -> Optional[int]:
session = self.repository.find_by_session_id(session_id)
if not session:
return None
current_index = session.get("current_question_index", 0)
total = session.get("total_questions", 0)
if current_index + 1 >= total:
self.end_session(session_id)
return None
new_index = current_index + 1
self.repository.update(session_id, {"current_question_index": new_index})
return new_index
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 questions = quiz.question_listings
time.sleep(2) time.sleep(2)
@ -165,88 +148,76 @@ class SessionService:
socketio.emit("quiz_question", question_to_send, room=session_id) socketio.emit("quiz_question", question_to_send, room=session_id)
time.sleep(q.duration) time.sleep(q.duration)
self.answer_redis_repository.auto_fill_incorrect_answers(
session_id=session_id,
question_index=q.index,
default_time_spent=q.duration,
)
def generate_quiz_recap(self, session_id: str, socketio: SocketIO): socketio.emit("quiz_done", room=session_id)
try:
answers = self.repository_redis.get_all_answers(session_id) def submit_answer(
scores = self.repository_redis.get_scores(session_id) self,
quiz = self.repository_redis.get_quiz_for_session(session_id) session_id: str,
questions = quiz.question_listings user_id: str,
question_index: int,
answer: Any,
time_spent: int,
) -> Dict[str, Any]:
recap_data = { quiz = self.quiz_redis_repository.get_quiz_for_session(session_id)
"session_id": session_id,
"total_questions": len(questions), question = next(
"answers": [], (q for q in quiz.question_listings if q.index == question_index),
"scores": [], None,
"questions": [], )
if question is None:
raise ValueError(
f"Question {question_index} not found in session {session_id}"
)
is_correct = self._is_correct(question, answer)
self.answer_redis_repository.save_user_answer(
session_id=session_id,
user_id=user_id,
question_index=question_index,
answer=answer,
correct=is_correct,
time_spent=time_spent,
)
scores = self.score_redis_repository.update_user_score(
session_id=session_id,
user_id=user_id,
correct=is_correct,
)
return {
"user_id": user_id,
"question_index": question_index,
"answer": answer,
"correct": is_correct,
"scores": scores,
}
def get_ranked_scores(self, session_id: str):
raw = self.score_redis_repository.get_scores(session_id)
ranked = [
{
"user_id": uid,
"correct": v.get("correct", 0),
"incorrect": v.get("incorrect", 0),
"total_score": v.get("correct", 0) * 10,
} }
for uid, v in raw.items()
]
ranked.sort(key=lambda x: x["total_score"], reverse=True)
return ranked
recap_data["questions"] = [ def _is_correct(self, q, ans) -> bool:
{ if q.type in {"multiple_choice", "true_false"}:
"index": q["index"], return str(ans).strip().lower() == str(q.target_answer).strip().lower()
"question": q["question"], if q.type == "essay":
"type": q["type"], return str(q.target_answer).lower() in str(ans).lower()
} # fallback
for q in questions return False
]
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,
)