diff --git a/alembic/versions/d3bd5df63c1b_test_model_add_password_and_expires_at_.py b/alembic/versions/d3bd5df63c1b_test_model_add_password_and_expires_at_.py
new file mode 100644
index 0000000..929972d
--- /dev/null
+++ b/alembic/versions/d3bd5df63c1b_test_model_add_password_and_expires_at_.py
@@ -0,0 +1,31 @@
+"""test model add password and expires_at fields
+
+Revision ID: d3bd5df63c1b
+Revises: f63140aa50c0
+Create Date: 2026-01-02 17:05:33.443875
+
+"""
+from collections.abc import Sequence
+
+from alembic import op
+import sqlalchemy as sa
+
+
+revision: str = 'd3bd5df63c1b'
+down_revision: str | None = 'f63140aa50c0'
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('tests', sa.Column('password', sa.String(length=255), nullable=True))
+ op.add_column('tests', sa.Column('expires_at', sa.DateTime(), nullable=True))
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('tests', 'expires_at')
+ op.drop_column('tests', 'password')
+ # ### end Alembic commands ###
diff --git a/src/trudex/application/__main__.py b/src/trudex/application/__main__.py
index a728242..a2484d0 100644
--- a/src/trudex/application/__main__.py
+++ b/src/trudex/application/__main__.py
@@ -8,12 +8,19 @@ from aiogram_dialog import setup_dialogs
from dishka import make_async_container
from dishka.integrations.aiogram import setup_dishka
+from trudex.application.bot.admin_dialogs.broadcast import broadcast_dialog as admin_broadcast_dialog
from trudex.application.bot.admin_dialogs.main_menu import admin_menu_dialog
+from trudex.application.bot.admin_dialogs.tests import tests_dialog as admin_tests_dialog
+from trudex.application.bot.admin_dialogs.users import users_dialog as admin_users_dialog
+from trudex.application.bot.creator_dialogs.broadcast import broadcast_dialog as creator_broadcast_dialog
from trudex.application.bot.creator_dialogs.main_menu import creator_menu_dialog
+from trudex.application.bot.creator_dialogs.tests import tests_dialog as creator_tests_dialog
+from trudex.application.bot.creator_dialogs.users import users_dialog as creator_users_dialog
from trudex.application.bot.handlers import router
from trudex.application.bot.middlewares.reject_not_admin import RejectNotAdminMiddleware
from trudex.application.bot.middlewares.reject_not_creator import RejectNotCreatorMiddleware
from trudex.application.bot.user_dialogs.main_menu import user_menu_dialog
+from trudex.infrastructure.database.repo.user import UserRepository
from trudex.infrastructure.di import DatabaseProvider
from trudex.infrastructure.utils.bot_commands import setup_bot_commands
from trudex.infrastructure.utils.config import Config
@@ -33,23 +40,33 @@ async def main() -> None:
)
dp = Dispatcher()
- dp.message.middleware(RejectNotAdminMiddleware())
- dp.message.middleware(RejectNotCreatorMiddleware())
- dp.include_router(router)
- dp.include_router(user_menu_dialog)
- dp.include_router(admin_menu_dialog)
- dp.include_router(creator_menu_dialog)
+ dp.include_routers(
+ router,
+ user_menu_dialog,
+ admin_menu_dialog,
+ admin_users_dialog,
+ admin_tests_dialog,
+ admin_broadcast_dialog,
+ creator_menu_dialog,
+ creator_users_dialog,
+ creator_tests_dialog,
+ creator_broadcast_dialog,
+ )
+
+ router.message.middleware(RejectNotAdminMiddleware())
+ router.message.middleware(RejectNotCreatorMiddleware())
container = make_async_container(DatabaseProvider())
- setup_dishka(container, dp, auto_inject=True)
setup_dialogs(dp)
+ setup_dishka(container, dp, auto_inject=True)
async with container() as request_container:
- from trudex.infrastructure.database.repo.user import UserRepository
user_repo = await request_container.get(UserRepository)
await setup_bot_commands(bot, config, user_repo)
+ await bot.delete_webhook(drop_pending_updates=True)
+
logging.info("Бот запущен")
try:
diff --git a/src/trudex/application/bot/admin_dialogs/broadcast.py b/src/trudex/application/bot/admin_dialogs/broadcast.py
new file mode 100644
index 0000000..69f9e8b
--- /dev/null
+++ b/src/trudex/application/bot/admin_dialogs/broadcast.py
@@ -0,0 +1,73 @@
+from aiogram.types import CallbackQuery, Message
+from aiogram_dialog import Dialog, DialogManager, Window, StartMode
+from aiogram_dialog.widgets.input import MessageInput
+from aiogram_dialog.widgets.kbd import Button, Row
+from aiogram_dialog.widgets.text import Const
+from dishka import FromDishka
+from dishka.integrations.aiogram_dialog import inject
+
+from trudex.application.bot.admin_dialogs.states import AdminBroadcastSG, AdminMenuSG
+from trudex.infrastructure.database.dao.user import UserDAO
+from trudex.infrastructure.utils.broadcast import broadcast_message
+
+
+async def on_broadcast_input(message: Message, _widget: MessageInput, manager: DialogManager):
+ manager.dialog_data["broadcast_message_id"] = message.message_id
+ manager.dialog_data["broadcast_chat_id"] = message.chat.id
+ await manager.switch_to(AdminBroadcastSG.broadcast_confirm)
+
+
+@inject
+async def on_broadcast_confirm(_callback: CallbackQuery, _button: Button, manager: DialogManager, user_dao: FromDishka[UserDAO]):
+ message_id = manager.dialog_data.get("broadcast_message_id")
+ chat_id = manager.dialog_data.get("broadcast_chat_id")
+
+ if not message_id or not chat_id or not _callback.message:
+ await _callback.answer("Ошибка: сообщение не найдено")
+ return
+
+ await _callback.message.answer("⏳ Рассылка началась...")
+
+ bot = _callback.bot
+ if not bot:
+ await _callback.answer("Ошибка: бот не найден")
+ return
+
+ stats = await broadcast_message(bot, message_id, chat_id, user_dao)
+
+ stats_text = (
+ f"✅ Рассылка завершена\n\n"
+ f"Всего пользователей: {stats.total}\n"
+ f"Успешно отправлено: {stats.success}\n"
+ f"Не удалось отправить: {stats.failed}"
+ )
+
+ await _callback.message.answer(stats_text)
+ await manager.done()
+
+
+async def on_broadcast_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await _callback.answer("Рассылка отменена")
+ await manager.done()
+
+
+async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
+
+
+broadcast_dialog = Dialog(
+ Window(
+ Const("📢 Рассылка\n\nОтправьте сообщение, которое хотите разослать всем пользователям:"),
+ MessageInput(on_broadcast_input),
+ Button(Const("◀️ Отмена"), id="back", on_click=on_back_to_main),
+ state=AdminBroadcastSG.broadcast_input,
+ ),
+ Window(
+ Const("⚠️ Подтверждение рассылки\n\nВы уверены, что хотите отправить это сообщение всем пользователям?"),
+ Row(
+ Button(Const("✅ Да"), id="broadcast_confirm", on_click=on_broadcast_confirm),
+ Button(Const("❌ Нет"), id="broadcast_cancel", on_click=on_broadcast_cancel),
+ ),
+ state=AdminBroadcastSG.broadcast_confirm,
+ ),
+)
diff --git a/src/trudex/application/bot/admin_dialogs/main_menu.py b/src/trudex/application/bot/admin_dialogs/main_menu.py
index 5eb37d4..dc967bd 100644
--- a/src/trudex/application/bot/admin_dialogs/main_menu.py
+++ b/src/trudex/application/bot/admin_dialogs/main_menu.py
@@ -1,139 +1,21 @@
-from aiogram.types import CallbackQuery, Message
-from aiogram_dialog import Dialog, DialogManager, Window
-from aiogram_dialog.widgets.input import MessageInput
-from aiogram_dialog.widgets.kbd import Back, Button, Cancel, Column, Row, ScrollingGroup, Select, SwitchTo
-from aiogram_dialog.widgets.text import Const, Format
-from dishka import FromDishka
-from dishka.integrations.aiogram import CONTAINER_NAME
-from dishka.integrations.aiogram_dialog import inject
+from aiogram.types import CallbackQuery
+from aiogram_dialog import Dialog, DialogManager, Window, StartMode
+from aiogram_dialog.widgets.kbd import Button, Column
+from aiogram_dialog.widgets.text import Const
-from trudex.application.bot.admin_dialogs.states import AdminMenuSG
-from trudex.infrastructure.database.dao.user import UserDAO
-from trudex.infrastructure.utils.broadcast import broadcast_message
+from trudex.application.bot.admin_dialogs.states import AdminMenuSG, AdminUsersSG, AdminTestsSG, AdminBroadcastSG
-@inject
-async def get_users_data(user_dao: FromDishka[UserDAO], **_kwargs):
- users = await user_dao.get_all()
-
- return {
- "users": [
- (f"{u.first_name} (@{u.username or 'нет'})", u.id)
- for u in users
- ],
- "count": len(users),
- }
-
-
-@inject
-async def get_user_detail_data(dialog_manager: DialogManager, user_dao: FromDishka[UserDAO], **_kwargs):
- user_id = dialog_manager.dialog_data.get("selected_user_id")
- if not user_id:
- return {"user_info": "Пользователь не выбран"}
-
- user = await user_dao.get_by_id(user_id)
- if not user:
- return {"user_info": "Пользователь не найден"}
-
- username_str = f"@{user.username}" if user.username else "—"
- last_name_str = user.last_name or "—"
- group_str = str(user.group) if user.group else "—"
- admin_status = "✅ Да" if user.is_admin else "❌ Нет"
-
- user_info = (
- f"👤 Информация о пользователе\n\n"
- f"ID: {user.id}\n"
- f"Имя: {user.first_name}\n"
- f"Фамилия: {last_name_str}\n"
- f"Username: {username_str}\n"
- f"Группа: {group_str}\n"
- f"Администратор: {admin_status}"
- )
-
- return {"user_info": user_info}
-
-
-async def on_user_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str):
- manager.dialog_data["selected_user_id"] = int(item_id)
- await manager.switch_to(AdminMenuSG.user_detail)
-
-
-async def on_input_mode(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminMenuSG.users_input)
-
-
-async def on_user_input(message: Message, _widget: MessageInput, manager: DialogManager):
- container = manager.middleware_data[CONTAINER_NAME]
- user_dao = await container.get(UserDAO)
-
- text = (message.text or "").strip()
-
- user = None
- if text.startswith("@"):
- username = text[1:]
- all_users = await user_dao.get_all()
- user = next((u for u in all_users if u.username == username), None)
- elif text.isdigit():
- user = await user_dao.get_by_id(int(text))
-
- if not user:
- await message.answer("❌ Пользователь не найден в базе данных.")
- return
-
- manager.dialog_data["selected_user_id"] = user.id
- await manager.switch_to(AdminMenuSG.user_detail)
-
-
-async def on_broadcast_input(message: Message, _widget: MessageInput, manager: DialogManager):
- manager.dialog_data["broadcast_message_id"] = message.message_id
- manager.dialog_data["broadcast_chat_id"] = message.chat.id
- await manager.switch_to(AdminMenuSG.broadcast_confirm)
-
-
-@inject
-async def on_broadcast_confirm(_callback: CallbackQuery, _button: Button, manager: DialogManager, user_dao: FromDishka[UserDAO]):
- message_id = manager.dialog_data.get("broadcast_message_id")
- chat_id = manager.dialog_data.get("broadcast_chat_id")
-
- if not message_id or not chat_id or not _callback.message:
- await _callback.answer("Ошибка: сообщение не найдено")
- return
-
- await _callback.message.answer("⏳ Рассылка началась...")
-
- bot = _callback.bot
- if not bot:
- await _callback.answer("Ошибка: бот не найден")
- return
-
- stats = await broadcast_message(bot, message_id, chat_id, user_dao)
-
- stats_text = (
- f"✅ Рассылка завершена\n\n"
- f"Всего пользователей: {stats.total}\n"
- f"Успешно отправлено: {stats.success}\n"
- f"Не удалось отправить: {stats.failed}"
- )
-
- await _callback.message.answer(stats_text)
- await manager.done()
-
-
-async def on_broadcast_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await _callback.answer("Рассылка отменена")
- await manager.switch_to(AdminMenuSG.main)
-
-
-async def on_tests_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None:
- await _callback.answer("Управление тестами")
+async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
+ await manager.start(AdminTestsSG.tests_list, mode=StartMode.RESET_STACK)
async def on_users_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(AdminMenuSG.users_list)
+ await manager.start(AdminUsersSG.users_list, mode=StartMode.RESET_STACK)
async def on_broadcast_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(AdminMenuSG.broadcast_input)
+ await manager.start(AdminBroadcastSG.broadcast_input, mode=StartMode.RESET_STACK)
admin_menu_dialog = Dialog(
@@ -146,51 +28,4 @@ admin_menu_dialog = Dialog(
),
state=AdminMenuSG.main,
),
- Window(
- Format("👥 Пользователи\n\nВсего: {count}"),
- ScrollingGroup(
- Select(
- Format("{item[0]}"),
- id="user_select",
- item_id_getter=lambda x: x[1],
- items="users",
- on_click=on_user_selected,
- ),
- id="users_scroll",
- width=1,
- height=7,
- ),
- Column(
- Button(Const("✏️ Ввести ID/Username"), id="input_mode", on_click=on_input_mode),
- Back(Const("◀️ Назад")),
- ),
- state=AdminMenuSG.users_list,
- getter=get_users_data,
- ),
- Window(
- Const("Введите ID или @username пользователя:"),
- MessageInput(on_user_input),
- SwitchTo(Const("◀️ Назад"), id="back_to_list", state=AdminMenuSG.users_list),
- state=AdminMenuSG.users_input,
- ),
- Window(
- Format("{user_info}"),
- SwitchTo(Const("◀️ Назад"), id="back_to_list", state=AdminMenuSG.users_list),
- state=AdminMenuSG.user_detail,
- getter=get_user_detail_data,
- ),
- Window(
- Const("📢 Рассылка\n\nОтправьте сообщение, которое хотите разослать всем пользователям:"),
- MessageInput(on_broadcast_input),
- SwitchTo(Const("◀️ Отмена"), id="cancel_broadcast", state=AdminMenuSG.main),
- state=AdminMenuSG.broadcast_input,
- ),
- Window(
- Const("⚠️ Подтверждение рассылки\n\nВы уверены, что хотите отправить это сообщение всем пользователям?"),
- Row(
- Button(Const("✅ Да"), id="broadcast_confirm", on_click=on_broadcast_confirm),
- Button(Const("❌ Нет"), id="broadcast_cancel", on_click=on_broadcast_cancel),
- ),
- state=AdminMenuSG.broadcast_confirm,
- ),
)
diff --git a/src/trudex/application/bot/admin_dialogs/states.py b/src/trudex/application/bot/admin_dialogs/states.py
index 266e10b..7bb6da9 100644
--- a/src/trudex/application/bot/admin_dialogs/states.py
+++ b/src/trudex/application/bot/admin_dialogs/states.py
@@ -3,8 +3,18 @@ from aiogram.fsm.state import State, StatesGroup
class AdminMenuSG(StatesGroup):
main = State()
+
+
+class AdminUsersSG(StatesGroup):
users_list = State()
users_input = State()
user_detail = State()
+
+
+class AdminTestsSG(StatesGroup):
+ tests_list = State()
+
+
+class AdminBroadcastSG(StatesGroup):
broadcast_input = State()
broadcast_confirm = State()
diff --git a/src/trudex/application/bot/admin_dialogs/tests.py b/src/trudex/application/bot/admin_dialogs/tests.py
new file mode 100644
index 0000000..eae7fe9
--- /dev/null
+++ b/src/trudex/application/bot/admin_dialogs/tests.py
@@ -0,0 +1,60 @@
+from aiogram.types import CallbackQuery
+from aiogram_dialog import Dialog, DialogManager, Window, StartMode
+from aiogram_dialog.widgets.kbd import Button, Column, ScrollingGroup, Select
+from aiogram_dialog.widgets.text import Const, Format
+from dishka import FromDishka
+from dishka.integrations.aiogram_dialog import inject
+
+from trudex.application.bot.admin_dialogs.states import AdminTestsSG, AdminMenuSG
+from trudex.infrastructure.database.dao.test import TestDAO
+
+
+@inject
+async def get_tests_data(test_dao: FromDishka[TestDAO], **_kwargs):
+ tests = await test_dao.get_all()
+
+ return {
+ "tests": [
+ (t.title, t.id)
+ for t in tests
+ ],
+ "count": len(tests),
+ }
+
+
+async def on_test_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str):
+ manager.dialog_data["selected_test_id"] = int(item_id)
+ await _callback.answer("Тест выбран")
+
+
+async def on_add_test_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager):
+ await _callback.answer("Добавление теста")
+
+
+async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
+
+
+tests_dialog = Dialog(
+ Window(
+ Format("📝 Тесты\n\nВсего: {count}"),
+ ScrollingGroup(
+ Select(
+ Format("{item[0]}"),
+ id="test_select",
+ item_id_getter=lambda x: x[1],
+ items="tests",
+ on_click=on_test_selected,
+ ),
+ id="tests_scroll",
+ width=1,
+ height=7,
+ ),
+ Column(
+ Button(Const("➕ Добавить тест"), id="add_test", on_click=on_add_test_clicked),
+ Button(Const("◀️ Назад"), id="back", on_click=on_back_clicked),
+ ),
+ state=AdminTestsSG.tests_list,
+ getter=get_tests_data,
+ ),
+)
diff --git a/src/trudex/application/bot/admin_dialogs/users.py b/src/trudex/application/bot/admin_dialogs/users.py
new file mode 100644
index 0000000..f0d3cf8
--- /dev/null
+++ b/src/trudex/application/bot/admin_dialogs/users.py
@@ -0,0 +1,124 @@
+from aiogram.types import CallbackQuery, Message
+from aiogram_dialog import Dialog, DialogManager, Window, StartMode
+from aiogram_dialog.widgets.input import MessageInput
+from aiogram_dialog.widgets.kbd import Button, Column, ScrollingGroup, Select, SwitchTo
+from aiogram_dialog.widgets.text import Const, Format
+from dishka import FromDishka
+from dishka.integrations.aiogram import CONTAINER_NAME
+from dishka.integrations.aiogram_dialog import inject
+
+from trudex.application.bot.admin_dialogs.states import AdminUsersSG, AdminMenuSG
+from trudex.infrastructure.database.dao.user import UserDAO
+
+
+@inject
+async def get_users_data(user_dao: FromDishka[UserDAO], **_kwargs):
+ users = await user_dao.get_all()
+
+ return {
+ "users": [
+ (f"{u.first_name} (@{u.username or 'нет'})", u.id)
+ for u in users
+ ],
+ "count": len(users),
+ }
+
+
+@inject
+async def get_user_detail_data(dialog_manager: DialogManager, user_dao: FromDishka[UserDAO], **_kwargs):
+ user_id = dialog_manager.dialog_data.get("selected_user_id")
+ if not user_id:
+ return {"user_info": "Пользователь не выбран"}
+
+ user = await user_dao.get_by_id(user_id)
+ if not user:
+ return {"user_info": "Пользователь не найден"}
+
+ username_str = f"@{user.username}" if user.username else "—"
+ last_name_str = user.last_name or "—"
+ group_str = str(user.group) if user.group else "—"
+ admin_status = "✅ Да" if user.is_admin else "❌ Нет"
+
+ user_info = (
+ f"👤 Информация о пользователе\n\n"
+ f"ID: {user.id}\n"
+ f"Имя: {user.first_name}\n"
+ f"Фамилия: {last_name_str}\n"
+ f"Username: {username_str}\n"
+ f"Группа: {group_str}\n"
+ f"Администратор: {admin_status}"
+ )
+
+ return {"user_info": user_info}
+
+
+async def on_user_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str):
+ manager.dialog_data["selected_user_id"] = int(item_id)
+ await manager.switch_to(AdminUsersSG.user_detail)
+
+
+async def on_input_mode(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await manager.switch_to(AdminUsersSG.users_input)
+
+
+async def on_user_input(message: Message, _widget: MessageInput, manager: DialogManager):
+ container = manager.middleware_data[CONTAINER_NAME]
+ user_dao = await container.get(UserDAO)
+
+ text = (message.text or "").strip()
+
+ user = None
+ if text.startswith("@"):
+ username = text[1:]
+ all_users = await user_dao.get_all()
+ user = next((u for u in all_users if u.username == username), None)
+ elif text.isdigit():
+ user = await user_dao.get_by_id(int(text))
+
+ if not user:
+ await message.answer("❌ Пользователь не найден в базе данных.")
+ return
+
+ manager.dialog_data["selected_user_id"] = user.id
+ await manager.switch_to(AdminUsersSG.user_detail)
+
+
+async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
+
+
+users_dialog = Dialog(
+ Window(
+ Format("👥 Пользователи\n\nВсего: {count}"),
+ ScrollingGroup(
+ Select(
+ Format("{item[0]}"),
+ id="user_select",
+ item_id_getter=lambda x: x[1],
+ items="users",
+ on_click=on_user_selected,
+ ),
+ id="users_scroll",
+ width=1,
+ height=7,
+ ),
+ Column(
+ Button(Const("✏️ Ввести ID/Username"), id="input_mode", on_click=on_input_mode),
+ Button(Const("◀️ Назад"), id="back", on_click=on_back_to_main),
+ ),
+ state=AdminUsersSG.users_list,
+ getter=get_users_data,
+ ),
+ Window(
+ Const("Введите ID или @username пользователя:"),
+ MessageInput(on_user_input),
+ SwitchTo(Const("◀️ Назад"), id="back_to_list", state=AdminUsersSG.users_list),
+ state=AdminUsersSG.users_input,
+ ),
+ Window(
+ Format("{user_info}"),
+ SwitchTo(Const("◀️ Назад"), id="back_to_list", state=AdminUsersSG.users_list),
+ state=AdminUsersSG.user_detail,
+ getter=get_user_detail_data,
+ ),
+)
diff --git a/src/trudex/application/bot/creator_dialogs/broadcast.py b/src/trudex/application/bot/creator_dialogs/broadcast.py
new file mode 100644
index 0000000..5e42087
--- /dev/null
+++ b/src/trudex/application/bot/creator_dialogs/broadcast.py
@@ -0,0 +1,73 @@
+from aiogram.types import CallbackQuery, Message
+from aiogram_dialog import Dialog, DialogManager, Window
+from aiogram_dialog.widgets.input import MessageInput
+from aiogram_dialog.widgets.kbd import Button, Row, Cancel
+from aiogram_dialog.widgets.text import Const
+from dishka import FromDishka
+from dishka.integrations.aiogram_dialog import inject
+
+from trudex.application.bot.creator_dialogs.states import CreatorBroadcastSG
+from trudex.infrastructure.database.dao.user import UserDAO
+from trudex.infrastructure.utils.broadcast import broadcast_message
+
+
+async def on_broadcast_input(message: Message, _widget: MessageInput, manager: DialogManager):
+ manager.dialog_data["broadcast_message_id"] = message.message_id
+ manager.dialog_data["broadcast_chat_id"] = message.chat.id
+ await manager.switch_to(CreatorBroadcastSG.broadcast_confirm)
+
+
+@inject
+async def on_broadcast_confirm(_callback: CallbackQuery, _button: Button, manager: DialogManager, user_dao: FromDishka[UserDAO]):
+ message_id = manager.dialog_data.get("broadcast_message_id")
+ chat_id = manager.dialog_data.get("broadcast_chat_id")
+
+ if not message_id or not chat_id or not _callback.message:
+ await _callback.answer("Ошибка: сообщение не найдено")
+ return
+
+ await _callback.message.answer("⏳ Рассылка началась...")
+
+ bot = _callback.bot
+ if not bot:
+ await _callback.answer("Ошибка: бот не найден")
+ return
+
+ stats = await broadcast_message(bot, message_id, chat_id, user_dao)
+
+ stats_text = (
+ f"✅ Рассылка завершена\n\n"
+ f"Всего пользователей: {stats.total}\n"
+ f"Успешно отправлено: {stats.success}\n"
+ f"Не удалось отправить: {stats.failed}"
+ )
+
+ await _callback.message.answer(stats_text)
+ await manager.done()
+
+
+async def on_broadcast_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await _callback.answer("Рассылка отменена")
+ await manager.done()
+
+
+async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
+
+
+broadcast_dialog = Dialog(
+ Window(
+ Const("📢 Рассылка\n\nОтправьте сообщение, которое хотите разослать всем пользователям:"),
+ MessageInput(on_broadcast_input),
+ Button(Const("◀️ Отмена"), id="back", on_click=on_back_to_main),
+ state=CreatorBroadcastSG.broadcast_input,
+ ),
+ Window(
+ Const("⚠️ Подтверждение рассылки\n\nВы уверены, что хотите отправить это сообщение всем пользователям?"),
+ Row(
+ Button(Const("✅ Да"), id="broadcast_confirm", on_click=on_broadcast_confirm),
+ Button(Const("❌ Нет"), id="broadcast_cancel", on_click=on_broadcast_cancel),
+ ),
+ state=CreatorBroadcastSG.broadcast_confirm,
+ ),
+)
diff --git a/src/trudex/application/bot/creator_dialogs/main_menu.py b/src/trudex/application/bot/creator_dialogs/main_menu.py
index 752904d..6159fcc 100644
--- a/src/trudex/application/bot/creator_dialogs/main_menu.py
+++ b/src/trudex/application/bot/creator_dialogs/main_menu.py
@@ -1,182 +1,21 @@
-from aiogram.types import CallbackQuery, Message
-from aiogram_dialog import Dialog, DialogManager, Window
-from aiogram_dialog.widgets.input import MessageInput
-from aiogram_dialog.widgets.kbd import Back, Button, Cancel, Column, Row, ScrollingGroup, Select, SwitchTo
-from aiogram_dialog.widgets.text import Const, Format
-from dishka import FromDishka
-from dishka.integrations.aiogram import CONTAINER_NAME
-from dishka.integrations.aiogram_dialog import inject
+from aiogram.types import CallbackQuery
+from aiogram_dialog import Dialog, DialogManager, Window, StartMode
+from aiogram_dialog.widgets.kbd import Button, Column
+from aiogram_dialog.widgets.text import Const
-from trudex.application.bot.creator_dialogs.states import CreatorMenuSG
-from trudex.infrastructure.database.dao.user import UserDAO
-from trudex.infrastructure.utils.broadcast import broadcast_message
+from trudex.application.bot.creator_dialogs.states import CreatorMenuSG, CreatorUsersSG, CreatorTestsSG, CreatorBroadcastSG
-@inject
-async def get_users_data(user_dao: FromDishka[UserDAO], **_kwargs):
- users = await user_dao.get_all()
-
- return {
- "users": [
- (f"{u.first_name} (@{u.username or 'нет'})", u.id)
- for u in users
- ],
- "count": len(users),
- }
-
-
-@inject
-async def get_user_detail_data(dialog_manager: DialogManager, user_dao: FromDishka[UserDAO], **_kwargs):
- user_id = dialog_manager.dialog_data.get("selected_user_id")
- if not user_id:
- return {"user_info": "Пользователь не выбран", "is_admin": True, "show_make_admin": False}
-
- user = await user_dao.get_by_id(user_id)
- if not user:
- return {"user_info": "Пользователь не найден", "is_admin": True, "show_make_admin": False}
-
- username_str = f"@{user.username}" if user.username else "—"
- last_name_str = user.last_name or "—"
- group_str = str(user.group) if user.group else "—"
- admin_status = "✅ Да" if user.is_admin else "❌ Нет"
-
- user_info = (
- f"👤 Информация о пользователе\n\n"
- f"ID: {user.id}\n"
- f"Имя: {user.first_name}\n"
- f"Фамилия: {last_name_str}\n"
- f"Username: {username_str}\n"
- f"Группа: {group_str}\n"
- f"Администратор: {admin_status}"
- )
-
- return {
- "user_info": user_info,
- "is_admin": user.is_admin,
- "show_make_admin": not user.is_admin,
- }
-
-
-@inject
-async def get_confirm_data(dialog_manager: DialogManager, user_dao: FromDishka[UserDAO], **_kwargs):
- user_id = dialog_manager.dialog_data.get("selected_user_id")
- if not user_id:
- return {"user_info": "Пользователь не выбран"}
-
- user = await user_dao.get_by_id(user_id)
- if not user:
- return {"user_info": "Пользователь не найден"}
-
- username_str = f"@{user.username}" if user.username else "—"
- return {
- "user_info": f"{user.first_name}\n{username_str}\nID: {user.id}"
- }
-
-
-async def on_user_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str):
- manager.dialog_data["selected_user_id"] = int(item_id)
- await manager.switch_to(CreatorMenuSG.user_detail)
-
-
-async def on_input_mode(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorMenuSG.users_input)
-
-
-async def on_user_input(message: Message, _widget: MessageInput, manager: DialogManager):
- container = manager.middleware_data[CONTAINER_NAME]
- user_dao = await container.get(UserDAO)
-
- text = (message.text or "").strip()
-
- user = None
- if text.startswith("@"):
- username = text[1:]
- all_users = await user_dao.get_all()
- user = next((u for u in all_users if u.username == username), None)
- elif text.isdigit():
- user = await user_dao.get_by_id(int(text))
-
- if not user:
- await message.answer("❌ Пользователь не найден в базе данных.")
- return
-
- manager.dialog_data["selected_user_id"] = user.id
- await manager.switch_to(CreatorMenuSG.user_detail)
-
-
-async def on_make_admin_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorMenuSG.make_admin_confirm)
-
-
-async def on_confirm_yes(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- container = manager.middleware_data[CONTAINER_NAME]
- user_dao = await container.get(UserDAO)
-
- user_id = manager.dialog_data.get("selected_user_id")
- if not user_id:
- await _callback.answer("Ошибка: пользователь не выбран")
- return
-
- await user_dao.update(user_id=user_id, is_admin=True)
- await _callback.answer("✅ Пользователь назначен администратором")
- await manager.switch_to(CreatorMenuSG.user_detail)
-
-
-async def on_confirm_no(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await _callback.answer("Отменено")
- await manager.switch_to(CreatorMenuSG.user_detail)
-
-
-async def on_broadcast_input(message: Message, _widget: MessageInput, manager: DialogManager):
- manager.dialog_data["broadcast_message_id"] = message.message_id
- manager.dialog_data["broadcast_chat_id"] = message.chat.id
- await manager.switch_to(CreatorMenuSG.broadcast_confirm)
-
-
-@inject
-async def on_broadcast_confirm(_callback: CallbackQuery, _button: Button, manager: DialogManager, user_dao: FromDishka[UserDAO]):
- message_id = manager.dialog_data.get("broadcast_message_id")
- chat_id = manager.dialog_data.get("broadcast_chat_id")
-
- if not message_id or not chat_id or not _callback.message:
- await _callback.answer("Ошибка: сообщение не найдено")
- return
-
- await _callback.message.answer("⏳ Рассылка началась...")
-
- bot = _callback.bot
- if not bot:
- await _callback.answer("Ошибка: бот не найден")
- return
-
- stats = await broadcast_message(bot, message_id, chat_id, user_dao)
-
- stats_text = (
- f"✅ Рассылка завершена\n\n"
- f"Всего пользователей: {stats.total}\n"
- f"Успешно отправлено: {stats.success}\n"
- f"Не удалось отправить: {stats.failed}"
- )
-
- await _callback.message.answer(stats_text)
- await manager.done()
-
-
-async def on_broadcast_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await _callback.answer("Рассылка отменена")
- await manager.switch_to(CreatorMenuSG.main)
-
-
-async def on_tests_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None:
- await _callback.answer("Тесты")
+async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
+ await manager.start(CreatorTestsSG.tests_list, mode=StartMode.RESET_STACK)
async def on_users_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(CreatorMenuSG.users_list)
+ await manager.start(CreatorUsersSG.users_list, mode=StartMode.RESET_STACK)
async def on_broadcast_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(CreatorMenuSG.broadcast_input)
+ await manager.start(CreatorBroadcastSG.broadcast_input, mode=StartMode.RESET_STACK)
creator_menu_dialog = Dialog(
@@ -189,64 +28,4 @@ creator_menu_dialog = Dialog(
),
state=CreatorMenuSG.main,
),
- Window(
- Format("👥 Пользователи\n\nВсего: {count}"),
- ScrollingGroup(
- Select(
- Format("{item[0]}"),
- id="user_select",
- item_id_getter=lambda x: x[1],
- items="users",
- on_click=on_user_selected,
- ),
- id="users_scroll",
- width=1,
- height=7,
- ),
- Column(
- Button(Const("✏️ Ввести ID/Username"), id="input_mode", on_click=on_input_mode),
- Cancel(Const("◀️ Назад")),
- ),
- state=CreatorMenuSG.users_list,
- getter=get_users_data,
- ),
- Window(
- Const("Введите ID или @username пользователя:"),
- MessageInput(on_user_input),
- SwitchTo(Const("◀️ Назад"), id="back_to_list", state=CreatorMenuSG.users_list),
- state=CreatorMenuSG.users_input,
- ),
- Window(
- Format("{user_info}"),
- Column(
- Button(Const("👑 Сделать администратором"), id="make_admin", on_click=on_make_admin_clicked, when="show_make_admin"),
- SwitchTo(Const("◀️ Назад"), id="back_to_list", state=CreatorMenuSG.users_list),
- ),
- state=CreatorMenuSG.user_detail,
- getter=get_user_detail_data,
- ),
- Window(
- Const("⚠️ Подтверждение\n\nВы уверены, что хотите назначить этого пользователя администратором?\n"),
- Format("{user_info}"),
- Row(
- Button(Const("✅ Да"), id="confirm_yes", on_click=on_confirm_yes),
- Button(Const("❌ Нет"), id="confirm_no", on_click=on_confirm_no),
- ),
- state=CreatorMenuSG.make_admin_confirm,
- getter=get_confirm_data,
- ),
- Window(
- Const("📢 Рассылка\n\nОтправьте сообщение, которое хотите разослать всем пользователям:"),
- MessageInput(on_broadcast_input),
- SwitchTo(Const("◀️ Отмена"), id="cancel_broadcast", state=CreatorMenuSG.main),
- state=CreatorMenuSG.broadcast_input,
- ),
- Window(
- Const("⚠️ Подтверждение рассылки\n\nВы уверены, что хотите отправить это сообщение всем пользователям?"),
- Row(
- Button(Const("✅ Да"), id="broadcast_confirm", on_click=on_broadcast_confirm),
- Button(Const("❌ Нет"), id="broadcast_cancel", on_click=on_broadcast_cancel),
- ),
- state=CreatorMenuSG.broadcast_confirm,
- ),
)
diff --git a/src/trudex/application/bot/creator_dialogs/states.py b/src/trudex/application/bot/creator_dialogs/states.py
index b2ee9e3..8db1252 100644
--- a/src/trudex/application/bot/creator_dialogs/states.py
+++ b/src/trudex/application/bot/creator_dialogs/states.py
@@ -3,9 +3,19 @@ from aiogram.fsm.state import State, StatesGroup
class CreatorMenuSG(StatesGroup):
main = State()
+
+
+class CreatorUsersSG(StatesGroup):
users_list = State()
users_input = State()
user_detail = State()
make_admin_confirm = State()
+
+
+class CreatorTestsSG(StatesGroup):
+ tests_list = State()
+
+
+class CreatorBroadcastSG(StatesGroup):
broadcast_input = State()
broadcast_confirm = State()
diff --git a/src/trudex/application/bot/creator_dialogs/tests.py b/src/trudex/application/bot/creator_dialogs/tests.py
new file mode 100644
index 0000000..403742f
--- /dev/null
+++ b/src/trudex/application/bot/creator_dialogs/tests.py
@@ -0,0 +1,60 @@
+from aiogram.types import CallbackQuery
+from aiogram_dialog import Dialog, DialogManager, Window, StartMode
+from aiogram_dialog.widgets.kbd import Button, Column, ScrollingGroup, Select
+from aiogram_dialog.widgets.text import Const, Format
+from dishka import FromDishka
+from dishka.integrations.aiogram_dialog import inject
+
+from trudex.application.bot.creator_dialogs.states import CreatorTestsSG, CreatorMenuSG
+from trudex.infrastructure.database.dao.test import TestDAO
+
+
+@inject
+async def get_tests_data(test_dao: FromDishka[TestDAO], **_kwargs):
+ tests = await test_dao.get_all()
+
+ return {
+ "tests": [
+ (t.title, t.id)
+ for t in tests
+ ],
+ "count": len(tests),
+ }
+
+
+async def on_test_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str):
+ manager.dialog_data["selected_test_id"] = int(item_id)
+ await _callback.answer("Тест выбран")
+
+
+async def on_add_test_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager):
+ await _callback.answer("Добавление теста")
+
+
+async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
+
+
+tests_dialog = Dialog(
+ Window(
+ Format("📝 Тесты\n\nВсего: {count}"),
+ ScrollingGroup(
+ Select(
+ Format("{item[0]}"),
+ id="test_select",
+ item_id_getter=lambda x: x[1],
+ items="tests",
+ on_click=on_test_selected,
+ ),
+ id="tests_scroll",
+ width=1,
+ height=7,
+ ),
+ Column(
+ Button(Const("➕ Добавить тест"), id="add_test", on_click=on_add_test_clicked),
+ Button(Const("◀️ Назад"), id="back", on_click=on_back_clicked),
+ ),
+ state=CreatorTestsSG.tests_list,
+ getter=get_tests_data,
+ ),
+)
diff --git a/src/trudex/application/bot/creator_dialogs/users.py b/src/trudex/application/bot/creator_dialogs/users.py
new file mode 100644
index 0000000..6702510
--- /dev/null
+++ b/src/trudex/application/bot/creator_dialogs/users.py
@@ -0,0 +1,180 @@
+from aiogram.types import CallbackQuery, Message
+from aiogram_dialog import Dialog, DialogManager, Window, StartMode
+from aiogram_dialog.widgets.input import MessageInput
+from aiogram_dialog.widgets.kbd import Button, Column, Row, ScrollingGroup, Select, SwitchTo
+from aiogram_dialog.widgets.text import Const, Format
+from dishka import FromDishka
+from dishka.integrations.aiogram import CONTAINER_NAME
+from dishka.integrations.aiogram_dialog import inject
+
+from trudex.application.bot.creator_dialogs.states import CreatorUsersSG, CreatorMenuSG
+from trudex.infrastructure.database.dao.user import UserDAO
+
+
+@inject
+async def get_users_data(user_dao: FromDishka[UserDAO], **_kwargs):
+ users = await user_dao.get_all()
+
+ return {
+ "users": [
+ (f"{u.first_name} (@{u.username or 'нет'})", u.id)
+ for u in users
+ ],
+ "count": len(users),
+ }
+
+
+@inject
+async def get_user_detail_data(dialog_manager: DialogManager, user_dao: FromDishka[UserDAO], **_kwargs):
+ user_id = dialog_manager.dialog_data.get("selected_user_id")
+ if not user_id:
+ return {"user_info": "Пользователь не выбран", "is_admin": True, "show_make_admin": False}
+
+ user = await user_dao.get_by_id(user_id)
+ if not user:
+ return {"user_info": "Пользователь не найден", "is_admin": True, "show_make_admin": False}
+
+ username_str = f"@{user.username}" if user.username else "—"
+ last_name_str = user.last_name or "—"
+ group_str = str(user.group) if user.group else "—"
+ admin_status = "✅ Да" if user.is_admin else "❌ Нет"
+
+ user_info = (
+ f"👤 Информация о пользователе\n\n"
+ f"ID: {user.id}\n"
+ f"Имя: {user.first_name}\n"
+ f"Фамилия: {last_name_str}\n"
+ f"Username: {username_str}\n"
+ f"Группа: {group_str}\n"
+ f"Администратор: {admin_status}"
+ )
+
+ return {
+ "user_info": user_info,
+ "is_admin": user.is_admin,
+ "show_make_admin": not user.is_admin,
+ }
+
+
+@inject
+async def get_confirm_data(dialog_manager: DialogManager, user_dao: FromDishka[UserDAO], **_kwargs):
+ user_id = dialog_manager.dialog_data.get("selected_user_id")
+ if not user_id:
+ return {"user_info": "Пользователь не выбран"}
+
+ user = await user_dao.get_by_id(user_id)
+ if not user:
+ return {"user_info": "Пользователь не найден"}
+
+ username_str = f"@{user.username}" if user.username else "—"
+ return {
+ "user_info": f"{user.first_name}\n{username_str}\nID: {user.id}"
+ }
+
+
+async def on_user_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str):
+ manager.dialog_data["selected_user_id"] = int(item_id)
+ await manager.switch_to(CreatorUsersSG.user_detail)
+
+
+async def on_input_mode(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await manager.switch_to(CreatorUsersSG.users_input)
+
+
+async def on_user_input(message: Message, _widget: MessageInput, manager: DialogManager):
+ container = manager.middleware_data[CONTAINER_NAME]
+ user_dao = await container.get(UserDAO)
+
+ text = (message.text or "").strip()
+
+ user = None
+ if text.startswith("@"):
+ username = text[1:]
+ all_users = await user_dao.get_all()
+ user = next((u for u in all_users if u.username == username), None)
+ elif text.isdigit():
+ user = await user_dao.get_by_id(int(text))
+
+ if not user:
+ await message.answer("❌ Пользователь не найден в базе данных.")
+ return
+
+ manager.dialog_data["selected_user_id"] = user.id
+ await manager.switch_to(CreatorUsersSG.user_detail)
+
+
+async def on_make_admin_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await manager.switch_to(CreatorUsersSG.make_admin_confirm)
+
+
+async def on_confirm_yes(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ container = manager.middleware_data[CONTAINER_NAME]
+ user_dao = await container.get(UserDAO)
+
+ user_id = manager.dialog_data.get("selected_user_id")
+ if not user_id:
+ await _callback.answer("Ошибка: пользователь не выбран")
+ return
+
+ await user_dao.update(user_id=user_id, is_admin=True)
+ await _callback.answer("✅ Пользователь назначен администратором")
+ await manager.switch_to(CreatorUsersSG.user_detail)
+
+
+async def on_confirm_no(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await _callback.answer("Отменено")
+ await manager.switch_to(CreatorUsersSG.user_detail)
+
+
+async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager):
+ await manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
+
+
+users_dialog = Dialog(
+ Window(
+ Format("👥 Пользователи\n\nВсего: {count}"),
+ ScrollingGroup(
+ Select(
+ Format("{item[0]}"),
+ id="user_select",
+ item_id_getter=lambda x: x[1],
+ items="users",
+ on_click=on_user_selected,
+ ),
+ id="users_scroll",
+ width=1,
+ height=7,
+ ),
+ Column(
+ Button(Const("✏️ Ввести ID/Username"), id="input_mode", on_click=on_input_mode),
+ Button(Const("◀️ Назад"), id="back", on_click=on_back_to_main),
+ ),
+ state=CreatorUsersSG.users_list,
+ getter=get_users_data,
+ ),
+ Window(
+ Const("Введите ID или @username пользователя:"),
+ MessageInput(on_user_input),
+ SwitchTo(Const("◀️ Назад"), id="back_to_list", state=CreatorUsersSG.users_list),
+ state=CreatorUsersSG.users_input,
+ ),
+ Window(
+ Format("{user_info}"),
+ Column(
+ Button(Const("👑 Сделать администратором"), id="make_admin", on_click=on_make_admin_clicked, when="show_make_admin"),
+ SwitchTo(Const("◀️ Назад"), id="back_to_list", state=CreatorUsersSG.users_list),
+ ),
+ state=CreatorUsersSG.user_detail,
+ getter=get_user_detail_data,
+ ),
+ Window(
+ Const("⚠️ Подтверждение\n\nВы уверены, что хотите назначить этого пользователя администратором?\n\n"),
+ Format("{user_info}"),
+ Row(
+ Button(Const("✅ Да"), id="confirm_yes", on_click=on_confirm_yes),
+ Button(Const("❌ Нет"), id="confirm_no", on_click=on_confirm_no),
+ ),
+ state=CreatorUsersSG.make_admin_confirm,
+ getter=get_confirm_data,
+ ),
+)
diff --git a/src/trudex/application/bot/handlers.py b/src/trudex/application/bot/handlers.py
index 6df0610..63ba8d4 100644
--- a/src/trudex/application/bot/handlers.py
+++ b/src/trudex/application/bot/handlers.py
@@ -29,13 +29,19 @@ async def start_handler(message: Message, user_dao: FromDishka[UserDAO], dialog_
@router.message(Command("admin"))
-async def admin_command(_message: Message, dialog_manager: DialogManager) -> None:
- await dialog_manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
+async def admin_command(message: Message, dialog_manager: DialogManager) -> None:
+ try:
+ await dialog_manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
+ except Exception as e:
+ await message.answer(f"Ошибка запуска диалога: {e}")
@router.message(Command("creator"))
-async def creator_command(_message: Message, dialog_manager: DialogManager) -> None:
- await dialog_manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
+async def creator_command(message: Message, dialog_manager: DialogManager) -> None:
+ try:
+ await dialog_manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
+ except Exception as e:
+ await message.answer(f"Ошибка запуска диалога: {e}")
@router.error()
diff --git a/src/trudex/domain/schemas.py b/src/trudex/domain/schemas.py
index ad55ab9..2ea0e68 100644
--- a/src/trudex/domain/schemas.py
+++ b/src/trudex/domain/schemas.py
@@ -20,6 +20,8 @@ class Test:
title: str
description: str | None = None
for_group: int | None = None
+ password: str | None = None
+ expires_at: datetime | None = None
is_active: bool = True
created_at: datetime | None = None
updated_at: datetime | None = None
diff --git a/src/trudex/infrastructure/database/dao/test.py b/src/trudex/infrastructure/database/dao/test.py
index 6996c7f..6a67452 100644
--- a/src/trudex/infrastructure/database/dao/test.py
+++ b/src/trudex/infrastructure/database/dao/test.py
@@ -27,12 +27,16 @@ class TestDAO:
title: str,
description: str | None = None,
for_group: int | None = None,
+ password: str | None = None,
+ expires_at: str | None = None,
is_active: bool = True,
) -> DomainTest:
test = Test(
title=title,
description=description,
for_group=for_group,
+ password=password,
+ expires_at=expires_at,
is_active=is_active,
)
self.session.add(test)
@@ -46,6 +50,8 @@ class TestDAO:
title: str | None = None,
description: str | None = None,
for_group: int | None = None,
+ password: str | None = None,
+ expires_at: str | None = None,
is_active: bool | None = None,
) -> DomainTest | None:
result = await self.session.execute(
@@ -61,6 +67,10 @@ class TestDAO:
test.description = description
if for_group is not None:
test.for_group = for_group
+ if password is not None:
+ test.password = password
+ if expires_at is not None:
+ test.expires_at = expires_at
if is_active is not None:
test.is_active = is_active
diff --git a/src/trudex/infrastructure/database/dto/test.py b/src/trudex/infrastructure/database/dto/test.py
index 55971fc..f3fa61e 100644
--- a/src/trudex/infrastructure/database/dto/test.py
+++ b/src/trudex/infrastructure/database/dto/test.py
@@ -12,6 +12,8 @@ class TestDTO:
title=self.model.title,
description=self.model.description,
for_group=self.model.for_group,
+ password=self.model.password,
+ expires_at=self.model.expires_at,
is_active=self.model.is_active,
created_at=self.model.created_at,
updated_at=self.model.updated_at,
diff --git a/src/trudex/infrastructure/database/models.py b/src/trudex/infrastructure/database/models.py
index 561c657..fe7d7b9 100644
--- a/src/trudex/infrastructure/database/models.py
+++ b/src/trudex/infrastructure/database/models.py
@@ -38,6 +38,8 @@ class Test(Base):
title: Mapped[str] = mapped_column(String(255))
description: Mapped[str | None] = mapped_column(Text)
for_group: Mapped[int | None] = mapped_column(default=None)
+ password: Mapped[str | None] = mapped_column(String(255), default=None)
+ expires_at: Mapped[datetime | None] = mapped_column(default=None)
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())