3294 lines
162 KiB
Python
3294 lines
162 KiB
Python
"""
|
||
Telegram бот для уведомлений и интеграции.
|
||
"""
|
||
import logging
|
||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton
|
||
from telegram.ext import (
|
||
Application,
|
||
CommandHandler,
|
||
CallbackQueryHandler,
|
||
MessageHandler,
|
||
filters,
|
||
ContextTypes,
|
||
)
|
||
from django.conf import settings
|
||
from django.utils import timezone
|
||
from asgiref.sync import sync_to_async
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def get_main_keyboard(role=None):
|
||
"""
|
||
Получить основную клавиатуру в зависимости от роли.
|
||
|
||
Args:
|
||
role: Роль пользователя ('mentor', 'client', 'parent', None)
|
||
|
||
Returns:
|
||
ReplyKeyboardMarkup: Клавиатура с кнопками
|
||
"""
|
||
if role == 'mentor':
|
||
keyboard = [
|
||
[KeyboardButton("📅 Расписание"), KeyboardButton("📚 Следующее занятие")],
|
||
[KeyboardButton("📝 Домашние задания"), KeyboardButton("👥 Клиенты")],
|
||
[KeyboardButton("📊 Статистика"), KeyboardButton("⚙️ Настройки")],
|
||
[KeyboardButton("ℹ️ Статус"), KeyboardButton("❓ Помощь")]
|
||
]
|
||
elif role == 'client':
|
||
keyboard = [
|
||
[KeyboardButton("📅 Моё расписание"), KeyboardButton("📚 Следующее занятие")],
|
||
[KeyboardButton("📝 Мои задания"), KeyboardButton("📊 Мой прогресс")],
|
||
[KeyboardButton("⚙️ Настройки"), KeyboardButton("❓ Помощь")]
|
||
]
|
||
elif role == 'parent':
|
||
keyboard = [
|
||
[KeyboardButton("📅 Расписание детей"), KeyboardButton("📚 Следующее занятие")],
|
||
[KeyboardButton("📝 Задания детей"), KeyboardButton("⚙️ Настройки")],
|
||
[KeyboardButton("ℹ️ Статус"), KeyboardButton("❓ Помощь")]
|
||
]
|
||
else:
|
||
# Для не связанных пользователей
|
||
keyboard = [
|
||
[KeyboardButton("🔗 Связать аккаунт"), KeyboardButton("❓ Помощь")]
|
||
]
|
||
|
||
return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False)
|
||
|
||
|
||
async def get_user_keyboard(telegram_id):
|
||
"""
|
||
Получить клавиатуру для пользователя по его telegram_id.
|
||
|
||
Args:
|
||
telegram_id: ID пользователя в Telegram
|
||
|
||
Returns:
|
||
ReplyKeyboardMarkup: Клавиатура с кнопками
|
||
"""
|
||
from apps.users.models import User
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
role = db_user.role if db_user else None
|
||
return get_main_keyboard(role)
|
||
|
||
|
||
class TelegramBot:
|
||
"""Класс для управления Telegram ботом."""
|
||
|
||
def __init__(self):
|
||
"""Инициализация бота."""
|
||
self.token = settings.TELEGRAM_BOT_TOKEN
|
||
self.application = None
|
||
self.use_webhook = getattr(settings, 'TELEGRAM_USE_WEBHOOK', False)
|
||
self.webhook_url = getattr(settings, 'TELEGRAM_WEBHOOK_URL', None)
|
||
self.webhook_secret_token = getattr(settings, 'TELEGRAM_WEBHOOK_SECRET_TOKEN', None)
|
||
|
||
async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Обработчик команды /start.
|
||
Приветствие и инструкции по связыванию аккаунта.
|
||
"""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
# Проверяем связан ли аккаунт
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
# Если аккаунт не связан, показываем общее приветствие
|
||
welcome_message = f"""
|
||
👋 Привет, {user.first_name}!
|
||
|
||
Я бот образовательной платформы. Я буду присылать вам уведомления о:
|
||
• Новых занятиях
|
||
• Домашних заданиях
|
||
• Сообщениях от ментора/ученика
|
||
• Напоминаниях о занятиях
|
||
|
||
📱 Чтобы связать ваш аккаунт:
|
||
1. Войдите на платформу
|
||
2. Перейдите в Профиль → Настройки → Telegram
|
||
3. Нажмите "Связать Telegram"
|
||
4. Введите код связывания
|
||
|
||
Или используйте команду:
|
||
/link <ваш_код_связывания>
|
||
|
||
После связывания вы увидите команды, доступные для вашей роли.
|
||
"""
|
||
else:
|
||
# Если аккаунт связан, показываем приветствие в зависимости от роли
|
||
role = db_user.role
|
||
user_name = db_user.get_full_name() or db_user.email
|
||
|
||
if role == 'mentor':
|
||
welcome_message = f"""
|
||
👋 Привет, {user_name}!
|
||
|
||
Я бот образовательной платформы для менторов.
|
||
|
||
Я буду присылать вам уведомления о:
|
||
• Новых запросах на занятия
|
||
• Отменах занятий
|
||
• Сданных домашних заданиях
|
||
• Сообщениях от учеников
|
||
• Напоминаниях о занятиях
|
||
|
||
📚 Доступные команды:
|
||
/help - Полный список команд
|
||
/schedule - Расписание занятий
|
||
/nextlesson - Следующее занятие
|
||
/homework - Домашние задания на проверку
|
||
/clients - Список клиентов
|
||
/stats - Статистика
|
||
/settings - Настройки уведомлений
|
||
/status - Статус аккаунта
|
||
"""
|
||
keyboard = get_main_keyboard('mentor')
|
||
await update.message.reply_text(
|
||
welcome_message,
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
elif role == 'client':
|
||
welcome_message = f"""
|
||
👋 Привет, {user_name}!
|
||
|
||
Я бот образовательной платформы для учеников.
|
||
|
||
Я буду присылать вам уведомления о:
|
||
• Новых занятиях
|
||
• Отменах занятий
|
||
• Новых домашних заданиях
|
||
• Проверенных домашних заданиях
|
||
• Сообщениях от ментора
|
||
• Напоминаниях о занятиях
|
||
|
||
📚 Доступные команды:
|
||
/help - Полный список команд
|
||
/schedule - Моё расписание
|
||
/nextlesson - Следующее занятие
|
||
/homework - Мои домашние задания
|
||
/progress - Мой прогресс обучения
|
||
/settings - Настройки уведомлений
|
||
/status - Статус аккаунта
|
||
"""
|
||
keyboard = get_main_keyboard('client')
|
||
await update.message.reply_text(
|
||
welcome_message,
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
elif role == 'parent':
|
||
welcome_message = f"""
|
||
👋 Привет, {user_name}!
|
||
|
||
Я бот образовательной платформы для родителей.
|
||
|
||
Я буду присылать вам уведомления о:
|
||
• Занятиях ваших детей
|
||
• Домашних заданиях детей
|
||
• Прогрессе обучения
|
||
• Отчётах от менторов
|
||
|
||
📚 Доступные команды:
|
||
/help - Полный список команд
|
||
/schedule - Расписание детей
|
||
/nextlesson - Следующее занятие ребёнка
|
||
/homework - Домашние задания детей
|
||
/settings - Настройки уведомлений
|
||
/status - Статус аккаунта
|
||
"""
|
||
keyboard = get_main_keyboard('parent')
|
||
await update.message.reply_text(
|
||
welcome_message,
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
else:
|
||
# Для других ролей (admin и т.д.)
|
||
welcome_message = f"""
|
||
👋 Привет, {user_name}!
|
||
|
||
Я бот образовательной платформы.
|
||
|
||
Я буду присылать вам уведомления о событиях на платформе.
|
||
|
||
📚 Доступные команды:
|
||
/help - Полный список команд
|
||
/settings - Настройки уведомлений
|
||
/status - Статус аккаунта
|
||
"""
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
welcome_message,
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Если дошли сюда, отправляем без клавиатуры
|
||
await update.message.reply_text(welcome_message)
|
||
|
||
async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /help."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
# Проверяем связан ли аккаунт
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
# Если аккаунт не связан, показываем общую справку
|
||
help_text = """
|
||
🔔 Доступные команды:
|
||
|
||
/start - Начать работу с ботом
|
||
/link <код> - Связать аккаунт с платформой
|
||
/help - Эта справка
|
||
|
||
💡 Как связать аккаунт:
|
||
1. Получите код связывания на платформе (Профиль → Telegram)
|
||
2. Отправьте команду: /link ВАШ_КОД
|
||
|
||
После связывания вы увидите команды, доступные для вашей роли.
|
||
"""
|
||
else:
|
||
# Если аккаунт связан, показываем справку в зависимости от роли
|
||
role = db_user.role
|
||
|
||
if role == 'mentor':
|
||
help_text = """
|
||
👨🏫 Справка для менторов:
|
||
|
||
🔔 Основные команды:
|
||
/start - Главное меню
|
||
/help - Эта справка
|
||
/status - Статус аккаунта
|
||
/settings - Настройки уведомлений
|
||
|
||
📚 Расписание:
|
||
/schedule - Ближайшие занятия (до 5 занятий)
|
||
/nextlesson - Следующее занятие
|
||
|
||
📝 Домашние задания:
|
||
/homework - Задания, требующие проверки
|
||
|
||
👥 Клиенты:
|
||
/clients - Список ваших клиентов со статистикой
|
||
/stats - Ваша статистика (занятия, ДЗ, клиенты)
|
||
|
||
🔗 Управление аккаунтом:
|
||
/unlink - Отвязать Telegram аккаунт
|
||
|
||
💡 Вы будете получать уведомления о:
|
||
• Новых запросах на занятия
|
||
• Отменах занятий
|
||
• Сданных домашних заданиях
|
||
• Сообщениях от учеников
|
||
"""
|
||
elif role == 'client':
|
||
help_text = """
|
||
👨🎓 Справка для учеников:
|
||
|
||
🔔 Основные команды:
|
||
/start - Главное меню
|
||
/help - Эта справка
|
||
/status - Статус аккаунта
|
||
/settings - Настройки уведомлений
|
||
|
||
📚 Расписание:
|
||
/schedule - Моё расписание (до 5 ближайших занятий)
|
||
/nextlesson - Следующее занятие
|
||
|
||
📝 Домашние задания:
|
||
/homework - Мои активные домашние задания
|
||
|
||
📊 Прогресс:
|
||
/progress - Мой прогресс обучения
|
||
|
||
🔗 Управление аккаунтом:
|
||
/unlink - Отвязать Telegram аккаунт
|
||
|
||
💡 Вы будете получать уведомления о:
|
||
• Новых занятиях
|
||
• Отменах занятий
|
||
• Новых домашних заданиях
|
||
• Проверенных домашних заданиях
|
||
• Сообщениях от ментора
|
||
"""
|
||
elif role == 'parent':
|
||
help_text = """
|
||
👨👩👧 Справка для родителей:
|
||
|
||
🔔 Основные команды:
|
||
/start - Главное меню
|
||
/help - Эта справка
|
||
/status - Статус аккаунта
|
||
/settings - Настройки уведомлений
|
||
|
||
📚 Расписание детей:
|
||
/schedule - Расписание всех детей (до 5 ближайших занятий)
|
||
/nextlesson - Следующее занятие ребёнка
|
||
|
||
📝 Домашние задания:
|
||
/homework - Домашние задания детей
|
||
|
||
🔗 Управление аккаунтом:
|
||
/unlink - Отвязать Telegram аккаунт
|
||
|
||
💡 Вы будете получать уведомления о:
|
||
• Занятиях ваших детей
|
||
• Домашних заданиях детей
|
||
• Прогрессе обучения
|
||
• Отчётах от менторов
|
||
"""
|
||
else:
|
||
# Для других ролей (admin и т.д.)
|
||
help_text = """
|
||
🔔 Доступные команды:
|
||
|
||
/start - Начать работу с ботом
|
||
/help - Эта справка
|
||
/settings - Настройки уведомлений
|
||
/status - Статус связывания
|
||
/unlink - Отвязать аккаунт
|
||
|
||
💡 Вы будете получать уведомления о событиях на платформе.
|
||
"""
|
||
|
||
# Добавляем клавиатуру если аккаунт связан
|
||
if db_user:
|
||
keyboard = get_main_keyboard(db_user.role)
|
||
await update.message.reply_text(help_text, reply_markup=keyboard)
|
||
else:
|
||
await update.message.reply_text(help_text)
|
||
|
||
async def link_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Обработчик команды /link.
|
||
Связывание Telegram аккаунта с аккаунтом на платформе.
|
||
"""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
telegram_username = user.username or ''
|
||
|
||
# Проверяем наличие кода
|
||
if not context.args:
|
||
await update.message.reply_text(
|
||
"❌ Укажите код связывания.\n\n"
|
||
"Использование: /link <код>\n\n"
|
||
"Получите код на платформе: Профиль → Настройки → Telegram"
|
||
)
|
||
return
|
||
|
||
link_code = context.args[0]
|
||
|
||
# Проверяем код и связываем аккаунт
|
||
from .services import TelegramLinkService
|
||
|
||
try:
|
||
result = await sync_to_async(TelegramLinkService.link_account)(
|
||
link_code=link_code,
|
||
telegram_id=telegram_id,
|
||
telegram_username=telegram_username
|
||
)
|
||
|
||
if result['success']:
|
||
user_name = result.get('user_name', 'Пользователь')
|
||
# Получаем роль пользователя для клавиатуры
|
||
from apps.users.models import User
|
||
linked_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
keyboard = get_main_keyboard(linked_user.role if linked_user else None)
|
||
await update.message.reply_text(
|
||
f"✅ Аккаунт успешно связан!\n\n"
|
||
f"👤 {user_name}\n\n"
|
||
f"Теперь вы будете получать уведомления в Telegram.",
|
||
reply_markup=keyboard
|
||
)
|
||
else:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
f"❌ Ошибка связывания: {result.get('error', 'Неизвестная ошибка')}",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error linking Telegram account: {e}")
|
||
await update.message.reply_text(
|
||
"❌ Произошла ошибка при связывании аккаунта.\n"
|
||
"Попробуйте позже или обратитесь в поддержку."
|
||
)
|
||
|
||
async def unlink_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""
|
||
Обработчик команды /unlink.
|
||
Отвязка Telegram аккаунта.
|
||
"""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from .services import TelegramLinkService
|
||
|
||
try:
|
||
result = await sync_to_async(TelegramLinkService.unlink_account)(telegram_id)
|
||
|
||
if result['success']:
|
||
await update.message.reply_text(
|
||
"✅ Аккаунт успешно отвязан.\n\n"
|
||
"Вы больше не будете получать уведомления в Telegram.\n\n"
|
||
"Чтобы снова связать аккаунт, используйте /link"
|
||
)
|
||
else:
|
||
await update.message.reply_text(
|
||
f"❌ {result.get('error', 'Аккаунт не найден')}"
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error unlinking Telegram account: {e}")
|
||
await update.message.reply_text(
|
||
"❌ Произошла ошибка при отвязке аккаунта."
|
||
)
|
||
|
||
async def status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /status."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if db_user:
|
||
role_display = db_user.get_role_display()
|
||
role_emoji = {
|
||
'mentor': '👨🏫',
|
||
'client': '👨🎓',
|
||
'parent': '👨👩👧',
|
||
'admin': '👤'
|
||
}.get(db_user.role, '👤')
|
||
|
||
status_message = (
|
||
f"✅ <b>Аккаунт связан</b>\n\n"
|
||
f"{role_emoji} {db_user.get_full_name() or db_user.email}\n"
|
||
f"📧 {db_user.email}\n"
|
||
f"🎭 Роль: {role_display}\n\n"
|
||
)
|
||
|
||
# Дополнительная информация в зависимости от роли
|
||
if db_user.role == 'parent':
|
||
from apps.users.models import Parent
|
||
try:
|
||
parent_profile = await sync_to_async(lambda: db_user.parent_profile)()
|
||
children = await sync_to_async(list)(parent_profile.children.all())
|
||
children_count = len(children)
|
||
status_message += f"👶 Привязано детей: {children_count}\n\n"
|
||
except:
|
||
pass
|
||
|
||
# Проверяем настройки уведомлений
|
||
try:
|
||
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
|
||
notifications_status = "✅ Включены" if preferences.telegram_enabled else "❌ Выключены"
|
||
status_message += f"🔔 Уведомления: {notifications_status}"
|
||
except:
|
||
status_message += "🔔 Уведомления: ⚙️ Настройте на платформе"
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(status_message, parse_mode='HTML', reply_markup=keyboard)
|
||
else:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан\n\n"
|
||
"Используйте /link <код> для связывания аккаунта.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error checking status: {e}")
|
||
await update.message.reply_text(
|
||
"❌ Ошибка проверки статуса."
|
||
)
|
||
|
||
async def settings_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /settings."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Получаем настройки уведомлений
|
||
try:
|
||
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
|
||
except:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"⚙️ Настройки уведомлений не найдены.\n\n"
|
||
"Настройте уведомления на платформе: Профиль → Настройки",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Формируем расширенное меню настроек
|
||
message_text = "⚙️ <b>Настройки</b>\n\n"
|
||
|
||
# Статус уведомлений
|
||
all_status = "✅ Включены" if preferences.enabled else "❌ Выключены"
|
||
telegram_status = "✅ Вкл" if preferences.telegram_enabled else "❌ Выкл"
|
||
email_status = "✅ Вкл" if preferences.email_enabled else "❌ Выкл"
|
||
in_app_status = "✅ Вкл" if preferences.in_app_enabled else "❌ Выкл"
|
||
|
||
message_text += f"🔔 Все уведомления: {all_status}\n"
|
||
message_text += f"📱 Telegram: {telegram_status}\n"
|
||
message_text += f"📧 Email: {email_status}\n"
|
||
message_text += f"💬 В приложении: {in_app_status}\n\n"
|
||
|
||
# Режим тишины
|
||
if preferences.quiet_hours_enabled and preferences.quiet_hours_start and preferences.quiet_hours_end:
|
||
quiet_start = preferences.quiet_hours_start.strftime('%H:%M')
|
||
quiet_end = preferences.quiet_hours_end.strftime('%H:%M')
|
||
message_text += f"🔇 Режим тишины: {quiet_start} - {quiet_end}\n\n"
|
||
else:
|
||
message_text += "🔇 Режим тишины: ❌ Выключен\n\n"
|
||
|
||
# Часовой пояс
|
||
user_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"📎 <b>Загрузка решения</b>\n\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Отправьте:\n"
|
||
f"• Текст решения\n"
|
||
f"• Файл (документ)\n"
|
||
f"• Фото с решением\n\n"
|
||
f"Или отправьте 'отмена' для отмены.",
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
elif query.data.startswith('homework_detail_'):
|
||
# Детали задания
|
||
homework_id = int(query.data.replace('homework_detail_', ''))
|
||
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
|
||
|
||
deadline_str = homework.deadline.strftime('%d.%m.%Y в %H:%M') if homework.deadline else 'Без дедлайна'
|
||
|
||
submission = await sync_to_async(
|
||
HomeworkSubmission.objects.filter(
|
||
homework=homework,
|
||
student=db_user
|
||
).first
|
||
)()
|
||
|
||
message = f"📝 <b>{homework.title}</b>\n\n"
|
||
message += f"📅 Дедлайн: {deadline_str}\n"
|
||
if homework.description:
|
||
desc = homework.description[:200] + "..." if len(homework.description) > 200 else homework.description
|
||
message += f"\n📄 {desc}\n"
|
||
|
||
if submission:
|
||
message += f"\n✅ <b>Статус: Сдано</b>\n"
|
||
if submission.status == 'graded' and submission.score is not None:
|
||
message += f"🎯 Оценка: {submission.score}/{homework.max_score}\n"
|
||
elif submission.status == 'returned':
|
||
message += f"🔄 Возвращено на доработку\n"
|
||
else:
|
||
message += f"⏳ Ожидает проверки\n"
|
||
else:
|
||
message += f"\n⏳ <b>Статус: Не сдано</b>\n"
|
||
|
||
inline_keyboard = []
|
||
if not submission or submission.status == 'returned':
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
"📎 Загрузить решение",
|
||
callback_data=f"homework_upload_{homework.id}"
|
||
)
|
||
])
|
||
|
||
if submission:
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
"👁️ Просмотреть решение",
|
||
callback_data=f"homework_view_{submission.id}"
|
||
)
|
||
])
|
||
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
"◀️ Назад к списку",
|
||
callback_data="homework_back"
|
||
)
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
return
|
||
|
||
elif query.data.startswith('homework_view_'):
|
||
# Просмотр решения
|
||
submission_id = int(query.data.replace('homework_view_', ''))
|
||
|
||
from apps.homework.models import HomeworkSubmission
|
||
submission = await sync_to_async(HomeworkSubmission.objects.select_related('homework').get)(id=submission_id)
|
||
|
||
if submission.student != db_user and db_user.role != 'mentor':
|
||
await query.answer("❌ У вас нет доступа", show_alert=True)
|
||
return
|
||
|
||
message = f"👁️ <b>Решение ДЗ</b>\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 = "📝 <b>Активные домашние задания:</b>\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"<b>{hw.title}</b>\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"📚 <b>{lesson.title}</b>\n\n"
|
||
|
||
if lesson.subject:
|
||
message += f"📖 Предмет: {lesson.subject.name}\n"
|
||
|
||
message += f"🕐 Начало: {local_start.strftime('%d.%m.%Y в %H:%M')}\n"
|
||
if local_end:
|
||
message += f"🕐 Окончание: {local_end.strftime('%d.%m.%Y в %H:%M')}\n"
|
||
message += f"⏱ Длительность: {lesson.duration} минут\n"
|
||
message += f"📊 Статус: {lesson.get_status_display()}\n\n"
|
||
|
||
if db_user.role == 'mentor':
|
||
if lesson.client:
|
||
message += f"👤 Студент: {lesson.client.user.get_full_name()}\n"
|
||
else:
|
||
message += f"👨🏫 Ментор: {lesson.mentor.get_full_name()}\n"
|
||
|
||
if lesson.description:
|
||
desc = lesson.description[:200] + "..." if len(lesson.description) > 200 else lesson.description
|
||
message += f"\n📄 {desc}\n"
|
||
|
||
if lesson.meeting_url:
|
||
message += f"\n🔗 <a href='{lesson.meeting_url}'>Ссылка на видеоконференцию</a>\n"
|
||
|
||
if lesson.mentor_grade is not None:
|
||
message += f"\n🎯 Оценка: {lesson.mentor_grade}/100\n"
|
||
|
||
inline_keyboard = [[
|
||
InlineKeyboardButton(
|
||
"🌐 Открыть на сайте",
|
||
url=f"{settings.FRONTEND_URL}/schedule"
|
||
)
|
||
]]
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
except Lesson.DoesNotExist:
|
||
await query.answer("❌ Занятие не найдено", show_alert=True)
|
||
except Exception as e:
|
||
logger.error(f"Error handling lesson detail: {e}", exc_info=True)
|
||
await query.answer("❌ Ошибка обработки запроса", show_alert=True)
|
||
return
|
||
|
||
# Обработка деталей клиента (для менторов)
|
||
if query.data.startswith('client_detail_'):
|
||
try:
|
||
client_id = int(query.data.replace('client_detail_', ''))
|
||
|
||
from apps.users.models import Client, User
|
||
from apps.schedule.models import Lesson
|
||
from apps.homework.models import HomeworkSubmission
|
||
from django.utils import timezone
|
||
from datetime import timedelta
|
||
|
||
client = await sync_to_async(
|
||
Client.objects.select_related('user').get
|
||
)(id=client_id)
|
||
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user or db_user.role != 'mentor' or db_user not in await sync_to_async(list)(client.mentors.all()):
|
||
await query.answer("❌ У вас нет доступа", show_alert=True)
|
||
return
|
||
|
||
client_name = client.user.get_full_name() or client.user.email
|
||
message = f"👤 <b>{client_name}</b>\n\n"
|
||
|
||
# Статистика занятий
|
||
all_lessons = await sync_to_async(list)(
|
||
Lesson.objects.filter(mentor=db_user, client=client)
|
||
)
|
||
total_lessons = len(all_lessons)
|
||
completed_lessons = len([l for l in all_lessons if l.status == 'completed'])
|
||
upcoming_lessons = len([
|
||
l for l in all_lessons
|
||
if l.start_time >= timezone.now() and l.status == 'scheduled'
|
||
])
|
||
|
||
# Занятия за месяц
|
||
month_ago = timezone.now() - timedelta(days=30)
|
||
lessons_this_month = len([
|
||
l for l in all_lessons
|
||
if l.start_time >= month_ago
|
||
])
|
||
|
||
message += f"📚 <b>Занятия:</b>\n"
|
||
message += f"• Всего: {total_lessons}\n"
|
||
message += f"• Завершено: {completed_lessons}\n"
|
||
message += f"• Предстоящих: {upcoming_lessons}\n"
|
||
message += f"• За месяц: {lessons_this_month}\n\n"
|
||
|
||
# Статистика ДЗ
|
||
all_submissions = await sync_to_async(list)(
|
||
HomeworkSubmission.objects.filter(
|
||
homework__mentor=db_user,
|
||
student=client.user
|
||
)
|
||
)
|
||
total_homeworks = len(all_submissions)
|
||
pending_homeworks = len([s for s in all_submissions if s.status == 'pending'])
|
||
graded_homeworks = len([s for s in all_submissions if s.status == 'graded'])
|
||
|
||
message += f"📝 <b>Домашние задания:</b>\n"
|
||
message += f"• Всего решений: {total_homeworks}\n"
|
||
message += f"• На проверке: {pending_homeworks}\n"
|
||
message += f"• Проверено: {graded_homeworks}\n"
|
||
|
||
if graded_homeworks > 0:
|
||
scores = [s.score for s in all_submissions if s.score is not None]
|
||
avg_score = sum(scores) / len(scores) if scores else 0
|
||
message += f"• Средний балл: {avg_score:.1f}\n"
|
||
|
||
# Доходы от клиента
|
||
lessons_with_price = [l for l in all_lessons if l.price and l.status == 'completed']
|
||
if lessons_with_price:
|
||
total_revenue = sum([float(l.price) for l in lessons_with_price])
|
||
message += f"\n💰 <b>Доходы:</b>\n"
|
||
message += f"• Всего: {total_revenue:.2f} ₽\n"
|
||
|
||
inline_keyboard = [[
|
||
InlineKeyboardButton(
|
||
"🌐 Открыть на сайте",
|
||
url=f"{settings.FRONTEND_URL}/students"
|
||
),
|
||
InlineKeyboardButton(
|
||
"◀️ Назад к списку",
|
||
callback_data="clients_back"
|
||
)
|
||
]]
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
except Client.DoesNotExist:
|
||
await query.answer("❌ Клиент не найден", show_alert=True)
|
||
except Exception as e:
|
||
logger.error(f"Error handling client detail: {e}", exc_info=True)
|
||
await query.answer("❌ Ошибка обработки запроса", show_alert=True)
|
||
return
|
||
|
||
# Обработка возврата к списку клиентов
|
||
if query.data == 'clients_back':
|
||
try:
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user or db_user.role != 'mentor':
|
||
await query.answer("❌ Ошибка", show_alert=True)
|
||
return
|
||
|
||
from apps.users.models import Client
|
||
clients = await sync_to_async(list)(
|
||
Client.objects.filter(mentors=db_user).select_related('user')[:10]
|
||
)
|
||
|
||
if not clients:
|
||
await query.edit_message_text("👥 У вас пока нет клиентов.")
|
||
return
|
||
|
||
message = "👥 <b>Ваши клиенты:</b>\n\n"
|
||
inline_keyboard = []
|
||
|
||
for client in clients:
|
||
client_name = client.user.get_full_name() or client.user.email
|
||
message += f"👤 <b>{client_name}</b>\n\n"
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"👤 {client_name[:30]}",
|
||
callback_data=f"client_detail_{client.id}"
|
||
)
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard)
|
||
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error handling clients back: {e}", exc_info=True)
|
||
await query.answer("❌ Ошибка", show_alert=True)
|
||
return
|
||
|
||
# Обработка подтверждения присутствия
|
||
if query.data.startswith('attendance_yes_') or query.data.startswith('attendance_no_'):
|
||
try:
|
||
lesson_id = int(query.data.split('_')[-1])
|
||
response_bool = query.data.startswith('attendance_yes_')
|
||
|
||
from apps.schedule.models import Lesson
|
||
from django.utils import timezone
|
||
|
||
lesson = await sync_to_async(Lesson.objects.select_related('client', 'client__user', 'mentor').get)(id=lesson_id)
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
# Проверяем, что пользователь - студент этого занятия
|
||
if not lesson.client or lesson.client.user.telegram_id != telegram_id:
|
||
await query.edit_message_text(
|
||
"❌ Ошибка: вы не являетесь студентом этого занятия"
|
||
)
|
||
return
|
||
|
||
# Сохраняем ответ
|
||
lesson.attendance_confirmed = response_bool
|
||
lesson.attendance_response_at = timezone.now()
|
||
await sync_to_async(lesson.save)(update_fields=['attendance_confirmed', 'attendance_response_at'])
|
||
|
||
# Отправляем уведомление ментору
|
||
from apps.notifications.services import NotificationService
|
||
await sync_to_async(NotificationService.send_attendance_response_to_mentor)(lesson, response_bool)
|
||
|
||
response_text = "будете присутствовать" if response_bool else "не сможете присутствовать"
|
||
|
||
await query.edit_message_text(
|
||
f"✅ <b>Ответ сохранен</b>\n\n"
|
||
f"Вы подтвердили, что {response_text} на занятии:\n"
|
||
f"<b>{lesson.title}</b>\n\n"
|
||
f"Преподаватель получил уведомление."
|
||
)
|
||
|
||
except Lesson.DoesNotExist:
|
||
await query.edit_message_text("❌ Занятие не найдено")
|
||
except Exception as e:
|
||
logger.error(f"Error processing attendance confirmation: {e}")
|
||
await query.edit_message_text("❌ Ошибка обработки ответа")
|
||
return
|
||
|
||
# Обработка настроек уведомлений
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await query.edit_message_text("❌ Аккаунт не связан.")
|
||
return
|
||
|
||
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
|
||
|
||
# Переключение всех уведомлений
|
||
if query.data == "settings_toggle_all":
|
||
preferences.enabled = not preferences.enabled
|
||
await sync_to_async(preferences.save)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Переключение Telegram уведомлений
|
||
elif query.data == "settings_toggle_telegram":
|
||
preferences.telegram_enabled = not preferences.telegram_enabled
|
||
await sync_to_async(preferences.save)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Переключение Email уведомлений
|
||
elif query.data == "settings_toggle_email":
|
||
preferences.email_enabled = not preferences.email_enabled
|
||
await sync_to_async(preferences.save)()
|
||
await self._refresh_settings_message(query, db_user, preferences)
|
||
return
|
||
|
||
# Режим тишины
|
||
elif query.data == "settings_quiet_hours":
|
||
await self._handle_quiet_hours(query, db_user, preferences)
|
||
return
|
||
|
||
# Типы уведомлений
|
||
elif query.data == "settings_notification_types":
|
||
await self._handle_notification_types(query, db_user, preferences)
|
||
return
|
||
|
||
# Часовой пояс
|
||
elif query.data == "settings_timezone":
|
||
await 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 = "📅 <b>Ближайшие занятия:</b>\n\n"
|
||
|
||
for lesson in lessons:
|
||
import pytz
|
||
from apps.users.utils import get_user_timezone
|
||
user_tz = get_user_timezone(db_user.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
local_time = lesson.start_time.astimezone(user_tz)
|
||
else:
|
||
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
|
||
local_time = utc_time.astimezone(user_tz)
|
||
|
||
time_str = local_time.strftime('%d.%m.%Y в %H:%M')
|
||
|
||
if db_user.role == 'mentor':
|
||
student_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Студент'
|
||
message += f"📚 <b>{lesson.title}</b>\n"
|
||
message += f"👤 {student_name}\n"
|
||
elif db_user.role == 'parent':
|
||
child_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Ребёнок'
|
||
message += f"📚 <b>{lesson.title}</b>\n"
|
||
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
|
||
message += f"👶 {child_name}\n"
|
||
else:
|
||
message += f"📚 <b>{lesson.title}</b>\n"
|
||
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
|
||
|
||
message += f"🕐 {time_str}\n\n"
|
||
|
||
await update.message.reply_text(message, parse_mode='HTML')
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in schedule_command: {e}", exc_info=True)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения расписания.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def nextlesson_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /nextlesson - показать следующее занятие."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
from apps.schedule.models import Lesson
|
||
from django.utils import timezone
|
||
|
||
now = timezone.now()
|
||
|
||
# Находим ближайшее занятие
|
||
if db_user.role == 'mentor':
|
||
lesson = await sync_to_async(
|
||
Lesson.objects.filter(
|
||
mentor=db_user,
|
||
start_time__gte=now
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time').first
|
||
)()
|
||
elif db_user.role == 'client':
|
||
# Проверяем наличие Client профиля
|
||
try:
|
||
from apps.users.models import Client
|
||
client_profile = await sync_to_async(
|
||
Client.objects.filter(user=db_user).first
|
||
)()
|
||
|
||
if not client_profile:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Профиль клиента не найден.\n\n"
|
||
"Обратитесь к администратору для настройки профиля.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
except Exception as e:
|
||
logger.error(f"Error getting client profile: {e}")
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения профиля клиента.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
lesson = await sync_to_async(
|
||
Lesson.objects.filter(
|
||
client=client_profile,
|
||
start_time__gte=now
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time').first
|
||
)()
|
||
elif db_user.role == 'parent':
|
||
from apps.users.models import Parent
|
||
try:
|
||
parent_profile = await sync_to_async(lambda: db_user.parent_profile)()
|
||
children = await sync_to_async(list)(parent_profile.children.all())
|
||
|
||
if not children:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ У вас нет привязанных детей.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
child_clients = list(children) # children уже список Client объектов
|
||
lesson = await sync_to_async(
|
||
Lesson.objects.filter(
|
||
client__in=child_clients,
|
||
start_time__gte=now
|
||
).select_related('mentor', 'client', 'client__user').order_by('start_time').first
|
||
)()
|
||
except:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения данных родителя.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
else:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Эта команда доступна только для менторов, клиентов и родителей.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
if not lesson:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"📅 У вас нет предстоящих занятий.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
import pytz
|
||
from apps.users.utils import get_user_timezone
|
||
user_tz = get_user_timezone(db_user.timezone or 'UTC')
|
||
if timezone.is_aware(lesson.start_time):
|
||
local_time = lesson.start_time.astimezone(user_tz)
|
||
else:
|
||
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
|
||
local_time = utc_time.astimezone(user_tz)
|
||
|
||
time_str = local_time.strftime('%d.%m.%Y в %H:%M')
|
||
|
||
message = "📚 <b>Следующее занятие:</b>\n\n"
|
||
message += f"<b>{lesson.title}</b>\n"
|
||
|
||
if db_user.role == 'mentor':
|
||
student_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Студент'
|
||
message += f"👤 {student_name}\n"
|
||
elif db_user.role == 'parent':
|
||
child_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Ребёнок'
|
||
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
|
||
message += f"👶 {child_name}\n"
|
||
else:
|
||
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
|
||
|
||
message += f"🕐 {time_str}\n"
|
||
|
||
if lesson.description:
|
||
message += f"\n📝 {lesson.description[:200]}"
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in nextlesson_command: {e}", exc_info=True)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения следующего занятия.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def 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"📝 <b>{hw.title}</b>\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✅ <b>Статус: Сдано</b>\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⏳ <b>Статус: Не сдано</b>\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 = "📝 <b>Активные домашние задания:</b>\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"<b>{hw.title}</b>\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 = "📝 <b>Домашние задания детей:</b>\n\n"
|
||
|
||
for hw in homeworks:
|
||
deadline_str = hw.deadline.strftime('%d.%m.%Y') if hw.deadline else 'Без дедлайна'
|
||
|
||
# Находим для какого ребёнка это задание
|
||
child_students = [student for student in hw.assigned_to.all() if student in child_users]
|
||
child_names = ', '.join([child.get_full_name() for child in child_students[:2]])
|
||
if len(child_students) > 2:
|
||
child_names += f" и ещё {len(child_students) - 2}"
|
||
|
||
message += f"<b>{hw.title}</b>\n"
|
||
message += f"👶 {child_names}\n"
|
||
message += f"📅 Дедлайн: {deadline_str}\n\n"
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
|
||
except Exception as e:
|
||
logger.error(f"Error in homework_command for parent: {e}")
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения домашних заданий.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
# Для ментора - показать задания требующие проверки
|
||
elif db_user.role == 'mentor':
|
||
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 = "📝 <b>Домашние задания на проверку:</b>\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"<b>{hw_title}</b>\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"✅ <b>Решение обновлено!</b>\n\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
# Определяем номер попытки
|
||
attempt_number = 1
|
||
if existing_submission:
|
||
attempt_number = existing_submission.attempt_number + 1
|
||
|
||
# Создаем новое решение
|
||
submission = HomeworkSubmission(
|
||
homework=homework,
|
||
student=db_user,
|
||
content=text_content,
|
||
status='pending',
|
||
attempt_number=attempt_number
|
||
)
|
||
await sync_to_async(submission.save)()
|
||
|
||
# Проверяем опоздание
|
||
await sync_to_async(submission.check_if_late)()
|
||
|
||
# Обновляем статистику задания
|
||
await sync_to_async(homework.update_statistics)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение загружено!</b>\n\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
# Отправляем уведомление ментору
|
||
from apps.notifications.services import NotificationService
|
||
await sync_to_async(NotificationService.create_notification_with_telegram)(
|
||
recipient=homework.mentor,
|
||
notification_type='homework_submitted',
|
||
title='📝 ДЗ сдано',
|
||
message=f'{db_user.get_full_name()} сдал ДЗ "{homework.title}"',
|
||
priority='normal',
|
||
action_url=f'/homework/{homework.id}/',
|
||
content_object=homework
|
||
)
|
||
|
||
# Очищаем состояние ожидания
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
keyboard = await get_user_keyboard(update.effective_user.id)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки для навигации:",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Homework.DoesNotExist:
|
||
await update.message.reply_text(
|
||
"❌ Домашнее задание не найдено."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
except Exception as e:
|
||
logger.error(f"Error handling text homework submission: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка загрузки решения.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
async def _refresh_settings_message(self, query, db_user, preferences):
|
||
"""Обновить сообщение с настройками."""
|
||
message_text = "⚙️ <b>Настройки</b>\n\n"
|
||
|
||
all_status = "✅ Включены" if preferences.enabled else "❌ Выключены"
|
||
telegram_status = "✅ Вкл" if preferences.telegram_enabled else "❌ Выкл"
|
||
email_status = "✅ Вкл" if preferences.email_enabled else "❌ Выкл"
|
||
in_app_status = "✅ Вкл" if preferences.in_app_enabled else "❌ Выкл"
|
||
|
||
message_text += f"🔔 Все уведомления: {all_status}\n"
|
||
message_text += f"📱 Telegram: {telegram_status}\n"
|
||
message_text += f"📧 Email: {email_status}\n"
|
||
message_text += f"💬 В приложении: {in_app_status}\n\n"
|
||
|
||
if preferences.quiet_hours_enabled and preferences.quiet_hours_start and preferences.quiet_hours_end:
|
||
quiet_start = preferences.quiet_hours_start.strftime('%H:%M')
|
||
quiet_end = preferences.quiet_hours_end.strftime('%H:%M')
|
||
message_text += f"🔇 Режим тишины: {quiet_start} - {quiet_end}\n\n"
|
||
else:
|
||
message_text += "🔇 Режим тишины: ❌ Выключен\n\n"
|
||
|
||
user_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 = "🔇 <b>Режим тишины</b>\n\n"
|
||
|
||
if preferences.quiet_hours_enabled and preferences.quiet_hours_start and preferences.quiet_hours_end:
|
||
quiet_start = preferences.quiet_hours_start.strftime('%H:%M')
|
||
quiet_end = preferences.quiet_hours_end.strftime('%H:%M')
|
||
message_text += f"Текущий: {quiet_start} - {quiet_end}\n\n"
|
||
else:
|
||
message_text += "Текущий: ❌ Выключен\n\n"
|
||
|
||
message_text += "Выберите действие:"
|
||
|
||
keyboard = []
|
||
|
||
# Предустановленные варианты
|
||
if not preferences.quiet_hours_enabled:
|
||
keyboard.append([
|
||
InlineKeyboardButton("✅ Включить (22:00 - 08:00)", callback_data="quiet_hours_enable_22_8")
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("✅ Включить (23:00 - 07:00)", callback_data="quiet_hours_enable_23_7")
|
||
])
|
||
else:
|
||
keyboard.append([
|
||
InlineKeyboardButton("❌ Выключить", callback_data="quiet_hours_disable")
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🕐 Изменить время", callback_data="quiet_hours_custom")
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
async def _handle_notification_types(self, query, db_user, preferences):
|
||
"""Обработка настроек типов уведомлений."""
|
||
from .models import Notification
|
||
|
||
# Показываем меню выбора типов уведомлений
|
||
message_text = "📋 <b>Типы уведомлений</b>\n\n"
|
||
message_text += "Выберите типы уведомлений для Telegram:\n\n"
|
||
|
||
# Группируем типы по категориям
|
||
lesson_types = [t for t in Notification.TYPE_CHOICES if 'lesson' in t[0]]
|
||
homework_types = [t for t in Notification.TYPE_CHOICES if 'homework' in t[0]]
|
||
other_types = [t for t in Notification.TYPE_CHOICES if 'lesson' not in t[0] and 'homework' not in t[0]]
|
||
|
||
keyboard = []
|
||
|
||
# Кнопки для занятий
|
||
if lesson_types:
|
||
keyboard.append([InlineKeyboardButton("📅 Занятия", callback_data="noop")])
|
||
for ntype, display in lesson_types:
|
||
type_prefs = preferences.type_preferences.get(ntype, {})
|
||
if not isinstance(type_prefs, dict):
|
||
type_prefs = {}
|
||
enabled = type_prefs.get('telegram', True)
|
||
status = "✅" if enabled else "❌"
|
||
# Сокращаем длинные названия
|
||
short_display = display.replace('Занятие ', '').replace(' занятие', '')
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"{status} {short_display}",
|
||
callback_data=f"toggle_type_{ntype}"
|
||
)
|
||
])
|
||
|
||
# Кнопки для домашних заданий
|
||
if homework_types:
|
||
keyboard.append([InlineKeyboardButton("📝 Домашние задания", callback_data="noop")])
|
||
for ntype, display in homework_types:
|
||
type_prefs = preferences.type_preferences.get(ntype, {})
|
||
if not isinstance(type_prefs, dict):
|
||
type_prefs = {}
|
||
enabled = type_prefs.get('telegram', True)
|
||
status = "✅" if enabled else "❌"
|
||
short_display = display.replace('Домашнее задание', 'ДЗ')
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"{status} {short_display}",
|
||
callback_data=f"toggle_type_{ntype}"
|
||
)
|
||
])
|
||
|
||
# Кнопки для других типов (первые 3)
|
||
if other_types:
|
||
keyboard.append([InlineKeyboardButton("📢 Другие", callback_data="noop")])
|
||
for ntype, display in other_types[:3]:
|
||
type_prefs = preferences.type_preferences.get(ntype, {})
|
||
if not isinstance(type_prefs, dict):
|
||
type_prefs = {}
|
||
enabled = type_prefs.get('telegram', True)
|
||
status = "✅" if enabled else "❌"
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"{status} {display}",
|
||
callback_data=f"toggle_type_{ntype}"
|
||
)
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
async def _handle_timezone(self, query, db_user):
|
||
"""Обработка настройки часового пояса."""
|
||
# Популярные часовые пояса
|
||
popular_timezones = [
|
||
('Europe/Moscow', 'Москва (UTC+3)'),
|
||
('Europe/Kiev', 'Киев (UTC+2)'),
|
||
('Asia/Almaty', 'Алматы (UTC+6)'),
|
||
('Europe/Minsk', 'Минск (UTC+3)'),
|
||
('Asia/Tashkent', 'Ташкент (UTC+5)'),
|
||
('Asia/Yekaterinburg', 'Екатеринбург (UTC+5)'),
|
||
('Asia/Novosibirsk', 'Новосибирск (UTC+7)'),
|
||
('Europe/Kaliningrad', 'Калининград (UTC+2)'),
|
||
]
|
||
|
||
message_text = "🕐 <b>Часовой пояс</b>\n\n"
|
||
message_text += "Текущий: " + (db_user.timezone or 'Europe/Moscow') + "\n\n"
|
||
message_text += "Выберите часовой пояс:"
|
||
|
||
keyboard = []
|
||
for tz, name in popular_timezones:
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
name + (" ✅" if db_user.timezone == tz else ""),
|
||
callback_data=f"set_timezone_{tz}"
|
||
)
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
async def _handle_language(self, query, db_user):
|
||
"""Обработка настройки языка."""
|
||
message_text = "🌐 <b>Язык интерфейса</b>\n\n"
|
||
message_text += "Текущий: " + ('Русский' if db_user.language == 'ru' else 'English') + "\n\n"
|
||
message_text += "Выберите язык:"
|
||
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton(
|
||
"Русский" + (" ✅" if db_user.language == 'ru' else ""),
|
||
callback_data="set_language_ru"
|
||
),
|
||
InlineKeyboardButton(
|
||
"English" + (" ✅" if db_user.language == 'en' else ""),
|
||
callback_data="set_language_en"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
|
||
]
|
||
]
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
async def clients_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /clients - список клиентов ментора."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User, Client
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
if db_user.role != 'mentor':
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Эта команда доступна только для менторов.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
# Получаем клиентов ментора
|
||
clients = await sync_to_async(list)(
|
||
Client.objects.filter(mentors=db_user).select_related('user')[:10]
|
||
)
|
||
|
||
if not clients:
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"👥 У вас пока нет клиентов.\n\n"
|
||
"Клиенты появятся здесь после того, как они запишутся на ваши занятия.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
message = "👥 <b>Ваши клиенты:</b>\n\n"
|
||
|
||
from apps.schedule.models import Lesson
|
||
from apps.homework.models import HomeworkSubmission
|
||
from django.utils import timezone
|
||
|
||
inline_keyboard = []
|
||
for client in clients:
|
||
client_name = client.user.get_full_name() or client.user.email
|
||
message += f"👤 <b>{client_name}</b>\n"
|
||
|
||
# Статистика занятий
|
||
lessons = await sync_to_async(list)(
|
||
Lesson.objects.filter(
|
||
mentor=db_user,
|
||
client=client
|
||
)
|
||
)
|
||
total_lessons = len(lessons)
|
||
completed_lessons = len([l for l in lessons if l.status == 'completed'])
|
||
upcoming_lessons = len([
|
||
l for l in lessons
|
||
if l.start_time >= timezone.now() and l.status == 'scheduled'
|
||
])
|
||
|
||
message += f"📚 Занятий: {total_lessons} (завершено: {completed_lessons}, предстоящих: {upcoming_lessons})\n"
|
||
|
||
# Статистика ДЗ
|
||
submissions = await sync_to_async(list)(
|
||
HomeworkSubmission.objects.filter(
|
||
homework__mentor=db_user,
|
||
student=client.user
|
||
)
|
||
)
|
||
total_homeworks = len(submissions)
|
||
graded_homeworks = len([s for s in submissions if s.status == 'graded'])
|
||
if graded_homeworks > 0:
|
||
scores = [s.score for s in submissions if s.score is not None]
|
||
avg_score = sum(scores) / len(scores) if scores else 0
|
||
message += f"📝 ДЗ: {total_homeworks} (проверено: {graded_homeworks}, средний балл: {avg_score:.1f})\n"
|
||
else:
|
||
message += f"📝 ДЗ: {total_homeworks}\n"
|
||
|
||
message += "\n"
|
||
|
||
# Добавляем кнопку для просмотра деталей клиента
|
||
inline_keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"👤 {client_name[:30]}",
|
||
callback_data=f"client_detail_{client.id}"
|
||
)
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(inline_keyboard) if inline_keyboard else None
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(message, parse_mode='HTML', reply_markup=reply_markup)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in clients_command: {e}", exc_info=True)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения списка клиентов.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /stats - статистика для ментора."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User, Client
|
||
|
||
try:
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
if db_user.role != 'mentor':
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Эта команда доступна только для менторов.",
|
||
reply_markup=keyboard
|
||
)
|
||
return
|
||
|
||
from apps.schedule.models import Lesson
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
from django.utils import timezone
|
||
from datetime import timedelta
|
||
|
||
now = timezone.now()
|
||
month_ago = now - timedelta(days=30)
|
||
|
||
# Общая статистика
|
||
total_clients = await sync_to_async(Client.objects.filter(mentors=db_user).count)()
|
||
|
||
# Занятия
|
||
all_lessons = await sync_to_async(list)(
|
||
Lesson.objects.filter(mentor=db_user)
|
||
)
|
||
total_lessons = len(all_lessons)
|
||
completed_lessons = len([l for l in all_lessons if l.status == 'completed'])
|
||
upcoming_lessons = len([
|
||
l for l in all_lessons
|
||
if l.start_time >= now and l.status == 'scheduled'
|
||
])
|
||
lessons_this_month = len([
|
||
l for l in all_lessons
|
||
if l.start_time >= month_ago
|
||
])
|
||
|
||
# Домашние задания
|
||
all_submissions = await sync_to_async(list)(
|
||
HomeworkSubmission.objects.filter(homework__mentor=db_user)
|
||
)
|
||
total_homeworks = len(all_submissions)
|
||
pending_homeworks = len([s for s in all_submissions if s.status == 'pending'])
|
||
graded_homeworks = len([s for s in all_submissions if s.status == 'graded'])
|
||
|
||
if graded_homeworks > 0:
|
||
scores = [s.score for s in all_submissions if s.score is not None]
|
||
avg_score = sum(scores) / len(scores) if scores else 0
|
||
else:
|
||
avg_score = 0
|
||
|
||
message = "📊 <b>Ваша статистика:</b>\n\n"
|
||
message += f"👥 Клиентов: {total_clients}\n\n"
|
||
message += f"📚 <b>Занятия:</b>\n"
|
||
message += f"• Всего: {total_lessons}\n"
|
||
message += f"• Завершено: {completed_lessons}\n"
|
||
message += f"• Предстоящих: {upcoming_lessons}\n"
|
||
message += f"• За месяц: {lessons_this_month}\n\n"
|
||
message += f"📝 <b>Домашние задания:</b>\n"
|
||
message += f"• Всего решений: {total_homeworks}\n"
|
||
message += f"• На проверке: {pending_homeworks}\n"
|
||
message += f"• Проверено: {graded_homeworks}\n"
|
||
if avg_score > 0:
|
||
message += f"• Средний балл: {avg_score:.1f}\n"
|
||
|
||
# Доходы (если есть занятия с ценой)
|
||
lessons_with_price = [l for l in all_lessons if l.price and l.status == 'completed']
|
||
if lessons_with_price:
|
||
total_revenue = sum([float(l.price) for l in lessons_with_price])
|
||
avg_price = total_revenue / len(lessons_with_price) if lessons_with_price else 0
|
||
revenue_this_month = sum([
|
||
float(l.price) for l in lessons_with_price
|
||
if l.start_time >= month_ago
|
||
])
|
||
|
||
message += f"\n💰 <b>Доходы:</b>\n"
|
||
message += f"• Всего: {total_revenue:.2f} ₽\n"
|
||
message += f"• Средняя цена: {avg_price:.2f} ₽\n"
|
||
message += f"• За месяц: {revenue_this_month:.2f} ₽\n"
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in stats_command: {e}", exc_info=True)
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка получения статистики.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def progress_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик команды /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 = "📊 <b>Ваш прогресс:</b>\n\n"
|
||
message += f"📅 <b>За последний месяц:</b>\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📈 <b>Всего:</b>\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📚 <b>Топ предметы (месяц):</b>\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(
|
||
"🔗 <b>Связывание аккаунта</b>\n\n"
|
||
"1. Войдите на платформу\n"
|
||
"2. Перейдите в Профиль → Настройки → Telegram\n"
|
||
"3. Нажмите \"Связать Telegram\"\n"
|
||
"4. Отправьте команду: /link ВАШ_КОД\n\n"
|
||
"Или используйте команду:\n"
|
||
"/link <ваш_код_связывания>",
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
# Если аккаунт связан, показываем клавиатуру
|
||
if db_user:
|
||
keyboard = get_main_keyboard(db_user.role)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки или команды для работы с ботом.\n\n"
|
||
"Введите /help для списка команд.",
|
||
reply_markup=keyboard
|
||
)
|
||
else:
|
||
keyboard = get_main_keyboard(None)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки или команды для работы с ботом.\n\n"
|
||
"Введите /help для списка команд.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
async def handle_document(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик загрузки документов (для решений ДЗ)."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта."
|
||
)
|
||
return
|
||
|
||
# Проверяем, ожидается ли загрузка файла для ДЗ
|
||
homework_id = context.user_data.get('waiting_for_homework_file')
|
||
if not homework_id:
|
||
await update.message.reply_text(
|
||
"📎 Чтобы загрузить файл для решения ДЗ:\n\n"
|
||
"1. Используйте команду /homework\n"
|
||
"2. Выберите задание\n"
|
||
"3. Нажмите кнопку '📎 Загрузить решение'"
|
||
)
|
||
return
|
||
|
||
# Получаем файл
|
||
document = update.message.document
|
||
if not document:
|
||
await update.message.reply_text("❌ Файл не найден.")
|
||
return
|
||
|
||
try:
|
||
# Скачиваем файл из Telegram
|
||
from telegram import Bot
|
||
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
|
||
file = await bot.get_file(document.file_id)
|
||
|
||
# Получаем домашнее задание для проверок
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
|
||
|
||
# Безопасность: Проверка размера файла (максимум 50MB для Telegram, но проверяем и настройки задания)
|
||
MAX_TELEGRAM_FILE_SIZE = 50 * 1024 * 1024 # 50 MB - максимум для Telegram
|
||
file_size = document.file_size or 0
|
||
|
||
if file_size > MAX_TELEGRAM_FILE_SIZE:
|
||
await update.message.reply_text(
|
||
f"❌ Файл слишком большой. Максимальный размер: {MAX_TELEGRAM_FILE_SIZE / (1024*1024):.0f} MB"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Проверяем размер файла согласно настройкам задания
|
||
if homework.max_file_size > 0 and file_size > homework.max_file_size:
|
||
max_size_mb = homework.max_file_size / (1024 * 1024)
|
||
await update.message.reply_text(
|
||
f"❌ Файл слишком большой. Максимальный размер для этого задания: {max_size_mb:.1f} MB"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Безопасность: Проверка типа файла
|
||
from apps.homework.utils import validate_file_type, sanitize_filename
|
||
safe_filename = sanitize_filename(document.file_name or 'file')
|
||
|
||
if homework.allowed_file_types and not validate_file_type(safe_filename, homework.allowed_file_types):
|
||
allowed = homework.allowed_file_types.replace(',', ', ')
|
||
await update.message.reply_text(
|
||
f"❌ Тип файла не разрешен.\n\n"
|
||
f"Разрешенные типы: {allowed}"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Создаем временный файл
|
||
import tempfile
|
||
import os
|
||
from django.core.files import File
|
||
|
||
file_ext = os.path.splitext(safe_filename)[1]
|
||
tmp_file_path = None
|
||
try:
|
||
with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as tmp_file:
|
||
tmp_file_path = tmp_file.name
|
||
await file.download_to_drive(tmp_file_path)
|
||
|
||
# Проверяем, что пользователь имеет право сдавать это ДЗ
|
||
if db_user.role != 'client' or db_user not in await sync_to_async(list)(homework.assigned_to.all()):
|
||
await update.message.reply_text(
|
||
"❌ У вас нет доступа к этому заданию."
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Проверяем, есть ли уже решение
|
||
existing_submission = await sync_to_async(
|
||
HomeworkSubmission.objects.filter(
|
||
homework=homework,
|
||
student=db_user
|
||
).order_by('-attempt_number').first
|
||
)()
|
||
|
||
if existing_submission and existing_submission.status != 'returned':
|
||
# Обновляем существующее решение
|
||
with open(tmp_file_path, 'rb') as f:
|
||
django_file = File(f, name=safe_filename)
|
||
existing_submission.attachment = django_file
|
||
existing_submission.content = f"Решение загружено через Telegram: {safe_filename}"
|
||
existing_submission.status = 'pending'
|
||
await sync_to_async(existing_submission.save)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение обновлено!</b>\n\n"
|
||
f"📎 Файл: {safe_filename}\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
# Определяем номер попытки
|
||
attempt_number = 1
|
||
if existing_submission:
|
||
attempt_number = existing_submission.attempt_number + 1
|
||
|
||
# Создаем новое решение
|
||
with open(tmp_file_path, 'rb') as f:
|
||
django_file = File(f, name=safe_filename)
|
||
submission = HomeworkSubmission(
|
||
homework=homework,
|
||
student=db_user,
|
||
content=f"Решение загружено через Telegram: {safe_filename}",
|
||
attachment=django_file,
|
||
status='pending',
|
||
attempt_number=attempt_number
|
||
)
|
||
await sync_to_async(submission.save)()
|
||
|
||
# Проверяем опоздание
|
||
await sync_to_async(submission.check_if_late)()
|
||
|
||
# Обновляем статистику задания
|
||
await sync_to_async(homework.update_statistics)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение загружено!</b>\n\n"
|
||
f"📎 Файл: {safe_filename}\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
# Отправляем уведомление ментору
|
||
from apps.notifications.services import NotificationService
|
||
await sync_to_async(NotificationService.create_notification_with_telegram)(
|
||
recipient=homework.mentor,
|
||
notification_type='homework_submitted',
|
||
title='📝 ДЗ сдано',
|
||
message=f'{db_user.get_full_name()} сдал ДЗ "{homework.title}"',
|
||
priority='normal',
|
||
action_url=f'/homework/{homework.id}/',
|
||
content_object=homework
|
||
)
|
||
|
||
# Очищаем состояние ожидания
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки для навигации:",
|
||
reply_markup=keyboard
|
||
)
|
||
finally:
|
||
# Гарантированно удаляем временный файл
|
||
if tmp_file_path and os.path.exists(tmp_file_path):
|
||
try:
|
||
os.unlink(tmp_file_path)
|
||
except Exception as e:
|
||
logger.error(f"Error deleting temp file {tmp_file_path}: {e}")
|
||
|
||
await bot.close()
|
||
|
||
except Homework.DoesNotExist:
|
||
await update.message.reply_text(
|
||
"❌ Домашнее задание не найдено."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
except Exception as e:
|
||
logger.error(f"Error handling document upload: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка загрузки файла.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
async def handle_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработчик загрузки фото (для решений ДЗ)."""
|
||
user = update.effective_user
|
||
telegram_id = user.id
|
||
|
||
from apps.users.models import User
|
||
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
|
||
|
||
if not db_user:
|
||
await update.message.reply_text(
|
||
"❌ Аккаунт не связан.\n\n"
|
||
"Используйте /link <код> для связывания аккаунта."
|
||
)
|
||
return
|
||
|
||
# Проверяем, ожидается ли загрузка файла для ДЗ
|
||
homework_id = context.user_data.get('waiting_for_homework_file')
|
||
if not homework_id:
|
||
await update.message.reply_text(
|
||
"📎 Чтобы загрузить фото для решения ДЗ:\n\n"
|
||
"1. Используйте команду /homework\n"
|
||
"2. Выберите задание\n"
|
||
"3. Нажмите кнопку '📎 Загрузить решение'"
|
||
)
|
||
return
|
||
|
||
# Получаем фото (берем самое большое)
|
||
photos = update.message.photo
|
||
if not photos:
|
||
await update.message.reply_text("❌ Фото не найдено.")
|
||
return
|
||
|
||
photo = photos[-1] # Самое большое фото
|
||
|
||
try:
|
||
# Скачиваем фото из Telegram
|
||
from telegram import Bot
|
||
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
|
||
file = await bot.get_file(photo.file_id)
|
||
|
||
# Получаем домашнее задание для проверок
|
||
from apps.homework.models import Homework, HomeworkSubmission
|
||
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
|
||
|
||
# Безопасность: Проверка размера файла (максимум 50MB для Telegram)
|
||
MAX_TELEGRAM_FILE_SIZE = 50 * 1024 * 1024 # 50 MB - максимум для Telegram
|
||
file_size = photo.file_size or 0
|
||
|
||
if file_size > MAX_TELEGRAM_FILE_SIZE:
|
||
await update.message.reply_text(
|
||
f"❌ Фото слишком большое. Максимальный размер: {MAX_TELEGRAM_FILE_SIZE / (1024*1024):.0f} MB"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Проверяем размер файла согласно настройкам задания
|
||
if homework.max_file_size > 0 and file_size > homework.max_file_size:
|
||
max_size_mb = homework.max_file_size / (1024 * 1024)
|
||
await update.message.reply_text(
|
||
f"❌ Фото слишком большое. Максимальный размер для этого задания: {max_size_mb:.1f} MB"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Безопасность: Проверка типа файла (фото должно быть разрешено)
|
||
from apps.homework.utils import validate_file_type, sanitize_filename
|
||
if homework.allowed_file_types and not validate_file_type('photo.jpg', homework.allowed_file_types):
|
||
allowed = homework.allowed_file_types.replace(',', ', ')
|
||
await update.message.reply_text(
|
||
f"❌ Фото не разрешено для этого задания.\n\n"
|
||
f"Разрешенные типы: {allowed}"
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
# Создаем временный файл
|
||
import tempfile
|
||
import os
|
||
from django.core.files import File
|
||
|
||
tmp_file_path = None
|
||
try:
|
||
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as tmp_file:
|
||
tmp_file_path = tmp_file.name
|
||
await file.download_to_drive(tmp_file_path)
|
||
|
||
# Проверяем, что пользователь имеет право сдавать это ДЗ
|
||
if db_user.role != 'client' or db_user not in await sync_to_async(list)(homework.assigned_to.all()):
|
||
await update.message.reply_text(
|
||
"❌ У вас нет доступа к этому заданию."
|
||
)
|
||
await bot.close()
|
||
return
|
||
|
||
from django.utils import timezone as tz
|
||
from apps.homework.utils import sanitize_filename
|
||
filename = sanitize_filename(f"photo_{homework.id}_{tz.now().strftime('%Y%m%d_%H%M%S')}.jpg")
|
||
|
||
# Проверяем, есть ли уже решение
|
||
existing_submission = await sync_to_async(
|
||
HomeworkSubmission.objects.filter(
|
||
homework=homework,
|
||
student=db_user
|
||
).order_by('-attempt_number').first
|
||
)()
|
||
|
||
if existing_submission and existing_submission.status != 'returned':
|
||
# Обновляем существующее решение
|
||
with open(tmp_file_path, 'rb') as f:
|
||
django_file = File(f, name=filename)
|
||
existing_submission.attachment = django_file
|
||
existing_submission.content = f"Решение загружено через Telegram (фото)"
|
||
existing_submission.status = 'pending'
|
||
await sync_to_async(existing_submission.save)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение обновлено!</b>\n\n"
|
||
f"📎 Фото загружено\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
# Определяем номер попытки
|
||
attempt_number = 1
|
||
if existing_submission:
|
||
attempt_number = existing_submission.attempt_number + 1
|
||
|
||
# Создаем новое решение
|
||
with open(tmp_file_path, 'rb') as f:
|
||
django_file = File(f, name=filename)
|
||
submission = HomeworkSubmission(
|
||
homework=homework,
|
||
student=db_user,
|
||
content="Решение загружено через Telegram (фото)",
|
||
attachment=django_file,
|
||
status='pending',
|
||
attempt_number=attempt_number
|
||
)
|
||
await sync_to_async(submission.save)()
|
||
|
||
# Проверяем опоздание
|
||
await sync_to_async(submission.check_if_late)()
|
||
|
||
# Обновляем статистику задания
|
||
await sync_to_async(homework.update_statistics)()
|
||
|
||
await update.message.reply_text(
|
||
f"✅ <b>Решение загружено!</b>\n\n"
|
||
f"📎 Фото загружено\n"
|
||
f"📝 Задание: {homework.title}\n\n"
|
||
f"Решение отправлено на проверку.",
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
# Отправляем уведомление ментору
|
||
from apps.notifications.services import NotificationService
|
||
await sync_to_async(NotificationService.create_notification_with_telegram)(
|
||
recipient=homework.mentor,
|
||
notification_type='homework_submitted',
|
||
title='📝 ДЗ сдано',
|
||
message=f'{db_user.get_full_name()} сдал ДЗ "{homework.title}"',
|
||
priority='normal',
|
||
action_url=f'/homework/{homework.id}/',
|
||
content_object=homework
|
||
)
|
||
|
||
# Очищаем состояние ожидания
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
keyboard = await get_user_keyboard(telegram_id)
|
||
await update.message.reply_text(
|
||
"Используйте кнопки для навигации:",
|
||
reply_markup=keyboard
|
||
)
|
||
finally:
|
||
# Гарантированно удаляем временный файл
|
||
if tmp_file_path and os.path.exists(tmp_file_path):
|
||
try:
|
||
os.unlink(tmp_file_path)
|
||
except Exception as e:
|
||
logger.error(f"Error deleting temp file {tmp_file_path}: {e}")
|
||
|
||
await bot.close()
|
||
|
||
except Homework.DoesNotExist:
|
||
await update.message.reply_text(
|
||
"❌ Домашнее задание не найдено."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
except Exception as e:
|
||
logger.error(f"Error handling photo upload: {e}", exc_info=True)
|
||
await update.message.reply_text(
|
||
"❌ Ошибка загрузки фото.\n\n"
|
||
"Попробуйте позже или обратитесь в поддержку."
|
||
)
|
||
context.user_data.pop('waiting_for_homework_file', None)
|
||
|
||
def setup_handlers(self):
|
||
"""Настройка обработчиков команд."""
|
||
if not self.application:
|
||
return
|
||
|
||
# Команды
|
||
self.application.add_handler(CommandHandler("start", self.start_command))
|
||
self.application.add_handler(CommandHandler("help", self.help_command))
|
||
self.application.add_handler(CommandHandler("link", self.link_command))
|
||
self.application.add_handler(CommandHandler("unlink", self.unlink_command))
|
||
self.application.add_handler(CommandHandler("status", self.status_command))
|
||
self.application.add_handler(CommandHandler("settings", self.settings_command))
|
||
|
||
# Команды для клиентов и менторов
|
||
self.application.add_handler(CommandHandler("schedule", self.schedule_command))
|
||
self.application.add_handler(CommandHandler("nextlesson", self.nextlesson_command))
|
||
self.application.add_handler(CommandHandler("homework", self.homework_command))
|
||
|
||
# Команды для клиентов
|
||
self.application.add_handler(CommandHandler("progress", self.progress_command))
|
||
|
||
# Команды для менторов
|
||
self.application.add_handler(CommandHandler("clients", self.clients_command))
|
||
self.application.add_handler(CommandHandler("stats", self.stats_command))
|
||
|
||
# Кнопки
|
||
self.application.add_handler(CallbackQueryHandler(self.button_callback))
|
||
|
||
# Обычные сообщения
|
||
self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message))
|
||
|
||
# Загрузка документов (для решений ДЗ)
|
||
self.application.add_handler(MessageHandler(filters.Document.ALL, self.handle_document))
|
||
|
||
# Загрузка фото (для решений ДЗ)
|
||
self.application.add_handler(MessageHandler(filters.PHOTO, self.handle_photo))
|
||
|
||
async def start(self):
|
||
"""Запуск бота."""
|
||
if not self.token:
|
||
logger.error("TELEGRAM_BOT_TOKEN not set")
|
||
return
|
||
|
||
logger.info("Starting Telegram bot...")
|
||
|
||
# Создаем приложение
|
||
self.application = Application.builder().token(self.token).build()
|
||
|
||
# Настраиваем обработчики
|
||
self.setup_handlers()
|
||
|
||
# Запускаем бота
|
||
await self.application.initialize()
|
||
await self.application.start()
|
||
|
||
if self.use_webhook and self.webhook_url:
|
||
# Используем webhook режим
|
||
logger.info(f"Setting up webhook: {self.webhook_url}")
|
||
await self.setup_webhook()
|
||
else:
|
||
# Используем polling режим
|
||
logger.info("Starting bot in polling mode...")
|
||
await self.application.updater.start_polling()
|
||
|
||
logger.info("Telegram bot started successfully")
|
||
|
||
async def setup_webhook(self):
|
||
"""Настройка webhook для бота."""
|
||
from telegram import Bot
|
||
from telegram.error import TelegramError
|
||
|
||
try:
|
||
bot = Bot(token=self.token)
|
||
|
||
# Устанавливаем webhook
|
||
webhook_kwargs = {
|
||
'url': self.webhook_url,
|
||
'allowed_updates': ['message', 'callback_query', 'inline_query', 'chosen_inline_result'],
|
||
}
|
||
|
||
# Добавляем secret token если указан
|
||
if self.webhook_secret_token:
|
||
webhook_kwargs['secret_token'] = self.webhook_secret_token
|
||
|
||
await bot.set_webhook(**webhook_kwargs)
|
||
|
||
# Проверяем информацию о webhook
|
||
webhook_info = await bot.get_webhook_info()
|
||
logger.info(f"Webhook info: {webhook_info.url}, pending updates: {webhook_info.pending_update_count}")
|
||
|
||
await bot.close()
|
||
logger.info("Webhook set successfully")
|
||
except TelegramError as e:
|
||
logger.error(f"Error setting webhook: {e}")
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Unexpected error setting webhook: {e}")
|
||
raise
|
||
|
||
async def remove_webhook(self):
|
||
"""Удаление webhook."""
|
||
from telegram import Bot
|
||
from telegram.error import TelegramError
|
||
|
||
try:
|
||
bot = Bot(token=self.token)
|
||
await bot.delete_webhook(drop_pending_updates=True)
|
||
await bot.close()
|
||
logger.info("Webhook removed successfully")
|
||
except TelegramError as e:
|
||
logger.error(f"Error removing webhook: {e}")
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Unexpected error removing webhook: {e}")
|
||
raise
|
||
|
||
async def stop(self):
|
||
"""Остановка бота."""
|
||
if self.application:
|
||
logger.info("Stopping Telegram bot...")
|
||
if not self.use_webhook:
|
||
# Останавливаем polling только если не используем webhook
|
||
await self.application.updater.stop()
|
||
await self.application.stop()
|
||
await self.application.shutdown()
|
||
logger.info("Telegram bot stopped")
|
||
|
||
def get_application(self):
|
||
"""
|
||
Получить экземпляр Application для обработки webhook.
|
||
|
||
ВАЖНО: Этот метод создает приложение, но не инициализирует его.
|
||
Инициализация происходит в process_webhook_update при первом запросе.
|
||
"""
|
||
if not self.application:
|
||
if not self.token:
|
||
logger.error("TELEGRAM_BOT_TOKEN not set")
|
||
return None
|
||
|
||
# Создаем приложение
|
||
self.application = Application.builder().token(self.token).build()
|
||
|
||
# Настраиваем обработчики
|
||
self.setup_handlers()
|
||
|
||
logger.info("Bot application created for webhook")
|
||
|
||
return self.application
|
||
|
||
async def process_webhook_update(self, update: Update):
|
||
"""
|
||
Обработать update от webhook.
|
||
|
||
Args:
|
||
update: Update объект от Telegram
|
||
"""
|
||
# Получаем или создаем приложение
|
||
if not self.application:
|
||
self.get_application()
|
||
|
||
if not self.application:
|
||
logger.error("Failed to initialize bot application")
|
||
return False
|
||
|
||
try:
|
||
# Убеждаемся что приложение инициализировано и запущено
|
||
# Для webhook режима мы не используем updater, только application
|
||
if not hasattr(self.application, '_webhook_initialized'):
|
||
await self.application.initialize()
|
||
await self.application.start()
|
||
self.application._webhook_initialized = True
|
||
logger.info("Bot application initialized for webhook")
|
||
|
||
# Обрабатываем update
|
||
await self.application.process_update(update)
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Error processing webhook update: {e}", exc_info=True)
|
||
return False
|
||
|
||
|
||
# Глобальный экземпляр бота
|
||
bot_instance = None
|
||
|
||
|
||
async def get_bot():
|
||
"""Получить экземпляр бота."""
|
||
global bot_instance
|
||
|
||
if bot_instance is None:
|
||
bot_instance = TelegramBot()
|
||
await bot_instance.start()
|
||
|
||
return bot_instance
|
||
|
||
|
||
async def send_telegram_message(telegram_id: int, message: str, parse_mode: str = 'HTML'):
|
||
"""
|
||
Отправить сообщение в Telegram.
|
||
|
||
Args:
|
||
telegram_id: ID пользователя в Telegram
|
||
message: Текст сообщения
|
||
parse_mode: Режим парсинга (HTML, Markdown)
|
||
"""
|
||
from telegram import Bot
|
||
from telegram.error import TelegramError
|
||
|
||
if not settings.TELEGRAM_BOT_TOKEN:
|
||
logger.error("TELEGRAM_BOT_TOKEN not set")
|
||
return False
|
||
|
||
try:
|
||
# Создаем временный экземпляр бота для отправки сообщения
|
||
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
|
||
await bot.send_message(
|
||
chat_id=telegram_id,
|
||
text=message,
|
||
parse_mode=parse_mode
|
||
)
|
||
await bot.close()
|
||
logger.info(f"Telegram message sent to {telegram_id}")
|
||
return True
|
||
except TelegramError as e:
|
||
logger.error(f"Telegram API error sending message to {telegram_id}: {e}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"Error sending Telegram message to {telegram_id}: {e}")
|
||
return False
|
||
|
||
|
||
async def send_telegram_message_with_buttons(telegram_id: int, message: str, reply_markup, parse_mode: str = 'HTML'):
|
||
"""
|
||
Отправить сообщение в Telegram с кнопками.
|
||
|
||
Args:
|
||
telegram_id: ID пользователя в Telegram
|
||
message: Текст сообщения
|
||
reply_markup: InlineKeyboardMarkup с кнопками
|
||
parse_mode: Режим парсинга (HTML, Markdown)
|
||
"""
|
||
from telegram import Bot
|
||
from telegram.error import TelegramError
|
||
|
||
if not settings.TELEGRAM_BOT_TOKEN:
|
||
logger.error("TELEGRAM_BOT_TOKEN not set")
|
||
return False
|
||
|
||
try:
|
||
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
|
||
await bot.send_message(
|
||
chat_id=telegram_id,
|
||
text=message,
|
||
parse_mode=parse_mode,
|
||
reply_markup=reply_markup
|
||
)
|
||
await bot.close()
|
||
logger.info(f"Telegram message with buttons sent to {telegram_id}")
|
||
return True
|
||
except TelegramError as e:
|
||
logger.error(f"Telegram API error sending message with buttons to {telegram_id}: {e}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"Error sending Telegram message with buttons to {telegram_id}: {e}")
|
||
return False
|
||
|