""" 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) 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"✅ Аккаунт связан\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 = "⚙️ Настройки\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_tz = db_user.timezone or 'Europe/Moscow' message_text += f"🕐 Часовой пояс: {user_tz}\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_timezone" ), 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_'): 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 self._handle_timezone(query, db_user) 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_"): timezone = query.data.replace("set_timezone_", "") db_user.timezone = timezone await sync_to_async(db_user.save)(update_fields=['timezone']) await query.answer(f"Часовой пояс установлен: {timezone}") await self._handle_timezone(query, db_user) 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_'): 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"📎 Загрузка решения\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"📝 {homework.title}\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✅ Статус: Сдано\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⏳ Статус: Не сдано\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').get)(id=submission_id) if submission.student != db_user and db_user.role != 'mentor': await query.answer("❌ У вас нет доступа", show_alert=True) return message = f"👁️ Решение ДЗ\n\n" message += f"📝 Задание: {submission.homework.title}\n" message += f"👤 Студент: {submission.student.get_full_name()}\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" if submission.feedback: feedback = submission.feedback[:200] + "..." if len(submission.feedback) > 200 else submission.feedback message += f"\n💬 Отзыв: {feedback}\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 == 'homework_back': # Возврат к списку заданий # Отправляем новое сообщение со списком from apps.homework.models import Homework, HomeworkSubmission from django.utils import timezone homeworks = await sync_to_async(list)( Homework.objects.filter( assigned_to=db_user, deadline__gte=timezone.now() ).order_by('deadline')[:5] ) if not homeworks: await query.edit_message_text("📝 У вас нет активных домашних заданий.") return message = "📝 Активные домашние задания:\n\n" inline_keyboard = [] for hw in homeworks: deadline_str = hw.deadline.strftime('%d.%m.%Y') if hw.deadline else 'Без дедлайна' submission = await sync_to_async( HomeworkSubmission.objects.filter( homework=hw, student=db_user ).first )() status = "✅ Сдано" if submission else "⏳ Не сдано" message += f"{hw.title}\n" message += f"📅 Дедлайн: {deadline_str}\n" message += f"{status}\n\n" inline_keyboard.append([ InlineKeyboardButton( f"📝 {hw.title[:30]}", callback_data=f"homework_detail_{hw.id}" ) ]) reply_markup = InlineKeyboardMarkup(inline_keyboard) await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup) 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"📚 {lesson.title}\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🔗 Ссылка на видеоконференцию\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"👤 {client_name}\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"📚 Занятия:\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"📝 Домашние задания:\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💰 Доходы:\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 = "👥 Ваши клиенты:\n\n" inline_keyboard = [] for client in clients: client_name = client.user.get_full_name() or client.user.email message += f"👤 {client_name}\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('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"✅ Ответ сохранен\n\n" f"Вы подтвердили, что {response_text} на занятии:\n" f"{lesson.title}\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 self._handle_timezone(query, db_user) 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_"): timezone = query.data.replace("set_timezone_", "") db_user.timezone = timezone await sync_to_async(db_user.save)(update_fields=['timezone']) await query.answer(f"Часовой пояс установлен: {timezone}") await self._handle_timezone(query, db_user) 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 = "📅 Ближайшие занятия:\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"📚 {lesson.title}\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"📚 {lesson.title}\n" message += f"👨‍🏫 {lesson.mentor.get_full_name()}\n" message += f"👶 {child_name}\n" else: message += f"📚 {lesson.title}\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 = "📚 Следующее занятие:\n\n" message += f"{lesson.title}\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 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': homeworks = await sync_to_async(list)( Homework.objects.filter( assigned_to=db_user, deadline__gte=timezone.now() ).order_by('deadline')[:5] ) if not homeworks: keyboard = await get_user_keyboard(telegram_id) await update.message.reply_text( "📝 У вас нет активных домашних заданий.", reply_markup=keyboard ) return # Если только одно задание, показываем детали с кнопками if len(homeworks) == 1: hw = homeworks[0] deadline_str = hw.deadline.strftime('%d.%m.%Y в %H:%M') if hw.deadline else 'Без дедлайна' # Проверяем сдано ли submission = await sync_to_async( HomeworkSubmission.objects.filter( homework=hw, student=db_user ).first )() message = f"📝 {hw.title}\n\n" message += f"📅 Дедлайн: {deadline_str}\n" if hw.description: desc = hw.description[:200] + "..." if len(hw.description) > 200 else hw.description message += f"\n📄 {desc}\n" if submission: message += f"\n✅ Статус: Сдано\n" if submission.status == 'graded' and submission.score is not None: message += f"🎯 Оценка: {submission.score}/{hw.max_score}\n" elif submission.status == 'returned': message += f"🔄 Возвращено на доработку\n" else: message += f"⏳ Ожидает проверки\n" else: message += f"\n⏳ Статус: Не сдано\n" # Создаем inline клавиатуру inline_keyboard = [] if not submission or submission.status == 'returned': inline_keyboard.append([ InlineKeyboardButton( "📎 Загрузить решение", callback_data=f"homework_upload_{hw.id}" ) ]) if submission: inline_keyboard.append([ InlineKeyboardButton( "👁️ Просмотреть решение", callback_data=f"homework_view_{submission.id}" ) ]) inline_keyboard.append([ InlineKeyboardButton( "🌐 Открыть на сайте", url=f"{settings.FRONTEND_URL}/homework" ) ]) reply_markup = InlineKeyboardMarkup(inline_keyboard) keyboard = await get_user_keyboard(telegram_id) await update.message.reply_text(message, parse_mode='HTML', reply_markup=reply_markup) await update.message.reply_text("Используйте кнопки для навигации:", reply_markup=keyboard) else: # Если несколько заданий, показываем список с кнопками message = "📝 Активные домашние задания:\n\n" inline_keyboard = [] for hw in homeworks: deadline_str = hw.deadline.strftime('%d.%m.%Y') if hw.deadline else 'Без дедлайна' # Проверяем сдано ли submission = await sync_to_async( HomeworkSubmission.objects.filter( homework=hw, student=db_user ).first )() status = "✅ Сдано" if submission else "⏳ Не сдано" message += f"{hw.title}\n" message += f"📅 Дедлайн: {deadline_str}\n" message += f"{status}\n\n" # Кнопка для просмотра деталей inline_keyboard.append([ InlineKeyboardButton( f"📝 {hw.title[:30]}", callback_data=f"homework_detail_{hw.id}" ) ]) reply_markup = InlineKeyboardMarkup(inline_keyboard) keyboard = await get_user_keyboard(telegram_id) await update.message.reply_text(message, parse_mode='HTML', reply_markup=reply_markup) await update.message.reply_text("Выберите задание для просмотра:", reply_markup=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 = "📝 Домашние задания детей:\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"{hw.title}\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': submissions = await sync_to_async(list)( HomeworkSubmission.objects.filter( homework__mentor=db_user, status='pending' ).order_by('-submitted_at')[:5] ) if not submissions: keyboard = await get_user_keyboard(telegram_id) await update.message.reply_text( "📝 Нет домашних заданий, требующих проверки.", reply_markup=keyboard ) return message = "📝 Домашние задания на проверку:\n\n" for submission in submissions: student_name = submission.student.get_full_name() if submission.student else 'Студент' hw_title = submission.homework.title submitted_at = submission.submitted_at.strftime('%d.%m.%Y') if submission.submitted_at else 'Неизвестно' message += f"{hw_title}\n" message += f"👤 {student_name}\n" message += f"📅 Сдано: {submitted_at}\n\n" keyboard = await get_user_keyboard(telegram_id) await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard) else: keyboard = await get_user_keyboard(telegram_id) await update.message.reply_text( "❌ Эта команда доступна только для менторов, клиентов и родителей.", reply_markup=keyboard ) except Exception as e: logger.error(f"Error in homework_command: {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"✅ Решение обновлено!\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"✅ Решение загружено!\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 _refresh_settings_message(self, query, db_user, preferences): """Обновить сообщение с настройками.""" message_text = "⚙️ Настройки\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_tz = db_user.timezone or 'Europe/Moscow' message_text += f"🕐 Часовой пояс: {user_tz}\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_timezone"), 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 = "🔇 Режим тишины\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 = "📋 Типы уведомлений\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 = "🕐 Часовой пояс\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 = "🌐 Язык интерфейса\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 = "👥 Ваши клиенты:\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"👤 {client_name}\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 = "📊 Ваша статистика:\n\n" message += f"👥 Клиентов: {total_clients}\n\n" message += f"📚 Занятия:\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"📝 Домашние задания:\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💰 Доходы:\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): """Обработчик команды /progress - прогресс обучения для клиента.""" 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 != 'client': 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 HomeworkSubmission from django.utils import timezone from datetime import timedelta now = timezone.now() month_ago = now - timedelta(days=30) # Занятия за последний месяц lessons = await sync_to_async(list)( Lesson.objects.filter( client__user=db_user, start_time__gte=month_ago ) ) total_lessons = len(lessons) completed_lessons = len([l for l in lessons if l.status == 'completed']) completion_rate = (completed_lessons / total_lessons * 100) if total_lessons > 0 else 0 # Домашние задания за последний месяц submissions = await sync_to_async(list)( HomeworkSubmission.objects.filter( student=db_user, submitted_at__gte=month_ago ) ) graded_submissions = [s for s in submissions if s.status == 'graded'] passed_submissions = len([s for s in graded_submissions if s.passed]) total_graded = len(graded_submissions) pass_rate = (passed_submissions / total_graded * 100) if total_graded > 0 else 0 # Средний балл if graded_submissions: scores = [s.score for s in graded_submissions if s.score is not None] avg_score = sum(scores) / len(scores) if scores else 0 else: avg_score = 0 # Всего занятий (все время) all_lessons = await sync_to_async(list)( Lesson.objects.filter(client__user=db_user) ) total_all_lessons = len(all_lessons) completed_all_lessons = len([l for l in all_lessons if l.status == 'completed']) # Всего ДЗ (все время) all_submissions = await sync_to_async(list)( HomeworkSubmission.objects.filter(student=db_user) ) total_all_homeworks = len(all_submissions) graded_all_homeworks = len([s for s in all_submissions if s.status == 'graded']) message = "📊 Ваш прогресс:\n\n" message += f"📅 За последний месяц:\n" message += f"• Занятий: {total_lessons} (завершено: {completed_lessons})\n" message += f"• Процент завершения: {completion_rate:.1f}%\n" message += f"• ДЗ сдано: {len(submissions)} (проверено: {total_graded})\n" if total_graded > 0: message += f"• Процент сдачи: {pass_rate:.1f}%\n" message += f"• Средний балл: {avg_score:.1f}\n" message += f"\n📈 Всего:\n" message += f"• Занятий: {total_all_lessons} (завершено: {completed_all_lessons})\n" message += f"• ДЗ сдано: {total_all_homeworks} (проверено: {graded_all_homeworks})\n" # Топ предметы if completed_lessons > 0: from collections import Counter subjects = [l.subject.name if l.subject else 'Без предмета' for l in lessons if l.status == 'completed' and l.subject] if subjects: subject_counts = Counter(subjects) top_subjects = subject_counts.most_common(3) message += f"\n📚 Топ предметы (месяц):\n" for subject, count in top_subjects: message += f"• {subject}: {count} занятий\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 progress_command: {e}", exc_info=True) keyboard = await get_user_keyboard(telegram_id) await update.message.reply_text( "❌ Ошибка получения прогресса.\n\n" "Попробуйте позже или обратитесь в поддержку.", 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)() # Проверяем, ожидается ли загрузка решения для ДЗ 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 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( "🔗 Связывание аккаунта\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"✅ Решение обновлено!\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"✅ Решение загружено!\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"✅ Решение обновлено!\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"✅ Решение загружено!\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