This commit is contained in:
@@ -12,10 +12,7 @@ trigger:
|
|||||||
- push
|
- push
|
||||||
- pull_request
|
- pull_request
|
||||||
|
|
||||||
# Глобальные переменные
|
# Примечание: Глобальные переменные определяются в шагах
|
||||||
environment:
|
|
||||||
IMAGE_NAME: quiz-bot
|
|
||||||
REGISTRY: localhost:5000 # Локальный registry или замените на ваш
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# 1. Клонирование и подготовка
|
# 1. Клонирование и подготовка
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
|||||||
# Production stage
|
# Production stage
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
# Создаем пользователя для безопасности
|
# Создание пользователя и группы для безопасности
|
||||||
RUN groupadd -r quizbot && useradd -r -g quizbot quizbot
|
RUN groupadd -r quizbot && useradd -r -g quizbot quizbot
|
||||||
|
|
||||||
# Устанавливаем системные зависимости
|
# Установка sqlite3 для работы с базой данных
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
@@ -34,9 +34,10 @@ ENV PATH="/opt/venv/bin:$PATH"
|
|||||||
# Создаем рабочую директорию
|
# Создаем рабочую директорию
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Создаем необходимые директории
|
# Создание директорий с правильными правами доступа
|
||||||
RUN mkdir -p /app/data /app/logs && \
|
RUN mkdir -p /app/data /app/logs && \
|
||||||
chown -R quizbot:quizbot /app
|
chown -R quizbot:quizbot /app && \
|
||||||
|
chmod -R 775 /app/data /app/logs
|
||||||
|
|
||||||
# Копируем код приложения
|
# Копируем код приложения
|
||||||
COPY --chown=quizbot:quizbot . .
|
COPY --chown=quizbot:quizbot . .
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import os
|
import os
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
def get_admin_ids() -> List[int]:
|
def get_admin_ids() -> List[int]:
|
||||||
admin_str = os.getenv("ADMIN_IDS", "")
|
admin_str = os.getenv("ADMIN_IDS", "")
|
||||||
if admin_str:
|
if admin_str:
|
||||||
return [int(x) for x in admin_str.split(",") if x.strip()]
|
return [int(x) for x in admin_str.split(",") if x.strip()]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
class Config:
|
||||||
bot_token: str = os.getenv("BOT_TOKEN", "")
|
bot_token: str = os.getenv("BOT_TOKEN", "")
|
||||||
@@ -26,4 +29,5 @@ class Config:
|
|||||||
guest_mode_enabled: bool = os.getenv("GUEST_MODE_ENABLED", "true").lower() == "true"
|
guest_mode_enabled: bool = os.getenv("GUEST_MODE_ENABLED", "true").lower() == "true"
|
||||||
test_mode_enabled: bool = os.getenv("TEST_MODE_ENABLED", "true").lower() == "true"
|
test_mode_enabled: bool = os.getenv("TEST_MODE_ENABLED", "true").lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
config = Config()
|
config = Config()
|
||||||
|
|||||||
0
data/korean_level_1.csv
Normal file → Executable file
0
data/korean_level_1.csv
Normal file → Executable file
0
data/korean_level_2.csv
Normal file → Executable file
0
data/korean_level_2.csv
Normal file → Executable file
0
data/korean_level_3.csv
Normal file → Executable file
0
data/korean_level_3.csv
Normal file → Executable file
0
data/korean_level_4.csv
Normal file → Executable file
0
data/korean_level_4.csv
Normal file → Executable file
0
data/korean_level_5.csv
Normal file → Executable file
0
data/korean_level_5.csv
Normal file → Executable file
BIN
data/quiz_bot.db
Normal file → Executable file
BIN
data/quiz_bot.db
Normal file → Executable file
Binary file not shown.
@@ -7,15 +7,15 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: quiz-bot
|
container_name: quiz-bot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
user: "0:0"
|
||||||
environment:
|
environment:
|
||||||
- BOT_TOKEN=${BOT_TOKEN}
|
- BOT_TOKEN=${BOT_TOKEN}
|
||||||
- DATABASE_PATH=data/quiz_bot.db
|
- DATABASE_PATH=data/quiz_bot.db
|
||||||
- CSV_DATA_PATH=data/
|
- CSV_DATA_PATH=data/
|
||||||
- LOG_LEVEL=INFO
|
- LOG_LEVEL=INFO
|
||||||
volumes:
|
volumes:
|
||||||
# Персистентное хранение данных
|
- "./data:/app/data"
|
||||||
- ./data:/app/data
|
- "./logs:/app/logs"
|
||||||
- ./logs:/app/logs
|
|
||||||
networks:
|
networks:
|
||||||
- quiz-bot-network
|
- quiz-bot-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -24,7 +24,12 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 60s
|
start_period: 60s
|
||||||
# Ограничения ресурсов
|
command: >
|
||||||
|
sh -c "
|
||||||
|
chown -R quizbot:quizbot /app/data /app/logs &&
|
||||||
|
chmod -R 775 /app/data /app/logs &&
|
||||||
|
python -m src.bot
|
||||||
|
"
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|||||||
@@ -5,3 +5,14 @@ pandas==2.1.4
|
|||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
asyncio-mqtt==0.16.1
|
asyncio-mqtt==0.16.1
|
||||||
loguru==0.7.2
|
loguru==0.7.2
|
||||||
|
pytest==7.4.0
|
||||||
|
pytest-asyncio==0.22.0
|
||||||
|
black
|
||||||
|
isort
|
||||||
|
flake8
|
||||||
|
mypy
|
||||||
|
pytest
|
||||||
|
pytest-asyncio
|
||||||
|
pytest-cov
|
||||||
|
safety
|
||||||
|
bandit
|
||||||
454
src/bot.py
454
src/bot.py
@@ -5,16 +5,18 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
|
import sys
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher, F
|
from aiogram import Bot, Dispatcher, F
|
||||||
from aiogram.filters import Command, StateFilter
|
from aiogram.filters import Command, StateFilter
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.fsm.state import State, StatesGroup
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
from aiogram.fsm.storage.memory import MemoryStorage
|
from aiogram.fsm.storage.memory import MemoryStorage
|
||||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, InaccessibleMessage
|
from aiogram.types import (CallbackQuery, InaccessibleMessage,
|
||||||
|
InlineKeyboardButton, InlineKeyboardMarkup, Message)
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Добавляем путь к проекту
|
# Добавляем путь к проекту
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
@@ -27,12 +29,14 @@ from src.services.csv_service import CSVQuizLoader, QuizGenerator
|
|||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
class QuizStates(StatesGroup):
|
class QuizStates(StatesGroup):
|
||||||
choosing_mode = State()
|
choosing_mode = State()
|
||||||
choosing_category = State()
|
choosing_category = State()
|
||||||
choosing_level = State()
|
choosing_level = State()
|
||||||
in_quiz = State()
|
in_quiz = State()
|
||||||
|
|
||||||
|
|
||||||
class QuizBot:
|
class QuizBot:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bot = Bot(token=config.bot_token)
|
self.bot = Bot(token=config.bot_token)
|
||||||
@@ -71,16 +75,26 @@ class QuizBot:
|
|||||||
username=user.username,
|
username=user.username,
|
||||||
first_name=user.first_name,
|
first_name=user.first_name,
|
||||||
last_name=user.last_name,
|
last_name=user.last_name,
|
||||||
language_code=user.language_code or 'ru'
|
language_code=user.language_code or "ru",
|
||||||
)
|
)
|
||||||
|
|
||||||
await state.set_state(QuizStates.choosing_mode)
|
await state.set_state(QuizStates.choosing_mode)
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
keyboard = InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="🎯 Гостевой режим (QUIZ)", callback_data="guest_mode")],
|
inline_keyboard=[
|
||||||
[InlineKeyboardButton(text="📚 Тестирование по материалам", callback_data="test_mode")],
|
[
|
||||||
[InlineKeyboardButton(text="📊 Моя статистика", callback_data="stats")],
|
InlineKeyboardButton(
|
||||||
])
|
text="🎯 Гостевой режим (QUIZ)", callback_data="guest_mode"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="📚 Тестирование по материалам", callback_data="test_mode"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[InlineKeyboardButton(text="📊 Моя статистика", callback_data="stats")],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
f"👋 Добро пожаловать в Quiz Bot, {user.first_name}!\n\n"
|
f"👋 Добро пожаловать в Quiz Bot, {user.first_name}!\n\n"
|
||||||
@@ -88,7 +102,7 @@ class QuizBot:
|
|||||||
"📚 <b>Тестирование</b> - серьезное изучение материалов с результатами\n\n"
|
"📚 <b>Тестирование</b> - серьезное изучение материалов с результатами\n\n"
|
||||||
"Выберите режим работы:",
|
"Выберите режим работы:",
|
||||||
reply_markup=keyboard,
|
reply_markup=keyboard,
|
||||||
parse_mode='HTML'
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def help_command(self, message: Message):
|
async def help_command(self, message: Message):
|
||||||
@@ -115,17 +129,21 @@ class QuizBot:
|
|||||||
📊 <b>Доступные категории:</b>
|
📊 <b>Доступные категории:</b>
|
||||||
• Корейский язык (уровни 1-5)
|
• Корейский язык (уровни 1-5)
|
||||||
• Более 120 уникальных вопросов"""
|
• Более 120 уникальных вопросов"""
|
||||||
await message.answer(help_text, parse_mode='HTML')
|
await message.answer(help_text, parse_mode="HTML")
|
||||||
|
|
||||||
async def stats_command(self, message: Message):
|
async def stats_command(self, message: Message):
|
||||||
"""Обработка команды /stats"""
|
"""Обработка команды /stats"""
|
||||||
user_stats = await self.db.get_user_stats(message.from_user.id)
|
user_stats = await self.db.get_user_stats(message.from_user.id)
|
||||||
|
|
||||||
if not user_stats or user_stats['total_questions'] == 0:
|
if not user_stats or user_stats["total_questions"] == 0:
|
||||||
await message.answer("📊 У вас пока нет статистики. Пройдите первый тест!")
|
await message.answer("📊 У вас пока нет статистики. Пройдите первый тест!")
|
||||||
return
|
return
|
||||||
|
|
||||||
accuracy = (user_stats['correct_answers'] / user_stats['total_questions']) * 100 if user_stats['total_questions'] > 0 else 0
|
accuracy = (
|
||||||
|
(user_stats["correct_answers"] / user_stats["total_questions"]) * 100
|
||||||
|
if user_stats["total_questions"] > 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
stats_text = f"""📊 <b>Ваша статистика:</b>
|
stats_text = f"""📊 <b>Ваша статистика:</b>
|
||||||
|
|
||||||
@@ -136,11 +154,17 @@ class QuizBot:
|
|||||||
🏆 Лучший результат: {user_stats['best_score'] or 0:.1f}%
|
🏆 Лучший результат: {user_stats['best_score'] or 0:.1f}%
|
||||||
📊 Средний балл: {user_stats['average_score'] or 0:.1f}%"""
|
📊 Средний балл: {user_stats['average_score'] or 0:.1f}%"""
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
keyboard = InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_menu")]
|
inline_keyboard=[
|
||||||
])
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🏠 Главное меню", callback_data="back_to_menu"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
await message.answer(stats_text, reply_markup=keyboard, parse_mode='HTML')
|
await message.answer(stats_text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
async def stop_command(self, message: Message):
|
async def stop_command(self, message: Message):
|
||||||
"""Остановка текущего теста"""
|
"""Остановка текущего теста"""
|
||||||
@@ -151,42 +175,60 @@ class QuizBot:
|
|||||||
else:
|
else:
|
||||||
await message.answer("❌ У вас нет активного теста.")
|
await message.answer("❌ У вас нет активного теста.")
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
keyboard = InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_menu")]
|
inline_keyboard=[
|
||||||
])
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🏠 Главное меню", callback_data="back_to_menu"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
await message.answer("Что бы вы хотели сделать дальше?", reply_markup=keyboard)
|
await message.answer("Что бы вы хотели сделать дальше?", reply_markup=keyboard)
|
||||||
|
|
||||||
async def guest_mode_handler(self, callback: CallbackQuery, state: FSMContext):
|
async def guest_mode_handler(self, callback: CallbackQuery, state: FSMContext):
|
||||||
"""Обработка выбора гостевого режима"""
|
"""Обработка выбора гостевого режима"""
|
||||||
await state.update_data(mode='guest')
|
await state.update_data(mode="guest")
|
||||||
await state.set_state(QuizStates.choosing_category)
|
await state.set_state(QuizStates.choosing_category)
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
keyboard = InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="🇰🇷 Корейский язык", callback_data="category_korean")],
|
inline_keyboard=[
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_menu")]
|
[
|
||||||
])
|
InlineKeyboardButton(
|
||||||
|
text="🇰🇷 Корейский язык", callback_data="category_korean"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_menu")],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
"🎯 <b>Гостевой режим</b>\n\nВыберите категорию для викторины:",
|
"🎯 <b>Гостевой режим</b>\n\nВыберите категорию для викторины:",
|
||||||
reply_markup=keyboard,
|
reply_markup=keyboard,
|
||||||
parse_mode='HTML'
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
async def test_mode_handler(self, callback: CallbackQuery, state: FSMContext):
|
async def test_mode_handler(self, callback: CallbackQuery, state: FSMContext):
|
||||||
"""Обработка выбора режима тестирования"""
|
"""Обработка выбора режима тестирования"""
|
||||||
await state.update_data(mode='test')
|
await state.update_data(mode="test")
|
||||||
await state.set_state(QuizStates.choosing_category)
|
await state.set_state(QuizStates.choosing_category)
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
keyboard = InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="🇰🇷 Корейский язык", callback_data="category_korean")],
|
inline_keyboard=[
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_menu")]
|
[
|
||||||
])
|
InlineKeyboardButton(
|
||||||
|
text="🇰🇷 Корейский язык", callback_data="category_korean"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_menu")],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
"📚 <b>Режим тестирования</b>\n\nВыберите категорию для серьезного изучения:",
|
"📚 <b>Режим тестирования</b>\n\nВыберите категорию для серьезного изучения:",
|
||||||
reply_markup=keyboard,
|
reply_markup=keyboard,
|
||||||
parse_mode='HTML'
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
@@ -195,19 +237,41 @@ class QuizBot:
|
|||||||
category = callback.data.split("_")[1]
|
category = callback.data.split("_")[1]
|
||||||
await state.update_data(category=category)
|
await state.update_data(category=category)
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
keyboard = InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="🥉 Уровень 1 (начальный)", callback_data="level_1")],
|
inline_keyboard=[
|
||||||
[InlineKeyboardButton(text="🥈 Уровень 2 (базовый)", callback_data="level_2")],
|
[
|
||||||
[InlineKeyboardButton(text="🥇 Уровень 3 (средний)", callback_data="level_3")],
|
InlineKeyboardButton(
|
||||||
[InlineKeyboardButton(text="🏆 Уровень 4 (продвинутый)", callback_data="level_4")],
|
text="🥉 Уровень 1 (начальный)", callback_data="level_1"
|
||||||
[InlineKeyboardButton(text="💎 Уровень 5 (эксперт)", callback_data="level_5")],
|
)
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_menu")]
|
],
|
||||||
])
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🥈 Уровень 2 (базовый)", callback_data="level_2"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🥇 Уровень 3 (средний)", callback_data="level_3"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🏆 Уровень 4 (продвинутый)", callback_data="level_4"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="💎 Уровень 5 (эксперт)", callback_data="level_5"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_menu")],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
f"🇰🇷 <b>Корейский язык</b>\n\nВыберите уровень сложности:",
|
f"🇰🇷 <b>Корейский язык</b>\n\nВыберите уровень сложности:",
|
||||||
reply_markup=keyboard,
|
reply_markup=keyboard,
|
||||||
parse_mode='HTML'
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
@@ -223,26 +287,34 @@ class QuizBot:
|
|||||||
if not questions:
|
if not questions:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
"❌ Вопросы для этого уровня пока недоступны.",
|
"❌ Вопросы для этого уровня пока недоступны.",
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
reply_markup=InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_menu")]
|
inline_keyboard=[
|
||||||
])
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🔙 Назад", callback_data="back_to_menu"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
),
|
||||||
)
|
)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Определяем количество вопросов
|
# Определяем количество вопросов
|
||||||
questions_count = 5 if data['mode'] == 'guest' else 10
|
questions_count = 5 if data["mode"] == "guest" else 10
|
||||||
|
|
||||||
# Берем случайные вопросы
|
# Берем случайные вопросы
|
||||||
selected_questions = random.sample(questions, min(questions_count, len(questions)))
|
selected_questions = random.sample(
|
||||||
|
questions, min(questions_count, len(questions))
|
||||||
|
)
|
||||||
|
|
||||||
# Создаем тестовую запись в БД
|
# Создаем тестовую запись в БД
|
||||||
test_id = await self.db.add_test(
|
test_id = await self.db.add_test(
|
||||||
name=f"{data['category'].title()} Level {level}",
|
name=f"{data['category'].title()} Level {level}",
|
||||||
description=f"Тест по {data['category']} языку, уровень {level}",
|
description=f"Тест по {data['category']} языку, уровень {level}",
|
||||||
level=level,
|
level=level,
|
||||||
category=data['category'],
|
category=data["category"],
|
||||||
csv_file=filename
|
csv_file=filename,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Начинаем сессию
|
# Начинаем сессию
|
||||||
@@ -250,7 +322,7 @@ class QuizBot:
|
|||||||
user_id=callback.from_user.id,
|
user_id=callback.from_user.id,
|
||||||
test_id=test_id or 1,
|
test_id=test_id or 1,
|
||||||
questions=selected_questions,
|
questions=selected_questions,
|
||||||
mode=data['mode']
|
mode=data["mode"],
|
||||||
)
|
)
|
||||||
|
|
||||||
await state.set_state(QuizStates.in_quiz)
|
await state.set_state(QuizStates.in_quiz)
|
||||||
@@ -260,13 +332,13 @@ class QuizBot:
|
|||||||
def shuffle_answers(self, question_data: dict) -> dict:
|
def shuffle_answers(self, question_data: dict) -> dict:
|
||||||
"""Перемешивает варианты ответов и обновляет правильный ответ"""
|
"""Перемешивает варианты ответов и обновляет правильный ответ"""
|
||||||
options = [
|
options = [
|
||||||
question_data['option1'],
|
question_data["option1"],
|
||||||
question_data['option2'],
|
question_data["option2"],
|
||||||
question_data['option3'],
|
question_data["option3"],
|
||||||
question_data['option4']
|
question_data["option4"],
|
||||||
]
|
]
|
||||||
|
|
||||||
correct_answer_text = options[question_data['correct_answer'] - 1]
|
correct_answer_text = options[question_data["correct_answer"] - 1]
|
||||||
|
|
||||||
# Перемешиваем варианты
|
# Перемешиваем варианты
|
||||||
random.shuffle(options)
|
random.shuffle(options)
|
||||||
@@ -276,37 +348,40 @@ class QuizBot:
|
|||||||
|
|
||||||
# Обновляем данные вопроса
|
# Обновляем данные вопроса
|
||||||
shuffled_question = question_data.copy()
|
shuffled_question = question_data.copy()
|
||||||
shuffled_question['option1'] = options[0]
|
shuffled_question["option1"] = options[0]
|
||||||
shuffled_question['option2'] = options[1]
|
shuffled_question["option2"] = options[1]
|
||||||
shuffled_question['option3'] = options[2]
|
shuffled_question["option3"] = options[2]
|
||||||
shuffled_question['option4'] = options[3]
|
shuffled_question["option4"] = options[3]
|
||||||
shuffled_question['correct_answer'] = new_correct_position
|
shuffled_question["correct_answer"] = new_correct_position
|
||||||
|
|
||||||
return shuffled_question
|
return shuffled_question
|
||||||
|
|
||||||
async def show_question_safe(self, callback: CallbackQuery, user_id: int, question_index: int):
|
async def show_question_safe(
|
||||||
|
self, callback: CallbackQuery, user_id: int, question_index: int
|
||||||
|
):
|
||||||
"""Безопасный показ вопроса через callback"""
|
"""Безопасный показ вопроса через callback"""
|
||||||
session = await self.db.get_active_session(user_id)
|
session = await self.db.get_active_session(user_id)
|
||||||
if not session or question_index >= len(session['questions_data']):
|
if not session or question_index >= len(session["questions_data"]):
|
||||||
return
|
return
|
||||||
|
|
||||||
question = session['questions_data'][question_index]
|
question = session["questions_data"][question_index]
|
||||||
|
|
||||||
# Перемешиваем варианты ответов только в тестовом режиме
|
# Перемешиваем варианты ответов только в тестовом режиме
|
||||||
if session['mode'] == 'test':
|
if session["mode"] == "test":
|
||||||
question = self.shuffle_answers(question)
|
question = self.shuffle_answers(question)
|
||||||
session['questions_data'][question_index] = question
|
session["questions_data"][question_index] = question
|
||||||
await self.db.update_session_questions(user_id, session['questions_data'])
|
await self.db.update_session_questions(user_id, session["questions_data"])
|
||||||
|
|
||||||
total_questions = len(session['questions_data'])
|
total_questions = len(session["questions_data"])
|
||||||
|
|
||||||
# Создаем клавиатуру с ответами
|
# Создаем клавиатуру с ответами
|
||||||
keyboard_builder = InlineKeyboardBuilder()
|
keyboard_builder = InlineKeyboardBuilder()
|
||||||
for i in range(1, 5):
|
for i in range(1, 5):
|
||||||
keyboard_builder.add(InlineKeyboardButton(
|
keyboard_builder.add(
|
||||||
text=f"{i}. {question[f'option{i}']}",
|
InlineKeyboardButton(
|
||||||
callback_data=f"answer_{i}"
|
text=f"{i}. {question[f'option{i}']}", callback_data=f"answer_{i}"
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
keyboard_builder.adjust(1)
|
keyboard_builder.adjust(1)
|
||||||
|
|
||||||
@@ -318,11 +393,25 @@ class QuizBot:
|
|||||||
# Безопасная отправка сообщения
|
# Безопасная отправка сообщения
|
||||||
if callback.message and not isinstance(callback.message, InaccessibleMessage):
|
if callback.message and not isinstance(callback.message, InaccessibleMessage):
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(question_text, reply_markup=keyboard_builder.as_markup(), parse_mode='HTML')
|
await callback.message.edit_text(
|
||||||
|
question_text,
|
||||||
|
reply_markup=keyboard_builder.as_markup(),
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
await self.bot.send_message(callback.from_user.id, question_text, reply_markup=keyboard_builder.as_markup(), parse_mode='HTML')
|
await self.bot.send_message(
|
||||||
|
callback.from_user.id,
|
||||||
|
question_text,
|
||||||
|
reply_markup=keyboard_builder.as_markup(),
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.bot.send_message(callback.from_user.id, question_text, reply_markup=keyboard_builder.as_markup(), parse_mode='HTML')
|
await self.bot.send_message(
|
||||||
|
callback.from_user.id,
|
||||||
|
question_text,
|
||||||
|
reply_markup=keyboard_builder.as_markup(),
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
|
||||||
async def answer_handler(self, callback: CallbackQuery, state: FSMContext):
|
async def answer_handler(self, callback: CallbackQuery, state: FSMContext):
|
||||||
"""Обработка ответа на вопрос"""
|
"""Обработка ответа на вопрос"""
|
||||||
@@ -334,33 +423,43 @@ class QuizBot:
|
|||||||
await callback.answer("❌ Сессия не найдена")
|
await callback.answer("❌ Сессия не найдена")
|
||||||
return
|
return
|
||||||
|
|
||||||
current_q_index = session['current_question']
|
current_q_index = session["current_question"]
|
||||||
question = session['questions_data'][current_q_index]
|
question = session["questions_data"][current_q_index]
|
||||||
is_correct = answer == question['correct_answer']
|
is_correct = answer == question["correct_answer"]
|
||||||
mode = session['mode']
|
mode = session["mode"]
|
||||||
|
|
||||||
# Обновляем счетчик правильных ответов
|
# Обновляем счетчик правильных ответов
|
||||||
if is_correct:
|
if is_correct:
|
||||||
session['correct_count'] += 1
|
session["correct_count"] += 1
|
||||||
|
|
||||||
# Обновляем прогресс в базе
|
# Обновляем прогресс в базе
|
||||||
await self.db.update_session_progress(
|
await self.db.update_session_progress(
|
||||||
user_id, current_q_index + 1, session['correct_count']
|
user_id, current_q_index + 1, session["correct_count"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Проверяем, есть ли еще вопросы
|
# Проверяем, есть ли еще вопросы
|
||||||
if current_q_index + 1 >= len(session['questions_data']):
|
if current_q_index + 1 >= len(session["questions_data"]):
|
||||||
# Тест завершен
|
# Тест завершен
|
||||||
score = (session['correct_count'] / len(session['questions_data'])) * 100
|
score = (session["correct_count"] / len(session["questions_data"])) * 100
|
||||||
await self.db.finish_session(user_id, score)
|
await self.db.finish_session(user_id, score)
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
keyboard = InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_menu")],
|
inline_keyboard=[
|
||||||
[InlineKeyboardButton(text="📊 Моя статистика", callback_data="stats")]
|
[
|
||||||
])
|
InlineKeyboardButton(
|
||||||
|
text="🏠 Главное меню", callback_data="back_to_menu"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="📊 Моя статистика", callback_data="stats"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Разный текст для разных режимов
|
# Разный текст для разных режимов
|
||||||
if mode == 'test':
|
if mode == "test":
|
||||||
final_text = (
|
final_text = (
|
||||||
f"🎉 <b>Тест завершен!</b>\n\n"
|
f"🎉 <b>Тест завершен!</b>\n\n"
|
||||||
f"📊 Результат: {session['correct_count']}/{len(session['questions_data'])}\n"
|
f"📊 Результат: {session['correct_count']}/{len(session['questions_data'])}\n"
|
||||||
@@ -369,7 +468,11 @@ class QuizBot:
|
|||||||
f"💡 Результат сохранен в вашей статистике"
|
f"💡 Результат сохранен в вашей статистике"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result_text = "✅ Правильно!" if is_correct else f"❌ Неправильно. Правильный ответ: {question['correct_answer']}"
|
result_text = (
|
||||||
|
"✅ Правильно!"
|
||||||
|
if is_correct
|
||||||
|
else f"❌ Неправильно. Правильный ответ: {question['correct_answer']}"
|
||||||
|
)
|
||||||
final_text = (
|
final_text = (
|
||||||
f"{result_text}\n\n"
|
f"{result_text}\n\n"
|
||||||
f"🎉 <b>Викторина завершена!</b>\n\n"
|
f"🎉 <b>Викторина завершена!</b>\n\n"
|
||||||
@@ -379,34 +482,68 @@ class QuizBot:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Безопасная отправка сообщения
|
# Безопасная отправка сообщения
|
||||||
if callback.message and not isinstance(callback.message, InaccessibleMessage):
|
if callback.message and not isinstance(
|
||||||
|
callback.message, InaccessibleMessage
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(final_text, reply_markup=keyboard, parse_mode='HTML')
|
await callback.message.edit_text(
|
||||||
|
final_text, reply_markup=keyboard, parse_mode="HTML"
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
await self.bot.send_message(callback.from_user.id, final_text, reply_markup=keyboard, parse_mode='HTML')
|
await self.bot.send_message(
|
||||||
|
callback.from_user.id,
|
||||||
|
final_text,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.bot.send_message(callback.from_user.id, final_text, reply_markup=keyboard, parse_mode='HTML')
|
await self.bot.send_message(
|
||||||
|
callback.from_user.id,
|
||||||
|
final_text,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Есть еще вопросы
|
# Есть еще вопросы
|
||||||
if mode == 'test':
|
if mode == "test":
|
||||||
# В тестовом режиме сразу переходим к следующему вопросу
|
# В тестовом режиме сразу переходим к следующему вопросу
|
||||||
await self.show_question_safe(callback, callback.from_user.id, current_q_index + 1)
|
await self.show_question_safe(
|
||||||
|
callback, callback.from_user.id, current_q_index + 1
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# В гостевом режиме показываем результат и кнопку "Следующий"
|
# В гостевом режиме показываем результат и кнопку "Следующий"
|
||||||
result_text = "✅ Правильно!" if is_correct else f"❌ Неправильно. Правильный ответ: {question['correct_answer']}"
|
result_text = (
|
||||||
|
"✅ Правильно!"
|
||||||
|
if is_correct
|
||||||
|
else f"❌ Неправильно. Правильный ответ: {question['correct_answer']}"
|
||||||
|
)
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
keyboard = InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="➡️ Следующий вопрос", callback_data="next_question")]
|
inline_keyboard=[
|
||||||
])
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="➡️ Следующий вопрос", callback_data="next_question"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Безопасная отправка сообщения
|
# Безопасная отправка сообщения
|
||||||
if callback.message and not isinstance(callback.message, InaccessibleMessage):
|
if callback.message and not isinstance(
|
||||||
|
callback.message, InaccessibleMessage
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(result_text, reply_markup=keyboard)
|
await callback.message.edit_text(
|
||||||
|
result_text, reply_markup=keyboard
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
await self.bot.send_message(callback.from_user.id, result_text, reply_markup=keyboard)
|
await self.bot.send_message(
|
||||||
|
callback.from_user.id, result_text, reply_markup=keyboard
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.bot.send_message(callback.from_user.id, result_text, reply_markup=keyboard)
|
await self.bot.send_message(
|
||||||
|
callback.from_user.id, result_text, reply_markup=keyboard
|
||||||
|
)
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
@@ -414,24 +551,30 @@ class QuizBot:
|
|||||||
"""Переход к следующему вопросу"""
|
"""Переход к следующему вопросу"""
|
||||||
session = await self.db.get_active_session(callback.from_user.id)
|
session = await self.db.get_active_session(callback.from_user.id)
|
||||||
if session:
|
if session:
|
||||||
await self.show_question_safe(callback, callback.from_user.id, session['current_question'])
|
await self.show_question_safe(
|
||||||
|
callback, callback.from_user.id, session["current_question"]
|
||||||
|
)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
async def stats_callback_handler(self, callback: CallbackQuery):
|
async def stats_callback_handler(self, callback: CallbackQuery):
|
||||||
"""Обработчик кнопки статистики через callback"""
|
"""Обработчик кнопки статистики через callback"""
|
||||||
user_stats = await self.db.get_user_stats(callback.from_user.id)
|
user_stats = await self.db.get_user_stats(callback.from_user.id)
|
||||||
|
|
||||||
if not user_stats or user_stats['total_questions'] == 0:
|
if not user_stats or user_stats["total_questions"] == 0:
|
||||||
stats_text = "📊 У вас пока нет статистики. Пройдите первый тест!"
|
stats_text = "📊 У вас пока нет статистики. Пройдите первый тест!"
|
||||||
else:
|
else:
|
||||||
accuracy = (user_stats['correct_answers'] / user_stats['total_questions']) * 100 if user_stats['total_questions'] > 0 else 0
|
accuracy = (
|
||||||
|
(user_stats["correct_answers"] / user_stats["total_questions"]) * 100
|
||||||
|
if user_stats["total_questions"] > 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
# Получаем дополнительную статистику
|
# Получаем дополнительную статистику
|
||||||
recent_results = await self.db.get_recent_results(callback.from_user.id, 3)
|
recent_results = await self.db.get_recent_results(callback.from_user.id, 3)
|
||||||
category_stats = await self.db.get_category_stats(callback.from_user.id)
|
category_stats = await self.db.get_category_stats(callback.from_user.id)
|
||||||
|
|
||||||
best_score = user_stats['best_score'] or 0
|
best_score = user_stats["best_score"] or 0
|
||||||
avg_score = user_stats['average_score'] or 0
|
avg_score = user_stats["average_score"] or 0
|
||||||
|
|
||||||
stats_text = f"""📊 <b>Ваша статистика:</b>
|
stats_text = f"""📊 <b>Ваша статистика:</b>
|
||||||
|
|
||||||
@@ -451,29 +594,56 @@ class QuizBot:
|
|||||||
if category_stats:
|
if category_stats:
|
||||||
stats_text += "\n\n🏷️ <b>По категориям:</b>"
|
stats_text += "\n\n🏷️ <b>По категориям:</b>"
|
||||||
for cat_stat in category_stats[:2]:
|
for cat_stat in category_stats[:2]:
|
||||||
cat_accuracy = (cat_stat['correct_answers'] / cat_stat['total_questions']) * 100 if cat_stat['total_questions'] > 0 else 0
|
cat_accuracy = (
|
||||||
|
(cat_stat["correct_answers"] / cat_stat["total_questions"])
|
||||||
|
* 100
|
||||||
|
if cat_stat["total_questions"] > 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
stats_text += f"\n📖 {cat_stat['category']}: {cat_stat['attempts']} попыток, {cat_accuracy:.1f}% точность"
|
stats_text += f"\n📖 {cat_stat['category']}: {cat_stat['attempts']} попыток, {cat_accuracy:.1f}% точность"
|
||||||
|
|
||||||
# Добавляем последние результаты
|
# Добавляем последние результаты
|
||||||
if recent_results:
|
if recent_results:
|
||||||
stats_text += "\n\n📈 <b>Последние результаты:</b>"
|
stats_text += "\n\n📈 <b>Последние результаты:</b>"
|
||||||
for result in recent_results:
|
for result in recent_results:
|
||||||
mode_emoji = "🎯" if result['mode'] == 'guest' else "📚"
|
mode_emoji = "🎯" if result["mode"] == "guest" else "📚"
|
||||||
stats_text += f"\n{mode_emoji} {result['score']:.1f}% ({result['correct_answers']}/{result['questions_asked']})"
|
stats_text += f"\n{mode_emoji} {result['score']:.1f}% ({result['correct_answers']}/{result['questions_asked']})"
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
keyboard = InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_menu")],
|
inline_keyboard=[
|
||||||
[InlineKeyboardButton(text="🔄 Обновить статистику", callback_data="stats")]
|
[
|
||||||
])
|
InlineKeyboardButton(
|
||||||
|
text="🏠 Главное меню", callback_data="back_to_menu"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🔄 Обновить статистику", callback_data="stats"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Безопасная отправка сообщения
|
# Безопасная отправка сообщения
|
||||||
if callback.message and not isinstance(callback.message, InaccessibleMessage):
|
if callback.message and not isinstance(callback.message, InaccessibleMessage):
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(stats_text, reply_markup=keyboard, parse_mode='HTML')
|
await callback.message.edit_text(
|
||||||
|
stats_text, reply_markup=keyboard, parse_mode="HTML"
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
await self.bot.send_message(callback.from_user.id, stats_text, reply_markup=keyboard, parse_mode='HTML')
|
await self.bot.send_message(
|
||||||
|
callback.from_user.id,
|
||||||
|
stats_text,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.bot.send_message(callback.from_user.id, stats_text, reply_markup=keyboard, parse_mode='HTML')
|
await self.bot.send_message(
|
||||||
|
callback.from_user.id,
|
||||||
|
stats_text,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
async def back_to_menu(self, callback: CallbackQuery, state: FSMContext):
|
async def back_to_menu(self, callback: CallbackQuery, state: FSMContext):
|
||||||
@@ -488,29 +658,50 @@ class QuizBot:
|
|||||||
username=user.username,
|
username=user.username,
|
||||||
first_name=user.first_name,
|
first_name=user.first_name,
|
||||||
last_name=user.last_name,
|
last_name=user.last_name,
|
||||||
language_code=user.language_code or 'ru'
|
language_code=user.language_code or "ru",
|
||||||
)
|
)
|
||||||
|
|
||||||
await state.set_state(QuizStates.choosing_mode)
|
await state.set_state(QuizStates.choosing_mode)
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
keyboard = InlineKeyboardMarkup(
|
||||||
[InlineKeyboardButton(text="🎯 Гостевой режим (QUIZ)", callback_data="guest_mode")],
|
inline_keyboard=[
|
||||||
[InlineKeyboardButton(text="📚 Тестирование по материалам", callback_data="test_mode")],
|
[
|
||||||
[InlineKeyboardButton(text="📊 Моя статистика", callback_data="stats")],
|
InlineKeyboardButton(
|
||||||
])
|
text="🎯 Гостевой режим (QUIZ)", callback_data="guest_mode"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="📚 Тестирование по материалам", callback_data="test_mode"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[InlineKeyboardButton(text="📊 Моя статистика", callback_data="stats")],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
text = (f"👋 Добро пожаловать в Quiz Bot, {user.first_name}!\n\n"
|
text = (
|
||||||
"🎯 <b>Гостевой режим</b> - быстрая викторина для развлечения\n"
|
f"👋 Добро пожаловать в Quiz Bot, {user.first_name}!\n\n"
|
||||||
"📚 <b>Тестирование</b> - серьезное изучение материалов с результатами\n\n"
|
"🎯 <b>Гостевой режим</b> - быстрая викторина для развлечения\n"
|
||||||
"Выберите режим работы:")
|
"📚 <b>Тестирование</b> - серьезное изучение материалов с результатами\n\n"
|
||||||
|
"Выберите режим работы:"
|
||||||
|
)
|
||||||
|
|
||||||
if callback.message and not isinstance(callback.message, InaccessibleMessage):
|
if callback.message and not isinstance(callback.message, InaccessibleMessage):
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode='HTML')
|
await callback.message.edit_text(
|
||||||
|
text, reply_markup=keyboard, parse_mode="HTML"
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
await self.bot.send_message(callback.from_user.id, text, reply_markup=keyboard, parse_mode='HTML')
|
await self.bot.send_message(
|
||||||
|
callback.from_user.id,
|
||||||
|
text,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await self.bot.send_message(callback.from_user.id, text, reply_markup=keyboard, parse_mode='HTML')
|
await self.bot.send_message(
|
||||||
|
callback.from_user.id, text, reply_markup=keyboard, parse_mode="HTML"
|
||||||
|
)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
def get_grade(self, score: float) -> str:
|
def get_grade(self, score: float) -> str:
|
||||||
@@ -527,7 +718,10 @@ class QuizBot:
|
|||||||
async def start(self):
|
async def start(self):
|
||||||
"""Запуск бота"""
|
"""Запуск бота"""
|
||||||
# Проверяем токен
|
# Проверяем токен
|
||||||
if not config.bot_token or config.bot_token in ['your_bot_token_here', 'test_token_for_demo_purposes']:
|
if not config.bot_token or config.bot_token in [
|
||||||
|
"your_bot_token_here",
|
||||||
|
"test_token_for_demo_purposes",
|
||||||
|
]:
|
||||||
print("❌ Ошибка: не настроен BOT_TOKEN в файле .env")
|
print("❌ Ошибка: не настроен BOT_TOKEN в файле .env")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -544,10 +738,12 @@ class QuizBot:
|
|||||||
logging.error(f"Error starting bot: {e}")
|
logging.error(f"Error starting bot: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
"""Главная функция"""
|
"""Главная функция"""
|
||||||
bot = QuizBot()
|
bot = QuizBot()
|
||||||
await bot.start()
|
await bot.start()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import aiosqlite
|
|
||||||
import logging
|
|
||||||
from typing import List, Dict, Optional, Tuple, Union
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import aiosqlite
|
||||||
|
|
||||||
|
|
||||||
class DatabaseManager:
|
class DatabaseManager:
|
||||||
def __init__(self, db_path: str):
|
def __init__(self, db_path: str):
|
||||||
@@ -11,7 +13,8 @@ class DatabaseManager:
|
|||||||
"""Инициализация базы данных и создание таблиц"""
|
"""Инициализация базы данных и создание таблиц"""
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
# Таблица пользователей
|
# Таблица пользователей
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
user_id INTEGER PRIMARY KEY,
|
user_id INTEGER PRIMARY KEY,
|
||||||
username TEXT,
|
username TEXT,
|
||||||
@@ -23,10 +26,12 @@ class DatabaseManager:
|
|||||||
total_questions INTEGER DEFAULT 0,
|
total_questions INTEGER DEFAULT 0,
|
||||||
correct_answers INTEGER DEFAULT 0
|
correct_answers INTEGER DEFAULT 0
|
||||||
)
|
)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Таблица тестов
|
# Таблица тестов
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS tests (
|
CREATE TABLE IF NOT EXISTS tests (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
@@ -37,10 +42,12 @@ class DatabaseManager:
|
|||||||
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
is_active BOOLEAN DEFAULT TRUE
|
is_active BOOLEAN DEFAULT TRUE
|
||||||
)
|
)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Таблица вопросов
|
# Таблица вопросов
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS questions (
|
CREATE TABLE IF NOT EXISTS questions (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
test_id INTEGER,
|
test_id INTEGER,
|
||||||
@@ -52,10 +59,12 @@ class DatabaseManager:
|
|||||||
correct_answer INTEGER NOT NULL,
|
correct_answer INTEGER NOT NULL,
|
||||||
FOREIGN KEY (test_id) REFERENCES tests (id)
|
FOREIGN KEY (test_id) REFERENCES tests (id)
|
||||||
)
|
)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Таблица результатов
|
# Таблица результатов
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS results (
|
CREATE TABLE IF NOT EXISTS results (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER,
|
user_id INTEGER,
|
||||||
@@ -70,10 +79,12 @@ class DatabaseManager:
|
|||||||
FOREIGN KEY (user_id) REFERENCES users (user_id),
|
FOREIGN KEY (user_id) REFERENCES users (user_id),
|
||||||
FOREIGN KEY (test_id) REFERENCES tests (id)
|
FOREIGN KEY (test_id) REFERENCES tests (id)
|
||||||
)
|
)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Таблица активных сессий
|
# Таблица активных сессий
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS active_sessions (
|
CREATE TABLE IF NOT EXISTS active_sessions (
|
||||||
user_id INTEGER PRIMARY KEY,
|
user_id INTEGER PRIMARY KEY,
|
||||||
test_id INTEGER,
|
test_id INTEGER,
|
||||||
@@ -85,22 +96,32 @@ class DatabaseManager:
|
|||||||
FOREIGN KEY (user_id) REFERENCES users (user_id),
|
FOREIGN KEY (user_id) REFERENCES users (user_id),
|
||||||
FOREIGN KEY (test_id) REFERENCES tests (id)
|
FOREIGN KEY (test_id) REFERENCES tests (id)
|
||||||
)
|
)
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
logging.info("Database initialized successfully")
|
logging.info("Database initialized successfully")
|
||||||
|
|
||||||
async def register_user(self, user_id: int, username: Optional[str] = None,
|
async def register_user(
|
||||||
first_name: Optional[str] = None, last_name: Optional[str] = None,
|
self,
|
||||||
language_code: str = 'ru', is_guest: bool = True) -> bool:
|
user_id: int,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
first_name: Optional[str] = None,
|
||||||
|
last_name: Optional[str] = None,
|
||||||
|
language_code: str = "ru",
|
||||||
|
is_guest: bool = True,
|
||||||
|
) -> bool:
|
||||||
"""Регистрация нового пользователя"""
|
"""Регистрация нового пользователя"""
|
||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
INSERT OR REPLACE INTO users
|
INSERT OR REPLACE INTO users
|
||||||
(user_id, username, first_name, last_name, language_code, is_guest)
|
(user_id, username, first_name, last_name, language_code, is_guest)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""", (user_id, username, first_name, last_name, language_code, is_guest))
|
""",
|
||||||
|
(user_id, username, first_name, last_name, language_code, is_guest),
|
||||||
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -123,15 +144,19 @@ class DatabaseManager:
|
|||||||
logging.error(f"Error getting user {user_id}: {e}")
|
logging.error(f"Error getting user {user_id}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def add_test(self, name: str, description: str, level: int,
|
async def add_test(
|
||||||
category: str, csv_file: str) -> Optional[int]:
|
self, name: str, description: str, level: int, category: str, csv_file: str
|
||||||
|
) -> Optional[int]:
|
||||||
"""Добавление нового теста"""
|
"""Добавление нового теста"""
|
||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
cursor = await db.execute("""
|
cursor = await db.execute(
|
||||||
|
"""
|
||||||
INSERT INTO tests (name, description, level, category, csv_file)
|
INSERT INTO tests (name, description, level, category, csv_file)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
""", (name, description, level, category, csv_file))
|
""",
|
||||||
|
(name, description, level, category, csv_file),
|
||||||
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return cursor.lastrowid
|
return cursor.lastrowid
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -145,7 +170,7 @@ class DatabaseManager:
|
|||||||
if category:
|
if category:
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT * FROM tests WHERE category = ? AND is_active = TRUE ORDER BY level",
|
"SELECT * FROM tests WHERE category = ? AND is_active = TRUE ORDER BY level",
|
||||||
(category,)
|
(category,),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
@@ -163,12 +188,22 @@ class DatabaseManager:
|
|||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
for q in questions:
|
for q in questions:
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
INSERT INTO questions
|
INSERT INTO questions
|
||||||
(test_id, question, option1, option2, option3, option4, correct_answer)
|
(test_id, question, option1, option2, option3, option4, correct_answer)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (test_id, q['question'], q['option1'], q['option2'],
|
""",
|
||||||
q['option3'], q['option4'], q['correct_answer']))
|
(
|
||||||
|
test_id,
|
||||||
|
q["question"],
|
||||||
|
q["option1"],
|
||||||
|
q["option2"],
|
||||||
|
q["option3"],
|
||||||
|
q["option4"],
|
||||||
|
q["correct_answer"],
|
||||||
|
),
|
||||||
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -179,10 +214,13 @@ class DatabaseManager:
|
|||||||
"""Получение случайных вопросов из теста"""
|
"""Получение случайных вопросов из теста"""
|
||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
cursor = await db.execute("""
|
cursor = await db.execute(
|
||||||
|
"""
|
||||||
SELECT * FROM questions WHERE test_id = ?
|
SELECT * FROM questions WHERE test_id = ?
|
||||||
ORDER BY RANDOM() LIMIT ?
|
ORDER BY RANDOM() LIMIT ?
|
||||||
""", (test_id, count))
|
""",
|
||||||
|
(test_id, count),
|
||||||
|
)
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
columns = [description[0] for description in cursor.description]
|
columns = [description[0] for description in cursor.description]
|
||||||
return [dict(zip(columns, row)) for row in rows]
|
return [dict(zip(columns, row)) for row in rows]
|
||||||
@@ -190,17 +228,21 @@ class DatabaseManager:
|
|||||||
logging.error(f"Error getting random questions: {e}")
|
logging.error(f"Error getting random questions: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def start_session(self, user_id: int, test_id: int,
|
async def start_session(
|
||||||
questions: List[Dict], mode: str) -> bool:
|
self, user_id: int, test_id: int, questions: List[Dict], mode: str
|
||||||
|
) -> bool:
|
||||||
"""Начало новой сессии викторины"""
|
"""Начало новой сессии викторины"""
|
||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
questions_json = json.dumps(questions)
|
questions_json = json.dumps(questions)
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
INSERT OR REPLACE INTO active_sessions
|
INSERT OR REPLACE INTO active_sessions
|
||||||
(user_id, test_id, questions_data, mode)
|
(user_id, test_id, questions_data, mode)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
""", (user_id, test_id, questions_json, mode))
|
""",
|
||||||
|
(user_id, test_id, questions_json, mode),
|
||||||
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -218,39 +260,48 @@ class DatabaseManager:
|
|||||||
if row:
|
if row:
|
||||||
columns = [description[0] for description in cursor.description]
|
columns = [description[0] for description in cursor.description]
|
||||||
session = dict(zip(columns, row))
|
session = dict(zip(columns, row))
|
||||||
session['questions_data'] = json.loads(session['questions_data'])
|
session["questions_data"] = json.loads(session["questions_data"])
|
||||||
return session
|
return session
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting active session: {e}")
|
logging.error(f"Error getting active session: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def update_session_progress(self, user_id: int, question_num: int,
|
async def update_session_progress(
|
||||||
correct_count: int) -> bool:
|
self, user_id: int, question_num: int, correct_count: int
|
||||||
|
) -> bool:
|
||||||
"""Обновление прогресса сессии"""
|
"""Обновление прогресса сессии"""
|
||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
UPDATE active_sessions
|
UPDATE active_sessions
|
||||||
SET current_question = ?, correct_count = ?
|
SET current_question = ?, correct_count = ?
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
""", (question_num, correct_count, user_id))
|
""",
|
||||||
|
(question_num, correct_count, user_id),
|
||||||
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error updating session progress: {e}")
|
logging.error(f"Error updating session progress: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def update_session_questions(self, user_id: int, questions_data: list) -> bool:
|
async def update_session_questions(
|
||||||
|
self, user_id: int, questions_data: list
|
||||||
|
) -> bool:
|
||||||
"""Обновление данных вопросов в сессии (например, после перемешивания)"""
|
"""Обновление данных вопросов в сессии (например, после перемешивания)"""
|
||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
questions_json = json.dumps(questions_data, ensure_ascii=False)
|
questions_json = json.dumps(questions_data, ensure_ascii=False)
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
UPDATE active_sessions
|
UPDATE active_sessions
|
||||||
SET questions_data = ?
|
SET questions_data = ?
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
""", (questions_json, user_id))
|
""",
|
||||||
|
(questions_json, user_id),
|
||||||
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -267,23 +318,37 @@ class DatabaseManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Сохраняем результат
|
# Сохраняем результат
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
INSERT INTO results
|
INSERT INTO results
|
||||||
(user_id, test_id, mode, questions_asked, correct_answers, score)
|
(user_id, test_id, mode, questions_asked, correct_answers, score)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""", (user_id, session['test_id'], session['mode'],
|
""",
|
||||||
len(session['questions_data']), session['correct_count'], score))
|
(
|
||||||
|
user_id,
|
||||||
|
session["test_id"],
|
||||||
|
session["mode"],
|
||||||
|
len(session["questions_data"]),
|
||||||
|
session["correct_count"],
|
||||||
|
score,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Обновляем статистику пользователя
|
# Обновляем статистику пользователя
|
||||||
await db.execute("""
|
await db.execute(
|
||||||
|
"""
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET total_questions = total_questions + ?,
|
SET total_questions = total_questions + ?,
|
||||||
correct_answers = correct_answers + ?
|
correct_answers = correct_answers + ?
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
""", (len(session['questions_data']), session['correct_count'], user_id))
|
""",
|
||||||
|
(len(session["questions_data"]), session["correct_count"], user_id),
|
||||||
|
)
|
||||||
|
|
||||||
# Удаляем активную сессию
|
# Удаляем активную сессию
|
||||||
await db.execute("DELETE FROM active_sessions WHERE user_id = ?", (user_id,))
|
await db.execute(
|
||||||
|
"DELETE FROM active_sessions WHERE user_id = ?", (user_id,)
|
||||||
|
)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return True
|
return True
|
||||||
@@ -295,7 +360,8 @@ class DatabaseManager:
|
|||||||
"""Получение статистики пользователя"""
|
"""Получение статистики пользователя"""
|
||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
cursor = await db.execute("""
|
cursor = await db.execute(
|
||||||
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
u.total_questions,
|
u.total_questions,
|
||||||
u.correct_answers,
|
u.correct_answers,
|
||||||
@@ -308,7 +374,9 @@ class DatabaseManager:
|
|||||||
LEFT JOIN results r ON u.user_id = r.user_id
|
LEFT JOIN results r ON u.user_id = r.user_id
|
||||||
WHERE u.user_id = ?
|
WHERE u.user_id = ?
|
||||||
GROUP BY u.user_id
|
GROUP BY u.user_id
|
||||||
""", (user_id,))
|
""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
|
||||||
row = await cursor.fetchone()
|
row = await cursor.fetchone()
|
||||||
if row:
|
if row:
|
||||||
@@ -324,7 +392,8 @@ class DatabaseManager:
|
|||||||
"""Получение последних результатов пользователя"""
|
"""Получение последних результатов пользователя"""
|
||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
cursor = await db.execute("""
|
cursor = await db.execute(
|
||||||
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
r.mode,
|
r.mode,
|
||||||
r.questions_asked,
|
r.questions_asked,
|
||||||
@@ -338,7 +407,9 @@ class DatabaseManager:
|
|||||||
WHERE r.user_id = ?
|
WHERE r.user_id = ?
|
||||||
ORDER BY r.end_time DESC
|
ORDER BY r.end_time DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""", (user_id, limit))
|
""",
|
||||||
|
(user_id, limit),
|
||||||
|
)
|
||||||
|
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
columns = [description[0] for description in cursor.description]
|
columns = [description[0] for description in cursor.description]
|
||||||
@@ -351,7 +422,8 @@ class DatabaseManager:
|
|||||||
"""Получение статистики по категориям"""
|
"""Получение статистики по категориям"""
|
||||||
try:
|
try:
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
cursor = await db.execute("""
|
cursor = await db.execute(
|
||||||
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
t.category,
|
t.category,
|
||||||
COUNT(r.id) as attempts,
|
COUNT(r.id) as attempts,
|
||||||
@@ -364,7 +436,9 @@ class DatabaseManager:
|
|||||||
WHERE r.user_id = ?
|
WHERE r.user_id = ?
|
||||||
GROUP BY t.category
|
GROUP BY t.category
|
||||||
ORDER BY attempts DESC
|
ORDER BY attempts DESC
|
||||||
""", (user_id,))
|
""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
columns = [description[0] for description in cursor.description]
|
columns = [description[0] for description in cursor.description]
|
||||||
|
|||||||
Reference in New Issue
Block a user