mirror of
https://github.com/koloideal/Quizzi.git
synced 2026-06-10 18:35:28 +03:00
commit
This commit is contained in:
@@ -54,18 +54,18 @@ SPEC_INFO = """<b>📋 Спецификация формата JSON</b>
|
|||||||
|
|
||||||
<b>Формат вопроса (single/multiple):</b>
|
<b>Формат вопроса (single/multiple):</b>
|
||||||
<code>{
|
<code>{
|
||||||
"text": "Текст вопроса",
|
|
||||||
"question_type": "single",
|
"question_type": "single",
|
||||||
"options": [
|
"question": "Текст вопроса",
|
||||||
{"text": "Вариант 1", "is_correct": true},
|
"answers": [
|
||||||
{"text": "Вариант 2", "is_correct": false}
|
{"option": "Вариант 1", "is_correct": true},
|
||||||
|
{"option": "Вариант 2", "is_correct": false}
|
||||||
]
|
]
|
||||||
}</code>
|
}</code>
|
||||||
|
|
||||||
<b>Формат вопроса (input):</b>
|
<b>Формат вопроса (input):</b>
|
||||||
<code>{
|
<code>{
|
||||||
"text": "Текст вопроса",
|
|
||||||
"question_type": "input",
|
"question_type": "input",
|
||||||
|
"question": "Текст вопроса",
|
||||||
"correct_answer": "правильный ответ"
|
"correct_answer": "правильный ответ"
|
||||||
}</code>
|
}</code>
|
||||||
|
|
||||||
@@ -75,96 +75,95 @@ SPEC_INFO = """<b>📋 Спецификация формата JSON</b>
|
|||||||
• Минимум 2 варианта ответа для single/multiple"""
|
• Минимум 2 варианта ответа для single/multiple"""
|
||||||
|
|
||||||
|
|
||||||
TEMPLATE_SINGLE = {
|
TEMPLATE_ULTIMATE = """// ═══════════════════════════════════════════════════════════════
|
||||||
"title": "Пример теста с одиночным выбором",
|
// УЛЬТИМАТИВНЫЙ ШАБЛОН ТЕСТА
|
||||||
"description": "Демонстрация формата single вопросов",
|
// ═══════════════════════════════════════════════════════════════
|
||||||
"password": None,
|
//
|
||||||
"attempts": None,
|
// 📝 Название: Ультимативный пример теста
|
||||||
"expires_at": None,
|
// 📄 Описание: Полная демонстрация всех возможностей формата
|
||||||
"for_group": None,
|
//
|
||||||
"questions": [
|
// ⚙️ НАСТРОЙКИ:
|
||||||
{
|
// • Пароль: test2024
|
||||||
"text": "Какой язык программирования используется для разработки Telegram ботов?",
|
// • Попыток: 5
|
||||||
"question_type": "single",
|
// • Срок действия: 31 декабря 2026, 23:59
|
||||||
"options": [
|
// • Для группы: 2024 (или null для всех)
|
||||||
{"text": "Python", "is_correct": True},
|
//
|
||||||
{"text": "HTML", "is_correct": False},
|
// ❓ ВОПРОСЫ (всего 6):
|
||||||
{"text": "CSS", "is_correct": False},
|
// 1. [single] - Один правильный ответ (3 варианта)
|
||||||
],
|
// 2. [single] - Один правильный ответ (4 варианта)
|
||||||
},
|
// 3. [multiple] - Несколько правильных (4 варианта, 2 верных)
|
||||||
],
|
// 4. [multiple] - Несколько правильных (5 вариантов, 3 верных)
|
||||||
}
|
// 5. [input] - Ввод текста (точный ответ)
|
||||||
|
// 6. [input] - Ввод текста (регистр игнорируется)
|
||||||
|
//
|
||||||
|
// 💡 ПОДСКАЗКИ:
|
||||||
|
// • null означает "не задано" / "без ограничений"
|
||||||
|
// • expires_at в формате ISO 8601: YYYY-MM-DDTHH:MM:SS
|
||||||
|
// • for_group - номер группы или null для всех пользователей
|
||||||
|
//
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
TEMPLATE_MULTIPLE = {
|
{
|
||||||
"title": "Пример теста с множественным выбором",
|
"title": "Ультимативный пример теста",
|
||||||
"description": "Демонстрация формата multiple вопросов",
|
"description": "Полная демонстрация всех возможностей формата: разные типы вопросов, настройки доступа, ограничения по времени и группам",
|
||||||
"password": None,
|
"password": "test2024",
|
||||||
"attempts": None,
|
"attempts": 5,
|
||||||
"expires_at": None,
|
"expires_at": "2026-12-31T23:59:59",
|
||||||
"for_group": None,
|
"for_group": 2024,
|
||||||
"questions": [
|
"questions": [
|
||||||
{
|
{
|
||||||
"text": "Выберите языки программирования:",
|
"question_type": "single",
|
||||||
"question_type": "multiple",
|
"question": "Какой язык программирования чаще всего используется для создания Telegram ботов?",
|
||||||
"options": [
|
"answers": [
|
||||||
{"text": "Python", "is_correct": True},
|
{"option": "Python", "is_correct": true},
|
||||||
{"text": "JavaScript", "is_correct": True},
|
{"option": "HTML", "is_correct": false},
|
||||||
{"text": "HTML", "is_correct": False},
|
{"option": "CSS", "is_correct": false}
|
||||||
{"text": "CSS", "is_correct": False},
|
]
|
||||||
],
|
},
|
||||||
},
|
{
|
||||||
],
|
"question_type": "single",
|
||||||
}
|
"question": "Сколько байт в одном килобайте?",
|
||||||
|
"answers": [
|
||||||
TEMPLATE_INPUT = {
|
{"option": "100", "is_correct": false},
|
||||||
"title": "Пример теста с вводом текста",
|
{"option": "1000", "is_correct": false},
|
||||||
"description": "Демонстрация формата input вопросов",
|
{"option": "1024", "is_correct": true},
|
||||||
"password": None,
|
{"option": "2048", "is_correct": false}
|
||||||
"attempts": None,
|
]
|
||||||
"expires_at": None,
|
},
|
||||||
"for_group": None,
|
{
|
||||||
"questions": [
|
"question_type": "multiple",
|
||||||
{
|
"question": "Выберите все языки программирования из списка:",
|
||||||
"text": "Как называется библиотека для создания Telegram ботов на Python?",
|
"answers": [
|
||||||
"question_type": "input",
|
{"option": "Python", "is_correct": true},
|
||||||
"correct_answer": "aiogram",
|
{"option": "JavaScript", "is_correct": true},
|
||||||
},
|
{"option": "HTML", "is_correct": false},
|
||||||
],
|
{"option": "CSS", "is_correct": false}
|
||||||
}
|
]
|
||||||
|
},
|
||||||
TEMPLATE_FULL = {
|
{
|
||||||
"title": "Полный пример теста",
|
"question_type": "multiple",
|
||||||
"description": "Тест со всеми типами вопросов и настройками",
|
"question": "Какие из перечисленных являются базами данных?",
|
||||||
"password": "secret123",
|
"answers": [
|
||||||
"attempts": 3,
|
{"option": "PostgreSQL", "is_correct": true},
|
||||||
"expires_at": "2026-12-31T23:59:59",
|
{"option": "MongoDB", "is_correct": true},
|
||||||
"for_group": 1234,
|
{"option": "Redis", "is_correct": true},
|
||||||
"questions": [
|
{"option": "React", "is_correct": false},
|
||||||
{
|
{"option": "Docker", "is_correct": false}
|
||||||
"text": "Выберите правильный ответ:",
|
]
|
||||||
"question_type": "single",
|
},
|
||||||
"options": [
|
{
|
||||||
{"text": "Вариант A", "is_correct": False},
|
"question_type": "input",
|
||||||
{"text": "Вариант B", "is_correct": True},
|
"question": "Как называется популярная библиотека для создания Telegram ботов на Python? (одно слово)",
|
||||||
{"text": "Вариант C", "is_correct": False},
|
"correct_answer": "aiogram"
|
||||||
],
|
},
|
||||||
},
|
{
|
||||||
{
|
"question_type": "input",
|
||||||
"text": "Выберите все правильные ответы:",
|
"question": "Напишите название протокола для безопасной передачи данных в интернете (4 буквы, регистр не важен)",
|
||||||
"question_type": "multiple",
|
"correct_answer": "HTTPS"
|
||||||
"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:
|
||||||
@@ -204,11 +203,14 @@ async def on_test_selected_for_export(
|
|||||||
item_id: str,
|
item_id: str,
|
||||||
test_repo: FromDishka[TestRepository],
|
test_repo: FromDishka[TestRepository],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
assert _callback.message is not None
|
||||||
|
await _callback.answer("⏳ Экспортирую тест...")
|
||||||
|
|
||||||
test_id = int(item_id)
|
test_id = int(item_id)
|
||||||
test, questions_with_options = await test_repo.get_full_test(test_id)
|
test, questions_with_options = await test_repo.get_full_test(test_id)
|
||||||
|
|
||||||
if not test:
|
if not test:
|
||||||
await _callback.answer("❌ Тест не найден")
|
await _callback.message.answer("❌ Тест не найден")
|
||||||
return
|
return
|
||||||
|
|
||||||
export_data: dict = {
|
export_data: dict = {
|
||||||
@@ -225,8 +227,8 @@ async def on_test_selected_for_export(
|
|||||||
|
|
||||||
for question, options in questions_with_options:
|
for question, options in questions_with_options:
|
||||||
question_data: dict = {
|
question_data: dict = {
|
||||||
"text": question.text,
|
|
||||||
"question_type": question.question_type.value,
|
"question_type": question.question_type.value,
|
||||||
|
"question": question.text,
|
||||||
}
|
}
|
||||||
|
|
||||||
if question.question_type == QuestionType.INPUT:
|
if question.question_type == QuestionType.INPUT:
|
||||||
@@ -234,8 +236,8 @@ async def on_test_selected_for_export(
|
|||||||
if correct_options:
|
if correct_options:
|
||||||
question_data["correct_answer"] = correct_options[0].text
|
question_data["correct_answer"] = correct_options[0].text
|
||||||
else:
|
else:
|
||||||
question_data["options"] = [
|
question_data["answers"] = [
|
||||||
{"text": o.text, "is_correct": o.is_correct}
|
{"option": o.text, "is_correct": o.is_correct}
|
||||||
for o in options
|
for o in options
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -243,41 +245,46 @@ async def on_test_selected_for_export(
|
|||||||
|
|
||||||
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
|
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# Build comment header
|
||||||
|
created_str = test.created_at.strftime("%d.%m.%Y %H:%M") if test.created_at else "—"
|
||||||
|
updated_str = test.updated_at.strftime("%d.%m.%Y %H:%M") if test.updated_at else "—"
|
||||||
|
questions_count = len(questions_with_options)
|
||||||
|
|
||||||
|
comment_header = f"""// ═══════════════════════════════════════════════════════════════
|
||||||
|
// ЭКСПОРТ ТЕСТА: {test.title}
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
//
|
||||||
|
// ❓ Вопросов: {questions_count}
|
||||||
|
// 📅 Создан: {created_str}
|
||||||
|
// 🔄 Обновлён: {updated_str}
|
||||||
|
//
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
full_content = comment_header + json_str
|
||||||
|
|
||||||
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"
|
filename = f"{safe_title}.json"
|
||||||
|
|
||||||
assert _callback.message is not None
|
|
||||||
await _callback.message.answer_document(
|
await _callback.message.answer_document(
|
||||||
document=BufferedInputFile(json_str.encode("utf-8"), filename=filename),
|
document=BufferedInputFile(full_content.encode("utf-8"), filename=filename),
|
||||||
caption=f"📤 <b>Экспорт теста:</b> {test.title}",
|
caption=f"📤 <b>Экспорт теста:</b> {test.title}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def send_template(callback: CallbackQuery, template: dict, name: str) -> None:
|
async def send_template(callback: CallbackQuery, template_str: str, name: str, title: str) -> None:
|
||||||
json_str = json.dumps(template, ensure_ascii=False, indent=2)
|
|
||||||
filename = f"template_{name}.json"
|
filename = f"template_{name}.json"
|
||||||
|
|
||||||
assert callback.message is not None
|
assert callback.message is not None
|
||||||
await callback.message.answer_document(
|
await callback.message.answer_document(
|
||||||
document=BufferedInputFile(json_str.encode("utf-8"), filename=filename),
|
document=BufferedInputFile(template_str.encode("utf-8"), filename=filename),
|
||||||
caption=f"📄 <b>Шаблон:</b> {template['title']}",
|
caption=f"📄 <b>Шаблон:</b> {title}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def on_template_single(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None:
|
async def on_template_ultimate(_callback: CallbackQuery, _button: Button, _manager: DialogManager) -> None:
|
||||||
await send_template(_callback, TEMPLATE_SINGLE, "single")
|
await send_template(_callback, TEMPLATE_ULTIMATE, "ultimate", "Ультимативный пример теста")
|
||||||
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
async def create_test_from_parsed(
|
async def create_test_from_parsed(
|
||||||
@@ -332,20 +339,22 @@ async def on_import_file(
|
|||||||
await message.answer("❌ Файл слишком большой (максимум 1 МБ)")
|
await message.answer("❌ Файл слишком большой (максимум 1 МБ)")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
progress_msg = await message.answer("⏳ Импортирую тест...")
|
||||||
|
|
||||||
file = await bot_inst.get_file(message.document.file_id)
|
file = await bot_inst.get_file(message.document.file_id)
|
||||||
if not file.file_path:
|
if not file.file_path:
|
||||||
await message.answer("❌ Не удалось загрузить файл")
|
await progress_msg.edit_text("❌ Не удалось загрузить файл")
|
||||||
return
|
return
|
||||||
|
|
||||||
file_bytes = await bot_inst.download_file(file.file_path)
|
file_bytes = await bot_inst.download_file(file.file_path)
|
||||||
if not file_bytes:
|
if not file_bytes:
|
||||||
await message.answer("❌ Не удалось загрузить файл")
|
await progress_msg.edit_text("❌ Не удалось загрузить файл")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
json_str = file_bytes.read().decode("utf-8")
|
json_str = file_bytes.read().decode("utf-8")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
await message.answer("❌ Файл должен быть в кодировке UTF-8")
|
await progress_msg.edit_text("❌ Файл должен быть в кодировке UTF-8")
|
||||||
return
|
return
|
||||||
|
|
||||||
parser = TestParser()
|
parser = TestParser()
|
||||||
@@ -353,7 +362,7 @@ async def on_import_file(
|
|||||||
|
|
||||||
if isinstance(result, list):
|
if isinstance(result, list):
|
||||||
if not result:
|
if not result:
|
||||||
await message.answer("❌ Неизвестная ошибка валидации")
|
await progress_msg.edit_text("❌ Неизвестная ошибка валидации")
|
||||||
return
|
return
|
||||||
error_lines = ["❌ <b>Ошибки валидации:</b>\n"]
|
error_lines = ["❌ <b>Ошибки валидации:</b>\n"]
|
||||||
for err in result[:10]:
|
for err in result[:10]:
|
||||||
@@ -361,12 +370,12 @@ async def on_import_file(
|
|||||||
error_lines.append(f"• {err.message}{path_str}")
|
error_lines.append(f"• {err.message}{path_str}")
|
||||||
if len(result) > 10:
|
if len(result) > 10:
|
||||||
error_lines.append(f"\n... и ещё {len(result) - 10} ошибок")
|
error_lines.append(f"\n... и ещё {len(result) - 10} ошибок")
|
||||||
await message.answer("\n".join(error_lines))
|
await progress_msg.edit_text("\n".join(error_lines))
|
||||||
return
|
return
|
||||||
|
|
||||||
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(
|
await progress_msg.edit_text(
|
||||||
f"✅ <b>Тест импортирован!</b>\n\n"
|
f"✅ <b>Тест импортирован!</b>\n\n"
|
||||||
f"📝 <b>Название:</b> {result.title}\n"
|
f"📝 <b>Название:</b> {result.title}\n"
|
||||||
f"❓ <b>Вопросов:</b> {len(result.questions)}\n\n"
|
f"❓ <b>Вопросов:</b> {len(result.questions)}\n\n"
|
||||||
@@ -407,14 +416,7 @@ shared_templates_dialog = Dialog(
|
|||||||
),
|
),
|
||||||
Window(
|
Window(
|
||||||
Const(SPEC_INFO),
|
Const(SPEC_INFO),
|
||||||
Row(
|
Button(Const("📦 Ультимативный шаблон"), id="tpl_ultimate", on_click=on_template_ultimate),
|
||||||
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),
|
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_templates),
|
||||||
state=SharedTemplatesSG.spec,
|
state=SharedTemplatesSG.spec,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -193,18 +193,18 @@ class TestParser:
|
|||||||
return questions
|
return questions
|
||||||
|
|
||||||
def _parse_question(self, data: dict, path: str, errors: list[ParseError]) -> ParsedQuestion | None:
|
def _parse_question(self, data: dict, path: str, errors: list[ParseError]) -> ParsedQuestion | None:
|
||||||
text = data.get("text")
|
text = data.get("question")
|
||||||
if not text or not isinstance(text, str):
|
if not text or not isinstance(text, str):
|
||||||
errors.append(ParseError("Поле 'text' обязательно и должно быть строкой", path=f"{path}.text"))
|
errors.append(ParseError("Поле 'question' обязательно и должно быть строкой", path=f"{path}.question"))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
if not text:
|
if not text:
|
||||||
errors.append(ParseError("Текст вопроса не может быть пустым", path=f"{path}.text"))
|
errors.append(ParseError("Текст вопроса не может быть пустым", path=f"{path}.question"))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if len(text) > 2000:
|
if len(text) > 2000:
|
||||||
errors.append(ParseError("Текст вопроса слишком длинный (максимум 2000)", path=f"{path}.text"))
|
errors.append(ParseError("Текст вопроса слишком длинный (максимум 2000)", path=f"{path}.question"))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
question_type = data.get("question_type")
|
question_type = data.get("question_type")
|
||||||
@@ -264,45 +264,45 @@ class TestParser:
|
|||||||
question_type: str,
|
question_type: str,
|
||||||
errors: list[ParseError],
|
errors: list[ParseError],
|
||||||
) -> ParsedQuestion | None:
|
) -> ParsedQuestion | None:
|
||||||
options_data = data.get("options")
|
options_data = data.get("answers")
|
||||||
|
|
||||||
if not options_data or not isinstance(options_data, list):
|
if not options_data or not isinstance(options_data, list):
|
||||||
errors.append(ParseError(
|
errors.append(ParseError(
|
||||||
f"Для типа '{question_type}' поле 'options' обязательно и должно быть массивом",
|
f"Для типа '{question_type}' поле 'answers' обязательно и должно быть массивом",
|
||||||
path=f"{path}.options"
|
path=f"{path}.answers"
|
||||||
))
|
))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if len(options_data) < 2:
|
if len(options_data) < 2:
|
||||||
errors.append(ParseError("Минимум 2 варианта ответа", path=f"{path}.options"))
|
errors.append(ParseError("Минимум 2 варианта ответа", path=f"{path}.answers"))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if len(options_data) > 10:
|
if len(options_data) > 10:
|
||||||
errors.append(ParseError("Максимум 10 вариантов ответа", path=f"{path}.options"))
|
errors.append(ParseError("Максимум 10 вариантов ответа", path=f"{path}.answers"))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
options: list[ParsedOption] = []
|
options: list[ParsedOption] = []
|
||||||
correct_count = 0
|
correct_count = 0
|
||||||
|
|
||||||
for j, opt_data in enumerate(options_data):
|
for j, opt_data in enumerate(options_data):
|
||||||
opt_path = f"{path}.options[{j}]"
|
opt_path = f"{path}.answers[{j}]"
|
||||||
|
|
||||||
if not isinstance(opt_data, dict):
|
if not isinstance(opt_data, dict):
|
||||||
errors.append(ParseError("Вариант ответа должен быть объектом", path=opt_path))
|
errors.append(ParseError("Вариант ответа должен быть объектом", path=opt_path))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
opt_text = opt_data.get("text")
|
opt_text = opt_data.get("option")
|
||||||
if not opt_text or not isinstance(opt_text, str):
|
if not opt_text or not isinstance(opt_text, str):
|
||||||
errors.append(ParseError("Поле 'text' обязательно", path=f"{opt_path}.text"))
|
errors.append(ParseError("Поле 'option' обязательно", path=f"{opt_path}.option"))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
opt_text = opt_text.strip()
|
opt_text = opt_text.strip()
|
||||||
if not opt_text:
|
if not opt_text:
|
||||||
errors.append(ParseError("Текст варианта не может быть пустым", path=f"{opt_path}.text"))
|
errors.append(ParseError("Текст варианта не может быть пустым", path=f"{opt_path}.option"))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(opt_text) > 255:
|
if len(opt_text) > 255:
|
||||||
errors.append(ParseError("Текст варианта слишком длинный (максимум 255)", path=f"{opt_path}.text"))
|
errors.append(ParseError("Текст варианта слишком длинный (максимум 255)", path=f"{opt_path}.option"))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
is_correct = opt_data.get("is_correct")
|
is_correct = opt_data.get("is_correct")
|
||||||
@@ -319,13 +319,13 @@ class TestParser:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if correct_count == 0:
|
if correct_count == 0:
|
||||||
errors.append(ParseError("Должен быть хотя бы один правильный ответ", path=f"{path}.options"))
|
errors.append(ParseError("Должен быть хотя бы один правильный ответ", path=f"{path}.answers"))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if question_type == "single" and correct_count > 1:
|
if question_type == "single" and correct_count > 1:
|
||||||
errors.append(ParseError(
|
errors.append(ParseError(
|
||||||
f"Для типа 'single' должен быть ровно один правильный ответ (найдено {correct_count})",
|
f"Для типа 'single' должен быть ровно один правильный ответ (найдено {correct_count})",
|
||||||
path=f"{path}.options"
|
path=f"{path}.answers"
|
||||||
))
|
))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user