Implement user-specific question numbering and update database schema. Added triggers for automatic question numbering and adjustments upon deletion. Enhanced CRUD operations to manage user_question_number effectively.

This commit is contained in:
2025-09-06 18:35:12 +03:00
parent 50be010026
commit 596a2fa813
111 changed files with 16847 additions and 65 deletions

615
keyboards/inline.py Normal file
View File

@@ -0,0 +1,615 @@
"""
Inline клавиатуры для бота
"""
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from models.question import Question
from typing import List
from services.utils import escape_html
def get_answer_keyboard(question_id: int, from_user_id: int = None) -> InlineKeyboardMarkup:
"""
Клавиатура для ответа на вопрос
Args:
question_id: ID вопроса
from_user_id: ID отправителя вопроса (для блокировки)
Returns:
Inline клавиатура
"""
builder = InlineKeyboardBuilder()
builder.add(
InlineKeyboardButton(
text="💬 Ответить",
callback_data=f"answer_{question_id}"
)
)
builder.add(
InlineKeyboardButton(
text="❌ Отклонить",
callback_data=f"reject_{question_id}"
)
)
# Добавляем кнопку блокировки, если есть ID отправителя
if from_user_id:
builder.add(
InlineKeyboardButton(
text="🚫 Заблокировать",
callback_data=f"block_{from_user_id}_{question_id}"
)
)
builder.add(
InlineKeyboardButton(
text="🗑️ Удалить",
callback_data=f"delete_{question_id}"
)
)
builder.adjust(1) # По одной кнопке в ряд
return builder.as_markup()
def get_admin_keyboard() -> InlineKeyboardMarkup:
"""
Клавиатура для администраторов
Returns:
Inline клавиатура
"""
builder = InlineKeyboardBuilder()
builder.add(
InlineKeyboardButton(
text="📊 Статистика",
callback_data="admin_stats"
)
)
builder.add(
InlineKeyboardButton(
text="📢 Рассылка",
callback_data="admin_broadcast"
)
)
builder.add(
InlineKeyboardButton(
text="🔍 Назначить суперпользователя",
callback_data="admin_assign_superuser"
)
)
builder.add(
InlineKeyboardButton(
text="🚫 Забанить пользователя",
callback_data="admin_ban_user"
)
)
builder.add(
InlineKeyboardButton(
text="🚦 Rate Limiting",
callback_data="admin_rate_limit"
)
)
builder.adjust(2) # По две кнопки в ряд
return builder.as_markup()
def get_superuser_assignment_keyboard(users: List, page: int = 0, per_page: int = 10) -> InlineKeyboardMarkup:
"""
Клавиатура для назначения суперпользователей
Args:
users: Список пользователей
page: Номер страницы
per_page: Количество пользователей на странице
Returns:
Inline клавиатура с пользователями для назначения
"""
builder = InlineKeyboardBuilder()
# Вычисляем диапазон пользователей для текущей страницы
start_idx = page * per_page
end_idx = min(start_idx + per_page, len(users))
page_users = users[start_idx:end_idx]
# Добавляем кнопки для пользователей
for user in page_users:
# Определяем статус пользователя
status_emoji = "🔍" if user.is_superuser else "👤"
button_text = f"{status_emoji} {user.display_name}"
# Обрезаем текст если слишком длинный
if len(button_text) > 30:
button_text = button_text[:27] + "..."
builder.add(
InlineKeyboardButton(
text=button_text,
callback_data=f"assign_superuser_{user.telegram_id}"
)
)
# Настраиваем расположение кнопок: 1 кнопка в ряд для лучшей читаемости
builder.adjust(1)
# Добавляем управляющие кнопки
control_buttons = []
# Кнопка "Предыдущая" (если есть предыдущие страницы)
if page > 0:
control_buttons.append(
InlineKeyboardButton(
text="⬅️ Предыдущая",
callback_data=f"superuser_page_{page - 1}"
)
)
# Кнопка "Следующая" (если есть следующие страницы)
total_pages = (len(users) + per_page - 1) // per_page
if page < total_pages - 1:
control_buttons.append(
InlineKeyboardButton(
text="Следующая ➡️",
callback_data=f"superuser_page_{page + 1}"
)
)
# Добавляем управляющие кнопки
if control_buttons:
builder.row(*control_buttons)
# Кнопка "Назад"
builder.add(
InlineKeyboardButton(
text="🔙 Назад к админ панели",
callback_data="back_to_admin"
)
)
return builder.as_markup()
def get_superuser_confirm_keyboard(user_id: int, is_superuser: bool) -> InlineKeyboardMarkup:
"""
Клавиатура для подтверждения назначения/снятия суперпользователя
Args:
user_id: ID пользователя
is_superuser: Текущий статус суперпользователя
Returns:
Inline клавиатура с кнопками подтверждения
"""
builder = InlineKeyboardBuilder()
if is_superuser:
# Если уже суперпользователь, предлагаем снять права
builder.add(
InlineKeyboardButton(
text="❌ Снять права суперпользователя",
callback_data=f"remove_superuser_{user_id}"
)
)
else:
# Если не суперпользователь, предлагаем назначить
builder.add(
InlineKeyboardButton(
text="✅ Назначить суперпользователем",
callback_data=f"confirm_superuser_{user_id}"
)
)
builder.add(
InlineKeyboardButton(
text="🔙 Назад к списку",
callback_data="admin_assign_superuser"
)
)
return builder.as_markup()
def get_ban_user_keyboard(users: List, page: int = 0, per_page: int = 10) -> InlineKeyboardMarkup:
"""
Клавиатура для выбора пользователя для бана
Args:
users: Список пользователей
page: Номер страницы
per_page: Количество пользователей на странице
Returns:
Inline клавиатура с пользователями для бана
"""
builder = InlineKeyboardBuilder()
# Вычисляем диапазон пользователей для текущей страницы
start_idx = page * per_page
end_idx = min(start_idx + per_page, len(users))
page_users = users[start_idx:end_idx]
# Добавляем кнопки для пользователей
for user in page_users:
# Определяем статус пользователя
status_emoji = "🚫" if user.is_banned else "👤"
button_text = f"{status_emoji} {user.display_name}"
# Обрезаем текст если слишком длинный
if len(button_text) > 30:
button_text = button_text[:27] + "..."
builder.add(
InlineKeyboardButton(
text=button_text,
callback_data=f"ban_user_select_{user.telegram_id}"
)
)
# Настраиваем расположение кнопок: 1 кнопка в ряд для лучшей читаемости
builder.adjust(1)
# Добавляем управляющие кнопки
control_buttons = []
# Кнопка "Предыдущая" (если есть предыдущие страницы)
if page > 0:
control_buttons.append(
InlineKeyboardButton(
text="⬅️ Предыдущая",
callback_data=f"ban_user_page_{page - 1}"
)
)
# Кнопка "Следующая" (если есть следующие страницы)
total_pages = (len(users) + per_page - 1) // per_page
if page < total_pages - 1:
control_buttons.append(
InlineKeyboardButton(
text="Следующая ➡️",
callback_data=f"ban_user_page_{page + 1}"
)
)
# Добавляем управляющие кнопки
if control_buttons:
builder.row(*control_buttons)
# Кнопка "Назад"
builder.add(
InlineKeyboardButton(
text="🔙 Назад к админ панели",
callback_data="back_to_admin"
)
)
return builder.as_markup()
def get_ban_duration_keyboard(user_id: int) -> InlineKeyboardMarkup:
"""
Клавиатура для выбора срока бана
Args:
user_id: ID пользователя для бана
Returns:
Inline клавиатура с вариантами срока бана
"""
builder = InlineKeyboardBuilder()
# Варианты срока бана
ban_options = [
("1 час", "ban_1h"),
("1 день", "ban_1d"),
("3 дня", "ban_3d"),
("1 неделя", "ban_1w"),
("1 месяц", "ban_1m"),
("Навсегда", "ban_forever")
]
for text, callback_data in ban_options:
builder.add(
InlineKeyboardButton(
text=text,
callback_data=f"{callback_data}_{user_id}"
)
)
builder.adjust(2) # По две кнопки в ряд
# Кнопка "Назад"
builder.add(
InlineKeyboardButton(
text="🔙 Назад к списку",
callback_data="admin_ban_user"
)
)
return builder.as_markup()
def get_ban_confirm_keyboard(user_id: int, duration: str, reason: str = None) -> InlineKeyboardMarkup:
"""
Клавиатура для подтверждения бана пользователя
Args:
user_id: ID пользователя
duration: Срок бана
reason: Причина бана
Returns:
Inline клавиатура с кнопками подтверждения
"""
builder = InlineKeyboardBuilder()
builder.add(
InlineKeyboardButton(
text="✅ Подтвердить бан",
callback_data=f"confirm_ban_{user_id}_{duration}"
)
)
builder.add(
InlineKeyboardButton(
text="❌ Отменить",
callback_data=f"ban_user_select_{user_id}"
)
)
builder.adjust(1)
return builder.as_markup()
def get_unban_keyboard(user_id: int) -> InlineKeyboardMarkup:
"""
Клавиатура для разбана пользователя
Args:
user_id: ID пользователя
Returns:
Inline клавиатура с кнопкой разбана
"""
builder = InlineKeyboardBuilder()
builder.add(
InlineKeyboardButton(
text="✅ Разбанить пользователя",
callback_data=f"unban_user_{user_id}"
)
)
builder.add(
InlineKeyboardButton(
text="🔙 Назад к списку",
callback_data="admin_ban_user"
)
)
builder.adjust(1)
return builder.as_markup()
def get_stats_keyboard() -> InlineKeyboardMarkup:
"""
Клавиатура для статистики
Returns:
Inline клавиатура
"""
builder = InlineKeyboardBuilder()
builder.add(
InlineKeyboardButton(
text="📈 Общая статистика",
callback_data="stats_general"
)
)
builder.add(
InlineKeyboardButton(
text="⬅️ Назад",
callback_data="back_to_admin"
)
)
builder.adjust(2)
return builder.as_markup()
def get_user_questions_keyboard(questions: list, page: int = 0, per_page: int = 9, total_questions: int = None) -> InlineKeyboardMarkup:
"""
Клавиатура со списком вопросов пользователя
Args:
questions: Список вопросов для текущей страницы
page: Номер страницы
per_page: Количество вопросов на странице
total_questions: Общее количество вопросов (для расчета пагинации)
Returns:
Inline клавиатура
"""
builder = InlineKeyboardBuilder()
# Если не передано общее количество вопросов, используем длину списка
if total_questions is None:
total_questions = len(questions)
# Вычисляем общее количество страниц
total_pages = (total_questions + per_page - 1) // per_page if total_questions > 0 else 1
# Добавляем кнопки для вопросов
for i, question_data in enumerate(questions):
# Проверяем, является ли элемент кортежем (question, author_user) или просто question
if isinstance(question_data, tuple):
question, author_user = question_data
else:
question = question_data
status_emoji = {
'pending': '',
'answered': '',
'rejected': '',
'deleted': '🗑️'
}
emoji = status_emoji.get(question.status.value, '')
# Используем user_question_number для отображения
display_number = question.user_question_number if question.user_question_number is not None else (page * per_page + i + 1)
text = f"{emoji} Вопрос #{display_number}"
# Обрезаем текст если слишком длинный
if len(text) > 30:
text = text[:27] + "..."
builder.add(
InlineKeyboardButton(
text=text,
callback_data=f"view_question_{question.id}"
)
)
# Настраиваем расположение кнопок: 3 кнопки в ряд для вопросов
builder.adjust(3)
# Добавляем управляющие кнопки только если есть больше одной страницы
if total_pages > 1:
control_buttons = []
# Кнопка "Предыдущая" (только если не первая страница)
if page > 0:
control_buttons.append(
InlineKeyboardButton(
text="⬅️ Предыдущая",
callback_data=f"questions_page_{page - 1}"
)
)
# Кнопка "В меню"
control_buttons.append(
InlineKeyboardButton(
text="🏠 В меню",
callback_data="back_to_main"
)
)
# Кнопка "Следующая" (только если не последняя страница)
if page < total_pages - 1:
control_buttons.append(
InlineKeyboardButton(
text="Следующая ➡️",
callback_data=f"questions_page_{page + 1}"
)
)
# Добавляем управляющие кнопки в отдельный ряд
builder.row(*control_buttons)
else:
# Если только одна страница, добавляем только кнопку "В меню"
builder.row(
InlineKeyboardButton(
text="🏠 В меню",
callback_data="back_to_main"
)
)
return builder.as_markup()
def get_question_view_keyboard(question: Question) -> InlineKeyboardMarkup:
"""
Клавиатура для просмотра конкретного вопроса
Args:
question: Объект вопроса
Returns:
Inline клавиатура
"""
builder = InlineKeyboardBuilder()
if question.status.value == 'pending':
# Если вопрос ожидает ответа
builder.add(
InlineKeyboardButton(
text="💬 Ответить",
callback_data=f"answer_{question.id}"
)
)
builder.add(
InlineKeyboardButton(
text="❌ Отклонить",
callback_data=f"reject_{question.id}"
)
)
elif question.status.value == 'answered':
# Если вопрос уже отвечен
builder.add(
InlineKeyboardButton(
text="✏️ Редактировать ответ",
callback_data=f"edit_answer_{question.id}"
)
)
# Общие действия
builder.add(
InlineKeyboardButton(
text="🗑️ Удалить",
callback_data=f"delete_{question.id}"
)
)
builder.add(
InlineKeyboardButton(
text="⬅️ К списку вопросов",
callback_data="back_to_questions"
)
)
builder.adjust(1)
return builder.as_markup()
def get_rate_limit_keyboard() -> InlineKeyboardMarkup:
"""
Клавиатура для управления rate limiting
Returns:
Inline клавиатура с кнопками rate limiting
"""
builder = InlineKeyboardBuilder()
builder.add(
InlineKeyboardButton(
text="📊 Статистика Rate Limiting",
callback_data="rate_limit_stats"
)
)
builder.add(
InlineKeyboardButton(
text="🔄 Сбросить статистику",
callback_data="rate_limit_reset_stats"
)
)
builder.add(
InlineKeyboardButton(
text="⚙️ Адаптировать конфигурацию",
callback_data="rate_limit_adapt"
)
)
builder.add(
InlineKeyboardButton(
text="⬅️ Назад к админке",
callback_data="back_to_admin"
)
)
builder.adjust(1)
return builder.as_markup()