feat: summary all session
This commit is contained in:
parent
a6ef847403
commit
3496310710
|
@ -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())
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
"question_index": question_index,
|
and ans.get("answer") == ""
|
||||||
"answer": "",
|
):
|
||||||
"is_true": False,
|
has_unanswered = True
|
||||||
"time_spent": default_time_spent,
|
ans.update(
|
||||||
}
|
{
|
||||||
)
|
"answer": "",
|
||||||
self.set_data(key, answers)
|
"is_true": False,
|
||||||
|
"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)
|
||||||
|
|
||||||
|
return users_with_unanswered
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue