From 0ae00e4b9d0be15e2bcfb4aa86f7168117113dc2 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Fri, 12 Jan 2024 03:12:30 +0900 Subject: [PATCH 1/2] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EB=8D=AE=EC=96=B4?= =?UTF-8?q?=EC=93=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/pytest.ini b/backend/pytest.ini index 287588a..87a53d5 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -1,3 +1,4 @@ [pytest] +env_override_existing_values = 1 env_files = .test.env \ No newline at end of file From 12bbcef1ca9e3c8f253c0b939733e554614d5fd6 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Fri, 12 Jan 2024 03:13:08 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EC=A0=9C=EC=B6=9C=EC=9D=84=20db?= =?UTF-8?q?=EC=97=90=20=EC=A0=80=EC=9E=A5.=20(=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=EB=8F=84=20=ED=8F=AC=ED=95=A8=EB=90=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/dependency.py | 5 ++ backend/src/quiz/dependency.py | 17 ++++++- backend/src/quiz/model.py | 1 + backend/src/quiz/repository.py | 42 +++++++++++++++++ backend/src/quiz/service.py | 47 +++++++------------ backend/src/repository.py | 36 ++++++++++++++ .../integration/test_quiz_submit_answer.py | 12 ++++- backend/tests/utils/quiz.py | 18 ++++++- 8 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 backend/src/quiz/repository.py create mode 100644 backend/src/repository.py diff --git a/backend/src/dependency.py b/backend/src/dependency.py index 8bef470..5996d3f 100644 --- a/backend/src/dependency.py +++ b/backend/src/dependency.py @@ -1,5 +1,6 @@ from typing import Annotated, Any, AsyncGenerator +from elasticsearch import AsyncElasticsearch from fastapi import Depends from sqlmodel.ext.asyncio.session import AsyncSession @@ -12,4 +13,8 @@ async def get_session() -> AsyncGenerator[AsyncSession, Any]: yield session +async def es_client() -> AsyncElasticsearch: + return AsyncElasticsearch(settings.ELASTIC_SEARCH_URL) # type: ignore + + SessionDep = Annotated[AsyncSession, Depends(get_session)] diff --git a/backend/src/quiz/dependency.py b/backend/src/quiz/dependency.py index df9c350..bbe6d4c 100644 --- a/backend/src/quiz/dependency.py +++ b/backend/src/quiz/dependency.py @@ -3,11 +3,24 @@ from fastapi import Depends from ..dependency import SessionDep +from .repository import QuizRepository, QuizSubmitRepository from .service import QuizService -async def get_quiz_service(session: SessionDep) -> QuizService: - return QuizService(session) +async def get_quiz_service( + quiz_repository: "QuizRepositoryDep", quiz_submit_repository: "QuizSubmitRepositoryDep" +) -> QuizService: + return QuizService(quiz_repository=quiz_repository, quiz_submit_repository=quiz_submit_repository) +async def get_quiz_repository(session: SessionDep) -> QuizRepository: + return QuizRepository(session) + + +async def get_quiz_submit_repository(session: SessionDep) -> QuizSubmitRepository: + return QuizSubmitRepository(session) + + +QuizRepositoryDep = Annotated[QuizRepository, Depends(get_quiz_repository)] +QuizSubmitRepositoryDep = Annotated[QuizSubmitRepository, Depends(get_quiz_submit_repository)] QuizServiceDep = Annotated[QuizService, Depends(get_quiz_service)] diff --git a/backend/src/quiz/model.py b/backend/src/quiz/model.py index 9a45165..bd242a1 100644 --- a/backend/src/quiz/model.py +++ b/backend/src/quiz/model.py @@ -31,6 +31,7 @@ class QuizSubmit(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) answer: str = Field(max_length=64) + correct: bool = Field() quiz_id: int = Field(foreign_key="quiz.id") quiz: Quiz = Relationship() diff --git a/backend/src/quiz/repository.py b/backend/src/quiz/repository.py new file mode 100644 index 0000000..76b57ff --- /dev/null +++ b/backend/src/quiz/repository.py @@ -0,0 +1,42 @@ +from datetime import datetime +from typing import Optional + +from sqlalchemy.orm import selectinload +from sqlmodel import select +from sqlmodel.ext.asyncio.session import AsyncSession + +from ..game.model import GameScreenshot +from ..repository import CRUDMixin, IRepository +from .model import Quiz, QuizSubmit + + +class QuizRepository(IRepository[Quiz], CRUDMixin): + model = Quiz + + def __init__(self, session: AsyncSession) -> None: + self._session = session + + async def get_with_game(self, *, id: int) -> Optional[Quiz]: + stmt = ( + select(Quiz) + .where(Quiz.id == id) + .options(selectinload(Quiz.screenshots).selectinload(GameScreenshot.game)) # type: ignore + ) + rs = await self._session.exec(stmt) + return rs.first() + + async def get_by_created_at_interval_with_screenshots(self, *, start_at: datetime, end_at: datetime): + stmts = ( + select(Quiz) + .where(Quiz.created_at >= start_at, Quiz.created_at <= end_at) + .options(selectinload(Quiz.screenshots)) # type: ignore + ) + rs = await self._session.exec(stmts) + return rs.all() + + +class QuizSubmitRepository(IRepository[QuizSubmit], CRUDMixin): + model = QuizSubmit + + def __init__(self, session: AsyncSession) -> None: + self._session = session diff --git a/backend/src/quiz/service.py b/backend/src/quiz/service.py index 2dc4c0d..c30e788 100644 --- a/backend/src/quiz/service.py +++ b/backend/src/quiz/service.py @@ -1,49 +1,34 @@ from datetime import datetime, time -from typing import Optional, Sequence +from typing import Sequence -from sqlalchemy.orm import selectinload -from sqlmodel import select -from sqlmodel.ext.asyncio.session import AsyncSession - -from ..game.model import GameScreenshot from .exception import QuizNotFoundError -from .model import Quiz +from .model import Quiz, QuizSubmit +from .repository import QuizRepository, QuizSubmitRepository class QuizService: - def __init__(self, session: AsyncSession) -> None: - self._session = session + def __init__(self, *, quiz_repository: QuizRepository, quiz_submit_repository: QuizSubmitRepository) -> None: + self._quiz_repo = quiz_repository + self._quiz_submit_repo = quiz_submit_repository async def get_today_quizes(self) -> Sequence[Quiz]: now = datetime.utcnow() today = now.date() - start_datetime = datetime.combine(today, time.min) - end_datetime = datetime.combine(today, time.max) + start_at = datetime.combine(today, time.min) + end_at = datetime.combine(today, time.max) - stmts = ( - select(Quiz) - .where(Quiz.created_at >= start_datetime, Quiz.created_at <= end_datetime) - .options(selectinload(Quiz.screenshots)) # type: ignore - ) - rs = await self._session.exec(stmts) - return rs.all() + return await self._quiz_repo.get_by_created_at_interval_with_screenshots(start_at=start_at, end_at=end_at) async def submit_answer(self, *, quiz_id: int, answer: str) -> bool: - """ - 퀴즈에 대한 정답 여부를 반환하는 함수 - """ - - # TODO: 제출 기록을 저장해야 함 - quiz = await self._get_quiz_by_id_with_game(quiz_id) + """퀴즈에 대한 정답 여부를 반환하는 함수""" + quiz = await self._quiz_repo.get_with_game(id=quiz_id) if quiz is None: raise QuizNotFoundError - return quiz.game.name == answer + correct = quiz.game.name == answer + quiz_submit = QuizSubmit(answer=answer, correct=correct, quiz_id=quiz_id) + + await self._quiz_submit_repo.create(model=quiz_submit) - async def _get_quiz_by_id_with_game(self, id: int) -> Optional[Quiz]: - stmt = ( - select(Quiz).where(Quiz.id == id).options(selectinload(Quiz.screenshots).selectinload(GameScreenshot.game)) # type: ignore - ) - rs = await self._session.exec(stmt) - return rs.first() + return correct diff --git a/backend/src/repository.py b/backend/src/repository.py new file mode 100644 index 0000000..7a84918 --- /dev/null +++ b/backend/src/repository.py @@ -0,0 +1,36 @@ +from typing import Generic, Protocol, Type, TypeVar + +from sqlalchemy import exc +from sqlmodel import SQLModel, exists, select +from sqlmodel.ext.asyncio.session import AsyncSession + +ModelTypeT = TypeVar("ModelTypeT", bound=SQLModel) +ModelTypeS = TypeVar("ModelTypeS", bound=SQLModel) + + +class IRepository(Protocol, Generic[ModelTypeT]): + _session: AsyncSession + model: Type[ModelTypeT] + + +class CRUDMixin: + async def get(self: IRepository[ModelTypeS], *, id: int) -> ModelTypeS | None: + stmt = select(self.model).where(self.model.id == id) + rs = await self._session.exec(stmt) + return rs.first() + + async def exists(self: IRepository[ModelTypeS], *, id: int) -> bool: + stmt = select(exists().where(self.model.id == id)) + rs = await self._session.exec(stmt) + return rs.one() + + async def create(self: IRepository[ModelTypeS], *, model: ModelTypeS) -> ModelTypeS: + try: + self._session.add(model) + await self._session.commit() + except exc.IntegrityError: + await self._session.rollback() + raise + + await self._session.refresh(model) + return model diff --git a/backend/tests/integration/test_quiz_submit_answer.py b/backend/tests/integration/test_quiz_submit_answer.py index d41bcef..76c9dc4 100644 --- a/backend/tests/integration/test_quiz_submit_answer.py +++ b/backend/tests/integration/test_quiz_submit_answer.py @@ -2,7 +2,7 @@ from fastapi.testclient import TestClient from sqlmodel import Session -from tests.utils.quiz import create_random_quiz +from tests.utils.quiz import create_random_quiz, get_quiz_submit def test_post_submit_true_answer(client: TestClient, session: Session): @@ -14,6 +14,11 @@ def test_post_submit_true_answer(client: TestClient, session: Session): res_json = res.json() assert res_json["correct"] is True + # check db + submit = get_quiz_submit(session, quiz_id=saved_quiz.id) + assert submit is not None + assert submit.correct is True + def test_post_submit_false_answer(client: TestClient, session: Session): saved_quiz = create_random_quiz(session) @@ -24,6 +29,11 @@ def test_post_submit_false_answer(client: TestClient, session: Session): res_json = res.json() assert res_json["correct"] is False + # check db + submit = get_quiz_submit(session, quiz_id=saved_quiz.id) + assert submit is not None + assert submit.correct is False + def test_post_submit_answer_with_invalid_quiz_id(client: TestClient): res = client.post("/quiz/submit_answer", json={"quiz_id": -1, "answer": "아무거나 빙빙바리바리구"}) diff --git a/backend/tests/utils/quiz.py b/backend/tests/utils/quiz.py index 1d9fc1a..2bfed84 100644 --- a/backend/tests/utils/quiz.py +++ b/backend/tests/utils/quiz.py @@ -1,7 +1,9 @@ -from sqlmodel import Session +from typing import Optional + +from sqlmodel import Session, select from src.game.model import GameScreenshot -from src.quiz.model import Quiz +from src.quiz.model import Quiz, QuizSubmit from .game import create_random_game from .screenshot import create_random_game_screenshot @@ -25,3 +27,15 @@ def create_random_quiz(session: Session, *, screenshots: list[GameScreenshot] | session.refresh(quiz) return quiz + + +def get_quiz_submit( + session: Session, *, quiz_id: int | None = None, answer: int | None = None +) -> Optional[QuizSubmit]: + stmt = select(QuizSubmit) + if quiz_id: + stmt = stmt.where(QuizSubmit.quiz_id == quiz_id) + if answer: + stmt = stmt.where(QuizSubmit.answer == answer) + + return session.exec(stmt).first()