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,
+ ),
+)