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"])
@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_size = request.args.get("page_size", default=10, type=int)
return controller.get_user_quiz(
user_id=request.view_args["user_id"], page=page, page_size=page_size
)
return controller.get_user_quiz(user_id=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 schemas.requests import QuizCreateSchema, UserAnswerSchema
from schemas.response import QuizCreationResponse
from schemas import MetaSchema
from services import QuizService, AnswerService
from helpers import make_response, make_error_response
from exception import ValidationException
class QuizController:
@ -29,7 +31,7 @@ class QuizController:
data=QuizCreationResponse(quiz_id=quiz_id),
status_code=201,
)
except ValidationError as e:
except (ValidationError, ValidationException) as e:
return make_response(e.errors(), status_code=400)
except Exception as e:
return make_error_response(e)
@ -46,11 +48,11 @@ class QuizController:
# except ValidationError as e:
# return jsonify({"error": "Validation error", "detail": e.errors()}), 400
def delete_quiz(self, quiz_id):
success = self.quiz_service.delete_quiz(quiz_id)
if not success:
return jsonify({"error": "Quiz not found"}), 400
return jsonify({"message": "Quiz deleted"}), 200
# def delete_quiz(self, quiz_id):
# success = self.quiz_service.delete_quiz(quiz_id)
# if not success:
# return jsonify({"error": "Quiz not found"}), 400
# return jsonify({"message": "Quiz deleted"}), 200
def quiz_recomendation(self):
try:
@ -63,16 +65,21 @@ class QuizController:
def submit_answer(self, answer_data):
try:
# Assuming answer_data is a dictionary with the necessary fields
answer_obj = UserAnswerSchema(**answer_data)
answer_id = self.answer_service.create_answer(answer_obj)
return make_response(
message="Answer submitted",
data={"answer_id": answer_id},
data={"answer_id": True},
status_code=201,
)
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:
return make_error_response(e)
@ -89,7 +96,11 @@ class QuizController:
user_id=user_id, page=page, page_size=page_size
)
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:
return make_error_response(e)

View File

@ -18,7 +18,12 @@ class Container(containers.DeclarativeContainer):
auth_service = providers.Factory(AuthService, user_repository)
user_service = providers.Factory(UserService, user_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
auth_controller = providers.Factory(AuthController, user_service, auth_service)

View File

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

View File

@ -8,3 +8,6 @@ class BaseExceptionTemplate(Exception):
def __str__(self):
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 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(
message: str,
data: Optional[dict] = None,
meta: Optional[dict] = None,
data: Optional[Union[dict, list]] = None,
page: Optional[int] = None,
page_size: Optional[int] = None,
total_all_data: Optional[int] = None,
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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,11 @@
from typing import Optional, List
from pydantic import BaseModel
from datetime import datetime
from .base import PyObjectId
class QuestionItemEntity(BaseModel):
_id: Optional[PyObjectId] = None
index: int
question: str
target_answer: str
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):
_id: Optional[PyObjectId] = None
session_id: Optional[PyObjectId]
quiz_id: PyObjectId
session_id: Optional[str]
quiz_id: str
user_id: str
answered_at: datetime
answers: List[AnswerItemEntity]

View File

@ -2,6 +2,7 @@ from bson import ObjectId
from typing import List, Optional
from models import QuizEntity
from pymongo.database import Database
from datetime import datetime
class QuizRepository:
@ -24,11 +25,20 @@ class QuizRepository:
) -> List[QuizEntity]:
skip = (page - 1) * page_size
cursor = (
self.collection.find({"user_id": ObjectId(user_id)})
.skip(skip)
.limit(page_size)
self.collection.find({"author_id": user_id}).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]:
cursor = self.collection.find().skip(skip).limit(limit)
@ -43,3 +53,6 @@ class QuizRepository:
def delete(self, quiz_id: str) -> bool:
result = self.collection.delete_one({"_id": ObjectId(quiz_id)})
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):
question_index: int
question: str
answer: str
correct_answer: str
is_correct: bool
duration: int
time_spent: float

View File

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

View File

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

View File

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

View File

@ -1,8 +1,11 @@
from typing import List, Optional
from pydantic import BaseModel
class QuestionItemSchema(BaseModel):
index: int
question: str
target_answer: str
duration: int
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:
def __init__(self, answer_repository: UserAnswerRepository):
def __init__(
self,
answer_repository: UserAnswerRepository,
quiz_repository: QuizRepository,
user_repositroy: UserRepository,
):
self.answer_repository = answer_repository
self.quiz_repository = quiz_repository
self.user_repositroy = user_repositroy
def get_answer_by_id(self, answer_id):
return self.answer_repository.get_answer_by_id(answer_id)
@ -12,8 +23,76 @@ class AnswerService:
if quiz_id is not None:
return self.answer_repository
def create_answer(self, answer_data):
return self.answer_repository.create(answer_data)
def create_answer(self, answer_data: UserAnswerSchema):
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):
return self.answer_repository.update(answer_id, answer_data)

View File

@ -1,8 +1,11 @@
from typing import List
from repositories import QuizRepository
from schemas import QuizGetSchema
from schemas.requests import QuizCreateSchema
from schemas.response import UserQuizListResponse
from exception import DataNotFoundException
from mapper import map_quiz_entity_to_schema
from exception import ValidationException
class QuizService:
@ -13,16 +16,27 @@ class QuizService:
data = self.quiz_repository.get_by_id(quiz_id)
if data is None:
raise DataNotFoundException("Quiz not found")
return map_quiz_entity_to_schema(data)
def get_user_quiz(
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)
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)
def update_quiz(self, quiz_id, quiz_data):