mirror of
https://github.com/koloideal/Quizzi.git
synced 2026-06-10 18:35:28 +03:00
commit
This commit is contained in:
@@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user