This commit is contained in:
2026-01-04 02:07:07 +03:00
parent 5d0e99f875
commit 0ebf915ec4
4 changed files with 366 additions and 6 deletions
@@ -8,6 +8,7 @@ class AdminMenuSG(StatesGroup):
class AdminTemplatesSG(StatesGroup): class AdminTemplatesSG(StatesGroup):
main = State() main = State()
export_list = State() export_list = State()
spec = State()
class AdminUsersSG(StatesGroup): class AdminUsersSG(StatesGroup):
@@ -20,6 +20,145 @@ TEMPLATES_INFO = (
"🔹 <b>Спецификация</b> — описание формата JSON для создания тестов вручную" "🔹 <b>Спецификация</b> — описание формата JSON для создания тестов вручную"
) )
SPEC_INFO = """<b>📋 Спецификация формата JSON</b>
<b>Структура файла:</b>
<code>{
"title": "Название теста",
"description": "Описание теста",
"password": null,
"attempts": null,
"expires_at": null,
"for_group": null,
"questions": [...]
}</code>
<b>Поля теста:</b>
• <code>title</code> — название (обязательно, до 255 символов)
• <code>description</code> — описание (до 2000 символов)
• <code>password</code> — пароль для доступа или <code>null</code>
• <code>attempts</code> — лимит попыток (1-100) или <code>null</code>
• <code>expires_at</code> — срок действия в ISO формате или <code>null</code>
• <code>for_group</code> — номер группы или <code>null</code> для всех
<b>Типы вопросов:</b>
• <code>single</code> — один правильный ответ
• <code>multiple</code> — несколько правильных ответов
• <code>input</code> — ввод текста (регистр и пробелы игнорируются)
<b>Формат вопроса (single/multiple):</b>
<code>{
"text": "Текст вопроса",
"question_type": "single",
"options": [
{"text": "Вариант 1", "is_correct": true},
{"text": "Вариант 2", "is_correct": false}
]
}</code>
<b>Формат вопроса (input):</b>
<code>{
"text": "Текст вопроса",
"question_type": "input",
"correct_answer": "правильный ответ"
}</code>
<b>⚠️ Важно:</b>
• Для <code>single</code> — ровно один <code>is_correct: true</code>
• Для <code>multiple</code> — один или более <code>is_correct: true</code>
• Минимум 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: async def on_export_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
await manager.switch_to(AdminTemplatesSG.export_list) await manager.switch_to(AdminTemplatesSG.export_list)
@@ -29,8 +168,8 @@ async def on_import_clicked(_callback: CallbackQuery, _button: Button, _manager:
await _callback.answer("🚧 В разработке", show_alert=True) await _callback.answer("🚧 В разработке", show_alert=True)
async def on_spec_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None: async def on_spec_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
await _callback.answer("🚧 В разработке", show_alert=True) await manager.switch_to(AdminTemplatesSG.spec)
async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
@@ -107,6 +246,33 @@ async def on_test_selected_for_export(
) )
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"📄 <b>Шаблон:</b> {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")
templates_dialog = Dialog( templates_dialog = Dialog(
Window( Window(
Const(TEMPLATES_INFO), Const(TEMPLATES_INFO),
@@ -136,4 +302,17 @@ templates_dialog = Dialog(
state=AdminTemplatesSG.export_list, state=AdminTemplatesSG.export_list,
getter=get_tests_for_export, 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=AdminTemplatesSG.spec,
),
) )
@@ -8,6 +8,7 @@ class CreatorMenuSG(StatesGroup):
class CreatorTemplatesSG(StatesGroup): class CreatorTemplatesSG(StatesGroup):
main = State() main = State()
export_list = State() export_list = State()
spec = State()
class CreatorUsersSG(StatesGroup): class CreatorUsersSG(StatesGroup):
@@ -20,6 +20,145 @@ TEMPLATES_INFO = (
"🔹 <b>Спецификация</b> — описание формата JSON для создания тестов вручную" "🔹 <b>Спецификация</b> — описание формата JSON для создания тестов вручную"
) )
SPEC_INFO = """<b>📋 Спецификация формата JSON</b>
<b>Структура файла:</b>
<code>{
"title": "Название теста",
"description": "Описание теста",
"password": null,
"attempts": null,
"expires_at": null,
"for_group": null,
"questions": [...]
}</code>
<b>Поля теста:</b>
• <code>title</code> — название (обязательно, до 255 символов)
• <code>description</code> — описание (до 2000 символов)
• <code>password</code> — пароль для доступа или <code>null</code>
• <code>attempts</code> — лимит попыток (1-100) или <code>null</code>
• <code>expires_at</code> — срок действия в ISO формате или <code>null</code>
• <code>for_group</code> — номер группы или <code>null</code> для всех
<b>Типы вопросов:</b>
• <code>single</code> — один правильный ответ
• <code>multiple</code> — несколько правильных ответов
• <code>input</code> — ввод текста (регистр и пробелы игнорируются)
<b>Формат вопроса (single/multiple):</b>
<code>{
"text": "Текст вопроса",
"question_type": "single",
"options": [
{"text": "Вариант 1", "is_correct": true},
{"text": "Вариант 2", "is_correct": false}
]
}</code>
<b>Формат вопроса (input):</b>
<code>{
"text": "Текст вопроса",
"question_type": "input",
"correct_answer": "правильный ответ"
}</code>
<b>⚠️ Важно:</b>
• Для <code>single</code> — ровно один <code>is_correct: true</code>
• Для <code>multiple</code> — один или более <code>is_correct: true</code>
• Минимум 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: async def on_export_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
await manager.switch_to(CreatorTemplatesSG.export_list) await manager.switch_to(CreatorTemplatesSG.export_list)
@@ -29,8 +168,8 @@ async def on_import_clicked(_callback: CallbackQuery, _button: Button, _manager:
await _callback.answer("🚧 В разработке", show_alert=True) await _callback.answer("🚧 В разработке", show_alert=True)
async def on_spec_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None: async def on_spec_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
await _callback.answer("🚧 В разработке", show_alert=True) await manager.switch_to(CreatorTemplatesSG.spec)
async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
@@ -107,6 +246,33 @@ async def on_test_selected_for_export(
) )
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"📄 <b>Шаблон:</b> {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")
templates_dialog = Dialog( templates_dialog = Dialog(
Window( Window(
Const(TEMPLATES_INFO), Const(TEMPLATES_INFO),
@@ -136,4 +302,17 @@ templates_dialog = Dialog(
state=CreatorTemplatesSG.export_list, state=CreatorTemplatesSG.export_list,
getter=get_tests_for_export, 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,
),
) )