This commit is contained in:
2026-01-04 01:56:49 +03:00
parent 1a8da5c070
commit 26f5ecd918
7 changed files with 302 additions and 0 deletions
+6
View File
@@ -14,6 +14,8 @@ from trudex.application.bot.admin_dialogs.broadcast import \
from trudex.application.bot.admin_dialogs.groups import \ from trudex.application.bot.admin_dialogs.groups import \
groups_dialog as admin_groups_dialog groups_dialog as admin_groups_dialog
from trudex.application.bot.admin_dialogs.main_menu import admin_menu_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 \ from trudex.application.bot.admin_dialogs.tests import \
tests_dialog as admin_tests_dialog tests_dialog as admin_tests_dialog
from trudex.application.bot.admin_dialogs.users import \ 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 groups_dialog as creator_groups_dialog
from trudex.application.bot.creator_dialogs.main_menu import \ from trudex.application.bot.creator_dialogs.main_menu import \
creator_menu_dialog 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 \ from trudex.application.bot.creator_dialogs.tests import \
tests_dialog as creator_tests_dialog tests_dialog as creator_tests_dialog
from trudex.application.bot.creator_dialogs.users import \ from trudex.application.bot.creator_dialogs.users import \
@@ -72,11 +76,13 @@ async def main() -> None:
admin_tests_dialog, admin_tests_dialog,
admin_groups_dialog, admin_groups_dialog,
admin_broadcast_dialog, admin_broadcast_dialog,
admin_templates_dialog,
creator_menu_dialog, creator_menu_dialog,
creator_users_dialog, creator_users_dialog,
creator_tests_dialog, creator_tests_dialog,
creator_groups_dialog, creator_groups_dialog,
creator_broadcast_dialog, creator_broadcast_dialog,
creator_templates_dialog,
create_test_dialog, create_test_dialog,
) )
@@ -6,6 +6,7 @@ from aiogram_dialog.widgets.text import Const
from trudex.application.bot.admin_dialogs.states import (AdminBroadcastSG, from trudex.application.bot.admin_dialogs.states import (AdminBroadcastSG,
AdminGroupsSG, AdminGroupsSG,
AdminMenuSG, AdminMenuSG,
AdminTemplatesSG,
AdminTestsSG, AdminTestsSG,
AdminUsersSG) 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) 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( admin_menu_dialog = Dialog(
Window( Window(
Const("🔧 <b>Админ-панель</b>\n\nВыберите раздел:"), Const("🔧 <b>Админ-панель</b>\n\nВыберите раздел:"),
@@ -34,6 +39,7 @@ admin_menu_dialog = Dialog(
Button(Const("👥 Пользователи"), id="users", on_click=on_users_clicked), Button(Const("👥 Пользователи"), id="users", on_click=on_users_clicked),
Button(Const("🎓 Группы"), id="groups", on_click=on_groups_clicked), Button(Const("🎓 Группы"), id="groups", on_click=on_groups_clicked),
Button(Const("📢 Рассылка"), id="broadcast", on_click=on_broadcast_clicked), Button(Const("📢 Рассылка"), id="broadcast", on_click=on_broadcast_clicked),
Button(Const("📦 Шаблоны тестов"), id="templates", on_click=on_templates_clicked),
), ),
state=AdminMenuSG.main, state=AdminMenuSG.main,
), ),
@@ -5,6 +5,11 @@ class AdminMenuSG(StatesGroup):
main = State() main = State()
class AdminTemplatesSG(StatesGroup):
main = State()
export_list = State()
class AdminUsersSG(StatesGroup): class AdminUsersSG(StatesGroup):
users_list = State() users_list = State()
users_input = State() users_input = State()
@@ -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 = (
"<b>📦 Шаблоны тестов</b>\n\n"
"Шаблоны позволяют экспортировать и импортировать тесты в формате JSON.\n\n"
"🔹 <b>Экспорт</b> — сохраните тест как файл для резервной копии или передачи\n"
"🔹 <b>Импорт</b> — загрузите тест из файла\n"
"🔹 <b>Спецификация</b> — описание формата 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"📤 <b>Экспорт теста</b>\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("<b>📤 Экспорт теста</b>\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,
),
)
@@ -6,6 +6,7 @@ from aiogram_dialog.widgets.text import Const
from trudex.application.bot.creator_dialogs.states import (CreatorBroadcastSG, from trudex.application.bot.creator_dialogs.states import (CreatorBroadcastSG,
CreatorGroupsSG, CreatorGroupsSG,
CreatorMenuSG, CreatorMenuSG,
CreatorTemplatesSG,
CreatorTestsSG, CreatorTestsSG,
CreatorUsersSG) 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) 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( creator_menu_dialog = Dialog(
Window( Window(
Const("👑 <b>Панель создателя</b>\n\nВыберите раздел:"), Const("👑 <b>Панель создателя</b>\n\nВыберите раздел:"),
@@ -34,6 +39,7 @@ creator_menu_dialog = Dialog(
Button(Const("👥 Пользователи"), id="users", on_click=on_users_clicked), Button(Const("👥 Пользователи"), id="users", on_click=on_users_clicked),
Button(Const("🎓 Группы"), id="groups", on_click=on_groups_clicked), Button(Const("🎓 Группы"), id="groups", on_click=on_groups_clicked),
Button(Const("📢 Рассылка"), id="broadcast", on_click=on_broadcast_clicked), Button(Const("📢 Рассылка"), id="broadcast", on_click=on_broadcast_clicked),
Button(Const("📦 Шаблоны тестов"), id="templates", on_click=on_templates_clicked),
), ),
state=CreatorMenuSG.main, state=CreatorMenuSG.main,
), ),
@@ -5,6 +5,11 @@ class CreatorMenuSG(StatesGroup):
main = State() main = State()
class CreatorTemplatesSG(StatesGroup):
main = State()
export_list = State()
class CreatorUsersSG(StatesGroup): class CreatorUsersSG(StatesGroup):
users_list = State() users_list = State()
users_input = State() users_input = State()
@@ -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 = (
"<b>📦 Шаблоны тестов</b>\n\n"
"Шаблоны позволяют экспортировать и импортировать тесты в формате JSON.\n\n"
"🔹 <b>Экспорт</b> — сохраните тест как файл для резервной копии или передачи\n"
"🔹 <b>Импорт</b> — загрузите тест из файла\n"
"🔹 <b>Спецификация</b> — описание формата 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"📤 <b>Экспорт теста</b>\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("<b>📤 Экспорт теста</b>\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,
),
)