From b3237e281891aaee04fada8b11eeda2c29903c90 Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 4 Jan 2026 02:43:25 +0300 Subject: [PATCH] commit --- src/trudex/application/__main__.py | 3 + .../bot/admin_dialogs/create_test.py | 576 ++++++++++++++++++ .../application/bot/admin_dialogs/groups.py | 2 + .../application/bot/admin_dialogs/states.py | 18 + .../bot/admin_dialogs/templates.py | 7 +- .../application/bot/admin_dialogs/tests.py | 6 +- .../bot/creator_dialogs/templates.py | 7 +- src/trudex/domain/test_parser.py | 4 +- 8 files changed, 612 insertions(+), 11 deletions(-) create mode 100644 src/trudex/application/bot/admin_dialogs/create_test.py diff --git a/src/trudex/application/__main__.py b/src/trudex/application/__main__.py index 6240db8..0b0b213 100644 --- a/src/trudex/application/__main__.py +++ b/src/trudex/application/__main__.py @@ -11,6 +11,8 @@ from dishka.integrations.aiogram import setup_dishka from trudex.application.bot.admin_dialogs.broadcast import \ broadcast_dialog as admin_broadcast_dialog +from trudex.application.bot.admin_dialogs.create_test import \ + admin_create_test_dialog 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 @@ -77,6 +79,7 @@ async def main() -> None: admin_groups_dialog, admin_broadcast_dialog, admin_templates_dialog, + admin_create_test_dialog, creator_menu_dialog, creator_users_dialog, creator_tests_dialog, diff --git a/src/trudex/application/bot/admin_dialogs/create_test.py b/src/trudex/application/bot/admin_dialogs/create_test.py new file mode 100644 index 0000000..cee34c2 --- /dev/null +++ b/src/trudex/application/bot/admin_dialogs/create_test.py @@ -0,0 +1,576 @@ +from datetime import date, datetime, time + +from aiogram.types import CallbackQuery, ContentType, Message +from aiogram_dialog import Dialog, DialogManager, StartMode, Window +from aiogram_dialog.widgets.input import MessageInput +from aiogram_dialog.widgets.kbd import (Button, Calendar, Cancel, Column, 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 (AdminCreateTestSG, + AdminTestsSG) +from trudex.infrastructure.database.dao.group import GroupDAO +from trudex.infrastructure.database.dao.option import OptionDAO +from trudex.infrastructure.database.dao.question import QuestionDAO +from trudex.infrastructure.database.dao.test import TestDAO +from trudex.infrastructure.database.repo.test import TestRepository +from trudex.infrastructure.utils.timezone import to_msk + + +async def on_title_input(message: Message, _widget: MessageInput, manager: DialogManager): + if not message.text: + await message.answer("❌ Название не может быть пустым") + return + + title = message.text.strip() + if not title: + await message.answer("❌ Название не может быть пустым") + return + + if len(title) > 255: + await message.answer("❌ Название слишком длинное (максимум 255 символов)") + return + + manager.dialog_data["title"] = title + await manager.switch_to(AdminCreateTestSG.input_description) + + +async def on_description_input(message: Message, _widget: MessageInput, manager: DialogManager): + if not message.text: + await message.answer("❌ Описание не может быть пустым") + return + + description = message.text.strip() + if not description: + await message.answer("❌ Описание не может быть пустым") + return + + if len(description) > 2000: + await message.answer("❌ Описание слишком длинное (максимум 2000 символов)") + return + + manager.dialog_data["description"] = description + await manager.switch_to(AdminCreateTestSG.input_password) + + +@inject +async def on_password_input(message: Message, _widget: MessageInput, manager: DialogManager, _group_dao: FromDishka[GroupDAO]): + if not message.text: + await message.answer("❌ Пароль не может быть пустым") + return + + password = message.text.strip() + if not password: + await message.answer("❌ Пароль не может быть пустым") + return + + if len(password) > 255: + await message.answer("❌ Пароль слишком длинный (максимум 255 символов)") + return + + manager.dialog_data["password"] = password + await manager.switch_to(AdminCreateTestSG.input_attempts) + + +@inject +async def on_skip_password(_callback: CallbackQuery, _button: Button, manager: DialogManager, _group_dao: FromDishka[GroupDAO]): + manager.dialog_data["password"] = None + await manager.switch_to(AdminCreateTestSG.input_attempts) + + +async def on_attempts_input(message: Message, _widget: MessageInput, manager: DialogManager): + if not message.text: + await message.answer("❌ Количество попыток не может быть пустым") + return + + attempts_str = message.text.strip() + + if not attempts_str.isdigit(): + await message.answer("❌ Количество попыток должно быть числом") + return + + attempts = int(attempts_str) + + if attempts < 1: + await message.answer("❌ Количество попыток должно быть больше 0") + return + + if attempts > 100: + await message.answer("❌ Количество попыток не может быть больше 100") + return + + manager.dialog_data["attempts"] = attempts + await manager.switch_to(AdminCreateTestSG.input_expires_at) + + +async def on_skip_attempts(_callback: CallbackQuery, _button: Button, manager: DialogManager): + manager.dialog_data["attempts"] = None + await manager.switch_to(AdminCreateTestSG.input_expires_at) + + +async def on_date_selected(_callback, _widget, manager: DialogManager, selected_date: date): + manager.dialog_data["expires_at"] = datetime.combine(selected_date, time.min) + await manager.switch_to(AdminCreateTestSG.input_for_group) + + +async def on_skip_expires(_callback: CallbackQuery, _button: Button, manager: DialogManager): + manager.dialog_data["expires_at"] = None + await manager.switch_to(AdminCreateTestSG.input_for_group) + + +@inject +async def get_groups_for_test(group_dao: FromDishka[GroupDAO], **_kwargs): + groups = await group_dao.get_all() + + return { + "groups": [(str(g.number), str(g.number)) for g in groups], + } + + +async def on_group_selected(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str): + manager.dialog_data["for_group"] = int(item_id) + await manager.switch_to(AdminCreateTestSG.confirm_test_info) + + +async def on_skip_group(_callback: CallbackQuery, _button: Button, manager: DialogManager): + manager.dialog_data["for_group"] = None + await manager.switch_to(AdminCreateTestSG.confirm_test_info) + + +async def get_test_info(dialog_manager: DialogManager, **_kwargs): + title = dialog_manager.dialog_data.get("title", "—") + description = dialog_manager.dialog_data.get("description", "—") + password = dialog_manager.dialog_data.get("password") + attempts = dialog_manager.dialog_data.get("attempts") + expires_at = dialog_manager.dialog_data.get("expires_at") + for_group = dialog_manager.dialog_data.get("for_group") + + password_str = f"🔒 {password}" if password else "Без пароля" + attempts_str = f"🔄 {attempts}" if attempts else "♾️ Без ограничений" + expires_at_msk = to_msk(expires_at) + expires_str = expires_at_msk.strftime("%d.%m.%Y") if expires_at_msk else "Без срока" + group_str = str(for_group) if for_group else "Для всех" + + return { + "info": ( + f"📝 Информация о тесте\n\n" + f"Название: {title}\n" + f"Описание: {description}\n" + f"Пароль: {password_str}\n" + f"Попыток: {attempts_str}\n" + f"Истекает: {expires_str}\n" + f"Для группы: {group_str}" + ) + } + + +@inject +async def on_confirm_test(_callback: CallbackQuery, _button: Button, manager: DialogManager, test_dao: FromDishka[TestDAO]): + title = manager.dialog_data.get("title") + assert isinstance(title, str) + description = manager.dialog_data.get("description") + password = manager.dialog_data.get("password") + attempts = manager.dialog_data.get("attempts") + expires_at = manager.dialog_data.get("expires_at") + for_group = manager.dialog_data.get("for_group") + + test = await test_dao.create( + title=title, + description=description, + password=password, + attempts=attempts, + expires_at=expires_at, + for_group=for_group, + ) + + manager.dialog_data["test_id"] = test.id + manager.dialog_data["questions"] = [] + await manager.switch_to(AdminCreateTestSG.add_question) + + +async def on_add_question(_callback: CallbackQuery, _button: Button, manager: DialogManager): + manager.dialog_data["current_question"] = {} + await manager.switch_to(AdminCreateTestSG.input_question_text) + + +async def on_question_input(message: Message, _widget: MessageInput, manager: DialogManager): + current_question = manager.dialog_data.get("current_question", {}) + + if message.content_type == ContentType.PHOTO: + photo = message.photo[-1] if message.photo else None + if photo: + text = (message.caption or "").strip() + if not text: + await message.answer("❌ Изображение должно содержать подпись с текстом вопроса") + return + if len(text) > 2000: + await message.answer("❌ Текст вопроса слишком длинный (максимум 2000 символов)") + return + current_question["tg_file_id"] = photo.file_id + current_question["text"] = text + elif message.content_type == ContentType.TEXT and message.text: + text = message.text.strip() + if not text: + await message.answer("❌ Текст вопроса не может быть пустым") + return + if len(text) > 2000: + await message.answer("❌ Текст вопроса слишком длинный (максимум 2000 символов)") + return + current_question["text"] = text + current_question["tg_file_id"] = None + else: + await message.answer("❌ Отправьте текст или фото с подписью") + return + + manager.dialog_data["current_question"] = current_question + await manager.switch_to(AdminCreateTestSG.select_question_type) + + +async def get_question_type_data(**_kwargs): + return { + "question_types": [ + ("single", "📌 Один правильный ответ"), + ("multiple", "📋 Несколько правильных ответов"), + ("input", "✏️ Ввод текста"), + ] + } + + +async def on_question_type_selected(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str): + current_question = manager.dialog_data.get("current_question", {}) + current_question["question_type"] = item_id + manager.dialog_data["current_question"] = current_question + + if item_id == "input": + await manager.switch_to(AdminCreateTestSG.input_correct_answer) + else: + manager.dialog_data["current_options"] = [] + await manager.switch_to(AdminCreateTestSG.input_options) + + +async def on_correct_answer_input(message: Message, _widget: MessageInput, manager: DialogManager): + if not message.text: + await message.answer("❌ Правильный ответ не может быть пустым") + return + + answer = message.text.strip() + if not answer: + await message.answer("❌ Правильный ответ не может быть пустым") + return + + if len(answer) > 255: + await message.answer("❌ Ответ слишком длинный (максимум 255 символов)") + return + + current_question = manager.dialog_data.get("current_question", {}) + current_question["correct_answer"] = answer + manager.dialog_data["current_question"] = current_question + await manager.switch_to(AdminCreateTestSG.confirm_question) + + +async def on_option_input(message: Message, _widget: MessageInput, manager: DialogManager): + if not message.text: + await message.answer("❌ Вариант ответа не может быть пустым") + return + + option_text = message.text.strip() + if not option_text: + await message.answer("❌ Вариант ответа не может быть пустым") + return + + if len(option_text) > 255: + await message.answer("❌ Вариант ответа слишком длинный (максимум 255 символов)") + return + + current_options = manager.dialog_data.get("current_options", []) + + if len(current_options) >= 10: + await message.answer("❌ Максимум 10 вариантов ответа") + return + + current_options.append({"text": option_text, "is_correct": False}) + manager.dialog_data["current_options"] = current_options + + await message.answer(f"✅ Вариант {len(current_options)} добавлен") + + +async def on_finish_options(_callback: CallbackQuery, _button: Button, manager: DialogManager): + current_options = manager.dialog_data.get("current_options", []) + if len(current_options) < 2: + await _callback.answer("❌ Добавьте минимум 2 варианта ответа", show_alert=True) + return + + await manager.switch_to(AdminCreateTestSG.mark_correct_options) + + +async def get_options_data(dialog_manager: DialogManager, **_kwargs): + current_options = dialog_manager.dialog_data.get("current_options", []) + formatted_options = [] + for i, opt in enumerate(current_options): + marker = "✅" if opt["is_correct"] else "❌" + formatted_options.append((str(i), f"{marker} {opt['text']}")) + return { + "options": formatted_options, + "options_count": len(current_options), + } + + +async def on_option_toggle(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str): + current_options = manager.dialog_data.get("current_options", []) + current_question = manager.dialog_data.get("current_question", {}) + question_type = current_question.get("question_type", "single") + + option_idx = int(item_id) + + if question_type == "single": + for opt in current_options: + opt["is_correct"] = False + current_options[option_idx]["is_correct"] = True + else: + current_options[option_idx]["is_correct"] = not current_options[option_idx]["is_correct"] + + manager.dialog_data["current_options"] = current_options + await _callback.answer() + + +async def on_confirm_correct(_callback: CallbackQuery, _button: Button, manager: DialogManager): + current_options = manager.dialog_data.get("current_options", []) + + if not any(opt["is_correct"] for opt in current_options): + await _callback.answer("❌ Отметьте хотя бы один правильный ответ", show_alert=True) + return + + await manager.switch_to(AdminCreateTestSG.confirm_question) + + +async def get_question_preview(dialog_manager: DialogManager, **_kwargs): + current_question = dialog_manager.dialog_data.get("current_question", {}) + current_options = dialog_manager.dialog_data.get("current_options", []) + + text = current_question.get("text", "") + question_type = current_question.get("question_type", "single") + has_image = current_question.get("tg_file_id") is not None + + type_names = { + "single": "📌 Один правильный ответ", + "multiple": "📋 Несколько правильных ответов", + "input": "✏️ Ввод текста", + } + + preview = f"📝 Предпросмотр вопроса\n\n" + preview += f"Текст: {text}\n" + preview += f"Тип: {type_names[question_type]}\n" + preview += f"Изображение: {'✅ Да' if has_image else '❌ Нет'}\n\n" + + if question_type == "input": + correct_answer = current_question.get("correct_answer", "") + preview += f"Правильный ответ: {correct_answer}" + else: + preview += "Варианты ответов:\n" + for i, opt in enumerate(current_options, 1): + marker = "✅" if opt["is_correct"] else "❌" + preview += f"{i}. {marker} {opt['text']}\n" + + return {"preview": preview} + + +@inject +async def on_save_question( + _callback: CallbackQuery, + _button: Button, + manager: DialogManager, + question_dao: FromDishka[QuestionDAO], + option_dao: FromDishka[OptionDAO], + test_repo: FromDishka[TestRepository], +): + test_id = manager.dialog_data.get("test_id") + assert isinstance(test_id, int) + current_question = manager.dialog_data.get("current_question", {}) + current_options = manager.dialog_data.get("current_options", []) + + questions_count = await test_repo.count_questions_in_test(test_id) + + question = await question_dao.create( + test_id=test_id, + text=current_question.get("text", ""), + position=questions_count, + question_type=current_question.get("question_type", "single"), + tg_file_id=current_question.get("tg_file_id"), + ) + + if current_question.get("question_type") == "input": + await option_dao.create( + question_id=question.id, + text=current_question.get("correct_answer", ""), + is_correct=True, + ) + else: + for opt in current_options: + await option_dao.create( + question_id=question.id, + text=opt["text"], + is_correct=opt["is_correct"], + ) + + questions = manager.dialog_data.get("questions", []) + questions.append(question.id) + manager.dialog_data["questions"] = questions + + manager.dialog_data.pop("current_question", None) + manager.dialog_data.pop("current_options", None) + + await _callback.answer("✅ Вопрос добавлен") + await manager.switch_to(AdminCreateTestSG.add_question) + + +async def on_cancel_question(_callback: CallbackQuery, _button: Button, manager: DialogManager): + manager.dialog_data.pop("current_question", None) + manager.dialog_data.pop("current_options", None) + await manager.switch_to(AdminCreateTestSG.add_question) + + +async def get_questions_count(dialog_manager: DialogManager, **_kwargs): + questions = dialog_manager.dialog_data.get("questions", []) + return {"questions_count": len(questions)} + + +async def on_finish_test(_callback: CallbackQuery, _button: Button, manager: DialogManager): + questions = manager.dialog_data.get("questions", []) + + if len(questions) == 0: + await _callback.answer("❌ Добавьте хотя бы один вопрос", show_alert=True) + return + + await _callback.answer("✅ Тест создан") + await manager.start(AdminTestsSG.tests_list, mode=StartMode.RESET_STACK) + + +async def on_cancel(_callback: CallbackQuery, _button: Button, manager: DialogManager): + await manager.start(AdminTestsSG.tests_list, mode=StartMode.RESET_STACK) + + +admin_create_test_dialog = Dialog( + Window( + Const("📝 Создание теста\n\n💬 Введите название теста:\n(максимум 255 символов)"), + MessageInput(on_title_input), + Cancel(Const("◀️ Отмена")), + state=AdminCreateTestSG.input_title, + ), + Window( + Const("📝 Создание теста\n\n📄 Введите описание теста:\n(максимум 2000 символов)"), + MessageInput(on_description_input), + state=AdminCreateTestSG.input_description, + ), + Window( + Const("🔒 Пароль\n\n🔑 Введите пароль для доступа к тесту или пропустите этот шаг:\n(максимум 255 символов)"), + MessageInput(on_password_input), + Button(Const("⏭️ Без пароля"), id="skip_password", on_click=on_skip_password), + state=AdminCreateTestSG.input_password, + ), + Window( + Const("🔄 Количество попыток\n\n🔢 Введите количество попыток (1-100) или пропустите для неограниченного количества:"), + MessageInput(on_attempts_input), + Button(Const("⏭️ Без ограничений"), id="skip_attempts", on_click=on_skip_attempts), + state=AdminCreateTestSG.input_attempts, + ), + Window( + Const("📅 Срок действия\n\n🗓 Выберите дату истечения теста или пропустите:"), + Calendar(id="calendar", on_click=on_date_selected), + Button(Const("⏭️ Без срока"), id="skip_expires", on_click=on_skip_expires), + state=AdminCreateTestSG.input_expires_at, + ), + Window( + Const("👥 Группа\n\n🎓 Выберите группу или пропустите для всех:"), + ScrollingGroup( + Select( + Format("{item[1]}"), + id="groups", + item_id_getter=lambda x: x[0], + items="groups", + on_click=on_group_selected, + ), + id="groups_scroll", + width=2, + height=7, + ), + Button(Const("⏭️ Для всех"), id="skip_group", on_click=on_skip_group), + state=AdminCreateTestSG.input_for_group, + getter=get_groups_for_test, + ), + Window( + Format("{info}\n\n✅ Подтвердите создание теста:"), + Row( + Button(Const("✅ Создать"), id="confirm", on_click=on_confirm_test), + Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel), + ), + state=AdminCreateTestSG.confirm_test_info, + getter=get_test_info, + ), + Window( + Format("➕ Добавление вопросов\n\n📊 Вопросов добавлено: {questions_count}\n\n💡 Добавьте вопросы к тесту:"), + Column( + Button(Const("➕ Добавить вопрос"), id="add_question", on_click=on_add_question), + Button(Const("✅ Завершить создание"), id="finish", on_click=on_finish_test), + ), + state=AdminCreateTestSG.add_question, + getter=get_questions_count, + ), + Window( + Const("❓ Текст вопроса\n\n📝 Отправьте текст вопроса или 📷 фото с подписью:\n(максимум 2000 символов)"), + MessageInput(on_question_input, content_types=[ContentType.TEXT, ContentType.PHOTO]), + Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question), + state=AdminCreateTestSG.input_question_text, + ), + Window( + Const("📋 Тип вопроса\n\n🎯 Выберите тип вопроса:"), + Column(Select( + Format("{item[1]}"), + id="question_type", + item_id_getter=lambda x: x[0], + items="question_types", + on_click=on_question_type_selected, + )), + Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question), + state=AdminCreateTestSG.select_question_type, + getter=get_question_type_data, + ), + Window( + Const("✏️ Правильный ответ\n\n💬 Введите правильный ответ (регистр и пробелы игнорируются):\n(максимум 255 символов)"), + MessageInput(on_correct_answer_input), + Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question), + state=AdminCreateTestSG.input_correct_answer, + ), + Window( + Format("📝 Варианты ответов\n\n📊 Добавлено вариантов: {options_count}/10\n\n💬 Введите вариант ответа:\n(максимум 255 символов)"), + MessageInput(on_option_input), + Button(Const("✅ Завершить добавление вариантов"), id="finish_options", on_click=on_finish_options), + Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question), + state=AdminCreateTestSG.input_options, + getter=get_options_data, + ), + Window( + Const("✅ Правильные ответы\n\nОтметьте правильные варианты ответов:"), + Column(Select( + Format("{item[1]}"), + id="options", + item_id_getter=lambda x: x[0], + items="options", + on_click=on_option_toggle, + )), + Button(Const("✅ Подтвердить выбор"), id="confirm", on_click=on_confirm_correct), + Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question), + state=AdminCreateTestSG.mark_correct_options, + getter=get_options_data, + ), + Window( + Format("{preview}\n\n💾 Сохранить вопрос?"), + Row( + Button(Const("✅ Сохранить"), id="save", on_click=on_save_question), + Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_question), + ), + state=AdminCreateTestSG.confirm_question, + getter=get_question_preview, + ), +) diff --git a/src/trudex/application/bot/admin_dialogs/groups.py b/src/trudex/application/bot/admin_dialogs/groups.py index 4d62d10..75bab05 100644 --- a/src/trudex/application/bot/admin_dialogs/groups.py +++ b/src/trudex/application/bot/admin_dialogs/groups.py @@ -116,6 +116,8 @@ async def get_delete_confirm_data(dialog_manager: DialogManager, **_kwargs): @inject async def on_confirm_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager, group_dao: FromDishka[GroupDAO]): group_id = manager.dialog_data.get("delete_group_id") + + assert isinstance(group_id, int) await group_dao.delete(group_id) diff --git a/src/trudex/application/bot/admin_dialogs/states.py b/src/trudex/application/bot/admin_dialogs/states.py index e346506..c374aa7 100644 --- a/src/trudex/application/bot/admin_dialogs/states.py +++ b/src/trudex/application/bot/admin_dialogs/states.py @@ -41,3 +41,21 @@ class AdminGroupsSG(StatesGroup): add_group_input_number = State() delete_groups_list = State() delete_confirm = State() + + +class AdminCreateTestSG(StatesGroup): + input_title = State() + input_description = State() + input_password = State() + input_attempts = State() + input_expires_at = State() + input_for_group = State() + confirm_test_info = State() + add_question = State() + input_question_text = State() + select_question_type = State() + input_correct_answer = State() + input_options = State() + mark_correct_options = State() + confirm_question = State() + test_created = State() diff --git a/src/trudex/application/bot/admin_dialogs/templates.py b/src/trudex/application/bot/admin_dialogs/templates.py index f8aa8ab..8529035 100644 --- a/src/trudex/application/bot/admin_dialogs/templates.py +++ b/src/trudex/application/bot/admin_dialogs/templates.py @@ -9,7 +9,7 @@ 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.application.bot.admin_dialogs.states import AdminMenuSG, AdminTemplatesSG, AdminTestsSG from trudex.domain.test_parser import ParsedTest, TestParser from trudex.infrastructure.database.dao.option import OptionDAO from trudex.infrastructure.database.dao.question import QuestionDAO @@ -291,6 +291,7 @@ async def create_test_from_parsed( attempts=parsed.attempts, expires_at=parsed.expires_at, for_group=parsed.for_group, + is_active=False, ) for position, q in enumerate(parsed.questions): @@ -361,7 +362,7 @@ async def on_import_file( await message.answer("\n".join(error_lines)) return - test_id = await create_test_from_parsed(result, test_dao, question_dao, option_dao) + await create_test_from_parsed(result, test_dao, question_dao, option_dao) await message.answer( f"✅ Тест импортирован!\n\n" @@ -370,7 +371,7 @@ async def on_import_file( f"Тест создан в деактивированном состоянии." ) - await manager.switch_to(AdminTemplatesSG.main) + await manager.start(AdminTestsSG.tests_list, mode=StartMode.RESET_STACK) templates_dialog = Dialog( diff --git a/src/trudex/application/bot/admin_dialogs/tests.py b/src/trudex/application/bot/admin_dialogs/tests.py index 8363be1..7bfbe80 100644 --- a/src/trudex/application/bot/admin_dialogs/tests.py +++ b/src/trudex/application/bot/admin_dialogs/tests.py @@ -13,8 +13,8 @@ from dishka import FromDishka from dishka.integrations.aiogram_dialog import inject from trudex.application.bot.admin_dialogs.states import (AdminMenuSG, - AdminTestsSG) -from trudex.application.bot.creator_dialogs.states import CreateTestSG + AdminTestsSG, + AdminCreateTestSG) from trudex.infrastructure.database.dao.group import GroupDAO from trudex.infrastructure.database.dao.test import TestDAO from trudex.infrastructure.database.repo.test import TestRepository @@ -420,7 +420,7 @@ async def on_remove_expires(_callback: CallbackQuery, _button: Button, manager: async def on_add_test_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager): - await manager.start(CreateTestSG.input_title, mode=StartMode.RESET_STACK) + await manager.start(AdminCreateTestSG.input_title, mode=StartMode.RESET_STACK) async def on_back_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager): diff --git a/src/trudex/application/bot/creator_dialogs/templates.py b/src/trudex/application/bot/creator_dialogs/templates.py index 599cae9..396c774 100644 --- a/src/trudex/application/bot/creator_dialogs/templates.py +++ b/src/trudex/application/bot/creator_dialogs/templates.py @@ -9,7 +9,7 @@ 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.application.bot.creator_dialogs.states import CreatorMenuSG, CreatorTemplatesSG, CreatorTestsSG from trudex.domain.test_parser import ParsedTest, TestParser from trudex.infrastructure.database.dao.option import OptionDAO from trudex.infrastructure.database.dao.question import QuestionDAO @@ -291,6 +291,7 @@ async def create_test_from_parsed( attempts=parsed.attempts, expires_at=parsed.expires_at, for_group=parsed.for_group, + is_active=False, ) for position, q in enumerate(parsed.questions): @@ -361,7 +362,7 @@ async def on_import_file( await message.answer("\n".join(error_lines)) return - test_id = await create_test_from_parsed(result, test_dao, question_dao, option_dao) + await create_test_from_parsed(result, test_dao, question_dao, option_dao) await message.answer( f"✅ Тест импортирован!\n\n" @@ -370,7 +371,7 @@ async def on_import_file( f"Тест создан в деактивированном состоянии." ) - await manager.switch_to(CreatorTemplatesSG.main) + await manager.start(CreatorTestsSG.tests_list, mode=StartMode.RESET_STACK) templates_dialog = Dialog( diff --git a/src/trudex/domain/test_parser.py b/src/trudex/domain/test_parser.py index b7524fb..9fc6009 100644 --- a/src/trudex/domain/test_parser.py +++ b/src/trudex/domain/test_parser.py @@ -127,11 +127,11 @@ class TestParser: return None if min_val is not None and value < min_val: - errors.append(ParseError(f"Поле '{key}' должно быть >= {min_val}", path=key)) + errors.append(ParseError(f"Поле '{key}' должно быть не меньше {min_val}", path=key)) return None if max_val is not None and value > max_val: - errors.append(ParseError(f"Поле '{key}' должно быть <= {max_val}", path=key)) + errors.append(ParseError(f"Поле '{key}' должно быть не больше {max_val}", path=key)) return None return value