From 339da89e72d1f6f5bc7fcc24ff4c3cf229bb6a26 Mon Sep 17 00:00:00 2001 From: akhdanre Date: Tue, 6 May 2025 20:20:42 +0700 Subject: [PATCH] feat: adding session controller --- app/blueprints/__init__.py | 2 + app/blueprints/session.py | 12 +++++ app/blueprints/socket.py | 0 app/controllers/__init__.py | 2 + app/controllers/session_controller.py | 27 ++++++++++ app/controllers/socket_conroller.py | 75 +++++++++++--------------- app/di_container.py | 14 +++++ app/main.py | 18 ++++--- app/models/entities/__init__.py | 2 + app/models/entities/session_entity.py | 18 +++++++ app/repositories/__init__.py | 2 + app/repositories/session_repostory.py | 50 ++++++++++++++++++ app/services/__init__.py | 2 + app/services/session_service.py | 76 +++++++++++++++++++++++++++ 14 files changed, 250 insertions(+), 50 deletions(-) create mode 100644 app/blueprints/session.py delete mode 100644 app/blueprints/socket.py create mode 100644 app/controllers/session_controller.py create mode 100644 app/models/entities/session_entity.py create mode 100644 app/repositories/session_repostory.py create mode 100644 app/services/session_service.py diff --git a/app/blueprints/__init__.py b/app/blueprints/__init__.py index 7b887f5..d5186f1 100644 --- a/app/blueprints/__init__.py +++ b/app/blueprints/__init__.py @@ -5,6 +5,7 @@ from .user import user_blueprint from .quiz import quiz_bp from .history import history_blueprint from .subject import subject_blueprint +from .session import session_bp __all__ = [ "default_blueprint", @@ -13,6 +14,7 @@ __all__ = [ "quiz_bp", "history_blueprint", "subject_blueprint", + "session_bp", ] diff --git a/app/blueprints/session.py b/app/blueprints/session.py new file mode 100644 index 0000000..e4ca2f2 --- /dev/null +++ b/app/blueprints/session.py @@ -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()) diff --git a/app/blueprints/socket.py b/app/blueprints/socket.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py index abf1a74..d4abb26 100644 --- a/app/controllers/__init__.py +++ b/app/controllers/__init__.py @@ -4,6 +4,7 @@ from .quiz_controller import QuizController from .history_controller import HistoryController from .subject_controller import SubjectController from .socket_conroller import SocketController +from .session_controller import SessionController __all__ = [ "AuthController", @@ -12,4 +13,5 @@ __all__ = [ "HistoryController", "SubjectController", "SocketController", + "SessionController", ] diff --git a/app/controllers/session_controller.py b/app/controllers/session_controller.py new file mode 100644 index 0000000..7b87978 --- /dev/null +++ b/app/controllers/session_controller.py @@ -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 diff --git a/app/controllers/socket_conroller.py b/app/controllers/socket_conroller.py index 370f8a1..ef5d1e1 100644 --- a/app/controllers/socket_conroller.py +++ b/app/controllers/socket_conroller.py @@ -1,11 +1,13 @@ +# socket_controller.py from flask_socketio import SocketIO, emit, join_room, leave_room from flask import request +from services import SessionService class SocketController: - def __init__(self, socketio: SocketIO): + def __init__(self, socketio: SocketIO, session_service: SessionService): self.socketio = socketio - self.rooms = {} # room_name -> set of sids + self.session_service = session_service self._register_events() def _register_events(self): @@ -16,67 +18,54 @@ class SocketController: @self.socketio.on("disconnect") def on_disconnect(): - sid = 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}") + print(f"Client disconnected: {request.sid}") @self.socketio.on("join_room") def handle_join_room(data): - if not isinstance(data, dict): - emit("error", {"message": "Invalid data format"}) + session_code = data.get("session_code") + 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 - room = data.get("room") - username = data.get("username", "anonymous") - sid = request.sid - - # 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."}) + session = self.session_service.join_session(session_code, user_id) + if session is None: + emit("error", {"message": "Failed to join session or session inactive"}) return - # Join room - self.rooms[room].add(sid) - join_room(room) + join_room(session_code) + user_data = self.session_service.join_session() emit( "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") def handle_leave_room(data): - room = data.get("room") + session_id = data.get("session_id") username = data.get("username", "anonymous") - sid = request.sid - if room in self.rooms and sid in self.rooms[room]: - self.rooms[room].remove(sid) - - leave_room(room) - print(f"{username} left room {room}") + leave_room(session_id) emit( "room_message", - {"message": f"{username} has left the room.", "room": room}, - room=room, + {"message": f"{username} has left the room.", "room": session_id}, + room=session_id, ) @self.socketio.on("send_message") def on_send_message(data): - room = data.get("room") + session_id = data.get("session_id") message = data.get("message") 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, + ) diff --git a/app/di_container.py b/app/di_container.py index c992868..e8aefcb 100644 --- a/app/di_container.py +++ b/app/di_container.py @@ -4,6 +4,7 @@ from repositories import ( QuizRepository, UserAnswerRepository, SubjectRepository, + SessionRepository, ) from services import ( @@ -13,6 +14,7 @@ from services import ( AnswerService, HistoryService, SubjectService, + SessionService, ) from controllers import ( @@ -21,6 +23,8 @@ from controllers import ( QuizController, HistoryController, SubjectController, + SocketController, + SessionController, ) @@ -29,11 +33,14 @@ class Container(containers.DeclarativeContainer): mongo = providers.Dependency() + socketio = providers.Dependency() + # repository user_repository = providers.Factory(UserRepository, mongo.provided.db) quiz_repository = providers.Factory(QuizRepository, mongo.provided.db) answer_repository = providers.Factory(UserAnswerRepository, mongo.provided.db) subject_repository = providers.Factory(SubjectRepository, mongo.provided.db) + session_repository = providers.Factory(SessionRepository, mongo.provided.db) # services auth_service = providers.Factory( @@ -70,9 +77,16 @@ class Container(containers.DeclarativeContainer): subject_repository, ) + session_service = providers.Factory( + SessionService, + session_repository, + ) + # controllers auth_controller = providers.Factory(AuthController, user_service, auth_service) user_controller = providers.Factory(UserController, user_service) quiz_controller = providers.Factory(QuizController, quiz_service, answer_service) history_controller = providers.Factory(HistoryController, history_service) subject_controller = providers.Factory(SubjectController, subject_service) + socket_controller = providers.Factory(SocketController, socketio, session_service) + session_controller = providers.Factory(SessionController, session_service) diff --git a/app/main.py b/app/main.py index 5cb723c..bc4cbd4 100644 --- a/app/main.py +++ b/app/main.py @@ -1,3 +1,5 @@ +# main.py + import eventlet eventlet.monkey_patch() @@ -5,7 +7,7 @@ eventlet.monkey_patch() import sys import os import logging -from flask import Flask, request +from flask import Flask from flask_socketio import SocketIO sys.path.append(os.path.dirname(__file__)) @@ -19,10 +21,9 @@ from blueprints import ( default_blueprint, history_blueprint, subject_blueprint, - socket, + session_bp, ) from database import init_db -from controllers import SocketController socketio = SocketIO(cors_allowed_origins="*") @@ -30,12 +31,12 @@ socketio = SocketIO(cors_allowed_origins="*") def createApp() -> Flask: app = Flask(__name__) + app.config.from_object(Config) + LoggerConfig.init_logger(app) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) - app.config.from_object(Config) - LoggerConfig.init_logger(app) container = Container() app.container = container @@ -43,8 +44,10 @@ def createApp() -> Flask: mongo = init_db(app) if mongo is not None: container.mongo.override(mongo) + container.socketio.override(socketio) + + container.socket_controller() - SocketController(socketio) socketio.init_app(app) container.wire( @@ -54,6 +57,7 @@ def createApp() -> Flask: "blueprints.quiz", "blueprints.history", "blueprints.subject", + "blueprints.session", ] ) @@ -63,11 +67,11 @@ def createApp() -> Flask: app.register_blueprint(quiz_bp, url_prefix="/api/quiz") app.register_blueprint(history_blueprint, url_prefix="/api/history") app.register_blueprint(subject_blueprint, url_prefix="/api/subject") + app.register_blueprint(session_bp, url_prefix="/api/session") return app if __name__ == "__main__": app = createApp() - socketio.run(app, host="0.0.0.0", port=5000, debug=Config.DEBUG) diff --git a/app/models/entities/__init__.py b/app/models/entities/__init__.py index 9ede322..f25508a 100644 --- a/app/models/entities/__init__.py +++ b/app/models/entities/__init__.py @@ -5,6 +5,7 @@ from .question_item_entity import QuestionItemEntity from .user_answer_entity import UserAnswerEntity from .answer_item import AnswerItemEntity from .subject_entity import SubjectEntity +from .session_entity import SessionEntity __all__ = [ "UserEntity", @@ -14,4 +15,5 @@ __all__ = [ "UserAnswerEntity", "AnswerItemEntity", "SubjectEntity", + "SessionEntity", ] diff --git a/app/models/entities/session_entity.py b/app/models/entities/session_entity.py new file mode 100644 index 0000000..9e00caa --- /dev/null +++ b/app/models/entities/session_entity.py @@ -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 diff --git a/app/repositories/__init__.py b/app/repositories/__init__.py index dac7659..457b80f 100644 --- a/app/repositories/__init__.py +++ b/app/repositories/__init__.py @@ -2,10 +2,12 @@ from .user_repository import UserRepository from .quiz_repositroy import QuizRepository from .answer_repository import UserAnswerRepository from .subject_repository import SubjectRepository +from .session_repostory import SessionRepository __all__ = [ "UserRepository", "QuizRepository", "UserAnswerRepository", "SubjectRepository", + "SessionRepository", ] diff --git a/app/repositories/session_repostory.py b/app/repositories/session_repostory.py new file mode 100644 index 0000000..ccca502 --- /dev/null +++ b/app/repositories/session_repostory.py @@ -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] diff --git a/app/services/__init__.py b/app/services/__init__.py index 4b5b657..fa10d3a 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -4,6 +4,7 @@ from .quiz_service import QuizService from .answer_service import AnswerService from .history_service import HistoryService from .subject_service import SubjectService +from .session_service import SessionService __all__ = [ "AuthService", @@ -12,4 +13,5 @@ __all__ = [ "AnswerService", "HistoryService", "SubjectService", + "SessionService", ] diff --git a/app/services/session_service.py b/app/services/session_service.py new file mode 100644 index 0000000..0f04193 --- /dev/null +++ b/app/services/session_service.py @@ -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