diff --git a/backend/migrations/env.py b/backend/migrations/env.py index 38798ad..2f3c64f 100644 --- a/backend/migrations/env.py +++ b/backend/migrations/env.py @@ -30,13 +30,14 @@ # from myapp import mymodel # target_metadata = mymodel.Base.metadata -from game.map.models import Map -from game.modules.models import Module -from game.levels.models import Level, LevelTheory, LevelTask -from game.units.tasks.models import TaskUnit, EmployeesTask -from game.units.tasks.questions.models import Question, Answer +from game.map import Map +from game.modules import Module +from game.levels import Level +from game.units.tasks import TaskUnit, EmployeesTask +from game.units.theory import TheoryUnit # , TheoryVideo +from game.units.tasks.questions import Question +from game.units.tasks.questions.answers import AnswerOption from game.units.tasks.proofs.models import Proof, ProofVideos, ProofImages -from game.units.theory.models import TheoryUnit, TheoryVideo from users.models import User from users.tutors.models import Tutor diff --git "a/backend/migrations/versions/2023_11_19_0024-da06417476fd_\320\274\320\265\320\275\321\217\320\265\321\202_\320\262_\320\274\320\276\320\264\320\265\320\273\320\270_\321\203\321\200\320\276\320\262\320\275\321\217_name_\320\275\320\260_title.py" "b/backend/migrations/versions/2023_11_19_0024-da06417476fd_\320\274\320\265\320\275\321\217\320\265\321\202_\320\262_\320\274\320\276\320\264\320\265\320\273\320\270_\321\203\321\200\320\276\320\262\320\275\321\217_name_\320\275\320\260_title.py" new file mode 100644 index 0000000..51c0521 --- /dev/null +++ "b/backend/migrations/versions/2023_11_19_0024-da06417476fd_\320\274\320\265\320\275\321\217\320\265\321\202_\320\262_\320\274\320\276\320\264\320\265\320\273\320\270_\321\203\321\200\320\276\320\262\320\275\321\217_name_\320\275\320\260_title.py" @@ -0,0 +1,44 @@ +"""меняет в модели уровня name на title + +Revision ID: da06417476fd +Revises: 29ed19cdffbf +Create Date: 2023-11-19 00:24:59.794292 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'da06417476fd' +down_revision: Union[str, None] = '29ed19cdffbf' +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.add_column('levels', sa.Column('title', sa.String(length=255), nullable=False)) + op.alter_column('levels', 'module_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_column('levels', 'name') + op.alter_column('modules', 'map_id', + existing_type=sa.UUID(), + nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('modules', 'map_id', + existing_type=sa.UUID(), + nullable=False) + op.add_column('levels', sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=False)) + op.alter_column('levels', 'module_id', + existing_type=sa.UUID(), + nullable=False) + op.drop_column('levels', 'title') + # ### end Alembic commands ### diff --git "a/backend/migrations/versions/2023_11_19_0049-5bbffa09568b_\320\274\320\265\320\275\321\217\320\265\321\202_\320\262_\320\274\320\276\320\264\320\265\320\273\320\270_\321\202\320\265\320\276\321\200\320\270\320\270_theme_\320\275\320\260_title.py" "b/backend/migrations/versions/2023_11_19_0049-5bbffa09568b_\320\274\320\265\320\275\321\217\320\265\321\202_\320\262_\320\274\320\276\320\264\320\265\320\273\320\270_\321\202\320\265\320\276\321\200\320\270\320\270_theme_\320\275\320\260_title.py" new file mode 100644 index 0000000..bec0187 --- /dev/null +++ "b/backend/migrations/versions/2023_11_19_0049-5bbffa09568b_\320\274\320\265\320\275\321\217\320\265\321\202_\320\262_\320\274\320\276\320\264\320\265\320\273\320\270_\321\202\320\265\320\276\321\200\320\270\320\270_theme_\320\275\320\260_title.py" @@ -0,0 +1,32 @@ +"""меняет в модели теории theme на title + +Revision ID: 5bbffa09568b +Revises: da06417476fd +Create Date: 2023-11-19 00:49:49.197801 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '5bbffa09568b' +down_revision: Union[str, None] = 'da06417476fd' +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.add_column('theory_blocks', sa.Column('title', sa.String(), nullable=False)) + op.drop_column('theory_blocks', 'theme') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('theory_blocks', sa.Column('theme', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_column('theory_blocks', 'title') + # ### end Alembic commands ### diff --git "a/backend/migrations/versions/2023_11_19_1035-43a526a12009_\320\264\320\276\320\261\320\260\320\262\320\273\321\217\320\265\321\202_\321\201\320\262\321\217\320\267\321\214_\320\274\320\265\320\266\320\264\321\203_\320\267\320\260\320\264\320\260\321\207\320\265\320\271_\320\270_\320\276\321\202\320\262\320\265\321\202\320\260\320\274\320\270.py" "b/backend/migrations/versions/2023_11_19_1035-43a526a12009_\320\264\320\276\320\261\320\260\320\262\320\273\321\217\320\265\321\202_\321\201\320\262\321\217\320\267\321\214_\320\274\320\265\320\266\320\264\321\203_\320\267\320\260\320\264\320\260\321\207\320\265\320\271_\320\270_\320\276\321\202\320\262\320\265\321\202\320\260\320\274\320\270.py" new file mode 100644 index 0000000..9d127b3 --- /dev/null +++ "b/backend/migrations/versions/2023_11_19_1035-43a526a12009_\320\264\320\276\320\261\320\260\320\262\320\273\321\217\320\265\321\202_\321\201\320\262\321\217\320\267\321\214_\320\274\320\265\320\266\320\264\321\203_\320\267\320\260\320\264\320\260\321\207\320\265\320\271_\320\270_\320\276\321\202\320\262\320\265\321\202\320\260\320\274\320\270.py" @@ -0,0 +1,36 @@ +"""добавляет связь между задачей и ответами + +Revision ID: 43a526a12009 +Revises: 5bbffa09568b +Create Date: 2023-11-19 10:35:20.123826 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '43a526a12009' +down_revision: Union[str, None] = '5bbffa09568b' +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.add_column('questions', sa.Column('task_id', sa.UUID(), nullable=True)) + op.create_foreign_key(None, 'questions', 'tasks', ['task_id'], ['id']) + op.add_column('tasks', sa.Column('level_id', sa.UUID(), nullable=True)) + op.create_foreign_key(None, 'tasks', 'levels', ['level_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'tasks', type_='foreignkey') + op.drop_column('tasks', 'level_id') + op.drop_constraint(None, 'questions', type_='foreignkey') + op.drop_column('questions', 'task_id') + # ### end Alembic commands ### diff --git "a/backend/migrations/versions/2023_11_19_1037-e21d5fb84a66_\320\264\320\276\320\261\320\260\320\262\320\273\321\217\320\265\321\202_\321\201\320\262\321\217\320\267\321\214_\320\274\320\265\320\266\320\264\321\203_\320\262\320\276\320\277\321\200\320\276\321\201\320\260\320\274\320\270_\320\270_.py" "b/backend/migrations/versions/2023_11_19_1037-e21d5fb84a66_\320\264\320\276\320\261\320\260\320\262\320\273\321\217\320\265\321\202_\321\201\320\262\321\217\320\267\321\214_\320\274\320\265\320\266\320\264\321\203_\320\262\320\276\320\277\321\200\320\276\321\201\320\260\320\274\320\270_\320\270_.py" new file mode 100644 index 0000000..0875070 --- /dev/null +++ "b/backend/migrations/versions/2023_11_19_1037-e21d5fb84a66_\320\264\320\276\320\261\320\260\320\262\320\273\321\217\320\265\321\202_\321\201\320\262\321\217\320\267\321\214_\320\274\320\265\320\266\320\264\321\203_\320\262\320\276\320\277\321\200\320\276\321\201\320\260\320\274\320\270_\320\270_.py" @@ -0,0 +1,30 @@ +"""добавляет связь между вопросами и ответами + +Revision ID: e21d5fb84a66 +Revises: 43a526a12009 +Create Date: 2023-11-19 10:37:12.398132 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e21d5fb84a66' +down_revision: Union[str, None] = '43a526a12009' +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_foreign_key(None, 'answers', 'questions', ['question_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'answers', type_='foreignkey') + # ### end Alembic commands ### diff --git "a/backend/migrations/versions/2023_11_19_1048-e8496d8a36fc_\320\262\321\200\320\265\320\274\320\265\320\275\320\275\320\276_\321\203\320\264\320\260\320\273\321\217\320\265\321\202_\321\202\320\260\320\261\320\273\320\270\321\206\321\203_\321\201_\320\262\320\270\320\264\320\265\320\276_\321\202\320\265\320\276\321\200\320\270\320\270.py" "b/backend/migrations/versions/2023_11_19_1048-e8496d8a36fc_\320\262\321\200\320\265\320\274\320\265\320\275\320\275\320\276_\321\203\320\264\320\260\320\273\321\217\320\265\321\202_\321\202\320\260\320\261\320\273\320\270\321\206\321\203_\321\201_\320\262\320\270\320\264\320\265\320\276_\321\202\320\265\320\276\321\200\320\270\320\270.py" new file mode 100644 index 0000000..f7391d6 --- /dev/null +++ "b/backend/migrations/versions/2023_11_19_1048-e8496d8a36fc_\320\262\321\200\320\265\320\274\320\265\320\275\320\275\320\276_\321\203\320\264\320\260\320\273\321\217\320\265\321\202_\321\202\320\260\320\261\320\273\320\270\321\206\321\203_\321\201_\320\262\320\270\320\264\320\265\320\276_\321\202\320\265\320\276\321\200\320\270\320\270.py" @@ -0,0 +1,36 @@ +"""временно удаляет таблицу с видео теории + +Revision ID: e8496d8a36fc +Revises: e21d5fb84a66 +Create Date: 2023-11-19 10:48:35.457329 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e8496d8a36fc' +down_revision: Union[str, None] = 'e21d5fb84a66' +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_table('theory_videos') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('theory_videos', + sa.Column('id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('theory_id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('url', sa.TEXT(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['theory_id'], ['theory_blocks.id'], name='theory_videos_theory_id_fkey'), + sa.PrimaryKeyConstraint('id', name='theory_videos_pkey') + ) + # ### end Alembic commands ### diff --git "a/backend/migrations/versions/2023_11_19_2146-4e3cf7a2acb2_\320\274\320\276\320\264\321\203\320\273\321\214_\320\270_\321\203\321\200\320\276\320\262\320\275\320\270_\320\276\320\261\321\217\320\267\320\260\321\202\320\265\320\273\321\214\320\275\320\276_\320\277\321\200\320\270\320\262\321\217\320\267\320\260\320\275\321\213_\320\272_.py" "b/backend/migrations/versions/2023_11_19_2146-4e3cf7a2acb2_\320\274\320\276\320\264\321\203\320\273\321\214_\320\270_\321\203\321\200\320\276\320\262\320\275\320\270_\320\276\320\261\321\217\320\267\320\260\321\202\320\265\320\273\321\214\320\275\320\276_\320\277\321\200\320\270\320\262\321\217\320\267\320\260\320\275\321\213_\320\272_.py" new file mode 100644 index 0000000..29d83d9 --- /dev/null +++ "b/backend/migrations/versions/2023_11_19_2146-4e3cf7a2acb2_\320\274\320\276\320\264\321\203\320\273\321\214_\320\270_\321\203\321\200\320\276\320\262\320\275\320\270_\320\276\320\261\321\217\320\267\320\260\321\202\320\265\320\273\321\214\320\275\320\276_\320\277\321\200\320\270\320\262\321\217\320\267\320\260\320\275\321\213_\320\272_.py" @@ -0,0 +1,40 @@ +"""модуль и уровни обязательно привязаны к родителю + +Revision ID: 4e3cf7a2acb2 +Revises: e8496d8a36fc +Create Date: 2023-11-19 21:46:34.041826 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '4e3cf7a2acb2' +down_revision: Union[str, None] = 'e8496d8a36fc' +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.alter_column('levels', 'module_id', + existing_type=sa.UUID(), + nullable=False) + op.alter_column('modules', 'map_id', + existing_type=sa.UUID(), + nullable=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('modules', 'map_id', + existing_type=sa.UUID(), + nullable=True) + op.alter_column('levels', 'module_id', + existing_type=sa.UUID(), + nullable=True) + # ### end Alembic commands ### diff --git "a/backend/migrations/versions/2023_11_19_2220-e545a099be25_\320\276\320\261\320\275\320\276\320\262\320\273\321\217\320\265\321\202_\320\274\320\276\320\264\320\265\320\273\320\270_\320\267\320\260\320\264\320\260\321\207\320\270_\320\262\320\276\320\277\321\200\320\276\321\201\320\276\320\262_\320\270_.py" "b/backend/migrations/versions/2023_11_19_2220-e545a099be25_\320\276\320\261\320\275\320\276\320\262\320\273\321\217\320\265\321\202_\320\274\320\276\320\264\320\265\320\273\320\270_\320\267\320\260\320\264\320\260\321\207\320\270_\320\262\320\276\320\277\321\200\320\276\321\201\320\276\320\262_\320\270_.py" new file mode 100644 index 0000000..a46d81e --- /dev/null +++ "b/backend/migrations/versions/2023_11_19_2220-e545a099be25_\320\276\320\261\320\275\320\276\320\262\320\273\321\217\320\265\321\202_\320\274\320\276\320\264\320\265\320\273\320\270_\320\267\320\260\320\264\320\260\321\207\320\270_\320\262\320\276\320\277\321\200\320\276\321\201\320\276\320\262_\320\270_.py" @@ -0,0 +1,40 @@ +"""обновляет модели задачи, вопросов и ответов + +Revision ID: e545a099be25 +Revises: 4e3cf7a2acb2 +Create Date: 2023-11-19 22:20:11.830596 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e545a099be25' +down_revision: Union[str, None] = '4e3cf7a2acb2' +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.add_column('answers', sa.Column('is_correct', sa.Boolean(), nullable=False)) + op.alter_column('tasks', 'level_id', + existing_type=sa.UUID(), + nullable=False) + op.add_column('theory_blocks', sa.Column('level_id', sa.UUID(), nullable=False)) + op.create_foreign_key(None, 'theory_blocks', 'levels', ['level_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'theory_blocks', type_='foreignkey') + op.drop_column('theory_blocks', 'level_id') + op.alter_column('tasks', 'level_id', + existing_type=sa.UUID(), + nullable=True) + op.drop_column('answers', 'is_correct') + # ### end Alembic commands ### diff --git "a/backend/migrations/versions/2023_11_19_2230-842b8bf56cef_\320\274\320\265\320\275\321\217\320\265\321\202_\320\275\320\260\320\267\320\262\320\260\320\275\320\270\320\265_\320\272\320\276\320\273\320\276\320\275\320\272\320\270_\321\203_\320\276\321\202\320\262\320\265\321\202\320\260.py" "b/backend/migrations/versions/2023_11_19_2230-842b8bf56cef_\320\274\320\265\320\275\321\217\320\265\321\202_\320\275\320\260\320\267\320\262\320\260\320\275\320\270\320\265_\320\272\320\276\320\273\320\276\320\275\320\272\320\270_\321\203_\320\276\321\202\320\262\320\265\321\202\320\260.py" new file mode 100644 index 0000000..69844cd --- /dev/null +++ "b/backend/migrations/versions/2023_11_19_2230-842b8bf56cef_\320\274\320\265\320\275\321\217\320\265\321\202_\320\275\320\260\320\267\320\262\320\260\320\275\320\270\320\265_\320\272\320\276\320\273\320\276\320\275\320\272\320\270_\321\203_\320\276\321\202\320\262\320\265\321\202\320\260.py" @@ -0,0 +1,32 @@ +"""меняет название колонки у ответа + +Revision ID: 842b8bf56cef +Revises: e545a099be25 +Create Date: 2023-11-19 22:30:13.120331 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '842b8bf56cef' +down_revision: Union[str, None] = 'e545a099be25' +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.add_column('answers', sa.Column('answer', sa.String(), nullable=False)) + op.drop_column('answers', 'content') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('answers', sa.Column('content', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_column('answers', 'answer') + # ### end Alembic commands ### diff --git "a/backend/migrations/versions/2023_11_20_0114-f074fb69a98a_\321\203\320\264\320\260\320\273\321\217\320\265\321\202_\320\277\321\200\320\276\320\274\320\265\320\266\321\203\321\202\320\276\321\207\320\275\321\213\320\265_\321\202\320\260\320\261\320\273\320\270\321\206\321\213.py" "b/backend/migrations/versions/2023_11_20_0114-f074fb69a98a_\321\203\320\264\320\260\320\273\321\217\320\265\321\202_\320\277\321\200\320\276\320\274\320\265\320\266\321\203\321\202\320\276\321\207\320\275\321\213\320\265_\321\202\320\260\320\261\320\273\320\270\321\206\321\213.py" new file mode 100644 index 0000000..d221643 --- /dev/null +++ "b/backend/migrations/versions/2023_11_20_0114-f074fb69a98a_\321\203\320\264\320\260\320\273\321\217\320\265\321\202_\320\277\321\200\320\276\320\274\320\265\320\266\321\203\321\202\320\276\321\207\320\275\321\213\320\265_\321\202\320\260\320\261\320\273\320\270\321\206\321\213.py" @@ -0,0 +1,44 @@ +"""удаляет промежуточные таблицы + +Revision ID: f074fb69a98a +Revises: 842b8bf56cef +Create Date: 2023-11-20 01:14:38.556334 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f074fb69a98a' +down_revision: Union[str, None] = '842b8bf56cef' +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_table('level_tasks') + op.drop_table('level_theory_blocks') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('level_theory_blocks', + sa.Column('level_id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('theory_id', sa.UUID(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['level_id'], ['levels.id'], name='level_theory_blocks_level_id_fkey'), + sa.ForeignKeyConstraint(['theory_id'], ['theory_blocks.id'], name='level_theory_blocks_theory_id_fkey'), + sa.PrimaryKeyConstraint('level_id', 'theory_id', name='level_theory_blocks_pkey') + ) + op.create_table('level_tasks', + sa.Column('level_id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('task_id', sa.UUID(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['level_id'], ['levels.id'], name='level_tasks_level_id_fkey'), + sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], name='level_tasks_task_id_fkey'), + sa.PrimaryKeyConstraint('level_id', 'task_id', name='level_tasks_pkey') + ) + # ### end Alembic commands ### diff --git a/backend/src/game/dependencies.py b/backend/src/game/dependencies.py index 3843ecd..5bca241 100644 --- a/backend/src/game/dependencies.py +++ b/backend/src/game/dependencies.py @@ -1,7 +1,13 @@ +from repository.level_repository import LevelRepository from repository.map_repository import MapRepository -from repository.models_repository import ModuleRepository +from repository.module_repository import ModuleRepository +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.task_unit_service import TaskUnitService +from services.theory_unit_service import TheoryUnitService def map_service() -> MapService: @@ -11,5 +17,16 @@ def map_service() -> MapService: def module_service() -> ModuleService: return ModuleService(ModuleRepository) + +def level_service() -> LevelService: + return LevelService(LevelRepository) + + +def task_unit_service() -> TaskUnitService: + return TaskUnitService(TaskUnitRepository) + + +def theory_unit_service() -> TheoryUnitService: + return TheoryUnitService(TheoryUnitRepository) # def users_service(): # return UsersService(UsersRepository) diff --git a/backend/src/game/levels/enums.py b/backend/src/game/levels/enums.py new file mode 100644 index 0000000..02d64dd --- /dev/null +++ b/backend/src/game/levels/enums.py @@ -0,0 +1,7 @@ +from enum import auto, Enum + + +class LevelStates(Enum): + NotViewed = auto() + Viewed = auto() + Completed = auto() diff --git a/backend/src/game/levels/models.py b/backend/src/game/levels/models.py index 6c30930..5128188 100644 --- a/backend/src/game/levels/models.py +++ b/backend/src/game/levels/models.py @@ -1,12 +1,14 @@ -import enum import typing import uuid -from sqlalchemy import String, Boolean, UUID, ForeignKey, Enum +from sqlalchemy import String, UUID, ForeignKey from sqlalchemy.dialects import postgresql from sqlalchemy.orm import Mapped, mapped_column, relationship from database import BaseModel +from game.levels.enums import LevelStates +from game.levels.schemas import LevelRead +from game.units import TaskUnit, TheoryUnit if typing.TYPE_CHECKING: from game.modules.models import Module @@ -14,22 +16,25 @@ from game.units.theory.models import TheoryUnit -class LevelStates(enum.Enum): - NotViewed = enum.auto() - Viewed = enum.auto() - Completed = enum.auto() - - class Level(BaseModel): __tablename__ = 'levels' id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4) - module_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('modules.id'), nullable=False) - name: Mapped[str] = mapped_column(String(length=255), nullable=False) + module_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('modules.id')) + title: Mapped[str] = mapped_column(String(length=255), nullable=False) module: Mapped["Module"] = relationship(back_populates='levels') - # theory: Mapped[list["TheoryUnit"]] = relationship(secondary="level_theory_blocks", back_populates='level') - # tasks: Mapped[list["TaskUnit"]] = relationship(secondary="level_tasks", back_populates='level') + + theory_units: Mapped[list["TheoryUnit"]] = relationship(back_populates='level', lazy='selectin') + + task_units: Mapped[list["TaskUnit"]] = relationship(back_populates='level', lazy='selectin') + + def to_read_schema(self) -> LevelRead: + return LevelRead(id=self.id, + module_id=self.module_id, + title=self.title, + theory_units=[unit.to_read_schema() for unit in self.theory_units], + task_units=[unit.to_read_schema() for unit in self.task_units]) class EmployeesLevel(BaseModel): @@ -39,17 +44,3 @@ class EmployeesLevel(BaseModel): employee_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('employees.id'), nullable=False, primary_key=True) state: Mapped[LevelStates] = mapped_column( postgresql.ENUM(LevelStates, name='level_states'), nullable=False, default=LevelStates.NotViewed) - - -class LevelTask(BaseModel): - __tablename__ = "level_tasks" - - level_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('levels.id'), nullable=False, primary_key=True) - task_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('tasks.id'), nullable=False, primary_key=True) - - -class LevelTheory(BaseModel): - __tablename__ = "level_theory_blocks" - - level_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('levels.id'), nullable=False, primary_key=True) - theory_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('theory_blocks.id'), nullable=False, primary_key=True) diff --git a/backend/src/game/levels/router.py b/backend/src/game/levels/router.py index fbb567e..4bcf472 100644 --- a/backend/src/game/levels/router.py +++ b/backend/src/game/levels/router.py @@ -3,31 +3,51 @@ from fastapi import APIRouter from game.levels.schemas import LevelRead, LevelCreate, LevelUpdate +from utils.types import LevelServiceType -router = APIRouter(prefix="/levels", tags=["Levels"]) +router = APIRouter(tags=["Levels"]) -@router.get("/") -async def root() -> list[LevelRead]: - return [] +@router.get("/levels/", tags=['Dev']) +async def root(level_service: LevelServiceType) -> list[LevelRead]: + return await level_service.get_all() -@router.get("/{id}") -async def get_level(id: UUID) -> LevelRead: - return LevelRead(id=id, is_accomplished=False) +@router.get("/maps/{map_id}/modules/{module_id}/levels/") +async def get_module_levels(map_id: UUID, + module_id: UUID, + level_service: LevelServiceType) -> list[LevelRead]: + return await level_service.get_all() -@router.post("/") -async def post_level(level_create: LevelCreate) -> LevelRead: - return LevelRead() +@router.get("/maps/{map_id}/modules/{module_id}/levels/{level_id}/") +async def get_module_level(map_id: UUID, + module_id: UUID, + level_id: UUID, + level_service: LevelServiceType) -> LevelRead: + return await level_service.get_one(level_id) -@router.delete("/{id}") -async def delete_level(id: UUID) -> LevelRead: - return LevelRead(id=id, is_accomplished=False) +@router.post("/maps/{map_id}/modules/{module_id}/levels/") +async def post_level_to_module(map_id: UUID, + module_id: UUID, + level_create: LevelCreate, + level_service: LevelServiceType) -> LevelRead: + return await level_service.create_one(level_create) -@router.patch("/{id}") -async def update_level(id: UUID, - level_update: LevelUpdate) -> LevelRead: - return LevelRead(id=id, is_accomplished=True) +@router.delete("/maps/{map_id}/modules/{module_id}/levels/{level_id}/") +async def delete_level_from_module(map_id: UUID, + module_id: UUID, + level_id: UUID, + level_service: LevelServiceType) -> LevelRead: + return await level_service.delete_one(level_id) + + +@router.patch("/maps/{map_id}/modules/{module_id}/levels/{level_id}/") +async def update_level_in_module(map_id: UUID, + module_id: UUID, + level_id: UUID, + level_update: LevelUpdate, + level_service: LevelServiceType) -> LevelRead: + return await level_service.update_one(level_id, level_update) diff --git a/backend/src/game/levels/schemas.py b/backend/src/game/levels/schemas.py index 541ec60..57f8081 100644 --- a/backend/src/game/levels/schemas.py +++ b/backend/src/game/levels/schemas.py @@ -2,28 +2,29 @@ from pydantic import BaseModel, ConfigDict +from game.units.tasks.schemas import TaskUnitRead +from game.units.theory.schemas import TheoryUnitRead + class __LevelBase(BaseModel): module_id: UUID title: str - theory_units_id: list[UUID] | None - task_units_id: list[UUID] | None model_config = ConfigDict(from_attributes=True) class LevelRead(__LevelBase): id: UUID - theory_units_id: list[UUID] - task_units_id: list[UUID] + theory_units: list[TheoryUnitRead] + task_units: list[TaskUnitRead] class LevelCreate(__LevelBase): + # theory_units: list[TheoryUnitRead] | None + # task_units: list[TaskUnitRead] | None pass class LevelUpdate(__LevelBase): module_id: UUID | None = None title: str | None = None - theory_units_id: list[UUID] | None = None - task_units_id: list[UUID] | None = None diff --git a/backend/src/game/map/router.py b/backend/src/game/map/router.py index bd7f1e5..28e6ed6 100644 --- a/backend/src/game/map/router.py +++ b/backend/src/game/map/router.py @@ -9,11 +9,11 @@ @router.get("/") -async def root(map_service: MapServiceType) -> list[MapRead]: +async def get_maps(map_service: MapServiceType) -> list[MapRead]: return await map_service.get_all() -@router.get("/{id}") +@router.get("/{id}/") async def get_map(id: UUID, map_service: MapServiceType) -> MapRead: return await map_service.get_one(id) @@ -25,13 +25,13 @@ async def post_map(map_create: MapCreate, return await map_service.create_one(map_create) -@router.delete("/{id}") +@router.delete("/{id}/") async def delete_map(id: UUID, map_service: MapServiceType) -> MapRead: return await map_service.delete_one(id) -@router.patch("/{id}") +@router.patch("/{id}/") async def update_map(id: UUID, map_update: MapUpdate, map_service: MapServiceType) -> MapRead: diff --git a/backend/src/game/modules/models.py b/backend/src/game/modules/models.py index 44d43f9..b82f667 100644 --- a/backend/src/game/modules/models.py +++ b/backend/src/game/modules/models.py @@ -7,8 +7,10 @@ from database import BaseModel from game.modules.schemas import ModuleRead -from game.levels import Level -from game.map import Map + +if typing.TYPE_CHECKING: + from game.levels import Level + from game.map import Map class Module(BaseModel): @@ -21,12 +23,10 @@ class Module(BaseModel): next_module_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('modules.id'), nullable=True) map: Mapped["Map"] = relationship(back_populates='modules', lazy='selectin') + levels: Mapped[list["Level"]] = relationship(back_populates='module', lazy='selectin') levels_ids: AssociationProxy[list[uuid.UUID]] = association_proxy('levels', 'id') - # next_module: Mapped["Module"] = relationship(back_populates='previous_module') - # previous_module: Mapped["Module"] = relationship(back_populates='next_module') - def to_read_schema(self) -> ModuleRead: return ModuleRead(id=self.id, title=self.title, diff --git a/backend/src/game/modules/router.py b/backend/src/game/modules/router.py index 998c6b3..54d4458 100644 --- a/backend/src/game/modules/router.py +++ b/backend/src/game/modules/router.py @@ -6,39 +6,49 @@ from game.modules.schemas import ModuleRead, ModuleCreate, ModuleUpdate from utils.types import ModuleServiceType -router = APIRouter(prefix="/modules", tags=["Module"]) +router = APIRouter(tags=["Module"]) -@router.get("/") +@router.get("/modules/", tags=["Dev"]) async def root(module_service: ModuleServiceType) -> list[ModuleRead]: return await module_service.get_all() -@router.get("/{id}") -async def get_module(id: UUID, - module_service: ModuleServiceType) -> ModuleRead: - return await module_service.get_one(id) +@router.get("/maps/{map_id}/modules/") +async def get_map_modules(map_id: UUID, + module_service: ModuleServiceType) -> list[ModuleRead]: + return await module_service.get_all() + + +@router.get("/maps/{map_id}/modules/{module_id}/") +async def get_map_module(map_id: UUID, + module_id: UUID, + module_service: ModuleServiceType) -> ModuleRead: + return await module_service.get_one(module_id) -@router.post("/") -async def post_module(module_create: ModuleCreate, - module_service: ModuleServiceType) -> ModuleRead: +@router.post("/maps/{map_id}/modules/") +async def post_module_to_map(map_id: UUID, + module_create: ModuleCreate, + module_service: ModuleServiceType) -> ModuleRead: return await module_service.create_one(module_create) -@router.delete("/{id}") -async def delete_module(id: UUID, - module_service: ModuleServiceType) -> ModuleRead: +@router.delete("/maps/{map_id}/modules/{module_id}/") +async def delete_module_from_map(map_id: UUID, + module_id: UUID, + module_service: ModuleServiceType) -> ModuleRead: # TODO: Добавить обработку ошибок # try: # deleted_module = # except NoResultFound as exc: # raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=exc.args) - return await module_service.delete_one(id) + return await module_service.delete_one(module_id) -@router.patch("/{id}") -async def update_module(id: UUID, - module_update: ModuleUpdate, - module_service: ModuleServiceType) -> ModuleRead: - return await module_service.update_one(id, module_update) +@router.patch("/maps/{map_id}/modules/{module_id}/") +async def update_module_on_map(map_id: UUID, + module_id: UUID, + module_update: ModuleUpdate, + module_service: ModuleServiceType) -> ModuleRead: + return await module_service.update_one(module_id, module_update) diff --git a/backend/src/game/units/__init__.py b/backend/src/game/units/__init__.py new file mode 100644 index 0000000..ebdf5e2 --- /dev/null +++ b/backend/src/game/units/__init__.py @@ -0,0 +1,2 @@ +from .tasks import TaskUnit +from .theory import TheoryUnit diff --git a/backend/src/game/units/tasks/__init__.py b/backend/src/game/units/tasks/__init__.py new file mode 100644 index 0000000..540523f --- /dev/null +++ b/backend/src/game/units/tasks/__init__.py @@ -0,0 +1 @@ +from .models import TaskUnit, EmployeesTask diff --git a/backend/src/game/units/tasks/enums.py b/backend/src/game/units/tasks/enums.py new file mode 100644 index 0000000..5ba4f57 --- /dev/null +++ b/backend/src/game/units/tasks/enums.py @@ -0,0 +1,12 @@ +from enum import Enum, auto, StrEnum + + +class TaskTypes(StrEnum): + Test = auto() + + +class TaskStates(Enum): + NotViewed = auto() + Viewed = auto() + Submitted = auto() + Finished = auto() diff --git a/backend/src/game/units/tasks/models.py b/backend/src/game/units/tasks/models.py index 51e93d6..a0676c6 100644 --- a/backend/src/game/units/tasks/models.py +++ b/backend/src/game/units/tasks/models.py @@ -1,39 +1,39 @@ import uuid -import enum from typing import TYPE_CHECKING -from sqlalchemy import UUID, Integer, Enum, ForeignKey, Boolean +from sqlalchemy import UUID, Integer, ForeignKey, Boolean, String from sqlalchemy.dialects import postgresql from sqlalchemy.orm import Mapped, mapped_column, relationship from database import BaseModel +from .enums import TaskTypes, TaskStates +from .schemas import TaskUnitRead if TYPE_CHECKING: from game.levels.models import Level from questions.models import Question -class TaskTypes(enum.Enum): - Test = enum.auto() - - -class TaskStates(enum.Enum): - NotViewed = enum.auto() - Viewed = enum.auto() - Submitted = enum.auto() - Finished = enum.auto() - - class TaskUnit(BaseModel): __tablename__ = 'tasks' id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4) - type: Mapped[TaskTypes] = mapped_column(postgresql.ENUM(TaskTypes, name='task_types'), nullable=False, default=TaskTypes.Test) + type: Mapped[TaskTypes] = mapped_column(postgresql.ENUM(TaskTypes, name='task_types'), nullable=False, + default=TaskTypes.Test) + level_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('levels.id'), default=uuid.uuid4) requires_review: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) score_reward: Mapped[int] = mapped_column(Integer, nullable=False, default=1) - questions: Mapped[list["Question"]] = relationship() - level: Mapped[list["Level"]] = relationship(secondary="level_tasks") + questions: Mapped[list["Question"]] = relationship(back_populates='task', lazy='selectin') + level: Mapped[list["Level"]] = relationship(back_populates='task_units', lazy='selectin') + + def to_read_schema(self) -> TaskUnitRead: + return TaskUnitRead(id=self.id, + level_id=self.level_id, + type=self.type, + requires_review=self.requires_review, + score_reward=self.score_reward, + questions=[model.to_read_schema() for model in self.questions]) class EmployeesTask(BaseModel): @@ -41,4 +41,5 @@ class EmployeesTask(BaseModel): task_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('tasks.id'), primary_key=True) employee_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('employees.id'), primary_key=True) - state: Mapped[TaskStates] = mapped_column(postgresql.ENUM(TaskStates, name='task_states'), nullable=False, default=TaskStates.NotViewed) + state: Mapped[TaskStates] = mapped_column(postgresql.ENUM(TaskStates, name='task_states'), nullable=False, + default=TaskStates.NotViewed) diff --git a/backend/src/game/units/tasks/questions/__init__.py b/backend/src/game/units/tasks/questions/__init__.py new file mode 100644 index 0000000..bfddaad --- /dev/null +++ b/backend/src/game/units/tasks/questions/__init__.py @@ -0,0 +1 @@ +from .models import Question diff --git a/backend/src/game/units/tasks/questions/answers/__init__.py b/backend/src/game/units/tasks/questions/answers/__init__.py new file mode 100644 index 0000000..d796dce --- /dev/null +++ b/backend/src/game/units/tasks/questions/answers/__init__.py @@ -0,0 +1 @@ +from .models import AnswerOption diff --git a/backend/src/game/units/tasks/questions/answers/models.py b/backend/src/game/units/tasks/questions/answers/models.py new file mode 100644 index 0000000..0b6698c --- /dev/null +++ b/backend/src/game/units/tasks/questions/answers/models.py @@ -0,0 +1,27 @@ +import uuid +from typing import TYPE_CHECKING + +from sqlalchemy import UUID, String, ForeignKey, Boolean +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from database import BaseModel +from game.units.tasks.questions.answers.schemas import AnswerOptionRead + +if TYPE_CHECKING: + from game.units.tasks.questions import Question + + +class AnswerOption(BaseModel): + __tablename__ = 'answers' + + id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4) + question_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('questions.id'), default=uuid.uuid4) + answer: Mapped[str] = mapped_column(String, nullable=False, default='Ответ?') + is_correct: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + + question: Mapped["Question"] = relationship(back_populates='possible_answers', lazy='selectin') + + def to_read_schema(self) -> AnswerOptionRead: + return AnswerOptionRead(id=self.id, + question_id=self.question_id, + answer=self.answer) diff --git a/backend/src/game/units/tasks/questions/answers/schemas.py b/backend/src/game/units/tasks/questions/answers/schemas.py new file mode 100644 index 0000000..a76465c --- /dev/null +++ b/backend/src/game/units/tasks/questions/answers/schemas.py @@ -0,0 +1,20 @@ +from uuid import UUID + +from pydantic import BaseModel + + +class __AnswerBase(BaseModel): + answer: str + + +class AnswerOptionRead(__AnswerBase): + question_id: UUID + + +class AnswerOptionCreate(__AnswerBase): + pass + + +class AnswerOptionUpdate(__AnswerBase): + answer: str | None = None + question_id: UUID | None = None diff --git a/backend/src/game/units/tasks/questions/enums.py b/backend/src/game/units/tasks/questions/enums.py new file mode 100644 index 0000000..d70dbd9 --- /dev/null +++ b/backend/src/game/units/tasks/questions/enums.py @@ -0,0 +1,6 @@ +from enum import Enum, auto + + +class QuestionTypes(Enum): + 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 4447494..6b29e46 100644 --- a/backend/src/game/units/tasks/questions/models.py +++ b/backend/src/game/units/tasks/questions/models.py @@ -1,41 +1,34 @@ import uuid -import enum from typing import TYPE_CHECKING -from sqlalchemy import UUID, Enum, String +from sqlalchemy import UUID, String, ForeignKey from sqlalchemy.dialects import postgresql from sqlalchemy.orm import Mapped, mapped_column, relationship from database import BaseModel +from game.units.tasks.questions.enums import QuestionTypes +from game.units.tasks.questions.schemas import QuestionRead if TYPE_CHECKING: - from game.units.tasks.models import TaskUnit - - -class QuestionTypes(enum.Enum): - SingleChoice = enum.auto() - MultipleChoice = enum.auto() + from game.units.tasks import TaskUnit + from game.units.tasks.questions.answers import AnswerOption class Question(BaseModel): __tablename__ = 'questions' id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4) + task_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('tasks.id'), default=uuid.uuid4, nullable=True) 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["Answer"]] = relationship(back_populates='question') - correct_answers: Mapped[list["Answer"]] = relationship(back_populates='question') - - -class Answer(BaseModel): - __tablename__ = 'answers' - - id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4) - question_id: Mapped[uuid.UUID] = mapped_column(UUID, default=uuid.uuid4) - content: Mapped[str] = mapped_column(String, nullable=False, default='Ответ?') + possible_answers: Mapped[list["AnswerOption"]] = relationship(back_populates='question', lazy='selectin') - question: Mapped["Question"] = relationship(back_populates='answers') + def to_read_schema(self) -> QuestionRead: + return QuestionRead(id=self.id, + task_id=self.task_id, + question=self.question, + possible_answers=self.possible_answers) diff --git a/backend/src/game/units/tasks/questions/schemas.py b/backend/src/game/units/tasks/questions/schemas.py index ff27f9b..4e8676a 100644 --- a/backend/src/game/units/tasks/questions/schemas.py +++ b/backend/src/game/units/tasks/questions/schemas.py @@ -2,10 +2,10 @@ from pydantic import BaseModel, ConfigDict -from game.units.tasks.questions.models import QuestionTypes +from game.units.tasks.questions.answers.schemas import AnswerOptionRead, AnswerOptionCreate +from game.units.tasks.questions.enums import QuestionTypes -# region Question class __QuestionBase(BaseModel): task_id: UUID type: QuestionTypes @@ -16,36 +16,15 @@ class __QuestionBase(BaseModel): class QuestionRead(__QuestionBase): id: UUID - possible_answers: list["AnswerRead"] + possible_answers: list[AnswerOptionRead] class QuestionCreate(__QuestionBase): - correct_answer: "AnswerCreate" - possible_answers: list["AnswerCreate"] + correct_answer: AnswerOptionCreate + possible_answers: list[AnswerOptionCreate] class QuestionUpdate(__QuestionBase): type: QuestionTypes | None = None question: str | None = None - possible_answers: list["AnswerUpdate"] | None = None - - -# endregion Question - -# region Answer -class __AnswerBase(BaseModel): - content: str - - -class AnswerRead(__AnswerBase): - question_id: UUID - - -class AnswerCreate(__AnswerBase): - pass - - -class AnswerUpdate(__AnswerBase): - content: str | None = None - question_id: UUID | None = None -# endregion Answer + possible_answers: list[AnswerOptionCreate] | None = None diff --git a/backend/src/game/units/tasks/router.py b/backend/src/game/units/tasks/router.py index a922ce3..ad1c8f1 100644 --- a/backend/src/game/units/tasks/router.py +++ b/backend/src/game/units/tasks/router.py @@ -3,31 +3,48 @@ from fastapi import APIRouter from game.units.tasks.schemas import TaskUnitRead, TaskUnitCreate, TaskUnitUpdate +from utils.types import TaskUnitServiceType -router = APIRouter(prefix="/tasks", tags=["Task"]) +router = APIRouter(tags=["Task"]) -@router.get("/") -async def root() -> list[TaskUnitRead]: - return [] +@router.get("/tasks/", tags=['Dev']) +async def root(task_unit_service: TaskUnitServiceType) -> list[TaskUnitRead]: + return await task_unit_service.get_all() -@router.get("/{id}") -async def get_task(id: UUID) -> TaskUnitRead: - return TaskUnitRead() +@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("/") -async def post_task(task_create: TaskUnitCreate) -> TaskUnitRead: - return TaskUnitRead() +@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: + return await task_unit_service.create_one(level_id, task_create) -@router.delete("/{id}") -async def delete_task(id: UUID) -> TaskUnitRead: - return TaskUnitRead() +@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: + return await task_unit_service.delete_one(task_id) -@router.patch("/{id}") -async def update_task(id: UUID, - task_update: TaskUnitUpdate) -> TaskUnitRead: - return TaskUnitRead() +@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: + return await task_unit_service.update_one(task_id, task_update) diff --git a/backend/src/game/units/tasks/schemas.py b/backend/src/game/units/tasks/schemas.py index 656a7c3..cde51ce 100644 --- a/backend/src/game/units/tasks/schemas.py +++ b/backend/src/game/units/tasks/schemas.py @@ -2,12 +2,11 @@ from pydantic import BaseModel, ConfigDict -from game.units.tasks.models import TaskTypes from game.units.tasks.questions.schemas import QuestionRead, QuestionCreate, QuestionUpdate +from game.units.tasks.enums import TaskTypes class __TaskUnitBase(BaseModel): - questions: list[QuestionRead] type: TaskTypes score_reward: int requires_review: bool @@ -16,16 +15,18 @@ class __TaskUnitBase(BaseModel): class TaskUnitRead(__TaskUnitBase): + questions: list[QuestionRead] + level_id: UUID id: UUID class TaskUnitCreate(__TaskUnitBase): - questions: list[QuestionCreate] + type: TaskTypes = TaskTypes.Test + score_reward: int = 1 + requires_review: bool = False class TaskUnitUpdate(__TaskUnitBase): - questions: list[QuestionUpdate] | None = None type: TaskTypes | None = None score_reward: int | None = None requires_review: bool | None = None - diff --git a/backend/src/game/units/theory/__init__.py b/backend/src/game/units/theory/__init__.py new file mode 100644 index 0000000..fba643e --- /dev/null +++ b/backend/src/game/units/theory/__init__.py @@ -0,0 +1 @@ +from .models import TheoryUnit diff --git a/backend/src/game/units/theory/models.py b/backend/src/game/units/theory/models.py index 2c00a08..551f326 100644 --- a/backend/src/game/units/theory/models.py +++ b/backend/src/game/units/theory/models.py @@ -6,6 +6,7 @@ from sqlalchemy_utils import URLType from database import BaseModel +from game.units.theory.schemas import TheoryUnitRead if TYPE_CHECKING: from game.levels.models import Level @@ -15,18 +16,24 @@ class TheoryUnit(BaseModel): __tablename__ = 'theory_blocks' id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4) - theme: Mapped[str] = mapped_column(String, nullable=False) + title: Mapped[str] = mapped_column(String, nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False, default="Текст теории") + level_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('levels.id'), default=uuid.uuid4) # videos: Mapped[list["TheoryVideo"]] = relationship(back_populates="theory_block") - level: Mapped[list["Level"]] = relationship(secondary="level_theory_blocks") - - -class TheoryVideo(BaseModel): - __tablename__ = 'theory_videos' - - id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4) - theory_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('theory_blocks.id'), nullable=False) - url: Mapped[URL] = mapped_column(URLType, nullable=False) - - theory: Mapped["TheoryUnit"] = relationship(back_populates="theory_video") + level: Mapped[list["Level"]] = relationship(back_populates='theory_units') + + def to_read_model(self) -> TheoryUnitRead: + return TheoryUnitRead(id=self.id, + title=self.title, + content=self.content) + + +# class TheoryVideo(BaseModel): +# __tablename__ = 'theory_videos' +# +# id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4) +# theory_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey('theory_blocks.id'), nullable=False) +# url: Mapped[URL] = mapped_column(URLType, nullable=False) +# +# theory: Mapped["TheoryUnit"] = relationship(back_populates="theory_video") diff --git a/backend/src/game/units/theory/router.py b/backend/src/game/units/theory/router.py index 63c9ba8..caf107f 100644 --- a/backend/src/game/units/theory/router.py +++ b/backend/src/game/units/theory/router.py @@ -2,54 +2,49 @@ from fastapi import APIRouter -from game.units.theory.schemas import (TheoryUnitRead, TheoryUnitCreate, TheoryUnitUpdate, - TheoryVideoRead, TheoryVideoCreate, TheoryVideoUpdate) +from game.units.theory.schemas import TheoryUnitRead, TheoryUnitCreate, TheoryUnitUpdate +from utils.types import TheoryUnitServiceType -router = APIRouter(prefix="/theory", tags=["Theory"]) +router = APIRouter(tags=["Theory"]) -@router.get("/") -async def root() -> list[TheoryUnitRead]: - return [] +@router.get('/theory/', tags=['Dev']) +async def root(theory_unit_service: TheoryUnitServiceType) -> list[TheoryUnitRead]: + return await theory_unit_service.get_all() -@router.get("/{id}") -async def get_theory_block(id: UUID) -> TheoryUnitRead: - return TheoryUnitRead() +@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("/videos/{id}") -async def get_theory_block_videos(id: UUID) -> list[TheoryVideoRead]: - return [] +@router.post("/maps/{map_id}/modules/{module_id}/levels/{level_id}/theory/") +async def post_theory_unit_to_level(map_id: UUID, + module_id: UUID, + level_id: UUID, + theory_create: TheoryUnitCreate, + theory_unit_service: TheoryUnitServiceType) -> TheoryUnitRead: + return await theory_unit_service.create_one(level_id, theory_create) -@router.post("/") -async def post_theory_block(theory_create: TheoryUnitCreate) -> TheoryUnitRead: - return TheoryUnitRead() +@router.delete("/maps/{map_id}/modules/{module_id}/levels/{level_id}/theory/{theory_id}/") +async def delete_theory_unit_from_level(map_id: UUID, + module_id: UUID, + level_id: UUID, + theory_id: UUID, + theory_unit_service: TheoryUnitServiceType) -> TheoryUnitRead: + return await theory_unit_service.delete_one(theory_id) -@router.post("/videos/") -async def post_theory_block_video(theory_video_create: TheoryVideoCreate) -> TheoryVideoRead: - return TheoryVideoRead() - - -@router.delete("/{id}") -async def delete_theory_block(id: UUID) -> TheoryUnitRead: - return TheoryUnitRead() - - -@router.delete("/videos/{id}") -async def delete_theory_block_video(id: UUID) -> TheoryVideoRead: - return TheoryVideoRead() - - -@router.patch("/{id}") -async def update_theory_block(id: UUID, - theory_update: TheoryUnitUpdate) -> TheoryUnitRead: - return TheoryUnitRead() - - -@router.patch("/videos/{id}") -async def update_theory_block_video(id: UUID, - theory_video_update: TheoryVideoUpdate) -> TheoryVideoRead: - return TheoryVideoRead() +@router.patch("/maps/{map_id}/modules/{module_id}/levels/{level_id}/theory/{theory_id}/") +async def update_theory_unit_in_level(map_id: UUID, + module_id: UUID, + level_id: UUID, + theory_id: UUID, + theory_update: TheoryUnitUpdate, + theory_unit_service: TheoryUnitServiceType) -> TheoryUnitRead: + return await theory_unit_service.update_one(theory_id, theory_update) diff --git a/backend/src/main.py b/backend/src/main.py index ed5c7a0..dc2e93b 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -2,11 +2,11 @@ from fastapi.responses import RedirectResponse from fastapi.middleware.cors import CORSMiddleware -# from game.levels.router import router as levels_router +from game.levels.router import router as levels_router 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.theory.router import router as theory_router +from game.units.tasks.router import router as tasks_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 @@ -54,11 +54,11 @@ async def unprotected_route(): include_routers( - # levels_router, + levels_router, modules_router, map_router, - # tasks_router, - # theory_router, + tasks_router, + theory_router, # users_router, # tutors_router, # employees_router, diff --git a/backend/src/repository/level_repository.py b/backend/src/repository/level_repository.py new file mode 100644 index 0000000..e36963a --- /dev/null +++ b/backend/src/repository/level_repository.py @@ -0,0 +1,6 @@ +from game.levels import Level +from repository.sqlalchemy_repository import SQLAlchemyRepository + + +class LevelRepository(SQLAlchemyRepository): + model = Level diff --git a/backend/src/repository/models_repository.py b/backend/src/repository/module_repository.py similarity index 100% rename from backend/src/repository/models_repository.py rename to backend/src/repository/module_repository.py diff --git a/backend/src/repository/sqlalchemy_repository.py b/backend/src/repository/sqlalchemy_repository.py index 5afaa9a..782a380 100644 --- a/backend/src/repository/sqlalchemy_repository.py +++ b/backend/src/repository/sqlalchemy_repository.py @@ -25,7 +25,7 @@ async def find_all(self) -> list[model]: async def get_one(self, id: uuid.UUID) -> model: async with async_session_maker() as session: - stmt = select(self.model).where(self.model.id == id).order_by(self.model.title) + stmt = select(self.model).where(self.model.id == id) res = await session.execute(stmt) return res.scalar_one() @@ -37,6 +37,7 @@ async def delete_one(self, id: uuid.UUID) -> model: return res.scalar_one() async def update_one(self, id: uuid.UUID, model: dict[str, typing.Any]) -> model: + model = {key: value for key, value in model.items() if value} async with async_session_maker() as session: stmt = update(self.model).where(self.model.id == id).values(**model).returning(self.model) res = await session.execute(stmt) diff --git a/backend/src/repository/task_unit_repository.py b/backend/src/repository/task_unit_repository.py new file mode 100644 index 0000000..9cfbb7f --- /dev/null +++ b/backend/src/repository/task_unit_repository.py @@ -0,0 +1,6 @@ +from game.units import TaskUnit +from repository.sqlalchemy_repository import SQLAlchemyRepository + + +class TaskUnitRepository(SQLAlchemyRepository): + model = TaskUnit diff --git a/backend/src/repository/theory_unit_repository.py b/backend/src/repository/theory_unit_repository.py new file mode 100644 index 0000000..02651f5 --- /dev/null +++ b/backend/src/repository/theory_unit_repository.py @@ -0,0 +1,6 @@ +from game.units import TheoryUnit +from repository.sqlalchemy_repository import SQLAlchemyRepository + + +class TheoryUnitRepository(SQLAlchemyRepository): + model = TheoryUnit diff --git a/backend/src/services/level_service.py b/backend/src/services/level_service.py new file mode 100644 index 0000000..7093de2 --- /dev/null +++ b/backend/src/services/level_service.py @@ -0,0 +1,32 @@ +import uuid + +from game.levels.schemas import LevelRead, LevelCreate, LevelUpdate +from repository.abstract import AbstractRepository + + +class LevelService: + __level_repo: AbstractRepository + + def __init__(self, level_repo: type[AbstractRepository]): + self.__level_repo = level_repo() + + async def create_one(self, level_create: LevelCreate) -> LevelRead: + level_dict = level_create.model_dump() + return await self.__level_repo.add_one(level_dict) + + async def get_all(self) -> list[LevelRead]: + models = await self.__level_repo.find_all() + return [model.to_read_schema() for model in models] + + async def get_one(self, id: uuid.UUID) -> LevelRead: + res = await self.__level_repo.get_one(id) + return res.to_read_schema() + + async def delete_one(self, id: uuid.UUID) -> LevelRead: + res = await self.__level_repo.delete_one(id) + return res.to_read_schema() + + async def update_one(self, id: uuid.UUID, level_update: LevelUpdate) -> LevelRead: + level_dict = level_update.model_dump() + res = await self.__level_repo.update_one(id, level_dict) + return res.to_read_schema() diff --git a/backend/src/services/task_unit_service.py b/backend/src/services/task_unit_service.py new file mode 100644 index 0000000..78289c2 --- /dev/null +++ b/backend/src/services/task_unit_service.py @@ -0,0 +1,33 @@ +import uuid + +from game.units.tasks.schemas import TaskUnitCreate, TaskUnitRead, TaskUnitUpdate +from repository.abstract import AbstractRepository + + +class TaskUnitService: + __task_unit_repo: AbstractRepository + + def __init__(self, map_repo: type[AbstractRepository]): + self.__task_unit_repo = map_repo() + + async def create_one(self, level_id: uuid.UUID, schema_create: TaskUnitCreate) -> TaskUnitRead: + schema_dict = schema_create.model_dump() + schema_dict['level_id'] = level_id + return await self.__task_unit_repo.add_one(schema_dict) + + async def get_all(self) -> list[TaskUnitRead]: + models = await self.__task_unit_repo.find_all() + return [model.to_read_schema() for model in models] + + async def get_one(self, id: uuid.UUID) -> TaskUnitRead: + res = await self.__task_unit_repo.get_one(id) + return res.to_read_schema() + + async def delete_one(self, id: uuid.UUID) -> TaskUnitRead: + res = await self.__task_unit_repo.delete_one(id) + return res.to_read_schema() + + async def update_one(self, id: uuid.UUID, schema_update: TaskUnitUpdate) -> TaskUnitRead: + schema_dict = schema_update.model_dump() + res = await self.__task_unit_repo.update_one(id, schema_dict) + return res.to_read_schema() diff --git a/backend/src/services/theory_unit_service.py b/backend/src/services/theory_unit_service.py new file mode 100644 index 0000000..67e0d94 --- /dev/null +++ b/backend/src/services/theory_unit_service.py @@ -0,0 +1,33 @@ +import uuid + +from game.units.theory.schemas import TheoryUnitRead, TheoryUnitCreate, TheoryUnitUpdate +from repository.abstract import AbstractRepository + + +class TheoryUnitService: + __theory_unit_repo: AbstractRepository + + def __init__(self, map_repo: type[AbstractRepository]): + self.__theory_unit_repo = map_repo() + + async def create_one(self, level_id: uuid.UUID, schema_create: TheoryUnitCreate) -> TheoryUnitRead: + schema_dict = schema_create.model_dump() + schema_dict['level_id'] = level_id + return await self.__theory_unit_repo.add_one(schema_dict) + + async def get_all(self) -> list[TheoryUnitRead]: + models = await self.__theory_unit_repo.find_all() + return [model.to_read_schema() for model in models] + + async def get_one(self, id: uuid.UUID) -> TheoryUnitRead: + res = await self.__theory_unit_repo.get_one(id) + return res.to_read_schema() + + async def delete_one(self, id: uuid.UUID) -> TheoryUnitRead: + res = await self.__theory_unit_repo.delete_one(id) + return res.to_read_schema() + + async def update_one(self, id: uuid.UUID, schema_update: TheoryUnitUpdate) -> TheoryUnitRead: + schema_dict = schema_update.model_dump() + res = await self.__theory_unit_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 530dd1c..c53a870 100644 --- a/backend/src/utils/types.py +++ b/backend/src/utils/types.py @@ -5,9 +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 +from game.dependencies import map_service, module_service, level_service, task_unit_service, theory_unit_service +from services.level_service import LevelService from services.map_service import MapService from services.module_service import ModuleService +from services.task_unit_service import TaskUnitService +from services.theory_unit_service import TheoryUnitService from users.models import User CurrentUser = Annotated[User, Depends(current_user)] @@ -15,7 +18,10 @@ MapServiceType = Annotated[MapService, Depends(map_service)] ModuleServiceType = Annotated[ModuleService, Depends(module_service)] +LevelServiceType = Annotated[LevelService, Depends(level_service)] +TaskUnitServiceType = Annotated[TaskUnitService, Depends(task_unit_service)] +TheoryUnitServiceType = Annotated[TheoryUnitService, Depends(theory_unit_service)] -AsyncDBSession = Annotated[AsyncSession, Depends(get_async_session)] -QueryDBLimit = Annotated[int, Query(ge=0, le=10 ** 3)] -QueryDBOffset = Annotated[int, Query(ge=0, le=10 ** 6)] \ No newline at end of file +# AsyncDBSession = Annotated[AsyncSession, Depends(get_async_session)] +# QueryDBLimit = Annotated[int, Query(ge=0, le=10 ** 3)] +# QueryDBOffset = Annotated[int, Query(ge=0, le=10 ** 6)]