From 26f5ecd9189896c0b34c19711fed92b8a798dee8 Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 4 Jan 2026 01:56:49 +0300 Subject: [PATCH] commit --- src/trudex/application/__main__.py | 6 + .../bot/admin_dialogs/main_menu.py | 6 + .../application/bot/admin_dialogs/states.py | 5 + .../bot/admin_dialogs/templates.py | 138 ++++++++++++++++++ .../bot/creator_dialogs/main_menu.py | 6 + .../application/bot/creator_dialogs/states.py | 5 + .../bot/creator_dialogs/templates.py | 136 +++++++++++++++++ 7 files changed, 302 insertions(+) create mode 100644 src/trudex/application/bot/admin_dialogs/templates.py create mode 100644 src/trudex/application/bot/creator_dialogs/templates.py diff --git a/src/trudex/application/__main__.py b/src/trudex/application/__main__.py index 727f465..6240db8 100644 --- a/src/trudex/application/__main__.py +++ b/src/trudex/application/__main__.py @@ -14,6 +14,8 @@ from trudex.application.bot.admin_dialogs.broadcast import \ 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 \ @@ -26,6 +28,8 @@ from trudex.application.bot.creator_dialogs.groups import \ groups_dialog as creator_groups_dialog from trudex.application.bot.creator_dialogs.main_menu import \ creator_menu_dialog +from trudex.application.bot.creator_dialogs.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 \ @@ -72,11 +76,13 @@ async def main() -> None: admin_tests_dialog, admin_groups_dialog, admin_broadcast_dialog, + admin_templates_dialog, creator_menu_dialog, creator_users_dialog, creator_tests_dialog, creator_groups_dialog, creator_broadcast_dialog, + creator_templates_dialog, create_test_dialog, ) diff --git a/src/trudex/application/bot/admin_dialogs/main_menu.py b/src/trudex/application/bot/admin_dialogs/main_menu.py index 9f0c52f..33d9d36 100644 --- a/src/trudex/application/bot/admin_dialogs/main_menu.py +++ b/src/trudex/application/bot/admin_dialogs/main_menu.py @@ -6,6 +6,7 @@ from aiogram_dialog.widgets.text import Const from trudex.application.bot.admin_dialogs.states import (AdminBroadcastSG, AdminGroupsSG, AdminMenuSG, + AdminTemplatesSG, AdminTestsSG, AdminUsersSG) @@ -26,6 +27,10 @@ async def on_broadcast_clicked(_callback: CallbackQuery, _button: Button, manage await manager.start(AdminBroadcastSG.broadcast_input, mode=StartMode.RESET_STACK) +async def on_templates_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: + await manager.start(AdminTemplatesSG.main, mode=StartMode.RESET_STACK) + + admin_menu_dialog = Dialog( Window( Const("🔧 Админ-панель\n\nВыберите раздел:"), @@ -34,6 +39,7 @@ admin_menu_dialog = Dialog( Button(Const("👥 Пользователи"), id="users", on_click=on_users_clicked), Button(Const("🎓 Группы"), id="groups", on_click=on_groups_clicked), Button(Const("📢 Рассылка"), id="broadcast", on_click=on_broadcast_clicked), + Button(Const("📦 Шаблоны тестов"), id="templates", on_click=on_templates_clicked), ), state=AdminMenuSG.main, ), diff --git a/src/trudex/application/bot/admin_dialogs/states.py b/src/trudex/application/bot/admin_dialogs/states.py index 44f013e..28720a3 100644 --- a/src/trudex/application/bot/admin_dialogs/states.py +++ b/src/trudex/application/bot/admin_dialogs/states.py @@ -5,6 +5,11 @@ class AdminMenuSG(StatesGroup): main = State() +class AdminTemplatesSG(StatesGroup): + main = State() + export_list = State() + + class AdminUsersSG(StatesGroup): users_list = State() users_input = State() diff --git a/src/trudex/application/bot/admin_dialogs/templates.py b/src/trudex/application/bot/admin_dialogs/templates.py new file mode 100644 index 0000000..976488c --- /dev/null +++ b/src/trudex/application/bot/admin_dialogs/templates.py @@ -0,0 +1,138 @@ +import json + +from aiogram.types import BufferedInputFile, CallbackQuery +from aiogram_dialog import Dialog, DialogManager, StartMode, Window +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.admin_dialogs.states import AdminMenuSG, AdminTemplatesSG +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 для создания тестов вручную" +) + + +async def on_export_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: + await manager.switch_to(AdminTemplatesSG.export_list) + + +async def on_import_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None: + await _callback.answer("🚧 В разработке", show_alert=True) + + +async def on_spec_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None: + await _callback.answer("🚧 В разработке", show_alert=True) + + +async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: + await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK) + + +async def on_back_to_templates(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: + await manager.switch_to(AdminTemplatesSG.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, + _manager: DialogManager, + item_id: str, + test_repo: FromDishka[TestRepository], +): + 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, + "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 == "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"📤 Экспорт теста\n\n📝 {test.title}", + ) + + +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=AdminTemplatesSG.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, + ), + id="tests_scroll", + width=1, + height=7, + ), + Button(Const("◀️ Назад"), id="back", on_click=on_back_to_templates), + state=AdminTemplatesSG.export_list, + getter=get_tests_for_export, + ), +) diff --git a/src/trudex/application/bot/creator_dialogs/main_menu.py b/src/trudex/application/bot/creator_dialogs/main_menu.py index 3d63ee0..a37ebbf 100644 --- a/src/trudex/application/bot/creator_dialogs/main_menu.py +++ b/src/trudex/application/bot/creator_dialogs/main_menu.py @@ -6,6 +6,7 @@ from aiogram_dialog.widgets.text import Const from trudex.application.bot.creator_dialogs.states import (CreatorBroadcastSG, CreatorGroupsSG, CreatorMenuSG, + CreatorTemplatesSG, CreatorTestsSG, CreatorUsersSG) @@ -26,6 +27,10 @@ async def on_broadcast_clicked(_callback: CallbackQuery, _button: Button, manage await manager.start(CreatorBroadcastSG.broadcast_input, mode=StartMode.RESET_STACK) +async def on_templates_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: + await manager.start(CreatorTemplatesSG.main, mode=StartMode.RESET_STACK) + + creator_menu_dialog = Dialog( Window( Const("👑 Панель создателя\n\nВыберите раздел:"), @@ -34,6 +39,7 @@ creator_menu_dialog = Dialog( Button(Const("👥 Пользователи"), id="users", on_click=on_users_clicked), Button(Const("🎓 Группы"), id="groups", on_click=on_groups_clicked), Button(Const("📢 Рассылка"), id="broadcast", on_click=on_broadcast_clicked), + Button(Const("📦 Шаблоны тестов"), id="templates", on_click=on_templates_clicked), ), state=CreatorMenuSG.main, ), diff --git a/src/trudex/application/bot/creator_dialogs/states.py b/src/trudex/application/bot/creator_dialogs/states.py index 2b0909f..02f22d4 100644 --- a/src/trudex/application/bot/creator_dialogs/states.py +++ b/src/trudex/application/bot/creator_dialogs/states.py @@ -5,6 +5,11 @@ class CreatorMenuSG(StatesGroup): main = State() +class CreatorTemplatesSG(StatesGroup): + main = State() + export_list = State() + + class CreatorUsersSG(StatesGroup): users_list = State() users_input = State() diff --git a/src/trudex/application/bot/creator_dialogs/templates.py b/src/trudex/application/bot/creator_dialogs/templates.py new file mode 100644 index 0000000..28b88a4 --- /dev/null +++ b/src/trudex/application/bot/creator_dialogs/templates.py @@ -0,0 +1,136 @@ +import json + +from aiogram.types import BufferedInputFile, CallbackQuery +from aiogram_dialog import Dialog, DialogManager, StartMode, Window +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 +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 для создания тестов вручную" +) + + +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 _callback.answer("🚧 В разработке", show_alert=True) + + +async def on_spec_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None: + await _callback.answer("🚧 В разработке", show_alert=True) + + +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, + _manager: DialogManager, + item_id: str, + test_repo: FromDishka[TestRepository], +): + 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 = { + "title": test.title, + "description": test.description, + "password": test.password, + "attempts": test.attempts, + "for_group": test.for_group, + "questions": [], + } + + for question, options in questions_with_options: + question_data = { + "text": question.text, + "question_type": question.question_type, + } + + if question.question_type == "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 + ] + + export_data["questions"].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"📤 Экспорт теста\n\n📝 {test.title}", + ) + + +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, + ), + 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, + ), +)