Files
tg_bot_language/bot/handlers/level_test.py

309 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from database.db import async_session_maker
from database.models import LanguageLevel
from services.user_service import UserService
from services.ai_service import ai_service
router = Router()
class LevelTestStates(StatesGroup):
"""Состояния для прохождения теста уровня"""
taking_test = State()
@router.message(Command("level_test"))
async def cmd_level_test(message: Message, state: FSMContext):
"""Обработчик команды /level_test"""
await start_level_test(message, state)
async def start_level_test(message: Message, state: FSMContext):
"""Начать тест определения уровня"""
# Показываем описание теста
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await message.answer(t(lang, 'level_test.intro'))
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'level_test.start_btn'), callback_data="start_test")],
[InlineKeyboardButton(text=t(lang, 'level_test.cancel_btn'), callback_data="cancel_test")]
])
await message.answer(t(lang, 'level_test.press_button'), reply_markup=keyboard)
@router.callback_query(F.data == "cancel_test")
async def cancel_test(callback: CallbackQuery, state: FSMContext):
"""Отменить тест"""
await state.clear()
await callback.message.delete()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await callback.message.answer(t(lang, 'level_test.cancelled'))
await callback.answer()
@router.callback_query(F.data == "start_test")
async def begin_test(callback: CallbackQuery, state: FSMContext):
"""Начать прохождение теста"""
# Сразу отвечаем на callback, чтобы избежать истечения таймаута
await callback.answer()
await callback.message.delete()
# Показываем индикатор загрузки
loading_msg = await callback.message.answer("🔄 Генерирую вопросы...")
# Генерируем тест через AI
questions = await ai_service.generate_level_test()
await loading_msg.delete()
if not questions:
await callback.message.answer(
"Не удалось сгенерировать тест. Попробуй позже или используй /settings для ручной установки уровня."
)
await state.clear()
await callback.answer()
return
# Сохраняем данные в состоянии
await state.update_data(
questions=questions,
current_question=0,
correct_answers=0,
answers=[] # Для отслеживания ответов по уровням
)
await state.set_state(LevelTestStates.taking_test)
# Показываем первый вопрос
await show_question(callback.message, state)
async def show_question(message: Message, state: FSMContext):
"""Показать текущий вопрос"""
data = await state.get_data()
questions = data.get('questions', [])
current_idx = data.get('current_question', 0)
if current_idx >= len(questions):
# Тест завершён
await finish_test(message, state)
return
question = questions[current_idx]
# Формируем текст вопроса
# Язык интерфейса
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
text = (
t(lang, 'level_test.q_header', i=current_idx + 1, n=len(questions)) + "\n\n" +
f"<b>{question['question']}</b>\n"
)
# Создаем кнопки с вариантами ответа
keyboard = []
letters = ['A', 'B', 'C', 'D']
for idx, option in enumerate(question['options']):
keyboard.append([
InlineKeyboardButton(
text=f"{letters[idx]}) {option}",
callback_data=f"answer_{idx}"
)
])
# Кнопка для показа перевода вопроса (локализованная)
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.chat.id)
from utils.i18n import t
lang = (user.language_interface if user else 'ru') or 'ru'
keyboard.append([
InlineKeyboardButton(text=t(lang, 'level_test.show_translation_btn'), callback_data=f"show_qtr_{current_idx}")
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
await message.answer(text, reply_markup=reply_markup)
@router.callback_query(F.data.startswith("show_qtr_"), LevelTestStates.taking_test)
async def show_question_translation(callback: CallbackQuery, state: FSMContext):
"""Показать перевод текущего вопроса"""
try:
idx = int(callback.data.split("_")[-1])
except Exception:
await callback.answer("Перевод недоступен", show_alert=True)
return
data = await state.get_data()
questions = data.get('questions', [])
if not (0 <= idx < len(questions)):
await callback.answer("Перевод недоступен", show_alert=True)
return
ru = questions[idx].get('question_ru') or "Перевод недоступен"
# Вставляем перевод в текущий текст сообщения
orig = callback.message.text or ""
marker = "Перевод вопроса:"
if marker in orig:
await callback.answer("Перевод уже показан")
return
new_text = f"{orig}\n{marker} <i>{ru}</i>"
try:
await callback.message.edit_text(new_text, reply_markup=callback.message.reply_markup)
except Exception:
# Запасной путь, если редактирование невозможно
await callback.message.answer(f"{marker} <i>{ru}</i>")
await callback.answer()
@router.callback_query(F.data.startswith("answer_"), LevelTestStates.taking_test)
async def process_answer(callback: CallbackQuery, state: FSMContext):
"""Обработать ответ на вопрос"""
answer_idx = int(callback.data.split("_")[1])
data = await state.get_data()
questions = data.get('questions', [])
current_idx = data.get('current_question', 0)
correct_answers = data.get('correct_answers', 0)
answers = data.get('answers', [])
question = questions[current_idx]
is_correct = (answer_idx == question['correct'])
# Сохраняем результат
if is_correct:
correct_answers += 1
# Сохраняем ответ с уровнем вопроса
answers.append({
'level': question['level'],
'correct': is_correct
})
# Показываем результат
if is_correct:
result_text = "✅ Правильно!"
else:
correct_option = question['options'][question['correct']]
result_text = f"❌ Неправильно\nПравильный ответ: <b>{correct_option}</b>"
await callback.message.edit_text(
f"❓ <b>Вопрос {current_idx + 1} из {len(questions)}</b>\n\n"
f"{result_text}"
)
# Переходим к следующему вопросу
await state.update_data(
current_question=current_idx + 1,
correct_answers=correct_answers,
answers=answers
)
# Небольшая пауза перед следующим вопросом
import asyncio
await asyncio.sleep(1.5)
await show_question(callback.message, state)
await callback.answer()
async def finish_test(message: Message, state: FSMContext):
"""Завершить тест и определить уровень"""
data = await state.get_data()
questions = data.get('questions', [])
correct_answers = data.get('correct_answers', 0)
answers = data.get('answers', [])
total = len(questions)
accuracy = int((correct_answers / total) * 100) if total > 0 else 0
# Определяем уровень на основе правильных ответов по уровням
level = determine_level(answers)
# Сохраняем уровень в базе данных
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.chat.id)
if user:
user.level = level
await session.commit()
# Описания уровней
level_descriptions = {
"A1": "Начальный - понимаешь основные фразы и можешь представиться",
"A2": "Элементарный - можешь общаться на простые темы",
"B1": "Средний - можешь поддержать беседу на знакомые темы",
"B2": "Выше среднего - свободно общаешься в большинстве ситуаций",
"C1": "Продвинутый - используешь язык гибко и эффективно",
"C2": "Профессиональный - владеешь языком на уровне носителя"
}
await state.clear()
result_text = (
f"🎉 <b>Тест завершён!</b>\n\n"
f"📊 Результаты:\n"
f"Правильных ответов: <b>{correct_answers}</b> из {total}\n"
f"Точность: <b>{accuracy}%</b>\n\n"
f"🎯 Твой уровень: <b>{level.value}</b>\n"
f"<i>{level_descriptions.get(level.value, '')}</i>\n\n"
f"Теперь задания и материалы будут подбираться под твой уровень!\n"
f"Ты можешь изменить уровень в любое время через /settings"
)
await message.answer(result_text)
def determine_level(answers: list) -> LanguageLevel:
"""
Определить уровень на основе ответов
Args:
answers: Список ответов с уровнями
Returns:
Определённый уровень
"""
# Подсчитываем правильные ответы по уровням
level_stats = {
'A1': {'correct': 0, 'total': 0},
'A2': {'correct': 0, 'total': 0},
'B1': {'correct': 0, 'total': 0},
'B2': {'correct': 0, 'total': 0},
'C1': {'correct': 0, 'total': 0},
'C2': {'correct': 0, 'total': 0}
}
for answer in answers:
level = answer['level']
if level in level_stats:
level_stats[level]['total'] += 1
if answer['correct']:
level_stats[level]['correct'] += 1
# Определяем уровень: ищем последний уровень, где правильно >= 50%
levels_order = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
determined_level = 'A1'
for level in levels_order:
if level_stats[level]['total'] > 0:
accuracy = level_stats[level]['correct'] / level_stats[level]['total']
if accuracy >= 0.5: # 50% и выше
determined_level = level
else:
# Если не прошёл этот уровень, останавливаемся
break
return LanguageLevel[determined_level]