feat: adding session controller

This commit is contained in:
akhdanre 2025-05-06 20:20:42 +07:00
parent fa971e1d24
commit 339da89e72
14 changed files with 250 additions and 50 deletions

View File

@ -5,6 +5,7 @@ from .user import user_blueprint
from .quiz import quiz_bp from .quiz import quiz_bp
from .history import history_blueprint from .history import history_blueprint
from .subject import subject_blueprint from .subject import subject_blueprint
from .session import session_bp
__all__ = [ __all__ = [
"default_blueprint", "default_blueprint",
@ -13,6 +14,7 @@ __all__ = [
"quiz_bp", "quiz_bp",
"history_blueprint", "history_blueprint",
"subject_blueprint", "subject_blueprint",
"session_bp",
] ]

12
app/blueprints/session.py Normal file
View File

@ -0,0 +1,12 @@
from flask import Blueprint, request
from di_container import Container
from dependency_injector.wiring import inject, Provide
from controllers import SessionController
session_bp = Blueprint("session", __name__)
@session_bp.route("", methods=["POST"])
@inject
def sessionGet(controller: SessionController = Provide[Container.session_controller]):
return controller.createRoom(request.get_json())

View File

@ -4,6 +4,7 @@ from .quiz_controller import QuizController
from .history_controller import HistoryController from .history_controller import HistoryController
from .subject_controller import SubjectController from .subject_controller import SubjectController
from .socket_conroller import SocketController from .socket_conroller import SocketController
from .session_controller import SessionController
__all__ = [ __all__ = [
"AuthController", "AuthController",
@ -12,4 +13,5 @@ __all__ = [
"HistoryController", "HistoryController",
"SubjectController", "SubjectController",
"SocketController", "SocketController",
"SessionController",
] ]

View File

@ -0,0 +1,27 @@
from flask import request, jsonify
from flask.views import MethodView
from services.session_service import SessionService
class SessionController(MethodView):
def __init__(self, session_service: SessionService):
self.session_service = session_service
def createRoom(self, data):
required_fields = [
"quiz_id",
"host_id",
"limit_participan",
]
for field in required_fields:
if field not in data:
return jsonify({"error": f"Missing field: {field}"}), 400
session = self.session_service.create_session(
quiz_id=data["quiz_id"],
host_id=data["host_id"],
limit_participan=data["limit_participan"],
)
return jsonify(session.dict()), 201

View File

@ -1,11 +1,13 @@
# socket_controller.py
from flask_socketio import SocketIO, emit, join_room, leave_room from flask_socketio import SocketIO, emit, join_room, leave_room
from flask import request from flask import request
from services import SessionService
class SocketController: class SocketController:
def __init__(self, socketio: SocketIO): def __init__(self, socketio: SocketIO, session_service: SessionService):
self.socketio = socketio self.socketio = socketio
self.rooms = {} # room_name -> set of sids self.session_service = session_service
self._register_events() self._register_events()
def _register_events(self): def _register_events(self):
@ -16,67 +18,54 @@ class SocketController:
@self.socketio.on("disconnect") @self.socketio.on("disconnect")
def on_disconnect(): def on_disconnect():
sid = request.sid print(f"Client disconnected: {request.sid}")
# Remove user from all rooms they joined
for room, members in self.rooms.items():
if sid in members:
members.remove(sid)
emit(
"room_message",
{"message": f"A user has disconnected.", "room": room},
room=room,
)
print(f"Client disconnected: {sid}")
@self.socketio.on("join_room") @self.socketio.on("join_room")
def handle_join_room(data): def handle_join_room(data):
if not isinstance(data, dict): session_code = data.get("session_code")
emit("error", {"message": "Invalid data format"}) user_id = data.get("user_id")
if not session_code or not user_id:
emit("error", {"message": "session_id and user_id are required"})
return return
room = data.get("room") session = self.session_service.join_session(session_code, user_id)
username = data.get("username", "anonymous") if session is None:
sid = request.sid emit("error", {"message": "Failed to join session or session inactive"})
# Create room if it doesn't exist
if room not in self.rooms:
self.rooms[room] = set()
# Check if room has space
if len(self.rooms[room]) >= 2:
emit("room_full", {"message": "Room is full. Max 2 users allowed."})
return return
# Join room join_room(session_code)
self.rooms[room].add(sid) user_data = self.session_service.join_session()
join_room(room)
emit( emit(
"room_message", "room_message",
{"message": f"{username} has joined the room.", "room": room}, {
room=room, "message": "someone has joined the room.",
"room": session_code,
"argument": "adm_update",
},
room=session_code,
) )
@self.socketio.on("leave_room") @self.socketio.on("leave_room")
def handle_leave_room(data): def handle_leave_room(data):
room = data.get("room") session_id = data.get("session_id")
username = data.get("username", "anonymous") username = data.get("username", "anonymous")
sid = request.sid
if room in self.rooms and sid in self.rooms[room]: leave_room(session_id)
self.rooms[room].remove(sid)
leave_room(room)
print(f"{username} left room {room}")
emit( emit(
"room_message", "room_message",
{"message": f"{username} has left the room.", "room": room}, {"message": f"{username} has left the room.", "room": session_id},
room=room, room=session_id,
) )
@self.socketio.on("send_message") @self.socketio.on("send_message")
def on_send_message(data): def on_send_message(data):
room = data.get("room") session_id = data.get("session_id")
message = data.get("message") message = data.get("message")
username = data.get("username", "anonymous") username = data.get("username", "anonymous")
print(f"[{room}] {username}: {message}")
emit("receive_message", {"message": message, "from": username}, room=room) emit(
"receive_message",
{"message": message, "from": username},
room=session_id,
)

View File

@ -4,6 +4,7 @@ from repositories import (
QuizRepository, QuizRepository,
UserAnswerRepository, UserAnswerRepository,
SubjectRepository, SubjectRepository,
SessionRepository,
) )
from services import ( from services import (
@ -13,6 +14,7 @@ from services import (
AnswerService, AnswerService,
HistoryService, HistoryService,
SubjectService, SubjectService,
SessionService,
) )
from controllers import ( from controllers import (
@ -21,6 +23,8 @@ from controllers import (
QuizController, QuizController,
HistoryController, HistoryController,
SubjectController, SubjectController,
SocketController,
SessionController,
) )
@ -29,11 +33,14 @@ class Container(containers.DeclarativeContainer):
mongo = providers.Dependency() mongo = providers.Dependency()
socketio = providers.Dependency()
# repository # repository
user_repository = providers.Factory(UserRepository, mongo.provided.db) user_repository = providers.Factory(UserRepository, mongo.provided.db)
quiz_repository = providers.Factory(QuizRepository, mongo.provided.db) quiz_repository = providers.Factory(QuizRepository, mongo.provided.db)
answer_repository = providers.Factory(UserAnswerRepository, mongo.provided.db) answer_repository = providers.Factory(UserAnswerRepository, mongo.provided.db)
subject_repository = providers.Factory(SubjectRepository, mongo.provided.db) subject_repository = providers.Factory(SubjectRepository, mongo.provided.db)
session_repository = providers.Factory(SessionRepository, mongo.provided.db)
# services # services
auth_service = providers.Factory( auth_service = providers.Factory(
@ -70,9 +77,16 @@ class Container(containers.DeclarativeContainer):
subject_repository, subject_repository,
) )
session_service = providers.Factory(
SessionService,
session_repository,
)
# controllers # controllers
auth_controller = providers.Factory(AuthController, user_service, auth_service) auth_controller = providers.Factory(AuthController, user_service, auth_service)
user_controller = providers.Factory(UserController, user_service) user_controller = providers.Factory(UserController, user_service)
quiz_controller = providers.Factory(QuizController, quiz_service, answer_service) quiz_controller = providers.Factory(QuizController, quiz_service, answer_service)
history_controller = providers.Factory(HistoryController, history_service) history_controller = providers.Factory(HistoryController, history_service)
subject_controller = providers.Factory(SubjectController, subject_service) subject_controller = providers.Factory(SubjectController, subject_service)
socket_controller = providers.Factory(SocketController, socketio, session_service)
session_controller = providers.Factory(SessionController, session_service)

View File

@ -1,3 +1,5 @@
# main.py
import eventlet import eventlet
eventlet.monkey_patch() eventlet.monkey_patch()
@ -5,7 +7,7 @@ eventlet.monkey_patch()
import sys import sys
import os import os
import logging import logging
from flask import Flask, request from flask import Flask
from flask_socketio import SocketIO from flask_socketio import SocketIO
sys.path.append(os.path.dirname(__file__)) sys.path.append(os.path.dirname(__file__))
@ -19,10 +21,9 @@ from blueprints import (
default_blueprint, default_blueprint,
history_blueprint, history_blueprint,
subject_blueprint, subject_blueprint,
socket, session_bp,
) )
from database import init_db from database import init_db
from controllers import SocketController
socketio = SocketIO(cors_allowed_origins="*") socketio = SocketIO(cors_allowed_origins="*")
@ -30,12 +31,12 @@ socketio = SocketIO(cors_allowed_origins="*")
def createApp() -> Flask: def createApp() -> Flask:
app = Flask(__name__) app = Flask(__name__)
app.config.from_object(Config)
LoggerConfig.init_logger(app)
logging.basicConfig( logging.basicConfig(
level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
) )
app.config.from_object(Config)
LoggerConfig.init_logger(app)
container = Container() container = Container()
app.container = container app.container = container
@ -43,8 +44,10 @@ def createApp() -> Flask:
mongo = init_db(app) mongo = init_db(app)
if mongo is not None: if mongo is not None:
container.mongo.override(mongo) container.mongo.override(mongo)
container.socketio.override(socketio)
container.socket_controller()
SocketController(socketio)
socketio.init_app(app) socketio.init_app(app)
container.wire( container.wire(
@ -54,6 +57,7 @@ def createApp() -> Flask:
"blueprints.quiz", "blueprints.quiz",
"blueprints.history", "blueprints.history",
"blueprints.subject", "blueprints.subject",
"blueprints.session",
] ]
) )
@ -63,11 +67,11 @@ def createApp() -> Flask:
app.register_blueprint(quiz_bp, url_prefix="/api/quiz") app.register_blueprint(quiz_bp, url_prefix="/api/quiz")
app.register_blueprint(history_blueprint, url_prefix="/api/history") app.register_blueprint(history_blueprint, url_prefix="/api/history")
app.register_blueprint(subject_blueprint, url_prefix="/api/subject") app.register_blueprint(subject_blueprint, url_prefix="/api/subject")
app.register_blueprint(session_bp, url_prefix="/api/session")
return app return app
if __name__ == "__main__": if __name__ == "__main__":
app = createApp() app = createApp()
socketio.run(app, host="0.0.0.0", port=5000, debug=Config.DEBUG) socketio.run(app, host="0.0.0.0", port=5000, debug=Config.DEBUG)

View File

@ -5,6 +5,7 @@ from .question_item_entity import QuestionItemEntity
from .user_answer_entity import UserAnswerEntity from .user_answer_entity import UserAnswerEntity
from .answer_item import AnswerItemEntity from .answer_item import AnswerItemEntity
from .subject_entity import SubjectEntity from .subject_entity import SubjectEntity
from .session_entity import SessionEntity
__all__ = [ __all__ = [
"UserEntity", "UserEntity",
@ -14,4 +15,5 @@ __all__ = [
"UserAnswerEntity", "UserAnswerEntity",
"AnswerItemEntity", "AnswerItemEntity",
"SubjectEntity", "SubjectEntity",
"SessionEntity",
] ]

View File

@ -0,0 +1,18 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
from models.entities import PyObjectId
class SessionEntity(BaseModel):
id: Optional[PyObjectId] = Field(default=None, alias="_id")
session_code: str
quiz_id: str
host_id: str
created_at: datetime = Field(default_factory=datetime.now)
started_at: datetime | None = None
ended_at: datetime | None = None
is_active: bool = True
participan_limit: int = 10
participants: List[str] = []
current_question_index: int = 0

View File

@ -2,10 +2,12 @@ from .user_repository import UserRepository
from .quiz_repositroy import QuizRepository from .quiz_repositroy import QuizRepository
from .answer_repository import UserAnswerRepository from .answer_repository import UserAnswerRepository
from .subject_repository import SubjectRepository from .subject_repository import SubjectRepository
from .session_repostory import SessionRepository
__all__ = [ __all__ = [
"UserRepository", "UserRepository",
"QuizRepository", "QuizRepository",
"UserAnswerRepository", "UserAnswerRepository",
"SubjectRepository", "SubjectRepository",
"SessionRepository",
] ]

View File

@ -0,0 +1,50 @@
from pymongo.collection import Collection
from pymongo.database import Database
from typing import Optional
from models.entities import SessionEntity
class SessionRepository:
COLLECTION_NAME = "session"
def __init__(self, db: Database):
self.collection: Collection = db[self.COLLECTION_NAME]
# self.collection.create_index("id", unique=True)
def insert(self, session_data: SessionEntity) -> str:
print(session_data.model_dump(by_alias=True, exclude_none=True))
result = self.collection.insert_one(
session_data.model_dump(by_alias=True, exclude_none=True)
)
return str(result.inserted_id)
def find_by_session_id(self, session_id: str) -> Optional[SessionEntity]:
doc = self.collection.find_one({"_id": session_id})
return SessionEntity(**doc) if doc else None
def find_by_session_code(self, session_code: str) -> Optional[SessionEntity]:
doc = self.collection.find_one({"session_code": session_code})
return SessionEntity(**doc) if doc else None
def update(self, session_id: str, update_fields: SessionEntity) -> bool:
"""Update specific fields using $set"""
result = self.collection.update_one(
{"_id": session_id},
{"$set": update_fields.model_dump(exclude_unset=True)},
)
return result.modified_count > 0
def add_participant(self, session_id: str, user_id: str) -> bool:
"""Add user_id to participants array without duplicates"""
result = self.collection.update_one(
{"_id": session_id}, {"$addToSet": {"participants": user_id}}
)
return result.modified_count > 0
def delete(self, session_id: str) -> bool:
result = self.collection.delete_one({"_id": session_id})
return result.deleted_count > 0
def list_active_sessions(self) -> list[SessionEntity]:
docs = self.collection.find({"is_active": True})
return [SessionEntity(**doc) for doc in docs]

View File

@ -4,6 +4,7 @@ from .quiz_service import QuizService
from .answer_service import AnswerService from .answer_service import AnswerService
from .history_service import HistoryService from .history_service import HistoryService
from .subject_service import SubjectService from .subject_service import SubjectService
from .session_service import SessionService
__all__ = [ __all__ = [
"AuthService", "AuthService",
@ -12,4 +13,5 @@ __all__ = [
"AnswerService", "AnswerService",
"HistoryService", "HistoryService",
"SubjectService", "SubjectService",
"SessionService",
] ]

View File

@ -0,0 +1,76 @@
from datetime import datetime
from typing import Optional
from uuid import uuid4
from repositories import SessionRepository, UserRepository
from models.entities import SessionEntity
from helpers import DatetimeUtil
class SessionService:
def __init__(self, repository: SessionRepository, user_repository: UserRepository):
self.repository = repository
self.user_repository = user_repository
def create_session(
self, quiz_id: str, host_id: str, limit_participan: int
) -> SessionEntity:
session = SessionEntity(
session_code=uuid4().hex[:6].upper(),
quiz_id=quiz_id,
host_id=host_id,
created_at=DatetimeUtil.now_iso(),
limit_participan=limit_participan,
participants=[],
current_question_index=0,
is_active=True,
)
self.repository.insert(session)
return session
def join_session(self, session_code: str, user_id: str) -> Optional[SessionEntity]:
user = self.user_repository.get_user_by_id(user_id)
session = self.repository.find_by_session_code(session_code=session_code)
if session is None or not session["is_active"]:
return None
if user_id not in session["participants"]:
session["participants"].append(user_id)
self.repository.update(
session.id, {"participants": session["participants"]}
)
response = {
"user_id": user.id,
"username": user.id,
"user_pic": user.pic_url,
"session_id": session.id,
}
return response
def start_session(self, session_id: str) -> bool:
now = DatetimeUtil.now_iso()
return self.repository.update(session_id, {"started_at": now})
def end_session(self, session_id: str) -> bool:
now = DatetimeUtil.now_iso()
return self.repository.update(session_id, {"ended_at": now, "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