feat: summary all session

This commit is contained in:
akhdanre 2025-05-17 23:45:48 +07:00
parent a6ef847403
commit 3496310710
6 changed files with 180 additions and 34 deletions

View File

@ -10,3 +10,9 @@ session_bp = Blueprint("session", __name__)
@inject @inject
def sessionGet(controller: SessionController = Provide[Container.session_controller]): def sessionGet(controller: SessionController = Provide[Container.session_controller]):
return controller.createRoom(request.get_json()) return controller.createRoom(request.get_json())
@session_bp.route("/summary", methods=["POST"])
@inject
def summary(controller: SessionController = Provide[Container.session_controller]):
return controller.summaryall(request.get_json())

View File

@ -30,3 +30,13 @@ class SessionController(MethodView):
data=session, data=session,
status_code=201, status_code=201,
) )
def summaryall(self, body):
self.session_service.summaryAllSessionData(
session_id=body.get("session_id"), start_time=""
)
return make_response(
message="succes create room",
data="",
status_code=201,
)

View File

@ -33,3 +33,12 @@ class DatetimeUtil:
"""Convert string ke datetime dengan timezone""" """Convert string ke datetime dengan timezone"""
dt = datetime.strptime(date_str, fmt) dt = datetime.strptime(date_str, fmt)
return dt.replace(tzinfo=ZoneInfo(tz)) return dt.replace(tzinfo=ZoneInfo(tz))
@staticmethod
def from_iso(date_str: str, tz: str = "UTC") -> datetime:
"""Convert ISO 8601 string to datetime with timezone awareness"""
dt = datetime.fromisoformat(date_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo(tz))
return dt

View File

@ -16,6 +16,28 @@ class AnswerMemoryRepository:
def _build_pattern_key(self, session_id: str) -> str: def _build_pattern_key(self, session_id: str) -> str:
return self.KEY_PATTERN_TEMPLATE.format(session_id=session_id) return self.KEY_PATTERN_TEMPLATE.format(session_id=session_id)
def initialize_empty_answers(
self,
session_id: str,
user_ids: List[str],
total_questions: int,
):
"""
Initialize empty answers for all users at the start of the quiz.
"""
for user_id in user_ids:
key = self._build_key(session_id, user_id)
answers = [
{
"question_index": idx + 1,
"answer": "",
"is_true": False,
"time_spent": 0.0,
}
for idx in range(total_questions)
]
self.set_data(key, answers)
def save_user_answer( def save_user_answer(
self, self,
session_id: str, session_id: str,
@ -25,11 +47,15 @@ class AnswerMemoryRepository:
correct: bool, correct: bool,
time_spent: float, time_spent: float,
): ):
"""
Update user's answer for a specific question.
Assumes answers have been initialized.
"""
key = self._build_key(session_id, user_id) key = self._build_key(session_id, user_id)
answers = self.get_data(key) or [] answers = self.get_data(key) or []
for ans in answers: for ans in answers:
if ans["question_index"] == question_index: if ans.get("question_index") == question_index:
ans.update( ans.update(
{ {
"answer": answer, "answer": answer,
@ -38,15 +64,6 @@ class AnswerMemoryRepository:
} }
) )
break break
else:
answers.append(
{
"question_index": question_index,
"answer": answer,
"is_true": correct,
"time_spent": time_spent,
}
)
self.set_data(key, answers) self.set_data(key, answers)
@ -68,8 +85,8 @@ class AnswerMemoryRepository:
def delete_all_answers(self, session_id: str): def delete_all_answers(self, session_id: str):
pattern = self._build_pattern_key(session_id) pattern = self._build_pattern_key(session_id)
keys = self.redis.keys(pattern) keys = self.redis.keys(pattern)
for key in keys: if keys:
self.redis.delete(key) self.redis.delete(*keys)
def set_data(self, key: str, value: Any): def set_data(self, key: str, value: Any):
self.redis.set(key, json.dumps(value)) self.redis.set(key, json.dumps(value))
@ -83,24 +100,39 @@ class AnswerMemoryRepository:
session_id: str, session_id: str,
question_index: int, question_index: int,
default_time_spent: float = 0.0, default_time_spent: float = 0.0,
): ) -> List[str]:
"""
Auto-fill unanswered specific question (by index) as incorrect for all users.
:return: List of user IDs who had not answered the specific question.
"""
pattern = self._build_pattern_key(session_id) pattern = self._build_pattern_key(session_id)
keys = self.redis.keys(pattern) keys = self.redis.keys(pattern)
users_with_unanswered = []
for key in keys: for key in keys:
answers = self.get_data(key) or [] answers = self.get_data(key) or []
has_answered = any( has_unanswered = False
ans["question_index"] == question_index for ans in answers
)
if not has_answered: for ans in answers:
answers.append( if (
ans.get("question_index") == question_index
and ans.get("answer") == ""
):
has_unanswered = True
ans.update(
{ {
"question_index": question_index,
"answer": "", "answer": "",
"is_true": False, "is_true": False,
"time_spent": default_time_spent, "time_spent": default_time_spent,
} }
) )
break # No need to check other answers for this user
if has_unanswered:
user_id = key.decode().split(":")[-1]
users_with_unanswered.append(user_id)
self.set_data(key, answers) self.set_data(key, answers)
return users_with_unanswered

View File

@ -2,6 +2,7 @@ from pymongo.collection import Collection
from pymongo.database import Database from pymongo.database import Database
from typing import Optional from typing import Optional
from app.models.entities import SessionEntity from app.models.entities import SessionEntity
from bson import ObjectId
class SessionRepository: class SessionRepository:
@ -25,11 +26,12 @@ class SessionRepository:
doc = self.collection.find_one({"session_code": session_code}) doc = self.collection.find_one({"session_code": session_code})
return SessionEntity(**doc) if doc else None return SessionEntity(**doc) if doc else None
from bson import ObjectId
def update(self, session_id: str, update_fields: SessionEntity) -> bool: def update(self, session_id: str, update_fields: SessionEntity) -> bool:
"""Update specific fields using $set"""
result = self.collection.update_one( result = self.collection.update_one(
{"_id": session_id}, {"_id": ObjectId(session_id)},
{"$set": update_fields}, {"$set": update_fields.model_dump(by_alias=True, exclude_none=True)},
) )
return result.modified_count > 0 return result.modified_count > 0

View File

@ -1,4 +1,4 @@
from typing import Any, Dict, Optional from typing import Any, Dict
from uuid import uuid4 from uuid import uuid4
from app.repositories import ( from app.repositories import (
SessionRepository, SessionRepository,
@ -10,10 +10,11 @@ from app.repositories import (
AnswerMemoryRepository, AnswerMemoryRepository,
ScoreMemoryRepository, ScoreMemoryRepository,
) )
from app.models.entities import SessionEntity from app.models.entities import SessionEntity, UserAnswerEntity, AnswerItemEntity
from app.helpers import DatetimeUtil from app.helpers import DatetimeUtil
from flask_socketio import SocketIO from flask_socketio import SocketIO
import time import time
from bson import ObjectId
class SessionService: class SessionService:
@ -136,8 +137,15 @@ class SessionService:
return {"is_success": False} return {"is_success": False}
def run_quiz_flow(self, session_id: str, socketio: SocketIO): def run_quiz_flow(self, session_id: str, socketio: SocketIO):
users = self.session_redis_repository.get_user_in_session(session_id)
quiz = self.quiz_redis_repository.get_quiz_for_session(session_id) quiz = self.quiz_redis_repository.get_quiz_for_session(session_id)
self.answer_redis_repository.initialize_empty_answers(
session_id=session_id,
user_ids=[u["id"] for u in users if "id" in u],
total_questions=quiz.total_quiz,
)
questions = quiz.question_listings questions = quiz.question_listings
start_quiz = DatetimeUtil.now_iso()
time.sleep(2) time.sleep(2)
for q in questions: for q in questions:
@ -148,12 +156,28 @@ 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( usersNotAnswer = self.answer_redis_repository.auto_fill_incorrect_answers(
session_id=session_id, session_id=session_id,
question_index=q.index, question_index=q.index,
default_time_spent=q.duration, default_time_spent=q.duration,
) )
for userId in usersNotAnswer:
self.score_redis_repository.update_user_score(
session_id=session_id,
user_id=userId,
correct=False,
)
socketio.emit(
"score_update",
{
"scores": self.get_ranked_scores(session_id),
},
room=session_id,
)
socketio.emit("clean_up", room=session_id)
self.summaryAllSessionData(session_id=session_id, start_time=start_quiz)
socketio.emit("quiz_done", room=session_id) socketio.emit("quiz_done", room=session_id)
def submit_answer( def submit_answer(
@ -219,5 +243,68 @@ class SessionService:
return str(ans).strip().lower() == str(q.target_answer).strip().lower() return str(ans).strip().lower() == str(q.target_answer).strip().lower()
if q.type == "essay": if q.type == "essay":
return str(q.target_answer).lower() in str(ans).lower() return str(q.target_answer).lower() in str(ans).lower()
# fallback
return False return False
def summaryAllSessionData(self, session_id: str, start_time):
session = self.session_redis_repository.get_session(session_id=session_id)
now = DatetimeUtil.now_iso()
session["id"] = ObjectId(session["id"])
session["participants"] = [
{"id": user["id"], "joined_at": user["joined_at"]}
for user in session["participants"]
]
session["created_at"] = DatetimeUtil.from_iso(session["created_at"])
session["started_at"] = DatetimeUtil.from_iso(start_time)
session["ended_at"] = DatetimeUtil.from_iso(now)
newData = SessionEntity(**session)
newData.is_active = False
answers = self.answer_redis_repository.get_all_user_answers(
session_id=session_id
)
quiz = self.quiz_repository.get_by_id(newData.quiz_id)
self.quiz_repository.update_user_playing(
quiz_id=quiz.id, total_user=quiz.total_user_playing + len(answers)
)
self.session_mongo_repository.update(
session_id=session_id,
update_fields=newData,
)
for key, value_list in answers.items():
answer_items = []
total_correct = 0
for item in sorted(value_list, key=lambda x: x["question_index"]):
is_correct = item["is_true"]
if is_correct:
total_correct += 1
answer_item = AnswerItemEntity(
question_index=item["question_index"],
answer=item["answer"],
is_correct=is_correct,
time_spent=item["time_spent"],
)
answer_items.append(answer_item)
total_questions = len(value_list)
total_score = (
(total_correct / total_questions) * 100 if total_questions > 0 else 0.0
)
userAnswer = UserAnswerEntity(
user_id=key,
quiz_id=str(quiz.id),
session_id=session_id,
total_correct=total_correct,
total_score=round(total_score, 2),
answers=answer_items,
answered_at=newData.started_at,
)
self.answer_repository.create(userAnswer)