From f7478801229b811f87e5c2af713792c50d7fa2e7 Mon Sep 17 00:00:00 2001 From: akhdanre Date: Wed, 7 May 2025 23:46:54 +0700 Subject: [PATCH] feat: adjustment on the dependencies --- __pycache__/run.cpython-310.pyc | Bin 0 -> 220 bytes app/__init__.py | 2 + app/blueprints/auth.py | 4 +- app/blueprints/history.py | 4 +- app/blueprints/quiz.py | 4 +- app/blueprints/session.py | 4 +- app/blueprints/subject.py | 4 +- app/blueprints/user.py | 4 +- app/controllers/auth_controller.py | 16 +- app/controllers/history_controller.py | 4 +- app/controllers/quiz_controller.py | 10 +- app/controllers/session_controller.py | 9 +- app/controllers/socket_conroller.py | 142 +++++++++++++++++- app/controllers/subject_controller.py | 4 +- app/controllers/user_controller.py | 10 +- app/di_container.py | 7 +- app/helpers/response_helper.py | 2 +- app/main.py | 31 ++-- app/mapper/quiz_mapper.py | 12 +- app/mapper/user_mapper.py | 4 +- app/models/entities/session_entity.py | 2 +- app/models/entities/subject_entity.py | 2 +- app/repositories/answer_repository.py | 2 +- app/repositories/quiz_repositroy.py | 4 +- app/repositories/session_repostory.py | 4 +- app/repositories/subject_repository.py | 2 +- app/repositories/user_repository.py | 2 +- .../response/quiz/quiz_data_rsp_schema.py | 2 +- app/services/answer_service.py | 10 +- app/services/auth_service.py | 10 +- app/services/history_service.py | 4 +- app/services/quiz_service.py | 23 +-- app/services/session_service.py | 67 ++++++--- app/services/subject_service.py | 8 +- app/services/user_service.py | 8 +- pytest.ini | 3 + run.py | 8 + ..._quiz_service.cpython-310-pytest-8.3.4.pyc | Bin 0 -> 5070 bytes test/service/test_quiz_service.py | 111 ++++++++++++++ 39 files changed, 411 insertions(+), 138 deletions(-) create mode 100644 __pycache__/run.cpython-310.pyc create mode 100644 app/__init__.py create mode 100644 pytest.ini create mode 100644 run.py create mode 100644 test/service/__pycache__/test_quiz_service.cpython-310-pytest-8.3.4.pyc create mode 100644 test/service/test_quiz_service.py diff --git a/__pycache__/run.cpython-310.pyc b/__pycache__/run.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc0bed3754ac56cf9115b5363964d1e1b700c12d GIT binary patch literal 220 zcmYj}F$%&k7=@FxLM;@W^#olE4sIfP0T;I}r6fNX+cYsrL0z3ahHYY6{h7&dr*ijYLohG<$)%?d^d&>JZt$@V0V-uNdu>&L=D z6<}*^pLio`2+Hj8xJW8%=ekm@8zxY>)N@!^vOya*)_1HIO1c(gr@8Z$*eXF literal 0 HcmV?d00001 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..8d0a6ee --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,2 @@ +# from flask import Flask +from app.main import createApp diff --git a/app/blueprints/auth.py b/app/blueprints/auth.py index ce87c62..eed5d4c 100644 --- a/app/blueprints/auth.py +++ b/app/blueprints/auth.py @@ -1,6 +1,6 @@ from flask import Blueprint -from controllers import AuthController -from di_container import Container +from app.controllers import AuthController +from app.di_container import Container from dependency_injector.wiring import inject, Provide diff --git a/app/blueprints/history.py b/app/blueprints/history.py index 5a06955..96108fd 100644 --- a/app/blueprints/history.py +++ b/app/blueprints/history.py @@ -1,6 +1,6 @@ from flask import Blueprint -from controllers import HistoryController -from di_container import Container +from app.controllers import HistoryController +from app.di_container import Container from dependency_injector.wiring import inject, Provide history_blueprint = Blueprint("history", __name__) diff --git a/app/blueprints/quiz.py b/app/blueprints/quiz.py index 31f4b5c..2316f64 100644 --- a/app/blueprints/quiz.py +++ b/app/blueprints/quiz.py @@ -1,7 +1,7 @@ from flask import Blueprint, request -from di_container import Container +from app.di_container import Container from dependency_injector.wiring import inject, Provide -from controllers import QuizController +from app.controllers import QuizController quiz_bp = Blueprint("quiz", __name__) diff --git a/app/blueprints/session.py b/app/blueprints/session.py index e4ca2f2..65a39c7 100644 --- a/app/blueprints/session.py +++ b/app/blueprints/session.py @@ -1,7 +1,7 @@ from flask import Blueprint, request -from di_container import Container from dependency_injector.wiring import inject, Provide -from controllers import SessionController +from app.di_container import Container +from app.controllers import SessionController session_bp = Blueprint("session", __name__) diff --git a/app/blueprints/subject.py b/app/blueprints/subject.py index 3e01644..dc2b94d 100644 --- a/app/blueprints/subject.py +++ b/app/blueprints/subject.py @@ -1,7 +1,7 @@ from flask import Blueprint, request -from di_container import Container from dependency_injector.wiring import inject, Provide -from controllers import SubjectController +from app.di_container import Container +from app.controllers import SubjectController subject_blueprint = Blueprint("subject", __name__) diff --git a/app/blueprints/user.py b/app/blueprints/user.py index 182a318..ed29a22 100644 --- a/app/blueprints/user.py +++ b/app/blueprints/user.py @@ -1,6 +1,6 @@ from flask import Blueprint -from controllers import UserController -from di_container import Container +from app.di_container import Container +from app.controllers import UserController from dependency_injector.wiring import inject, Provide user_blueprint = Blueprint("user", __name__) diff --git a/app/controllers/auth_controller.py b/app/controllers/auth_controller.py index 768602a..3e76fe6 100644 --- a/app/controllers/auth_controller.py +++ b/app/controllers/auth_controller.py @@ -1,13 +1,13 @@ from flask import jsonify, request, current_app from pydantic import ValidationError -from models.login.login_response import UserResponseModel -from schemas.basic_response_schema import ResponseSchema -from schemas.google_login_schema import GoogleLoginSchema -from schemas import LoginSchema -from services import UserService, AuthService -from exception import AuthException -from mapper import UserMapper -from helpers import make_response +from app.models.login.login_response import UserResponseModel +from app.schemas.basic_response_schema import ResponseSchema +from app.schemas.google_login_schema import GoogleLoginSchema +from app.schemas import LoginSchema +from app.services import UserService, AuthService +from app.exception import AuthException +from app.mapper import UserMapper +from app.helpers import make_response import logging logging = logging.getLogger(__name__) diff --git a/app/controllers/history_controller.py b/app/controllers/history_controller.py index b08e719..6ed0574 100644 --- a/app/controllers/history_controller.py +++ b/app/controllers/history_controller.py @@ -1,5 +1,5 @@ -from services import HistoryService -from helpers import make_error_response, make_response +from app.services import HistoryService +from app.helpers import make_error_response, make_response class HistoryController: diff --git a/app/controllers/quiz_controller.py b/app/controllers/quiz_controller.py index f6630cf..5fd58b8 100644 --- a/app/controllers/quiz_controller.py +++ b/app/controllers/quiz_controller.py @@ -1,10 +1,10 @@ import json from pydantic import ValidationError -from schemas.requests import QuizCreateSchema, UserAnswerSchema -from schemas.response import QuizCreationResponse -from services import QuizService, AnswerService -from helpers import make_response, make_error_response -from exception import ValidationException, DataNotFoundException +from app.schemas.requests import QuizCreateSchema, UserAnswerSchema +from app.schemas.response import QuizCreationResponse +from app.services import QuizService, AnswerService +from app.helpers import make_response, make_error_response +from app.exception import ValidationException, DataNotFoundException class QuizController: diff --git a/app/controllers/session_controller.py b/app/controllers/session_controller.py index 7b87978..55de170 100644 --- a/app/controllers/session_controller.py +++ b/app/controllers/session_controller.py @@ -1,6 +1,7 @@ from flask import request, jsonify from flask.views import MethodView -from services.session_service import SessionService +from app.services.session_service import SessionService +from app.helpers import make_response class SessionController(MethodView): @@ -24,4 +25,8 @@ class SessionController(MethodView): limit_participan=data["limit_participan"], ) - return jsonify(session.dict()), 201 + return make_response( + message="succes create room", + data=session, + status_code=201, + ) diff --git a/app/controllers/socket_conroller.py b/app/controllers/socket_conroller.py index ef5d1e1..f204245 100644 --- a/app/controllers/socket_conroller.py +++ b/app/controllers/socket_conroller.py @@ -1,7 +1,8 @@ -# socket_controller.py from flask_socketio import SocketIO, emit, join_room, leave_room from flask import request -from services import SessionService +from app.services import SessionService +import threading +import time class SocketController: @@ -22,33 +23,81 @@ class SocketController: @self.socketio.on("join_room") def handle_join_room(data): - session_code = data.get("session_code") - user_id = data.get("user_id") + session_code = data["session_code"] + user_id = data["user_id"] if not session_code or not user_id: - emit("error", {"message": "session_id and user_id are required"}) + emit("error", {"message": "session_code and user_id are required"}) return - session = self.session_service.join_session(session_code, user_id) + session = self.session_service.join_session( + session_code=session_code, user_id=user_id + ) + if session is None: emit("error", {"message": "Failed to join session or session inactive"}) return join_room(session_code) - user_data = self.session_service.join_session() + 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 + emit( "room_message", { - "message": "someone has joined the room.", + "message": f"user {session['username']} has joined the room.", "room": session_code, "argument": "adm_update", + "data": session, }, room=session_code, ) + @self.socketio.on("submit_answer") + def handle_submit_answer(data): + session_id = data.get("session_id") + user_id = data.get("user_id") + question_index = data.get("question_index") + answer = data.get("answer") + + if not all([session_id, user_id, question_index is not None, answer]): + emit( + "error", + { + "message": "session_id, user_id, question_index, and answer are required" + }, + ) + return + + print(f"User {user_id} answered question {question_index} with {answer}") + + # TODO: kamu bisa menyimpan jawaban ke database di sini + # self.answer_service.save_answer(session_id, user_id, question_index, answer) + + # Kirim notifikasi ke admin (host) atau semua peserta kalau perlu + emit( + "answer_submitted", + { + "user_id": user_id, + "question_index": question_index, + "answer": answer, + }, + room=session_id, + ) + @self.socketio.on("leave_room") def handle_leave_room(data): session_id = data.get("session_id") + user_id = data.get("user_id") username = data.get("username", "anonymous") leave_room(session_id) @@ -69,3 +118,80 @@ class SocketController: {"message": message, "from": username}, room=session_id, ) + + @self.socketio.on("end_session") + def handle_end_session(data): + session_id = data.get("session_id") + user_id = data.get("user_id") + + if not session_id or not user_id: + emit("error", {"message": "session_id and user_id required"}) + return + + self.session_service.end_session(session_id=session_id, user_id=user_id) + + emit( + "room_closed", + {"message": "Session has ended.", "room": session_id}, + room=session_id, + ) + + @self.socketio.on("start_quiz") + def handle_start_quiz(data): + session_code = data.get("session_code") + if not session_code: + emit("error", {"message": "session_code is required"}) + return + + emit("quiz_started", {"message": "Quiz has started!"}, room=session_code) + + # Jalankan thread untuk mengirim soal simulasi setiap 5 detik + threading.Thread( + target=self._simulate_quiz_flow, args=(session_code,) + ).start() + + def _simulate_quiz_flow(self, session_code): + questions = [ + { + "question_index": 0, + "question": "Apa ibu kota Indonesia?", + "type": "option", + "options": ["Jakarta", "Bandung", "Surabaya"], + }, + { + "question_index": 1, + "question": "2 + 2 = ?", + "type": "option", + "options": ["3", "4", "5"], + }, + { + "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", + "options": [], + }, + ] + + for q in questions: + print(f"Sending question {q['question_index']} to {session_code}") + self.socketio.emit("quiz_question", q, room=session_code) + time.sleep(20) + # send true ansewr + time.sleep(5) + + # Setelah selesai semua soal, kirim command bahwa quiz selesai + self.socketio.emit( + "quiz_done", {"message": "Quiz has ended!"}, room=session_code + ) diff --git a/app/controllers/subject_controller.py b/app/controllers/subject_controller.py index a67e388..6f080e4 100644 --- a/app/controllers/subject_controller.py +++ b/app/controllers/subject_controller.py @@ -1,5 +1,5 @@ -from services.subject_service import SubjectService -from helpers import make_response, make_error_response +from app.services.subject_service import SubjectService +from app.helpers import make_response, make_error_response class SubjectController: diff --git a/app/controllers/user_controller.py b/app/controllers/user_controller.py index b11a75c..b8c6fee 100644 --- a/app/controllers/user_controller.py +++ b/app/controllers/user_controller.py @@ -1,11 +1,11 @@ # /controllers/user_controller.py from flask import jsonify, request, current_app -from services import UserService -from schemas import RegisterSchema +from app.services import UserService +from app.schemas import RegisterSchema from pydantic import ValidationError -from schemas import ResponseSchema -from exception import AlreadyExistException -from helpers import make_response +from app.schemas import ResponseSchema +from app.exception import AlreadyExistException +from app.helpers import make_response class UserController: diff --git a/app/di_container.py b/app/di_container.py index e8aefcb..cd771f4 100644 --- a/app/di_container.py +++ b/app/di_container.py @@ -1,5 +1,5 @@ from dependency_injector import containers, providers -from repositories import ( +from app.repositories import ( UserRepository, QuizRepository, UserAnswerRepository, @@ -7,7 +7,7 @@ from repositories import ( SessionRepository, ) -from services import ( +from app.services import ( UserService, AuthService, QuizService, @@ -17,7 +17,7 @@ from services import ( SessionService, ) -from controllers import ( +from app.controllers import ( UserController, AuthController, QuizController, @@ -80,6 +80,7 @@ class Container(containers.DeclarativeContainer): session_service = providers.Factory( SessionService, session_repository, + user_repository, ) # controllers diff --git a/app/helpers/response_helper.py b/app/helpers/response_helper.py index 1eb68f4..a48f396 100644 --- a/app/helpers/response_helper.py +++ b/app/helpers/response_helper.py @@ -1,6 +1,6 @@ from flask import jsonify, current_app from typing import Optional, Union -from schemas import ResponseSchema, MetaSchema +from app.schemas import ResponseSchema, MetaSchema import math diff --git a/app/main.py b/app/main.py index bc4cbd4..9962377 100644 --- a/app/main.py +++ b/app/main.py @@ -10,11 +10,11 @@ import logging from flask import Flask from flask_socketio import SocketIO -sys.path.append(os.path.dirname(__file__)) +# sys.path.append(os.path.dirname(__file__)) -from di_container import Container -from configs import Config, LoggerConfig -from blueprints import ( +from app.di_container import Container +from app.configs import Config, LoggerConfig +from app.blueprints import ( auth_blueprint, user_blueprint, quiz_bp, @@ -23,7 +23,7 @@ from blueprints import ( subject_blueprint, session_bp, ) -from database import init_db +from app.database import init_db socketio = SocketIO(cors_allowed_origins="*") @@ -45,19 +45,19 @@ def createApp() -> Flask: if mongo is not None: container.mongo.override(mongo) container.socketio.override(socketio) - - container.socket_controller() + + container.socket_controller() socketio.init_app(app) container.wire( modules=[ - "blueprints.auth", - "blueprints.user", - "blueprints.quiz", - "blueprints.history", - "blueprints.subject", - "blueprints.session", + "app.blueprints.auth", + "app.blueprints.user", + "app.blueprints.quiz", + "app.blueprints.history", + "app.blueprints.subject", + "app.blueprints.session", ] ) @@ -70,8 +70,3 @@ def createApp() -> Flask: 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/mapper/quiz_mapper.py b/app/mapper/quiz_mapper.py index 2abcadf..a6bd117 100644 --- a/app/mapper/quiz_mapper.py +++ b/app/mapper/quiz_mapper.py @@ -1,10 +1,10 @@ from datetime import datetime -from helpers import DatetimeUtil -from models import QuizEntity, QuestionItemEntity, UserEntity -from models.entities import SubjectEntity -from schemas import QuizGetSchema, QuestionItemSchema -from schemas.response import ListingQuizResponse -from schemas.requests import QuizCreateSchema +from app.helpers import DatetimeUtil +from app.models import QuizEntity, QuestionItemEntity, UserEntity +from app.models.entities import SubjectEntity +from app.schemas import QuizGetSchema, QuestionItemSchema +from app.schemas.response import ListingQuizResponse +from app.schemas.requests import QuizCreateSchema class QuizMapper: diff --git a/app/mapper/user_mapper.py b/app/mapper/user_mapper.py index 867ec4e..9bb2354 100644 --- a/app/mapper/user_mapper.py +++ b/app/mapper/user_mapper.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Dict, Optional -from models import UserEntity, UserResponseModel -from schemas import RegisterSchema +from app.models import UserEntity, UserResponseModel +from app.schemas import RegisterSchema class UserMapper: diff --git a/app/models/entities/session_entity.py b/app/models/entities/session_entity.py index 9e00caa..faf8a04 100644 --- a/app/models/entities/session_entity.py +++ b/app/models/entities/session_entity.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import List, Optional from pydantic import BaseModel, Field -from models.entities import PyObjectId +from app.models.entities import PyObjectId class SessionEntity(BaseModel): diff --git a/app/models/entities/subject_entity.py b/app/models/entities/subject_entity.py index 2dfd9bd..3d6b8bd 100644 --- a/app/models/entities/subject_entity.py +++ b/app/models/entities/subject_entity.py @@ -1,7 +1,7 @@ from typing import Optional from bson import ObjectId from pydantic import BaseModel, Field -from models.entities import PyObjectId +from app.models.entities import PyObjectId class SubjectEntity(BaseModel): diff --git a/app/repositories/answer_repository.py b/app/repositories/answer_repository.py index 8039dd4..c2acefc 100644 --- a/app/repositories/answer_repository.py +++ b/app/repositories/answer_repository.py @@ -1,7 +1,7 @@ from pymongo.collection import Collection from bson import ObjectId from typing import Optional, List -from models import UserAnswerEntity +from app.models import UserAnswerEntity class UserAnswerRepository: diff --git a/app/repositories/quiz_repositroy.py b/app/repositories/quiz_repositroy.py index 06dd7a9..88e1368 100644 --- a/app/repositories/quiz_repositroy.py +++ b/app/repositories/quiz_repositroy.py @@ -1,10 +1,8 @@ from bson import ObjectId from typing import List, Optional -from models import QuizEntity +from app.models.entities import QuizEntity from pymongo.database import Database from pymongo.collection import Collection -from datetime import datetime -from helpers import DatetimeUtil class QuizRepository: diff --git a/app/repositories/session_repostory.py b/app/repositories/session_repostory.py index ccca502..e8f2683 100644 --- a/app/repositories/session_repostory.py +++ b/app/repositories/session_repostory.py @@ -1,7 +1,7 @@ from pymongo.collection import Collection from pymongo.database import Database from typing import Optional -from models.entities import SessionEntity +from app.models.entities import SessionEntity class SessionRepository: @@ -30,7 +30,7 @@ class SessionRepository: """Update specific fields using $set""" result = self.collection.update_one( {"_id": session_id}, - {"$set": update_fields.model_dump(exclude_unset=True)}, + {"$set": update_fields}, ) return result.modified_count > 0 diff --git a/app/repositories/subject_repository.py b/app/repositories/subject_repository.py index bf0c99b..7ad08cf 100644 --- a/app/repositories/subject_repository.py +++ b/app/repositories/subject_repository.py @@ -2,7 +2,7 @@ from typing import List, Optional from pymongo.database import Database from pymongo.collection import Collection from bson import ObjectId, errors as bson_errors -from models.entities import SubjectEntity +from app.models.entities import SubjectEntity class SubjectRepository: diff --git a/app/repositories/user_repository.py b/app/repositories/user_repository.py index edf3d08..1303ea1 100644 --- a/app/repositories/user_repository.py +++ b/app/repositories/user_repository.py @@ -1,6 +1,6 @@ from typing import Optional from bson import ObjectId -from models import UserEntity +from app.models.entities import UserEntity class UserRepository: diff --git a/app/schemas/response/quiz/quiz_data_rsp_schema.py b/app/schemas/response/quiz/quiz_data_rsp_schema.py index 73424a5..3d61ba2 100644 --- a/app/schemas/response/quiz/quiz_data_rsp_schema.py +++ b/app/schemas/response/quiz/quiz_data_rsp_schema.py @@ -1,6 +1,6 @@ from typing import List from pydantic import BaseModel -from schemas.response.recomendation.recomendation_response_schema import ( +from app.schemas.response.recomendation.recomendation_response_schema import ( ListingQuizResponse, ) diff --git a/app/services/answer_service.py b/app/services/answer_service.py index 9b46f2a..559b915 100644 --- a/app/services/answer_service.py +++ b/app/services/answer_service.py @@ -1,8 +1,8 @@ -from repositories import UserAnswerRepository, QuizRepository, UserRepository -from schemas.requests import UserAnswerSchema -from models import UserAnswerEntity -from models.entities import AnswerItemEntity -from exception import ValidationException +from app.repositories import UserAnswerRepository, QuizRepository, UserRepository +from app.schemas.requests import UserAnswerSchema +from app.models import UserAnswerEntity +from app.models.entities import AnswerItemEntity +from app.exception import ValidationException class AnswerService: diff --git a/app/services/auth_service.py b/app/services/auth_service.py index 2ad8707..57b6ba3 100644 --- a/app/services/auth_service.py +++ b/app/services/auth_service.py @@ -1,10 +1,10 @@ -from schemas import LoginSchema -from repositories import UserRepository -from mapper import UserMapper +from app.schemas import LoginSchema +from app.repositories import UserRepository +from app.mapper import UserMapper from google.oauth2 import id_token from google.auth.transport import requests -from configs import Config -from exception import AuthException +from app.configs import Config +from app.exception import AuthException from werkzeug.security import check_password_hash diff --git a/app/services/history_service.py b/app/services/history_service.py index 57ab51f..0c6ab3d 100644 --- a/app/services/history_service.py +++ b/app/services/history_service.py @@ -1,5 +1,5 @@ -from repositories import UserAnswerRepository, QuizRepository -from schemas.response import HistoryResultSchema, QuizHistoryResponse, QuestionResult +from app.repositories import UserAnswerRepository, QuizRepository +from app.schemas.response import HistoryResultSchema, QuizHistoryResponse, QuestionResult class HistoryService: diff --git a/app/services/quiz_service.py b/app/services/quiz_service.py index 411dc83..bb43f73 100644 --- a/app/services/quiz_service.py +++ b/app/services/quiz_service.py @@ -1,10 +1,13 @@ -from repositories import QuizRepository, UserRepository, SubjectRepository -from schemas.requests import QuizCreateSchema -from schemas.response import UserQuizListResponse, ListingQuizResponse, QuizGetSchema -from exception import DataNotFoundException -from mapper import QuizMapper -from exception import ValidationException -from helpers import DatetimeUtil +from app.repositories import QuizRepository, UserRepository, SubjectRepository +from app.schemas.requests import QuizCreateSchema +from app.schemas.response import ( + UserQuizListResponse, + ListingQuizResponse, + QuizGetSchema, +) +from app.exception import DataNotFoundException, ValidationException +from app.mapper import QuizMapper +from app.helpers import DatetimeUtil class QuizService: @@ -43,7 +46,7 @@ class QuizService: if author is None: continue mapped_quizzes.append( - QuizMapper.quiz_to_recomendation_mapper( + QuizMapper.quiz_to_recomendation_app.mapper( quiz_entity=quiz, user_entity=author, ) @@ -65,7 +68,7 @@ class QuizService: user = self.user_repostory.get_user_by_id(user_id) quiz_data = [ - QuizMapper.quiz_to_recomendation_mapper(quiz, user) for quiz in quizzes + QuizMapper.quiz_to_recomendation_app.mapper(quiz, user) for quiz in quizzes ] return UserQuizListResponse(total=total_user_quiz, quizzes=quiz_data) @@ -107,7 +110,7 @@ class QuizService: for quiz in data: author = self.user_repostory.get_user_by_id(user_id=quiz.author_id) result.append( - QuizMapper.quiz_to_recomendation_mapper( + QuizMapper.quiz_to_recomendation_app.mapper( quiz_entity=quiz, user_entity=author, ) diff --git a/app/services/session_service.py b/app/services/session_service.py index 0f04193..1887ec5 100644 --- a/app/services/session_service.py +++ b/app/services/session_service.py @@ -1,9 +1,8 @@ -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 +from app.repositories import SessionRepository, UserRepository +from app.models.entities import SessionEntity +from app.helpers import DatetimeUtil class SessionService: @@ -11,11 +10,10 @@ class SessionService: self.repository = repository self.user_repository = user_repository - def create_session( - self, quiz_id: str, host_id: str, limit_participan: int - ) -> SessionEntity: + def create_session(self, quiz_id: str, host_id: str, limit_participan: int) -> str: + generateed_code = uuid4().hex[:6].upper() session = SessionEntity( - session_code=uuid4().hex[:6].upper(), + session_code=generateed_code, quiz_id=quiz_id, host_id=host_id, created_at=DatetimeUtil.now_iso(), @@ -24,36 +22,59 @@ class SessionService: 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]: + return { + "session_id": self.repository.insert(session), + "session_code": generateed_code, + } + + def join_session(self, session_code: str, user_id: str) -> dict: 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"]: + if session is None or session.is_active == False: return None - if user_id not in session["participants"]: - session["participants"].append(user_id) - self.repository.update( - session.id, {"participants": session["participants"]} - ) + if session.host_id == user_id: + return {"is_admin": True, "message": "admin joined"} + + 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, + "is_admin": False, + "user_id": str(user.id), + "username": user.name, "user_pic": user.pic_url, - "session_id": session.id, + "session_id": str(session.id), } return response + def leave_session(self, session_id: str, user_id: str) -> dict: + session = self.repository.get_by_id(session_id) + + if session is None: + return {"error": "Session not found"} + + if user_id == session.host_id: + return {"message": "Host cannot leave the session"} + + if user_id in session.participants: + session.participants.remove(user_id) + self.repository.update(session.id, {"participants": session.participants}) + return {"message": "User has left the session"} + + return {"message": "User not in session"} + 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 end_session(self, session_id: str, user_id: str): + session = self.repository.find_by_id(session_id) + if session and session.host_id == user_id: + session.is_active = False + self.repository.update(session_id, {"is_active": False}) def advance_question(self, session_id: str) -> Optional[int]: session = self.repository.find_by_session_id(session_id) diff --git a/app/services/subject_service.py b/app/services/subject_service.py index 8cf2f35..e4644d7 100644 --- a/app/services/subject_service.py +++ b/app/services/subject_service.py @@ -1,8 +1,8 @@ from typing import List, Optional -from models.entities import SubjectEntity -from schemas.requests import SubjectCreateRequest, SubjectUpdateRequest -from schemas.response import GetSubjectResponse -from repositories import SubjectRepository +from app.models.entities import SubjectEntity +from app.schemas.requests import SubjectCreateRequest, SubjectUpdateRequest +from app.schemas.response import GetSubjectResponse +from app.repositories import SubjectRepository class SubjectService: diff --git a/app/services/user_service.py b/app/services/user_service.py index 760f848..d8902ac 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -1,8 +1,8 @@ from flask import current_app -from repositories import UserRepository -from schemas import RegisterSchema -from mapper import UserMapper -from exception import AlreadyExistException +from app.repositories import UserRepository +from app.schemas import RegisterSchema +from app.mapper import UserMapper +from app.exception import AlreadyExistException from werkzeug.security import generate_password_hash diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b8757d1 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +# pytest.ini +[pytest] +pythonpath = . diff --git a/run.py b/run.py new file mode 100644 index 0000000..33ad077 --- /dev/null +++ b/run.py @@ -0,0 +1,8 @@ +# run.py +from app.main import createApp, socketio + +app = createApp() + +# if __name__ == "__main__": +# # Untuk dev/testing +# socketio.run(app, host="0.0.0.0", port=5000, debug=True) diff --git a/test/service/__pycache__/test_quiz_service.cpython-310-pytest-8.3.4.pyc b/test/service/__pycache__/test_quiz_service.cpython-310-pytest-8.3.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64f33c988363265170de9e60d906b9236c6c15bc GIT binary patch literal 5070 zcmb7ITXWmS6~^L55Cla^mgH-i#Ep|MZKcXdJDoO@y7eV>TXWLLNn1>(D2!c8puvlG z!MM`UPtNqIp8f@@Z~8C&1$ga~|3YUv$vJ092$X2MB?o6O++AS5{myp|IBYh313v%y z+vWY0Rm1ox1Lx<^K+|Y$z%&gY;LXY~atlsxS zpXzqj=m%jybth}~SHcylyIHH>4%_|JaFrRK8ob86uMO@Ax3~7p3eSmkUVmiDTf&`g z9KroK+oh}Hzknw)x*b;3@5Os*a&MSCf=1(a$gk2wK(md_n^MHN*iH6DKZd^b zFO-nj|JzjMp9?h_4wUGa#m22TkGsSC&Tu^7xBr@mQJxM5#knuzEafrQ%gzMvUHzEQ zxB-{S!3e@AG&$qu*G6b@i`!7z+~F>ij@~1nRp&mmYrMe&C_UcfD^S*Xi?^Zl`6^$7 zvcb>sbtnVA!8f67^7H%xlq>uqzXWBAU*0u3SBlnsq4LW169&&R|6tvL>n2?4I+)ye zW=xET%}R4tTDd(jdJHZTE{n707L0PZ0k<%_9V=`-2IL|sMnje6LwOLk#sFuvV@-`8 z^hAv$d$9N6vb-MhAtXME=DHy+F0WcnrU$X*2u= z|2^C|h3XcKU%(cf1fQC){WVl_peQ$=v!{gaQ*(lNvYr`P6GYXfn$dHnF1L=@Q$ToH zn?Q}hiQu@YM^s15?Jhha;JMRbp+Cndq7 zJ-7(+J5b>W9e@`Bz*8Xc5&K7r;6#uaLi>9dW5f5Eg}SqA>=^3G6X?h9A5mE}#X~fuE=`~@l})SIxql6D*@2E?)>OS^f`>A65gqgR`H9CYc( z{FC|ABqG))pjSsGVmI~k#>9Naxcdn3oFP=}7BzHT;NCF{>+v|>A4D$y;A732}A1yNh`xG`Id zsNB$}a{KW(&I(^Q?#r<#tV|4!HHYPSe5eb!ppy^tI6Hk(7wDua%g$;04$PU+HbBy{ zH8WtF?C|}i2exE^m!H@lVCmPL7%}@*=Vk60v-2v?j=1c0)MMKQ~F4hnN`5u~6^nUXUXfs0eQ>D_u-WJTMVCNHd;PTtA#cpw( z1qdxzv~>Ox2)VUqa{IZ7s*B|34k$3n0n5!&4y>s?f!0zvxOPSk+-~6xbsE6pf<1sN z!OjmxqNwR!1un4qcno3(*+G;+J^_1H9Y>>k2?492A?a--l!uUvb}TKd-h6V=ps1xN zNjZ)3HA1;EJ$jPn`_WveX3z2y#2Qbp4IB+j5|tk<#rbtBEuf9^Qf3afpn-(lgGBG2 z3=^B@)*{~A)iI3Z#hyBdHVKOh!r~%PiJY~r{2r_Y0p|Nq$RB_ySW+KuN7eH*A zLuz%km!s2nniXJ3KtVf6LsL!wh3!&n=iQ5rUo=bEC5>%sS-q?74pbgPZXl5qS{dGl z+ObKDUJC3b0(N1&xww7`QxzW83Q21K(z?bDKUfOy(gj`)@sDBEy-Hfp1K-C~fPi!W zJeQj@D6v)3GD_?OC`mdghIu*(_!%r$;nv64L~cpkz0SuDAt(3*yH2U34lRq1FZ!eW z;4>Ip$pT>K%Y(|2sSEo?MJ%a>>Cp0^G7J88&mr%0r#1B8BMALKEibQAuy-oK{!9pY zK_nqqDj}rU#0SiWqiB@H2ar1=V-O6ADyMjL9>L?G=xd~4~(s}u0N=O5fAx*CG7(D%=7^F&_C4O-p%5@oL3 z3>TTmMRhZ}=YQHvW#~RWhTaUmL%d`&l?mNWyYQlD5cfqC6@C=;hkOi)cvJ)t+&s?o zFB1LTvWl;|{gmy3_gtv5Tf*fp1 ziYGG7MNu!iirT!-hsi{USQg%RkmlG3oAcUMAO1q3uo(vNXaq0%^cRGpeZ0^a z!CfeCQxZk@^iTQ^B?^yW6K9F}e@Xg<5i`QVL*m_x;U0*K#B=~(p3=cG> 0) + + def test_search_quiz_author_missing(self): + fake_quiz = MagicMock(author_id="user123") + self.quiz_repo.search_by_title_or_category.return_value = [fake_quiz] + self.quiz_repo.count_by_search.return_value = 1 + self.user_repo.get_user_by_id.return_value = None # simulate missing author + + result, total = self.service.search_quiz("math", "subj1") + self.assertEqual(result, []) # filtered out + self.assertEqual(total, 1) + + def test_create_quiz_with_invalid_options(self): + question = MagicMock(type="option", options=["a", "b"]) # only 2 options + quiz_schema = MagicMock(question_listings=[question]) + + with self.assertRaises(ValidationException): + self.service.create_quiz(quiz_schema) + + def test_create_quiz_valid(self): + question = MagicMock(type="option", options=["a", "b", "c", "d"], duration=30) + quiz_schema = MagicMock(question_listings=[question]) + self.quiz_repo.create.return_value = "quiz_id_123" + + result = self.service.create_quiz(quiz_schema) + self.assertEqual(result, "quiz_id_123") + + def test_get_user_quiz_success(self): + self.quiz_repo.get_by_user_id.return_value = [MagicMock()] + self.quiz_repo.count_by_user_id.return_value = 1 + self.user_repo.get_user_by_id.return_value = MagicMock() + + result = self.service.get_user_quiz("user123") + self.assertIsInstance(result, UserQuizListResponse) + self.assertEqual(result.total, 1) + + def test_get_user_quiz_empty(self): + self.quiz_repo.get_by_user_id.return_value = [] + result = self.service.get_user_quiz("user123") + self.assertEqual(result.total, 0) + self.assertEqual(result.quizzes, []) + + def test_get_quiz_recommendation_success(self): + quiz = MagicMock(author_id="user123") + self.quiz_repo.get_top_played_quizzes.return_value = [quiz] + self.user_repo.get_user_by_id.return_value = MagicMock() + + result = self.service.get_quiz_recommendation(1, 10) + self.assertTrue(len(result) > 0) + + def test_get_quiz_recommendation_empty(self): + self.quiz_repo.get_top_played_quizzes.return_value = [] + with self.assertRaises(DataNotFoundException): + self.service.get_quiz_recommendation(1, 10) + + def test_update_quiz(self): + self.quiz_repo.update.return_value = True + result = self.service.update_quiz("quiz_id", {"title": "updated"}) + self.assertTrue(result) + + def test_delete_quiz(self): + self.quiz_repo.delete.return_value = True + result = self.service.delete_quiz("quiz_id") + self.assertTrue(result) + + +if __name__ == "__main__": + unittest.main()