feat: adding session controller
This commit is contained in:
parent
fa971e1d24
commit
339da89e72
|
@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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())
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
18
app/main.py
18
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)
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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]
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue