4179 lines
214 KiB
Python
4179 lines
214 KiB
Python
"""
|
||
Telegram бот для уведомлений и интеграции.
|
||
"""
|
||
import logging
|
||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton
|
||
from telegram.ext import (
|
||
Application,
|
||
CommandHandler,
|
||
CallbackQueryHandler,
|
||
MessageHandler,
|
||
filters,
|
||
ContextTypes,
|
||
)
|
||
from django.conf import settings
|
||
from django.utils import timezone
|
||
from asgiref.sync import sync_to_async
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def get_main_keyboard(role=None):
|
||
"""
|
||
Получить основную клавиатуру в зависимости от роли.
|
||
|
||
Args:
|
||
role: Роль пользователя ('mentor', 'client', 'parent', None)
|
||
|
||
Returns:
|
||
ReplyKeyboardMarkup: Клавиатура с кнопками
|
||
"""
|
||
if role == 'mentor':
|
||
keyboard = [
|
||
[KeyboardButton("📅 Расписание"), KeyboardButton("📚 Следующее занятие")],
|
||
[KeyboardButton("📝 Домашние задания"), KeyboardButton("👥 Клиенты")],
|
||
[KeyboardButton("📊 Статистика"), KeyboardButton("⚙️ Настройки")],
|
||
[KeyboardButton("ℹ️ Статус"), KeyboardButton("❓ Помощь")]
|
||
]
|
||
elif role == 'client':
|
||
keyboard = [
|
||
[KeyboardButton("📅 Моё расписание"), KeyboardButton("📚 Следующее занятие")],
|
||
[KeyboardButton("📝 Мои задания"), KeyboardButton("📊 Мой прогресс")],
|
||
[KeyboardButton("⚙️ Настройки"), KeyboardButton("❓ Помощь")]
|
||
]
|
||
elif role == 'parent':
|
||
keyboard = [
|
||
[KeyboardButton("📅 Расписание детей"), KeyboardButton("📚 Следующее занятие")],
|
||
[KeyboardButton("📝 Задания детей"), KeyboardButton("⚙️ Настройки")],
|
||
[KeyboardButton("ℹ️ Статус"), KeyboardButton("❓ Помощь")]
|
||
]
|
||
else:
|
||
# Для не связанных пользователей
|
||
keyboard = [
|
||
[KeyboardButton("🔗 Связать аккаунт"), KeyboardButton("❓ Помощь")]
|
||
]
|
||
|
||
return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False)
|
||
|
||
|
||
async def get_user_keyboard(telegram_id):
|
||
"""
|
||
Получить клавиатуру для пользователя по его telegram_id.
|
||
|
||
Args:
|
||
telegram_id: ID пользователя в Telegram
|
||
|
||
Returns:
|
||
ReplyKeyboardMarkup: Клавиатура с кнопками
|
||
"""
|
||
from apps.users.models import User
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
role = db_user.role if db_user else None
|
||
return get_main_keyboard(role)
|
||
|
||
|
||
# Текст кнопки «Назад» в панели списка заданий
|
||
HOMEWORK_LIST_BACK_BUTTON = "◀️ Назад в меню"
|
||
|
||
# Кнопки для «Мой прогресс»
|
||
PROGRESS_BACK_TO_MENU = "◀️ В меню"
|
||
PROGRESS_PERIOD_WEEK = "📅 За текущую неделю"
|
||
PROGRESS_PERIOD_30 = "📅 За последние 30 дней"
|
||
PROGRESS_PERIOD_ALL = "📅 За всё время"
|
||
PROGRESS_BACK_SUBJECT = "◀️ К выбору предмета"
|
||
|
||
|
||
def make_homework_list_keyboard(homeworks):
|
||
"""
|
||
Собрать клавиатуру панели со списком заданий (кнопки в панели Telegram).
|
||
|
||
Args:
|
||
homeworks: список объектов Homework
|
||
|
||
Returns:
|
||
(ReplyKeyboardMarkup, dict): клавиатура и маппинг текст_кнопки -> homework_id
|
||
"""
|
||
mapping = {}
|
||
rows = []
|
||
for idx, hw in enumerate(homeworks, 1):
|
||
btn_text = f"📝 Задание {idx}"
|
||
mapping[btn_text] = hw.id
|
||
rows.append([KeyboardButton(btn_text)])
|
||
rows.append([KeyboardButton(HOMEWORK_LIST_BACK_BUTTON)])
|
||
keyboard = ReplyKeyboardMarkup(rows, resize_keyboard=True, one_time_keyboard=False)
|
||
return keyboard, mapping
|
||
|
||
|
||
class TelegramBot:
|
||
"""Класс для управления Telegram ботом."""
|
||
|
||
def __init__(self):
|
||
"""Инициализация бота."""
|
||
self.token = settings.TELEGRAM_BOT_TOKEN
|
||
self.application = None
|
||
self.use_webhook = getattr(settings, 'TELEGRAM_USE_WEBHOOK', False)
|
||
self.webhook_url = getattr(settings, 'TELEGRAM_WEBHOOK_URL', None)
|
||
self.webhook_secret_token = getattr(settings, 'TELEGRAM_WEBHOOK_SECRET_TOKEN', None)
|
||
|
||
async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Обработчик команды /start.
|
||
Приветствие и инструкции по связыванию аккаунта.
|
||
"""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
# Проверяем связан ли аккаунт
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
# Если аккаунт не связан, показываем общее приветствие
|
||
welcome_message = f"""
|
||
👋 Привет, {user.first_name}!
|
||
|
||
Я бот образовательной платформы. Я буду присылать вам уведомления о:
|
||
• Новых занятиях
|
||
• Домашних заданиях
|
||
• Сообщениях от ментора/ученика
|
||
• Напоминаниях о занятиях
|
||
|
||
📱 Чтобы связать ваш аккаунт:
|
||
1. Войдите на платформу
|
||
2. Перейдите в Профиль → Настройки → Telegram
|
||
3. Нажмите "Связать Telegram"
|
||
4. Введите код связывания
|
||
|
||
Или используйте команду:
|
||
/link <ваш_код_связывания>
|
||
|
||
После связывания вы увидите команды, доступные для вашей роли.
|
||
"""
|
||
else:
|
||
# Если аккаунт связан, показываем приветствие в зависимости от роли
|
||
role = db_user.role
|
||
user_name = db_user.get_full_name() or db_user.email
|
||
|
||
if role == 'mentor':
|
||
welcome_message = f"""
|
||
👋 Привет, {user_name}!
|
||
|
||
Я бот образовательной платформы для менторов.
|
||
|
||
Я буду присылать вам уведомления о:
|
||
• Новых запросах на занятия
|
||
• Отменах занятий
|
||
• Сданных домашних заданиях
|
||
• Сообщениях от учеников
|
||
• Напоминаниях о занятиях
|
||
|
||
📚 Доступные команды:
|
||
/help - Полный список команд
|
||
/schedule - Расписание занятий
|
||
/nextlesson - Следующее занятие
|
||
/homework - Домашние задания на проверку
|
||
/clients - Список клиентов
|
||
/stats - Статистика
|
||
/settings - Настройки уведомлений
|
||
/status - Статус аккаунта
|
||
"""
|
||
keyboard = get_main_keyboard('mentor')
|
||
await update.message.reply_text(
|
||
welcome_message,
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
elif role == 'client':
|
||
welcome_message = f"""
|
||
👋 Привет, {user_name}!
|
||
|
||
Я бот образовательной платформы для учеников.
|
||
|
||
Я буду присылать вам уведомления о:
|
||
• Новых занятиях
|
||
• Отменах занятий
|
||
• Новых домашних заданиях
|
||
• Проверенных домашних заданиях
|
||
• Сообщениях от ментора
|
||
• Напоминаниях о занятиях
|
||
|
||
📚 Доступные команды:
|
||
/help - Полный список команд
|
||
/schedule - Моё расписание
|
||
/nextlesson - Следующее занятие
|
||
/homework - Мои домашние задания
|
||
/progress - Мой прогресс обучения
|
||
/settings - Настройки уведомлений
|
||
/status - Статус аккаунта
|
||
"""
|
||
keyboard = get_main_keyboard('client')
|
||
await update.message.reply_text(
|
||
welcome_message,
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
elif role == 'parent':
|
||
welcome_message = f"""
|
||
👋 Привет, {user_name}!
|
||
|
||
Я бот образовательной платформы для родителей.
|
||
|
||
Я буду присылать вам уведомления о:
|
||
• Занятиях ваших детей
|
||
• Домашних заданиях детей
|
||
• Прогрессе обучения
|
||
• Отчётах от менторов
|
||
|
||
📚 Доступные команды:
|
||
/help - Полный список команд
|
||
/schedule - Расписание детей
|
||
/nextlesson - Следующее занятие ребёнка
|
||
/homework - Домашние задания детей
|
||
/settings - Настройки уведомлений
|
||
/status - Статус аккаунта
|
||
"""
|
||
keyboard = get_main_keyboard('parent')
|
||
await update.message.reply_text(
|
||
welcome_message,
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
else:
|
||
# Для других ролей (admin и т.д.)
|
||
welcome_message = f"""
|
||
👋 Привет, {user_name}!
|
||
|
||
Я бот образовательной платформы.
|
||
|
||
Я буду присылать вам уведомления о событиях на платформе.
|
||
|
||
📚 Доступные команды:
|
||
/help - Полный список команд
|
||
/settings - Настройки уведомлений
|
||
/status - Статус аккаунта
|
||
"""
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
welcome_message,
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Если дошли сюда, отправляем без клавиатуры
|
||
await update.message.reply_text(welcome_message)
|
||
|
||
async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /help."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
# Проверяем связан ли аккаунт
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
# Если аккаунт не связан, показываем общую справку
|
||
help_text = """
|
||
🔔 Доступные команды:
|
||
|
||
/start - Начать работу с ботом
|
||
/link <код> - Связать аккаунт с платформой
|
||
/help - Эта справка
|
||
|
||
💡 Как связать аккаунт:
|
||
1. Получите код связывания на платформе (Профиль → Telegram)
|
||
2. Отправьте команду: /link ВАШ_КОД
|
||
|
||
После связывания вы увидите команды, доступные для вашей роли.
|
||
"""
|
||
else:
|
||
# Если аккаунт связан, показываем справку в зависимости от роли
|
||
role = db_user.role
|
||
|
||
if role == 'mentor':
|
||
help_text = """
|
||
👨🏫 Справка для менторов:
|
||
|
||
🔔 Основные команды:
|
||
/start - Главное меню
|
||
/help - Эта справка
|
||
/status - Статус аккаунта
|
||
/settings - Настройки уведомлений
|
||
|
||
📚 Расписание:
|
||
/schedule - Ближайшие занятия (до 5 занятий)
|
||
/nextlesson - Следующее занятие
|
||
|
||
📝 Домашние задания:
|
||
/homework - Задания, требующие проверки
|
||
|
||
👥 Клиенты:
|
||
/clients - Список ваших клиентов со статистикой
|
||
/stats - Ваша статистика (занятия, ДЗ, клиенты)
|
||
|
||
🔗 Управление аккаунтом:
|
||
/unlink - Отвязать Telegram аккаунт
|
||
|
||
💡 Вы будете получать уведомления о:
|
||
• Новых запросах на занятия
|
||
• Отменах занятий
|
||
• Сданных домашних заданиях
|
||
• Сообщениях от учеников
|
||
"""
|
||
elif role == 'client':
|
||
help_text = """
|
||
👨🎓 Справка для учеников:
|
||
|
||
🔔 Основные команды:
|
||
/start - Главное меню
|
||
/help - Эта справка
|
||
/status - Статус аккаунта
|
||
/settings - Настройки уведомлений
|
||
|
||
📚 Расписание:
|
||
/schedule - Моё расписание (до 5 ближайших занятий)
|
||
/nextlesson - Следующее занятие
|
||
|
||
📝 Домашние задания:
|
||
/homework - Мои активные домашние задания
|
||
|
||
📊 Прогресс:
|
||
/progress - Мой прогресс обучения
|
||
|
||
🔗 Управление аккаунтом:
|
||
/unlink - Отвязать Telegram аккаунт
|
||
|
||
💡 Вы будете получать уведомления о:
|
||
• Новых занятиях
|
||
• Отменах занятий
|
||
• Новых домашних заданиях
|
||
• Проверенных домашних заданиях
|
||
• Сообщениях от ментора
|
||
"""
|
||
elif role == 'parent':
|
||
help_text = """
|
||
👨👩👧 Справка для родителей:
|
||
|
||
🔔 Основные команды:
|
||
/start - Главное меню
|
||
/help - Эта справка
|
||
/status - Статус аккаунта
|
||
/settings - Настройки уведомлений
|
||
|
||
📚 Расписание детей:
|
||
/schedule - Расписание всех детей (до 5 ближайших занятий)
|
||
/nextlesson - Следующее занятие ребёнка
|
||
|
||
📝 Домашние задания:
|
||
/homework - Домашние задания детей
|
||
|
||
🔗 Управление аккаунтом:
|
||
/unlink - Отвязать Telegram аккаунт
|
||
|
||
💡 Вы будете получать уведомления о:
|
||
• Занятиях ваших детей
|
||
• Домашних заданиях детей
|
||
• Прогрессе обучения
|
||
• Отчётах от менторов
|
||
"""
|
||
else:
|
||
# Для других ролей (admin и т.д.)
|
||
help_text = """
|
||
🔔 Доступные команды:
|
||
|
||
/start - Начать работу с ботом
|
||
/help - Эта справка
|
||
/settings - Настройки уведомлений
|
||
/status - Статус связывания
|
||
/unlink - Отвязать аккаунт
|
||
|
||
💡 Вы будете получать уведомления о событиях на платформе.
|
||
"""
|
||
|
||
# Добавляем клавиатуру если аккаунт связан
|
||
if db_user:
|
||
keyboard = get_main_keyboard(db_user.role)
|
||
await update.message.reply_text(help_text, reply_markup=keyboard)
|
||
else:
|
||
await update.message.reply_text(help_text)
|
||
|
||
async def link_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Обработчик команды /link.
|
||
Связывание Telegram аккаунта с аккаунтом на платформе.
|
||
"""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
telegram_username = user.username or ''
|
||
|
||
# Проверяем наличие кода
|
||
if not context.args:
|
||
await update.message.reply_text(
|
||
"❌ Укажите код связывания.\n\n"
|
||
"Использование: /link <код>\n\n"
|
||
"Получите код на платформе: Профиль → Настройки → Telegram"
|
||
)
|
||
return
|
||
|
||
link_code = context.args[0]
|
||
|
||
# Проверяем код и связываем аккаунт
|
||
from .services import TelegramLinkService
|
||
|
||
try:
|
||
result = await sync_to_async(TelegramLinkService.link_account)(
|
||
link_code=link_code,
|
||
telegram_id=telegram_id,
|
||
telegram_username=telegram_username
|
||
)
|
||
|
||
if result['success']:
|
||
user_name = result.get('user_name', 'Пользователь')
|
||
# Получаем роль пользователя для клавиатуры
|
||
from apps.users.models import User
|
||
linked_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
keyboard = get_main_keyboard(linked_user.role if linked_user else None)
|
||
await update.message.reply_text(
|
||
f"✅ Аккаунт успешно связан!\n\n"
|
||
f"👤 {user_name}\n\n"
|
||
f"Теперь вы будете получать уведомления в Telegram.",
|
||
reply_markup=keyboard
|
||
)
|
||
else:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
f"❌ Ошибка связывания: {result.get('error', 'Неизвестная ошибка')}",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error linking Telegram account: {e}")
|
||
await update.message.reply_text(
|
||
"❌ Произошла ошибка при связывании аккаунта.\n"
|
||
"Попробуйте позже или обратитесь в поддержку."
|
||
)
|
||
|
||
async def unlink_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Обработчик команды /unlink.
|
||
Отвязка Telegram аккаунта.
|
||
"""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from .services import TelegramLinkService
|
||
|
||
try:
|
||
result = await sync_to_async(TelegramLinkService.unlink_account)(telegram_id)
|
||
|
||
if result['success']:
|
||
await update.message.reply_text(
|
||
"✅ Аккаунт успешно отвязан.\n\n"
|
||
"Вы больше не будете получать уведомления в Telegram.\n\n"
|
||
"Чтобы снова связать аккаунт, используйте /link"
|
||
)
|
||
else:
|
||
await update.message.reply_text(
|
||
f"❌ {result.get('error', 'Аккаунт не найден')}"
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error unlinking Telegram account: {e}")
|
||
await update.message.reply_text(
|
||
"❌ Произошла ошибка при отвязке аккаунта."
|
||
)
|
||
|
||
async def status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /status."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if db_user:
|
||
role_display = db_user.get_role_display()
|
||
role_emoji = {
|
||
'mentor': '👨🏫',
|
||
'client': '👨🎓',
|
||
'parent': '👨👩👧',
|
||
'admin': '👤'
|
||
}.get(db_user.role, '👤')
|
||
|
||
status_message = (
|
||
f"✅ <b>Аккаунт связан</b>\n\n"
|
||
f"{role_emoji} {db_user.get_full_name() or db_user.email}\n"
|
||
f"📧 {db_user.email}\n"
|
||
f"🎭 Роль: {role_display}\n\n"
|
||
)
|
||
|
||
# Дополнительная информация в зависимости от роли
|
||
if db_user.role == 'parent':
|
||
from apps.users.models import Parent
|
||
try:
|
||
parent_profile = await sync_to_async(lambda: db_user.parent_profile)()
|
||
children = await sync_to_async(list)(parent_profile.children.all())
|
||
children_count = len(children)
|
||
status_message += f"👶 Привязано детей: {children_count}\n\n"
|
||
except:
|
||
pass
|
||
|
||
# Проверяем настройки уведомлений
|
||
try:
|
||
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
|
||
notifications_status = "✅ Включены" if preferences.telegram_enabled else "❌ Выключены"
|
||
status_message += f"🔔 Уведомления: {notifications_status}"
|
||
except:
|
||
status_message += "🔔 Уведомления: ⚙️ Настройте на платформе"
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(status_message, parse_mode='HTML', reply_markup=keyboard)
|
||
else:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан\n\n"
|
||
"Используйте /link <код> для связывания аккаунта.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error checking status: {e}")
|
||
await update.message.reply_text(
|
||
"❌ Ошибка проверки статуса."
|
||
)
|
||
|
||
async def settings_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /settings."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Получаем настройки уведомлений
|
||
try:
|
||
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
|
||
except:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"⚙️ Настройки уведомлений не найдены.\n\n"
|
||
"Настройте уведомления на платформе: Профиль → Настройки",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Формируем расширенное меню настроек
|
||
message_text = "⚙️ <b>Настройки</b>\n\n"
|
||
|
||
# Статус уведомлений
|
||
all_status = "✅ Включены" if preferences.enabled else "❌ Выключены"
|
||
telegram_status = "✅ Вкл" if preferences.telegram_enabled else "❌ Выкл"
|
||
email_status = "✅ Вкл" if preferences.email_enabled else "❌ Выкл"
|
||
in_app_status = "✅ Вкл" if preferences.in_app_enabled else "❌ Выкл"
|
||
|
||
message_text += f"🔔 Все уведомления: {all_status}\n"
|
||
message_text += f"📱 Telegram: {telegram_status}\n"
|
||
message_text += f"📧 Email: {email_status}\n"
|
||
message_text += f"💬 В приложении: {in_app_status}\n\n"
|
||
|
||
# Режим тишины
|
||
if preferences.quiet_hours_enabled and preferences.quiet_hours_start and preferences.quiet_hours_end:
|
||
quiet_start = preferences.quiet_hours_start.strftime('%H:%M')
|
||
quiet_end = preferences.quiet_hours_end.strftime('%H:%M')
|
||
message_text += f"🔇 Режим тишины: {quiet_start} - {quiet_end}\n\n"
|
||
else:
|
||
message_text += "🔇 Режим тишины: ❌ Выключен\n\n"
|
||
|
||
# Язык
|
||
user_lang = db_user.language or 'ru'
|
||
lang_display = 'Русский' if user_lang == 'ru' else 'English'
|
||
message_text += f"🌐 Язык: {lang_display}\n"
|
||
|
||
# Создаем inline клавиатуру
|
||
keyboard = []
|
||
|
||
# Основные настройки уведомлений
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
"🔔 " + ("Выключить все" if preferences.enabled else "Включить все"),
|
||
callback_data="settings_toggle_all"
|
||
)
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
"📱 Telegram: " + ("Выкл" if preferences.telegram_enabled else "Вкл"),
|
||
callback_data="settings_toggle_telegram"
|
||
),
|
||
InlineKeyboardButton(
|
||
"📧 Email: " + ("Выкл" if preferences.email_enabled else "Вкл"),
|
||
callback_data="settings_toggle_email"
|
||
)
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
"🔇 Режим тишины",
|
||
callback_data="settings_quiet_hours"
|
||
),
|
||
InlineKeyboardButton(
|
||
"📋 Типы уведомлений",
|
||
callback_data="settings_notification_types"
|
||
)
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("🌐 Язык", callback_data="settings_language")
|
||
])
|
||
|
||
# Добавляем кнопку с URL только если это не localhost
|
||
frontend_url = settings.FRONTEND_URL
|
||
if frontend_url and 'localhost' not in frontend_url and '127.0.0.1' not in frontend_url:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🌐 Открыть на сайте", url=f"{frontend_url}/profile")
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
# Добавляем основную клавиатуру вместе с inline кнопками
|
||
main_keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
message_text,
|
||
parse_mode='HTML',
|
||
reply_markup=reply_markup
|
||
)
|
||
# Отправляем отдельное сообщение с основной клавиатурой
|
||
await update.message.reply_text(
|
||
"Используйте кнопки ниже для навигации:",
|
||
reply_markup=main_keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting settings: {e}")
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения настроек."
|
||
)
|
||
|
||
async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик нажатий на кнопки."""
|
||
query = update.callback_query
|
||
await query.answer()
|
||
|
||
# Сначала обрабатываем настройки (они имеют приоритет)
|
||
if (query.data.startswith('settings_') or query.data.startswith('toggle_type_')
|
||
or query.data.startswith('set_timezone_') or query.data.startswith('set_language_')
|
||
or query.data.startswith('quiet_hours_') or query.data == "noop"):
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await query.edit_message_text("❌ Аккаунт не связан.")
|
||
return
|
||
|
||
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
|
||
|
||
# Переключение всех уведомлений
|
||
if query.data == "settings_toggle_all":
|
||
preferences.enabled = not preferences.enabled
|
||
await sync_to_async(preferences.save)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Переключение Telegram уведомлений
|
||
elif query.data == "settings_toggle_telegram":
|
||
preferences.telegram_enabled = not preferences.telegram_enabled
|
||
await sync_to_async(preferences.save)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Переключение Email уведомлений
|
||
elif query.data == "settings_toggle_email":
|
||
preferences.email_enabled = not preferences.email_enabled
|
||
await sync_to_async(preferences.save)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Режим тишины
|
||
elif query.data == "settings_quiet_hours":
|
||
await self._handle_quiet_hours(query, db_user, preferences)
|
||
return
|
||
|
||
# Обработка режима тишины
|
||
elif query.data.startswith("quiet_hours_"):
|
||
from datetime import time
|
||
|
||
if query.data == "quiet_hours_disable":
|
||
preferences.quiet_hours_enabled = False
|
||
await sync_to_async(preferences.save)()
|
||
await query.answer("Режим тишины выключен")
|
||
await self._handle_quiet_hours(query, db_user, preferences)
|
||
return
|
||
elif query.data == "quiet_hours_enable_22_8":
|
||
preferences.quiet_hours_enabled = True
|
||
preferences.quiet_hours_start = time(22, 0)
|
||
preferences.quiet_hours_end = time(8, 0)
|
||
await sync_to_async(preferences.save)()
|
||
await query.answer("Режим тишины: 22:00 - 08:00")
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
elif query.data == "quiet_hours_enable_23_7":
|
||
preferences.quiet_hours_enabled = True
|
||
preferences.quiet_hours_start = time(23, 0)
|
||
preferences.quiet_hours_end = time(7, 0)
|
||
await sync_to_async(preferences.save)()
|
||
await query.answer("Режим тишины: 23:00 - 07:00")
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
elif query.data == "quiet_hours_custom":
|
||
await query.answer("Настройка времени через сайт", show_alert=True)
|
||
await self._handle_quiet_hours(query, db_user, preferences)
|
||
return
|
||
|
||
# Типы уведомлений
|
||
elif query.data == "settings_notification_types":
|
||
await self._handle_notification_types(query, db_user, preferences)
|
||
return
|
||
|
||
# Часовой пояс — кнопка убрана; при нажатии старой кнопки просто возврат в настройки
|
||
elif query.data == "settings_timezone":
|
||
await query.answer()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Язык
|
||
elif query.data == "settings_language":
|
||
await self._handle_language(query, db_user)
|
||
return
|
||
|
||
# Обработка типов уведомлений
|
||
elif query.data.startswith("toggle_type_"):
|
||
ntype = query.data.replace("toggle_type_", "")
|
||
type_prefs = preferences.type_preferences.get(ntype, {})
|
||
if not isinstance(type_prefs, dict):
|
||
type_prefs = {}
|
||
|
||
current = type_prefs.get('telegram', True)
|
||
type_prefs['telegram'] = not current
|
||
preferences.type_preferences[ntype] = type_prefs
|
||
await sync_to_async(preferences.save)()
|
||
|
||
# Получаем название типа для ответа
|
||
from .models import Notification
|
||
type_display = dict(Notification.TYPE_CHOICES).get(ntype, ntype)
|
||
status = "включены" if not current else "выключены"
|
||
await query.answer(f"{type_display} {status}")
|
||
await self._handle_notification_types(query, db_user, preferences)
|
||
return
|
||
|
||
# Игнорируем заголовки (noop)
|
||
elif query.data == "noop":
|
||
await query.answer()
|
||
return
|
||
|
||
# Установка часового пояса (меню убрано; при нажатии старой кнопки — возврат в настройки)
|
||
elif query.data.startswith("set_timezone_"):
|
||
await query.answer()
|
||
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Установка языка
|
||
elif query.data.startswith("set_language_"):
|
||
language = query.data.replace("set_language_", "")
|
||
db_user.language = language
|
||
await sync_to_async(db_user.save)(update_fields=['language'])
|
||
await query.answer(f"Язык установлен: {language}")
|
||
await self._handle_language(query, db_user)
|
||
return
|
||
|
||
# Возврат к настройкам
|
||
elif query.data == "settings_back":
|
||
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error handling settings callback: {e}", exc_info=True)
|
||
await query.edit_message_text("❌ Ошибка обновления настроек.")
|
||
return
|
||
|
||
# Обработка домашних заданий и кнопок ментора (список заданий → выбор задания)
|
||
if (query.data.startswith('homework_') or query.data.startswith('mentor_submission_')
|
||
or query.data.startswith('submission_') or query.data == 'mentor_homework_back'):
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await query.answer("❌ Аккаунт не связан", show_alert=True)
|
||
return
|
||
|
||
try:
|
||
if query.data.startswith('homework_upload_'):
|
||
# Загрузка решения ДЗ
|
||
homework_id = int(query.data.replace('homework_upload_', ''))
|
||
|
||
from apps.homework.models import Homework
|
||
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
|
||
|
||
# Проверяем доступ
|
||
if db_user.role != 'client' or db_user not in await sync_to_async(list)(homework.assigned_to.all()):
|
||
await query.answer("❌ У вас нет доступа к этому заданию", show_alert=True)
|
||
return
|
||
|
||
# Сохраняем ID задания в контексте
|
||
context.user_data['waiting_for_homework_file'] = homework_id
|
||
|
||
await query.answer("📎 Отправьте файл или фото для решения")
|
||
await query.edit_message_text(
|
||
f"📎 <b>Загрузка решения</b>\n\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Отправьте:\n"
|
||
f"• Текст решения\n"
|
||
f"• Файл (документ)\n"
|
||
f"• Фото с решением\n\n"
|
||
f"Или отправьте 'отмена' для отмены.",
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
elif query.data.startswith('homework_detail_'):
|
||
# Детали задания
|
||
homework_id = int(query.data.replace('homework_detail_', ''))
|
||
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
|
||
|
||
deadline_str = homework.deadline.strftime('%d.%m.%Y в %H:%M') if homework.deadline else 'Без дедлайна'
|
||
|
||
submission = await sync_to_async(
|
||
HomeworkSubmission.objects.filter(
|
||
homework=homework,
|
||
student=db_user
|
||
).first
|
||
)()
|
||
|
||
message = f"📝 <b>{homework.title}</b>\n\n"
|
||
message += f"📅 Дедлайн: {deadline_str}\n"
|
||
if homework.description:
|
||
desc = homework.description[:200] + "..." if len(homework.description) > 200 else homework.description
|
||
message += f"\n📄 {desc}\n"
|
||
|
||
if submission:
|
||
message += f"\n✅ <b>Статус: Сдано</b>\n"
|
||
if submission.status == 'graded' and submission.score is not None:
|
||
message += f"🎯 Оценка: {submission.score}/{homework.max_score}\n"
|
||
elif submission.status == 'returned':
|
||
message += f"🔄 Возвращено на доработку\n"
|
||
else:
|
||
message += f"⏳ Ожидает проверки\n"
|
||
else:
|
||
message += f"\n⏳ <b>Статус: Не сдано</b>\n"
|
||
|
||
inline_keyboard = []
|
||
if not submission or submission.status == 'returned':
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
"📎 Загрузить решение",
|
||
callback_data=f"homework_upload_{homework.id}"
|
||
)
|
||
])
|
||
|
||
if submission:
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
"👁️ Просмотреть решение",
|
||
callback_data=f"homework_view_{submission.id}"
|
||
)
|
||
])
|
||
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
"◀️ Назад к списку",
|
||
callback_data="homework_back"
|
||
)
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
return
|
||
|
||
elif query.data.startswith('homework_view_'):
|
||
# Просмотр решения
|
||
submission_id = int(query.data.replace('homework_view_', ''))
|
||
|
||
from apps.homework.models import HomeworkSubmission
|
||
submission = await sync_to_async(HomeworkSubmission.objects.select_related('homework', 'student').get)(id=submission_id)
|
||
if submission.student_id != db_user.id and db_user.role != 'mentor':
|
||
await query.answer("❌ У вас нет доступа", show_alert=True)
|
||
return
|
||
view_data = await sync_to_async(lambda s: (
|
||
s.homework.title,
|
||
s.student.get_full_name(),
|
||
s.homework.max_score,
|
||
s.submitted_at.strftime('%d.%m.%Y в %H:%M') if s.submitted_at else 'Неизвестно',
|
||
s.get_status_display(),
|
||
))(submission)
|
||
hw_title_v, student_name_v, hw_max_v, submitted_at_v, status_v = view_data
|
||
message = f"👁️ <b>Решение ДЗ</b>\n\n"
|
||
message += f"📝 Задание: {hw_title_v}\n"
|
||
message += f"👤 Студент: {student_name_v}\n"
|
||
message += f"📅 Сдано: {submitted_at_v}\n"
|
||
message += f"📊 Статус: {status_v}\n"
|
||
if submission.score is not None:
|
||
message += f"🎯 Оценка: {submission.score}/{hw_max_v}\n"
|
||
|
||
# Комментарии (экранируем HTML, чтобы не сломать сообщение в Telegram)
|
||
import html as _html
|
||
if submission.feedback and submission.feedback.strip():
|
||
feedback = submission.feedback[:500] + "..." if len(submission.feedback) > 500 else submission.feedback
|
||
message += f"\n💬 <b>Комментарий ментора:</b>\n<i>{_html.escape(feedback)}</i>\n"
|
||
if submission.ai_checked_at and submission.ai_feedback and submission.ai_feedback.strip():
|
||
ai_feedback = submission.ai_feedback[:500] + "..." if len(submission.ai_feedback) > 500 else submission.ai_feedback
|
||
message += f"\n🤖 <b>Комментарий ИИ:</b>\n<i>{_html.escape(ai_feedback)}</i>\n"
|
||
|
||
if submission.attachment:
|
||
message += f"\n📎 Файл прикреплен"
|
||
|
||
inline_keyboard = [[
|
||
InlineKeyboardButton(
|
||
"🌐 Открыть на сайте",
|
||
url=f"{settings.FRONTEND_URL}/homework"
|
||
)
|
||
]]
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
return
|
||
|
||
elif query.data.startswith('mentor_submission_'):
|
||
# Просмотр submission ментором: все обращения к БД/связям — только внутри sync_to_async
|
||
submission_id = int(query.data.replace('mentor_submission_', ''))
|
||
import html
|
||
|
||
def _load_mentor_submission(sid, mentor_user_id):
|
||
from apps.homework.models import HomeworkSubmission, HomeworkFile
|
||
sub = HomeworkSubmission.objects.select_related('homework', 'student').prefetch_related(
|
||
'files'
|
||
).get(id=sid)
|
||
if sub.homework.mentor_id != mentor_user_id:
|
||
return None
|
||
# Основной файл
|
||
att_name = None
|
||
has_att = bool(getattr(sub, 'attachment', None))
|
||
if has_att:
|
||
att_name = getattr(sub.attachment, 'name', None) or 'Файл'
|
||
if att_name and '/' in att_name:
|
||
att_name = att_name.split('/')[-1]
|
||
# Список всех файлов: основной + доп. файлы решения (используем prefetch)
|
||
file_names = []
|
||
if att_name:
|
||
file_names.append(att_name)
|
||
for hf in sub.files.all():
|
||
if getattr(hf, 'file_type', None) == 'submission' and hf.file:
|
||
name = getattr(hf.file, 'name', None) or 'Файл'
|
||
if '/' in name:
|
||
name = name.split('/')[-1]
|
||
if name and name not in file_names:
|
||
file_names.append(name)
|
||
return {
|
||
'id': sub.id,
|
||
'student_name': sub.student.get_full_name(),
|
||
'hw_title': sub.homework.title,
|
||
'hw_id': sub.homework_id,
|
||
'max_score': sub.homework.max_score,
|
||
'submitted_at': sub.submitted_at,
|
||
'status_display': sub.get_status_display(),
|
||
'content': (sub.content or '').strip(),
|
||
'attachment_url': (sub.attachment_url or '').strip(),
|
||
'score': sub.score,
|
||
'status': sub.status,
|
||
'ai_checked_at': sub.ai_checked_at,
|
||
'ai_score': sub.ai_score,
|
||
'ai_feedback': (sub.ai_feedback or '').strip(),
|
||
'feedback': (sub.feedback or '').strip(),
|
||
'has_attachment': has_att,
|
||
'attachment_name': att_name,
|
||
'file_names': file_names,
|
||
'sub': sub,
|
||
}
|
||
|
||
data = await sync_to_async(_load_mentor_submission)(submission_id, db_user.id)
|
||
if data is None or db_user.role != 'mentor':
|
||
await query.answer("❌ У вас нет доступа", show_alert=True)
|
||
return
|
||
|
||
submitted_at = data['submitted_at'].strftime('%d.%m.%Y в %H:%M') if data['submitted_at'] else 'Неизвестно'
|
||
message = f"📝 <b>{data['hw_title']}</b>\n\n"
|
||
message += f"👤 <b>Студент:</b> {data['student_name']}\n"
|
||
message += f"📅 <b>Сдано:</b> {submitted_at}\n"
|
||
message += f"📊 <b>Статус:</b> {data['status_display']}\n\n"
|
||
|
||
# Текст, который отправил студент
|
||
message += "📄 <b>Текст от студента:</b>\n"
|
||
if data['content']:
|
||
content_preview = data['content'][:1500] + "…" if len(data['content']) > 1500 else data['content']
|
||
message += "<i>" + html.escape(content_preview) + "</i>\n\n"
|
||
else:
|
||
message += "<i>Студент текста не написал.</i>\n\n"
|
||
|
||
# Все прикреплённые файлы (основной + доп. из HomeworkFile)
|
||
message += "📎 <b>Прикреплённые файлы:</b>\n"
|
||
file_names = data.get('file_names') or []
|
||
if file_names:
|
||
for i, name in enumerate(file_names[:15], 1):
|
||
message += f" {i}. {html.escape(name)}\n"
|
||
if len(file_names) > 15:
|
||
message += f" … и ещё {len(file_names) - 15}\n"
|
||
else:
|
||
message += " <i>Файлов нет</i>\n"
|
||
if data['attachment_url']:
|
||
message += f"🔗 <b>Ссылка студента:</b> {data['attachment_url'][:100]}{'…' if len(data['attachment_url']) > 100 else ''}\n"
|
||
message += "\n<i>Скачать/открыть файлы — кнопка «Открыть на сайте» ниже.</i>\n\n"
|
||
|
||
if data['status'] == 'graded' and data['score'] is not None:
|
||
message += f"🎯 <b>Оценка:</b> {data['score']}/{data['max_score']}\n\n"
|
||
|
||
message += "💬 <b>Комментарии к решению:</b>\n"
|
||
has_comment = False
|
||
if data['ai_checked_at'] and data['ai_feedback']:
|
||
ai_score_text = f"{data['ai_score']}/5" if data['ai_score'] is not None else "—"
|
||
message += f"🤖 <b>ИИ</b> (оценка: {ai_score_text}):\n"
|
||
ai_fb = data['ai_feedback'][:800] + "..." if len(data['ai_feedback']) > 800 else data['ai_feedback']
|
||
message += f"<i>{html.escape(ai_fb)}</i>\n\n"
|
||
has_comment = True
|
||
if data['feedback']:
|
||
message += "✏️ <b>Ваш комментарий:</b>\n"
|
||
fb = data['feedback'][:800] + "..." if len(data['feedback']) > 800 else data['feedback']
|
||
message += f"<i>{html.escape(fb)}</i>\n"
|
||
has_comment = True
|
||
if not has_comment:
|
||
message += "<i>Пока нет комментариев.</i>\n"
|
||
|
||
inline_keyboard = [
|
||
[InlineKeyboardButton("✏️ Редактировать ответ", callback_data=f"submission_edit_feedback_{data['id']}")],
|
||
]
|
||
if data['status'] == 'pending' or (data['ai_checked_at'] and data['status'] != 'graded'):
|
||
if data['ai_score'] and data['ai_feedback']:
|
||
inline_keyboard.append([InlineKeyboardButton("💾 Сохранить ответ", callback_data=f"submission_publish_ai_{data['id']}")])
|
||
inline_keyboard.append([InlineKeyboardButton("⭐ Выставить оценку", callback_data=f"submission_grade_{data['id']}")])
|
||
elif data['status'] == 'graded':
|
||
inline_keyboard.append([InlineKeyboardButton("⭐ Изменить оценку", callback_data=f"submission_grade_{data['id']}")])
|
||
# Вернуть на пересдачу — студент получит уведомление
|
||
inline_keyboard.append([InlineKeyboardButton("🔄 Вернуть на доработку", callback_data=f"submission_return_revision_{data['id']}")])
|
||
inline_keyboard.append([InlineKeyboardButton("🌐 Открыть на сайте", url=f"{settings.FRONTEND_URL}/homework/{data['hw_id']}/submissions/{data['id']}/")])
|
||
inline_keyboard.append([InlineKeyboardButton("◀️ Назад к списку", callback_data="mentor_homework_back")])
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
# Картинка вложения — только в sync_to_async, submission в data['sub']
|
||
def _get_attachment_photo_data(s):
|
||
if not s:
|
||
return None, None, False
|
||
att = getattr(s, 'attachment', None)
|
||
if not att:
|
||
return None, None, False
|
||
name = (getattr(att, 'name', None) or '').lower()
|
||
if not any(name.endswith(ext) for ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp')):
|
||
return None, None, False
|
||
path, url = None, None
|
||
try:
|
||
path = getattr(att, 'path', None)
|
||
path = str(path) if path else None
|
||
except Exception:
|
||
pass
|
||
try:
|
||
url = getattr(att, 'url', None)
|
||
except Exception:
|
||
pass
|
||
return path, url, True
|
||
|
||
_path, _url, _is_image = await sync_to_async(_get_attachment_photo_data)(data['sub'])
|
||
if _is_image:
|
||
try:
|
||
import os
|
||
sent = False
|
||
if _path and os.path.exists(_path):
|
||
await context.bot.send_photo(
|
||
chat_id=query.message.chat_id,
|
||
photo=_path,
|
||
caption="📎 Файл решения (прикреплён учеником)"
|
||
)
|
||
sent = True
|
||
if not sent and _url:
|
||
base = getattr(settings, 'BASE_URL', None) or getattr(settings, 'API_URL', None) or ''
|
||
if base:
|
||
photo_url = base.rstrip('/') + _url
|
||
await context.bot.send_photo(
|
||
chat_id=query.message.chat_id,
|
||
photo=photo_url,
|
||
caption="📎 Файл решения (прикреплён учеником)"
|
||
)
|
||
except Exception as img_err:
|
||
logger.warning("Could not send submission image to Telegram: %s", img_err)
|
||
|
||
return
|
||
|
||
elif query.data.startswith('submission_publish_ai_'):
|
||
# Публикация черновика ИИ как есть
|
||
submission_id = int(query.data.replace('submission_publish_ai_', ''))
|
||
|
||
from apps.homework.models import HomeworkSubmission, Homework
|
||
submission = await sync_to_async(
|
||
HomeworkSubmission.objects.select_related('homework', 'student').get
|
||
)(id=submission_id)
|
||
homework_mentor_id = await sync_to_async(
|
||
lambda hw_id: Homework.objects.values_list('mentor_id', flat=True).get(id=hw_id)
|
||
)(submission.homework_id)
|
||
if db_user.role != 'mentor' or homework_mentor_id != db_user.id:
|
||
await query.answer("❌ У вас нет доступа", show_alert=True)
|
||
return
|
||
|
||
if not submission.ai_score or not submission.ai_feedback:
|
||
await query.answer("❌ Нет данных от ИИ для публикации", show_alert=True)
|
||
return
|
||
|
||
# Публикуем оценку и комментарий ИИ
|
||
await sync_to_async(submission.grade)(
|
||
submission.ai_score,
|
||
submission.ai_feedback,
|
||
checked_by=db_user
|
||
)
|
||
submission.graded_by_ai = True
|
||
await sync_to_async(submission.save)(update_fields=['graded_by_ai'])
|
||
|
||
# Обновляем статистику и уведомление — всё в sync (доступ к homework/student)
|
||
def _publish_ai_and_notify(sub):
|
||
sub.homework.update_statistics()
|
||
from apps.notifications.services import NotificationService
|
||
msg = f'Проверено ДЗ "{sub.homework.title}". Оценка: {sub.ai_score}/{sub.homework.max_score}'
|
||
if sub.ai_feedback and str(sub.ai_feedback).strip():
|
||
comment = (sub.ai_feedback[:500] + '…') if len(sub.ai_feedback) > 500 else sub.ai_feedback
|
||
msg += f'\n\n💬 Комментарий:\n{comment}'
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=sub.student,
|
||
notification_type='homework_reviewed',
|
||
title='✅ ДЗ проверено',
|
||
message=msg,
|
||
priority='normal',
|
||
action_url=f'/homework/{sub.homework.id}/submissions/{sub.id}/',
|
||
content_object=sub
|
||
)
|
||
return (sub.homework.title, sub.student.get_full_name(), sub.homework.max_score)
|
||
hw_title, student_name, hw_max = await sync_to_async(_publish_ai_and_notify)(submission)
|
||
|
||
await query.answer("✅ Оценка опубликована!")
|
||
await query.edit_message_text(
|
||
f"✅ <b>Оценка опубликована!</b>\n\n"
|
||
f"📝 Задание: {hw_title}\n"
|
||
f"👤 Студент: {student_name}\n"
|
||
f"⭐ Оценка: {submission.ai_score}/{hw_max}\n"
|
||
f"💬 Комментарий: {submission.ai_feedback[:200]}{'...' if len(submission.ai_feedback) > 200 else ''}\n\n"
|
||
f"📤 Студент получил уведомление.",
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
elif query.data.startswith('submission_grade_'):
|
||
# Начало редактирования оценки
|
||
submission_id = int(query.data.replace('submission_grade_', ''))
|
||
|
||
from apps.homework.models import HomeworkSubmission, Homework
|
||
submission = await sync_to_async(HomeworkSubmission.objects.select_related('homework', 'student').get)(id=submission_id)
|
||
homework_mentor_id = await sync_to_async(
|
||
lambda hw_id: Homework.objects.values_list('mentor_id', flat=True).get(id=hw_id)
|
||
)(submission.homework_id)
|
||
if db_user.role != 'mentor' or homework_mentor_id != db_user.id:
|
||
await query.answer("❌ У вас нет доступа", show_alert=True)
|
||
return
|
||
|
||
grade_display = await sync_to_async(lambda s: (
|
||
s.score if s.score is not None else (s.ai_score if s.ai_score else None),
|
||
s.homework.max_score,
|
||
s.homework.title,
|
||
s.student.get_full_name()
|
||
))(submission)
|
||
current_score, hw_max, hw_title, student_name = grade_display
|
||
current_score_text = f" (текущая: {current_score}/{hw_max})" if current_score else ""
|
||
|
||
context.user_data['waiting_for_submission_score'] = submission_id
|
||
await query.answer()
|
||
await query.message.reply_text(
|
||
f"⭐ <b>Выставление оценки</b>\n\n"
|
||
f"📝 Задание: {hw_title}\n"
|
||
f"👤 Студент: {student_name}\n\n"
|
||
f"Введите оценку от 1 до {hw_max}{current_score_text}.\n\n"
|
||
f"Или отправьте оценку и комментарий в формате:\n"
|
||
f"<code>5\nОтличная работа!</code>",
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
elif query.data.startswith('submission_edit_feedback_'):
|
||
# Начало редактирования ответа
|
||
import html as _html_mod
|
||
submission_id = int(query.data.replace('submission_edit_feedback_', ''))
|
||
|
||
from apps.homework.models import HomeworkSubmission, Homework
|
||
submission = await sync_to_async(HomeworkSubmission.objects.select_related('homework', 'student').get)(id=submission_id)
|
||
homework_mentor_id = await sync_to_async(
|
||
lambda hw_id: Homework.objects.values_list('mentor_id', flat=True).get(id=hw_id)
|
||
)(submission.homework_id)
|
||
if db_user.role != 'mentor' or homework_mentor_id != db_user.id:
|
||
await query.answer("❌ У вас нет доступа", show_alert=True)
|
||
return
|
||
|
||
current_feedback = (submission.feedback or submission.ai_feedback or "").strip()
|
||
cf_preview = current_feedback[:200] + "..." if len(current_feedback) > 200 else current_feedback
|
||
current_feedback_text = f"\n\nТекущий ответ:\n<i>{_html_mod.escape(cf_preview)}</i>" if current_feedback else ""
|
||
hw_title_fb, student_name_fb = await sync_to_async(
|
||
lambda s: (s.homework.title, s.student.get_full_name())
|
||
)(submission)
|
||
context.user_data['waiting_for_submission_feedback'] = submission_id
|
||
await query.answer()
|
||
await query.message.reply_text(
|
||
f"✏️ <b>Редактировать ответ</b>\n\n"
|
||
f"📝 Задание: {hw_title_fb}\n"
|
||
f"👤 Студент: {student_name_fb}\n\n"
|
||
f"Введите ваш ответ. Отправка сообщения сохранит ответ.{current_feedback_text}",
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
elif query.data.startswith('submission_return_revision_'):
|
||
# Вернуть ДЗ на доработку — студент получит уведомление
|
||
submission_id = int(query.data.replace('submission_return_revision_', ''))
|
||
from apps.homework.models import HomeworkSubmission, Homework
|
||
submission = await sync_to_async(
|
||
HomeworkSubmission.objects.select_related('homework', 'student').get
|
||
)(id=submission_id)
|
||
homework_mentor_id = await sync_to_async(
|
||
lambda hw_id: Homework.objects.values_list('mentor_id', flat=True).get(id=hw_id)
|
||
)(submission.homework_id)
|
||
if db_user.role != 'mentor' or homework_mentor_id != db_user.id:
|
||
await query.answer("❌ У вас нет доступа", show_alert=True)
|
||
return
|
||
hw_title_rr, student_name_rr = await sync_to_async(
|
||
lambda s: (s.homework.title, s.student.get_full_name())
|
||
)(submission)
|
||
context.user_data['waiting_for_return_revision'] = submission_id
|
||
await query.answer()
|
||
await query.message.reply_text(
|
||
f"🔄 <b>Вернуть на доработку</b>\n\n"
|
||
f"📝 Задание: {hw_title_rr}\n"
|
||
f"👤 Студент: {student_name_rr}\n\n"
|
||
f"Введите комментарий для студента (почему нужно доработать). "
|
||
f"Студент получит уведомление и сможет отправить решение заново.\n\n"
|
||
f"Или отправьте «отмена» для отмены.",
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
elif query.data == 'mentor_homework_back':
|
||
# Возврат к списку заданий для ментора
|
||
from apps.homework.models import HomeworkSubmission
|
||
from django.utils import timezone
|
||
|
||
def _mentor_submissions_list(mentor_user):
|
||
qs = HomeworkSubmission.objects.filter(
|
||
homework__mentor=mentor_user,
|
||
status='pending'
|
||
).select_related('homework', 'student').order_by('-submitted_at')[:5]
|
||
return [
|
||
{
|
||
'id': s.id,
|
||
'student_name': s.student.get_full_name() if s.student else 'Студент',
|
||
'hw_title': s.homework.title or 'Без названия',
|
||
'submitted_at': s.submitted_at.strftime('%d.%m.%Y') if s.submitted_at else 'Неизвестно',
|
||
'ai_checked_at': getattr(s, 'ai_checked_at', None),
|
||
}
|
||
for s in qs
|
||
]
|
||
submissions_data = await sync_to_async(_mentor_submissions_list)(db_user)
|
||
if not submissions_data:
|
||
message = "📝 Нет домашних заданий, требующих проверки."
|
||
keyboard = await get_user_keyboard(update.effective_user.id)
|
||
await query.edit_message_text(message, parse_mode='HTML')
|
||
await query.message.reply_text("Используйте кнопки для навигации:", reply_markup=keyboard)
|
||
return
|
||
message = "📝 <b>Домашние задания на проверку:</b>\n\n"
|
||
inline_keyboard = []
|
||
for item in submissions_data:
|
||
ai_status = " 🤖 ИИ проверил" if item['ai_checked_at'] else ""
|
||
message += f"<b>{item['hw_title']}</b>{ai_status}\n"
|
||
message += f"👤 {item['student_name']}\n"
|
||
message += f"📅 Сдано: {item['submitted_at']}\n\n"
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"👁️ {item['hw_title'][:30]} — {item['student_name']}",
|
||
callback_data=f"mentor_submission_{item['id']}"
|
||
)
|
||
])
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
return
|
||
|
||
elif query.data == 'homework_back':
|
||
# Возврат к списку заданий в виде кнопок в панели
|
||
from apps.homework.models import Homework
|
||
from django.utils import timezone
|
||
|
||
# Используем правильный sync_to_async
|
||
def get_homeworks():
|
||
return list(Homework.objects.filter(
|
||
assigned_to=db_user,
|
||
deadline__gte=timezone.now()
|
||
).order_by('deadline')[:5])
|
||
|
||
homeworks = await sync_to_async(get_homeworks)()
|
||
|
||
if not homeworks:
|
||
keyboard = await get_user_keyboard(query.from_user.id)
|
||
await query.edit_message_text("📝 У вас нет активных домашних заданий.")
|
||
await context.bot.send_message(
|
||
chat_id=query.message.chat_id,
|
||
text="Возврат в главное меню.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Возврат к списку в виде кнопок в панели
|
||
homework_keyboard, homework_buttons = make_homework_list_keyboard(homeworks)
|
||
context.user_data['homework_buttons'] = homework_buttons
|
||
context.user_data['homework_list_active'] = True
|
||
context.user_data.pop('current_homework_id', None)
|
||
|
||
await query.edit_message_text(
|
||
"📝 <b>Домашние задания</b>\n\nВыберите задание кнопкой в панели ниже:",
|
||
parse_mode='HTML'
|
||
)
|
||
await context.bot.send_message(
|
||
chat_id=query.message.chat_id,
|
||
text="👇",
|
||
reply_markup=homework_keyboard
|
||
)
|
||
return
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error handling homework callback: {e}", exc_info=True)
|
||
await query.answer("❌ Ошибка обработки запроса", show_alert=True)
|
||
return
|
||
|
||
# Обработка деталей занятия
|
||
if query.data.startswith('lesson_detail_'):
|
||
try:
|
||
lesson_id = int(query.data.replace('lesson_detail_', ''))
|
||
|
||
from apps.schedule.models import Lesson
|
||
from django.utils import timezone
|
||
import pytz
|
||
|
||
lesson = await sync_to_async(
|
||
Lesson.objects.select_related('mentor', 'client', 'client__user', 'subject').get
|
||
)(id=lesson_id)
|
||
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await query.answer("❌ Аккаунт не связан", show_alert=True)
|
||
return
|
||
|
||
# Проверяем доступ
|
||
has_access = False
|
||
if db_user.role == 'mentor' and lesson.mentor == db_user:
|
||
has_access = True
|
||
elif db_user.role == 'client' and lesson.client and lesson.client.user == db_user:
|
||
has_access = True
|
||
elif db_user.role == 'parent' and lesson.client and lesson.client.user.parent_profile and lesson.client.user.parent_profile.user == db_user:
|
||
has_access = True
|
||
|
||
if not has_access:
|
||
await query.answer("❌ У вас нет доступа к этому занятию", show_alert=True)
|
||
return
|
||
|
||
# Формируем сообщение
|
||
from apps.users.utils import get_user_timezone
|
||
user_tz = get_user_timezone(db_user.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
local_start = lesson.start_time.astimezone(user_tz)
|
||
local_end = lesson.end_time.astimezone(user_tz) if lesson.end_time else None
|
||
else:
|
||
utc_start = timezone.make_aware(lesson.start_time, pytz.UTC)
|
||
local_start = utc_start.astimezone(user_tz)
|
||
local_end = lesson.end_time.astimezone(user_tz) if lesson.end_time and timezone.is_aware(lesson.end_time) else None
|
||
|
||
message = f"📚 <b>{lesson.title}</b>\n\n"
|
||
|
||
if lesson.subject:
|
||
message += f"📖 Предмет: {lesson.subject.name}\n"
|
||
|
||
message += f"🕐 Начало: {local_start.strftime('%d.%m.%Y в %H:%M')}\n"
|
||
if local_end:
|
||
message += f"🕐 Окончание: {local_end.strftime('%d.%m.%Y в %H:%M')}\n"
|
||
message += f"⏱ Длительность: {lesson.duration} минут\n"
|
||
message += f"📊 Статус: {lesson.get_status_display()}\n\n"
|
||
|
||
if db_user.role == 'mentor':
|
||
if lesson.client:
|
||
message += f"👤 Студент: {lesson.client.user.get_full_name()}\n"
|
||
else:
|
||
message += f"👨🏫 Ментор: {lesson.mentor.get_full_name()}\n"
|
||
|
||
if lesson.description:
|
||
desc = lesson.description[:200] + "..." if len(lesson.description) > 200 else lesson.description
|
||
message += f"\n📄 {desc}\n"
|
||
|
||
if lesson.meeting_url:
|
||
message += f"\n🔗 <a href='{lesson.meeting_url}'>Ссылка на видеоконференцию</a>\n"
|
||
|
||
if lesson.mentor_grade is not None:
|
||
message += f"\n🎯 Оценка: {lesson.mentor_grade}/100\n"
|
||
|
||
inline_keyboard = [[
|
||
InlineKeyboardButton(
|
||
"🌐 Открыть на сайте",
|
||
url=f"{settings.FRONTEND_URL}/schedule"
|
||
)
|
||
]]
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
except Lesson.DoesNotExist:
|
||
await query.answer("❌ Занятие не найдено", show_alert=True)
|
||
except Exception as e:
|
||
logger.error(f"Error handling lesson detail: {e}", exc_info=True)
|
||
await query.answer("❌ Ошибка обработки запроса", show_alert=True)
|
||
return
|
||
|
||
# Обработка деталей клиента (для менторов)
|
||
if query.data.startswith('client_detail_'):
|
||
try:
|
||
client_id = int(query.data.replace('client_detail_', ''))
|
||
|
||
from apps.users.models import Client, User
|
||
from apps.schedule.models import Lesson
|
||
from apps.homework.models import HomeworkSubmission
|
||
from django.utils import timezone
|
||
from datetime import timedelta
|
||
|
||
client = await sync_to_async(
|
||
Client.objects.select_related('user').get
|
||
)(id=client_id)
|
||
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user or db_user.role != 'mentor' or db_user not in await sync_to_async(list)(client.mentors.all()):
|
||
await query.answer("❌ У вас нет доступа", show_alert=True)
|
||
return
|
||
|
||
client_name = client.user.get_full_name() or client.user.email
|
||
message = f"👤 <b>{client_name}</b>\n\n"
|
||
|
||
# Статистика занятий
|
||
all_lessons = await sync_to_async(list)(
|
||
Lesson.objects.filter(mentor=db_user, client=client)
|
||
)
|
||
total_lessons = len(all_lessons)
|
||
completed_lessons = len([l for l in all_lessons if l.status == 'completed'])
|
||
upcoming_lessons = len([
|
||
l for l in all_lessons
|
||
if l.start_time >= timezone.now() and l.status == 'scheduled'
|
||
])
|
||
|
||
# Занятия за месяц
|
||
month_ago = timezone.now() - timedelta(days=30)
|
||
lessons_this_month = len([
|
||
l for l in all_lessons
|
||
if l.start_time >= month_ago
|
||
])
|
||
|
||
message += f"📚 <b>Занятия:</b>\n"
|
||
message += f"• Всего: {total_lessons}\n"
|
||
message += f"• Завершено: {completed_lessons}\n"
|
||
message += f"• Предстоящих: {upcoming_lessons}\n"
|
||
message += f"• За месяц: {lessons_this_month}\n\n"
|
||
|
||
# Статистика ДЗ
|
||
all_submissions = await sync_to_async(list)(
|
||
HomeworkSubmission.objects.filter(
|
||
homework__mentor=db_user,
|
||
student=client.user
|
||
)
|
||
)
|
||
total_homeworks = len(all_submissions)
|
||
pending_homeworks = len([s for s in all_submissions if s.status == 'pending'])
|
||
graded_homeworks = len([s for s in all_submissions if s.status == 'graded'])
|
||
|
||
message += f"📝 <b>Домашние задания:</b>\n"
|
||
message += f"• Всего решений: {total_homeworks}\n"
|
||
message += f"• На проверке: {pending_homeworks}\n"
|
||
message += f"• Проверено: {graded_homeworks}\n"
|
||
|
||
if graded_homeworks > 0:
|
||
scores = [s.score for s in all_submissions if s.score is not None]
|
||
avg_score = sum(scores) / len(scores) if scores else 0
|
||
message += f"• Средний балл: {avg_score:.1f}\n"
|
||
|
||
# Доходы от клиента
|
||
lessons_with_price = [l for l in all_lessons if l.price and l.status == 'completed']
|
||
if lessons_with_price:
|
||
total_revenue = sum([float(l.price) for l in lessons_with_price])
|
||
message += f"\n💰 <b>Доходы:</b>\n"
|
||
message += f"• Всего: {total_revenue:.2f} ₽\n"
|
||
|
||
inline_keyboard = [[
|
||
InlineKeyboardButton(
|
||
"🌐 Открыть на сайте",
|
||
url=f"{settings.FRONTEND_URL}/students"
|
||
),
|
||
InlineKeyboardButton(
|
||
"◀️ Назад к списку",
|
||
callback_data="clients_back"
|
||
)
|
||
]]
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
except Client.DoesNotExist:
|
||
await query.answer("❌ Клиент не найден", show_alert=True)
|
||
except Exception as e:
|
||
logger.error(f"Error handling client detail: {e}", exc_info=True)
|
||
await query.answer("❌ Ошибка обработки запроса", show_alert=True)
|
||
return
|
||
|
||
# Обработка возврата к списку клиентов
|
||
if query.data == 'clients_back':
|
||
try:
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user or db_user.role != 'mentor':
|
||
await query.answer("❌ Ошибка", show_alert=True)
|
||
return
|
||
|
||
from apps.users.models import Client
|
||
clients = await sync_to_async(list)(
|
||
Client.objects.filter(mentors=db_user).select_related('user')[:10]
|
||
)
|
||
|
||
if not clients:
|
||
await query.edit_message_text("👥 У вас пока нет клиентов.")
|
||
return
|
||
|
||
message = "👥 <b>Ваши клиенты:</b>\n\n"
|
||
inline_keyboard = []
|
||
|
||
for client in clients:
|
||
client_name = client.user.get_full_name() or client.user.email
|
||
message += f"👤 <b>{client_name}</b>\n\n"
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"👤 {client_name[:30]}",
|
||
callback_data=f"client_detail_{client.id}"
|
||
)
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error handling clients back: {e}", exc_info=True)
|
||
await query.answer("❌ Ошибка", show_alert=True)
|
||
return
|
||
|
||
# Обработка подтверждения занятия (Занятие состоялось / Занятие отменилось)
|
||
if query.data.startswith('lesson_confirm_') or query.data.startswith('lesson_cancel_'):
|
||
try:
|
||
lesson_id = int(query.data.split('_')[-1])
|
||
is_confirmed = query.data.startswith('lesson_confirm_')
|
||
|
||
from apps.schedule.models import Lesson
|
||
from django.utils import timezone
|
||
|
||
lesson = await sync_to_async(
|
||
Lesson.objects.select_related('client', 'client__user', 'mentor').get
|
||
)(id=lesson_id)
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
# Только ментор может подтверждать
|
||
if not lesson.mentor or lesson.mentor.telegram_id != telegram_id:
|
||
await query.answer("❌ Только ментор занятия может подтвердить.", show_alert=True)
|
||
return
|
||
|
||
if is_confirmed:
|
||
# Занятие состоялось — оставляем completed
|
||
await query.edit_message_text(
|
||
f"✅ <b>Подтверждено</b>\n\n"
|
||
f"Занятие «{lesson.title}» состоялось."
|
||
)
|
||
else:
|
||
# Занятие отменилось — меняем статус
|
||
lesson.status = 'cancelled'
|
||
lesson.cancelled_at = timezone.now()
|
||
await sync_to_async(lesson.save)(update_fields=['status', 'cancelled_at'])
|
||
await query.edit_message_text(
|
||
f"❌ <b>Отменено</b>\n\n"
|
||
f"Занятие «{lesson.title}» отмечено как отменённое."
|
||
)
|
||
|
||
await query.answer()
|
||
except Lesson.DoesNotExist:
|
||
await query.answer("❌ Занятие не найдено", show_alert=True)
|
||
except Exception as e:
|
||
logger.error(f"Error handling lesson confirmation: {e}", exc_info=True)
|
||
await query.answer("❌ Ошибка обработки", show_alert=True)
|
||
return
|
||
|
||
# Обработка подтверждения присутствия
|
||
if query.data.startswith('attendance_yes_') or query.data.startswith('attendance_no_'):
|
||
try:
|
||
lesson_id = int(query.data.split('_')[-1])
|
||
response_bool = query.data.startswith('attendance_yes_')
|
||
|
||
from apps.schedule.models import Lesson
|
||
from django.utils import timezone
|
||
|
||
lesson = await sync_to_async(Lesson.objects.select_related('client', 'client__user', 'mentor').get)(id=lesson_id)
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
# Проверяем, что пользователь - студент этого занятия
|
||
if not lesson.client or lesson.client.user.telegram_id != telegram_id:
|
||
await query.edit_message_text(
|
||
"❌ Ошибка: вы не являетесь студентом этого занятия"
|
||
)
|
||
return
|
||
|
||
# Сохраняем ответ
|
||
lesson.attendance_confirmed = response_bool
|
||
lesson.attendance_response_at = timezone.now()
|
||
await sync_to_async(lesson.save)(update_fields=['attendance_confirmed', 'attendance_response_at'])
|
||
|
||
# Отправляем уведомление ментору
|
||
from apps.notifications.services import NotificationService
|
||
await sync_to_async(NotificationService.send_attendance_response_to_mentor)(lesson, response_bool)
|
||
|
||
response_text = "будете присутствовать" if response_bool else "не сможете присутствовать"
|
||
|
||
await query.edit_message_text(
|
||
f"✅ <b>Ответ сохранен</b>\n\n"
|
||
f"Вы подтвердили, что {response_text} на занятии:\n"
|
||
f"<b>{lesson.title}</b>\n\n"
|
||
f"Преподаватель получил уведомление."
|
||
)
|
||
|
||
except Lesson.DoesNotExist:
|
||
await query.edit_message_text("❌ Занятие не найдено")
|
||
except Exception as e:
|
||
logger.error(f"Error processing attendance confirmation: {e}")
|
||
await query.edit_message_text("❌ Ошибка обработки ответа")
|
||
return
|
||
|
||
# Обработка настроек уведомлений
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await query.edit_message_text("❌ Аккаунт не связан.")
|
||
return
|
||
|
||
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
|
||
|
||
# Переключение всех уведомлений
|
||
if query.data == "settings_toggle_all":
|
||
preferences.enabled = not preferences.enabled
|
||
await sync_to_async(preferences.save)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Переключение Telegram уведомлений
|
||
elif query.data == "settings_toggle_telegram":
|
||
preferences.telegram_enabled = not preferences.telegram_enabled
|
||
await sync_to_async(preferences.save)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Переключение Email уведомлений
|
||
elif query.data == "settings_toggle_email":
|
||
preferences.email_enabled = not preferences.email_enabled
|
||
await sync_to_async(preferences.save)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Режим тишины
|
||
elif query.data == "settings_quiet_hours":
|
||
await self._handle_quiet_hours(query, db_user, preferences)
|
||
return
|
||
|
||
# Типы уведомлений
|
||
elif query.data == "settings_notification_types":
|
||
await self._handle_notification_types(query, db_user, preferences)
|
||
return
|
||
|
||
# Часовой пояс — кнопка убрана
|
||
elif query.data == "settings_timezone":
|
||
await query.answer()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Язык
|
||
elif query.data == "settings_language":
|
||
await self._handle_language(query, db_user)
|
||
return
|
||
|
||
# Обработка типов уведомлений
|
||
elif query.data.startswith("toggle_type_"):
|
||
ntype = query.data.replace("toggle_type_", "")
|
||
type_prefs = preferences.type_preferences.get(ntype, {})
|
||
if not isinstance(type_prefs, dict):
|
||
type_prefs = {}
|
||
|
||
current = type_prefs.get('telegram', True)
|
||
type_prefs['telegram'] = not current
|
||
preferences.type_preferences[ntype] = type_prefs
|
||
await sync_to_async(preferences.save)()
|
||
|
||
await query.answer("Настройка обновлена")
|
||
await self._handle_notification_types(query, db_user, preferences)
|
||
return
|
||
|
||
# Установка часового пояса (меню убрано)
|
||
elif query.data.startswith("set_timezone_"):
|
||
await query.answer()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Установка языка
|
||
elif query.data.startswith("set_language_"):
|
||
language = query.data.replace("set_language_", "")
|
||
db_user.language = language
|
||
await sync_to_async(db_user.save)(update_fields=['language'])
|
||
await query.answer(f"Язык установлен: {language}")
|
||
await self._handle_language(query, db_user)
|
||
return
|
||
|
||
# Возврат к настройкам
|
||
elif query.data == "settings_back":
|
||
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Старый обработчик для обратной совместимости
|
||
elif query.data == "toggle_notifications":
|
||
preferences.telegram_enabled = not preferences.telegram_enabled
|
||
await sync_to_async(preferences.save)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error handling settings callback: {e}", exc_info=True)
|
||
await query.edit_message_text(
|
||
"❌ Ошибка обновления настроек."
|
||
)
|
||
|
||
async def schedule_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /schedule - показать расписание."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта."
|
||
)
|
||
return
|
||
|
||
# Получаем ближайшие занятия
|
||
from apps.schedule.models import Lesson
|
||
from django.utils import timezone
|
||
|
||
now = timezone.now()
|
||
|
||
# Для ментора - все занятия
|
||
if db_user.role == 'mentor':
|
||
lessons = await sync_to_async(list)(
|
||
Lesson.objects.filter(
|
||
mentor=db_user,
|
||
start_time__gte=now
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time')[:5]
|
||
)
|
||
# Для клиента - только его занятия
|
||
elif db_user.role == 'client':
|
||
# Проверяем наличие Client профиля
|
||
try:
|
||
from apps.users.models import Client
|
||
client_profile = await sync_to_async(
|
||
Client.objects.filter(user=db_user).first
|
||
)()
|
||
|
||
if not client_profile:
|
||
await update.message.reply_text(
|
||
"❌ Профиль клиента не найден.\n\n"
|
||
"Обратитесь к администратору для настройки профиля."
|
||
)
|
||
return
|
||
except Exception as e:
|
||
logger.error(f"Error getting client profile: {e}")
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения профиля клиента."
|
||
)
|
||
return
|
||
|
||
lessons = await sync_to_async(list)(
|
||
Lesson.objects.filter(
|
||
client=client_profile,
|
||
start_time__gte=now
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time')[:5]
|
||
)
|
||
# Для родителя - занятия всех детей
|
||
elif db_user.role == 'parent':
|
||
from apps.users.models import Parent
|
||
try:
|
||
parent_profile = await sync_to_async(lambda: db_user.parent_profile)()
|
||
children = await sync_to_async(list)(parent_profile.children.all())
|
||
|
||
if not children:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ У вас нет привязанных детей.\n\n"
|
||
"Обратитесь к администратору для привязки детей к вашему аккаунту.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Получаем занятия всех детей
|
||
child_clients = list(children) # children уже список Client объектов
|
||
lessons = await sync_to_async(list)(
|
||
Lesson.objects.filter(
|
||
client__in=child_clients,
|
||
start_time__gte=now
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time')[:5]
|
||
)
|
||
except:
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения данных родителя."
|
||
)
|
||
return
|
||
else:
|
||
await update.message.reply_text(
|
||
"❌ Эта команда доступна только для менторов, клиентов и родителей."
|
||
)
|
||
return
|
||
|
||
if not lessons:
|
||
await update.message.reply_text(
|
||
"📅 У вас нет предстоящих занятий."
|
||
)
|
||
return
|
||
|
||
message = "📅 <b>Ближайшие занятия:</b>\n\n"
|
||
|
||
for lesson in lessons:
|
||
import pytz
|
||
from apps.users.utils import get_user_timezone
|
||
user_tz = get_user_timezone(db_user.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
local_time = lesson.start_time.astimezone(user_tz)
|
||
else:
|
||
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
|
||
local_time = utc_time.astimezone(user_tz)
|
||
|
||
time_str = local_time.strftime('%d.%m.%Y в %H:%M')
|
||
|
||
if db_user.role == 'mentor':
|
||
student_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Студент'
|
||
message += f"📚 <b>{lesson.title}</b>\n"
|
||
message += f"👤 {student_name}\n"
|
||
elif db_user.role == 'parent':
|
||
child_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Ребёнок'
|
||
message += f"📚 <b>{lesson.title}</b>\n"
|
||
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
|
||
message += f"👶 {child_name}\n"
|
||
else:
|
||
message += f"📚 <b>{lesson.title}</b>\n"
|
||
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
|
||
|
||
message += f"🕐 {time_str}\n\n"
|
||
|
||
await update.message.reply_text(message, parse_mode='HTML')
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in schedule_command: {e}", exc_info=True)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения расписания.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def nextlesson_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /nextlesson - показать следующее занятие."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
from apps.schedule.models import Lesson
|
||
from django.utils import timezone
|
||
|
||
now = timezone.now()
|
||
|
||
# Находим ближайшее занятие
|
||
if db_user.role == 'mentor':
|
||
lesson = await sync_to_async(
|
||
Lesson.objects.filter(
|
||
mentor=db_user,
|
||
start_time__gte=now
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time').first
|
||
)()
|
||
elif db_user.role == 'client':
|
||
# Проверяем наличие Client профиля
|
||
try:
|
||
from apps.users.models import Client
|
||
client_profile = await sync_to_async(
|
||
Client.objects.filter(user=db_user).first
|
||
)()
|
||
|
||
if not client_profile:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Профиль клиента не найден.\n\n"
|
||
"Обратитесь к администратору для настройки профиля.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
except Exception as e:
|
||
logger.error(f"Error getting client profile: {e}")
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения профиля клиента.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
lesson = await sync_to_async(
|
||
Lesson.objects.filter(
|
||
client=client_profile,
|
||
start_time__gte=now
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time').first
|
||
)()
|
||
elif db_user.role == 'parent':
|
||
from apps.users.models import Parent
|
||
try:
|
||
parent_profile = await sync_to_async(lambda: db_user.parent_profile)()
|
||
children = await sync_to_async(list)(parent_profile.children.all())
|
||
|
||
if not children:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ У вас нет привязанных детей.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
child_clients = list(children) # children уже список Client объектов
|
||
lesson = await sync_to_async(
|
||
Lesson.objects.filter(
|
||
client__in=child_clients,
|
||
start_time__gte=now
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time').first
|
||
)()
|
||
except:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения данных родителя.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
else:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Эта команда доступна только для менторов, клиентов и родителей.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
if not lesson:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"📅 У вас нет предстоящих занятий.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
import pytz
|
||
from apps.users.utils import get_user_timezone
|
||
user_tz = get_user_timezone(db_user.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
local_time = lesson.start_time.astimezone(user_tz)
|
||
else:
|
||
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
|
||
local_time = utc_time.astimezone(user_tz)
|
||
|
||
time_str = local_time.strftime('%d.%m.%Y в %H:%M')
|
||
|
||
message = "📚 <b>Следующее занятие:</b>\n\n"
|
||
message += f"<b>{lesson.title}</b>\n"
|
||
|
||
if db_user.role == 'mentor':
|
||
student_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Студент'
|
||
message += f"👤 {student_name}\n"
|
||
elif db_user.role == 'parent':
|
||
child_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Ребёнок'
|
||
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
|
||
message += f"👶 {child_name}\n"
|
||
else:
|
||
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
|
||
|
||
message += f"🕐 {time_str}\n"
|
||
|
||
if lesson.description:
|
||
message += f"\n📝 {lesson.description[:200]}"
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in nextlesson_command: {e}", exc_info=True)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения следующего занятия.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def _get_client_homework_detail(self, homework_id: int, db_user):
|
||
"""Вернуть (текст сообщения, ReplyKeyboardMarkup) для деталей задания клиента."""
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
|
||
# Используем sync_to_async правильно для всех DB операций
|
||
try:
|
||
# Получаем homework с prefetch для оптимизации
|
||
homework = await sync_to_async(
|
||
lambda: Homework.objects.select_related('mentor').prefetch_related('assigned_to').get(id=homework_id)
|
||
)()
|
||
except Homework.DoesNotExist:
|
||
return None, None
|
||
|
||
# Проверяем доступ (используем sync_to_async для проверки M2M)
|
||
assigned_users = await sync_to_async(list)(homework.assigned_to.all())
|
||
if db_user.role != 'client' or db_user not in assigned_users:
|
||
return None, None
|
||
|
||
deadline_str = homework.deadline.strftime('%d.%m.%Y в %H:%M') if homework.deadline else 'Без дедлайна'
|
||
|
||
# Получаем submission через sync_to_async
|
||
submission = await sync_to_async(
|
||
lambda: HomeworkSubmission.objects.filter(homework=homework, student=db_user).first()
|
||
)()
|
||
|
||
message = f"📝 <b>{homework.title}</b>\n\n"
|
||
message += f"📅 Дедлайн: {deadline_str}\n"
|
||
message += f"🎯 Макс. баллов: {homework.max_score}\n"
|
||
if homework.description:
|
||
desc = homework.description[:300] + "..." if len(homework.description) > 300 else homework.description
|
||
message += f"\n📄 {desc}\n"
|
||
|
||
if submission:
|
||
message += f"\n✅ <b>Статус: Сдано</b>\n"
|
||
if submission.status == 'graded' and submission.score is not None:
|
||
message += f"🎯 Оценка: {submission.score}/{homework.max_score}\n"
|
||
# Краткий комментарий к оценке — студент видит, что есть отзыв
|
||
comment = (submission.feedback or submission.ai_feedback or "").strip()
|
||
if comment:
|
||
preview = comment[:120] + "…" if len(comment) > 120 else comment
|
||
import html as _html
|
||
message += f"💬 <i>{_html.escape(preview)}</i>\n"
|
||
elif submission.status == 'returned':
|
||
message += f"🔄 Возвращено на доработку\n"
|
||
else:
|
||
message += f"⏳ Ожидает проверки\n"
|
||
else:
|
||
message += f"\n⏳ <b>Статус: Не сдано</b>\n"
|
||
|
||
# Создаём Reply клавиатуру для управления ДЗ
|
||
keyboard_buttons = []
|
||
if not submission or submission.status == 'returned':
|
||
keyboard_buttons.append([KeyboardButton("📎 Загрузить решение")])
|
||
if submission:
|
||
keyboard_buttons.append([KeyboardButton("👁️ Просмотреть решение")])
|
||
keyboard_buttons.append([KeyboardButton("◀️ Назад к списку"), KeyboardButton("🏠 В главное меню")])
|
||
|
||
reply_keyboard = ReplyKeyboardMarkup(keyboard_buttons, resize_keyboard=True, one_time_keyboard=False)
|
||
return message, reply_keyboard
|
||
|
||
async def _show_submission_view(self, update: Update, context: ContextTypes.DEFAULT_TYPE, submission, db_user):
|
||
"""Показать просмотр решения ДЗ (оценка и комментарии к работе)."""
|
||
import html as _html
|
||
|
||
message = f"👁️ <b>Решение ДЗ</b>\n\n"
|
||
message += f"📝 Задание: {submission.homework.title}\n"
|
||
message += f"📅 Сдано: {submission.submitted_at.strftime('%d.%m.%Y в %H:%M') if submission.submitted_at else 'Неизвестно'}\n"
|
||
message += f"📊 Статус: {submission.get_status_display()}\n"
|
||
|
||
if submission.score is not None:
|
||
message += f"🎯 Оценка: {submission.score}/{submission.homework.max_score}\n"
|
||
|
||
# Комментарий к оценённой работе — студент должен всегда видеть отзыв
|
||
mentor_fb = (submission.feedback or "").strip()
|
||
ai_fb = (submission.ai_feedback or "").strip() if submission.ai_checked_at else ""
|
||
|
||
if mentor_fb or ai_fb:
|
||
message += f"\n📋 <b>Комментарий к вашей работе:</b>\n"
|
||
if mentor_fb:
|
||
feedback_esc = _html.escape(mentor_fb[:800] + "..." if len(mentor_fb) > 800 else mentor_fb)
|
||
message += f"💬 <i>{feedback_esc}</i>\n"
|
||
if ai_fb and ai_fb != mentor_fb:
|
||
ai_esc = _html.escape(ai_fb[:800] + "..." if len(ai_fb) > 800 else ai_fb)
|
||
message += f"🤖 <i>{ai_esc}</i>\n"
|
||
elif submission.status == 'graded':
|
||
message += f"\n📋 <b>Комментарий к вашей работе:</b> нет текста отзыва.\n"
|
||
|
||
message += f"\n🌐 <a href='{settings.FRONTEND_URL}/homework'>Открыть на сайте</a>"
|
||
|
||
await update.message.reply_text(message, parse_mode='HTML', disable_web_page_preview=True)
|
||
|
||
async def homework_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /homework - показать домашние задания."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта."
|
||
)
|
||
return
|
||
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
|
||
# Для клиента - показать его задания
|
||
if db_user.role == 'client':
|
||
# Используем правильный sync_to_async для избежания ошибки SynchronousOnlyOperation
|
||
def get_client_homeworks():
|
||
return list(Homework.objects.filter(
|
||
assigned_to=db_user,
|
||
deadline__gte=timezone.now()
|
||
).order_by('deadline')[:5])
|
||
|
||
homeworks = await sync_to_async(get_client_homeworks)()
|
||
|
||
if not homeworks:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"📝 У вас нет активных домашних заданий.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Если только одно задание, показываем его сразу с Reply клавиатурой
|
||
if len(homeworks) == 1:
|
||
hw = homeworks[0]
|
||
context.user_data['current_homework_id'] = hw.id
|
||
msg, reply_markup = await self._get_client_homework_detail(hw.id, db_user)
|
||
if msg and reply_markup:
|
||
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=reply_markup)
|
||
else:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text("❌ Ошибка загрузки задания.", reply_markup=keyboard)
|
||
else:
|
||
# Несколько заданий: список в виде кнопок в панели Telegram
|
||
homework_keyboard, homework_buttons = make_homework_list_keyboard(homeworks)
|
||
context.user_data['homework_buttons'] = homework_buttons
|
||
context.user_data['homework_list_active'] = True
|
||
await update.message.reply_text(
|
||
"📝 <b>Домашние задания</b>\n\nВыберите задание кнопкой в панели ниже:",
|
||
parse_mode='HTML',
|
||
reply_markup=homework_keyboard
|
||
)
|
||
|
||
# Для родителя - показать задания всех детей
|
||
elif db_user.role == 'parent':
|
||
from apps.users.models import Parent
|
||
try:
|
||
parent_profile = await sync_to_async(lambda: db_user.parent_profile)()
|
||
children = await sync_to_async(list)(parent_profile.children.all())
|
||
|
||
if not children:
|
||
await update.message.reply_text(
|
||
"❌ У вас нет привязанных детей."
|
||
)
|
||
return
|
||
|
||
child_users = [child.user for child in children]
|
||
homeworks = await sync_to_async(list)(
|
||
Homework.objects.filter(
|
||
assigned_to__in=child_users,
|
||
deadline__gte=timezone.now()
|
||
).order_by('deadline')[:5]
|
||
)
|
||
|
||
if not homeworks:
|
||
await update.message.reply_text(
|
||
"📝 У ваших детей нет активных домашних заданий."
|
||
)
|
||
return
|
||
|
||
message = "📝 <b>Домашние задания детей:</b>\n\n"
|
||
|
||
for hw in homeworks:
|
||
deadline_str = hw.deadline.strftime('%d.%m.%Y') if hw.deadline else 'Без дедлайна'
|
||
|
||
# Находим для какого ребёнка это задание
|
||
child_students = [student for student in hw.assigned_to.all() if student in child_users]
|
||
child_names = ', '.join([child.get_full_name() for child in child_students[:2]])
|
||
if len(child_students) > 2:
|
||
child_names += f" и ещё {len(child_students) - 2}"
|
||
|
||
message += f"<b>{hw.title}</b>\n"
|
||
message += f"👶 {child_names}\n"
|
||
message += f"📅 Дедлайн: {deadline_str}\n\n"
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
|
||
except Exception as e:
|
||
logger.error(f"Error in homework_command for parent: {e}")
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения домашних заданий.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
# Для ментора - показать задания требующие проверки
|
||
elif db_user.role == 'mentor':
|
||
def _mentor_submissions_display(mentor_user):
|
||
qs = HomeworkSubmission.objects.filter(
|
||
homework__mentor=mentor_user,
|
||
status='pending'
|
||
).select_related('homework', 'student').order_by('-submitted_at')[:5]
|
||
return [
|
||
{
|
||
'id': s.id,
|
||
'student_name': (s.student.get_full_name() or 'Студент') if s.student else 'Студент',
|
||
'hw_title': (s.homework.title or 'Без названия') if s.homework else 'Без названия',
|
||
'submitted_at': s.submitted_at.strftime('%d.%m.%Y') if s.submitted_at else 'Неизвестно',
|
||
'ai_checked_at': getattr(s, 'ai_checked_at', None),
|
||
}
|
||
for s in qs
|
||
]
|
||
submissions_data = await sync_to_async(_mentor_submissions_display)(db_user)
|
||
if not submissions_data:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"📝 Нет домашних заданий, требующих проверки.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
message = "📝 <b>Домашние задания на проверку:</b>\n\n"
|
||
inline_keyboard = []
|
||
for item in submissions_data:
|
||
ai_status = " 🤖 ИИ проверил" if item['ai_checked_at'] else ""
|
||
message += f"<b>{item['hw_title']}</b>{ai_status}\n"
|
||
message += f"👤 {item['student_name']}\n"
|
||
message += f"📅 Сдано: {item['submitted_at']}\n\n"
|
||
btn_text = f"👁️ {item['hw_title'][:28]} — {item['student_name'][:15]}"
|
||
if len(btn_text) > 60:
|
||
btn_text = btn_text[:57] + "..."
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(btn_text, callback_data=f"mentor_submission_{item['id']}")
|
||
])
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await update.message.reply_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
else:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Эта команда доступна только для менторов, клиентов и родителей.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception("Error in homework_command: %s", e)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения домашних заданий.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def _handle_text_homework_submission(self, update: Update, context: ContextTypes.DEFAULT_TYPE, db_user, homework_id: int, text_content: str):
|
||
"""
|
||
Обработка текстового решения домашнего задания.
|
||
|
||
Args:
|
||
update: Update объект от Telegram
|
||
context: Context объект
|
||
db_user: Пользователь из базы данных
|
||
homework_id: ID домашнего задания
|
||
text_content: Текстовое содержание решения
|
||
"""
|
||
try:
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
from django.utils import timezone as tz
|
||
|
||
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
|
||
|
||
# Проверяем, что пользователь имеет право сдавать это ДЗ
|
||
if db_user.role != 'client' or db_user not in await sync_to_async(list)(homework.assigned_to.all()):
|
||
await update.message.reply_text(
|
||
"❌ У вас нет доступа к этому заданию."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
return
|
||
|
||
# Проверяем, есть ли уже решение
|
||
existing_submission = await sync_to_async(
|
||
HomeworkSubmission.objects.filter(
|
||
homework=homework,
|
||
student=db_user
|
||
).order_by('-attempt_number').first
|
||
)()
|
||
|
||
if existing_submission and existing_submission.status != 'returned':
|
||
# Обновляем существующее решение
|
||
existing_submission.content = text_content
|
||
existing_submission.status = 'pending'
|
||
await sync_to_async(existing_submission.save)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение обновлено!</b>\n\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
# Определяем номер попытки
|
||
attempt_number = 1
|
||
if existing_submission:
|
||
attempt_number = existing_submission.attempt_number + 1
|
||
|
||
# Создаем новое решение
|
||
submission = HomeworkSubmission(
|
||
homework=homework,
|
||
student=db_user,
|
||
content=text_content,
|
||
status='pending',
|
||
attempt_number=attempt_number
|
||
)
|
||
await sync_to_async(submission.save)()
|
||
|
||
# Проверяем опоздание
|
||
await sync_to_async(submission.check_if_late)()
|
||
|
||
# Обновляем статистику задания
|
||
await sync_to_async(homework.update_statistics)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение загружено!</b>\n\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
# Отправляем уведомление ментору
|
||
from apps.notifications.services import NotificationService
|
||
await sync_to_async(NotificationService.create_notification_with_telegram)(
|
||
recipient=homework.mentor,
|
||
notification_type='homework_submitted',
|
||
title='📝 ДЗ сдано',
|
||
message=f'{db_user.get_full_name()} сдал ДЗ "{homework.title}"',
|
||
priority='normal',
|
||
action_url=f'/homework/{homework.id}/',
|
||
content_object=homework
|
||
)
|
||
|
||
# Очищаем состояние ожидания
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
keyboard = await get_user_keyboard(update.effective_user.id)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки для навигации:",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Homework.DoesNotExist:
|
||
await update.message.reply_text(
|
||
"❌ Домашнее задание не найдено."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
except Exception as e:
|
||
logger.error(f"Error handling text homework submission: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка загрузки решения.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
async def _handle_submission_grade_input(self, update: Update, context: ContextTypes.DEFAULT_TYPE, db_user, submission_id: int, message_text: str):
|
||
"""Обработка ввода оценки для submission."""
|
||
try:
|
||
from apps.homework.models import HomeworkSubmission
|
||
from django.utils import timezone
|
||
|
||
from apps.homework.models import Homework
|
||
submission = await sync_to_async(
|
||
HomeworkSubmission.objects.select_related('homework', 'student').get
|
||
)(id=submission_id)
|
||
homework_mentor_id = await sync_to_async(
|
||
lambda hw_id: Homework.objects.values_list('mentor_id', flat=True).get(id=hw_id)
|
||
)(submission.homework_id)
|
||
if db_user.role != 'mentor' or homework_mentor_id != db_user.id:
|
||
await update.message.reply_text("❌ У вас нет доступа к этому заданию.")
|
||
context.user_data.pop('waiting_for_submission_score', None)
|
||
return
|
||
|
||
hw_max_score = await sync_to_async(lambda s: s.homework.max_score)(submission)
|
||
# Парсим ввод: может быть только оценка или "оценка\nкомментарий"
|
||
lines = message_text.strip().split('\n', 1)
|
||
score_text = lines[0].strip()
|
||
feedback_text = lines[1].strip() if len(lines) > 1 else None
|
||
|
||
# Пытаемся извлечь оценку
|
||
try:
|
||
score = float(score_text.replace(',', '.'))
|
||
score = int(round(score))
|
||
except ValueError:
|
||
await update.message.reply_text(
|
||
f"❌ Неверный формат оценки.\n\n"
|
||
f"Введите число от 1 до {hw_max_score}."
|
||
)
|
||
return
|
||
|
||
# Проверяем диапазон
|
||
if score < 1 or score > hw_max_score:
|
||
await update.message.reply_text(
|
||
f"❌ Оценка должна быть от 1 до {hw_max_score}."
|
||
)
|
||
return
|
||
|
||
# Если комментарий не указан, используем существующий или ai_feedback
|
||
if not feedback_text:
|
||
feedback_text = submission.feedback or submission.ai_feedback or ""
|
||
|
||
# Выставляем оценку
|
||
await sync_to_async(submission.grade)(score, feedback_text, checked_by=db_user)
|
||
|
||
def _grade_done_and_notify(sub, sc, fb):
|
||
sub.homework.update_statistics()
|
||
from apps.notifications.services import NotificationService
|
||
msg = f'Проверено ДЗ "{sub.homework.title}". Оценка: {sc}/{sub.homework.max_score}'
|
||
if fb and str(fb).strip():
|
||
comment = (fb[:500] + '…') if len(fb) > 500 else fb
|
||
msg += f'\n\n💬 Комментарий:\n{comment}'
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=sub.student,
|
||
notification_type='homework_reviewed',
|
||
title='✅ ДЗ проверено',
|
||
message=msg,
|
||
priority='normal',
|
||
action_url=f'/homework/{sub.homework.id}/submissions/{sub.id}/',
|
||
content_object=sub
|
||
)
|
||
return (sub.homework.title, sub.student.get_full_name(), sub.homework.max_score)
|
||
hw_title, student_name, _hw_max = await sync_to_async(_grade_done_and_notify)(submission, score, feedback_text)
|
||
|
||
context.user_data.pop('waiting_for_submission_score', None)
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Оценка выставлена!</b>\n\n"
|
||
f"📝 Задание: {hw_title}\n"
|
||
f"👤 Студент: {student_name}\n"
|
||
f"⭐ Оценка: {score}/{_hw_max}\n"
|
||
f"💬 Комментарий: {feedback_text[:100]}{'...' if len(feedback_text) > 100 else ''}",
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
keyboard = await get_user_keyboard(update.effective_user.id)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки для навигации:",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except HomeworkSubmission.DoesNotExist:
|
||
await update.message.reply_text("❌ Решение не найдено.")
|
||
context.user_data.pop('waiting_for_submission_score', None)
|
||
except Exception as e:
|
||
logger.error(f"Error handling submission grade input: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка выставления оценки.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку."
|
||
)
|
||
context.user_data.pop('waiting_for_submission_score', None)
|
||
|
||
async def _handle_submission_feedback_input(self, update: Update, context: ContextTypes.DEFAULT_TYPE, db_user, submission_id: int, message_text: str):
|
||
"""Обработка ввода комментария для submission."""
|
||
try:
|
||
from apps.homework.models import HomeworkSubmission
|
||
from django.utils import timezone
|
||
|
||
from apps.homework.models import Homework
|
||
submission = await sync_to_async(
|
||
HomeworkSubmission.objects.select_related('homework', 'student').get
|
||
)(id=submission_id)
|
||
homework_mentor_id = await sync_to_async(
|
||
lambda hw_id: Homework.objects.values_list('mentor_id', flat=True).get(id=hw_id)
|
||
)(submission.homework_id)
|
||
if db_user.role != 'mentor' or homework_mentor_id != db_user.id:
|
||
await update.message.reply_text("❌ У вас нет доступа к этому заданию.")
|
||
context.user_data.pop('waiting_for_submission_feedback', None)
|
||
return
|
||
|
||
# Обновляем комментарий
|
||
submission.feedback = message_text.strip()
|
||
await sync_to_async(submission.save)(update_fields=['feedback'])
|
||
|
||
# Если есть оценка, обновляем статус на graded
|
||
if submission.score is not None and submission.status != 'graded':
|
||
submission.status = 'graded'
|
||
submission.checked_by = db_user
|
||
submission.checked_at = timezone.now()
|
||
await sync_to_async(submission.save)(update_fields=['status', 'checked_by', 'checked_at'])
|
||
|
||
await sync_to_async(lambda s: s.homework.update_statistics())(submission)
|
||
context.user_data.pop('waiting_for_submission_feedback', None)
|
||
hw_title_f, student_name_f = await sync_to_async(
|
||
lambda s: (s.homework.title, s.student.get_full_name())
|
||
)(submission)
|
||
await update.message.reply_text(
|
||
f"✅ <b>Ответ сохранён!</b>\n\n"
|
||
f"📝 Задание: {hw_title_f}\n"
|
||
f"👤 Студент: {student_name_f}\n"
|
||
f"💬 Ответ: {message_text[:200]}{'...' if len(message_text) > 200 else ''}",
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
keyboard = await get_user_keyboard(update.effective_user.id)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки для навигации:",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except HomeworkSubmission.DoesNotExist:
|
||
await update.message.reply_text("❌ Решение не найдено.")
|
||
context.user_data.pop('waiting_for_submission_feedback', None)
|
||
except Exception as e:
|
||
logger.error(f"Error handling submission feedback input: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка сохранения комментария.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку."
|
||
)
|
||
context.user_data.pop('waiting_for_submission_feedback', None)
|
||
|
||
async def _handle_return_revision_input(
|
||
self, update: Update, context: ContextTypes.DEFAULT_TYPE, db_user, submission_id: int, feedback_text: str
|
||
):
|
||
"""Вернуть ДЗ на доработку и уведомить студента."""
|
||
try:
|
||
from apps.homework.models import HomeworkSubmission, Homework
|
||
from apps.notifications.services import NotificationService
|
||
|
||
submission = await sync_to_async(
|
||
HomeworkSubmission.objects.select_related('homework', 'student').get
|
||
)(id=submission_id)
|
||
homework_mentor_id = await sync_to_async(
|
||
lambda hw_id: Homework.objects.values_list('mentor_id', flat=True).get(id=hw_id)
|
||
)(submission.homework_id)
|
||
if db_user.role != 'mentor' or homework_mentor_id != db_user.id:
|
||
await update.message.reply_text("❌ У вас нет доступа к этому заданию.")
|
||
context.user_data.pop('waiting_for_return_revision', None)
|
||
return
|
||
|
||
feedback_text = (feedback_text or '').strip() or 'Нужно доработать решение.'
|
||
|
||
def _do_return_and_notify():
|
||
submission.return_for_revision(feedback_text, checked_by=db_user)
|
||
hw_title = submission.homework.title
|
||
student_name = submission.student.get_full_name()
|
||
msg = f'ДЗ "{hw_title}" возвращено на доработку. Нужно отправить решение заново.'
|
||
comment = (feedback_text[:500] + '…') if len(feedback_text) > 500 else feedback_text
|
||
msg += f'\n\n💬 Комментарий:\n{comment}'
|
||
NotificationService.create_notification_with_telegram(
|
||
recipient=submission.student,
|
||
notification_type='homework_returned',
|
||
title='🔄 ДЗ возвращено на доработку',
|
||
message=msg,
|
||
priority='normal',
|
||
action_url=f'/homework/{submission.homework.id}/submissions/{submission.id}/',
|
||
content_object=submission
|
||
)
|
||
return (hw_title, student_name)
|
||
|
||
hw_title, student_name = await sync_to_async(_do_return_and_notify)()
|
||
|
||
context.user_data.pop('waiting_for_return_revision', None)
|
||
await update.message.reply_text(
|
||
f"🔄 <b>ДЗ возвращено на доработку</b>\n\n"
|
||
f"📝 Задание: {hw_title}\n"
|
||
f"👤 Студент: {student_name}\n"
|
||
f"💬 Комментарий отправлен. Студент получил уведомление и может отправить решение заново.",
|
||
parse_mode='HTML'
|
||
)
|
||
keyboard = await get_user_keyboard(update.effective_user.id)
|
||
await update.message.reply_text("Используйте кнопки для навигации:", reply_markup=keyboard)
|
||
except HomeworkSubmission.DoesNotExist:
|
||
await update.message.reply_text("❌ Решение не найдено.")
|
||
context.user_data.pop('waiting_for_return_revision', None)
|
||
except Exception as e:
|
||
logger.error(f"Error handling return revision: {e}", exc_info=True)
|
||
await update.message.reply_text("❌ Ошибка. Попробуйте позже.")
|
||
context.user_data.pop('waiting_for_return_revision', None)
|
||
|
||
async def _refresh_settings_message(self, query, db_user, preferences):
|
||
"""Обновить сообщение с настройками."""
|
||
message_text = "⚙️ <b>Настройки</b>\n\n"
|
||
|
||
all_status = "✅ Включены" if preferences.enabled else "❌ Выключены"
|
||
telegram_status = "✅ Вкл" if preferences.telegram_enabled else "❌ Выкл"
|
||
email_status = "✅ Вкл" if preferences.email_enabled else "❌ Выкл"
|
||
in_app_status = "✅ Вкл" if preferences.in_app_enabled else "❌ Выкл"
|
||
|
||
message_text += f"🔔 Все уведомления: {all_status}\n"
|
||
message_text += f"📱 Telegram: {telegram_status}\n"
|
||
message_text += f"📧 Email: {email_status}\n"
|
||
message_text += f"💬 В приложении: {in_app_status}\n\n"
|
||
|
||
if preferences.quiet_hours_enabled and preferences.quiet_hours_start and preferences.quiet_hours_end:
|
||
quiet_start = preferences.quiet_hours_start.strftime('%H:%M')
|
||
quiet_end = preferences.quiet_hours_end.strftime('%H:%M')
|
||
message_text += f"🔇 Режим тишины: {quiet_start} - {quiet_end}\n\n"
|
||
else:
|
||
message_text += "🔇 Режим тишины: ❌ Выключен\n\n"
|
||
|
||
user_lang = db_user.language or 'ru'
|
||
lang_display = 'Русский' if user_lang == 'ru' else 'English'
|
||
message_text += f"🌐 Язык: {lang_display}\n"
|
||
|
||
keyboard = []
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
"🔔 " + ("Выключить все" if preferences.enabled else "Включить все"),
|
||
callback_data="settings_toggle_all"
|
||
)
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
"📱 Telegram: " + ("Выкл" if preferences.telegram_enabled else "Вкл"),
|
||
callback_data="settings_toggle_telegram"
|
||
),
|
||
InlineKeyboardButton(
|
||
"📧 Email: " + ("Выкл" if preferences.email_enabled else "Вкл"),
|
||
callback_data="settings_toggle_email"
|
||
)
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🔇 Режим тишины", callback_data="settings_quiet_hours"),
|
||
InlineKeyboardButton("📋 Типы уведомлений", callback_data="settings_notification_types")
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🌐 Язык", callback_data="settings_language")
|
||
])
|
||
|
||
frontend_url = settings.FRONTEND_URL
|
||
if frontend_url and 'localhost' not in frontend_url and '127.0.0.1' not in frontend_url:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🌐 Открыть на сайте", url=f"{frontend_url}/profile")
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
async def _handle_quiet_hours(self, query, db_user, preferences):
|
||
"""Обработка настроек режима тишины."""
|
||
from datetime import time
|
||
|
||
message_text = "🔇 <b>Режим тишины</b>\n\n"
|
||
|
||
if preferences.quiet_hours_enabled and preferences.quiet_hours_start and preferences.quiet_hours_end:
|
||
quiet_start = preferences.quiet_hours_start.strftime('%H:%M')
|
||
quiet_end = preferences.quiet_hours_end.strftime('%H:%M')
|
||
message_text += f"Текущий: {quiet_start} - {quiet_end}\n\n"
|
||
else:
|
||
message_text += "Текущий: ❌ Выключен\n\n"
|
||
|
||
message_text += "Выберите действие:"
|
||
|
||
keyboard = []
|
||
|
||
# Предустановленные варианты
|
||
if not preferences.quiet_hours_enabled:
|
||
keyboard.append([
|
||
InlineKeyboardButton("✅ Включить (22:00 - 08:00)", callback_data="quiet_hours_enable_22_8")
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("✅ Включить (23:00 - 07:00)", callback_data="quiet_hours_enable_23_7")
|
||
])
|
||
else:
|
||
keyboard.append([
|
||
InlineKeyboardButton("❌ Выключить", callback_data="quiet_hours_disable")
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🕐 Изменить время", callback_data="quiet_hours_custom")
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
async def _handle_notification_types(self, query, db_user, preferences):
|
||
"""Обработка настроек типов уведомлений."""
|
||
from .models import Notification
|
||
|
||
# Показываем меню выбора типов уведомлений
|
||
message_text = "📋 <b>Типы уведомлений</b>\n\n"
|
||
message_text += "Выберите типы уведомлений для Telegram:\n\n"
|
||
|
||
# Группируем типы по категориям
|
||
lesson_types = [t for t in Notification.TYPE_CHOICES if 'lesson' in t[0]]
|
||
homework_types = [t for t in Notification.TYPE_CHOICES if 'homework' in t[0]]
|
||
other_types = [t for t in Notification.TYPE_CHOICES if 'lesson' not in t[0] and 'homework' not in t[0]]
|
||
|
||
keyboard = []
|
||
|
||
# Кнопки для занятий
|
||
if lesson_types:
|
||
keyboard.append([InlineKeyboardButton("📅 Занятия", callback_data="noop")])
|
||
for ntype, display in lesson_types:
|
||
type_prefs = preferences.type_preferences.get(ntype, {})
|
||
if not isinstance(type_prefs, dict):
|
||
type_prefs = {}
|
||
enabled = type_prefs.get('telegram', True)
|
||
status = "✅" if enabled else "❌"
|
||
# Сокращаем длинные названия
|
||
short_display = display.replace('Занятие ', '').replace(' занятие', '')
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"{status} {short_display}",
|
||
callback_data=f"toggle_type_{ntype}"
|
||
)
|
||
])
|
||
|
||
# Кнопки для домашних заданий
|
||
if homework_types:
|
||
keyboard.append([InlineKeyboardButton("📝 Домашние задания", callback_data="noop")])
|
||
for ntype, display in homework_types:
|
||
type_prefs = preferences.type_preferences.get(ntype, {})
|
||
if not isinstance(type_prefs, dict):
|
||
type_prefs = {}
|
||
enabled = type_prefs.get('telegram', True)
|
||
status = "✅" if enabled else "❌"
|
||
short_display = display.replace('Домашнее задание', 'ДЗ')
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"{status} {short_display}",
|
||
callback_data=f"toggle_type_{ntype}"
|
||
)
|
||
])
|
||
|
||
# Кнопки для других типов (первые 3)
|
||
if other_types:
|
||
keyboard.append([InlineKeyboardButton("📢 Другие", callback_data="noop")])
|
||
for ntype, display in other_types[:3]:
|
||
type_prefs = preferences.type_preferences.get(ntype, {})
|
||
if not isinstance(type_prefs, dict):
|
||
type_prefs = {}
|
||
enabled = type_prefs.get('telegram', True)
|
||
status = "✅" if enabled else "❌"
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"{status} {display}",
|
||
callback_data=f"toggle_type_{ntype}"
|
||
)
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
async def _handle_timezone(self, query, db_user):
|
||
"""Обработка настройки часового пояса."""
|
||
# Популярные часовые пояса
|
||
popular_timezones = [
|
||
('Europe/Moscow', 'Москва (UTC+3)'),
|
||
('Europe/Kiev', 'Киев (UTC+2)'),
|
||
('Asia/Almaty', 'Алматы (UTC+6)'),
|
||
('Europe/Minsk', 'Минск (UTC+3)'),
|
||
('Asia/Tashkent', 'Ташкент (UTC+5)'),
|
||
('Asia/Yekaterinburg', 'Екатеринбург (UTC+5)'),
|
||
('Asia/Novosibirsk', 'Новосибирск (UTC+7)'),
|
||
('Europe/Kaliningrad', 'Калининград (UTC+2)'),
|
||
]
|
||
|
||
message_text = "🕐 <b>Часовой пояс</b>\n\n"
|
||
message_text += "Текущий: " + (db_user.timezone or 'Europe/Moscow') + "\n\n"
|
||
message_text += "Выберите часовой пояс:"
|
||
|
||
keyboard = []
|
||
for tz, name in popular_timezones:
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
name + (" ✅" if db_user.timezone == tz else ""),
|
||
callback_data=f"set_timezone_{tz}"
|
||
)
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
async def _handle_language(self, query, db_user):
|
||
"""Обработка настройки языка."""
|
||
message_text = "🌐 <b>Язык интерфейса</b>\n\n"
|
||
message_text += "Текущий: " + ('Русский' if db_user.language == 'ru' else 'English') + "\n\n"
|
||
message_text += "Выберите язык:"
|
||
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton(
|
||
"Русский" + (" ✅" if db_user.language == 'ru' else ""),
|
||
callback_data="set_language_ru"
|
||
),
|
||
InlineKeyboardButton(
|
||
"English" + (" ✅" if db_user.language == 'en' else ""),
|
||
callback_data="set_language_en"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
|
||
]
|
||
]
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
async def clients_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /clients - список клиентов ментора."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User, Client
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
if db_user.role != 'mentor':
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Эта команда доступна только для менторов.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Получаем клиентов ментора
|
||
clients = await sync_to_async(list)(
|
||
Client.objects.filter(mentors=db_user).select_related('user')[:10]
|
||
)
|
||
|
||
if not clients:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"👥 У вас пока нет клиентов.\n\n"
|
||
"Клиенты появятся здесь после того, как они запишутся на ваши занятия.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
message = "👥 <b>Ваши клиенты:</b>\n\n"
|
||
|
||
from apps.schedule.models import Lesson
|
||
from apps.homework.models import HomeworkSubmission
|
||
from django.utils import timezone
|
||
|
||
inline_keyboard = []
|
||
for client in clients:
|
||
client_name = client.user.get_full_name() or client.user.email
|
||
message += f"👤 <b>{client_name}</b>\n"
|
||
|
||
# Статистика занятий
|
||
lessons = await sync_to_async(list)(
|
||
Lesson.objects.filter(
|
||
mentor=db_user,
|
||
client=client
|
||
)
|
||
)
|
||
total_lessons = len(lessons)
|
||
completed_lessons = len([l for l in lessons if l.status == 'completed'])
|
||
upcoming_lessons = len([
|
||
l for l in lessons
|
||
if l.start_time >= timezone.now() and l.status == 'scheduled'
|
||
])
|
||
|
||
message += f"📚 Занятий: {total_lessons} (завершено: {completed_lessons}, предстоящих: {upcoming_lessons})\n"
|
||
|
||
# Статистика ДЗ
|
||
submissions = await sync_to_async(list)(
|
||
HomeworkSubmission.objects.filter(
|
||
homework__mentor=db_user,
|
||
student=client.user
|
||
)
|
||
)
|
||
total_homeworks = len(submissions)
|
||
graded_homeworks = len([s for s in submissions if s.status == 'graded'])
|
||
if graded_homeworks > 0:
|
||
scores = [s.score for s in submissions if s.score is not None]
|
||
avg_score = sum(scores) / len(scores) if scores else 0
|
||
message += f"📝 ДЗ: {total_homeworks} (проверено: {graded_homeworks}, средний балл: {avg_score:.1f})\n"
|
||
else:
|
||
message += f"📝 ДЗ: {total_homeworks}\n"
|
||
|
||
message += "\n"
|
||
|
||
# Добавляем кнопку для просмотра деталей клиента
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"👤 {client_name[:30]}",
|
||
callback_data=f"client_detail_{client.id}"
|
||
)
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard) if inline_keyboard else None
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in clients_command: {e}", exc_info=True)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения списка клиентов.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /stats - статистика для ментора."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User, Client
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
if db_user.role != 'mentor':
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Эта команда доступна только для менторов.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
from apps.schedule.models import Lesson
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
from django.utils import timezone
|
||
from datetime import timedelta
|
||
|
||
now = timezone.now()
|
||
month_ago = now - timedelta(days=30)
|
||
|
||
# Общая статистика
|
||
total_clients = await sync_to_async(Client.objects.filter(mentors=db_user).count)()
|
||
|
||
# Занятия
|
||
all_lessons = await sync_to_async(list)(
|
||
Lesson.objects.filter(mentor=db_user)
|
||
)
|
||
total_lessons = len(all_lessons)
|
||
completed_lessons = len([l for l in all_lessons if l.status == 'completed'])
|
||
upcoming_lessons = len([
|
||
l for l in all_lessons
|
||
if l.start_time >= now and l.status == 'scheduled'
|
||
])
|
||
lessons_this_month = len([
|
||
l for l in all_lessons
|
||
if l.start_time >= month_ago
|
||
])
|
||
|
||
# Домашние задания
|
||
all_submissions = await sync_to_async(list)(
|
||
HomeworkSubmission.objects.filter(homework__mentor=db_user)
|
||
)
|
||
total_homeworks = len(all_submissions)
|
||
pending_homeworks = len([s for s in all_submissions if s.status == 'pending'])
|
||
graded_homeworks = len([s for s in all_submissions if s.status == 'graded'])
|
||
|
||
if graded_homeworks > 0:
|
||
scores = [s.score for s in all_submissions if s.score is not None]
|
||
avg_score = sum(scores) / len(scores) if scores else 0
|
||
else:
|
||
avg_score = 0
|
||
|
||
message = "📊 <b>Ваша статистика:</b>\n\n"
|
||
message += f"👥 Клиентов: {total_clients}\n\n"
|
||
message += f"📚 <b>Занятия:</b>\n"
|
||
message += f"• Всего: {total_lessons}\n"
|
||
message += f"• Завершено: {completed_lessons}\n"
|
||
message += f"• Предстоящих: {upcoming_lessons}\n"
|
||
message += f"• За месяц: {lessons_this_month}\n\n"
|
||
message += f"📝 <b>Домашние задания:</b>\n"
|
||
message += f"• Всего решений: {total_homeworks}\n"
|
||
message += f"• На проверке: {pending_homeworks}\n"
|
||
message += f"• Проверено: {graded_homeworks}\n"
|
||
if avg_score > 0:
|
||
message += f"• Средний балл: {avg_score:.1f}\n"
|
||
|
||
# Доходы (если есть занятия с ценой)
|
||
lessons_with_price = [l for l in all_lessons if l.price and l.status == 'completed']
|
||
if lessons_with_price:
|
||
total_revenue = sum([float(l.price) for l in lessons_with_price])
|
||
avg_price = total_revenue / len(lessons_with_price) if lessons_with_price else 0
|
||
revenue_this_month = sum([
|
||
float(l.price) for l in lessons_with_price
|
||
if l.start_time >= month_ago
|
||
])
|
||
|
||
message += f"\n💰 <b>Доходы:</b>\n"
|
||
message += f"• Всего: {total_revenue:.2f} ₽\n"
|
||
message += f"• Средняя цена: {avg_price:.2f} ₽\n"
|
||
message += f"• За месяц: {revenue_this_month:.2f} ₽\n"
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in stats_command: {e}", exc_info=True)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения статистики.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def progress_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Мой прогресс: выбор предмета → период → средние оценки от репетитора и от школы."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
if db_user.role != 'client':
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Эта команда доступна только для учеников.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
from apps.schedule.models import Lesson
|
||
|
||
def _get_subject_names():
|
||
lessons = Lesson.objects.filter(client__user=db_user).select_related('subject', 'mentor_subject')
|
||
names = set()
|
||
for lec in lessons:
|
||
name = lec.subject_name or (lec.subject.name if lec.subject else '') or (lec.mentor_subject.name if lec.mentor_subject else '') or 'Без предмета'
|
||
if name and name.strip():
|
||
names.add(name.strip())
|
||
return sorted(names)
|
||
|
||
subject_names = await sync_to_async(_get_subject_names)()
|
||
|
||
if not subject_names:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"📊 У вас пока нет занятий по предметам.\n"
|
||
"После проведённых занятий здесь можно будет посмотреть прогресс.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
context.user_data['progress_step'] = 'subject'
|
||
context.user_data['progress_subjects'] = subject_names
|
||
|
||
rows = [[KeyboardButton(name)] for name in subject_names]
|
||
rows.append([KeyboardButton(PROGRESS_BACK_TO_MENU)])
|
||
keyboard = ReplyKeyboardMarkup(rows, resize_keyboard=True, one_time_keyboard=False)
|
||
|
||
await update.message.reply_text(
|
||
"📊 <b>Мой прогресс</b>\n\n"
|
||
"Выберите предмет, по которому проводились занятия:",
|
||
parse_mode='HTML',
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in progress_command: {e}", exc_info=True)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка. Попробуйте позже.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def _progress_send_result(
|
||
self, update: Update, context: ContextTypes.DEFAULT_TYPE,
|
||
db_user, subject_name: str, period: str
|
||
):
|
||
"""Посчитать и отправить прогресс по предмету за период (средняя от репетитора, от школы)."""
|
||
from apps.schedule.models import Lesson
|
||
from django.utils import timezone
|
||
from datetime import timedelta
|
||
|
||
now = timezone.now()
|
||
if period == PROGRESS_PERIOD_WEEK:
|
||
start = now - timedelta(days=7)
|
||
elif period == PROGRESS_PERIOD_30:
|
||
start = now - timedelta(days=30)
|
||
else:
|
||
start = None
|
||
|
||
def _get_lessons_and_grades():
|
||
qs = Lesson.objects.filter(client__user=db_user).select_related('subject', 'mentor_subject')
|
||
if start:
|
||
qs = qs.filter(start_time__gte=start)
|
||
lessons = list(qs)
|
||
mentor_grades = []
|
||
school_grades = []
|
||
count = 0
|
||
target = (subject_name or '').strip()
|
||
for lec in lessons:
|
||
name = ((lec.subject_name or '') or (lec.subject.name if lec.subject else '') or (lec.mentor_subject.name if lec.mentor_subject else '') or 'Без предмета').strip()
|
||
if name != target:
|
||
continue
|
||
count += 1
|
||
if lec.mentor_grade is not None:
|
||
mentor_grades.append(lec.mentor_grade)
|
||
if lec.school_grade is not None:
|
||
school_grades.append(lec.school_grade)
|
||
return count, mentor_grades, school_grades
|
||
|
||
total_lessons, mentor_grades, school_grades = await sync_to_async(_get_lessons_and_grades)()
|
||
|
||
avg_mentor = sum(mentor_grades) / len(mentor_grades) if mentor_grades else None
|
||
avg_school = sum(school_grades) / len(school_grades) if school_grades else None
|
||
|
||
period_label = "за текущую неделю" if period == PROGRESS_PERIOD_WEEK else "за последние 30 дней" if period == PROGRESS_PERIOD_30 else "за всё время"
|
||
mentor_str = f"{avg_mentor:.1f}" if avg_mentor is not None else "—"
|
||
school_str = f"{avg_school:.1f}" if avg_school is not None else "—"
|
||
message = (
|
||
f"📊 <b>Прогресс: {subject_name}</b>\n\n"
|
||
f"Период: {period_label}\n"
|
||
f"Занятий: {total_lessons}\n\n"
|
||
f"📌 <b>Средняя оценка от репетитора:</b> {mentor_str}\n"
|
||
f"📌 <b>Средняя оценка от школы:</b> {school_str}"
|
||
)
|
||
|
||
context.user_data.pop('progress_step', None)
|
||
context.user_data.pop('progress_subjects', None)
|
||
context.user_data.pop('progress_subject', None)
|
||
keyboard = await get_user_keyboard(update.effective_user.id)
|
||
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
|
||
|
||
async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик обычных сообщений и кнопок."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
message_text = update.message.text
|
||
|
||
from apps.users.models import User
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
# Проверяем, ожидается ли ввод оценки или комментария для submission
|
||
if db_user and db_user.role == 'mentor':
|
||
submission_id = context.user_data.get('waiting_for_submission_score')
|
||
if submission_id:
|
||
# Если пользователь отправил текст "отмена", отменяем
|
||
if message_text and message_text.lower() in ['отмена', 'cancel', '/cancel']:
|
||
context.user_data.pop('waiting_for_submission_score', None)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Выставление оценки отменено.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
else:
|
||
# Обрабатываем ввод оценки
|
||
await self._handle_submission_grade_input(update, context, db_user, submission_id, message_text)
|
||
return
|
||
|
||
submission_id = context.user_data.get('waiting_for_submission_feedback')
|
||
if submission_id:
|
||
# Если пользователь отправил текст "отмена", отменяем
|
||
if message_text and message_text.lower() in ['отмена', 'cancel', '/cancel']:
|
||
context.user_data.pop('waiting_for_submission_feedback', None)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Редактирование ответа отменено.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
else:
|
||
# Обрабатываем ввод комментария
|
||
await self._handle_submission_feedback_input(update, context, db_user, submission_id, message_text)
|
||
return
|
||
|
||
submission_id = context.user_data.get('waiting_for_return_revision')
|
||
if submission_id:
|
||
if message_text and message_text.lower() in ['отмена', 'cancel', '/cancel']:
|
||
context.user_data.pop('waiting_for_return_revision', None)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text("❌ Возврат на доработку отменён.", reply_markup=keyboard)
|
||
return
|
||
else:
|
||
await self._handle_return_revision_input(update, context, db_user, submission_id, message_text or '')
|
||
return
|
||
|
||
# Проверяем, ожидается ли загрузка решения для ДЗ
|
||
if db_user:
|
||
homework_id = context.user_data.get('waiting_for_homework_file')
|
||
if homework_id:
|
||
# Если пользователь отправил текст "отмена", отменяем загрузку
|
||
if message_text and message_text.lower() in ['отмена', 'cancel', '/cancel']:
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Загрузка решения отменена.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
else:
|
||
# Обрабатываем текстовое решение
|
||
await self._handle_text_homework_submission(update, context, db_user, homework_id, message_text)
|
||
return
|
||
|
||
# Список заданий в панели: нажатие на задание или «Назад в меню»
|
||
if db_user and db_user.role == 'client':
|
||
# Обработка кнопок управления конкретным ДЗ
|
||
if message_text == "🏠 В главное меню":
|
||
context.user_data.pop('homework_list_active', None)
|
||
context.user_data.pop('homework_buttons', None)
|
||
context.user_data.pop('current_homework_id', None)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text("Возврат в главное меню.", reply_markup=keyboard)
|
||
return
|
||
|
||
if message_text == "◀️ Назад к списку":
|
||
# Возврат к списку заданий
|
||
from apps.homework.models import Homework
|
||
|
||
def get_homeworks():
|
||
return list(Homework.objects.filter(
|
||
assigned_to=db_user,
|
||
deadline__gte=timezone.now()
|
||
).order_by('deadline')[:5])
|
||
|
||
homeworks = await sync_to_async(get_homeworks)()
|
||
if homeworks:
|
||
homework_keyboard, homework_buttons = make_homework_list_keyboard(homeworks)
|
||
context.user_data['homework_buttons'] = homework_buttons
|
||
context.user_data['homework_list_active'] = True
|
||
context.user_data.pop('current_homework_id', None)
|
||
await update.message.reply_text(
|
||
"📝 <b>Домашние задания</b>\n\nВыберите задание кнопкой в панели ниже:",
|
||
parse_mode='HTML',
|
||
reply_markup=homework_keyboard
|
||
)
|
||
else:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text("📝 У вас нет активных домашних заданий.", reply_markup=keyboard)
|
||
return
|
||
|
||
if message_text == "📎 Загрузить решение":
|
||
homework_id = context.user_data.get('current_homework_id')
|
||
if homework_id:
|
||
context.user_data['waiting_for_homework_file'] = homework_id
|
||
await update.message.reply_text(
|
||
"📎 <b>Загрузка решения</b>\n\n"
|
||
"Отправьте файл с решением (документ, фото) или текстовое решение.\n\n"
|
||
"Или отправьте 'отмена' для отмены.",
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
if message_text == "👁️ Просмотреть решение":
|
||
homework_id = context.user_data.get('current_homework_id')
|
||
if homework_id:
|
||
from apps.homework.models import HomeworkSubmission
|
||
# select_related чтобы не было lazy load в async и комментарии точно подгружены
|
||
submission = await sync_to_async(
|
||
lambda: HomeworkSubmission.objects.select_related('homework').filter(
|
||
homework_id=homework_id, student=db_user
|
||
).first()
|
||
)()
|
||
if submission:
|
||
await self._show_submission_view(update, context, submission, db_user)
|
||
return
|
||
|
||
# Обработка выбора задания из списка
|
||
if context.user_data.get('homework_list_active'):
|
||
homework_buttons = context.user_data.get('homework_buttons') or {}
|
||
if message_text == HOMEWORK_LIST_BACK_BUTTON:
|
||
context.user_data.pop('homework_list_active', None)
|
||
context.user_data.pop('homework_buttons', None)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text("Возврат в главное меню.", reply_markup=keyboard)
|
||
return
|
||
if message_text in homework_buttons:
|
||
homework_id = homework_buttons[message_text]
|
||
context.user_data['current_homework_id'] = homework_id
|
||
msg, reply_markup = await self._get_client_homework_detail(homework_id, db_user)
|
||
if msg and reply_markup:
|
||
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=reply_markup)
|
||
else:
|
||
await update.message.reply_text("❌ Задание не найдено или у вас нет доступа.")
|
||
return
|
||
|
||
# Мой прогресс: выбор предмета → период → результат
|
||
if db_user and db_user.role == 'client':
|
||
progress_step = context.user_data.get('progress_step')
|
||
if progress_step == 'subject':
|
||
if message_text == PROGRESS_BACK_TO_MENU:
|
||
context.user_data.pop('progress_step', None)
|
||
context.user_data.pop('progress_subjects', None)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text("Возврат в меню.", reply_markup=keyboard)
|
||
return
|
||
subjects = context.user_data.get('progress_subjects') or []
|
||
if message_text in subjects:
|
||
context.user_data['progress_subject'] = message_text
|
||
context.user_data['progress_step'] = 'period'
|
||
period_keyboard = ReplyKeyboardMarkup(
|
||
[
|
||
[KeyboardButton(PROGRESS_PERIOD_WEEK)],
|
||
[KeyboardButton(PROGRESS_PERIOD_30)],
|
||
[KeyboardButton(PROGRESS_PERIOD_ALL)],
|
||
[KeyboardButton(PROGRESS_BACK_SUBJECT), KeyboardButton(PROGRESS_BACK_TO_MENU)],
|
||
],
|
||
resize_keyboard=True,
|
||
one_time_keyboard=False,
|
||
)
|
||
await update.message.reply_text(
|
||
f"📊 Предмет: <b>{message_text}</b>\n\nВыберите период:",
|
||
parse_mode='HTML',
|
||
reply_markup=period_keyboard
|
||
)
|
||
return
|
||
elif progress_step == 'period':
|
||
if message_text == PROGRESS_BACK_TO_MENU:
|
||
context.user_data.pop('progress_step', None)
|
||
context.user_data.pop('progress_subjects', None)
|
||
context.user_data.pop('progress_subject', None)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text("Возврат в меню.", reply_markup=keyboard)
|
||
return
|
||
if message_text == PROGRESS_BACK_SUBJECT:
|
||
from apps.schedule.models import Lesson
|
||
def _get_subject_names():
|
||
lessons = Lesson.objects.filter(client__user=db_user).select_related('subject', 'mentor_subject')
|
||
names = set()
|
||
for lec in lessons:
|
||
name = lec.subject_name or (lec.subject.name if lec.subject else '') or (lec.mentor_subject.name if lec.mentor_subject else '') or 'Без предмета'
|
||
if name and name.strip():
|
||
names.add(name.strip())
|
||
return sorted(names)
|
||
subject_names = await sync_to_async(_get_subject_names)()
|
||
context.user_data['progress_step'] = 'subject'
|
||
context.user_data['progress_subjects'] = subject_names
|
||
context.user_data.pop('progress_subject', None)
|
||
rows = [[KeyboardButton(n)] for n in subject_names]
|
||
rows.append([KeyboardButton(PROGRESS_BACK_TO_MENU)])
|
||
keyboard = ReplyKeyboardMarkup(rows, resize_keyboard=True, one_time_keyboard=False)
|
||
await update.message.reply_text(
|
||
"📊 Выберите предмет:",
|
||
parse_mode='HTML',
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
if message_text in (PROGRESS_PERIOD_WEEK, PROGRESS_PERIOD_30, PROGRESS_PERIOD_ALL):
|
||
subject_name = context.user_data.get('progress_subject', '')
|
||
await self._progress_send_result(update, context, db_user, subject_name, message_text)
|
||
return
|
||
|
||
# Обработка нажатий на кнопки
|
||
if message_text == "📅 Расписание" or message_text == "📅 Моё расписание" or message_text == "📅 Расписание детей":
|
||
await self.schedule_command(update, context)
|
||
elif message_text == "📚 Следующее занятие":
|
||
await self.nextlesson_command(update, context)
|
||
elif message_text == "📝 Домашние задания" or message_text == "📝 Мои задания" or message_text == "📝 Задания детей":
|
||
await self.homework_command(update, context)
|
||
elif message_text == "📊 Мой прогресс":
|
||
await self.progress_command(update, context)
|
||
elif message_text == "👥 Клиенты":
|
||
await self.clients_command(update, context)
|
||
elif message_text == "📊 Статистика":
|
||
await self.stats_command(update, context)
|
||
elif message_text == "⚙️ Настройки":
|
||
await self.settings_command(update, context)
|
||
elif message_text == "ℹ️ Статус":
|
||
await self.status_command(update, context)
|
||
elif message_text == "❓ Помощь":
|
||
await self.help_command(update, context)
|
||
elif message_text == "🔗 Связать аккаунт":
|
||
await update.message.reply_text(
|
||
"🔗 <b>Связывание аккаунта</b>\n\n"
|
||
"1. Войдите на платформу\n"
|
||
"2. Перейдите в Профиль → Настройки → Telegram\n"
|
||
"3. Нажмите \"Связать Telegram\"\n"
|
||
"4. Отправьте команду: /link ВАШ_КОД\n\n"
|
||
"Или используйте команду:\n"
|
||
"/link <ваш_код_связывания>",
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
# Если аккаунт связан, показываем клавиатуру
|
||
if db_user:
|
||
keyboard = get_main_keyboard(db_user.role)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки или команды для работы с ботом.\n\n"
|
||
"Введите /help для списка команд.",
|
||
reply_markup=keyboard
|
||
)
|
||
else:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки или команды для работы с ботом.\n\n"
|
||
"Введите /help для списка команд.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def handle_document(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик загрузки документов (для решений ДЗ)."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта."
|
||
)
|
||
return
|
||
|
||
# Проверяем, ожидается ли загрузка файла для ДЗ
|
||
homework_id = context.user_data.get('waiting_for_homework_file')
|
||
if not homework_id:
|
||
await update.message.reply_text(
|
||
"📎 Чтобы загрузить файл для решения ДЗ:\n\n"
|
||
"1. Используйте команду /homework\n"
|
||
"2. Выберите задание\n"
|
||
"3. Нажмите кнопку '📎 Загрузить решение'"
|
||
)
|
||
return
|
||
|
||
# Получаем файл
|
||
document = update.message.document
|
||
if not document:
|
||
await update.message.reply_text("❌ Файл не найден.")
|
||
return
|
||
|
||
try:
|
||
# Скачиваем файл из Telegram
|
||
from telegram import Bot
|
||
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
|
||
file = await bot.get_file(document.file_id)
|
||
|
||
# Получаем домашнее задание для проверок
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
|
||
|
||
# Безопасность: Проверка размера файла (максимум 50MB для Telegram, но проверяем и настройки задания)
|
||
MAX_TELEGRAM_FILE_SIZE = 50 * 1024 * 1024 # 50 MB - максимум для Telegram
|
||
file_size = document.file_size or 0
|
||
|
||
if file_size > MAX_TELEGRAM_FILE_SIZE:
|
||
await update.message.reply_text(
|
||
f"❌ Файл слишком большой. Максимальный размер: {MAX_TELEGRAM_FILE_SIZE / (1024*1024):.0f} MB"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Проверяем размер файла согласно настройкам задания
|
||
if homework.max_file_size > 0 and file_size > homework.max_file_size:
|
||
max_size_mb = homework.max_file_size / (1024 * 1024)
|
||
await update.message.reply_text(
|
||
f"❌ Файл слишком большой. Максимальный размер для этого задания: {max_size_mb:.1f} MB"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Безопасность: Проверка типа файла
|
||
from apps.homework.utils import validate_file_type, sanitize_filename
|
||
safe_filename = sanitize_filename(document.file_name or 'file')
|
||
|
||
if homework.allowed_file_types and not validate_file_type(safe_filename, homework.allowed_file_types):
|
||
allowed = homework.allowed_file_types.replace(',', ', ')
|
||
await update.message.reply_text(
|
||
f"❌ Тип файла не разрешен.\n\n"
|
||
f"Разрешенные типы: {allowed}"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Создаем временный файл
|
||
import tempfile
|
||
import os
|
||
from django.core.files import File
|
||
|
||
file_ext = os.path.splitext(safe_filename)[1]
|
||
tmp_file_path = None
|
||
try:
|
||
with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as tmp_file:
|
||
tmp_file_path = tmp_file.name
|
||
await file.download_to_drive(tmp_file_path)
|
||
|
||
# Проверяем, что пользователь имеет право сдавать это ДЗ
|
||
if db_user.role != 'client' or db_user not in await sync_to_async(list)(homework.assigned_to.all()):
|
||
await update.message.reply_text(
|
||
"❌ У вас нет доступа к этому заданию."
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Проверяем, есть ли уже решение
|
||
existing_submission = await sync_to_async(
|
||
HomeworkSubmission.objects.filter(
|
||
homework=homework,
|
||
student=db_user
|
||
).order_by('-attempt_number').first
|
||
)()
|
||
|
||
if existing_submission and existing_submission.status != 'returned':
|
||
# Обновляем существующее решение
|
||
with open(tmp_file_path, 'rb') as f:
|
||
django_file = File(f, name=safe_filename)
|
||
existing_submission.attachment = django_file
|
||
existing_submission.content = f"Решение загружено через Telegram: {safe_filename}"
|
||
existing_submission.status = 'pending'
|
||
await sync_to_async(existing_submission.save)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение обновлено!</b>\n\n"
|
||
f"📎 Файл: {safe_filename}\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
# Определяем номер попытки
|
||
attempt_number = 1
|
||
if existing_submission:
|
||
attempt_number = existing_submission.attempt_number + 1
|
||
|
||
# Создаем новое решение
|
||
with open(tmp_file_path, 'rb') as f:
|
||
django_file = File(f, name=safe_filename)
|
||
submission = HomeworkSubmission(
|
||
homework=homework,
|
||
student=db_user,
|
||
content=f"Решение загружено через Telegram: {safe_filename}",
|
||
attachment=django_file,
|
||
status='pending',
|
||
attempt_number=attempt_number
|
||
)
|
||
await sync_to_async(submission.save)()
|
||
|
||
# Проверяем опоздание
|
||
await sync_to_async(submission.check_if_late)()
|
||
|
||
# Обновляем статистику задания
|
||
await sync_to_async(homework.update_statistics)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение загружено!</b>\n\n"
|
||
f"📎 Файл: {safe_filename}\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
# Отправляем уведомление ментору
|
||
from apps.notifications.services import NotificationService
|
||
await sync_to_async(NotificationService.create_notification_with_telegram)(
|
||
recipient=homework.mentor,
|
||
notification_type='homework_submitted',
|
||
title='📝 ДЗ сдано',
|
||
message=f'{db_user.get_full_name()} сдал ДЗ "{homework.title}"',
|
||
priority='normal',
|
||
action_url=f'/homework/{homework.id}/',
|
||
content_object=homework
|
||
)
|
||
|
||
# Очищаем состояние ожидания
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки для навигации:",
|
||
reply_markup=keyboard
|
||
)
|
||
finally:
|
||
# Гарантированно удаляем временный файл
|
||
if tmp_file_path and os.path.exists(tmp_file_path):
|
||
try:
|
||
os.unlink(tmp_file_path)
|
||
except Exception as e:
|
||
logger.error(f"Error deleting temp file {tmp_file_path}: {e}")
|
||
|
||
await bot.close()
|
||
|
||
except Homework.DoesNotExist:
|
||
await update.message.reply_text(
|
||
"❌ Домашнее задание не найдено."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
except Exception as e:
|
||
logger.error(f"Error handling document upload: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка загрузки файла.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
async def handle_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик загрузки фото (для решений ДЗ)."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта."
|
||
)
|
||
return
|
||
|
||
# Проверяем, ожидается ли загрузка файла для ДЗ
|
||
homework_id = context.user_data.get('waiting_for_homework_file')
|
||
if not homework_id:
|
||
await update.message.reply_text(
|
||
"📎 Чтобы загрузить фото для решения ДЗ:\n\n"
|
||
"1. Используйте команду /homework\n"
|
||
"2. Выберите задание\n"
|
||
"3. Нажмите кнопку '📎 Загрузить решение'"
|
||
)
|
||
return
|
||
|
||
# Получаем фото (берем самое большое)
|
||
photos = update.message.photo
|
||
if not photos:
|
||
await update.message.reply_text("❌ Фото не найдено.")
|
||
return
|
||
|
||
photo = photos[-1] # Самое большое фото
|
||
|
||
try:
|
||
# Скачиваем фото из Telegram
|
||
from telegram import Bot
|
||
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
|
||
file = await bot.get_file(photo.file_id)
|
||
|
||
# Получаем домашнее задание для проверок
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
|
||
|
||
# Безопасность: Проверка размера файла (максимум 50MB для Telegram)
|
||
MAX_TELEGRAM_FILE_SIZE = 50 * 1024 * 1024 # 50 MB - максимум для Telegram
|
||
file_size = photo.file_size or 0
|
||
|
||
if file_size > MAX_TELEGRAM_FILE_SIZE:
|
||
await update.message.reply_text(
|
||
f"❌ Фото слишком большое. Максимальный размер: {MAX_TELEGRAM_FILE_SIZE / (1024*1024):.0f} MB"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Проверяем размер файла согласно настройкам задания
|
||
if homework.max_file_size > 0 and file_size > homework.max_file_size:
|
||
max_size_mb = homework.max_file_size / (1024 * 1024)
|
||
await update.message.reply_text(
|
||
f"❌ Фото слишком большое. Максимальный размер для этого задания: {max_size_mb:.1f} MB"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Безопасность: Проверка типа файла (фото должно быть разрешено)
|
||
from apps.homework.utils import validate_file_type, sanitize_filename
|
||
if homework.allowed_file_types and not validate_file_type('photo.jpg', homework.allowed_file_types):
|
||
allowed = homework.allowed_file_types.replace(',', ', ')
|
||
await update.message.reply_text(
|
||
f"❌ Фото не разрешено для этого задания.\n\n"
|
||
f"Разрешенные типы: {allowed}"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Создаем временный файл
|
||
import tempfile
|
||
import os
|
||
from django.core.files import File
|
||
|
||
tmp_file_path = None
|
||
try:
|
||
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as tmp_file:
|
||
tmp_file_path = tmp_file.name
|
||
await file.download_to_drive(tmp_file_path)
|
||
|
||
# Проверяем, что пользователь имеет право сдавать это ДЗ
|
||
if db_user.role != 'client' or db_user not in await sync_to_async(list)(homework.assigned_to.all()):
|
||
await update.message.reply_text(
|
||
"❌ У вас нет доступа к этому заданию."
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
from django.utils import timezone as tz
|
||
from apps.homework.utils import sanitize_filename
|
||
filename = sanitize_filename(f"photo_{homework.id}_{tz.now().strftime('%Y%m%d_%H%M%S')}.jpg")
|
||
|
||
# Проверяем, есть ли уже решение
|
||
existing_submission = await sync_to_async(
|
||
HomeworkSubmission.objects.filter(
|
||
homework=homework,
|
||
student=db_user
|
||
).order_by('-attempt_number').first
|
||
)()
|
||
|
||
if existing_submission and existing_submission.status != 'returned':
|
||
# Обновляем существующее решение
|
||
with open(tmp_file_path, 'rb') as f:
|
||
django_file = File(f, name=filename)
|
||
existing_submission.attachment = django_file
|
||
existing_submission.content = f"Решение загружено через Telegram (фото)"
|
||
existing_submission.status = 'pending'
|
||
await sync_to_async(existing_submission.save)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение обновлено!</b>\n\n"
|
||
f"📎 Фото загружено\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
# Определяем номер попытки
|
||
attempt_number = 1
|
||
if existing_submission:
|
||
attempt_number = existing_submission.attempt_number + 1
|
||
|
||
# Создаем новое решение
|
||
with open(tmp_file_path, 'rb') as f:
|
||
django_file = File(f, name=filename)
|
||
submission = HomeworkSubmission(
|
||
homework=homework,
|
||
student=db_user,
|
||
content="Решение загружено через Telegram (фото)",
|
||
attachment=django_file,
|
||
status='pending',
|
||
attempt_number=attempt_number
|
||
)
|
||
await sync_to_async(submission.save)()
|
||
|
||
# Проверяем опоздание
|
||
await sync_to_async(submission.check_if_late)()
|
||
|
||
# Обновляем статистику задания
|
||
await sync_to_async(homework.update_statistics)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение загружено!</b>\n\n"
|
||
f"📎 Фото загружено\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
# Отправляем уведомление ментору
|
||
from apps.notifications.services import NotificationService
|
||
await sync_to_async(NotificationService.create_notification_with_telegram)(
|
||
recipient=homework.mentor,
|
||
notification_type='homework_submitted',
|
||
title='📝 ДЗ сдано',
|
||
message=f'{db_user.get_full_name()} сдал ДЗ "{homework.title}"',
|
||
priority='normal',
|
||
action_url=f'/homework/{homework.id}/',
|
||
content_object=homework
|
||
)
|
||
|
||
# Очищаем состояние ожидания
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки для навигации:",
|
||
reply_markup=keyboard
|
||
)
|
||
finally:
|
||
# Гарантированно удаляем временный файл
|
||
if tmp_file_path and os.path.exists(tmp_file_path):
|
||
try:
|
||
os.unlink(tmp_file_path)
|
||
except Exception as e:
|
||
logger.error(f"Error deleting temp file {tmp_file_path}: {e}")
|
||
|
||
await bot.close()
|
||
|
||
except Homework.DoesNotExist:
|
||
await update.message.reply_text(
|
||
"❌ Домашнее задание не найдено."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
except Exception as e:
|
||
logger.error(f"Error handling photo upload: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка загрузки фото.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
def setup_handlers(self):
|
||
"""Настройка обработчиков команд."""
|
||
if not self.application:
|
||
return
|
||
|
||
# Команды
|
||
self.application.add_handler(CommandHandler("start", self.start_command))
|
||
self.application.add_handler(CommandHandler("help", self.help_command))
|
||
self.application.add_handler(CommandHandler("link", self.link_command))
|
||
self.application.add_handler(CommandHandler("unlink", self.unlink_command))
|
||
self.application.add_handler(CommandHandler("status", self.status_command))
|
||
self.application.add_handler(CommandHandler("settings", self.settings_command))
|
||
|
||
# Команды для клиентов и менторов
|
||
self.application.add_handler(CommandHandler("schedule", self.schedule_command))
|
||
self.application.add_handler(CommandHandler("nextlesson", self.nextlesson_command))
|
||
self.application.add_handler(CommandHandler("homework", self.homework_command))
|
||
|
||
# Команды для клиентов
|
||
self.application.add_handler(CommandHandler("progress", self.progress_command))
|
||
|
||
# Команды для менторов
|
||
self.application.add_handler(CommandHandler("clients", self.clients_command))
|
||
self.application.add_handler(CommandHandler("stats", self.stats_command))
|
||
|
||
# Кнопки
|
||
self.application.add_handler(CallbackQueryHandler(self.button_callback))
|
||
|
||
# Обычные сообщения
|
||
self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message))
|
||
|
||
# Загрузка документов (для решений ДЗ)
|
||
self.application.add_handler(MessageHandler(filters.Document.ALL, self.handle_document))
|
||
|
||
# Загрузка фото (для решений ДЗ)
|
||
self.application.add_handler(MessageHandler(filters.PHOTO, self.handle_photo))
|
||
|
||
async def start(self):
|
||
"""Запуск бота."""
|
||
if not self.token:
|
||
logger.error("TELEGRAM_BOT_TOKEN not set")
|
||
return
|
||
|
||
logger.info("Starting Telegram bot...")
|
||
|
||
# Создаем приложение
|
||
self.application = Application.builder().token(self.token).build()
|
||
|
||
# Настраиваем обработчики
|
||
self.setup_handlers()
|
||
|
||
# Запускаем бота
|
||
await self.application.initialize()
|
||
await self.application.start()
|
||
|
||
if self.use_webhook and self.webhook_url:
|
||
# Используем webhook режим
|
||
logger.info(f"Setting up webhook: {self.webhook_url}")
|
||
await self.setup_webhook()
|
||
else:
|
||
# Используем polling режим
|
||
logger.info("Starting bot in polling mode...")
|
||
await self.application.updater.start_polling()
|
||
|
||
logger.info("Telegram bot started successfully")
|
||
|
||
async def setup_webhook(self):
|
||
"""Настройка webhook для бота."""
|
||
from telegram import Bot
|
||
from telegram.error import TelegramError
|
||
|
||
try:
|
||
bot = Bot(token=self.token)
|
||
|
||
# Устанавливаем webhook
|
||
webhook_kwargs = {
|
||
'url': self.webhook_url,
|
||
'allowed_updates': ['message', 'callback_query', 'inline_query', 'chosen_inline_result'],
|
||
}
|
||
|
||
# Добавляем secret token если указан
|
||
if self.webhook_secret_token:
|
||
webhook_kwargs['secret_token'] = self.webhook_secret_token
|
||
|
||
await bot.set_webhook(**webhook_kwargs)
|
||
|
||
# Проверяем информацию о webhook
|
||
webhook_info = await bot.get_webhook_info()
|
||
logger.info(f"Webhook info: {webhook_info.url}, pending updates: {webhook_info.pending_update_count}")
|
||
|
||
await bot.close()
|
||
logger.info("Webhook set successfully")
|
||
except TelegramError as e:
|
||
logger.error(f"Error setting webhook: {e}")
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Unexpected error setting webhook: {e}")
|
||
raise
|
||
|
||
async def remove_webhook(self):
|
||
"""Удаление webhook."""
|
||
from telegram import Bot
|
||
from telegram.error import TelegramError
|
||
|
||
try:
|
||
bot = Bot(token=self.token)
|
||
await bot.delete_webhook(drop_pending_updates=True)
|
||
await bot.close()
|
||
logger.info("Webhook removed successfully")
|
||
except TelegramError as e:
|
||
logger.error(f"Error removing webhook: {e}")
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Unexpected error removing webhook: {e}")
|
||
raise
|
||
|
||
async def stop(self):
|
||
"""Остановка бота."""
|
||
if self.application:
|
||
logger.info("Stopping Telegram bot...")
|
||
if not self.use_webhook:
|
||
# Останавливаем polling только если не используем webhook
|
||
await self.application.updater.stop()
|
||
await self.application.stop()
|
||
await self.application.shutdown()
|
||
logger.info("Telegram bot stopped")
|
||
|
||
def get_application(self):
|
||
"""
|
||
Получить экземпляр Application для обработки webhook.
|
||
|
||
ВАЖНО: Этот метод создает приложение, но не инициализирует его.
|
||
Инициализация происходит в process_webhook_update при первом запросе.
|
||
"""
|
||
if not self.application:
|
||
if not self.token:
|
||
logger.error("TELEGRAM_BOT_TOKEN not set")
|
||
return None
|
||
|
||
# Создаем приложение
|
||
self.application = Application.builder().token(self.token).build()
|
||
|
||
# Настраиваем обработчики
|
||
self.setup_handlers()
|
||
|
||
logger.info("Bot application created for webhook")
|
||
|
||
return self.application
|
||
|
||
async def process_webhook_update(self, update: Update):
|
||
"""
|
||
Обработать update от webhook.
|
||
|
||
Args:
|
||
update: Update объект от Telegram
|
||
"""
|
||
# Получаем или создаем приложение
|
||
if not self.application:
|
||
self.get_application()
|
||
|
||
if not self.application:
|
||
logger.error("Failed to initialize bot application")
|
||
return False
|
||
|
||
try:
|
||
# Убеждаемся что приложение инициализировано и запущено
|
||
# Для webhook режима мы не используем updater, только application
|
||
if not hasattr(self.application, '_webhook_initialized'):
|
||
await self.application.initialize()
|
||
await self.application.start()
|
||
self.application._webhook_initialized = True
|
||
logger.info("Bot application initialized for webhook")
|
||
|
||
# Обрабатываем update
|
||
await self.application.process_update(update)
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Error processing webhook update: {e}", exc_info=True)
|
||
return False
|
||
|
||
|
||
# Глобальный экземпляр бота
|
||
bot_instance = None
|
||
|
||
|
||
async def get_bot():
|
||
"""Получить экземпляр бота."""
|
||
global bot_instance
|
||
|
||
if bot_instance is None:
|
||
bot_instance = TelegramBot()
|
||
await bot_instance.start()
|
||
|
||
return bot_instance
|
||
|
||
|
||
async def send_telegram_message(telegram_id: int, message: str, parse_mode: str = 'HTML'):
|
||
"""
|
||
Отправить сообщение в Telegram.
|
||
|
||
Args:
|
||
telegram_id: ID пользователя в Telegram
|
||
message: Текст сообщения
|
||
parse_mode: Режим парсинга (HTML, Markdown)
|
||
"""
|
||
from telegram import Bot
|
||
from telegram.error import TelegramError
|
||
|
||
if not settings.TELEGRAM_BOT_TOKEN:
|
||
logger.error("TELEGRAM_BOT_TOKEN not set")
|
||
return False
|
||
|
||
try:
|
||
# Создаем временный экземпляр бота для отправки сообщения
|
||
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
|
||
await bot.send_message(
|
||
chat_id=telegram_id,
|
||
text=message,
|
||
parse_mode=parse_mode
|
||
)
|
||
await bot.close()
|
||
logger.info(f"Telegram message sent to {telegram_id}")
|
||
return True
|
||
except TelegramError as e:
|
||
logger.error(f"Telegram API error sending message to {telegram_id}: {e}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"Error sending Telegram message to {telegram_id}: {e}")
|
||
return False
|
||
|
||
|
||
async def send_telegram_message_with_buttons(telegram_id: int, message: str, reply_markup, parse_mode: str = 'HTML'):
|
||
"""
|
||
Отправить сообщение в Telegram с кнопками.
|
||
|
||
Args:
|
||
telegram_id: ID пользователя в Telegram
|
||
message: Текст сообщения
|
||
reply_markup: InlineKeyboardMarkup с кнопками
|
||
parse_mode: Режим парсинга (HTML, Markdown)
|
||
"""
|
||
from telegram import Bot
|
||
from telegram.error import TelegramError
|
||
|
||
if not settings.TELEGRAM_BOT_TOKEN:
|
||
logger.error("TELEGRAM_BOT_TOKEN not set")
|
||
return False
|
||
|
||
try:
|
||
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
|
||
await bot.send_message(
|
||
chat_id=telegram_id,
|
||
text=message,
|
||
parse_mode=parse_mode,
|
||
reply_markup=reply_markup
|
||
)
|
||
await bot.close()
|
||
logger.info(f"Telegram message with buttons sent to {telegram_id}")
|
||
return True
|
||
except TelegramError as e:
|
||
logger.error(f"Telegram API error sending message with buttons to {telegram_id}: {e}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"Error sending Telegram message with buttons to {telegram_id}: {e}")
|
||
return False
|
||
|