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

478
handlers/answers.py Normal file
View File

@@ -0,0 +1,478 @@
"""
Обработчики для работы с ответами на вопросы
"""
from datetime import datetime
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from config import config
from models.question import Question, QuestionStatus
from services.infrastructure.database import DatabaseService
from services.auth.auth_new import AuthService
from services.utils import is_valid_answer_text, format_question_info, send_answer_to_author, escape_html
from services.infrastructure.logger import get_logger
from services.infrastructure.metrics import get_metrics_service, track_answer_processing
from dependencies import inject_answer_services, inject_main_menu_services
from keyboards.inline import get_question_view_keyboard
from keyboards.reply import get_main_keyboard_for_user, get_cancel_keyboard
logger = get_logger(__name__)
router = Router()
class AnswerStates(StatesGroup):
"""Состояния для работы с ответами"""
waiting_for_answer = State()
editing_answer = State()
confirming_delete = State()
@router.callback_query(F.data.startswith("view_question_"))
async def view_question_callback(callback: CallbackQuery):
"""Обработчик просмотра конкретного вопроса"""
try:
question_id = int(callback.data.split("_")[2])
from dependencies import get_database
db = get_database()
# Получаем вопрос
question = await db.get_question(question_id)
if not question:
await callback.answer("❌ Вопрос не найден", show_alert=True)
return
if question.to_user_id != callback.from_user.id:
await callback.answer("У вас нет прав на этот вопрос", show_alert=True)
return
# Формируем текст сообщения
question_text = format_question_info(question, show_answer=True)
# Получаем клавиатуру
keyboard = get_question_view_keyboard(question)
# Обновляем сообщение
await callback.message.edit_text(
question_text,
reply_markup=keyboard,
parse_mode="HTML"
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка при просмотре вопроса: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
@router.callback_query(F.data.startswith("answer_"))
async def answer_callback(callback: CallbackQuery, state: FSMContext):
"""Обработчик создания нового ответа"""
try:
logger.info(f"Получен callback для ответа: {callback.data}")
question_id = int(callback.data.split("_")[1])
logger.info(f"Извлечен question_id: {question_id}")
# Получаем базу данных
from dependencies import get_database
db = get_database()
# Получаем вопрос
question = await db.get_question(question_id)
if not question:
await callback.answer("❌ Вопрос не найден", show_alert=True)
return
# Проверяем права доступа
if question.to_user_id != callback.from_user.id:
await callback.answer("У вас нет прав на этот вопрос", show_alert=True)
return
# Проверяем, что вопрос еще не отвечен
if question.status == QuestionStatus.ANSWERED:
await callback.answer("На этот вопрос уже отвечено", show_alert=True)
return
# Устанавливаем состояние ожидания ответа
logger.info(f"Устанавливаем состояние waiting_for_answer для вопроса {question_id}")
await state.set_state(AnswerStates.waiting_for_answer)
await state.update_data(question_id=question_id)
logger.info(f"Состояние установлено, данные сохранены")
# Отправляем сообщение с просьбой ввести ответ
logger.info(f"Отправляем сообщение с просьбой ввести ответ для вопроса {question_id}")
await callback.message.edit_text(
f"✏️ <b>Ответ на вопрос #{question_id}</b>\n\n"
f"❓ <b>Вопрос:</b>\n{escape_html(question.message_text)}\n\n"
f"💬 <b>Введите ваш ответ:</b>",
reply_markup=get_cancel_keyboard(),
parse_mode="HTML"
)
await callback.answer()
logger.info(f"Callback обработан успешно для вопроса {question_id}")
except Exception as e:
logger.error(f"Ошибка при создании ответа: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
@router.callback_query(F.data.startswith("edit_answer_"))
async def edit_answer_callback(callback: CallbackQuery, state: FSMContext):
"""Обработчик редактирования ответа"""
try:
question_id = int(callback.data.split("_")[2])
from dependencies import get_database
db = get_database()
# Получаем вопрос
question = await db.get_question(question_id)
if not question:
await callback.answer("❌ Вопрос не найден", show_alert=True)
return
if question.to_user_id != callback.from_user.id:
await callback.answer("У вас нет прав на этот вопрос", show_alert=True)
return
if question.status != QuestionStatus.ANSWERED:
await callback.answer("На этот вопрос еще не отвечено", show_alert=True)
return
# Устанавливаем состояние редактирования ответа
await state.set_state(AnswerStates.editing_answer)
await state.update_data(question_id=question_id, current_answer=question.answer_text)
# Отправляем сообщение с просьбой ввести новый ответ
await callback.message.edit_text(
f"✏️ <b>Редактирование ответа на вопрос #{question_id}</b>\n\n"
f"📝 <b>Вопрос:</b>\n{escape_html(question.message_text)}\n\n"
f"💬 <b>Текущий ответ:</b>\n{escape_html(question.answer_text)}\n\n"
f"✍️ <b>Введите новый ответ:</b>",
reply_markup=None,
parse_mode="HTML"
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка при редактировании ответа: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
@router.message(StateFilter(AnswerStates.waiting_for_answer))
@inject_answer_services
async def process_new_answer(message: Message, state: FSMContext, validator, **kwargs):
"""Обработка нового ответа"""
try:
logger.info(f"Получено сообщение в состоянии waiting_for_answer: {message.text[:50]}...")
# Получаем данные из состояния
data = await state.get_data()
question_id = data.get('question_id')
logger.info(f"Получен question_id из состояния: {question_id}")
if not question_id:
await message.answer(
"❌ Ошибка: не найден вопрос для ответа.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
await state.clear()
return
# Валидируем текст ответа
validation_result = validator.validate_answer_text(
message.text,
config.MAX_ANSWER_LENGTH
)
if not validation_result:
logger.warning(f"⚠️ Невалидный ответ от пользователя {message.from_user.id}: {validation_result.error_message}")
await message.answer(
f"{validation_result.error_message}\n\n"
"Попробуйте отправить ответ еще раз:",
reply_markup=get_cancel_keyboard()
)
return
# Используем санитизированный текст
sanitized_answer_text = validation_result.sanitized_value
# Сохраняем ответ в БД
from dependencies import get_database
db = get_database()
question = await db.get_question(question_id)
if question:
question.answer_text = sanitized_answer_text
question.answered_at = datetime.now()
question.mark_as_answered() # Устанавливаем статус ANSWERED
await db.update_question(question)
# Отправляем ответ автору вопроса
logger.info(f"Попытка отправить ответ автору вопроса {question.id}, from_user_id: {question.from_user_id}")
await send_answer_to_author(message.bot, question, question.answer_text)
# Отправляем подтверждение
await message.answer(
"✅ <b>Ответ отправлен!</b>\n\n"
"💬 Ваш ответ был успешно отправлен автору вопроса.\n\n"
"📋 Используйте 'Мои вопросы' для просмотра всех вопросов и ответов.",
reply_markup=get_main_keyboard_for_user(message.from_user.id),
parse_mode="HTML"
)
await state.clear()
except Exception as e:
logger.error(f"Ошибка при обработке нового ответа: {e}")
await message.answer(
"❌ Произошла ошибка при отправке ответа. Попробуйте позже.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
await state.clear()
@router.message(StateFilter(AnswerStates.editing_answer))
@inject_answer_services
async def process_edited_answer(message: Message, state: FSMContext, validator, **kwargs):
"""Обработка отредактированного ответа"""
try:
# Получаем данные из состояния
data = await state.get_data()
question_id = data.get('question_id')
current_answer = data.get('current_answer')
if not question_id:
await message.answer(
"❌ Ошибка: не найден вопрос для редактирования.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
await state.clear()
return
# Валидируем текст ответа
validation_result = validator.validate_answer_text(
message.text,
config.MAX_ANSWER_LENGTH
)
if not validation_result:
logger.warning(f"⚠️ Невалидный отредактированный ответ от пользователя {message.from_user.id}: {validation_result.error_message}")
await message.answer(
f"{validation_result.error_message}\n\n"
"Попробуйте отправить ответ еще раз:",
reply_markup=get_cancel_keyboard()
)
return
# Используем санитизированный текст
sanitized_answer_text = validation_result.sanitized_value
# Сохраняем отредактированный ответ
from dependencies import get_database
db = get_database()
question = await db.get_question(question_id)
if question:
question.answer_text = sanitized_answer_text
question.answered_at = datetime.now() # Обновляем время ответа
await db.update_question(question)
# Отправляем ответ автору вопроса
logger.info(f"Попытка отправить ответ автору вопроса {question.id}, from_user_id: {question.from_user_id}")
await send_answer_to_author(message.bot, question, question.answer_text)
# Отправляем подтверждение
await message.answer(
"✅ <b>Ответ обновлен!</b>\n\n"
"💬 Ваш ответ был успешно обновлен.\n\n"
"📋 Используйте 'Мои вопросы' для просмотра всех вопросов и ответов.",
reply_markup=get_main_keyboard_for_user(message.from_user.id),
parse_mode="HTML"
)
await state.clear()
except Exception as e:
logger.error(f"Ошибка при обработке отредактированного ответа: {e}")
await message.answer(
"❌ Произошла ошибка при обновлении ответа. Попробуйте позже.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
await state.clear()
@router.callback_query(F.data.startswith("confirm_delete_"))
async def confirm_delete_callback(callback: CallbackQuery):
"""Обработчик подтверждения удаления вопроса"""
try:
question_id = int(callback.data.split("_")[2])
from dependencies import get_database
db = get_database()
# Получаем вопрос
question = await db.get_question(question_id)
if not question:
await callback.answer("❌ Вопрос не найден", show_alert=True)
return
if question.to_user_id != callback.from_user.id:
await callback.answer("У вас нет прав на этот вопрос", show_alert=True)
return
# Удаляем вопрос
question.mark_as_deleted()
await db.update_question(question)
# Обновляем сообщение
await callback.message.edit_text(
f"🗑️ <b>Вопрос #{question.user_question_number if question.user_question_number is not None else question_id} удален</b>\n\n"
f"📅 Удален: {question.answered_at.strftime('%d.%m.%Y %H:%M')}",
reply_markup=None,
parse_mode="HTML"
)
await callback.answer("🗑️ Вопрос удален")
except Exception as e:
logger.error(f"Ошибка при подтверждении удаления: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
@router.callback_query(F.data.startswith("cancel_delete_"))
async def cancel_delete_callback(callback: CallbackQuery):
"""Обработчик отмены удаления вопроса"""
try:
question_id = int(callback.data.split("_")[2])
from dependencies import get_database
db = get_database()
# Получаем вопрос
question = await db.get_question(question_id)
if not question:
await callback.answer("❌ Вопрос не найден", show_alert=True)
return
if question.to_user_id != callback.from_user.id:
await callback.answer("У вас нет прав на этот вопрос", show_alert=True)
return
# Возвращаемся к просмотру вопроса
question_text = format_question_info(question, show_answer=True)
keyboard = get_question_view_keyboard(question)
await callback.message.edit_text(
question_text,
reply_markup=keyboard,
parse_mode="HTML"
)
await callback.answer("❌ Удаление отменено")
except Exception as e:
logger.error(f"Ошибка при отмене удаления: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
@router.callback_query(F.data == "back_to_questions")
async def back_to_questions_callback(callback: CallbackQuery):
"""Обработчик кнопки 'Назад к списку'"""
try:
from dependencies import get_database
db = get_database()
# Получаем вопросы пользователя
questions = await db.get_user_questions(callback.from_user.id, limit=10)
if not questions:
await callback.message.edit_text(
"📭 У вас пока нет вопросов.\n\n"
"🔗 Поделитесь своей ссылкой, чтобы получать анонимные вопросы!",
reply_markup=None
)
else:
questions_text = f"📋 <b>Ваши вопросы ({len(questions)}):</b>\n\n"
for i, question in enumerate(questions, 1):
status_emoji = {
'pending': '',
'answered': '',
'rejected': '',
'deleted': '🗑️'
}
emoji = status_emoji.get(question.status.value, '')
preview = question.get_question_preview(50)
# Используем user_question_number для отображения, если он есть
display_number = question.user_question_number if question.user_question_number is not None else i
questions_text += f"{i}. {emoji} <b>#{display_number}</b>\n"
questions_text += f" {preview}\n"
questions_text += f" 📅 {question.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
questions_text += "💡 Нажмите на номер вопроса для просмотра деталей."
await callback.message.edit_text(
questions_text,
reply_markup=None,
parse_mode="HTML"
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка при возврате к списку вопросов: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
@router.callback_query(
F.data == "back_to_main",
)
@inject_main_menu_services
async def back_to_main_callback(
callback: CallbackQuery,
auth: AuthService,
**kwargs
):
"""Обработчик кнопки 'Назад' в главное меню"""
try:
# Используем инжектированную систему авторизации
is_admin = auth.is_admin(callback.from_user.id)
if is_admin:
from keyboards.inline import get_admin_keyboard
keyboard = get_admin_keyboard()
text = "🏠 <b>Главное меню (Админ)</b>\n\nВыберите действие:"
else:
keyboard = get_main_keyboard_for_user(callback.from_user.id)
text = "🏠 <b>Главное меню</b>\n\nВыберите действие:"
await callback.message.edit_text(
text,
reply_markup=None,
parse_mode="HTML"
)
# Отправляем новое сообщение с клавиатурой
await callback.message.answer(
text,
reply_markup=keyboard,
parse_mode="HTML"
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка при возврате в главное меню: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)