fix: answer input

This commit is contained in:
akhdanre 2025-04-30 15:21:33 +07:00
parent bd3db93279
commit 0773609fda
21 changed files with 212 additions and 48 deletions

View File

@ -51,9 +51,9 @@ def get_quiz_recommendation(
@quiz_bp.route("/user/<user_id>", methods=["GET"]) @quiz_bp.route("/user/<user_id>", methods=["GET"])
@inject @inject
def get_user_quiz(controller: QuizController = Provide[Container.quiz_controller]): def get_user_quiz(
user_id: str, controller: QuizController = Provide[Container.quiz_controller]
):
page = request.args.get("page", default=1, type=int) page = request.args.get("page", default=1, type=int)
page_size = request.args.get("page_size", default=10, type=int) page_size = request.args.get("page_size", default=10, type=int)
return controller.get_user_quiz( return controller.get_user_quiz(user_id=user_id, page=page, page_size=page_size)
user_id=request.view_args["user_id"], page=page, page_size=page_size
)

View File

@ -1,9 +1,11 @@
from flask import jsonify import json
from pydantic import ValidationError from pydantic import ValidationError
from schemas.requests import QuizCreateSchema, UserAnswerSchema from schemas.requests import QuizCreateSchema, UserAnswerSchema
from schemas.response import QuizCreationResponse from schemas.response import QuizCreationResponse
from schemas import MetaSchema
from services import QuizService, AnswerService from services import QuizService, AnswerService
from helpers import make_response, make_error_response from helpers import make_response, make_error_response
from exception import ValidationException
class QuizController: class QuizController:
@ -29,7 +31,7 @@ class QuizController:
data=QuizCreationResponse(quiz_id=quiz_id), data=QuizCreationResponse(quiz_id=quiz_id),
status_code=201, status_code=201,
) )
except ValidationError as e: except (ValidationError, ValidationException) as e:
return make_response(e.errors(), status_code=400) return make_response(e.errors(), status_code=400)
except Exception as e: except Exception as e:
return make_error_response(e) return make_error_response(e)
@ -46,11 +48,11 @@ class QuizController:
# except ValidationError as e: # except ValidationError as e:
# return jsonify({"error": "Validation error", "detail": e.errors()}), 400 # return jsonify({"error": "Validation error", "detail": e.errors()}), 400
def delete_quiz(self, quiz_id): # def delete_quiz(self, quiz_id):
success = self.quiz_service.delete_quiz(quiz_id) # success = self.quiz_service.delete_quiz(quiz_id)
if not success: # if not success:
return jsonify({"error": "Quiz not found"}), 400 # return jsonify({"error": "Quiz not found"}), 400
return jsonify({"message": "Quiz deleted"}), 200 # return jsonify({"message": "Quiz deleted"}), 200
def quiz_recomendation(self): def quiz_recomendation(self):
try: try:
@ -63,16 +65,21 @@ class QuizController:
def submit_answer(self, answer_data): def submit_answer(self, answer_data):
try: try:
# Assuming answer_data is a dictionary with the necessary fields
answer_obj = UserAnswerSchema(**answer_data) answer_obj = UserAnswerSchema(**answer_data)
answer_id = self.answer_service.create_answer(answer_obj) answer_id = self.answer_service.create_answer(answer_obj)
return make_response( return make_response(
message="Answer submitted", message="Answer submitted",
data={"answer_id": answer_id}, data={"answer_id": True},
status_code=201, status_code=201,
) )
except ValidationError as e: except ValidationError as e:
return make_response(e.errors(), status_code=400) return make_response(
message="validation error", data=json.loads(e.json()), status_code=400
)
except ValidationException as e:
return make_response(
message=f"validation issue {e.message}", status_code=400
)
except Exception as e: except Exception as e:
return make_error_response(e) return make_error_response(e)
@ -89,7 +96,11 @@ class QuizController:
user_id=user_id, page=page, page_size=page_size user_id=user_id, page=page, page_size=page_size
) )
return make_response( return make_response(
message="User quizzes retrieved successfully", data=result message="User quizzes retrieved successfully",
data=result.quizzes,
page=page,
page_size=page_size,
total_all_data=result.total,
) )
except Exception as e: except Exception as e:
return make_error_response(e) return make_error_response(e)

View File

@ -18,7 +18,12 @@ class Container(containers.DeclarativeContainer):
auth_service = providers.Factory(AuthService, user_repository) auth_service = providers.Factory(AuthService, user_repository)
user_service = providers.Factory(UserService, user_repository) user_service = providers.Factory(UserService, user_repository)
quiz_service = providers.Factory(QuizService, quiz_repository) quiz_service = providers.Factory(QuizService, quiz_repository)
answer_service = providers.Factory(AnswerService, answer_repository) answer_service = providers.Factory(
AnswerService,
answer_repository,
quiz_repository,
user_repository,
)
# controllers # controllers
auth_controller = providers.Factory(AuthController, user_service, auth_service) auth_controller = providers.Factory(AuthController, user_service, auth_service)

View File

@ -1,10 +1,12 @@
from .auth_exception import AuthException from .auth_exception import AuthException
from .already_exist_exception import AlreadyExistException from .already_exist_exception import AlreadyExistException
from .data_not_found_exception import DataNotFoundException from .data_not_found_exception import DataNotFoundException
from .validation_exception import ValidationException
__all__ = [ __all__ = [
"AuthException", "AuthException",
"AlreadyExistException", "AlreadyExistException",
"DataNotFoundException", "DataNotFoundException",
"ValidationException",
] ]

View File

@ -8,3 +8,6 @@ class BaseExceptionTemplate(Exception):
def __str__(self): def __str__(self):
return f"{self.__class__.__name__}: {self.message}" return f"{self.__class__.__name__}: {self.message}"
def json(self):
return {"error": self.__class__.__name__, "message": self.message}

View File

@ -0,0 +1,8 @@
from .base_exception import BaseExceptionTemplate
class ValidationException(BaseExceptionTemplate):
"""Exception for validation"""
def __init__(self, message: str = "validation error, check yout input"):
super().__init__(message, status_code=400)

View File

@ -1,15 +1,37 @@
from flask import jsonify, current_app from flask import jsonify, current_app
from typing import Optional, Union from typing import Optional, Union
from schemas import ResponseSchema from schemas import ResponseSchema, MetaSchema
import math
def calculate_total_page(total_data: int, page_size: int) -> int:
if not page_size or page_size <= 0:
return 1
return math.ceil(total_data / page_size)
def make_response( def make_response(
message: str, message: str,
data: Optional[dict] = None, data: Optional[Union[dict, list]] = None,
meta: Optional[dict] = None, page: Optional[int] = None,
page_size: Optional[int] = None,
total_all_data: Optional[int] = None,
status_code: int = 200, status_code: int = 200,
): ):
response = ResponseSchema(message=message, data=data, meta=meta) meta = None
if page is not None and page_size is not None and total_all_data is not None:
meta = MetaSchema(
current_page=page,
total_all_data=total_all_data,
total_data=len(data) if data else 0,
total_page=calculate_total_page(total_all_data, page_size),
)
response = ResponseSchema(
message=message,
data=data,
meta=meta,
)
return jsonify(response.model_dump()), status_code return jsonify(response.model_dump()), status_code

View File

@ -4,10 +4,12 @@ from schemas import QuizGetSchema, QuestionItemSchema
def map_question_entity_to_schema(entity: QuestionItemEntity) -> QuestionItemSchema: def map_question_entity_to_schema(entity: QuestionItemEntity) -> QuestionItemSchema:
return QuestionItemSchema( return QuestionItemSchema(
index=entity.index,
question=entity.question, question=entity.question,
target_answer=entity.target_answer, target_answer=entity.target_answer,
duration=entity.duration, duration=entity.duration,
type=entity.type, type=entity.type,
options=entity.options,
) )

View File

@ -3,6 +3,7 @@ from .base import PyObjectId
from .quiz_entity import QuizEntity from .quiz_entity import QuizEntity
from .question_item_entity import QuestionItemEntity from .question_item_entity import QuestionItemEntity
from .user_answer_entity import UserAnswerEntity from .user_answer_entity import UserAnswerEntity
from .answer_item import AnswerItemEntity
__all__ = [ __all__ = [
"UserEntity", "UserEntity",

View File

@ -3,9 +3,6 @@ from pydantic import BaseModel
class AnswerItemEntity(BaseModel): class AnswerItemEntity(BaseModel):
question_index: int question_index: int
question: str
answer: str answer: str
correct_answer: str
is_correct: bool is_correct: bool
duration: int
time_spent: float time_spent: float

View File

@ -1,12 +1,11 @@
from typing import Optional, List from typing import Optional, List
from pydantic import BaseModel from pydantic import BaseModel
from datetime import datetime
from .base import PyObjectId
class QuestionItemEntity(BaseModel): class QuestionItemEntity(BaseModel):
_id: Optional[PyObjectId] = None index: int
question: str question: str
target_answer: str target_answer: str
duration: int duration: int
type: str # "isian" | "true_false" type: str
options: Optional[List[str]] = None

View File

@ -9,8 +9,8 @@ from .base import PyObjectId
class UserAnswerEntity(BaseModel): class UserAnswerEntity(BaseModel):
_id: Optional[PyObjectId] = None _id: Optional[PyObjectId] = None
session_id: Optional[PyObjectId] session_id: Optional[str]
quiz_id: PyObjectId quiz_id: str
user_id: str user_id: str
answered_at: datetime answered_at: datetime
answers: List[AnswerItemEntity] answers: List[AnswerItemEntity]

View File

@ -2,6 +2,7 @@ from bson import ObjectId
from typing import List, Optional from typing import List, Optional
from models import QuizEntity from models import QuizEntity
from pymongo.database import Database from pymongo.database import Database
from datetime import datetime
class QuizRepository: class QuizRepository:
@ -24,11 +25,20 @@ class QuizRepository:
) -> List[QuizEntity]: ) -> List[QuizEntity]:
skip = (page - 1) * page_size skip = (page - 1) * page_size
cursor = ( cursor = (
self.collection.find({"user_id": ObjectId(user_id)}) self.collection.find({"author_id": user_id}).skip(skip).limit(page_size)
.skip(skip)
.limit(page_size)
) )
return [QuizEntity(**doc) for doc in cursor]
quiz_list = []
for doc in cursor:
if "date" in doc and isinstance(doc["date"], str):
try:
doc["date"] = datetime.strptime(doc["date"], "%d-%m-%Y")
except ValueError:
doc["date"] = None
quiz_list.append(QuizEntity(**doc))
return quiz_list
def get_all(self, skip: int = 0, limit: int = 10) -> List[QuizEntity]: def get_all(self, skip: int = 0, limit: int = 10) -> List[QuizEntity]:
cursor = self.collection.find().skip(skip).limit(limit) cursor = self.collection.find().skip(skip).limit(limit)
@ -43,3 +53,6 @@ class QuizRepository:
def delete(self, quiz_id: str) -> bool: def delete(self, quiz_id: str) -> bool:
result = self.collection.delete_one({"_id": ObjectId(quiz_id)}) result = self.collection.delete_one({"_id": ObjectId(quiz_id)})
return result.deleted_count > 0 return result.deleted_count > 0
def count_by_user_id(self, user_id: str) -> int:
return self.collection.count_documents({"author_id": user_id})

View File

@ -3,9 +3,6 @@ from pydantic import BaseModel
class AnswerItemSchema(BaseModel): class AnswerItemSchema(BaseModel):
question_index: int question_index: int
question: str
answer: str answer: str
correct_answer: str
is_correct: bool is_correct: bool
duration: int
time_spent: float time_spent: float

View File

@ -5,11 +5,8 @@ from .answer_item_request_schema import AnswerItemSchema
class UserAnswerSchema(BaseModel): class UserAnswerSchema(BaseModel):
session_id: Optional[str] = None
quiz_id: str quiz_id: str
user_id: str user_id: str
session_id: Optional[str] = None
answered_at: datetime answered_at: datetime
total_score: int
total_correct: int
total_questions: int
answers: List[AnswerItemSchema] answers: List[AnswerItemSchema]

View File

@ -3,6 +3,7 @@ from pydantic import BaseModel
class QuestionItemSchema(BaseModel): class QuestionItemSchema(BaseModel):
index: int
question: str question: str
target_answer: str target_answer: str
duration: int duration: int

View File

@ -1,9 +1,11 @@
from .quiz.quiz_creation_response import QuizCreationResponse from .quiz.quiz_creation_response import QuizCreationResponse
from .quiz.quiz_get_response import QuizGetSchema from .quiz.quiz_get_response import QuizGetSchema
from .quiz.question_item_schema import QuestionItemSchema from .quiz.question_item_schema import QuestionItemSchema
from .quiz.quiz_data_rsp_schema import UserQuizListResponse
__all__ = [ __all__ = [
"QuizCreationResponse", "QuizCreationResponse",
"QuizGetSchema", "QuizGetSchema",
"QuestionItemSchema", "QuestionItemSchema",
"UserQuizListResponse"
] ]

View File

@ -1,8 +1,11 @@
from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
class QuestionItemSchema(BaseModel): class QuestionItemSchema(BaseModel):
index: int
question: str question: str
target_answer: str target_answer: str
duration: int duration: int
type: str type: str
options: Optional[List[str]] = None

View File

@ -0,0 +1,8 @@
from typing import List
from pydantic import BaseModel
from schemas.response import QuizGetSchema
class UserQuizListResponse(BaseModel):
total: int
quizzes: List[QuizGetSchema]

View File

@ -1,9 +1,20 @@
from repositories import UserAnswerRepository from repositories import UserAnswerRepository, QuizRepository, UserRepository
from schemas.requests import UserAnswerSchema
from models import UserAnswerEntity
from models.entities import AnswerItemEntity
from exception import ValidationException
class AnswerService: class AnswerService:
def __init__(self, answer_repository: UserAnswerRepository): def __init__(
self,
answer_repository: UserAnswerRepository,
quiz_repository: QuizRepository,
user_repositroy: UserRepository,
):
self.answer_repository = answer_repository self.answer_repository = answer_repository
self.quiz_repository = quiz_repository
self.user_repositroy = user_repositroy
def get_answer_by_id(self, answer_id): def get_answer_by_id(self, answer_id):
return self.answer_repository.get_answer_by_id(answer_id) return self.answer_repository.get_answer_by_id(answer_id)
@ -12,8 +23,76 @@ class AnswerService:
if quiz_id is not None: if quiz_id is not None:
return self.answer_repository return self.answer_repository
def create_answer(self, answer_data): def create_answer(self, answer_data: UserAnswerSchema):
return self.answer_repository.create(answer_data) quiz_data = self.quiz_repository.get_by_id(answer_data.quiz_id)
user_data = self.user_repositroy.get_user_by_id(answer_data.user_id)
if not user_data:
raise ValidationException(message="user is not registered")
question_map = {q.index: q for q in quiz_data.question_listings}
answer_item_Entity = []
total_correct = 0
for user_answer in answer_data.answers:
question = question_map.get(user_answer.question_index)
if question is None:
raise ValueError(
f"Question index {user_answer.question_index} tidak ditemukan di kuis."
)
correct = False
if question.type in ["fill_the_blank", "true_false"]:
correct = (
user_answer.answer.strip().lower()
== question.target_answer.strip().lower()
)
elif question.type == "option":
try:
answer_index = int(user_answer.answer)
if 0 <= answer_index < len(question.options):
correct = str(answer_index) == question.target_answer
else:
raise ValueError(
f"Index jawaban tidak valid untuk soal {question.index}"
)
except ValueError:
raise ValueError(
f"Jawaban bukan index valid untuk soal {question.index}"
)
else:
raise ValueError(f"Tipe soal tidak dikenali: {question.type}")
user_answer.is_correct = correct
if correct:
total_correct += 1
answer_item_Entity.append(
AnswerItemEntity(
question_index=user_answer.question_index,
answer=user_answer.answer,
is_correct=user_answer.is_correct,
time_spent=user_answer.time_spent,
)
)
total_questions = len(quiz_data.question_listings)
total_score = (
total_correct * 100 // total_questions
) # contoh perhitungan: nilai 100 dibagi rata
# Buat entitas yang akan disimpan
answer_entity = UserAnswerEntity(
session_id=answer_data.session_id,
quiz_id=answer_data.quiz_id,
user_id=answer_data.user_id,
answered_at=answer_data.answered_at,
answers=answer_item_Entity,
total_correct=total_correct,
total_questions=total_questions,
total_score=total_score,
)
self.answer_repository.create(answer_entity)
return True
def update_answer(self, answer_id, answer_data): def update_answer(self, answer_id, answer_data):
return self.answer_repository.update(answer_id, answer_data) return self.answer_repository.update(answer_id, answer_data)

View File

@ -1,8 +1,11 @@
from typing import List from typing import List
from repositories import QuizRepository from repositories import QuizRepository
from schemas import QuizGetSchema from schemas import QuizGetSchema
from schemas.requests import QuizCreateSchema
from schemas.response import UserQuizListResponse
from exception import DataNotFoundException from exception import DataNotFoundException
from mapper import map_quiz_entity_to_schema from mapper import map_quiz_entity_to_schema
from exception import ValidationException
class QuizService: class QuizService:
@ -13,16 +16,27 @@ class QuizService:
data = self.quiz_repository.get_by_id(quiz_id) data = self.quiz_repository.get_by_id(quiz_id)
if data is None: if data is None:
raise DataNotFoundException("Quiz not found") raise DataNotFoundException("Quiz not found")
return map_quiz_entity_to_schema(data) return map_quiz_entity_to_schema(data)
def get_user_quiz( def get_user_quiz(
self, user_id: str, page: int = 1, page_size: int = 10 self, user_id: str, page: int = 1, page_size: int = 10
) -> List[QuizGetSchema]: ) -> UserQuizListResponse:
quizzes = self.quiz_repository.get_by_user_id(user_id, page, page_size) quizzes = self.quiz_repository.get_by_user_id(user_id, page, page_size)
return [QuizGetSchema.model_validate(quiz) for quiz in quizzes] total_user_quiz = self.quiz_repository.count_by_user_id(user_id)
return UserQuizListResponse(
total=total_user_quiz,
quizzes=[map_quiz_entity_to_schema(quiz) for quiz in quizzes],
)
def create_quiz(self, quiz_data: QuizCreateSchema):
for question in quiz_data.question_listings:
if question.type == "option" and (
not question.options or len(question.options) != 4
):
raise ValidationException(
"Option type questions must have exactly 4 options."
)
def create_quiz(self, quiz_data):
return self.quiz_repository.create(quiz_data) return self.quiz_repository.create(quiz_data)
def update_quiz(self, quiz_id, quiz_data): def update_quiz(self, quiz_id, quiz_data):