feat: adjustment on the socket session
This commit is contained in:
parent
1db14658e1
commit
76bbbf0824
11
.env.example
11
.env.example
|
@ -1,6 +1,7 @@
|
||||||
|
# Existing Configurations
|
||||||
MONGO_URI=
|
MONGO_URI=
|
||||||
FLASK_ENV=
|
FLASK_ENV=development
|
||||||
DEBUG=
|
DEBUG=True
|
||||||
|
|
||||||
SECRET_KEY=
|
SECRET_KEY=
|
||||||
|
|
||||||
|
@ -10,3 +11,9 @@ GOOGLE_CLIENT_SECRET=
|
||||||
GOOGLE_AUHT_URI=
|
GOOGLE_AUHT_URI=
|
||||||
GOOGLE_TOKEN_URI=
|
GOOGLE_TOKEN_URI=
|
||||||
GOOGLE_AUTH_PROVIDER_X509_CERT_URL=
|
GOOGLE_AUTH_PROVIDER_X509_CERT_URL=
|
||||||
|
|
||||||
|
# Redis Configuration
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_DB=0
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Load variabel dari file .env
|
# Load variables from .env
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
# Flask Environment Settings
|
||||||
FLASK_ENV = os.getenv("FLASK_ENV", "development")
|
FLASK_ENV = os.getenv("FLASK_ENV", "development")
|
||||||
DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "t")
|
DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "t")
|
||||||
|
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY", "your_secret_key")
|
SECRET_KEY = os.getenv("SECRET_KEY", "your_secret_key")
|
||||||
|
|
||||||
|
# MongoDB Settings
|
||||||
MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/yourdb")
|
MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/yourdb")
|
||||||
|
|
||||||
|
# Google OAuth Settings
|
||||||
GOOGLE_PROJECT_ID = os.getenv("GOOGLE_PROJECT_ID")
|
GOOGLE_PROJECT_ID = os.getenv("GOOGLE_PROJECT_ID")
|
||||||
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
|
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
|
||||||
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
|
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
|
||||||
|
@ -22,6 +25,17 @@ class Config:
|
||||||
"GOOGLE_TOKEN_URI", "https://oauth2.googleapis.com/token"
|
"GOOGLE_TOKEN_URI", "https://oauth2.googleapis.com/token"
|
||||||
)
|
)
|
||||||
GOOGLE_AUTH_PROVIDER_X509_CERT_URL = os.getenv("GOOGLE_AUTH_PROVIDER_X509_CERT_URL")
|
GOOGLE_AUTH_PROVIDER_X509_CERT_URL = os.getenv("GOOGLE_AUTH_PROVIDER_X509_CERT_URL")
|
||||||
|
|
||||||
GOOGLE_SCOPE = "email profile"
|
GOOGLE_SCOPE = "email profile"
|
||||||
GOOGLE_BASE_URL = "https://www.googleapis.com/oauth2/v1/"
|
GOOGLE_BASE_URL = "https://www.googleapis.com/oauth2/v1/"
|
||||||
|
|
||||||
|
# Redis Configuration
|
||||||
|
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
|
||||||
|
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
|
||||||
|
REDIS_DB = int(os.getenv("REDIS_DB", 0))
|
||||||
|
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def REDIS_URL(self):
|
||||||
|
if self.REDIS_PASSWORD:
|
||||||
|
return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
||||||
|
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
||||||
|
|
|
@ -3,14 +3,82 @@ from flask import request
|
||||||
from app.services import SessionService
|
from app.services import SessionService
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
|
from redis import Redis
|
||||||
|
|
||||||
|
|
||||||
|
class RedisRepository:
|
||||||
|
"""Small helper wrapper to (de)serialize python objects to Redis."""
|
||||||
|
|
||||||
|
def __init__(self, redis: Redis):
|
||||||
|
self.redis = redis
|
||||||
|
|
||||||
|
def set_data(self, key: str, value):
|
||||||
|
self.redis.set(key, json.dumps(value))
|
||||||
|
|
||||||
|
def get_data(self, key: str):
|
||||||
|
data = self.redis.get(key)
|
||||||
|
return json.loads(data) if data else None
|
||||||
|
|
||||||
|
def delete_key(self, key: str):
|
||||||
|
self.redis.delete(key)
|
||||||
|
|
||||||
|
|
||||||
class SocketController:
|
class SocketController:
|
||||||
def __init__(self, socketio: SocketIO, session_service: SessionService):
|
def __init__(
|
||||||
|
self,
|
||||||
|
socketio: SocketIO,
|
||||||
|
redis: Redis,
|
||||||
|
session_service: SessionService,
|
||||||
|
):
|
||||||
self.socketio = socketio
|
self.socketio = socketio
|
||||||
self.session_service = session_service
|
self.session_service = session_service
|
||||||
|
self.redis_repo = RedisRepository(redis)
|
||||||
|
# Menyimpan SID admin untuk setiap session \u2192 {session_code: sid}
|
||||||
|
self.admin_sids: dict[str, str] = {}
|
||||||
self._register_events()
|
self._register_events()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Helper utilities
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
@staticmethod
|
||||||
|
def _is_correct(user_answer, question: dict) -> bool:
|
||||||
|
"""Bandingkan jawaban user dengan kunci jawaban."""
|
||||||
|
|
||||||
|
print("user answer", user_answer, "question_target", question["target_answer"])
|
||||||
|
if question["type"] == "fill_the_blank":
|
||||||
|
return (
|
||||||
|
str(user_answer).strip().lower()
|
||||||
|
== str(question["target_answer"]).strip().lower()
|
||||||
|
)
|
||||||
|
elif question["type"] == "true_false":
|
||||||
|
return bool(user_answer) == question["target_answer"]
|
||||||
|
elif question["type"] == "option":
|
||||||
|
# user_answer bisa dikirim sebagai index atau value teks option.
|
||||||
|
# Pastikan di‑cast ke int terlebih dahulu jika memungkinkan.
|
||||||
|
try:
|
||||||
|
return int(user_answer) == question["target_answer"]
|
||||||
|
except ValueError:
|
||||||
|
return (
|
||||||
|
str(user_answer).strip().lower()
|
||||||
|
== str(question["options"][question["target_answer"]])
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _questions_key(self, session_code: str) -> str:
|
||||||
|
return f"session:{session_code}:questions"
|
||||||
|
|
||||||
|
def _answers_key(self, session_code: str) -> str:
|
||||||
|
return f"session:{session_code}:answers"
|
||||||
|
|
||||||
|
def _scores_key(self, session_code: str) -> str:
|
||||||
|
return f"session:{session_code}:scores"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Socket.IO event bindings
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
def _register_events(self):
|
def _register_events(self):
|
||||||
@self.socketio.on("connect")
|
@self.socketio.on("connect")
|
||||||
def on_connect():
|
def on_connect():
|
||||||
|
@ -23,8 +91,8 @@ class SocketController:
|
||||||
|
|
||||||
@self.socketio.on("join_room")
|
@self.socketio.on("join_room")
|
||||||
def handle_join_room(data):
|
def handle_join_room(data):
|
||||||
session_code = data["session_code"]
|
session_code = data.get("session_code")
|
||||||
user_id = data["user_id"]
|
user_id = data.get("user_id")
|
||||||
|
|
||||||
if not session_code or not user_id:
|
if not session_code or not user_id:
|
||||||
emit("error", {"message": "session_code and user_id are required"})
|
emit("error", {"message": "session_code and user_id are required"})
|
||||||
|
@ -33,43 +101,47 @@ class SocketController:
|
||||||
session = self.session_service.join_session(
|
session = self.session_service.join_session(
|
||||||
session_code=session_code, user_id=user_id
|
session_code=session_code, user_id=user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if session is None:
|
if session is None:
|
||||||
emit("error", {"message": "Failed to join session or session inactive"})
|
emit("error", {"message": "Failed to join session or session inactive"})
|
||||||
return
|
return
|
||||||
|
|
||||||
join_room(session_code)
|
join_room(session_code)
|
||||||
if session["is_admin"] == True:
|
|
||||||
emit(
|
|
||||||
"room_message",
|
|
||||||
{
|
|
||||||
"message": f"admin has joined the room.",
|
|
||||||
"room": session_code,
|
|
||||||
"argument": "adm_update",
|
|
||||||
},
|
|
||||||
room=session_code,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
# Kalau user ini admin, simpan SID‑nya.
|
||||||
|
if session["is_admin"]:
|
||||||
|
self.admin_sids[session_code] = request.sid
|
||||||
|
message = "Admin has joined the room."
|
||||||
|
else:
|
||||||
|
message = f"User {session['username']} has joined the room."
|
||||||
|
|
||||||
|
print(message)
|
||||||
emit(
|
emit(
|
||||||
"room_message",
|
"room_message",
|
||||||
{
|
{
|
||||||
"message": f"user {session['username']} has joined the room.",
|
"type": "join",
|
||||||
|
"message": message,
|
||||||
"room": session_code,
|
"room": session_code,
|
||||||
"argument": "adm_update",
|
"argument": "adm_update",
|
||||||
"data": session,
|
"data": session if not session["is_admin"] else None,
|
||||||
},
|
},
|
||||||
room=session_code,
|
room=session_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.socketio.on("submit_answer")
|
@self.socketio.on("submit_answer")
|
||||||
def handle_submit_answer(data):
|
def handle_submit_answer(data):
|
||||||
session_id = data.get("session_id")
|
session_code = data.get("session_id") # front‑end masih mengirim session_id
|
||||||
user_id = data.get("user_id")
|
user_id = data.get("user_id")
|
||||||
question_index = data.get("question_index")
|
question_index = data.get("question_index")
|
||||||
answer = data.get("answer")
|
user_answer = data.get("answer")
|
||||||
|
|
||||||
if not all([session_id, user_id, question_index is not None, answer]):
|
if not all(
|
||||||
|
[
|
||||||
|
session_code,
|
||||||
|
user_id,
|
||||||
|
question_index is not None,
|
||||||
|
user_answer is not None,
|
||||||
|
]
|
||||||
|
):
|
||||||
emit(
|
emit(
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
@ -78,62 +150,130 @@ class SocketController:
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"User {user_id} answered question {question_index} with {answer}")
|
# ----- 1. Ambil list pertanyaan untuk mendapatkan kunci jawaban ----------
|
||||||
|
questions = (
|
||||||
|
self.redis_repo.get_data(self._questions_key(session_code)) or []
|
||||||
|
)
|
||||||
|
question = next(
|
||||||
|
(q for q in questions if q["index"] == question_index), None
|
||||||
|
)
|
||||||
|
if question is None:
|
||||||
|
emit("error", {"message": "Question not found"})
|
||||||
|
return
|
||||||
|
|
||||||
# TODO: kamu bisa menyimpan jawaban ke database di sini
|
is_correct = self._is_correct(user_answer, question)
|
||||||
# self.answer_service.save_answer(session_id, user_id, question_index, answer)
|
|
||||||
|
|
||||||
# Kirim notifikasi ke admin (host) atau semua peserta kalau perlu
|
print(
|
||||||
|
f"User {user_id} answered Q{question_index} with '{user_answer}' -> {'✔' if is_correct else '✖'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- 2. Simpan jawaban ke Redis --------------------------------------
|
||||||
|
answers = self.redis_repo.get_data(self._answers_key(session_code)) or []
|
||||||
|
answers.append(
|
||||||
|
{
|
||||||
|
"user_id": user_id,
|
||||||
|
"question_index": question_index,
|
||||||
|
"answer": user_answer,
|
||||||
|
"correct": is_correct,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.redis_repo.set_data(self._answers_key(session_code), answers)
|
||||||
|
|
||||||
|
# ----- 3. Update skor per‑user -----------------------------------------
|
||||||
|
scores = self.redis_repo.get_data(self._scores_key(session_code)) or {}
|
||||||
|
user_score = scores.get(str(user_id), {"correct": 0, "incorrect": 0})
|
||||||
|
if is_correct:
|
||||||
|
user_score["correct"] += 1
|
||||||
|
else:
|
||||||
|
user_score["incorrect"] += 1
|
||||||
|
scores[str(user_id)] = user_score
|
||||||
|
self.redis_repo.set_data(self._scores_key(session_code), scores)
|
||||||
|
|
||||||
|
# ----- 4. Beri tahu user (ack) -----------------------------------------
|
||||||
emit(
|
emit(
|
||||||
"answer_submitted",
|
"answer_submitted",
|
||||||
{
|
{
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"question_index": question_index,
|
"question_index": question_index,
|
||||||
"answer": answer,
|
"answer": user_answer,
|
||||||
|
"correct": is_correct,
|
||||||
},
|
},
|
||||||
room=session_id,
|
room=request.sid,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ----- 5. Kirim update skor hanya ke admin -----------------------------
|
||||||
|
admin_sid = self.admin_sids.get(session_code)
|
||||||
|
if admin_sid:
|
||||||
|
emit("score_update", scores, room=admin_sid)
|
||||||
|
|
||||||
@self.socketio.on("leave_room")
|
@self.socketio.on("leave_room")
|
||||||
def handle_leave_room(data):
|
def handle_leave_room(data):
|
||||||
session_id = data.get("session_id")
|
session_code = data.get("session_id")
|
||||||
user_id = data.get("user_id")
|
user_id = data.get("user_id")
|
||||||
username = data.get("username", "anonymous")
|
username = data.get("username", "anonymous")
|
||||||
|
|
||||||
leave_room(session_id)
|
leave_room(session_code)
|
||||||
emit(
|
emit(
|
||||||
"room_message",
|
"room_message",
|
||||||
{"message": f"{username} has left the room.", "room": session_id},
|
{
|
||||||
room=session_id,
|
"type": "leave",
|
||||||
|
"message": f"{username} has left the room.",
|
||||||
|
"room": session_code,
|
||||||
|
"data": user_id,
|
||||||
|
},
|
||||||
|
room=session_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.socketio.on("send_message")
|
@self.socketio.on("send_message")
|
||||||
def on_send_message(data):
|
def on_send_message(data):
|
||||||
session_id = data.get("session_id")
|
session_code = data.get("session_id")
|
||||||
message = data.get("message")
|
message = data.get("message")
|
||||||
username = data.get("username", "anonymous")
|
username = data.get("username", "anonymous")
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
"receive_message",
|
"receive_message",
|
||||||
{"message": message, "from": username},
|
{"message": message, "from": username},
|
||||||
room=session_id,
|
room=session_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.socketio.on("end_session")
|
@self.socketio.on("end_session")
|
||||||
def handle_end_session(data):
|
def handle_end_session(data):
|
||||||
session_id = data.get("session_id")
|
session_code = data.get("session_id")
|
||||||
user_id = data.get("user_id")
|
user_id = data.get("user_id")
|
||||||
|
|
||||||
if not session_id or not user_id:
|
if not session_code or not user_id:
|
||||||
emit("error", {"message": "session_id and user_id required"})
|
emit("error", {"message": "session_id and user_id required"})
|
||||||
return
|
return
|
||||||
|
|
||||||
self.session_service.end_session(session_id=session_id, user_id=user_id)
|
# Validasi user berhak mengakhiri session
|
||||||
|
self.session_service.end_session(session_id=session_code, user_id=user_id)
|
||||||
|
|
||||||
|
answers = self.redis_repo.get_data(self._answers_key(session_code)) or []
|
||||||
|
scores = self.redis_repo.get_data(self._scores_key(session_code)) or {}
|
||||||
|
|
||||||
|
print("\n📦 Final Quiz Data for Session", session_code)
|
||||||
|
print("------------------------------------------------------------")
|
||||||
|
for entry in answers:
|
||||||
|
status = "✔" if entry["correct"] else "✖"
|
||||||
|
print(
|
||||||
|
f"User {entry['user_id']} - Q{entry['question_index']}: {entry['answer']} {status}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n🏁 Rekap Skor:")
|
||||||
|
for uid, sc in scores.items():
|
||||||
|
print(f"User {uid}: Benar {sc['correct']} | Salah {sc['incorrect']}")
|
||||||
|
|
||||||
|
# Bersihkan semua data session di Redis
|
||||||
|
for key in [
|
||||||
|
self._answers_key(session_code),
|
||||||
|
self._scores_key(session_code),
|
||||||
|
self._questions_key(session_code),
|
||||||
|
]:
|
||||||
|
self.redis_repo.delete_key(key)
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
"room_closed",
|
"room_closed",
|
||||||
{"message": "Session has ended.", "room": session_id},
|
{"message": "Session has ended.", "room": session_code},
|
||||||
room=session_id,
|
room=session_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.socketio.on("start_quiz")
|
@self.socketio.on("start_quiz")
|
||||||
|
@ -144,54 +284,120 @@ class SocketController:
|
||||||
return
|
return
|
||||||
|
|
||||||
emit("quiz_started", {"message": "Quiz has started!"}, room=session_code)
|
emit("quiz_started", {"message": "Quiz has started!"}, room=session_code)
|
||||||
|
|
||||||
# Jalankan thread untuk mengirim soal simulasi setiap 5 detik
|
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._simulate_quiz_flow, args=(session_code,)
|
target=self._simulate_quiz_flow, args=(session_code,), daemon=True
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
def _simulate_quiz_flow(self, session_code):
|
# ---------------------------------------------------------------------
|
||||||
|
# Quiz flow simulation -------------------------------------------------
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
def _simulate_quiz_flow(self, session_code: str):
|
||||||
|
"""Mengirim list pertanyaan satu per satu secara otomatis (demo)."""
|
||||||
questions = [
|
questions = [
|
||||||
{
|
{
|
||||||
"question_index": 0,
|
"index": 1,
|
||||||
"question": "Apa ibu kota Indonesia?",
|
"question": "Kerajaan Hindu tertua di Indonesia adalah?",
|
||||||
"type": "option",
|
"target_answer": "Kutai",
|
||||||
"options": ["Jakarta", "Bandung", "Surabaya"],
|
"duration": 30,
|
||||||
|
"type": "fill_the_blank",
|
||||||
|
"options": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"question_index": 1,
|
"index": 2,
|
||||||
"question": "2 + 2 = ?",
|
"question": "Apakah benar Majapahit mencapai puncak kejayaan pada masa Hayam Wuruk?",
|
||||||
"type": "option",
|
"target_answer": True,
|
||||||
"options": ["3", "4", "5"],
|
"duration": 30,
|
||||||
},
|
|
||||||
{
|
|
||||||
"question_index": 2,
|
|
||||||
"question": "Siapa presiden pertama Indonesia?",
|
|
||||||
"type": "option",
|
|
||||||
"options": ["Sukarno", "Soeharto", "Jokowi"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"question_index": 3,
|
|
||||||
"question": "Tuliskan nama lengkap presiden pertama Indonesia.",
|
|
||||||
"type": "fill_in_the_blank",
|
|
||||||
"options": [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"question_index": 4,
|
|
||||||
"question": "Indonesia merdeka pada tahun 1945.",
|
|
||||||
"type": "true_false",
|
"type": "true_false",
|
||||||
"options": [],
|
"options": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"question": "Kerajaan maritim terbesar di Asia Tenggara pada abad ke‑7 adalah?",
|
||||||
|
"target_answer": 2,
|
||||||
|
"duration": 30,
|
||||||
|
"type": "option",
|
||||||
|
"options": ["Majapahit", "Tarumanegara", "Sriwijaya", "Mataram Kuno"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"question": "Prasasti Yupa merupakan peninggalan dari kerajaan?",
|
||||||
|
"target_answer": "Kutai",
|
||||||
|
"duration": 30,
|
||||||
|
"type": "fill_the_blank",
|
||||||
|
"options": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"question": "Apakah Tarumanegara terletak di wilayah Kalimantan Timur?",
|
||||||
|
"target_answer": False,
|
||||||
|
"duration": 30,
|
||||||
|
"type": "true_false",
|
||||||
|
"options": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 6,
|
||||||
|
"question": "Kitab Negarakertagama ditulis oleh?",
|
||||||
|
"target_answer": 0,
|
||||||
|
"duration": 30,
|
||||||
|
"type": "option",
|
||||||
|
"options": [
|
||||||
|
"Empu Tantular",
|
||||||
|
"Empu Prapanca",
|
||||||
|
"Hayam Wuruk",
|
||||||
|
"Gajah Mada",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 7,
|
||||||
|
"question": "Tokoh yang terkenal dengan Sumpah Palapa adalah?",
|
||||||
|
"target_answer": "Gajah Mada",
|
||||||
|
"duration": 30,
|
||||||
|
"type": "fill_the_blank",
|
||||||
|
"options": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 8,
|
||||||
|
"question": "Candi Borobudur dibangun oleh kerajaan Hindu?",
|
||||||
|
"target_answer": False,
|
||||||
|
"duration": 30,
|
||||||
|
"type": "true_false",
|
||||||
|
"options": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 9,
|
||||||
|
"question": "Raja terkenal dari Kerajaan Sriwijaya adalah?",
|
||||||
|
"target_answer": 1,
|
||||||
|
"duration": 30,
|
||||||
|
"type": "option",
|
||||||
|
"options": [
|
||||||
|
"Dapunta Hyang",
|
||||||
|
"Balaputradewa",
|
||||||
|
"Airlangga",
|
||||||
|
"Hayam Wuruk",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 10,
|
||||||
|
"question": "Candi Prambanan merupakan peninggalan agama?",
|
||||||
|
"target_answer": "Hindu",
|
||||||
|
"duration": 30,
|
||||||
|
"type": "fill_the_blank",
|
||||||
|
"options": None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for q in questions:
|
# Simpan ke Redis agar bisa dipakai saat evaluasi jawaban
|
||||||
print(f"Sending question {q['question_index']} to {session_code}")
|
self.redis_repo.set_data(self._questions_key(session_code), questions)
|
||||||
self.socketio.emit("quiz_question", q, room=session_code)
|
|
||||||
time.sleep(20)
|
# Beri sedikit jeda sebelum pertanyaan pertama
|
||||||
# send true ansewr
|
time.sleep(2)
|
||||||
time.sleep(5)
|
|
||||||
|
for q in questions:
|
||||||
|
print(f"\n📢 Mengirim pertanyaan {q['index']} ke room {session_code}")
|
||||||
|
q.pop("target_answer")
|
||||||
|
self.socketio.emit("quiz_question", q, room=session_code)
|
||||||
|
time.sleep(q["duration"])
|
||||||
|
|
||||||
# Setelah selesai semua soal, kirim command bahwa quiz selesai
|
|
||||||
self.socketio.emit(
|
self.socketio.emit(
|
||||||
"quiz_done", {"message": "Quiz has ended!"}, room=session_code
|
"quiz_done", {"message": "Quiz has ended!"}, room=session_code
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,7 +32,7 @@ class Container(containers.DeclarativeContainer):
|
||||||
"""Dependency Injection Container"""
|
"""Dependency Injection Container"""
|
||||||
|
|
||||||
mongo = providers.Dependency()
|
mongo = providers.Dependency()
|
||||||
|
redis = providers.Dependency()
|
||||||
socketio = providers.Dependency()
|
socketio = providers.Dependency()
|
||||||
|
|
||||||
# repository
|
# repository
|
||||||
|
@ -89,5 +89,10 @@ class Container(containers.DeclarativeContainer):
|
||||||
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)
|
socket_controller = providers.Factory(
|
||||||
|
SocketController,
|
||||||
|
socketio,
|
||||||
|
redis,
|
||||||
|
session_service,
|
||||||
|
)
|
||||||
session_controller = providers.Factory(SessionController, session_service)
|
session_controller = providers.Factory(SessionController, session_service)
|
||||||
|
|
27
app/main.py
27
app/main.py
|
@ -1,17 +1,11 @@
|
||||||
# main.py
|
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
|
|
||||||
eventlet.monkey_patch()
|
eventlet.monkey_patch()
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
|
|
||||||
# sys.path.append(os.path.dirname(__file__))
|
|
||||||
|
|
||||||
from app.di_container import Container
|
from app.di_container import Container
|
||||||
from app.configs import Config, LoggerConfig
|
from app.configs import Config, LoggerConfig
|
||||||
from app.blueprints import (
|
from app.blueprints import (
|
||||||
|
@ -24,12 +18,10 @@ from app.blueprints import (
|
||||||
session_bp,
|
session_bp,
|
||||||
)
|
)
|
||||||
from app.database import init_db
|
from app.database import init_db
|
||||||
|
from redis import Redis
|
||||||
|
|
||||||
|
|
||||||
socketio = SocketIO(cors_allowed_origins="*")
|
def createApp() -> tuple[Flask, SocketIO]:
|
||||||
|
|
||||||
|
|
||||||
def createApp() -> Flask:
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
LoggerConfig.init_logger(app)
|
LoggerConfig.init_logger(app)
|
||||||
|
@ -44,8 +36,19 @@ 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)
|
|
||||||
|
|
||||||
|
redis_url = Config().REDIS_URL
|
||||||
|
redis_client = Redis.from_url(redis_url)
|
||||||
|
redis_client.ping()
|
||||||
|
container.redis.override(redis_client)
|
||||||
|
|
||||||
|
socketio = SocketIO(
|
||||||
|
cors_allowed_origins="*",
|
||||||
|
# message_queue=redis_url,
|
||||||
|
async_mode="eventlet",
|
||||||
|
)
|
||||||
|
|
||||||
|
container.socketio.override(socketio)
|
||||||
container.socket_controller()
|
container.socket_controller()
|
||||||
|
|
||||||
socketio.init_app(app)
|
socketio.init_app(app)
|
||||||
|
@ -69,4 +72,4 @@ def createApp() -> Flask:
|
||||||
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")
|
app.register_blueprint(session_bp, url_prefix="/api/session")
|
||||||
|
|
||||||
return app
|
return app, socketio
|
||||||
|
|
|
@ -42,8 +42,9 @@ class UserMapper:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def user_entity_to_response(user: UserEntity) -> UserResponseModel:
|
def user_entity_to_response(user: UserEntity) -> UserResponseModel:
|
||||||
|
print(str(user.id))
|
||||||
return UserResponseModel(
|
return UserResponseModel(
|
||||||
id=str(user.id) if user.id else None,
|
_id=str(user.id) if user.id else None,
|
||||||
google_id=user.google_id,
|
google_id=user.google_id,
|
||||||
email=user.email,
|
email=user.email,
|
||||||
name=user.name,
|
name=user.name,
|
||||||
|
|
|
@ -71,7 +71,7 @@ class SessionService:
|
||||||
return self.repository.update(session_id, {"started_at": now})
|
return self.repository.update(session_id, {"started_at": now})
|
||||||
|
|
||||||
def end_session(self, session_id: str, user_id: str):
|
def end_session(self, session_id: str, user_id: str):
|
||||||
session = self.repository.find_by_id(session_id)
|
session = self.repository.find_by_session_id(session_id)
|
||||||
if session and session.host_id == user_id:
|
if session and session.host_id == user_id:
|
||||||
session.is_active = False
|
session.is_active = False
|
||||||
self.repository.update(session_id, {"is_active": False})
|
self.repository.update(session_id, {"is_active": False})
|
||||||
|
|
|
@ -12,6 +12,7 @@ cryptography==44.0.2
|
||||||
dependency-injector==4.46.0
|
dependency-injector==4.46.0
|
||||||
dnspython==2.7.0
|
dnspython==2.7.0
|
||||||
email_validator==2.2.0
|
email_validator==2.2.0
|
||||||
|
eventlet==0.39.1
|
||||||
exceptiongroup==1.2.2
|
exceptiongroup==1.2.2
|
||||||
Flask==3.0.3
|
Flask==3.0.3
|
||||||
Flask-Bcrypt==1.0.1
|
Flask-Bcrypt==1.0.1
|
||||||
|
@ -23,6 +24,8 @@ flask-swagger-ui==4.11.1
|
||||||
google-auth==2.38.0
|
google-auth==2.38.0
|
||||||
google-auth-httplib2==0.2.0
|
google-auth-httplib2==0.2.0
|
||||||
google-auth-oauthlib==1.2.1
|
google-auth-oauthlib==1.2.1
|
||||||
|
greenlet==3.2.1
|
||||||
|
gunicorn==23.0.0
|
||||||
h11==0.14.0
|
h11==0.14.0
|
||||||
httplib2==0.22.0
|
httplib2==0.22.0
|
||||||
idna==3.10
|
idna==3.10
|
||||||
|
|
4
run.py
4
run.py
|
@ -1,7 +1,7 @@
|
||||||
# run.py
|
# run.py
|
||||||
from app.main import createApp, socketio
|
from app.main import createApp
|
||||||
|
|
||||||
app = createApp()
|
app, socketio = createApp()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
socketio.run(app, host="0.0.0.0", port=5000, debug=True)
|
socketio.run(app, host="0.0.0.0", port=5000, debug=True)
|
||||||
|
|
Loading…
Reference in New Issue