From faf6a0f4b9e792dce7c1170aa5e19590e1b9d986 Mon Sep 17 00:00:00 2001 From: akhdanre Date: Sun, 25 May 2025 19:08:51 +0700 Subject: [PATCH] feat: adding recomendation --- app/blueprints/quiz.py | 17 ++++-- app/controllers/quiz_controller.py | 29 +++++++++-- app/di_container.py | 1 + app/mapper/quiz_mapper.py | 2 +- app/models/entities/quiz_entity.py | 3 +- app/repositories/quiz_repositroy.py | 39 ++++---------- app/schemas/response/__init__.py | 2 +- .../response/quiz/quiz_data_rsp_schema.py | 4 +- app/services/quiz_service.py | 52 ++++++++++++++----- docs/rest_api_docs.yaml | 6 +-- test/service/test_quiz_service.py | 8 +-- 11 files changed, 100 insertions(+), 63 deletions(-) diff --git a/app/blueprints/quiz.py b/app/blueprints/quiz.py index 7ded161..e5a1b4a 100644 --- a/app/blueprints/quiz.py +++ b/app/blueprints/quiz.py @@ -56,14 +56,25 @@ def get_answer(controller: QuizController = Provide[Container.quiz_controller]): ) -@quiz_bp.route("/recomendation", methods=["GET"]) +@quiz_bp.route("/populer", methods=["GET"]) @inject -def get_quiz_recommendation( +def get_quiz_populer( controller: QuizController = Provide[Container.quiz_controller], ): page = request.args.get("page") limit = request.args.get("limit") - return controller.get_quiz_recommendation(page=page, limit=limit) + return controller.get_quiz_populer(page=page, limit=limit) + + +@quiz_bp.route("/recommendation", methods=["GET"]) +@inject +def get_quiz_recommendation( + controller: QuizController = Provide[Container.quiz_controller], +): + user_id = request.args.get("user_id") + page = request.args.get("page") + limit = request.args.get("limit") + return controller.get_quiz_recommendation(user_id=user_id, page=page, limit=limit) @quiz_bp.route("/user/", methods=["GET"]) diff --git a/app/controllers/quiz_controller.py b/app/controllers/quiz_controller.py index 8c2cdb2..2fa7ade 100644 --- a/app/controllers/quiz_controller.py +++ b/app/controllers/quiz_controller.py @@ -43,9 +43,9 @@ class QuizController: except Exception as e: return make_error_response(e) - def quiz_recomendation(self): + def quiz_populer(self): try: - result = self.quiz_service.get_quiz_recommendation() + result = self.quiz_service.get_quiz_populer() if not result: return make_response(message="Quiz not found", status_code=404) return make_response(message="Quiz Found", data=result.model_dump()) @@ -94,14 +94,33 @@ class QuizController: except Exception as e: return make_error_response(e) - def get_quiz_recommendation(self, page, limit): + def get_quiz_populer(self, page, limit): try: page = int(page) if page is not None else 1 limit = int(limit) if limit is not None else 3 - result = self.quiz_service.get_quiz_recommendation(page=page, limit=limit) + result = self.quiz_service.get_quiz_populer(page=page, limit=limit) + return make_response(message="success retrieve populer quiz", data=result) + except DataNotFoundException as e: + return make_response(message=e.message, status_code=e.status_code) + except ValueError as e: + return make_response(message=str(e), data=None, status_code=400) + except ValidationError as e: return make_response( - message="success retrieve recommendation quiz", data=result + message="validation error", data=json.loads(e.json()), status_code=400 ) + except Exception as e: + return make_error_response(e) + + def get_quiz_recommendation(self, user_id, page, limit): + try: + page = int(page) if page is not None else 1 + limit = int(limit) if limit is not None else 3 + result = self.quiz_service.get_quiz_recommendation( + user_id=user_id, + page=page, + limit=limit, + ) + return make_response(message="success retrieve populer quiz", data=result) except DataNotFoundException as e: return make_response(message=e.message, status_code=e.status_code) except ValueError as e: diff --git a/app/di_container.py b/app/di_container.py index fb0ac4f..20dc267 100644 --- a/app/di_container.py +++ b/app/di_container.py @@ -75,6 +75,7 @@ class Container(containers.DeclarativeContainer): quiz_repository, user_repository, subject_repository, + answer_repository, ) answer_service = providers.Factory( AnswerService, diff --git a/app/mapper/quiz_mapper.py b/app/mapper/quiz_mapper.py index a6bd117..2692d7a 100644 --- a/app/mapper/quiz_mapper.py +++ b/app/mapper/quiz_mapper.py @@ -76,7 +76,7 @@ class QuizMapper: ) @staticmethod - def quiz_to_recomendation_mapper( + def quiz_to_populer_mapper( quiz_entity: QuizEntity, user_entity: UserEntity, ) -> ListingQuizResponse: diff --git a/app/models/entities/quiz_entity.py b/app/models/entities/quiz_entity.py index 96f1d2e..e9c82e2 100644 --- a/app/models/entities/quiz_entity.py +++ b/app/models/entities/quiz_entity.py @@ -14,8 +14,9 @@ class QuizEntity(BaseModel): is_public: bool = False date: datetime total_quiz: int = 0 - limit_duration: Optional[int] = 0 # in + limit_duration: Optional[int] = 0 total_user_playing: int = 0 + language_code: Optional[str] = "id" question_listings: Optional[list[QuestionItemEntity]] = [] class ConfigDict: diff --git a/app/repositories/quiz_repositroy.py b/app/repositories/quiz_repositroy.py index 36e3917..28bcd93 100644 --- a/app/repositories/quiz_repositroy.py +++ b/app/repositories/quiz_repositroy.py @@ -20,34 +20,6 @@ class QuizRepository: return QuizEntity(**data) return None - # def search_by_title_or_category( - # self, keyword: str, page: int, page_size: int - # ) -> List[QuizEntity]: - # skip = (page - 1) * page_size - # pipeline = [ - # { - # "$lookup": { - # "from": "category", - # "localField": "category_id", - # "foreignField": "_id", - # "as": "category_info", - # } - # }, - # {"$unwind": "$category_info"}, - # { - # "$match": { - # "$or": [ - # {"title": {"$regex": keyword, "$options": "i"}}, - # {"category_info.name": {"$regex": keyword, "$options": "i"}}, - # ] - # } - # }, - # {"$skip": skip}, - # {"$limit": page_size}, - # ] - # cursor = self.collection.aggregate(pipeline) - # return [QuizEntity(**doc) for doc in cursor] - def search_by_title_or_category( self, keyword: str, subject_id: Optional[str], page: int, page_size: int ) -> List[QuizEntity]: @@ -86,7 +58,6 @@ class QuizRepository: object_ids = [ObjectId(qid) for qid in quiz_ids] cursor = self.collection.find({"_id": {"$in": object_ids}}) datas = list(cursor) - if not datas: return None @@ -137,3 +108,13 @@ class QuizRepository: .limit(limit) ) return [QuizEntity(**doc) for doc in cursor] + + def get_random_quizzes_by_subjects( + self, subject_ids: List[str], limit: int = 3 + ) -> List[QuizEntity]: + pipeline = [ + {"$match": {"subject_id": {"$in": subject_ids}, "is_public": True}}, + {"$sample": {"size": limit}}, + ] + cursor = self.collection.aggregate(pipeline) + return [QuizEntity(**doc) for doc in cursor] diff --git a/app/schemas/response/__init__.py b/app/schemas/response/__init__.py index da62b7f..9eb7900 100644 --- a/app/schemas/response/__init__.py +++ b/app/schemas/response/__init__.py @@ -4,11 +4,11 @@ from .quiz.question_item_schema import QuestionItemSchema from .quiz.quiz_data_rsp_schema import UserQuizListResponse from .history.history_response import HistoryResultSchema from .history.detail_history_response import QuizHistoryResponse, QuestionResult -from .recomendation.recomendation_response_schema import ListingQuizResponse from .subject.get_subject_schema import GetSubjectResponse from .auth.login_response import LoginResponseSchema from .user.user_response_scema import UserResponseSchema from .answer.answer_session_response import AnsweredQuizResponse +from .recomendation.recomendation_response_schema import ListingQuizResponse __all__ = [ "QuizCreationResponse", diff --git a/app/schemas/response/quiz/quiz_data_rsp_schema.py b/app/schemas/response/quiz/quiz_data_rsp_schema.py index 3d61ba2..36ef59f 100644 --- a/app/schemas/response/quiz/quiz_data_rsp_schema.py +++ b/app/schemas/response/quiz/quiz_data_rsp_schema.py @@ -1,8 +1,6 @@ from typing import List from pydantic import BaseModel -from app.schemas.response.recomendation.recomendation_response_schema import ( - ListingQuizResponse, -) +from ..recomendation.recomendation_response_schema import ListingQuizResponse class UserQuizListResponse(BaseModel): diff --git a/app/services/quiz_service.py b/app/services/quiz_service.py index 34be52c..ef956b9 100644 --- a/app/services/quiz_service.py +++ b/app/services/quiz_service.py @@ -1,25 +1,34 @@ -from app.repositories import QuizRepository, UserRepository, SubjectRepository +from app.repositories import ( + QuizRepository, + UserRepository, + SubjectRepository, + UserAnswerRepository, +) from app.schemas.requests import QuizCreateSchema from app.schemas.response import ( UserQuizListResponse, - ListingQuizResponse, QuizGetSchema, + ListingQuizResponse, ) from app.exception import DataNotFoundException, ValidationException from app.mapper import QuizMapper from app.helpers import DatetimeUtil +from flask import current_app + class QuizService: def __init__( self, - quiz_repository=QuizRepository, - user_repository=UserRepository, - subject_repository=SubjectRepository, + quiz_repository: QuizRepository, + user_repository: UserRepository, + subject_repository: SubjectRepository, + answer_repository: UserAnswerRepository, ): self.quiz_repository = quiz_repository self.user_repostory = user_repository self.subject_repository = subject_repository + self.answer_repository = answer_repository def get_quiz(self, quiz_id) -> QuizGetSchema: data = self.quiz_repository.get_by_id(quiz_id) @@ -46,7 +55,7 @@ class QuizService: if author is None: continue mapped_quizzes.append( - QuizMapper.quiz_to_recomendation_mapper( + QuizMapper.quiz_to_populer_mapper( quiz_entity=quiz, user_entity=author, ) @@ -63,13 +72,9 @@ class QuizService: total_user_quiz = self.quiz_repository.count_by_user_id(user_id) - - user = self.user_repostory.get_user_by_id(user_id) - quiz_data = [ - QuizMapper.quiz_to_recomendation_mapper(quiz, user) for quiz in quizzes - ] + quiz_data = [QuizMapper.quiz_to_populer_mapper(quiz, user) for quiz in quizzes] return UserQuizListResponse(total=total_user_quiz, quizzes=quiz_data) @@ -100,7 +105,7 @@ class QuizService: def delete_quiz(self, quiz_id): return self.quiz_repository.delete(quiz_id) - def get_quiz_recommendation(self, page: int, limit: int): + def get_quiz_populer(self, page: int, limit: int): data = self.quiz_repository.get_top_played_quizzes(page=page, limit=limit) if not data: @@ -110,9 +115,30 @@ 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_populer_mapper( quiz_entity=quiz, user_entity=author, ) ) return result + + def get_quiz_recommendation(self, user_id: str, page: int, limit: int): + current_app.logger.info("in here is should execute") + user_answer = self.answer_repository.get_by_user(user_id=user_id) + + if not user_answer: + return [] + + quiz_ids = list({answer.quiz_id for answer in user_answer}) + quiz_work = self.quiz_repository.get_by_ids(quiz_ids) + + if not quiz_work: + return [] + + quiz_subjects = list({quiz.subject_id for quiz in quiz_work}) + + result = self.quiz_repository.get_random_quizzes_by_subjects( + subject_ids=quiz_subjects, limit=limit + ) + + return result diff --git a/docs/rest_api_docs.yaml b/docs/rest_api_docs.yaml index 381badf..6b333b1 100644 --- a/docs/rest_api_docs.yaml +++ b/docs/rest_api_docs.yaml @@ -225,7 +225,7 @@ paths: "404": $ref: "#/components/responses/NotFound" - /quiz/recomendation: + /quiz/populer: get: summary: Get recommended quizzes description: Returns a list of recommended quizzes for the user @@ -235,7 +235,7 @@ paths: - $ref: "#/components/parameters/LimitParam" responses: "200": - description: Successfully retrieved recommendation quiz list + description: Successfully retrieved populer quiz list content: application/json: schema: @@ -243,7 +243,7 @@ paths: properties: message: type: string - example: success retrieve recommendation quiz + example: success retrieve populer quiz data: type: array items: diff --git a/test/service/test_quiz_service.py b/test/service/test_quiz_service.py index 0c88517..9c0c48b 100644 --- a/test/service/test_quiz_service.py +++ b/test/service/test_quiz_service.py @@ -114,7 +114,7 @@ # assert result.quizzes == [] -# def test_get_quiz_recommendation_found(quiz_service, mock_repositories): +# def test_get_quiz_populer_found(quiz_service, mock_repositories): # quiz = QuizEntity( # id=ObjectId(), # author_id="user1", @@ -131,14 +131,14 @@ # mock_repositories["quiz_repository"].get_top_played_quizzes.return_value = [quiz] # mock_repositories["user_repository"].get_user_by_id.return_value = MagicMock() -# result = quiz_service.get_quiz_recommendation(1, 5) +# result = quiz_service.get_quiz_populer(1, 5) # assert len(result) == 1 -# def test_get_quiz_recommendation_not_found(quiz_service, mock_repositories): +# def test_get_quiz_populer_not_found(quiz_service, mock_repositories): # mock_repositories["quiz_repository"].get_top_played_quizzes.return_value = [] # with pytest.raises(DataNotFoundException): -# quiz_service.get_quiz_recommendation(1, 5) +# quiz_service.get_quiz_populer(1, 5) # def test_update_quiz(quiz_service, mock_repositories):