diff --git "a/backend/migrations/versions/2023_11_20_1248-a2f543f8e4c6_\320\277\320\265\321\200\320\265\320\275\320\276\321\201\320\270\321\202_\320\276\321\202\320\262\320\265\321\202\321\201\321\202\320\262\320\265\320\275\320\275\320\276\321\201\321\202\321\214_\320\267\320\260_\320\277\321\200\320\260\320\262\320\270\320\273\321\214\320\275\321\213\320\271_.py" "b/backend/migrations/versions/2023_11_20_1248-a2f543f8e4c6_\320\277\320\265\321\200\320\265\320\275\320\276\321\201\320\270\321\202_\320\276\321\202\320\262\320\265\321\202\321\201\321\202\320\262\320\265\320\275\320\275\320\276\321\201\321\202\321\214_\320\267\320\260_\320\277\321\200\320\260\320\262\320\270\320\273\321\214\320\275\321\213\320\271_.py" new file mode 100644 index 0000000..c8cedfc --- /dev/null +++ "b/backend/migrations/versions/2023_11_20_1248-a2f543f8e4c6_\320\277\320\265\321\200\320\265\320\275\320\276\321\201\320\270\321\202_\320\276\321\202\320\262\320\265\321\202\321\201\321\202\320\262\320\265\320\275\320\275\320\276\321\201\321\202\321\214_\320\267\320\260_\320\277\321\200\320\260\320\262\320\270\320\273\321\214\320\275\321\213\320\271_.py" @@ -0,0 +1,30 @@ +"""переносит ответственность за правильный вариант ответа на вопроса на ответ + +Revision ID: a2f543f8e4c6 +Revises: f074fb69a98a +Create Date: 2023-11-20 12:48:48.808979 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a2f543f8e4c6' +down_revision: Union[str, None] = 'f074fb69a98a' +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.drop_column('questions', 'correct_answer_id') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('questions', sa.Column('correct_answer_id', sa.UUID(), autoincrement=False, nullable=False)) + # ### end Alembic commands ### diff --git a/backend/src/game/dependencies.py b/backend/src/game/dependencies.py index 5bca241..e25825a 100644 --- a/backend/src/game/dependencies.py +++ b/backend/src/game/dependencies.py @@ -1,11 +1,13 @@ from repository.level_repository import LevelRepository from repository.map_repository import MapRepository from repository.module_repository import ModuleRepository +from repository.question_repository import QuestionRepository from repository.task_unit_repository import TaskUnitRepository from repository.theory_unit_repository import TheoryUnitRepository from services.level_service import LevelService from services.map_service import MapService from services.module_service import ModuleService +from services.question_service import QuestionService from services.task_unit_service import TaskUnitService from services.theory_unit_service import TheoryUnitService @@ -28,5 +30,9 @@ def task_unit_service() -> TaskUnitService: def theory_unit_service() -> TheoryUnitService: return TheoryUnitService(TheoryUnitRepository) + + +def question_service() -> QuestionService: + return QuestionService(QuestionRepository) # def users_service(): # return UsersService(UsersRepository) diff --git a/backend/src/game/units/tasks/questions/answers/schemas.py b/backend/src/game/units/tasks/questions/answers/schemas.py index a76465c..cd1a2ba 100644 --- a/backend/src/game/units/tasks/questions/answers/schemas.py +++ b/backend/src/game/units/tasks/questions/answers/schemas.py @@ -8,13 +8,14 @@ class __AnswerBase(BaseModel): class AnswerOptionRead(__AnswerBase): + id: UUID question_id: UUID class AnswerOptionCreate(__AnswerBase): - pass + is_correct: bool class AnswerOptionUpdate(__AnswerBase): answer: str | None = None - question_id: UUID | None = None + is_correct: bool | None = None diff --git a/backend/src/game/units/tasks/questions/enums.py b/backend/src/game/units/tasks/questions/enums.py index d70dbd9..1b85323 100644 --- a/backend/src/game/units/tasks/questions/enums.py +++ b/backend/src/game/units/tasks/questions/enums.py @@ -1,6 +1,6 @@ -from enum import Enum, auto +from enum import auto, StrEnum -class QuestionTypes(Enum): +class QuestionTypes(StrEnum): SingleChoice = auto() MultipleChoice = auto() diff --git a/backend/src/game/units/tasks/questions/models.py b/backend/src/game/units/tasks/questions/models.py index 6b29e46..feb9da6 100644 --- a/backend/src/game/units/tasks/questions/models.py +++ b/backend/src/game/units/tasks/questions/models.py @@ -22,13 +22,13 @@ class Question(BaseModel): type: Mapped[QuestionTypes] = mapped_column( postgresql.ENUM(QuestionTypes, name='question_types'), nullable=False, default=QuestionTypes.SingleChoice) question: Mapped[str] = mapped_column(String, nullable=False, default="Вопрос!") - correct_answer_id: Mapped[uuid.UUID] = mapped_column(UUID, default=uuid.uuid4) task: Mapped["TaskUnit"] = relationship(back_populates='questions') possible_answers: Mapped[list["AnswerOption"]] = relationship(back_populates='question', lazy='selectin') def to_read_schema(self) -> QuestionRead: return QuestionRead(id=self.id, + type=self.type, task_id=self.task_id, question=self.question, - possible_answers=self.possible_answers) + possible_answers=[model.to_read_schema() for model in self.possible_answers]) diff --git a/backend/src/game/units/tasks/questions/router.py b/backend/src/game/units/tasks/questions/router.py new file mode 100644 index 0000000..4730248 --- /dev/null +++ b/backend/src/game/units/tasks/questions/router.py @@ -0,0 +1,44 @@ +from uuid import UUID + +from fastapi import APIRouter + +from game.units.tasks.questions.schemas import QuestionRead, QuestionCreate, QuestionUpdate +from utils.types import QuestionServiceType + +router = APIRouter(tags=["Questions"]) + + +@router.get('/questions/', tags=["Dev"]) +async def root(question_service: QuestionServiceType) -> list[QuestionRead]: + return await question_service.get_all() + + +@router.post('/maps/{map_id}/modules/{module_id}/levels/{level_id}/tasks/{task_id}/') +async def post_question_to_task_unit(map_id: UUID, + module_id: UUID, + level_id: UUID, + task_id: UUID, + question_create: QuestionCreate, + question_service: QuestionServiceType) -> QuestionRead: + return await question_service.create_one(task_id, question_create) + + +@router.delete("/maps/{map_id}/modules/{module_id}/levels/{level_id}/tasks/{task_id}/questions/{question_id}/") +async def delete_question_from_task_unit(map_id: UUID, + module_id: UUID, + level_id: UUID, + task_id: UUID, + question_id: UUID, + question_service: QuestionServiceType) -> QuestionRead: + return await question_service.delete_one(question_id) + + +@router.patch("/maps/{map_id}/modules/{module_id}/levels/{level_id}/tasks/{task_id}/questions/{question_id}/") +async def update_question_in_task_unit(map_id: UUID, + module_id: UUID, + level_id: UUID, + task_id: UUID, + question_id: UUID, + question_update: QuestionUpdate, + question_service: QuestionServiceType) -> QuestionRead: + return await question_service.update_one(question_id, question_update) diff --git a/backend/src/game/units/tasks/questions/schemas.py b/backend/src/game/units/tasks/questions/schemas.py index 4e8676a..c7a2420 100644 --- a/backend/src/game/units/tasks/questions/schemas.py +++ b/backend/src/game/units/tasks/questions/schemas.py @@ -7,7 +7,6 @@ class __QuestionBase(BaseModel): - task_id: UUID type: QuestionTypes question: str @@ -15,12 +14,13 @@ class __QuestionBase(BaseModel): class QuestionRead(__QuestionBase): + task_id: UUID id: UUID possible_answers: list[AnswerOptionRead] class QuestionCreate(__QuestionBase): - correct_answer: AnswerOptionCreate + type: QuestionTypes = QuestionTypes.SingleChoice possible_answers: list[AnswerOptionCreate] diff --git a/backend/src/game/units/tasks/router.py b/backend/src/game/units/tasks/router.py index ad1c8f1..ad6a277 100644 --- a/backend/src/game/units/tasks/router.py +++ b/backend/src/game/units/tasks/router.py @@ -13,38 +13,38 @@ async def root(task_unit_service: TaskUnitServiceType) -> list[TaskUnitRead]: return await task_unit_service.get_all() -@router.get("/maps/{map_id}/modules/{module_id}/levels/{level_id}/tasks/{task_id}/") -async def get_task_in_level(map_id: UUID, - module_id: UUID, - level_id: UUID, - task_id: UUID, - task_unit_service: TaskUnitServiceType) -> TaskUnitRead: - return await task_unit_service.get_one(task_id) +# @router.get("/maps/{map_id}/modules/{module_id}/levels/{level_id}/tasks/{task_id}/") +# async def get_task_in_level(map_id: UUID, +# module_id: UUID, +# level_id: UUID, +# task_id: UUID, +# task_unit_service: TaskUnitServiceType) -> TaskUnitRead: +# return await task_unit_service.get_one(task_id) @router.post("/maps/{map_id}/modules/{module_id}/levels/{level_id}/tasks/") -async def post_task_to_level(map_id: UUID, - module_id: UUID, - level_id: UUID, - task_create: TaskUnitCreate, - task_unit_service: TaskUnitServiceType) -> TaskUnitRead: +async def post_task_unit_to_level(map_id: UUID, + module_id: UUID, + level_id: UUID, + task_create: TaskUnitCreate, + task_unit_service: TaskUnitServiceType) -> TaskUnitRead: return await task_unit_service.create_one(level_id, task_create) @router.delete("/maps/{map_id}/modules/{module_id}/levels/{level_id}/tasks/{task_id}/") -async def delete_task_from_level(map_id: UUID, - module_id: UUID, - level_id: UUID, - task_id: UUID, - task_unit_service: TaskUnitServiceType) -> TaskUnitRead: +async def delete_task_unit_from_level(map_id: UUID, + module_id: UUID, + level_id: UUID, + task_id: UUID, + task_unit_service: TaskUnitServiceType) -> TaskUnitRead: return await task_unit_service.delete_one(task_id) @router.patch("/maps/{map_id}/modules/{module_id}/levels/{level_id}/tasks/{task_id}/") -async def update_task_in_level(map_id: UUID, - module_id: UUID, - level_id: UUID, - task_id: UUID, - task_update: TaskUnitUpdate, - task_unit_service: TaskUnitServiceType) -> TaskUnitRead: +async def update_task_unit_in_level(map_id: UUID, + module_id: UUID, + level_id: UUID, + task_id: UUID, + task_update: TaskUnitUpdate, + task_unit_service: TaskUnitServiceType) -> TaskUnitRead: return await task_unit_service.update_one(task_id, task_update) diff --git a/backend/src/game/units/theory/router.py b/backend/src/game/units/theory/router.py index caf107f..b62e137 100644 --- a/backend/src/game/units/theory/router.py +++ b/backend/src/game/units/theory/router.py @@ -13,13 +13,13 @@ async def root(theory_unit_service: TheoryUnitServiceType) -> list[TheoryUnitRea return await theory_unit_service.get_all() -@router.get("/maps/{map_id}/modules/{module_id}/levels/{level_id}/theory/{theory_id}/") -async def get_level_theory_unit(map_id: UUID, - module_id: UUID, - level_id: UUID, - theory_id: UUID, - theory_unit_service: TheoryUnitServiceType) -> TheoryUnitRead: - return await theory_unit_service.get_one(theory_id) +# @router.get("/maps/{map_id}/modules/{module_id}/levels/{level_id}/theory/{theory_id}/") +# async def get_level_theory_unit(map_id: UUID, +# module_id: UUID, +# level_id: UUID, +# theory_id: UUID, +# theory_unit_service: TheoryUnitServiceType) -> TheoryUnitRead: +# return await theory_unit_service.get_one(theory_id) @router.post("/maps/{map_id}/modules/{module_id}/levels/{level_id}/theory/") diff --git a/backend/src/main.py b/backend/src/main.py index dc2e93b..fafd7b2 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -6,8 +6,9 @@ from game.modules.router import router as modules_router from game.map.router import router as map_router from game.units.tasks.router import router as tasks_router +from game.units.tasks.questions.router import router as questions_router from game.units.theory.router import router as theory_router -# + # from users.router import router as users_router # from users.tutors.router import router as tutors_router # from users.employees.router import router as employees_router @@ -58,6 +59,7 @@ async def unprotected_route(): modules_router, map_router, tasks_router, + questions_router, theory_router, # users_router, # tutors_router, diff --git a/backend/src/repository/question_repository.py b/backend/src/repository/question_repository.py new file mode 100644 index 0000000..e8ff743 --- /dev/null +++ b/backend/src/repository/question_repository.py @@ -0,0 +1,28 @@ +import typing +import uuid + +from sqlalchemy import insert + +from database import async_session_maker +from game.units.tasks.questions import Question +from game.units.tasks.questions.answers import AnswerOption +from repository.sqlalchemy_repository import SQLAlchemyRepository + + +class QuestionRepository(SQLAlchemyRepository): + model = Question + + async def add_one(self, model: dict[str, typing.Any]) -> Question: + async with async_session_maker() as session: + possible_answers_list = model.pop('possible_answers') + question_to_add = Question(**model) + + possible_answers = [AnswerOption(**answer_option_dict, question_id=question_to_add.id) for + answer_option_dict in possible_answers_list] + for answer_option in possible_answers: + question_to_add.possible_answers.append(answer_option) + + session.add(question_to_add) + await session.commit() + await session.refresh(question_to_add) + return question_to_add diff --git a/backend/src/services/question_service.py b/backend/src/services/question_service.py new file mode 100644 index 0000000..33c8328 --- /dev/null +++ b/backend/src/services/question_service.py @@ -0,0 +1,33 @@ +import uuid + +from game.units.tasks.questions.schemas import QuestionRead, QuestionCreate, QuestionUpdate +from repository.abstract import AbstractRepository + + +class QuestionService: + __question_repo: AbstractRepository + + def __init__(self, map_repo: type[AbstractRepository]): + self.__question_repo = map_repo() + + async def create_one(self, task_id: uuid.UUID, schema_create: QuestionCreate) -> QuestionRead: + schema_dict = schema_create.model_dump(include={'type', 'possible_answers', 'question'}) + schema_dict['task_id'] = task_id + return await self.__question_repo.add_one(schema_dict) + + async def get_all(self) -> list[QuestionRead]: + models = await self.__question_repo.find_all() + return [model.to_read_schema() for model in models] + + async def get_one(self, id: uuid.UUID) -> QuestionRead: + res = await self.__question_repo.get_one(id) + return res.to_read_schema() + + async def delete_one(self, id: uuid.UUID) -> QuestionRead: + res = await self.__question_repo.delete_one(id) + return res.to_read_schema() + + async def update_one(self, id: uuid.UUID, schema_update: QuestionUpdate) -> QuestionRead: + schema_dict = schema_update.model_dump() + res = await self.__question_repo.update_one(id, schema_dict) + return res.to_read_schema() diff --git a/backend/src/utils/types.py b/backend/src/utils/types.py index c53a870..304151a 100644 --- a/backend/src/utils/types.py +++ b/backend/src/utils/types.py @@ -5,10 +5,12 @@ from auth.base_config import current_user, current_superuser from database import get_async_session -from game.dependencies import map_service, module_service, level_service, task_unit_service, theory_unit_service +from game.dependencies import map_service, module_service, level_service, task_unit_service, theory_unit_service, \ + question_service from services.level_service import LevelService from services.map_service import MapService from services.module_service import ModuleService +from services.question_service import QuestionService from services.task_unit_service import TaskUnitService from services.theory_unit_service import TheoryUnitService from users.models import User @@ -21,6 +23,7 @@ LevelServiceType = Annotated[LevelService, Depends(level_service)] TaskUnitServiceType = Annotated[TaskUnitService, Depends(task_unit_service)] TheoryUnitServiceType = Annotated[TheoryUnitService, Depends(theory_unit_service)] +QuestionServiceType = Annotated[QuestionService, Depends(question_service)] # AsyncDBSession = Annotated[AsyncSession, Depends(get_async_session)] # QueryDBLimit = Annotated[int, Query(ge=0, le=10 ** 3)]