diff --git a/src/trudex/application/__main__.py b/src/trudex/application/__main__.py
index 45d3fa9..ff8d583 100644
--- a/src/trudex/application/__main__.py
+++ b/src/trudex/application/__main__.py
@@ -9,23 +9,18 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
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.create_test import admin_create_test_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.templates import templates_dialog as admin_templates_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.admin_dialogs.users import admin_users_dialog
from trudex.application.bot.creator_dialogs.main_menu import creator_menu_dialog
-from trudex.application.bot.creator_dialogs.templates import templates_dialog as creator_templates_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.creator_dialogs.users import 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.shared_dialogs.broadcast import shared_broadcast_dialog
+from trudex.application.bot.shared_dialogs.create_test import shared_create_test_dialog
+from trudex.application.bot.shared_dialogs.groups import shared_groups_dialog
+from trudex.application.bot.shared_dialogs.templates import shared_templates_dialog
+from trudex.application.bot.shared_dialogs.tests import shared_tests_dialog
from trudex.application.bot.user_dialogs.deeplink import deeplink_dialog
from trudex.application.bot.user_dialogs.main_menu import user_menu_dialog
from trudex.application.bot.user_dialogs.registration import registration_dialog
@@ -57,20 +52,18 @@ async def main() -> None:
take_test_dialog,
registration_dialog,
deeplink_dialog,
+ # Shared dialogs
+ shared_tests_dialog,
+ shared_groups_dialog,
+ shared_broadcast_dialog,
+ shared_templates_dialog,
+ shared_create_test_dialog,
+ # Admin dialogs
admin_menu_dialog,
admin_users_dialog,
- admin_tests_dialog,
- admin_groups_dialog,
- admin_broadcast_dialog,
- admin_templates_dialog,
- admin_create_test_dialog,
+ # Creator dialogs
creator_menu_dialog,
creator_users_dialog,
- creator_tests_dialog,
- creator_groups_dialog,
- creator_broadcast_dialog,
- creator_templates_dialog,
- create_test_dialog,
)
router.message.middleware(RejectNotAdminMiddleware())
diff --git a/src/trudex/application/bot/admin_dialogs/groups.py b/src/trudex/application/bot/admin_dialogs/groups.py
deleted file mode 100644
index 8b80665..0000000
--- a/src/trudex/application/bot/admin_dialogs/groups.py
+++ /dev/null
@@ -1,186 +0,0 @@
-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.text import Const, Format
-from dishka import FromDishka
-from dishka.integrations.aiogram_dialog import inject
-
-from trudex.application.bot.admin_dialogs.states import AdminGroupsSG, AdminMenuSG
-from trudex.infrastructure.database.dao.group import GroupDAO
-
-
-async def on_group_click(_callback: CallbackQuery, _widget, _manager: DialogManager, _item_id: str):
- await _callback.answer("ℹ️ Для удаления используйте кнопку 'Удалить группу'")
-
-
-@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)
-
- message_text = "👥 Управление группами\n\n"
- if success_message:
- message_text += f"{success_message}\n\n"
- message_text += f"📊 Всего групп: {len(groups)}\n\nСписок групп:"
-
- return {
- "groups": [(str(g.id), str(g.number)) for g in groups],
- "groups_count": len(groups),
- "message_text": message_text,
- }
-
-
-async def on_add_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminGroupsSG.add_group_input_number)
-
-
-async def on_delete_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminGroupsSG.delete_groups_list)
-
-
-async def on_back_to_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
-
-
-@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
-
- number_str = message.text.strip()
-
- if not number_str.isdigit():
- await message.answer("❌ Номер группы должен содержать только цифры")
- return
-
- number = int(number_str)
-
- if number < 1000 or number > 9999:
- await message.answer("❌ Номер группы должен быть четырехзначным (1000-9999)")
- return
-
- existing = await group_dao.get_by_number(number)
- if existing:
- await message.answer(f"❌ Группа с номером {number} уже существует")
- return
-
- try:
- await group_dao.create(number=number)
- manager.dialog_data["success_message"] = f"✅ Группа {number} создана"
- except Exception as e:
- await message.answer(f"❌ Ошибка создания группы: {e}")
- return
-
- await manager.switch_to(AdminGroupsSG.groups_list)
-
-
-async def on_cancel_add(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminGroupsSG.groups_list)
-
-
-@inject
-async def get_delete_groups_data(dialog_manager: DialogManager, group_dao: FromDishka[GroupDAO], **_kwargs):
- groups = await group_dao.get_all()
-
- return {
- "groups": [(str(g.id), f"{g.number}") for g in groups],
- "groups_count": len(groups),
- }
-
-
-@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)
- return
-
- manager.dialog_data["delete_group_id"] = group.id
- manager.dialog_data["delete_group_number"] = group.number
- await manager.switch_to(AdminGroupsSG.delete_confirm)
-
-
-async def get_delete_confirm_data(dialog_manager: DialogManager, **_kwargs):
- number = dialog_manager.dialog_data.get("delete_group_number", "")
-
- return {
- "group_info": str(number)
- }
-
-
-@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")
-
- assert isinstance(group_id, int)
-
- await group_dao.delete(group_id)
-
- manager.dialog_data["success_message"] = "✅ Группа удалена"
- await manager.switch_to(AdminGroupsSG.groups_list)
-
-
-async def on_cancel_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminGroupsSG.delete_groups_list)
-
-
-groups_dialog = Dialog(
- Window(
- Format("{message_text}"),
- ScrollingGroup(
- Select(
- Format("{item[1]}"),
- id="groups",
- item_id_getter=lambda x: x[0],
- items="groups",
- on_click=on_group_click,
- ),
- id="groups_scroll",
- width=2,
- height=7,
- ),
- Column(
- Button(Const("➕ Добавить группу"), id="add", on_click=on_add_group),
- Button(Const("🗑 Удалить группу"), id="delete", on_click=on_delete_group),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_menu),
- ),
- state=AdminGroupsSG.groups_list,
- getter=get_groups_data,
- ),
- Window(
- Const("➕ Добавление группы\n\n🔢 Введите номер группы (четырехзначное число 1000-9999):"),
- MessageInput(on_group_number_input),
- Button(Const("◀️ Отмена"), id="cancel", on_click=on_cancel_add),
- state=AdminGroupsSG.add_group_input_number,
- ),
- Window(
- Format("🗑 Удаление группы\n\nВыберите группу для удаления:\n\n📊 Всего групп: {groups_count}"),
- ScrollingGroup(
- Select(
- Format("{item[1]}"),
- id="delete_groups",
- item_id_getter=lambda x: x[0],
- items="groups",
- on_click=on_select_group_to_delete,
- ),
- id="delete_groups_scroll",
- width=2,
- height=7,
- ),
- Button(Const("◀️ Назад"), id="back", on_click=on_cancel_add),
- state=AdminGroupsSG.delete_groups_list,
- getter=get_delete_groups_data,
- ),
- Window(
- Format("⚠️ Подтверждение удаления\n\nТочно хотите удалить группу?\n\n👥 {group_info}"),
- Row(
- Button(Const("✅ Да, удалить"), id="confirm", on_click=on_confirm_delete),
- Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_delete),
- ),
- state=AdminGroupsSG.delete_confirm,
- getter=get_delete_confirm_data,
- ),
-)
diff --git a/src/trudex/application/bot/admin_dialogs/main_menu.py b/src/trudex/application/bot/admin_dialogs/main_menu.py
index c59ad27..52f5ee5 100644
--- a/src/trudex/application/bot/admin_dialogs/main_menu.py
+++ b/src/trudex/application/bot/admin_dialogs/main_menu.py
@@ -1,30 +1,35 @@
from aiogram.types import CallbackQuery
-from aiogram_dialog import Dialog, DialogManager, StartMode, Window
+from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Column
from aiogram_dialog.widgets.text import Const
-from trudex.application.bot.admin_dialogs.states import (AdminBroadcastSG, AdminGroupsSG, AdminMenuSG,
- AdminTemplatesSG, AdminTestsSG, AdminUsersSG)
+from trudex.application.bot.admin_dialogs.states import AdminMenuSG, AdminUsersSG
+from trudex.application.bot.shared_dialogs.states import (
+ SharedBroadcastSG,
+ SharedGroupsSG,
+ SharedTemplatesSG,
+ SharedTestsSG,
+)
async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(AdminTestsSG.tests_list, mode=StartMode.RESET_STACK)
+ await manager.start(SharedTestsSG.tests_list)
async def on_users_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(AdminUsersSG.users_list, mode=StartMode.RESET_STACK)
+ await manager.start(AdminUsersSG.users_list)
async def on_groups_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(AdminGroupsSG.groups_list, mode=StartMode.RESET_STACK)
+ await manager.start(SharedGroupsSG.groups_list)
async def on_broadcast_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(AdminBroadcastSG.broadcast_input, mode=StartMode.RESET_STACK)
+ await manager.start(SharedBroadcastSG.broadcast_input)
async def on_templates_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(AdminTemplatesSG.main, mode=StartMode.RESET_STACK)
+ await manager.start(SharedTemplatesSG.main)
admin_menu_dialog = Dialog(
diff --git a/src/trudex/application/bot/admin_dialogs/states.py b/src/trudex/application/bot/admin_dialogs/states.py
index c374aa7..fcc4680 100644
--- a/src/trudex/application/bot/admin_dialogs/states.py
+++ b/src/trudex/application/bot/admin_dialogs/states.py
@@ -5,57 +5,7 @@ class AdminMenuSG(StatesGroup):
main = State()
-class AdminTemplatesSG(StatesGroup):
- main = State()
- export_list = State()
- spec = State()
- import_file = State()
-
-
class AdminUsersSG(StatesGroup):
users_list = State()
users_input = State()
user_detail = State()
-
-
-class AdminTestsSG(StatesGroup):
- tests_list = State()
- test_detail = State()
- share_test = State()
- edit_menu = State()
- edit_password = State()
- edit_attempts = State()
- edit_group = State()
- edit_expires = State()
- statistics = State()
- attempt_detail = State()
-
-
-class AdminBroadcastSG(StatesGroup):
- broadcast_input = State()
- broadcast_confirm = State()
-
-
-class AdminGroupsSG(StatesGroup):
- groups_list = State()
- add_group_input_number = State()
- delete_groups_list = State()
- delete_confirm = State()
-
-
-class AdminCreateTestSG(StatesGroup):
- input_title = State()
- input_description = State()
- input_password = State()
- input_attempts = State()
- input_expires_at = State()
- input_for_group = State()
- confirm_test_info = State()
- add_question = State()
- input_question_text = State()
- select_question_type = State()
- input_correct_answer = State()
- input_options = State()
- mark_correct_options = State()
- confirm_question = State()
- test_created = State()
diff --git a/src/trudex/application/bot/admin_dialogs/users.py b/src/trudex/application/bot/admin_dialogs/users.py
index 55cba6b..c1b67ba 100644
--- a/src/trudex/application/bot/admin_dialogs/users.py
+++ b/src/trudex/application/bot/admin_dialogs/users.py
@@ -1,25 +1,26 @@
from aiogram.types import CallbackQuery, Message
-from aiogram_dialog import Dialog, DialogManager, StartMode, Window
+from aiogram_dialog import Dialog, DialogManager, Window
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_dialog import inject
-from trudex.application.bot.admin_dialogs.states import AdminMenuSG, AdminUsersSG
+from trudex.application.bot.admin_dialogs.states import AdminUsersSG
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()
+ users_sorted = sorted(users, key=lambda u: u.created_at or u.id, reverse=True)
return {
"users": [
- (f"{u.name or u.first_name} (@{u.username or 'нет'})", u.id)
- for u in users
+ (f"{'👑 ' if u.is_admin else ''}{u.name or u.first_name} (@{u.username or 'нет'})", u.id)
+ for u in users_sorted
],
- "count": len(users),
+ "count": len(users_sorted),
}
@@ -34,7 +35,6 @@ async def get_user_detail_data(dialog_manager: DialogManager, user_dao: FromDish
return {"user_info": "Пользователь не найден"}
username_str = f"@{user.username}" if user.username else "—"
- last_name_str = user.last_name or "—"
name_str = user.name or "—"
group_str = str(user.group) if user.group else "—"
admin_status = "✅ Да" if user.is_admin else "❌ Нет"
@@ -42,8 +42,7 @@ async def get_user_detail_data(dialog_manager: DialogManager, user_dao: FromDish
user_info = (
f"👤 Информация о пользователе\n\n"
f"ID: {user.id}\n"
- f"Имя: {user.first_name}\n"
- f"Фамилия: {last_name_str}\n"
+ f"Ник: {user.first_name}\n"
f"Имя и фамилия: {name_str}\n"
f"Username: {username_str}\n"
f"Группа: {group_str}\n"
@@ -83,10 +82,10 @@ async def on_user_input(message: Message, _widget: MessageInput, manager: Dialog
async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
+ await manager.done()
-users_dialog = Dialog(
+admin_users_dialog = Dialog(
Window(
Format("👥 Пользователи\n\nВсего: {count}"),
ScrollingGroup(
diff --git a/src/trudex/application/bot/creator_dialogs/broadcast.py b/src/trudex/application/bot/creator_dialogs/broadcast.py
deleted file mode 100644
index ba534fd..0000000
--- a/src/trudex/application/bot/creator_dialogs/broadcast.py
+++ /dev/null
@@ -1,73 +0,0 @@
-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, Row
-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, CreatorMenuSG
-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.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
-
-
-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/create_test.py b/src/trudex/application/bot/creator_dialogs/create_test.py
deleted file mode 100644
index 22ce7cb..0000000
--- a/src/trudex/application/bot/creator_dialogs/create_test.py
+++ /dev/null
@@ -1,574 +0,0 @@
-from datetime import date, datetime, time
-
-from aiogram.types import CallbackQuery, ContentType, Message
-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.text import Const, Format
-from dishka import FromDishka
-from dishka.integrations.aiogram_dialog import inject
-
-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
-from trudex.infrastructure.database.dao.test import TestDAO
-from trudex.infrastructure.database.repo.test import TestRepository
-from trudex.infrastructure.utils.timezone import to_msk
-
-
-async def on_title_input(message: Message, _widget: MessageInput, manager: DialogManager):
- if not message.text:
- await message.answer("❌ Название не может быть пустым")
- return
-
- title = message.text.strip()
- if not title:
- await message.answer("❌ Название не может быть пустым")
- return
-
- if len(title) > 255:
- await message.answer("❌ Название слишком длинное (максимум 255 символов)")
- return
-
- manager.dialog_data["title"] = title
- await manager.switch_to(CreateTestSG.input_description)
-
-
-async def on_description_input(message: Message, _widget: MessageInput, manager: DialogManager):
- if not message.text:
- await message.answer("❌ Описание не может быть пустым")
- return
-
- description = message.text.strip()
- if not description:
- await message.answer("❌ Описание не может быть пустым")
- return
-
- if len(description) > 2000:
- await message.answer("❌ Описание слишком длинное (максимум 2000 символов)")
- return
-
- manager.dialog_data["description"] = description
- await manager.switch_to(CreateTestSG.input_password)
-
-
-@inject
-async def on_password_input(message: Message, _widget: MessageInput, manager: DialogManager, _group_dao: FromDishka[GroupDAO]):
- if not message.text:
- await message.answer("❌ Пароль не может быть пустым")
- return
-
- password = message.text.strip()
- if not password:
- await message.answer("❌ Пароль не может быть пустым")
- return
-
- if len(password) > 255:
- await message.answer("❌ Пароль слишком длинный (максимум 255 символов)")
- return
-
- manager.dialog_data["password"] = password
- await manager.switch_to(CreateTestSG.input_attempts)
-
-
-@inject
-async def on_skip_password(_callback: CallbackQuery, _button: Button, manager: DialogManager, _group_dao: FromDishka[GroupDAO]):
- manager.dialog_data["password"] = None
- await manager.switch_to(CreateTestSG.input_attempts)
-
-
-async def on_attempts_input(message: Message, _widget: MessageInput, manager: DialogManager):
- if not message.text:
- await message.answer("❌ Количество попыток не может быть пустым")
- return
-
- attempts_str = message.text.strip()
-
- if not attempts_str.isdigit():
- await message.answer("❌ Количество попыток должно быть числом")
- return
-
- attempts = int(attempts_str)
-
- if attempts < 1:
- await message.answer("❌ Количество попыток должно быть больше 0")
- return
-
- if attempts > 100:
- await message.answer("❌ Количество попыток не может быть больше 100")
- return
-
- manager.dialog_data["attempts"] = attempts
- await manager.switch_to(CreateTestSG.input_expires_at)
-
-
-async def on_skip_attempts(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- manager.dialog_data["attempts"] = None
- await manager.switch_to(CreateTestSG.input_expires_at)
-
-
-async def on_date_selected(_callback, _widget, manager: DialogManager, selected_date: date):
- manager.dialog_data["expires_at"] = datetime.combine(selected_date, time.min)
- await manager.switch_to(CreateTestSG.input_for_group)
-
-
-async def on_skip_expires(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- manager.dialog_data["expires_at"] = None
- await manager.switch_to(CreateTestSG.input_for_group)
-
-
-@inject
-async def get_groups_for_test(group_dao: FromDishka[GroupDAO], **_kwargs):
- groups = await group_dao.get_all()
-
- return {
- "groups": [(str(g.number), str(g.number)) for g in groups],
- }
-
-
-async def on_group_selected(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str):
- manager.dialog_data["for_group"] = int(item_id)
- await manager.switch_to(CreateTestSG.confirm_test_info)
-
-
-async def on_skip_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- manager.dialog_data["for_group"] = None
- await manager.switch_to(CreateTestSG.confirm_test_info)
-
-
-async def get_test_info(dialog_manager: DialogManager, **_kwargs):
- title = dialog_manager.dialog_data.get("title", "—")
- description = dialog_manager.dialog_data.get("description", "—")
- password = dialog_manager.dialog_data.get("password")
- attempts = dialog_manager.dialog_data.get("attempts")
- expires_at = dialog_manager.dialog_data.get("expires_at")
- for_group = dialog_manager.dialog_data.get("for_group")
-
- password_str = f"🔒 {password}" if password else "Без пароля"
- attempts_str = f"🔄 {attempts}" if attempts else "♾️ Без ограничений"
- expires_at_msk = to_msk(expires_at)
- expires_str = expires_at_msk.strftime("%d.%m.%Y") if expires_at_msk else "Без срока"
- group_str = str(for_group) if for_group else "Для всех"
-
- return {
- "info": (
- f"📝 Информация о тесте\n\n"
- f"Название: {title}\n"
- f"Описание: {description}\n"
- f"Пароль: {password_str}\n"
- f"Попыток: {attempts_str}\n"
- f"Истекает: {expires_str}\n"
- f"Для группы: {group_str}"
- )
- }
-
-
-@inject
-async def on_confirm_test(_callback: CallbackQuery, _button: Button, manager: DialogManager, test_dao: FromDishka[TestDAO]):
- title = manager.dialog_data.get("title")
- assert isinstance(title, str)
- description = manager.dialog_data.get("description")
- password = manager.dialog_data.get("password")
- attempts = manager.dialog_data.get("attempts")
- expires_at = manager.dialog_data.get("expires_at")
- for_group = manager.dialog_data.get("for_group")
-
- test = await test_dao.create(
- title=title,
- description=description,
- password=password,
- attempts=attempts,
- expires_at=expires_at,
- for_group=for_group,
- )
-
- manager.dialog_data["test_id"] = test.id
- manager.dialog_data["questions"] = []
- await manager.switch_to(CreateTestSG.add_question)
-
-
-async def on_add_question(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- manager.dialog_data["current_question"] = {}
- await manager.switch_to(CreateTestSG.input_question_text)
-
-
-async def on_question_input(message: Message, _widget: MessageInput, manager: DialogManager):
- current_question = manager.dialog_data.get("current_question", {})
-
- if message.content_type == ContentType.PHOTO:
- photo = message.photo[-1] if message.photo else None
- if photo:
- text = (message.caption or "").strip()
- if not text:
- await message.answer("❌ Изображение должно содержать подпись с текстом вопроса")
- return
- if len(text) > 2000:
- await message.answer("❌ Текст вопроса слишком длинный (максимум 2000 символов)")
- return
- current_question["tg_file_id"] = photo.file_id
- current_question["text"] = text
- elif message.content_type == ContentType.TEXT and message.text:
- text = message.text.strip()
- if not text:
- await message.answer("❌ Текст вопроса не может быть пустым")
- return
- if len(text) > 2000:
- await message.answer("❌ Текст вопроса слишком длинный (максимум 2000 символов)")
- return
- current_question["text"] = text
- current_question["tg_file_id"] = None
- else:
- await message.answer("❌ Отправьте текст или фото с подписью")
- return
-
- manager.dialog_data["current_question"] = current_question
- await manager.switch_to(CreateTestSG.select_question_type)
-
-
-async def get_question_type_data(**_kwargs):
- return {
- "question_types": [
- ("single", "📌 Один правильный ответ"),
- ("multiple", "� Несколько правильных ответов"),
- ("input", "✏️ Ввод текста"),
- ]
- }
-
-
-async def on_question_type_selected(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str):
- current_question = manager.dialog_data.get("current_question", {})
- current_question["question_type"] = item_id
- manager.dialog_data["current_question"] = current_question
-
- if item_id == "input":
- await manager.switch_to(CreateTestSG.input_correct_answer)
- else:
- manager.dialog_data["current_options"] = []
- await manager.switch_to(CreateTestSG.input_options)
-
-
-async def on_correct_answer_input(message: Message, _widget: MessageInput, manager: DialogManager):
- if not message.text:
- await message.answer("❌ Правильный ответ не может быть пустым")
- return
-
- answer = message.text.strip()
- if not answer:
- await message.answer("❌ Правильный ответ не может быть пустым")
- return
-
- if len(answer) > 255:
- await message.answer("❌ Ответ слишком длинный (максимум 255 символов)")
- return
-
- current_question = manager.dialog_data.get("current_question", {})
- current_question["correct_answer"] = answer
- manager.dialog_data["current_question"] = current_question
- await manager.switch_to(CreateTestSG.confirm_question)
-
-
-async def on_option_input(message: Message, _widget: MessageInput, manager: DialogManager):
- if not message.text:
- await message.answer("❌ Вариант ответа не может быть пустым")
- return
-
- option_text = message.text.strip()
- if not option_text:
- await message.answer("❌ Вариант ответа не может быть пустым")
- return
-
- if len(option_text) > 255:
- await message.answer("❌ Вариант ответа слишком длинный (максимум 255 символов)")
- return
-
- current_options = manager.dialog_data.get("current_options", [])
-
- if len(current_options) >= 10:
- await message.answer("❌ Максимум 10 вариантов ответа")
- return
-
- current_options.append({"text": option_text, "is_correct": False})
- manager.dialog_data["current_options"] = current_options
-
- await message.answer(f"✅ Вариант {len(current_options)} добавлен")
-
-
-async def on_finish_options(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- current_options = manager.dialog_data.get("current_options", [])
- if len(current_options) < 2:
- await _callback.answer("❌ Добавьте минимум 2 варианта ответа", show_alert=True)
- return
-
- await manager.switch_to(CreateTestSG.mark_correct_options)
-
-
-async def get_options_data(dialog_manager: DialogManager, **_kwargs):
- current_options = dialog_manager.dialog_data.get("current_options", [])
- formatted_options = []
- for i, opt in enumerate(current_options):
- marker = "✅" if opt["is_correct"] else "❌"
- formatted_options.append((str(i), f"{marker} {opt['text']}"))
- return {
- "options": formatted_options,
- "options_count": len(current_options),
- }
-
-
-async def on_option_toggle(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str):
- current_options = manager.dialog_data.get("current_options", [])
- current_question = manager.dialog_data.get("current_question", {})
- question_type = current_question.get("question_type", "single")
-
- option_idx = int(item_id)
-
- if question_type == "single":
- for opt in current_options:
- opt["is_correct"] = False
- current_options[option_idx]["is_correct"] = True
- else:
- current_options[option_idx]["is_correct"] = not current_options[option_idx]["is_correct"]
-
- manager.dialog_data["current_options"] = current_options
- await _callback.answer()
-
-
-async def on_confirm_correct(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- current_options = manager.dialog_data.get("current_options", [])
-
- if not any(opt["is_correct"] for opt in current_options):
- await _callback.answer("❌ Отметьте хотя бы один правильный ответ", show_alert=True)
- return
-
- await manager.switch_to(CreateTestSG.confirm_question)
-
-
-async def get_question_preview(dialog_manager: DialogManager, **_kwargs):
- current_question = dialog_manager.dialog_data.get("current_question", {})
- current_options = dialog_manager.dialog_data.get("current_options", [])
-
- text = current_question.get("text", "")
- question_type = current_question.get("question_type", "single")
- has_image = current_question.get("tg_file_id") is not None
-
- type_names = {
- "single": "📌 Один правильный ответ",
- "multiple": "📋 Несколько правильных ответов",
- "input": "✏️ Ввод текста",
- }
-
- preview = f"📝 Предпросмотр вопроса\n\n"
- preview += f"Текст: {text}\n"
- preview += f"Тип: {type_names[question_type]}\n"
- preview += f"Изображение: {'✅ Да' if has_image else '❌ Нет'}\n\n"
-
- if question_type == "input":
- correct_answer = current_question.get("correct_answer", "")
- preview += f"Правильный ответ: {correct_answer}"
- else:
- preview += "Варианты ответов:\n"
- for i, opt in enumerate(current_options, 1):
- marker = "✅" if opt["is_correct"] else "❌"
- preview += f"{i}. {marker} {opt['text']}\n"
-
- return {"preview": preview}
-
-
-@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")
- assert isinstance(test_id, int)
- current_question = manager.dialog_data.get("current_question", {})
- current_options = manager.dialog_data.get("current_options", [])
-
- questions_count = await test_repo.count_questions_in_test(test_id)
-
- question = await question_dao.create(
- test_id=test_id,
- text=current_question.get("text", ""),
- position=questions_count,
- question_type=current_question.get("question_type", "single"),
- tg_file_id=current_question.get("tg_file_id"),
- )
-
- if current_question.get("question_type") == "input":
- await option_dao.create(
- question_id=question.id,
- text=current_question.get("correct_answer", ""),
- is_correct=True,
- )
- else:
- for opt in current_options:
- await option_dao.create(
- question_id=question.id,
- text=opt["text"],
- is_correct=opt["is_correct"],
- )
-
- questions = manager.dialog_data.get("questions", [])
- questions.append(question.id)
- manager.dialog_data["questions"] = questions
-
- manager.dialog_data.pop("current_question", None)
- manager.dialog_data.pop("current_options", None)
-
- await _callback.answer("✅ Вопрос добавлен")
- await manager.switch_to(CreateTestSG.add_question)
-
-
-async def on_cancel_question(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- manager.dialog_data.pop("current_question", None)
- manager.dialog_data.pop("current_options", None)
- await manager.switch_to(CreateTestSG.add_question)
-
-
-async def get_questions_count(dialog_manager: DialogManager, **_kwargs):
- questions = dialog_manager.dialog_data.get("questions", [])
- return {"questions_count": len(questions)}
-
-
-async def on_finish_test(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- questions = manager.dialog_data.get("questions", [])
-
- if len(questions) == 0:
- await _callback.answer("❌ Добавьте хотя бы один вопрос", show_alert=True)
- return
-
- await _callback.answer("✅ Тест создан")
- await manager.start(CreatorTestsSG.tests_list, mode=StartMode.RESET_STACK)
-
-
-async def on_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.start(CreatorTestsSG.tests_list, mode=StartMode.RESET_STACK)
-
-
-create_test_dialog = Dialog(
- Window(
- Const("📝 Создание теста\n\n💬 Введите название теста:\n(максимум 255 символов)"),
- MessageInput(on_title_input),
- Cancel(Const("◀️ Отмена")),
- state=CreateTestSG.input_title,
- ),
- Window(
- Const("📝 Создание теста\n\n📄 Введите описание теста:\n(максимум 2000 символов)"),
- MessageInput(on_description_input),
- state=CreateTestSG.input_description,
- ),
- Window(
- Const("🔒 Пароль\n\n🔑 Введите пароль для доступа к тесту или пропустите этот шаг:\n(максимум 255 символов)"),
- MessageInput(on_password_input),
- Button(Const("⏭️ Без пароля"), id="skip_password", on_click=on_skip_password),
- state=CreateTestSG.input_password,
- ),
- Window(
- Const("🔄 Количество попыток\n\n🔢 Введите количество попыток (1-100) или пропустите для неограниченного количества:"),
- MessageInput(on_attempts_input),
- Button(Const("⏭️ Без ограничений"), id="skip_attempts", on_click=on_skip_attempts),
- state=CreateTestSG.input_attempts,
- ),
- Window(
- Const("📅 Срок действия\n\n🗓 Выберите дату истечения теста или пропустите:"),
- Calendar(id="calendar", on_click=on_date_selected),
- Button(Const("⏭️ Без срока"), id="skip_expires", on_click=on_skip_expires),
- state=CreateTestSG.input_expires_at,
- ),
- Window(
- Const("👥 Группа\n\n🎓 Выберите группу или пропустите для всех:"),
- ScrollingGroup(
- Select(
- Format("{item[1]}"),
- id="groups",
- item_id_getter=lambda x: x[0],
- items="groups",
- on_click=on_group_selected,
- ),
- id="groups_scroll",
- width=2,
- height=7,
- ),
- Button(Const("⏭️ Для всех"), id="skip_group", on_click=on_skip_group),
- state=CreateTestSG.input_for_group,
- getter=get_groups_for_test,
- ),
- Window(
- Format("{info}\n\n✅ Подтвердите создание теста:"),
- Row(
- Button(Const("✅ Создать"), id="confirm", on_click=on_confirm_test),
- Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel),
- ),
- state=CreateTestSG.confirm_test_info,
- getter=get_test_info,
- ),
- Window(
- Format("➕ Добавление вопросов\n\n📊 Вопросов добавлено: {questions_count}\n\n💡 Добавьте вопросы к тесту:"),
- Column(
- Button(Const("➕ Добавить вопрос"), id="add_question", on_click=on_add_question),
- Button(Const("✅ Завершить создание"), id="finish", on_click=on_finish_test),
- ),
- state=CreateTestSG.add_question,
- getter=get_questions_count,
- ),
- Window(
- Const("❓ Текст вопроса\n\n📝 Отправьте текст вопроса или 📷 фото с подписью:\n(максимум 2000 символов)"),
- MessageInput(on_question_input, content_types=[ContentType.TEXT, ContentType.PHOTO]),
- Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
- state=CreateTestSG.input_question_text,
- ),
- Window(
- Const("📋 Тип вопроса\n\n🎯 Выберите тип вопроса:"),
- Column(Select(
- Format("{item[1]}"),
- id="question_type",
- item_id_getter=lambda x: x[0],
- items="question_types",
- on_click=on_question_type_selected,
- )),
- Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
- state=CreateTestSG.select_question_type,
- getter=get_question_type_data,
- ),
- Window(
- Const("✏️ Правильный ответ\n\n💬 Введите правильный ответ (регистр и пробелы игнорируются):\n(максимум 255 символов)"),
- MessageInput(on_correct_answer_input),
- Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
- state=CreateTestSG.input_correct_answer,
- ),
- Window(
- Format("📝 Варианты ответов\n\n📊 Добавлено вариантов: {options_count}/10\n\n💬 Введите вариант ответа:\n(максимум 255 символов)"),
- MessageInput(on_option_input),
- Button(Const("✅ Завершить добавление вариантов"), id="finish_options", on_click=on_finish_options),
- Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
- state=CreateTestSG.input_options,
- getter=get_options_data,
- ),
- Window(
- Const("✅ Правильные ответы\n\nОтметьте правильные варианты ответов:"),
- Column(Select(
- Format("{item[1]}"),
- id="options",
- item_id_getter=lambda x: x[0],
- items="options",
- on_click=on_option_toggle,
- )),
- Button(Const("✅ Подтвердить выбор"), id="confirm", on_click=on_confirm_correct),
- Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
- state=CreateTestSG.mark_correct_options,
- getter=get_options_data,
- ),
- Window(
- Format("{preview}\n\n💾 Сохранить вопрос?"),
- Row(
- Button(Const("✅ Сохранить"), id="save", on_click=on_save_question),
- Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_question),
- ),
- state=CreateTestSG.confirm_question,
- getter=get_question_preview,
- ),
-)
diff --git a/src/trudex/application/bot/creator_dialogs/main_menu.py b/src/trudex/application/bot/creator_dialogs/main_menu.py
index b77a873..d11cdae 100644
--- a/src/trudex/application/bot/creator_dialogs/main_menu.py
+++ b/src/trudex/application/bot/creator_dialogs/main_menu.py
@@ -1,30 +1,35 @@
from aiogram.types import CallbackQuery
-from aiogram_dialog import Dialog, DialogManager, StartMode, Window
+from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Column
from aiogram_dialog.widgets.text import Const
-from trudex.application.bot.creator_dialogs.states import (CreatorBroadcastSG, CreatorGroupsSG, CreatorMenuSG,
- CreatorTemplatesSG, CreatorTestsSG, CreatorUsersSG)
+from trudex.application.bot.creator_dialogs.states import CreatorMenuSG, CreatorUsersSG
+from trudex.application.bot.shared_dialogs.states import (
+ SharedBroadcastSG,
+ SharedGroupsSG,
+ SharedTemplatesSG,
+ SharedTestsSG,
+)
async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(CreatorTestsSG.tests_list, mode=StartMode.RESET_STACK)
+ await manager.start(SharedTestsSG.tests_list)
async def on_users_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(CreatorUsersSG.users_list, mode=StartMode.RESET_STACK)
+ await manager.start(CreatorUsersSG.users_list)
async def on_groups_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(CreatorGroupsSG.groups_list, mode=StartMode.RESET_STACK)
+ await manager.start(SharedGroupsSG.groups_list)
async def on_broadcast_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(CreatorBroadcastSG.broadcast_input, mode=StartMode.RESET_STACK)
+ await manager.start(SharedBroadcastSG.broadcast_input)
async def on_templates_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(CreatorTemplatesSG.main, mode=StartMode.RESET_STACK)
+ await manager.start(SharedTemplatesSG.main)
creator_menu_dialog = Dialog(
diff --git a/src/trudex/application/bot/creator_dialogs/states.py b/src/trudex/application/bot/creator_dialogs/states.py
index 6b21c34..749c2f1 100644
--- a/src/trudex/application/bot/creator_dialogs/states.py
+++ b/src/trudex/application/bot/creator_dialogs/states.py
@@ -5,59 +5,9 @@ class CreatorMenuSG(StatesGroup):
main = State()
-class CreatorTemplatesSG(StatesGroup):
- main = State()
- export_list = State()
- spec = State()
- import_file = State()
-
-
class CreatorUsersSG(StatesGroup):
users_list = State()
users_input = State()
user_detail = State()
make_admin_confirm = State()
remove_admin_confirm = State()
-
-
-class CreatorTestsSG(StatesGroup):
- tests_list = State()
- test_detail = State()
- share_test = State()
- edit_menu = State()
- edit_password = State()
- edit_attempts = State()
- edit_group = State()
- edit_expires = State()
- statistics = State()
- attempt_detail = State()
-
-
-class CreatorBroadcastSG(StatesGroup):
- broadcast_input = State()
- broadcast_confirm = State()
-
-
-class CreatorGroupsSG(StatesGroup):
- groups_list = State()
- add_group_input_number = State()
- delete_groups_list = State()
- delete_confirm = State()
-
-
-class CreateTestSG(StatesGroup):
- input_title = State()
- input_description = State()
- input_password = State()
- input_attempts = State()
- input_expires_at = State()
- input_for_group = State()
- confirm_test_info = State()
- add_question = State()
- input_question_text = State()
- select_question_type = State()
- input_correct_answer = State()
- input_options = State()
- mark_correct_options = State()
- confirm_question = State()
- test_created = State()
diff --git a/src/trudex/application/bot/creator_dialogs/templates.py b/src/trudex/application/bot/creator_dialogs/templates.py
deleted file mode 100644
index 9a12de9..0000000
--- a/src/trudex/application/bot/creator_dialogs/templates.py
+++ /dev/null
@@ -1,425 +0,0 @@
-import json
-
-from aiogram import Bot
-from aiogram.types import BufferedInputFile, CallbackQuery, ContentType, Message
-from aiogram_dialog import Dialog, DialogManager, StartMode, Window
-from aiogram_dialog.widgets.input import MessageInput
-from aiogram_dialog.widgets.kbd import Button, 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 CreatorMenuSG, CreatorTemplatesSG, CreatorTestsSG
-from trudex.domain.schemas import QuestionType
-from trudex.domain.test_parser import ParsedTest, TestParser
-from trudex.infrastructure.database.dao.option import OptionDAO
-from trudex.infrastructure.database.dao.question import QuestionDAO
-from trudex.infrastructure.database.dao.test import TestDAO
-from trudex.infrastructure.database.repo.test import TestRepository
-
-TEMPLATES_INFO = (
- "📦 Шаблоны тестов\n\n"
- "Шаблоны позволяют экспортировать и импортировать тесты в формате JSON.\n\n"
- "🔹 Экспорт — сохраните тест как файл для резервной копии или передачи\n"
- "🔹 Импорт — загрузите тест из файла\n"
- "🔹 Спецификация — описание формата JSON для создания тестов вручную"
-)
-
-SPEC_INFO = """📋 Спецификация формата JSON
-
-Структура файла:
-{
- "title": "Название теста",
- "description": "Описание теста",
- "password": null,
- "attempts": null,
- "expires_at": null,
- "for_group": null,
- "questions": [...]
-}
-
-Поля теста:
-• title — название (обязательно, до 255 символов)
-• description — описание (до 2000 символов)
-• password — пароль для доступа или null
-• attempts — лимит попыток (1-100) или null
-• expires_at — срок действия в ISO формате или null
-• for_group — номер группы или null для всех
-
-Типы вопросов:
-• single — один правильный ответ
-• multiple — несколько правильных ответов
-• input — ввод текста (регистр и пробелы игнорируются)
-
-Формат вопроса (single/multiple):
-{
- "text": "Текст вопроса",
- "question_type": "single",
- "options": [
- {"text": "Вариант 1", "is_correct": true},
- {"text": "Вариант 2", "is_correct": false}
- ]
-}
-
-Формат вопроса (input):
-{
- "text": "Текст вопроса",
- "question_type": "input",
- "correct_answer": "правильный ответ"
-}
-
-⚠️ Важно:
-• Для single — ровно один is_correct: true
-• Для multiple — один или более is_correct: true
-• Минимум 2 варианта ответа для single/multiple"""
-
-TEMPLATE_SINGLE = {
- "title": "Пример теста с одиночным выбором",
- "description": "Демонстрация формата single вопросов",
- "password": None,
- "attempts": None,
- "expires_at": None,
- "for_group": None,
- "questions": [
- {
- "text": "Какой язык программирования используется для разработки Telegram ботов?",
- "question_type": "single",
- "options": [
- {"text": "Python", "is_correct": True},
- {"text": "HTML", "is_correct": False},
- {"text": "CSS", "is_correct": False},
- ],
- },
- ],
-}
-
-TEMPLATE_MULTIPLE = {
- "title": "Пример теста с множественным выбором",
- "description": "Демонстрация формата multiple вопросов",
- "password": None,
- "attempts": None,
- "expires_at": None,
- "for_group": None,
- "questions": [
- {
- "text": "Выберите языки программирования:",
- "question_type": "multiple",
- "options": [
- {"text": "Python", "is_correct": True},
- {"text": "JavaScript", "is_correct": True},
- {"text": "HTML", "is_correct": False},
- {"text": "CSS", "is_correct": False},
- ],
- },
- ],
-}
-
-TEMPLATE_INPUT = {
- "title": "Пример теста с вводом текста",
- "description": "Демонстрация формата input вопросов",
- "password": None,
- "attempts": None,
- "expires_at": None,
- "for_group": None,
- "questions": [
- {
- "text": "Как называется библиотека для создания Telegram ботов на Python?",
- "question_type": "input",
- "correct_answer": "aiogram",
- },
- ],
-}
-
-TEMPLATE_FULL = {
- "title": "Полный пример теста",
- "description": "Тест со всеми типами вопросов и настройками",
- "password": "secret123",
- "attempts": 3,
- "expires_at": "2026-12-31T23:59:59",
- "for_group": 1234,
- "questions": [
- {
- "text": "Выберите правильный ответ:",
- "question_type": "single",
- "options": [
- {"text": "Вариант A", "is_correct": False},
- {"text": "Вариант B", "is_correct": True},
- {"text": "Вариант C", "is_correct": False},
- ],
- },
- {
- "text": "Выберите все правильные ответы:",
- "question_type": "multiple",
- "options": [
- {"text": "Ответ 1", "is_correct": True},
- {"text": "Ответ 2", "is_correct": True},
- {"text": "Ответ 3", "is_correct": False},
- ],
- },
- {
- "text": "Введите ответ:",
- "question_type": "input",
- "correct_answer": "ответ",
- },
- ],
-}
-
-
-async def on_export_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(CreatorTemplatesSG.export_list)
-
-
-async def on_import_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(CreatorTemplatesSG.import_file)
-
-
-async def on_spec_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(CreatorTemplatesSG.spec)
-
-
-async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
-
-
-async def on_back_to_templates(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(CreatorTemplatesSG.main)
-
-
-@inject
-async def get_tests_for_export(test_dao: FromDishka[TestDAO], **_kwargs):
- tests = await test_dao.get_all()
- return {
- "tests": [(f"📝 {t.title}", t.id) for t in tests],
- "count": len(tests),
- }
-
-
-@inject
-async def on_test_selected_for_export(
- _callback: CallbackQuery,
- _widget: Select, # type: ignore[type-arg]
- _manager: DialogManager,
- item_id: str,
- test_repo: FromDishka[TestRepository],
-) -> None:
- test_id = int(item_id)
- test, questions_with_options = await test_repo.get_full_test(test_id)
-
- if not test:
- await _callback.answer("❌ Тест не найден")
- return
-
- export_data: dict = {
- "title": test.title,
- "description": test.description,
- "password": test.password,
- "attempts": test.attempts,
- "expires_at": test.expires_at.isoformat() if test.expires_at else None,
- "for_group": test.for_group,
- "questions": [],
- }
-
- questions_list: list = export_data["questions"]
-
- for question, options in questions_with_options:
- question_data: dict = {
- "text": question.text,
- "question_type": question.question_type,
- }
-
- if question.question_type == QuestionType.INPUT:
- correct_options = [o for o in options if o.is_correct]
- if correct_options:
- question_data["correct_answer"] = correct_options[0].text
- else:
- question_data["options"] = [
- {"text": o.text, "is_correct": o.is_correct}
- for o in options
- ]
-
- questions_list.append(question_data)
-
- json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
-
- safe_title = "".join(c if c.isalnum() or c in "-_" else "_" for c in test.title)[:50]
- filename = f"{safe_title}.json"
-
- assert _callback.message is not None
- await _callback.message.answer_document(
- document=BufferedInputFile(json_str.encode("utf-8"), filename=filename),
- caption=f"📤 Экспорт теста: {test.title}",
- )
-
-
-async def send_template(callback: CallbackQuery, template: dict, name: str) -> None:
- json_str = json.dumps(template, ensure_ascii=False, indent=2)
- filename = f"template_{name}.json"
-
- assert callback.message is not None
- await callback.message.answer_document(
- document=BufferedInputFile(json_str.encode("utf-8"), filename=filename),
- caption=f"📄 Шаблон: {template['title']}",
- )
-
-
-async def on_template_single(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None:
- await send_template(_callback, TEMPLATE_SINGLE, "single")
-
-
-async def on_template_multiple(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None:
- await send_template(_callback, TEMPLATE_MULTIPLE, "multiple")
-
-
-async def on_template_input(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None:
- await send_template(_callback, TEMPLATE_INPUT, "input")
-
-
-async def on_template_full(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None:
- await send_template(_callback, TEMPLATE_FULL, "full")
-
-
-async def create_test_from_parsed(
- parsed: ParsedTest,
- test_dao: TestDAO,
- question_dao: QuestionDAO,
- option_dao: OptionDAO,
-) -> int:
- test = await test_dao.create(
- title=parsed.title,
- description=parsed.description,
- password=parsed.password,
- attempts=parsed.attempts,
- expires_at=parsed.expires_at,
- for_group=parsed.for_group,
- is_active=False,
- )
-
- for position, q in enumerate(parsed.questions):
- question = await question_dao.create(
- test_id=test.id,
- text=q.text,
- position=position,
- question_type=q.question_type,
- )
-
- for opt in q.options:
- await option_dao.create(
- question_id=question.id,
- text=opt.text,
- is_correct=opt.is_correct,
- )
-
- return test.id
-
-
-@inject
-async def on_import_file(
- message: Message,
- _widget: MessageInput,
- manager: DialogManager,
- bot_inst: FromDishka[Bot],
- test_dao: FromDishka[TestDAO],
- question_dao: FromDishka[QuestionDAO],
- option_dao: FromDishka[OptionDAO],
-) -> None:
- if not message.document:
- await message.answer("❌ Отправьте JSON файл")
- return
-
- if message.document.file_size and message.document.file_size > 1024 * 1024:
- await message.answer("❌ Файл слишком большой (максимум 1 МБ)")
- return
-
- file = await bot_inst.get_file(message.document.file_id)
- if not file.file_path:
- await message.answer("❌ Не удалось загрузить файл")
- return
-
- file_bytes = await bot_inst.download_file(file.file_path)
- if not file_bytes:
- await message.answer("❌ Не удалось загрузить файл")
- return
-
- try:
- json_str = file_bytes.read().decode("utf-8")
- except UnicodeDecodeError:
- await message.answer("❌ Файл должен быть в кодировке UTF-8")
- return
-
- parser = TestParser()
- result = parser.parse(json_str)
-
- if isinstance(result, list):
- if not result:
- await message.answer("❌ Неизвестная ошибка валидации")
- return
- error_lines = ["❌ Ошибки валидации:\n"]
- for err in result[:10]:
- path_str = f" ({err.path})" if err.path else ""
- error_lines.append(f"• {err.message}{path_str}")
- if len(result) > 10:
- error_lines.append(f"\n... и ещё {len(result) - 10} ошибок")
- await message.answer("\n".join(error_lines))
- return
-
- await create_test_from_parsed(result, test_dao, question_dao, option_dao)
-
- await message.answer(
- f"✅ Тест импортирован!\n\n"
- f"📝 Название: {result.title}\n"
- f"❓ Вопросов: {len(result.questions)}\n\n"
- f"Тест создан в деактивированном состоянии."
- )
-
- await manager.start(CreatorTestsSG.tests_list, mode=StartMode.RESET_STACK)
-
-
-templates_dialog = Dialog(
- Window(
- Const(TEMPLATES_INFO),
- Row(
- Button(Const("📤 Экспорт"), id="export", on_click=on_export_clicked),
- Button(Const("📥 Импорт"), id="import", on_click=on_import_clicked),
- ),
- Button(Const("📋 Спецификация"), id="spec", on_click=on_spec_clicked),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_clicked),
- state=CreatorTemplatesSG.main,
- ),
- Window(
- Format("📤 Экспорт теста\n\nВыберите тест для экспорта:\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_for_export, # type: ignore[arg-type]
- ),
- id="tests_scroll",
- width=1,
- height=7,
- ),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_templates),
- state=CreatorTemplatesSG.export_list,
- getter=get_tests_for_export,
- ),
- Window(
- Const(SPEC_INFO),
- Row(
- Button(Const("📌 Single"), id="tpl_single", on_click=on_template_single),
- Button(Const("📋 Multiple"), id="tpl_multiple", on_click=on_template_multiple),
- ),
- Row(
- Button(Const("✏️ Input"), id="tpl_input", on_click=on_template_input),
- Button(Const("📦 Полный"), id="tpl_full", on_click=on_template_full),
- ),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_templates),
- state=CreatorTemplatesSG.spec,
- ),
- Window(
- Const("📥 Импорт теста\n\nОтправьте JSON файл с тестом.\n\nФормат файла описан в разделе «Спецификация»"),
- MessageInput(on_import_file, content_types=[ContentType.DOCUMENT]),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_templates),
- state=CreatorTemplatesSG.import_file,
- ),
-)
diff --git a/src/trudex/application/bot/creator_dialogs/tests.py b/src/trudex/application/bot/creator_dialogs/tests.py
deleted file mode 100644
index 6cd9ba4..0000000
--- a/src/trudex/application/bot/creator_dialogs/tests.py
+++ /dev/null
@@ -1,561 +0,0 @@
-import asyncio
-import functools
-from datetime import date, datetime, time
-
-from aiogram import Bot
-from aiogram.enums import ContentType
-from aiogram.types import BufferedInputFile, CallbackQuery, Message
-from aiogram_dialog import Dialog, DialogManager, StartMode, Window
-from aiogram_dialog.api.entities import MediaAttachment
-from aiogram_dialog.widgets.input import MessageInput
-from aiogram_dialog.widgets.kbd import Button, Calendar, Column, Row, ScrollingGroup, Select
-from aiogram_dialog.widgets.media import DynamicMedia
-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 CreateTestSG, CreatorMenuSG, CreatorTestsSG
-from trudex.infrastructure.database.dao.group import GroupDAO
-from trudex.infrastructure.database.dao.test import TestDAO
-from trudex.infrastructure.database.repo.test import TestRepository
-from trudex.infrastructure.database.repo.test_attempt import TestAttemptRepository
-from trudex.infrastructure.utils.config import Config
-from trudex.infrastructure.utils.qr_generator import generate_qr_bytes
-from trudex.infrastructure.utils.test_id_to_hash import encode_id
-from trudex.infrastructure.utils.timezone import to_msk
-
-
-@inject
-async def get_tests_data(test_dao: FromDishka[TestDAO], **_kwargs):
- tests = await test_dao.get_all()
-
- return {
- "tests": [
- (f"{'🟢' if t.is_active else '🔴'} {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 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": "◀️ Назад",
- "results_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": "◀️ Назад",
- "results_button_text": "👁 Показать результаты",
- }
-
- status = "🟢 Активен" if test.is_active else "🔴 Деактивирован"
- password_str = f"🔒 {test.password}" if test.password else "🔓 Без пароля"
- attempts_str = f"🔄 {test.attempts}" if test.attempts else "♾️ Без ограничений"
- expires_str = f"📅 {to_msk(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 "👥 Для всех"
- results_str = "👁 Результаты видны" if test.are_results_viewable else "🔒 Результаты скрыты"
-
- test_info = (
- f"📝 Информация о тесте\n\n"
- f"Название:\n
{test.title}\n" - f"Описание:\n
{test.description or '—'}\n\n" - f"Статус: {status}\n" - f"Вопросов: {questions_count}\n" - f"Пароль: {password_str}\n" - f"Попытки: {attempts_str}\n" - f"Срок: {expires_str}\n" - f"Группа: {group_str}\n" - f"Видимость: {results_str}\n\n" - f"Создан: {to_msk(test.created_at).strftime('%d.%m.%Y %H:%M') if test.created_at else '—'}" - ) - - button_text = "🔴 Деактивировать" if test.is_active else "🟢 Активировать" - results_button_text = "🔒 Скрыть результаты" if test.are_results_viewable else "👁 Показать результаты" - - return { - "test_info": test_info, - "is_active": test.is_active, - "button_text": button_text, - "results_button_text": results_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) - - -@inject -async def on_toggle_results_viewable(_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, are_results_viewable=not test.are_results_viewable) - action = "скрыты" if test.are_results_viewable 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_statistics(_callback: CallbackQuery, _button: Button, manager: DialogManager): - await manager.switch_to(CreatorTestsSG.statistics) - - -@inject -async def get_statistics_data( - dialog_manager: DialogManager, - attempt_repo: FromDishka[TestAttemptRepository], - **_kwargs -): - test_id = dialog_manager.dialog_data.get("selected_test_id") - - if not test_id: - return {"attempts": [], "count": 0} - - attempts_with_users = await attempt_repo.get_test_attempts_with_users(test_id) - - results = [] - for attempt, user_name in attempts_with_users: - status = "✅" if attempt.is_passed else "❌" - finished_at_msk = to_msk(attempt.finished_at) - date_str = finished_at_msk.strftime("%d.%m.%Y %H:%M") if finished_at_msk else "" - results.append((f"{status} {user_name} — {attempt.score}% ({date_str})", attempt.id)) - - return { - "attempts": results, - "count": len(results), - } - - -async def on_attempt_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str): - manager.dialog_data["selected_attempt_id"] = int(item_id) - await manager.switch_to(CreatorTestsSG.attempt_detail) - - -async def on_back_to_statistics(_callback: CallbackQuery, _button: Button, manager: DialogManager): - await manager.switch_to(CreatorTestsSG.statistics) - - -@inject -async def get_attempt_detail( - dialog_manager: DialogManager, - attempt_repo: FromDishka[TestAttemptRepository], - test_repo: FromDishka[TestRepository], - **_kwargs -): - attempt_id = dialog_manager.dialog_data.get("selected_attempt_id") - - if not attempt_id: - return {"attempt_info": "❌ Результат не найден"} - - attempt, answers = await attempt_repo.get_attempt_with_answers(attempt_id) - - if not attempt: - return {"attempt_info": "❌ Результат не найден"} - - status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден" - finished_at_msk = to_msk(attempt.finished_at) - date_str = finished_at_msk.strftime("%d.%m.%Y %H:%M") if finished_at_msk else "—" - - lines = [ - f"📊 Результат прохождения\n", - f"📈 Результат: {attempt.score}%", - f"📅 Дата: {date_str}", - f"🏆 Статус: {status}\n", - "📋 Ответы:\n", - ] - - for i, answer in enumerate(answers, 1): - question, options = await test_repo.get_question_with_options(answer.question_id) - if not question: - continue - - correct_options = [opt for opt in options if opt.is_correct] - correct_texts = [opt.text for opt in correct_options] - - status_icon = "✅" if answer.is_correct else "❌" - - user_answer = answer.text_answer or "" - if "|" in user_answer: - user_answer = ", ".join(user_answer.split("|")) - - lines.append(f"{status_icon} Вопрос {i}") - lines.append(f"
{question.text}") - lines.append(f"👤 Ответ: {user_answer or '—'}") - lines.append(f"✓ Правильно: {', '.join(correct_texts)}\n") - - return {"attempt_info": "\n".join(lines)} - - -@inject -async def on_share_test(_callback: CallbackQuery, _button: Button, manager: DialogManager, config: FromDishka[Config], bot_inst: FromDishka[Bot]): - test_id = manager.dialog_data.get("selected_test_id") - - if not test_id: - return { - "share_link": "Ошибка: тест не найден" - } - - test_hash = encode_id( - test_id, - config.security.encode_key, - config.security.encoded_string_length - ) - - bot_info = await bot_inst.get_me() - bot_username = bot_info.username or "your_bot" - share_link = f"https://t.me/{bot_username}?start={test_hash}" - - loop = asyncio.get_running_loop() - qr_bytes = await loop.run_in_executor( - None, - functools.partial(generate_qr_bytes, share_link) - ) - - assert _callback.message is not None - - await _callback.message.answer_photo( - photo=BufferedInputFile(qr_bytes, filename="qr.png"), - caption=f"🔗 Поделиться тестом\n\n📎 Ссылка на тест:\n
{share_link}\n\n💡 Отправьте эту ссылку или QR-код пользователям для прохождения теста"
- )
-
-
-async def on_edit_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorTestsSG.edit_menu)
-
-
-async def on_back_to_detail(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorTestsSG.test_detail)
-
-
-async def on_back_to_edit_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorTestsSG.edit_menu)
-
-
-async def on_edit_password(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorTestsSG.edit_password)
-
-
-async def on_edit_attempts(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorTestsSG.edit_attempts)
-
-
-async def on_edit_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorTestsSG.edit_group)
-
-
-async def on_edit_expires(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorTestsSG.edit_expires)
-
-
-@inject
-async def on_password_input(message: Message, _widget: MessageInput, manager: DialogManager, test_dao: FromDishka[TestDAO]):
- test_id = manager.dialog_data.get("selected_test_id")
- if not test_id:
- await message.answer("❌ Тест не найден")
- return
-
- if not message.text:
- await message.answer("❌ Пароль не может быть пустым")
- return
-
- password = message.text.strip()
- if len(password) > 255:
- await message.answer("❌ Пароль слишком длинный (максимум 255 символов)")
- return
-
- await test_dao.update(test_id, password=password)
- await message.answer("✅ Пароль обновлен")
- await manager.switch_to(CreatorTestsSG.test_detail)
-
-
-@inject
-async def on_remove_password(_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
-
- await test_dao.update(test_id, password=None)
- await _callback.answer("✅ Пароль удален")
- await manager.switch_to(CreatorTestsSG.test_detail)
-
-
-@inject
-async def on_attempts_input_edit(message: Message, _widget: MessageInput, manager: DialogManager, test_dao: FromDishka[TestDAO]):
- test_id = manager.dialog_data.get("selected_test_id")
- if not test_id:
- await message.answer("❌ Тест не найден")
- return
-
- if not message.text:
- await message.answer("❌ Количество попыток не может быть пустым")
- return
-
- attempts_str = message.text.strip()
-
- if not attempts_str.isdigit():
- await message.answer("❌ Количество попыток должно быть числом")
- return
-
- attempts = int(attempts_str)
-
- if attempts < 1:
- await message.answer("❌ Количество попыток должно быть больше 0")
- return
-
- if attempts > 100:
- await message.answer("❌ Количество попыток не может быть больше 100")
- return
-
- await test_dao.update(test_id, attempts=attempts)
- await message.answer("✅ Количество попыток обновлено")
- await manager.switch_to(CreatorTestsSG.test_detail)
-
-
-@inject
-async def on_remove_attempts(_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
-
- await test_dao.update(test_id, attempts=None)
- await _callback.answer("✅ Ограничение попыток удалено")
- await manager.switch_to(CreatorTestsSG.test_detail)
-
-
-@inject
-async def get_groups_for_edit(dialog_manager: DialogManager, group_dao: FromDishka[GroupDAO], **_kwargs):
- groups = await group_dao.get_all()
-
- return {
- "groups": [(str(g.number), str(g.number)) for g in groups],
- }
-
-
-@inject
-async def on_group_selected_for_test(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str, test_dao: FromDishka[TestDAO]):
- test_id = manager.dialog_data.get("selected_test_id")
- if not test_id:
- await _callback.answer("❌ Тест не найден")
- return
-
- await test_dao.update(test_id, for_group=int(item_id))
- await _callback.answer("✅ Группа обновлена")
- await manager.switch_to(CreatorTestsSG.test_detail)
-
-
-@inject
-async def on_remove_group(_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
-
- await test_dao.update(test_id, for_group=None)
- await _callback.answer("✅ Тест теперь доступен для всех групп")
- await manager.switch_to(CreatorTestsSG.test_detail)
-
-
-@inject
-async def on_date_selected_for_test(_callback, _widget, manager: DialogManager, selected_date: date, test_dao: FromDishka[TestDAO]):
- test_id = manager.dialog_data.get("selected_test_id")
- if not test_id:
- await _callback.answer("❌ Тест не найден")
- return
-
- expires_at = datetime.combine(selected_date, time.min)
- await test_dao.update(test_id, expires_at=expires_at)
- await _callback.answer("✅ Срок действия обновлен")
- await manager.switch_to(CreatorTestsSG.test_detail)
-
-
-@inject
-async def on_remove_expires(_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
-
- await test_dao.update(test_id, expires_at=None)
- await _callback.answer("✅ Срок действия удален")
- await manager.switch_to(CreatorTestsSG.test_detail)
-
-
-async def on_add_test_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.start(CreateTestSG.input_title, mode=StartMode.RESET_STACK)
-
-
-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,
- ),
- Window(
- Format("{test_info}"),
- Column(
- Button(
- Format("{button_text}"),
- id="toggle_active",
- on_click=on_toggle_active
- ),
- Button(
- Format("{results_button_text}"),
- id="toggle_results",
- on_click=on_toggle_results_viewable
- ),
- Button(Const("📊 Статистика"), id="statistics", on_click=on_statistics),
- Button(Const("🔗 Поделиться"), id="share", on_click=on_share_test),
- Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list),
- ),
- state=CreatorTestsSG.test_detail,
- getter=get_test_detail,
- ),
- Window(
- Const("✏️ Изменить тест\n\nВыберите, что хотите изменить:"),
- Column(
- Button(Const("🔑 Пароль"), id="edit_password", on_click=on_edit_password),
- Button(Const("🔄 Попытки"), id="edit_attempts", on_click=on_edit_attempts),
- Button(Const("👥 Группа"), id="edit_group", on_click=on_edit_group),
- Button(Const("📅 Срок действия"), id="edit_expires", on_click=on_edit_expires),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_detail),
- ),
- state=CreatorTestsSG.edit_menu,
- ),
- Window(
- Const("🔑 Изменение пароля\n\n💬 Введите новый пароль или удалите текущий:\n(максимум 255 символов)"),
- MessageInput(on_password_input),
- Column(
- Button(Const("🗑 Удалить пароль"), id="remove_password", on_click=on_remove_password),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_edit_menu),
- ),
- state=CreatorTestsSG.edit_password,
- ),
- Window(
- Const("🔄 Изменение количества попыток\n\n🔢 Введите новое количество попыток (1-100) или удалите ограничение:"),
- MessageInput(on_attempts_input_edit),
- Column(
- Button(Const("🗑 Без ограничений"), id="remove_attempts", on_click=on_remove_attempts),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_edit_menu),
- ),
- state=CreatorTestsSG.edit_attempts,
- ),
- Window(
- Const("👥 Изменение группы\n\n🎓 Выберите группу или удалите привязку:"),
- ScrollingGroup(
- Select(
- Format("{item[1]}"),
- id="groups",
- item_id_getter=lambda x: x[0],
- items="groups",
- on_click=on_group_selected_for_test,
- ),
- id="groups_scroll",
- width=2,
- height=7,
- ),
- Column(
- Button(Const("🗑 Для всех групп"), id="remove_group", on_click=on_remove_group),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_edit_menu),
- ),
- state=CreatorTestsSG.edit_group,
- getter=get_groups_for_edit,
- ),
- Window(
- Const("📅 Изменение срока действия\n\n🗓 Выберите новую дату или удалите срок:"),
- Calendar(id="calendar", on_click=on_date_selected_for_test),
- Column(
- Button(Const("🗑 Удалить срок"), id="remove_expires", on_click=on_remove_expires),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_edit_menu),
- ),
- state=CreatorTestsSG.edit_expires,
- ),
- Window(
- Format("📊 Статистика теста\n\nПрошли тест: {count}"),
- ScrollingGroup(
- Select(
- Format("{item[0]}"),
- id="attempt_select",
- item_id_getter=lambda x: x[1],
- items="attempts",
- on_click=on_attempt_selected,
- ),
- id="attempts_scroll",
- width=1,
- height=7,
- ),
- Column(
- Button(Const("🔄 Обновить"), id="refresh", on_click=on_statistics),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_detail),
- ),
- state=CreatorTestsSG.statistics,
- getter=get_statistics_data,
- ),
- Window(
- Format("{attempt_info}"),
- Button(Const("◀️ Назад"), id="back", on_click=on_back_to_statistics),
- state=CreatorTestsSG.attempt_detail,
- getter=get_attempt_detail,
- ),
-)
-
diff --git a/src/trudex/application/bot/creator_dialogs/users.py b/src/trudex/application/bot/creator_dialogs/users.py
index 6f19b84..6158593 100644
--- a/src/trudex/application/bot/creator_dialogs/users.py
+++ b/src/trudex/application/bot/creator_dialogs/users.py
@@ -2,14 +2,14 @@ import asyncio
from aiogram import Bot
from aiogram.types import CallbackQuery, Message
-from aiogram_dialog import Dialog, DialogManager, StartMode, Window
+from aiogram_dialog import Dialog, DialogManager, Window
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_dialog import inject
-from trudex.application.bot.creator_dialogs.states import CreatorMenuSG, CreatorUsersSG
+from trudex.application.bot.creator_dialogs.states import CreatorUsersSG
from trudex.infrastructure.database.dao.user import UserDAO
from trudex.infrastructure.database.repo.user import UserRepository
from trudex.infrastructure.utils.bot_commands import setup_bot_commands
@@ -19,7 +19,7 @@ from trudex.infrastructure.utils.config import Config
@inject
async def get_users_data(user_dao: FromDishka[UserDAO], **_kwargs):
users = await user_dao.get_all()
- users_sorted = sorted(users, key=lambda u: u.created_at, reverse=True)
+ users_sorted = sorted(users, key=lambda u: u.created_at or u.id, reverse=True)
return {
"users": [
@@ -206,10 +206,10 @@ async def on_confirm_no(_callback: CallbackQuery, _button: Button, manager: Dial
async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
+ await manager.done()
-users_dialog = Dialog(
+creator_users_dialog = Dialog(
Window(
Format("👥 Пользователи\n\nВсего: {count}"),
ScrollingGroup(
diff --git a/src/trudex/application/bot/shared_dialogs/__init__.py b/src/trudex/application/bot/shared_dialogs/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/trudex/application/bot/admin_dialogs/broadcast.py b/src/trudex/application/bot/shared_dialogs/broadcast.py
similarity index 85%
rename from src/trudex/application/bot/admin_dialogs/broadcast.py
rename to src/trudex/application/bot/shared_dialogs/broadcast.py
index 44a82ba..2940f82 100644
--- a/src/trudex/application/bot/admin_dialogs/broadcast.py
+++ b/src/trudex/application/bot/shared_dialogs/broadcast.py
@@ -1,12 +1,12 @@
from aiogram.types import CallbackQuery, Message
-from aiogram_dialog import Dialog, DialogManager, StartMode, Window
+from aiogram_dialog import Dialog, DialogManager, 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.shared_dialogs.states import SharedBroadcastSG
from trudex.infrastructure.database.dao.user import UserDAO
from trudex.infrastructure.utils.broadcast import broadcast_message
@@ -14,7 +14,7 @@ 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)
+ await manager.switch_to(SharedBroadcastSG.broadcast_confirm)
@inject
@@ -48,19 +48,19 @@ async def on_broadcast_confirm(_callback: CallbackQuery, _button: Button, manage
async def on_broadcast_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await _callback.answer("Рассылка отменена")
- await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
+ await manager.done()
async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
+ await manager.done()
-broadcast_dialog = Dialog(
+shared_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,
+ state=SharedBroadcastSG.broadcast_input,
),
Window(
Const("⚠️ Подтверждение рассылки\n\nВы уверены, что хотите отправить это сообщение всем пользователям?"),
@@ -68,6 +68,6 @@ broadcast_dialog = Dialog(
Button(Const("✅ Да"), id="broadcast_confirm", on_click=on_broadcast_confirm),
Button(Const("❌ Нет"), id="broadcast_cancel", on_click=on_broadcast_cancel),
),
- state=AdminBroadcastSG.broadcast_confirm,
+ state=SharedBroadcastSG.broadcast_confirm,
),
)
diff --git a/src/trudex/application/bot/admin_dialogs/create_test.py b/src/trudex/application/bot/shared_dialogs/create_test.py
similarity index 90%
rename from src/trudex/application/bot/admin_dialogs/create_test.py
rename to src/trudex/application/bot/shared_dialogs/create_test.py
index 006e48a..c91a134 100644
--- a/src/trudex/application/bot/admin_dialogs/create_test.py
+++ b/src/trudex/application/bot/shared_dialogs/create_test.py
@@ -8,7 +8,7 @@ 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 AdminCreateTestSG, AdminTestsSG
+from trudex.application.bot.shared_dialogs.states import SharedCreateTestSG, SharedTestsSG
from trudex.infrastructure.database.dao.group import GroupDAO
from trudex.infrastructure.database.dao.option import OptionDAO
from trudex.infrastructure.database.dao.question import QuestionDAO
@@ -32,7 +32,7 @@ async def on_title_input(message: Message, _widget: MessageInput, manager: Dialo
return
manager.dialog_data["title"] = title
- await manager.switch_to(AdminCreateTestSG.input_description)
+ await manager.switch_to(SharedCreateTestSG.input_description)
async def on_description_input(message: Message, _widget: MessageInput, manager: DialogManager):
@@ -50,7 +50,7 @@ async def on_description_input(message: Message, _widget: MessageInput, manager:
return
manager.dialog_data["description"] = description
- await manager.switch_to(AdminCreateTestSG.input_password)
+ await manager.switch_to(SharedCreateTestSG.input_password)
@inject
@@ -69,13 +69,13 @@ async def on_password_input(message: Message, _widget: MessageInput, manager: Di
return
manager.dialog_data["password"] = password
- await manager.switch_to(AdminCreateTestSG.input_attempts)
+ await manager.switch_to(SharedCreateTestSG.input_attempts)
@inject
async def on_skip_password(_callback: CallbackQuery, _button: Button, manager: DialogManager, _group_dao: FromDishka[GroupDAO]):
manager.dialog_data["password"] = None
- await manager.switch_to(AdminCreateTestSG.input_attempts)
+ await manager.switch_to(SharedCreateTestSG.input_attempts)
async def on_attempts_input(message: Message, _widget: MessageInput, manager: DialogManager):
@@ -100,41 +100,38 @@ async def on_attempts_input(message: Message, _widget: MessageInput, manager: Di
return
manager.dialog_data["attempts"] = attempts
- await manager.switch_to(AdminCreateTestSG.input_expires_at)
+ await manager.switch_to(SharedCreateTestSG.input_expires_at)
async def on_skip_attempts(_callback: CallbackQuery, _button: Button, manager: DialogManager):
manager.dialog_data["attempts"] = None
- await manager.switch_to(AdminCreateTestSG.input_expires_at)
+ await manager.switch_to(SharedCreateTestSG.input_expires_at)
async def on_date_selected(_callback, _widget, manager: DialogManager, selected_date: date):
manager.dialog_data["expires_at"] = datetime.combine(selected_date, time.min)
- await manager.switch_to(AdminCreateTestSG.input_for_group)
+ await manager.switch_to(SharedCreateTestSG.input_for_group)
async def on_skip_expires(_callback: CallbackQuery, _button: Button, manager: DialogManager):
manager.dialog_data["expires_at"] = None
- await manager.switch_to(AdminCreateTestSG.input_for_group)
+ await manager.switch_to(SharedCreateTestSG.input_for_group)
@inject
async def get_groups_for_test(group_dao: FromDishka[GroupDAO], **_kwargs):
groups = await group_dao.get_all()
-
- return {
- "groups": [(str(g.number), str(g.number)) for g in groups],
- }
+ return {"groups": [(str(g.number), str(g.number)) for g in groups]}
async def on_group_selected(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str):
manager.dialog_data["for_group"] = int(item_id)
- await manager.switch_to(AdminCreateTestSG.confirm_test_info)
+ await manager.switch_to(SharedCreateTestSG.confirm_test_info)
async def on_skip_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
manager.dialog_data["for_group"] = None
- await manager.switch_to(AdminCreateTestSG.confirm_test_info)
+ await manager.switch_to(SharedCreateTestSG.confirm_test_info)
async def get_test_info(dialog_manager: DialogManager, **_kwargs):
@@ -185,12 +182,12 @@ async def on_confirm_test(_callback: CallbackQuery, _button: Button, manager: Di
manager.dialog_data["test_id"] = test.id
manager.dialog_data["questions"] = []
- await manager.switch_to(AdminCreateTestSG.add_question)
+ await manager.switch_to(SharedCreateTestSG.add_question)
async def on_add_question(_callback: CallbackQuery, _button: Button, manager: DialogManager):
manager.dialog_data["current_question"] = {}
- await manager.switch_to(AdminCreateTestSG.input_question_text)
+ await manager.switch_to(SharedCreateTestSG.input_question_text)
async def on_question_input(message: Message, _widget: MessageInput, manager: DialogManager):
@@ -223,7 +220,7 @@ async def on_question_input(message: Message, _widget: MessageInput, manager: Di
return
manager.dialog_data["current_question"] = current_question
- await manager.switch_to(AdminCreateTestSG.select_question_type)
+ await manager.switch_to(SharedCreateTestSG.select_question_type)
async def get_question_type_data(**_kwargs):
@@ -242,10 +239,10 @@ async def on_question_type_selected(_callback: CallbackQuery, _widget, manager:
manager.dialog_data["current_question"] = current_question
if item_id == "input":
- await manager.switch_to(AdminCreateTestSG.input_correct_answer)
+ await manager.switch_to(SharedCreateTestSG.input_correct_answer)
else:
manager.dialog_data["current_options"] = []
- await manager.switch_to(AdminCreateTestSG.input_options)
+ await manager.switch_to(SharedCreateTestSG.input_options)
async def on_correct_answer_input(message: Message, _widget: MessageInput, manager: DialogManager):
@@ -265,7 +262,7 @@ async def on_correct_answer_input(message: Message, _widget: MessageInput, manag
current_question = manager.dialog_data.get("current_question", {})
current_question["correct_answer"] = answer
manager.dialog_data["current_question"] = current_question
- await manager.switch_to(AdminCreateTestSG.confirm_question)
+ await manager.switch_to(SharedCreateTestSG.confirm_question)
async def on_option_input(message: Message, _widget: MessageInput, manager: DialogManager):
@@ -300,7 +297,7 @@ async def on_finish_options(_callback: CallbackQuery, _button: Button, manager:
await _callback.answer("❌ Добавьте минимум 2 варианта ответа", show_alert=True)
return
- await manager.switch_to(AdminCreateTestSG.mark_correct_options)
+ await manager.switch_to(SharedCreateTestSG.mark_correct_options)
async def get_options_data(dialog_manager: DialogManager, **_kwargs):
@@ -340,7 +337,7 @@ async def on_confirm_correct(_callback: CallbackQuery, _button: Button, manager:
await _callback.answer("❌ Отметьте хотя бы один правильный ответ", show_alert=True)
return
- await manager.switch_to(AdminCreateTestSG.confirm_question)
+ await manager.switch_to(SharedCreateTestSG.confirm_question)
async def get_question_preview(dialog_manager: DialogManager, **_kwargs):
@@ -357,7 +354,7 @@ async def get_question_preview(dialog_manager: DialogManager, **_kwargs):
"input": "✏️ Ввод текста",
}
- preview = f"📝 Предпросмотр вопроса\n\n"
+ preview = "📝 Предпросмотр вопроса\n\n"
preview += f"Текст: {text}\n"
preview += f"Тип: {type_names[question_type]}\n"
preview += f"Изображение: {'✅ Да' if has_image else '❌ Нет'}\n\n"
@@ -420,13 +417,13 @@ async def on_save_question(
manager.dialog_data.pop("current_options", None)
await _callback.answer("✅ Вопрос добавлен")
- await manager.switch_to(AdminCreateTestSG.add_question)
+ await manager.switch_to(SharedCreateTestSG.add_question)
async def on_cancel_question(_callback: CallbackQuery, _button: Button, manager: DialogManager):
manager.dialog_data.pop("current_question", None)
manager.dialog_data.pop("current_options", None)
- await manager.switch_to(AdminCreateTestSG.add_question)
+ await manager.switch_to(SharedCreateTestSG.add_question)
async def get_questions_count(dialog_manager: DialogManager, **_kwargs):
@@ -442,42 +439,42 @@ async def on_finish_test(_callback: CallbackQuery, _button: Button, manager: Dia
return
await _callback.answer("✅ Тест создан")
- await manager.start(AdminTestsSG.tests_list, mode=StartMode.RESET_STACK)
+ await manager.start(SharedTestsSG.tests_list, mode=StartMode.RESET_STACK)
async def on_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.start(AdminTestsSG.tests_list, mode=StartMode.RESET_STACK)
+ await manager.start(SharedTestsSG.tests_list, mode=StartMode.RESET_STACK)
-admin_create_test_dialog = Dialog(
+shared_create_test_dialog = Dialog(
Window(
Const("📝 Создание теста\n\n💬 Введите название теста:\n(максимум 255 символов)"),
MessageInput(on_title_input),
Cancel(Const("◀️ Отмена")),
- state=AdminCreateTestSG.input_title,
+ state=SharedCreateTestSG.input_title,
),
Window(
Const("📝 Создание теста\n\n📄 Введите описание теста:\n(максимум 2000 символов)"),
MessageInput(on_description_input),
- state=AdminCreateTestSG.input_description,
+ state=SharedCreateTestSG.input_description,
),
Window(
Const("🔒 Пароль\n\n🔑 Введите пароль для доступа к тесту или пропустите этот шаг:\n(максимум 255 символов)"),
MessageInput(on_password_input),
Button(Const("⏭️ Без пароля"), id="skip_password", on_click=on_skip_password),
- state=AdminCreateTestSG.input_password,
+ state=SharedCreateTestSG.input_password,
),
Window(
Const("🔄 Количество попыток\n\n🔢 Введите количество попыток (1-100) или пропустите для неограниченного количества:"),
MessageInput(on_attempts_input),
Button(Const("⏭️ Без ограничений"), id="skip_attempts", on_click=on_skip_attempts),
- state=AdminCreateTestSG.input_attempts,
+ state=SharedCreateTestSG.input_attempts,
),
Window(
Const("📅 Срок действия\n\n🗓 Выберите дату истечения теста или пропустите:"),
Calendar(id="calendar", on_click=on_date_selected),
Button(Const("⏭️ Без срока"), id="skip_expires", on_click=on_skip_expires),
- state=AdminCreateTestSG.input_expires_at,
+ state=SharedCreateTestSG.input_expires_at,
),
Window(
Const("👥 Группа\n\n🎓 Выберите группу или пропустите для всех:"),
@@ -494,7 +491,7 @@ admin_create_test_dialog = Dialog(
height=7,
),
Button(Const("⏭️ Для всех"), id="skip_group", on_click=on_skip_group),
- state=AdminCreateTestSG.input_for_group,
+ state=SharedCreateTestSG.input_for_group,
getter=get_groups_for_test,
),
Window(
@@ -503,7 +500,7 @@ admin_create_test_dialog = Dialog(
Button(Const("✅ Создать"), id="confirm", on_click=on_confirm_test),
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel),
),
- state=AdminCreateTestSG.confirm_test_info,
+ state=SharedCreateTestSG.confirm_test_info,
getter=get_test_info,
),
Window(
@@ -512,14 +509,14 @@ admin_create_test_dialog = Dialog(
Button(Const("➕ Добавить вопрос"), id="add_question", on_click=on_add_question),
Button(Const("✅ Завершить создание"), id="finish", on_click=on_finish_test),
),
- state=AdminCreateTestSG.add_question,
+ state=SharedCreateTestSG.add_question,
getter=get_questions_count,
),
Window(
Const("❓ Текст вопроса\n\n📝 Отправьте текст вопроса или 📷 фото с подписью:\n(максимум 2000 символов)"),
MessageInput(on_question_input, content_types=[ContentType.TEXT, ContentType.PHOTO]),
Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
- state=AdminCreateTestSG.input_question_text,
+ state=SharedCreateTestSG.input_question_text,
),
Window(
Const("📋 Тип вопроса\n\n🎯 Выберите тип вопроса:"),
@@ -531,21 +528,21 @@ admin_create_test_dialog = Dialog(
on_click=on_question_type_selected,
)),
Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
- state=AdminCreateTestSG.select_question_type,
+ state=SharedCreateTestSG.select_question_type,
getter=get_question_type_data,
),
Window(
Const("✏️ Правильный ответ\n\n💬 Введите правильный ответ (регистр и пробелы игнорируются):\n(максимум 255 символов)"),
MessageInput(on_correct_answer_input),
Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
- state=AdminCreateTestSG.input_correct_answer,
+ state=SharedCreateTestSG.input_correct_answer,
),
Window(
Format("📝 Варианты ответов\n\n📊 Добавлено вариантов: {options_count}/10\n\n💬 Введите вариант ответа:\n(максимум 255 символов)"),
MessageInput(on_option_input),
Button(Const("✅ Завершить добавление вариантов"), id="finish_options", on_click=on_finish_options),
Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
- state=AdminCreateTestSG.input_options,
+ state=SharedCreateTestSG.input_options,
getter=get_options_data,
),
Window(
@@ -559,7 +556,7 @@ admin_create_test_dialog = Dialog(
)),
Button(Const("✅ Подтвердить выбор"), id="confirm", on_click=on_confirm_correct),
Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
- state=AdminCreateTestSG.mark_correct_options,
+ state=SharedCreateTestSG.mark_correct_options,
getter=get_options_data,
),
Window(
@@ -568,7 +565,7 @@ admin_create_test_dialog = Dialog(
Button(Const("✅ Сохранить"), id="save", on_click=on_save_question),
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_question),
),
- state=AdminCreateTestSG.confirm_question,
+ state=SharedCreateTestSG.confirm_question,
getter=get_question_preview,
),
)
diff --git a/src/trudex/application/bot/creator_dialogs/groups.py b/src/trudex/application/bot/shared_dialogs/groups.py
similarity index 82%
rename from src/trudex/application/bot/creator_dialogs/groups.py
rename to src/trudex/application/bot/shared_dialogs/groups.py
index b8ff31c..dac8871 100644
--- a/src/trudex/application/bot/creator_dialogs/groups.py
+++ b/src/trudex/application/bot/shared_dialogs/groups.py
@@ -1,12 +1,12 @@
from aiogram.types import CallbackQuery, Message
-from aiogram_dialog import Dialog, DialogManager, StartMode, Window
+from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.input import MessageInput
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 CreatorGroupsSG, CreatorMenuSG
+from trudex.application.bot.shared_dialogs.states import SharedGroupsSG
from trudex.infrastructure.database.dao.group import GroupDAO
@@ -33,16 +33,15 @@ async def get_groups_data(group_dao: FromDishka[GroupDAO], dialog_manager: Dialo
async def on_add_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorGroupsSG.add_group_input_number)
+ await manager.switch_to(SharedGroupsSG.add_group_input_number)
async def on_delete_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorGroupsSG.delete_groups_list)
+ await manager.switch_to(SharedGroupsSG.delete_groups_list)
async def on_back_to_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- from trudex.application.bot.creator_dialogs.states import CreatorMenuSG
- await manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
+ await manager.done()
@inject
@@ -68,18 +67,13 @@ async def on_group_number_input(message: Message, _widget: MessageInput, manager
await message.answer(f"❌ Группа с номером {number} уже существует")
return
- try:
- await group_dao.create(number=number)
- manager.dialog_data["success_message"] = f"✅ Группа {number} создана"
- except Exception as e:
- await message.answer(f"❌ Ошибка создания группы: {e}")
- return
-
- await manager.switch_to(CreatorGroupsSG.groups_list)
+ await group_dao.create(number=number)
+ manager.dialog_data["success_message"] = f"✅ Группа {number} создана"
+ await manager.switch_to(SharedGroupsSG.groups_list)
async def on_cancel_add(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorGroupsSG.groups_list)
+ await manager.switch_to(SharedGroupsSG.groups_list)
@inject
@@ -101,34 +95,29 @@ async def on_select_group_to_delete(_callback: CallbackQuery, _widget, manager:
manager.dialog_data["delete_group_id"] = group.id
manager.dialog_data["delete_group_number"] = group.number
- await manager.switch_to(CreatorGroupsSG.delete_confirm)
+ await manager.switch_to(SharedGroupsSG.delete_confirm)
async def get_delete_confirm_data(dialog_manager: DialogManager, **_kwargs):
number = dialog_manager.dialog_data.get("delete_group_number", "")
-
- return {
- "group_info": str(number)
- }
+ return {"group_info": str(number)}
@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")
-
assert isinstance(group_id, int)
await group_dao.delete(group_id)
-
manager.dialog_data["success_message"] = "✅ Группа удалена"
- await manager.switch_to(CreatorGroupsSG.groups_list)
+ await manager.switch_to(SharedGroupsSG.groups_list)
async def on_cancel_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(CreatorGroupsSG.delete_groups_list)
+ await manager.switch_to(SharedGroupsSG.delete_groups_list)
-groups_dialog = Dialog(
+shared_groups_dialog = Dialog(
Window(
Format("{message_text}"),
ScrollingGroup(
@@ -148,14 +137,14 @@ groups_dialog = Dialog(
Button(Const("🗑 Удалить группу"), id="delete", on_click=on_delete_group),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_menu),
),
- state=CreatorGroupsSG.groups_list,
+ state=SharedGroupsSG.groups_list,
getter=get_groups_data,
),
Window(
Const("➕ Добавление группы\n\n🔢 Введите номер группы (четырехзначное число 1000-9999):"),
MessageInput(on_group_number_input),
Button(Const("◀️ Отмена"), id="cancel", on_click=on_cancel_add),
- state=CreatorGroupsSG.add_group_input_number,
+ state=SharedGroupsSG.add_group_input_number,
),
Window(
Format("🗑 Удаление группы\n\nВыберите группу для удаления:\n\n📊 Всего групп: {groups_count}"),
@@ -172,7 +161,7 @@ groups_dialog = Dialog(
height=7,
),
Button(Const("◀️ Назад"), id="back", on_click=on_cancel_add),
- state=CreatorGroupsSG.delete_groups_list,
+ state=SharedGroupsSG.delete_groups_list,
getter=get_delete_groups_data,
),
Window(
@@ -181,7 +170,7 @@ groups_dialog = Dialog(
Button(Const("✅ Да, удалить"), id="confirm", on_click=on_confirm_delete),
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_delete),
),
- state=CreatorGroupsSG.delete_confirm,
+ state=SharedGroupsSG.delete_confirm,
getter=get_delete_confirm_data,
),
)
diff --git a/src/trudex/application/bot/shared_dialogs/states.py b/src/trudex/application/bot/shared_dialogs/states.py
new file mode 100644
index 0000000..3f6255c
--- /dev/null
+++ b/src/trudex/application/bot/shared_dialogs/states.py
@@ -0,0 +1,51 @@
+from aiogram.fsm.state import State, StatesGroup
+
+
+class SharedTemplatesSG(StatesGroup):
+ main = State()
+ export_list = State()
+ spec = State()
+ import_file = State()
+
+
+class SharedTestsSG(StatesGroup):
+ tests_list = State()
+ test_detail = State()
+ share_test = State()
+ edit_menu = State()
+ edit_password = State()
+ edit_attempts = State()
+ edit_group = State()
+ edit_expires = State()
+ statistics = State()
+ attempt_detail = State()
+
+
+class SharedBroadcastSG(StatesGroup):
+ broadcast_input = State()
+ broadcast_confirm = State()
+
+
+class SharedGroupsSG(StatesGroup):
+ groups_list = State()
+ add_group_input_number = State()
+ delete_groups_list = State()
+ delete_confirm = State()
+
+
+class SharedCreateTestSG(StatesGroup):
+ input_title = State()
+ input_description = State()
+ input_password = State()
+ input_attempts = State()
+ input_expires_at = State()
+ input_for_group = State()
+ confirm_test_info = State()
+ add_question = State()
+ input_question_text = State()
+ select_question_type = State()
+ input_correct_answer = State()
+ input_options = State()
+ mark_correct_options = State()
+ confirm_question = State()
+ test_created = State()
diff --git a/src/trudex/application/bot/admin_dialogs/templates.py b/src/trudex/application/bot/shared_dialogs/templates.py
similarity index 95%
rename from src/trudex/application/bot/admin_dialogs/templates.py
rename to src/trudex/application/bot/shared_dialogs/templates.py
index 80329aa..dcc9599 100644
--- a/src/trudex/application/bot/admin_dialogs/templates.py
+++ b/src/trudex/application/bot/shared_dialogs/templates.py
@@ -9,7 +9,7 @@ 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 AdminMenuSG, AdminTemplatesSG, AdminTestsSG
+from trudex.application.bot.shared_dialogs.states import SharedTemplatesSG, SharedTestsSG
from trudex.domain.schemas import QuestionType
from trudex.domain.test_parser import ParsedTest, TestParser
from trudex.infrastructure.database.dao.option import OptionDAO
@@ -17,6 +17,7 @@ from trudex.infrastructure.database.dao.question import QuestionDAO
from trudex.infrastructure.database.dao.test import TestDAO
from trudex.infrastructure.database.repo.test import TestRepository
+
TEMPLATES_INFO = (
"📦 Шаблоны тестов\n\n"
"Шаблоны позволяют экспортировать и импортировать тесты в формате JSON.\n\n"
@@ -73,6 +74,7 @@ SPEC_INFO = """📋 Спецификация формата JSON
• Для multiple — один или более is_correct: true
• Минимум 2 варианта ответа для single/multiple"""
+
TEMPLATE_SINGLE = {
"title": "Пример теста с одиночным выбором",
"description": "Демонстрация формата single вопросов",
@@ -166,23 +168,23 @@ TEMPLATE_FULL = {
async def on_export_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(AdminTemplatesSG.export_list)
+ await manager.switch_to(SharedTemplatesSG.export_list)
async def on_import_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(AdminTemplatesSG.import_file)
+ await manager.switch_to(SharedTemplatesSG.import_file)
async def on_spec_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(AdminTemplatesSG.spec)
+ await manager.switch_to(SharedTemplatesSG.spec)
async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
+ await manager.done()
async def on_back_to_templates(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
- await manager.switch_to(AdminTemplatesSG.main)
+ await manager.switch_to(SharedTemplatesSG.main)
@inject
@@ -224,7 +226,7 @@ async def on_test_selected_for_export(
for question, options in questions_with_options:
question_data: dict = {
"text": question.text,
- "question_type": question.question_type,
+ "question_type": question.question_type.value,
}
if question.question_type == QuestionType.INPUT:
@@ -371,10 +373,10 @@ async def on_import_file(
f"Тест создан в деактивированном состоянии."
)
- await manager.start(AdminTestsSG.tests_list, mode=StartMode.RESET_STACK)
+ await manager.start(SharedTestsSG.tests_list, mode=StartMode.RESET_STACK)
-templates_dialog = Dialog(
+shared_templates_dialog = Dialog(
Window(
Const(TEMPLATES_INFO),
Row(
@@ -383,7 +385,7 @@ templates_dialog = Dialog(
),
Button(Const("📋 Спецификация"), id="spec", on_click=on_spec_clicked),
Button(Const("◀️ Назад"), id="back", on_click=on_back_clicked),
- state=AdminTemplatesSG.main,
+ state=SharedTemplatesSG.main,
),
Window(
Format("📤 Экспорт теста\n\nВыберите тест для экспорта:\n\nВсего: {count}"),
@@ -400,7 +402,7 @@ templates_dialog = Dialog(
height=7,
),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_templates),
- state=AdminTemplatesSG.export_list,
+ state=SharedTemplatesSG.export_list,
getter=get_tests_for_export,
),
Window(
@@ -414,12 +416,12 @@ templates_dialog = Dialog(
Button(Const("📦 Полный"), id="tpl_full", on_click=on_template_full),
),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_templates),
- state=AdminTemplatesSG.spec,
+ state=SharedTemplatesSG.spec,
),
Window(
Const("📥 Импорт теста\n\nОтправьте JSON файл с тестом.\n\nФормат файла описан в разделе «Спецификация»"),
MessageInput(on_import_file, content_types=[ContentType.DOCUMENT]),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_templates),
- state=AdminTemplatesSG.import_file,
+ state=SharedTemplatesSG.import_file,
),
)
diff --git a/src/trudex/application/bot/admin_dialogs/tests.py b/src/trudex/application/bot/shared_dialogs/tests.py
similarity index 91%
rename from src/trudex/application/bot/admin_dialogs/tests.py
rename to src/trudex/application/bot/shared_dialogs/tests.py
index 6cd66ab..e97f320 100644
--- a/src/trudex/application/bot/admin_dialogs/tests.py
+++ b/src/trudex/application/bot/shared_dialogs/tests.py
@@ -11,7 +11,7 @@ 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 AdminCreateTestSG, AdminMenuSG, AdminTestsSG
+from trudex.application.bot.shared_dialogs.states import SharedCreateTestSG, SharedTestsSG
from trudex.infrastructure.database.dao.group import GroupDAO
from trudex.infrastructure.database.dao.test import TestDAO
from trudex.infrastructure.database.repo.test import TestRepository
@@ -37,7 +37,7 @@ 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 manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
@inject
@@ -108,7 +108,7 @@ async def on_toggle_active(_callback: CallbackQuery, _button: Button, manager: D
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)
+ await manager.switch_to(SharedTestsSG.test_detail)
@inject
@@ -124,15 +124,15 @@ async def on_toggle_results_viewable(_callback: CallbackQuery, _button: Button,
await test_dao.update(test_id, are_results_viewable=not test.are_results_viewable)
action = "скрыты" if test.are_results_viewable else "видны"
await _callback.answer(f"✅ Результаты теперь {action}")
- await manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminTestsSG.tests_list)
+ await manager.switch_to(SharedTestsSG.tests_list)
async def on_statistics(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminTestsSG.statistics)
+ await manager.switch_to(SharedTestsSG.statistics)
@inject
@@ -163,11 +163,11 @@ async def get_statistics_data(
async def on_attempt_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str):
manager.dialog_data["selected_attempt_id"] = int(item_id)
- await manager.switch_to(AdminTestsSG.attempt_detail)
+ await manager.switch_to(SharedTestsSG.attempt_detail)
async def on_back_to_statistics(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminTestsSG.statistics)
+ await manager.switch_to(SharedTestsSG.statistics)
@inject
@@ -192,7 +192,7 @@ async def get_attempt_detail(
date_str = finished_at_msk.strftime("%d.%m.%Y %H:%M") if finished_at_msk else "—"
lines = [
- f"📊 Результат прохождения\n",
+ "📊 Результат прохождения\n",
f"📈 Результат: {attempt.score}%",
f"📅 Дата: {date_str}",
f"🏆 Статус: {status}\n",
@@ -226,8 +226,9 @@ async def on_share_test(_callback: CallbackQuery, _button: Button, manager: Dial
test_id = manager.dialog_data.get("selected_test_id")
if not test_id:
- await _callback.answer("Ошибка: тест не найден")
- return
+ return {
+ "share_link": "Ошибка: тест не найден"
+ }
test_hash = encode_id(
test_id,
@@ -254,31 +255,31 @@ async def on_share_test(_callback: CallbackQuery, _button: Button, manager: Dial
async def on_edit_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminTestsSG.edit_menu)
+ await manager.switch_to(SharedTestsSG.edit_menu)
async def on_back_to_detail(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
async def on_back_to_edit_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminTestsSG.edit_menu)
+ await manager.switch_to(SharedTestsSG.edit_menu)
async def on_edit_password(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminTestsSG.edit_password)
+ await manager.switch_to(SharedTestsSG.edit_password)
async def on_edit_attempts(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminTestsSG.edit_attempts)
+ await manager.switch_to(SharedTestsSG.edit_attempts)
async def on_edit_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminTestsSG.edit_group)
+ await manager.switch_to(SharedTestsSG.edit_group)
async def on_edit_expires(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.switch_to(AdminTestsSG.edit_expires)
+ await manager.switch_to(SharedTestsSG.edit_expires)
@inject
@@ -299,7 +300,7 @@ async def on_password_input(message: Message, _widget: MessageInput, manager: Di
await test_dao.update(test_id, password=password)
await message.answer("✅ Пароль обновлен")
- await manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
@inject
@@ -311,7 +312,7 @@ async def on_remove_password(_callback: CallbackQuery, _button: Button, manager:
await test_dao.update(test_id, password=None)
await _callback.answer("✅ Пароль удален")
- await manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
@inject
@@ -343,7 +344,7 @@ async def on_attempts_input_edit(message: Message, _widget: MessageInput, manage
await test_dao.update(test_id, attempts=attempts)
await message.answer("✅ Количество попыток обновлено")
- await manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
@inject
@@ -355,7 +356,7 @@ async def on_remove_attempts(_callback: CallbackQuery, _button: Button, manager:
await test_dao.update(test_id, attempts=None)
await _callback.answer("✅ Ограничение попыток удалено")
- await manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
@inject
@@ -376,7 +377,7 @@ async def on_group_selected_for_test(_callback: CallbackQuery, _widget, manager:
await test_dao.update(test_id, for_group=int(item_id))
await _callback.answer("✅ Группа обновлена")
- await manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
@inject
@@ -388,7 +389,7 @@ async def on_remove_group(_callback: CallbackQuery, _button: Button, manager: Di
await test_dao.update(test_id, for_group=None)
await _callback.answer("✅ Тест теперь доступен для всех групп")
- await manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
@inject
@@ -401,7 +402,7 @@ async def on_date_selected_for_test(_callback, _widget, manager: DialogManager,
expires_at = datetime.combine(selected_date, time.min)
await test_dao.update(test_id, expires_at=expires_at)
await _callback.answer("✅ Срок действия обновлен")
- await manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
@inject
@@ -413,18 +414,18 @@ async def on_remove_expires(_callback: CallbackQuery, _button: Button, manager:
await test_dao.update(test_id, expires_at=None)
await _callback.answer("✅ Срок действия удален")
- await manager.switch_to(AdminTestsSG.test_detail)
+ await manager.switch_to(SharedTestsSG.test_detail)
async def on_add_test_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.start(AdminCreateTestSG.input_title, mode=StartMode.RESET_STACK)
+ await manager.start(SharedCreateTestSG.input_title, mode=StartMode.RESET_STACK)
async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager):
- await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
+ await manager.done()
-tests_dialog = Dialog(
+shared_tests_dialog = Dialog(
Window(
Format("📝 Тесты\n\nВсего: {count}"),
ScrollingGroup(
@@ -443,7 +444,7 @@ tests_dialog = Dialog(
Button(Const("➕ Добавить тест"), id="add_test", on_click=on_add_test_clicked),
Button(Const("◀️ Назад"), id="back", on_click=on_back_clicked),
),
- state=AdminTestsSG.tests_list,
+ state=SharedTestsSG.tests_list,
getter=get_tests_data,
),
Window(
@@ -464,7 +465,7 @@ tests_dialog = Dialog(
Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_list),
),
- state=AdminTestsSG.test_detail,
+ state=SharedTestsSG.test_detail,
getter=get_test_detail,
),
Window(
@@ -476,7 +477,7 @@ tests_dialog = Dialog(
Button(Const("📅 Срок действия"), id="edit_expires", on_click=on_edit_expires),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_detail),
),
- state=AdminTestsSG.edit_menu,
+ state=SharedTestsSG.edit_menu,
),
Window(
Const("🔑 Изменение пароля\n\n💬 Введите новый пароль или удалите текущий:\n(максимум 255 символов)"),
@@ -485,7 +486,7 @@ tests_dialog = Dialog(
Button(Const("🗑 Удалить пароль"), id="remove_password", on_click=on_remove_password),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_edit_menu),
),
- state=AdminTestsSG.edit_password,
+ state=SharedTestsSG.edit_password,
),
Window(
Const("🔄 Изменение количества попыток\n\n🔢 Введите новое количество попыток (1-100) или удалите ограничение:"),
@@ -494,7 +495,7 @@ tests_dialog = Dialog(
Button(Const("🗑 Без ограничений"), id="remove_attempts", on_click=on_remove_attempts),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_edit_menu),
),
- state=AdminTestsSG.edit_attempts,
+ state=SharedTestsSG.edit_attempts,
),
Window(
Const("👥 Изменение группы\n\n🎓 Выберите группу или удалите привязку:"),
@@ -514,7 +515,7 @@ tests_dialog = Dialog(
Button(Const("🗑 Для всех групп"), id="remove_group", on_click=on_remove_group),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_edit_menu),
),
- state=AdminTestsSG.edit_group,
+ state=SharedTestsSG.edit_group,
getter=get_groups_for_edit,
),
Window(
@@ -524,7 +525,7 @@ tests_dialog = Dialog(
Button(Const("🗑 Удалить срок"), id="remove_expires", on_click=on_remove_expires),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_edit_menu),
),
- state=AdminTestsSG.edit_expires,
+ state=SharedTestsSG.edit_expires,
),
Window(
Format("📊 Статистика теста\n\nПрошли тест: {count}"),
@@ -544,13 +545,13 @@ tests_dialog = Dialog(
Button(Const("🔄 Обновить"), id="refresh", on_click=on_statistics),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_detail),
),
- state=AdminTestsSG.statistics,
+ state=SharedTestsSG.statistics,
getter=get_statistics_data,
),
Window(
Format("{attempt_info}"),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_statistics),
- state=AdminTestsSG.attempt_detail,
+ state=SharedTestsSG.attempt_detail,
getter=get_attempt_detail,
),
)