diff --git a/src/trudex/application/__main__.py b/src/trudex/application/__main__.py index b76d899..054d2ce 100644 --- a/src/trudex/application/__main__.py +++ b/src/trudex/application/__main__.py @@ -8,22 +8,35 @@ 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.groups import groups_dialog as admin_groups_dialog +from trudex.application.bot.admin_dialogs.broadcast import \ + broadcast_dialog as admin_broadcast_dialog +from trudex.application.bot.admin_dialogs.groups import \ + groups_dialog as admin_groups_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.create_test import create_test_dialog -from trudex.application.bot.creator_dialogs.groups import groups_dialog as creator_groups_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.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.create_test import \ + create_test_dialog +from trudex.application.bot.creator_dialogs.groups import \ + groups_dialog as creator_groups_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.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.application.bot.user_dialogs.registration import registration_dialog +from trudex.application.bot.user_dialogs.registration import \ + registration_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 diff --git a/src/trudex/application/bot/admin_dialogs/broadcast.py b/src/trudex/application/bot/admin_dialogs/broadcast.py index 69f9e8b..937ccfd 100644 --- a/src/trudex/application/bot/admin_dialogs/broadcast.py +++ b/src/trudex/application/bot/admin_dialogs/broadcast.py @@ -1,12 +1,13 @@ from aiogram.types import CallbackQuery, Message -from aiogram_dialog import Dialog, DialogManager, Window, StartMode +from aiogram_dialog import Dialog, DialogManager, StartMode, Window 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.application.bot.admin_dialogs.states import (AdminBroadcastSG, + AdminMenuSG) from trudex.infrastructure.database.dao.user import UserDAO from trudex.infrastructure.utils.broadcast import broadcast_message diff --git a/src/trudex/application/bot/admin_dialogs/groups.py b/src/trudex/application/bot/admin_dialogs/groups.py index 821bc5b..4d62d10 100644 --- a/src/trudex/application/bot/admin_dialogs/groups.py +++ b/src/trudex/application/bot/admin_dialogs/groups.py @@ -1,11 +1,14 @@ from aiogram.types import CallbackQuery, Message from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog.widgets.input import MessageInput -from aiogram_dialog.widgets.kbd import Button, Column, Row, ScrollingGroup, Select +from aiogram_dialog.widgets.kbd import (Button, Column, Row, ScrollingGroup, + Select) from aiogram_dialog.widgets.text import Const, Format -from dishka.integrations.aiogram import CONTAINER_NAME +from dishka import FromDishka +from dishka.integrations.aiogram_dialog import inject -from trudex.application.bot.admin_dialogs.states import AdminGroupsSG, AdminMenuSG +from trudex.application.bot.admin_dialogs.states import (AdminGroupsSG, + AdminMenuSG) from trudex.infrastructure.database.dao.group import GroupDAO @@ -13,10 +16,8 @@ async def on_group_click(_callback: CallbackQuery, _widget, _manager: DialogMana await _callback.answer("ℹ️ Для удаления используйте кнопку 'Удалить группу'") -async def get_groups_data(dialog_manager: DialogManager, **_kwargs): - container = dialog_manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - +@inject +async def get_groups_data(dialog_manager: DialogManager, group_dao: FromDishka[GroupDAO], **_kwargs): groups = await group_dao.get_all() success_message = dialog_manager.dialog_data.pop("success_message", None) @@ -45,7 +46,8 @@ async def on_back_to_menu(_callback: CallbackQuery, _button: Button, manager: Di await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK) -async def on_group_number_input(message: Message, _widget: MessageInput, manager: DialogManager): +@inject +async def on_group_number_input(message: Message, _widget: MessageInput, manager: DialogManager, group_dao: FromDishka[GroupDAO]): if not message.text: await message.answer("❌ Номер группы не может быть пустым") return @@ -62,9 +64,6 @@ async def on_group_number_input(message: Message, _widget: MessageInput, manager await message.answer("❌ Номер группы должен быть четырехзначным (1000-9999)") return - container = manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - existing = await group_dao.get_by_number(number) if existing: await message.answer(f"❌ Группа с номером {number} уже существует") @@ -84,10 +83,8 @@ async def on_cancel_add(_callback: CallbackQuery, _button: Button, manager: Dial await manager.switch_to(AdminGroupsSG.groups_list) -async def get_delete_groups_data(dialog_manager: DialogManager, **_kwargs): - container = dialog_manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - +@inject +async def get_delete_groups_data(dialog_manager: DialogManager, group_dao: FromDishka[GroupDAO], **_kwargs): groups = await group_dao.get_all() return { @@ -96,10 +93,8 @@ async def get_delete_groups_data(dialog_manager: DialogManager, **_kwargs): } -async def on_select_group_to_delete(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str): - container = manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - +@inject +async def on_select_group_to_delete(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str, group_dao: FromDishka[GroupDAO]): group = await group_dao.get_by_id(int(item_id)) if not group: await _callback.answer("❌ Группа не найдена", show_alert=True) @@ -118,10 +113,8 @@ async def get_delete_confirm_data(dialog_manager: DialogManager, **_kwargs): } -async def on_confirm_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager): - container = manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - +@inject +async def on_confirm_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager, group_dao: FromDishka[GroupDAO]): group_id = manager.dialog_data.get("delete_group_id") await group_dao.delete(group_id) diff --git a/src/trudex/application/bot/admin_dialogs/main_menu.py b/src/trudex/application/bot/admin_dialogs/main_menu.py index 007c039..9f0c52f 100644 --- a/src/trudex/application/bot/admin_dialogs/main_menu.py +++ b/src/trudex/application/bot/admin_dialogs/main_menu.py @@ -1,9 +1,13 @@ from aiogram.types import CallbackQuery -from aiogram_dialog import Dialog, DialogManager, Window, StartMode +from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog.widgets.kbd import Button, Column from aiogram_dialog.widgets.text import Const -from trudex.application.bot.admin_dialogs.states import AdminMenuSG, AdminUsersSG, AdminTestsSG, AdminBroadcastSG, AdminGroupsSG +from trudex.application.bot.admin_dialogs.states import (AdminBroadcastSG, + AdminGroupsSG, + AdminMenuSG, + AdminTestsSG, + AdminUsersSG) async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: diff --git a/src/trudex/application/bot/admin_dialogs/states.py b/src/trudex/application/bot/admin_dialogs/states.py index 312340b..71bd8f1 100644 --- a/src/trudex/application/bot/admin_dialogs/states.py +++ b/src/trudex/application/bot/admin_dialogs/states.py @@ -13,6 +13,7 @@ class AdminUsersSG(StatesGroup): class AdminTestsSG(StatesGroup): tests_list = State() + test_detail = State() class AdminBroadcastSG(StatesGroup): diff --git a/src/trudex/application/bot/admin_dialogs/tests.py b/src/trudex/application/bot/admin_dialogs/tests.py index eae7fe9..1cf6a51 100644 --- a/src/trudex/application/bot/admin_dialogs/tests.py +++ b/src/trudex/application/bot/admin_dialogs/tests.py @@ -1,12 +1,15 @@ 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 import Dialog, DialogManager, StartMode, Window +from aiogram_dialog.widgets.kbd import (Button, Column, Row, 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.application.bot.admin_dialogs.states import (AdminMenuSG, + AdminTestsSG) from trudex.infrastructure.database.dao.test import TestDAO +from trudex.infrastructure.database.repo.test import TestRepository @inject @@ -24,7 +27,74 @@ async def get_tests_data(test_dao: FromDishka[TestDAO], **_kwargs): 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("Тест выбран") + await manager.switch_to(AdminTestsSG.test_detail) + + +@inject +async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[TestRepository], dialog_manager: DialogManager, **_kwargs): + test_id = dialog_manager.dialog_data.get("selected_test_id") + + if not test_id: + return { + "test_info": "Тест не найден", + "is_active": False, + "button_text": "◀️ Назад", + } + + test = await test_dao.get_by_id(test_id) + questions_count = await test_repo.count_questions_in_test(test_id) + + if not test: + return { + "test_info": "Тест не найден", + "is_active": False, + "button_text": "◀️ Назад", + } + + status = "🟢 Активен" if test.is_active else "🔴 Деактивирован" + password_str = f"🔒 {test.password}" if test.password else "🔓 Без пароля" + expires_str = test.expires_at.strftime("%d.%m.%Y %H:%M") if test.expires_at else "♾️ Без срока" + group_str = f"🎓 Группа {test.for_group}" if test.for_group else "👥 Для всех" + + test_info = ( + f"📝 Информация о тесте\n\n" + f"Название: {test.title}\n" + f"Описание: {test.description or '—'}\n\n" + f"Статус: {status}\n" + f"Вопросов: {questions_count}\n" + f"{password_str}\n" + f"{expires_str}\n" + f"{group_str}\n\n" + f"Создан: {test.created_at.strftime('%d.%m.%Y %H:%M') if test.created_at else '—'}" + ) + + button_text = "🔴 Деактивировать" if test.is_active else "🟢 Активировать" + + return { + "test_info": test_info, + "is_active": test.is_active, + "button_text": button_text, + } + + +@inject +async def on_toggle_active(_callback: CallbackQuery, _button: Button, manager: DialogManager, test_dao: FromDishka[TestDAO]): + test_id = manager.dialog_data.get("selected_test_id") + if not test_id: + await _callback.answer("❌ Тест не найден") + return + + test = await test_dao.get_by_id(test_id) + + if test: + await test_dao.update(test_id, is_active=not test.is_active) + action = "деактивирован" if test.is_active else "активирован" + await _callback.answer(f"✅ Тест {action}") + await manager.switch_to(AdminTestsSG.test_detail) + + +async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: DialogManager): + await manager.switch_to(AdminTestsSG.tests_list) async def on_add_test_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager): @@ -57,4 +127,17 @@ tests_dialog = Dialog( state=AdminTestsSG.tests_list, getter=get_tests_data, ), + Window( + Format("{test_info}"), + Row( + Button( + Format("{button_text}"), + id="toggle_active", + on_click=on_toggle_active + ), + Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list), + ), + state=AdminTestsSG.test_detail, + getter=get_test_detail, + ), ) diff --git a/src/trudex/application/bot/admin_dialogs/users.py b/src/trudex/application/bot/admin_dialogs/users.py index f0d3cf8..a9c2f20 100644 --- a/src/trudex/application/bot/admin_dialogs/users.py +++ b/src/trudex/application/bot/admin_dialogs/users.py @@ -1,13 +1,14 @@ from aiogram.types import CallbackQuery, Message -from aiogram_dialog import Dialog, DialogManager, Window, StartMode +from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog.widgets.input import MessageInput -from aiogram_dialog.widgets.kbd import Button, Column, ScrollingGroup, Select, SwitchTo +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.application.bot.admin_dialogs.states import (AdminMenuSG, + AdminUsersSG) from trudex.infrastructure.database.dao.user import UserDAO @@ -61,10 +62,8 @@ async def on_input_mode(_callback: CallbackQuery, _button: Button, manager: Dial 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) - +@inject +async def on_user_input(message: Message, _widget: MessageInput, manager: DialogManager, user_dao: FromDishka[UserDAO]): text = (message.text or "").strip() user = None diff --git a/src/trudex/application/bot/creator_dialogs/broadcast.py b/src/trudex/application/bot/creator_dialogs/broadcast.py index 5e42087..5663b79 100644 --- a/src/trudex/application/bot/creator_dialogs/broadcast.py +++ b/src/trudex/application/bot/creator_dialogs/broadcast.py @@ -1,7 +1,7 @@ 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.kbd import Button, Cancel, Row from aiogram_dialog.widgets.text import Const from dishka import FromDishka from dishka.integrations.aiogram_dialog import inject diff --git a/src/trudex/application/bot/creator_dialogs/create_test.py b/src/trudex/application/bot/creator_dialogs/create_test.py index 50c9cc0..bb96ce3 100644 --- a/src/trudex/application/bot/creator_dialogs/create_test.py +++ b/src/trudex/application/bot/creator_dialogs/create_test.py @@ -1,14 +1,16 @@ from datetime import date, datetime from aiogram.types import CallbackQuery, ContentType, Message -from aiogram_dialog import Dialog, DialogManager, Window, StartMode +from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog.widgets.input import MessageInput -from aiogram_dialog.widgets.kbd import Button, Calendar, Cancel, Column, Row, ScrollingGroup, Select +from aiogram_dialog.widgets.kbd import (Button, Calendar, Cancel, Column, Row, + ScrollingGroup, Select) from aiogram_dialog.widgets.text import Const, Format -from dishka.integrations.aiogram import CONTAINER_NAME +from dishka import FromDishka from dishka.integrations.aiogram_dialog import inject -from trudex.application.bot.creator_dialogs.states import CreateTestSG, CreatorTestsSG +from trudex.application.bot.creator_dialogs.states import (CreateTestSG, + CreatorTestsSG) from trudex.infrastructure.database.dao.group import GroupDAO from trudex.infrastructure.database.dao.option import OptionDAO from trudex.infrastructure.database.dao.question import QuestionDAO @@ -52,7 +54,8 @@ async def on_description_input(message: Message, _widget: MessageInput, manager: await manager.switch_to(CreateTestSG.input_password) -async def on_password_input(message: Message, _widget: MessageInput, manager: DialogManager): +@inject +async def on_password_input(message: Message, _widget: MessageInput, manager: DialogManager, group_dao: FromDishka[GroupDAO]): if not message.text: await message.answer("❌ Пароль не может быть пустым") return @@ -68,8 +71,6 @@ async def on_password_input(message: Message, _widget: MessageInput, manager: Di manager.dialog_data["password"] = password - container = manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) groups = await group_dao.get_all() if len(groups) == 0: @@ -79,10 +80,9 @@ async def on_password_input(message: Message, _widget: MessageInput, manager: Di await manager.switch_to(CreateTestSG.input_expires_at) -async def on_skip_password(_callback: CallbackQuery, _button: Button, manager: DialogManager): +@inject +async def on_skip_password(_callback: CallbackQuery, _button: Button, manager: DialogManager, group_dao: FromDishka[GroupDAO]): manager.dialog_data["password"] = None - container = manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) groups = await group_dao.get_all() if len(groups) == 0: @@ -102,9 +102,8 @@ async def on_skip_expires(_callback: CallbackQuery, _button: Button, manager: Di await manager.switch_to(CreateTestSG.input_for_group) -async def get_groups_for_test(dialog_manager: DialogManager, **_kwargs): - container = dialog_manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) +@inject +async def get_groups_for_test(dialog_manager: DialogManager, group_dao: FromDishka[GroupDAO], **_kwargs): groups = await group_dao.get_all() return { @@ -145,10 +144,8 @@ async def get_test_info(dialog_manager: DialogManager, **_kwargs): } -async def on_confirm_test(_callback: CallbackQuery, _button: Button, manager: DialogManager): - container = manager.middleware_data[CONTAINER_NAME] - test_dao = await container.get(TestDAO) - +@inject +async def on_confirm_test(_callback: CallbackQuery, _button: Button, manager: DialogManager, test_dao: FromDishka[TestDAO]): title = manager.dialog_data.get("title") description = manager.dialog_data.get("description") password = manager.dialog_data.get("password") @@ -351,12 +348,15 @@ async def get_question_preview(dialog_manager: DialogManager, **_kwargs): return {"preview": preview} -async def on_save_question(_callback: CallbackQuery, _button: Button, manager: DialogManager): - container = manager.middleware_data[CONTAINER_NAME] - question_dao = await container.get(QuestionDAO) - option_dao = await container.get(OptionDAO) - test_repo = await container.get(TestRepository) - +@inject +async def on_save_question( + _callback: CallbackQuery, + _button: Button, + manager: DialogManager, + question_dao: FromDishka[QuestionDAO], + option_dao: FromDishka[OptionDAO], + test_repo: FromDishka[TestRepository], +): test_id = manager.dialog_data.get("test_id") current_question = manager.dialog_data.get("current_question", {}) current_options = manager.dialog_data.get("current_options", []) diff --git a/src/trudex/application/bot/creator_dialogs/groups.py b/src/trudex/application/bot/creator_dialogs/groups.py index 6f5bd97..9642616 100644 --- a/src/trudex/application/bot/creator_dialogs/groups.py +++ b/src/trudex/application/bot/creator_dialogs/groups.py @@ -1,11 +1,14 @@ from aiogram.types import CallbackQuery, Message from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog.widgets.input import MessageInput -from aiogram_dialog.widgets.kbd import Button, Column, Row, ScrollingGroup, Select +from aiogram_dialog.widgets.kbd import (Button, Column, Row, ScrollingGroup, + Select) from aiogram_dialog.widgets.text import Const, Format -from dishka.integrations.aiogram import CONTAINER_NAME +from dishka import FromDishka +from dishka.integrations.aiogram_dialog import inject -from trudex.application.bot.creator_dialogs.states import CreatorGroupsSG +from trudex.application.bot.creator_dialogs.states import (CreatorGroupsSG, + CreatorMenuSG) from trudex.infrastructure.database.dao.group import GroupDAO @@ -13,10 +16,8 @@ async def on_group_click(_callback: CallbackQuery, _widget, _manager: DialogMana await _callback.answer("ℹ️ Для удаления используйте кнопку 'Удалить группу'") -async def get_groups_data(dialog_manager: DialogManager, **_kwargs): - container = dialog_manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - +@inject +async def get_groups_data(group_dao: FromDishka[GroupDAO], dialog_manager: DialogManager, **_kwargs): groups = await group_dao.get_all() success_message = dialog_manager.dialog_data.pop("success_message", None) @@ -46,7 +47,8 @@ async def on_back_to_menu(_callback: CallbackQuery, _button: Button, manager: Di await manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK) -async def on_group_number_input(message: Message, _widget: MessageInput, manager: DialogManager): +@inject +async def on_group_number_input(message: Message, _widget: MessageInput, manager: DialogManager, group_dao: FromDishka[GroupDAO]): if not message.text: await message.answer("❌ Номер группы не может быть пустым") return @@ -63,9 +65,6 @@ async def on_group_number_input(message: Message, _widget: MessageInput, manager await message.answer("❌ Номер группы должен быть четырехзначным (1000-9999)") return - container = manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - existing = await group_dao.get_by_number(number) if existing: await message.answer(f"❌ Группа с номером {number} уже существует") @@ -85,10 +84,8 @@ async def on_cancel_add(_callback: CallbackQuery, _button: Button, manager: Dial await manager.switch_to(CreatorGroupsSG.groups_list) -async def get_delete_groups_data(dialog_manager: DialogManager, **_kwargs): - container = dialog_manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - +@inject +async def get_delete_groups_data(group_dao: FromDishka[GroupDAO], **_kwargs): groups = await group_dao.get_all() return { @@ -97,10 +94,8 @@ async def get_delete_groups_data(dialog_manager: DialogManager, **_kwargs): } -async def on_select_group_to_delete(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str): - container = manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - +@inject +async def on_select_group_to_delete(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str, group_dao: FromDishka[GroupDAO]): group = await group_dao.get_by_id(int(item_id)) if not group: await _callback.answer("❌ Группа не найдена", show_alert=True) @@ -119,10 +114,8 @@ async def get_delete_confirm_data(dialog_manager: DialogManager, **_kwargs): } -async def on_confirm_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager): - container = manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - +@inject +async def on_confirm_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager, group_dao: FromDishka[GroupDAO]): group_id = manager.dialog_data.get("delete_group_id") await group_dao.delete(group_id) diff --git a/src/trudex/application/bot/creator_dialogs/main_menu.py b/src/trudex/application/bot/creator_dialogs/main_menu.py index ccede3f..3d63ee0 100644 --- a/src/trudex/application/bot/creator_dialogs/main_menu.py +++ b/src/trudex/application/bot/creator_dialogs/main_menu.py @@ -1,9 +1,13 @@ from aiogram.types import CallbackQuery -from aiogram_dialog import Dialog, DialogManager, Window, StartMode +from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog.widgets.kbd import Button, Column from aiogram_dialog.widgets.text import Const -from trudex.application.bot.creator_dialogs.states import CreatorMenuSG, CreatorUsersSG, CreatorTestsSG, CreatorBroadcastSG, CreatorGroupsSG +from trudex.application.bot.creator_dialogs.states import (CreatorBroadcastSG, + CreatorGroupsSG, + CreatorMenuSG, + CreatorTestsSG, + CreatorUsersSG) async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: diff --git a/src/trudex/application/bot/creator_dialogs/states.py b/src/trudex/application/bot/creator_dialogs/states.py index 876135f..f6744a4 100644 --- a/src/trudex/application/bot/creator_dialogs/states.py +++ b/src/trudex/application/bot/creator_dialogs/states.py @@ -14,6 +14,7 @@ class CreatorUsersSG(StatesGroup): class CreatorTestsSG(StatesGroup): tests_list = State() + test_detail = State() class CreatorBroadcastSG(StatesGroup): diff --git a/src/trudex/application/bot/creator_dialogs/tests.py b/src/trudex/application/bot/creator_dialogs/tests.py index 67d29e9..9c6a63d 100644 --- a/src/trudex/application/bot/creator_dialogs/tests.py +++ b/src/trudex/application/bot/creator_dialogs/tests.py @@ -1,12 +1,16 @@ 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 import Dialog, DialogManager, StartMode, Window +from aiogram_dialog.widgets.kbd import (Button, Column, Row, 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, CreateTestSG +from trudex.application.bot.creator_dialogs.states import (CreateTestSG, + CreatorMenuSG, + CreatorTestsSG) from trudex.infrastructure.database.dao.test import TestDAO +from trudex.infrastructure.database.repo.test import TestRepository @inject @@ -24,7 +28,74 @@ async def get_tests_data(test_dao: FromDishka[TestDAO], **_kwargs): 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("Тест выбран") + await manager.switch_to(CreatorTestsSG.test_detail) + + +@inject +async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[TestRepository], dialog_manager: DialogManager, **_kwargs): + test_id = dialog_manager.dialog_data.get("selected_test_id") + + if not test_id: + return { + "test_info": "Тест не найден", + "is_active": False, + "button_text": "◀️ Назад", + } + + test = await test_dao.get_by_id(test_id) + questions_count = await test_repo.count_questions_in_test(test_id) + + if not test: + return { + "test_info": "Тест не найден", + "is_active": False, + "button_text": "◀️ Назад", + } + + status = "🟢 Активен" if test.is_active else "🔴 Деактивирован" + password_str = f"🔒 {test.password}" if test.password else "🔓 Без пароля" + expires_str = test.expires_at.strftime("%d.%m.%Y %H:%M") if test.expires_at else "♾️ Без срока" + group_str = f"🎓 Группа {test.for_group}" if test.for_group else "👥 Для всех" + + test_info = ( + f"📝 Информация о тесте\n\n" + f"Название: {test.title}\n" + f"Описание: {test.description or '—'}\n\n" + f"Статус: {status}\n" + f"Вопросов: {questions_count}\n" + f"{password_str}\n" + f"{expires_str}\n" + f"{group_str}\n\n" + f"Создан: {test.created_at.strftime('%d.%m.%Y %H:%M') if test.created_at else '—'}" + ) + + button_text = "🔴 Деактивировать" if test.is_active else "🟢 Активировать" + + return { + "test_info": test_info, + "is_active": test.is_active, + "button_text": button_text, + } + + +@inject +async def on_toggle_active(_callback: CallbackQuery, _button: Button, manager: DialogManager, test_dao: FromDishka[TestDAO]): + test_id = manager.dialog_data.get("selected_test_id") + if not test_id: + await _callback.answer("❌ Тест не найден") + return + + test = await test_dao.get_by_id(test_id) + + if test: + await test_dao.update(test_id, is_active=not test.is_active) + action = "деактивирован" if test.is_active else "активирован" + await _callback.answer(f"✅ Тест {action}") + await manager.switch_to(CreatorTestsSG.test_detail) + + +async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: DialogManager): + await manager.switch_to(CreatorTestsSG.tests_list) async def on_add_test_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager): @@ -57,4 +128,17 @@ tests_dialog = Dialog( state=CreatorTestsSG.tests_list, getter=get_tests_data, ), + Window( + Format("{test_info}"), + Row( + Button( + Format("{button_text}"), + id="toggle_active", + on_click=on_toggle_active + ), + Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list), + ), + state=CreatorTestsSG.test_detail, + getter=get_test_detail, + ), ) diff --git a/src/trudex/application/bot/creator_dialogs/users.py b/src/trudex/application/bot/creator_dialogs/users.py index 6702510..a42b207 100644 --- a/src/trudex/application/bot/creator_dialogs/users.py +++ b/src/trudex/application/bot/creator_dialogs/users.py @@ -1,13 +1,14 @@ from aiogram.types import CallbackQuery, Message -from aiogram_dialog import Dialog, DialogManager, Window, StartMode +from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog.widgets.input import MessageInput -from aiogram_dialog.widgets.kbd import Button, Column, Row, ScrollingGroup, Select, SwitchTo +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.application.bot.creator_dialogs.states import (CreatorMenuSG, + CreatorUsersSG) from trudex.infrastructure.database.dao.user import UserDAO @@ -81,10 +82,8 @@ async def on_input_mode(_callback: CallbackQuery, _button: Button, manager: Dial 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) - +@inject +async def on_user_input(message: Message, _widget: MessageInput, manager: DialogManager, user_dao: FromDishka[UserDAO]): text = (message.text or "").strip() user = None @@ -107,10 +106,8 @@ async def on_make_admin_clicked(_callback: CallbackQuery, _button: Button, manag 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) - +@inject +async def on_confirm_yes(_callback: CallbackQuery, _button: Button, manager: DialogManager, user_dao: FromDishka[UserDAO]): user_id = manager.dialog_data.get("selected_user_id") if not user_id: await _callback.answer("Ошибка: пользователь не выбран") diff --git a/src/trudex/application/bot/handlers.py b/src/trudex/application/bot/handlers.py index 5455a9c..c19724e 100644 --- a/src/trudex/application/bot/handlers.py +++ b/src/trudex/application/bot/handlers.py @@ -7,11 +7,11 @@ from dishka.integrations.aiogram import FromDishka from trudex.application.bot.admin_dialogs.states import AdminMenuSG from trudex.application.bot.creator_dialogs.states import CreatorMenuSG -from trudex.application.bot.user_dialogs.states import UserMenuSG, UserRegistrationSG +from trudex.application.bot.user_dialogs.states import (UserMenuSG, + UserRegistrationSG) from trudex.infrastructure.database.dao.group import GroupDAO from trudex.infrastructure.database.dao.user import UserDAO - router = Router() diff --git a/src/trudex/application/bot/user_dialogs/registration.py b/src/trudex/application/bot/user_dialogs/registration.py index f597895..d1d77f0 100644 --- a/src/trudex/application/bot/user_dialogs/registration.py +++ b/src/trudex/application/bot/user_dialogs/registration.py @@ -2,17 +2,17 @@ from aiogram.types import CallbackQuery from aiogram_dialog import Dialog, DialogManager, StartMode, Window from aiogram_dialog.widgets.kbd import ScrollingGroup, Select from aiogram_dialog.widgets.text import Const, Format -from dishka.integrations.aiogram import CONTAINER_NAME +from dishka import FromDishka +from dishka.integrations.aiogram_dialog import inject -from trudex.application.bot.user_dialogs.states import UserMenuSG, UserRegistrationSG +from trudex.application.bot.user_dialogs.states import (UserMenuSG, + UserRegistrationSG) from trudex.infrastructure.database.dao.group import GroupDAO from trudex.infrastructure.database.dao.user import UserDAO -async def get_groups_for_registration(dialog_manager: DialogManager, **_kwargs): - container = dialog_manager.middleware_data[CONTAINER_NAME] - group_dao = await container.get(GroupDAO) - +@inject +async def get_groups_for_registration(dialog_manager: DialogManager, group_dao: FromDishka[GroupDAO], **_kwargs): groups = await group_dao.get_all() return { @@ -20,10 +20,8 @@ async def get_groups_for_registration(dialog_manager: DialogManager, **_kwargs): } -async def on_group_selected(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str): - container = manager.middleware_data[CONTAINER_NAME] - user_dao = await container.get(UserDAO) - +@inject +async def on_group_selected(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str, user_dao: FromDishka[UserDAO]): user_id = manager.start_data.get("user_id") await user_dao.update(user_id=user_id, group=int(item_id)) diff --git a/src/trudex/infrastructure/database/dao/__init__.py b/src/trudex/infrastructure/database/dao/__init__.py index ecf61f2..9d85bbf 100644 --- a/src/trudex/infrastructure/database/dao/__init__.py +++ b/src/trudex/infrastructure/database/dao/__init__.py @@ -1,5 +1,5 @@ +from .option import OptionDAO as OptionDAO +from .question import QuestionDAO as QuestionDAO from .test import TestDAO as TestDAO from .user import UserDAO as UserDAO -from .question import QuestionDAO as QuestionDAO -from .option import OptionDAO as OptionDAO \ No newline at end of file diff --git a/src/trudex/infrastructure/database/dto/test_attempt.py b/src/trudex/infrastructure/database/dto/test_attempt.py index 786eb38..9fb3255 100644 --- a/src/trudex/infrastructure/database/dto/test_attempt.py +++ b/src/trudex/infrastructure/database/dto/test_attempt.py @@ -1,5 +1,6 @@ from trudex.domain.schemas import TestAttempt as DomainTestAttempt -from trudex.infrastructure.database.models import TestAttempt as TestAttemptModel +from trudex.infrastructure.database.models import \ + TestAttempt as TestAttemptModel class TestAttemptDTO: diff --git a/src/trudex/infrastructure/database/models.py b/src/trudex/infrastructure/database/models.py index bac500f..2d03310 100644 --- a/src/trudex/infrastructure/database/models.py +++ b/src/trudex/infrastructure/database/models.py @@ -2,7 +2,8 @@ from datetime import datetime from enum import Enum from typing import final -from sqlalchemy import BigInteger, CheckConstraint, ForeignKey, Integer, String, Text, func +from sqlalchemy import (BigInteger, CheckConstraint, ForeignKey, Integer, + String, Text, func) from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship diff --git a/src/trudex/infrastructure/database/repo/__init__.py b/src/trudex/infrastructure/database/repo/__init__.py index 3d25dad..1f6a9b8 100644 --- a/src/trudex/infrastructure/database/repo/__init__.py +++ b/src/trudex/infrastructure/database/repo/__init__.py @@ -1,5 +1,6 @@ from trudex.infrastructure.database.repo.test import TestRepository -from trudex.infrastructure.database.repo.test_attempt import TestAttemptRepository +from trudex.infrastructure.database.repo.test_attempt import \ + TestAttemptRepository from trudex.infrastructure.database.repo.user import UserRepository __all__ = ["TestRepository", "TestAttemptRepository", "UserRepository"] diff --git a/src/trudex/infrastructure/database/repo/test.py b/src/trudex/infrastructure/database/repo/test.py index fc36ad5..fba422b 100644 --- a/src/trudex/infrastructure/database/repo/test.py +++ b/src/trudex/infrastructure/database/repo/test.py @@ -11,11 +11,9 @@ from trudex.infrastructure.database.dao.test import TestDAO from trudex.infrastructure.database.dto.option import OptionDTO from trudex.infrastructure.database.dto.question import QuestionDTO from trudex.infrastructure.database.dto.test import TestDTO -from trudex.infrastructure.database.models import ( - Option as OptionModel, - Question as QuestionModel, - Test as TestModel, -) +from trudex.infrastructure.database.models import Option as OptionModel +from trudex.infrastructure.database.models import Question as QuestionModel +from trudex.infrastructure.database.models import Test as TestModel @final diff --git a/src/trudex/infrastructure/database/repo/test_attempt.py b/src/trudex/infrastructure/database/repo/test_attempt.py index d6043cb..476cb23 100644 --- a/src/trudex/infrastructure/database/repo/test_attempt.py +++ b/src/trudex/infrastructure/database/repo/test_attempt.py @@ -10,10 +10,9 @@ from trudex.infrastructure.database.dao.test_attempt import TestAttemptDAO from trudex.infrastructure.database.dao.user_answer import UserAnswerDAO from trudex.infrastructure.database.dto.test_attempt import TestAttemptDTO from trudex.infrastructure.database.dto.user_answer import UserAnswerDTO -from trudex.infrastructure.database.models import ( - TestAttempt as TestAttemptModel, - UserAnswer as UserAnswerModel, -) +from trudex.infrastructure.database.models import \ + TestAttempt as TestAttemptModel +from trudex.infrastructure.database.models import UserAnswer as UserAnswerModel @final @@ -177,7 +176,8 @@ class TestAttemptRepository: } async def get_most_difficult_questions(self, test_id: int, limit: int = 10) -> list[tuple[int, float]]: - from trudex.infrastructure.database.models import Question as QuestionModel + from trudex.infrastructure.database.models import \ + Question as QuestionModel result = await self.session.execute( select( diff --git a/src/trudex/infrastructure/di.py b/src/trudex/infrastructure/di.py index 06756ed..536735d 100644 --- a/src/trudex/infrastructure/di.py +++ b/src/trudex/infrastructure/di.py @@ -12,7 +12,8 @@ from trudex.infrastructure.database.dao.test_attempt import TestAttemptDAO from trudex.infrastructure.database.dao.user import UserDAO from trudex.infrastructure.database.dao.user_answer import UserAnswerDAO from trudex.infrastructure.database.repo.test import TestRepository -from trudex.infrastructure.database.repo.test_attempt import TestAttemptRepository +from trudex.infrastructure.database.repo.test_attempt import \ + TestAttemptRepository from trudex.infrastructure.database.repo.user import UserRepository from trudex.infrastructure.utils.config import Config diff --git a/src/trudex/infrastructure/utils/bot_commands.py b/src/trudex/infrastructure/utils/bot_commands.py index d017f64..15b73ab 100644 --- a/src/trudex/infrastructure/utils/bot_commands.py +++ b/src/trudex/infrastructure/utils/bot_commands.py @@ -1,5 +1,6 @@ from aiogram import Bot -from aiogram.types import BotCommand, BotCommandScopeAllPrivateChats, BotCommandScopeChat +from aiogram.types import (BotCommand, BotCommandScopeAllPrivateChats, + BotCommandScopeChat) from trudex.infrastructure.database.repo.user import UserRepository from trudex.infrastructure.utils.config import Config