diff --git a/src/trudex/application/bot/admin_dialogs/states.py b/src/trudex/application/bot/admin_dialogs/states.py index 28720a3..d9b1e88 100644 --- a/src/trudex/application/bot/admin_dialogs/states.py +++ b/src/trudex/application/bot/admin_dialogs/states.py @@ -8,6 +8,7 @@ class AdminMenuSG(StatesGroup): class AdminTemplatesSG(StatesGroup): main = State() export_list = State() + spec = State() class AdminUsersSG(StatesGroup): diff --git a/src/trudex/application/bot/admin_dialogs/templates.py b/src/trudex/application/bot/admin_dialogs/templates.py index ea9f0df..e861db4 100644 --- a/src/trudex/application/bot/admin_dialogs/templates.py +++ b/src/trudex/application/bot/admin_dialogs/templates.py @@ -20,6 +20,145 @@ TEMPLATES_INFO = ( "🔹 Спецификация — описание формата JSON для создания тестов вручную" ) +SPEC_INFO = """📋 Спецификация формата JSON + +Структура файла: +{ + "title": "Название теста", + "description": "Описание теста", + "password": null, + "attempts": null, + "expires_at": null, + "for_group": null, + "questions": [...] +} + +Поля теста: +• title — название (обязательно, до 255 символов) +• description — описание (до 2000 символов) +• password — пароль для доступа или null +• attempts — лимит попыток (1-100) или null +• expires_at — срок действия в ISO формате или null +• for_group — номер группы или null для всех + +Типы вопросов: +• single — один правильный ответ +• multiple — несколько правильных ответов +• input — ввод текста (регистр и пробелы игнорируются) + +Формат вопроса (single/multiple): +{ + "text": "Текст вопроса", + "question_type": "single", + "options": [ + {"text": "Вариант 1", "is_correct": true}, + {"text": "Вариант 2", "is_correct": false} + ] +} + +Формат вопроса (input): +{ + "text": "Текст вопроса", + "question_type": "input", + "correct_answer": "правильный ответ" +} + +⚠️ Важно: +• Для single — ровно один is_correct: true +• Для multiple — один или более is_correct: true +• Минимум 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: 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) -async def on_spec_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 manager.switch_to(AdminTemplatesSG.spec) async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: @@ -97,7 +236,7 @@ async def on_test_selected_for_export( 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] + 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 @@ -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"📄 Шаблон: {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( Window( Const(TEMPLATES_INFO), @@ -136,4 +302,17 @@ templates_dialog = Dialog( state=AdminTemplatesSG.export_list, 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, + ), ) diff --git a/src/trudex/application/bot/creator_dialogs/states.py b/src/trudex/application/bot/creator_dialogs/states.py index 02f22d4..eff6b23 100644 --- a/src/trudex/application/bot/creator_dialogs/states.py +++ b/src/trudex/application/bot/creator_dialogs/states.py @@ -8,6 +8,7 @@ class CreatorMenuSG(StatesGroup): class CreatorTemplatesSG(StatesGroup): main = State() export_list = State() + spec = State() class CreatorUsersSG(StatesGroup): diff --git a/src/trudex/application/bot/creator_dialogs/templates.py b/src/trudex/application/bot/creator_dialogs/templates.py index ba21d49..570b878 100644 --- a/src/trudex/application/bot/creator_dialogs/templates.py +++ b/src/trudex/application/bot/creator_dialogs/templates.py @@ -20,6 +20,145 @@ TEMPLATES_INFO = ( "🔹 Спецификация — описание формата JSON для создания тестов вручную" ) +SPEC_INFO = """📋 Спецификация формата JSON + +Структура файла: +{ + "title": "Название теста", + "description": "Описание теста", + "password": null, + "attempts": null, + "expires_at": null, + "for_group": null, + "questions": [...] +} + +Поля теста: +• title — название (обязательно, до 255 символов) +• description — описание (до 2000 символов) +• password — пароль для доступа или null +• attempts — лимит попыток (1-100) или null +• expires_at — срок действия в ISO формате или null +• for_group — номер группы или null для всех + +Типы вопросов: +• single — один правильный ответ +• multiple — несколько правильных ответов +• input — ввод текста (регистр и пробелы игнорируются) + +Формат вопроса (single/multiple): +{ + "text": "Текст вопроса", + "question_type": "single", + "options": [ + {"text": "Вариант 1", "is_correct": true}, + {"text": "Вариант 2", "is_correct": false} + ] +} + +Формат вопроса (input): +{ + "text": "Текст вопроса", + "question_type": "input", + "correct_answer": "правильный ответ" +} + +⚠️ Важно: +• Для single — ровно один is_correct: true +• Для multiple — один или более is_correct: true +• Минимум 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: 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) -async def on_spec_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 manager.switch_to(CreatorTemplatesSG.spec) async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: @@ -97,7 +236,7 @@ async def on_test_selected_for_export( 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] + 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 @@ -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"📄 Шаблон: {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( Window( Const(TEMPLATES_INFO), @@ -136,4 +302,17 @@ templates_dialog = Dialog( state=CreatorTemplatesSG.export_list, 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, + ), )