Skip to content

Commit

Permalink
Merge pull request #64 from 2jun0/GDET-76
Browse files Browse the repository at this point in the history
GDET-76: 퀴즈 정답 오답시 게임 점수 변화 구현
  • Loading branch information
2jun0 authored Apr 12, 2024
2 parents 8122d16 + abf200d commit befc9f9
Show file tree
Hide file tree
Showing 48 changed files with 833 additions and 391 deletions.
3 changes: 1 addition & 2 deletions backend/.test.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ GOOGLE_OAUTH2_CLIENT_SECRET=ee
FACEBOOK_OAUTH2_CLIENT_ID=ee
FACEBOOK_OAUTH2_CLIENT_SECRET=ee

ENVIRONMENT=TEST
TEST_DATABASE_URL=sqlite:///test.db
ENVIRONMENT=TEST
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""0009-ADD-GAME-SOLVED-TABLE
Revision ID: db66457957a9
Revises: 07a7ec136a04
Create Date: 2024-04-09 17:43:04.298908
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import sqlmodel


# revision identifiers, used by Alembic.
revision: str = 'db66457957a9'
down_revision: Union[str, None] = '07a7ec136a04'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('solved_game',
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('game_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['game_id'], ['game.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'game_id')
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('solved_game')
# ### end Alembic commands ###
150 changes: 92 additions & 58 deletions backend/poetry.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fastapi = "^0.105.0"
pydantic-settings = "^2.1.0"
sqlalchemy = "^2.0.23"
redis = "^5.0.1"
sqlmodel = "^0.0.14"
sqlmodel = "0.0.16"
fastapi-restful = "^0.5.0"
typing-inspect = "^0.9.0"
elasticsearch = "7.13.4"
Expand All @@ -22,6 +22,7 @@ uvicorn = "^0.25.0"
fastapi-users = {extras = ["oauth", "sqlalchemy"], version = "^12.1.2"}
fastapi-users-db-sqlmodel = "^0.3.0"
aiohttp = "^3.9.3"
async-sqlmodel = "^0.1.3"


[tool.poetry.group.dev.dependencies]
Expand All @@ -30,6 +31,7 @@ faker = "^21.0.0"
aiosqlite = "^0.19.0"
pytest = "^7.4.4"
alembic = "^1.13.1"
pytest-asyncio = "^0.23.6"

[build-system]
requires = ["poetry-core"]
Expand Down
3 changes: 2 additions & 1 deletion backend/pytest.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[pytest]
env_override_existing_values = 1
env_files =
.test.env
.test.env
asyncio_mode=auto
12 changes: 11 additions & 1 deletion backend/src/auth/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@

from fastapi import Depends

from ..dependency import SessionDep
from .model import User
from .repository import UserRepository
from .schema import UserRead
from .user import fastapi_users

current_active_user = fastapi_users.current_user(active=True)

CURRENT_USER_DEP = Annotated[UserRead, Depends(current_active_user)]

async def get_user_repository(session: SessionDep) -> UserRepository:
return UserRepository(session=session)


UserRepositoryDep = Annotated[UserRepository, Depends(get_user_repository)]
CURRENT_USER_DEP = Annotated[User, Depends(current_active_user)]
CURRENT_READ_USER_DEP = Annotated[UserRead, Depends(current_active_user)]
11 changes: 11 additions & 0 deletions backend/src/auth/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from sqlmodel.ext.asyncio.session import AsyncSession

from ..repository import CRUDMixin, IRepository
from .model import User


class UserRepository(IRepository[User], CRUDMixin):
model = User

def __init__(self, session: AsyncSession) -> None:
self._session = session
6 changes: 6 additions & 0 deletions backend/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ class Config(BaseSettings):
FACEBOOK_OAUTH2_CLIENT_SECRET: str
FACEBOOK_OAUTH2_REDIRECT_URL: str | None = None

# score
SCORE_DIFF_ON_CORRECT_FIRST: int = 10
SCORE_DIFF_ON_CORRECT_REPEAT: int = 2
SCORE_DIFF_ON_FAILED: int = -2
SCORE_DIFF_ON_FAILED_AFTER_PREV_SOLVED: int = -5

ENVIRONMENT: Envrionment = Envrionment.PRODUCTION

model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
Expand Down
1 change: 1 addition & 0 deletions backend/src/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
async def get_session() -> AsyncGenerator[AsyncSession, Any]:
async with AsyncSession(engine, expire_on_commit=False) as session:
yield session
await session.commit()


async def es_client() -> AsyncGenerator[AsyncElasticsearch, Any]:
Expand Down
20 changes: 17 additions & 3 deletions backend/src/game/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@

from fastapi import Depends

from ..dependency import ElasticSearchClientDep
from ..dependency import ElasticSearchClientDep, SessionDep
from .manager import GameManager
from .repository import SolvedGameRepository
from .service import GameService


async def get_game_service(es_client: ElasticSearchClientDep) -> GameService:
return GameService(es_client)
async def get_solved_game_repository(session: SessionDep):
return SolvedGameRepository(session)


async def get_game_manager(solved_game_repository: "SolvedGameRepositoryDep") -> GameManager:
return GameManager(solved_game_repository=solved_game_repository)


async def get_game_service(
es_client: ElasticSearchClientDep, solved_game_repository: "SolvedGameRepositoryDep"
) -> GameService:
return GameService(es_client=es_client, solved_game_repository=solved_game_repository)


SolvedGameRepositoryDep = Annotated[SolvedGameRepository, Depends(get_solved_game_repository)]
GameManagerDep = Annotated[GameManager, Depends(get_game_manager)]
GameServiceDep = Annotated[GameService, Depends(get_game_service)]
5 changes: 5 additions & 0 deletions backend/src/game/exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from ..exception import BadRequestError


class GameAlreadySolvedError(BadRequestError):
DETAIL = "Game Already Solved"
16 changes: 16 additions & 0 deletions backend/src/game/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from .model import SolvedGame
from .repository import SolvedGameRepository


class GameManager:
def __init__(self, *, solved_game_repository: SolvedGameRepository) -> None:
self._solved_game_repo = solved_game_repository

async def solve_game(self, *, game_id: int, user_id: int):
exists = await self.has_solved_game(game_id=game_id, user_id=user_id)
if not exists:
solved_game = SolvedGame(user_id=user_id, game_id=game_id)
await self._solved_game_repo.create(model=solved_game)

async def has_solved_game(self, *, game_id: int, user_id: int):
return await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id)
18 changes: 17 additions & 1 deletion backend/src/game/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from datetime import datetime
from typing import Awaitable

from async_sqlmodel import AsyncSQLModel, AwaitableField
from sqlalchemy import BigInteger, Column, UniqueConstraint
from sqlmodel import Field, Relationship, SQLModel

Expand Down Expand Up @@ -55,7 +57,7 @@ class GameAlias(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True):
# Screenshot


class GameScreenshot(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True):
class GameScreenshot(CreatedAtMixin, UpdatedAtMixin, AsyncSQLModel, table=True):
__tablename__: str = "game_screenshot"

id: int | None = Field(default=None, primary_key=True)
Expand All @@ -64,3 +66,17 @@ class GameScreenshot(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True):

game_id: int = Field(foreign_key="game.id")
game: Game = Relationship()
awt_game: Awaitable[Game] = AwaitableField(field="game")


# Solved


class SolvedGame(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True):
__tablename__: str = "solved_game"
__table_args__ = (UniqueConstraint("user_id", "game_id"),)

id: int | None = Field(default=None, primary_key=True)

game_id: int = Field(foreign_key="game.id")
user_id: int = Field(foreign_key="user.id")
22 changes: 22 additions & 0 deletions backend/src/game/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession

from ..repository import CRUDMixin, IRepository
from .model import SolvedGame


class SolvedGameRepository(IRepository[SolvedGame], CRUDMixin):
model = SolvedGame

def __init__(self, session: AsyncSession) -> None:
self._session = session

async def get_by_user_and_game(self, *, user_id: int, game_id: int) -> SolvedGame:
stmt = select(SolvedGame).where(SolvedGame.user_id == user_id, SolvedGame.game_id == game_id)
rs = await self._session.exec(stmt)
return rs.one()

async def exists_by_user_and_game(self, *, user_id: int, game_id: int) -> bool:
stmt = select(SolvedGame).where(SolvedGame.user_id == user_id, SolvedGame.game_id == game_id)
rs = await self._session.exec(stmt)
return rs.one_or_none() is not None
3 changes: 2 additions & 1 deletion backend/src/game/service.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from elasticsearch import AsyncElasticsearch

from ..es import GAME_INDEX
from .repository import SolvedGameRepository
from .schema import AutoCompleteName


class GameService:
def __init__(self, es_client: AsyncElasticsearch) -> None:
def __init__(self, *, es_client: AsyncElasticsearch, solved_game_repository: SolvedGameRepository) -> None:
self._es_client = es_client

async def auto_complete_name(self, query: str) -> list[AutoCompleteName]:
Expand Down
13 changes: 6 additions & 7 deletions backend/src/quiz/daily_quiz_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ class DailyQuizLoader:
def __init__(self, *, daily_quiz_repository: DailyQuizRepository) -> None:
self._daily_quiz_repo = daily_quiz_repository

def _daily_quizzes(self, daily_quizzes: Iterable[DailyQuiz]) -> list[schema.DailyQuiz]:
async def _daily_quizzes(self, daily_quizzes: Iterable[DailyQuiz]) -> list[schema.DailyQuiz]:
quizzes: list[schema.DailyQuiz] = []
for daily_quiz in daily_quizzes:
assert daily_quiz.quiz.id is not None
quiz = await daily_quiz.awt_quiz
assert quiz.id is not None

screenshots = [Url(s.url) for s in daily_quiz.quiz.screenshots]
screenshots = [Url(s.url) for s in await quiz.awt_screenshots]
quizzes.append(
schema.DailyQuiz(quiz_id=daily_quiz.quiz_id, screenshots=screenshots, feature=daily_quiz.feature)
)
Expand All @@ -27,7 +28,5 @@ def _daily_quizzes(self, daily_quizzes: Iterable[DailyQuiz]) -> list[schema.Dail
async def get_daily_quizzes(self) -> Sequence[schema.DailyQuiz]:
utc_now_date = datetime.utcnow().date()

daily_quizzes = await self._daily_quiz_repo.get_by_target_date_with_quiz_and_screenshots(
target_date=utc_now_date
)
return self._daily_quizzes(daily_quizzes)
daily_quizzes = await self._daily_quiz_repo.get_by_target_date(target_date=utc_now_date)
return await self._daily_quizzes(daily_quizzes)
44 changes: 28 additions & 16 deletions backend/src/quiz/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from fastapi import Depends

from ..dependency import SessionDep
from ..game.dependency import GameManagerDep
from ..rank_score.dependency import RankScoreManagerDep
from .daily_quiz_loader import DailyQuizLoader
from .quiz_answer_service import QuizAnswerService
from .quiz_manager import QuizManager
from .quiz_service import QuizService
from .quiz_validator import QuizValidator
from .repository import DailyQuizRepository, QuizAnswerRepository, QuizRepository
Expand All @@ -22,46 +25,55 @@ async def get_daily_quiz_repository(session: SessionDep) -> DailyQuizRepository:
return DailyQuizRepository(session)


QuizRepositoryDep = Annotated[QuizRepository, Depends(get_quiz_repository)]
QuizAnswerRepositoryDep = Annotated[QuizAnswerRepository, Depends(get_quiz_answer_repository)]
DailyQuizRepositoryDep = Annotated[DailyQuizRepository, Depends(get_daily_quiz_repository)]
async def get_quiz_manager() -> QuizManager:
return QuizManager()


async def get_quiz_validator() -> QuizValidator:
return QuizValidator()


QuizValidatorDep = Annotated[QuizValidator, Depends(get_quiz_validator)]
async def get_quiz_validator(quiz_manager: "QuizManagerDep") -> QuizValidator:
return QuizValidator(quiz_manager=quiz_manager)


async def get_quiz_service(
quiz_repository: QuizRepositoryDep,
quiz_answer_repository: QuizAnswerRepositoryDep,
quiz_validator: QuizValidatorDep,
quiz_repository: "QuizRepositoryDep",
quiz_answer_repository: "QuizAnswerRepositoryDep",
quiz_validator: "QuizValidatorDep",
) -> QuizService:
return QuizService(
quiz_repository=quiz_repository, quiz_answer_repository=quiz_answer_repository, quiz_validator=quiz_validator
)


async def get_quiz_answer_service(
quiz_repository: QuizRepositoryDep,
quiz_answer_repository: QuizAnswerRepositoryDep,
quiz_validator: QuizValidatorDep,
quiz_repository: "QuizRepositoryDep",
quiz_answer_repository: "QuizAnswerRepositoryDep",
quiz_validator: "QuizValidatorDep",
game_manager: GameManagerDep,
quiz_manager: "QuizManagerDep",
rank_score_manager: RankScoreManagerDep,
) -> QuizAnswerService:
return QuizAnswerService(
quiz_repository=quiz_repository, quiz_answer_repository=quiz_answer_repository, quiz_validator=quiz_validator
quiz_repository=quiz_repository,
quiz_answer_repository=quiz_answer_repository,
quiz_validator=quiz_validator,
game_manager=game_manager,
quiz_manager=quiz_manager,
rank_score_manager=rank_score_manager,
)


async def get_daily_quiz_loader(
daily_quiz_repository: DailyQuizRepositoryDep,
daily_quiz_repository: "DailyQuizRepositoryDep",
):
return DailyQuizLoader(
daily_quiz_repository=daily_quiz_repository,
)


QuizRepositoryDep = Annotated[QuizRepository, Depends(get_quiz_repository)]
QuizAnswerRepositoryDep = Annotated[QuizAnswerRepository, Depends(get_quiz_answer_repository)]
DailyQuizRepositoryDep = Annotated[DailyQuizRepository, Depends(get_daily_quiz_repository)]
QuizManagerDep = Annotated[QuizManager, Depends(get_quiz_manager)]
QuizValidatorDep = Annotated[QuizValidator, Depends(get_quiz_validator)]
QuizServiceDep = Annotated[QuizService, Depends(get_quiz_service)]
QuizAnswerServiceDep = Annotated[QuizAnswerService, Depends(get_quiz_answer_service)]
DailyQuizLoaderDep = Annotated[DailyQuizLoader, Depends(get_daily_quiz_loader)]
2 changes: 1 addition & 1 deletion backend/src/quiz/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class QuizNotFoundError(NotFoundError):


class QuizAlreadyCompletedError(BadRequestError):
DETAIL = "Quiz Alreay Completed"
DETAIL = "Quiz Already Completed"


class QuizNotCompletedError(BadRequestError):
Expand Down
Loading

0 comments on commit befc9f9

Please sign in to comment.