From e1c2ddbdb9c241a9bdbcf311552b909217f494b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=A6=D0=BE=D0=B9?= Date: Sat, 7 Dec 2024 09:09:07 +0900 Subject: [PATCH] Bot functionsa mainly operational --- bot/handlers.py | 448 +++++++++++++++++- bot/management/commands/run_bot.py | 93 +++- hotels/admin.py | 49 +- hotels/migrations/0004_datalog.py | 26 + ...figuration_apirequestlog_delete_datalog.py | 47 ++ ...iguration_remove_hotel_api_key_and_more.py | 45 ++ hotels/migrations/0007_pmsintegrationlog.py | 28 ++ hotels/migrations/0008_hotel_pms.py | 19 + hotels/migrations/0009_alter_hotel_pms.py | 19 + ..._hotels_apir_api_id_686bb0_idx_and_more.py | 33 ++ hotels/models.py | 95 +++- hotels/pms_check.py | 92 ++++ hotels/pms_parse.py | 53 +++ touchh/settings.py | 7 - users/admin.py | 9 +- users/migrations/0005_notificationsettings.py | 29 ++ users/models.py | 16 +- 17 files changed, 1048 insertions(+), 60 deletions(-) create mode 100644 hotels/migrations/0004_datalog.py create mode 100644 hotels/migrations/0005_apiconfiguration_apirequestlog_delete_datalog.py create mode 100644 hotels/migrations/0006_pmsconfiguration_remove_hotel_api_key_and_more.py create mode 100644 hotels/migrations/0007_pmsintegrationlog.py create mode 100644 hotels/migrations/0008_hotel_pms.py create mode 100644 hotels/migrations/0009_alter_hotel_pms.py create mode 100644 hotels/migrations/0010_apirequestlog_hotels_apir_api_id_686bb0_idx_and_more.py create mode 100644 hotels/pms_check.py create mode 100644 hotels/pms_parse.py create mode 100644 users/migrations/0005_notificationsettings.py diff --git a/bot/handlers.py b/bot/handlers.py index 02de6963..76d83830 100644 --- a/bot/handlers.py +++ b/bot/handlers.py @@ -1,29 +1,435 @@ -from telegram import Update +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ContextTypes from users.models import User -from hotels.models import Hotel -from asgiref.sync import sync_to_async # Импортируем sync_to_async +from hotels.models import Hotel, UserHotel +from users.models import NotificationSettings +from asgiref.sync import sync_to_async +import smtplib +from hotels.models import PMSIntegrationLog +import requests +from email.mime.text import MIMEText +from django.core.mail import send_mail +from datetime import datetime +# --- Вспомогательные функции --- +async def get_user_from_chat_id(chat_id): + """Получение пользователя из базы по chat_id.""" + return await sync_to_async(User.objects.filter(chat_id=chat_id).first)() + + +async def get_hotel_by_id(hotel_id): + """Получение отеля по ID.""" + return await sync_to_async(Hotel.objects.filter(id=hotel_id).first)() + + +# --- Обработчики команд --- async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработчик команды /start""" - await update.message.reply_text("Привет! Я бот, работающий с Django. Используй /users или /hotels для проверки базы данных.") + """Обработчик команды /start с проверкой chat_id""" + user_id = update.message.from_user.id + print(f"Пользователь {user_id} вызвал команду /start") -async def list_users(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработчик команды /users""" - # Выполняем запрос к базе данных через sync_to_async - users = await sync_to_async(list)(User.objects.all()) # Преобразуем QuerySet в список - if users: - user_list = "\n".join([f"{user.id}: {user.username}" for user in users]) - await update.message.reply_text(f"Список пользователей:\n{user_list}") + user = await get_user_from_chat_id(user_id) + if user: + keyboard = [ + [InlineKeyboardButton("📊 Статистика", callback_data="stats")], + [InlineKeyboardButton("🏨 Управление отелями", callback_data="manage_hotels")], + [InlineKeyboardButton("👤 Пользователи", callback_data="manage_users")], + [InlineKeyboardButton("⚙️ Настройки", callback_data="settings")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text("Выберите действие:", reply_markup=reply_markup) else: - await update.message.reply_text("В базе данных нет пользователей.") + print(f"Пользователь {user_id} не зарегистрирован.") + await update.message.reply_text("Вы не зарегистрированы в системе. Обратитесь к администратору.") + + +async def manage_hotels(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Вывод списка отелей, связанных с пользователем""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + print(f"Пользователь {user_id} выбрал 'Управление отелями'") + + user = await get_user_from_chat_id(user_id) + if not user: + print(f"Пользователь {user_id} не зарегистрирован.") + await query.edit_message_text("Вы не зарегистрированы в системе.") + return + + user_hotels = await sync_to_async(list)( + UserHotel.objects.filter(user=user).select_related("hotel") + ) + + if not user_hotels: + print(f"У пользователя {user_id} нет связанных отелей.") + await query.edit_message_text("У вас нет связанных отелей.") + return + + keyboard = [ + [InlineKeyboardButton(f"🏨 {hotel.hotel.name}", callback_data=f"hotel_{hotel.hotel.id}")] + for hotel in user_hotels + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Выберите отель:", reply_markup=reply_markup) + + +# --- Обработчики кнопок --- +async def handle_button_click(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик всех нажатий кнопок""" + query = update.callback_query + print(f"Обработчик кнопок: Получен callback_data = {query.data}") + + if query.data == "manage_hotels": + await manage_hotels(update, context) + elif query.data.startswith("hotel_"): + await hotel_actions(update, context) + elif query.data.startswith("delete_hotel_"): + await delete_hotel(update, context) + elif query.data.startswith("check_pms_"): + await check_pms(update, context) + elif query.data.startswith("setup_rooms_"): + await setup_rooms(update, context) + elif query.data == "settings": + await settings_menu(update, context) + elif query.data == "toggle_telegram": + await toggle_telegram(update, context) + elif query.data == "toggle_email": + await toggle_email(update, context) + elif query.data == "set_notification_time": + await set_notification_time(update, context) + elif query.data == "current_settings": + await show_current_settings(update, context) -async def list_hotels(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработчик команды /hotels""" - # Выполняем запрос к базе данных через sync_to_async - hotels = await sync_to_async(list)(Hotel.objects.all()) # Преобразуем QuerySet в список - if hotels: - hotel_list = "\n".join([f"{hotel.id}: {hotel.name}" for hotel in hotels]) - await update.message.reply_text(f"Список отелей:\n{hotel_list}") else: - await update.message.reply_text("В базе данных нет отелей.") + print(f"Неизвестный callback_data: {query.data}") + await query.edit_message_text("Команда не распознана.") + + +async def hotel_actions(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик действий для выбранного отеля""" + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[1]) + print(f"Пользователь {query.from_user.id} выбрал отель с ID {hotel_id}") + + hotel = await get_hotel_by_id(hotel_id) + if not hotel: + print(f"Отель с ID {hotel_id} не найден.") + await query.edit_message_text("Отель не найден.") + return + + keyboard = [ + [InlineKeyboardButton("🗑️ Удалить отель", callback_data=f"delete_hotel_{hotel_id}")], + [InlineKeyboardButton("🔗 Проверить интеграцию с PMS", callback_data=f"check_pms_{hotel_id}")], + [InlineKeyboardButton("🛏️ Настроить номера", callback_data=f"setup_rooms_{hotel_id}")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(f"Управление отелем: {hotel.name}", reply_markup=reply_markup) + + +async def delete_hotel(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Удаление отеля""" + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[2]) + print(f"Пользователь {query.from_user.id} выбрал удаление отеля с ID {hotel_id}") + + hotel = await get_hotel_by_id(hotel_id) + if hotel: + hotel_name = hotel.name + await sync_to_async(hotel.delete)() + print(f"Отель {hotel_name} удалён.") + await query.edit_message_text(f"Отель {hotel_name} успешно удалён.") + else: + print(f"Отель с ID {hotel_id} не найден.") + await query.edit_message_text("Отель не найден.") + + +async def check_pms(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Проверить интеграцию с PMS""" + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[2]) + print(f"Пользователь {query.from_user.id} проверяет интеграцию PMS для отеля с ID {hotel_id}") + + # Асинхронно получаем отель + hotel = await sync_to_async(Hotel.objects.select_related('api', 'pms').get)(id=hotel_id) + if not hotel: + print(f"Отель с ID {hotel_id} не найден.") + await query.edit_message_text("Отель не найден.") + return + + # Асинхронно извлекаем связанные данные + api_name = hotel.api.name if hotel.api else "Не настроен" + pms_name = hotel.pms.name if hotel.pms else "Не указана" + + # Формируем сообщение + status_message = ( + f"Отель: {hotel.name}\n" + f"PMS система: {pms_name}\n" + f"API: {api_name}" + ) + + await query.edit_message_text(status_message) +async def setup_rooms(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Настроить номера отеля""" + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[2]) + print(f"Пользователь {query.from_user.id} настраивает номера для отеля с ID {hotel_id}") + + hotel = await get_hotel_by_id(hotel_id) + if not hotel: + print(f"Отель с ID {hotel_id} не найден.") + await query.edit_message_text("Отель не найден.") + return + + await query.edit_message_text(f"Настройка номеров для отеля: {hotel.name}") + + +async def settings_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Меню настроек уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await get_user_from_chat_id(user_id) + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + + telegram_status = "✅" if settings.telegram_enabled else "❌" + email_status = "✅" if settings.email_enabled else "❌" + + keyboard = [ + [InlineKeyboardButton(f"{telegram_status} Уведомления в Telegram", callback_data="toggle_telegram")], + [InlineKeyboardButton(f"{email_status} Уведомления по Email", callback_data="toggle_email")], + [InlineKeyboardButton("🕒 Настроить время уведомлений", callback_data="set_notification_time")], + [InlineKeyboardButton("📋 Показать текущие настройки", callback_data="current_settings")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Настройки уведомлений:", reply_markup=reply_markup) + +async def toggle_telegram(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Переключение состояния Telegram-уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await get_user_from_chat_id(user_id) + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + settings.telegram_enabled = not settings.telegram_enabled + await sync_to_async(settings.save)() + + print(f"Пользователь {user_id} переключил Telegram-уведомления: {settings.telegram_enabled}") + await settings_menu(update, context) + + +async def toggle_email(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Переключение состояния Email-уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await get_user_from_chat_id(user_id) + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + settings.email_enabled = not settings.email_enabled + await sync_to_async(settings.save)() + + print(f"Пользователь {user_id} переключил Email-уведомления: {settings.email_enabled}") + await settings_menu(update, context) + + +async def set_notification_time(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Настройка времени уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await get_user_from_chat_id(user_id) + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + await query.edit_message_text("Введите новое время для уведомлений в формате HH:MM (например, 08:30):") + context.user_data["set_time"] = True + print(f"Пользователь {user_id} настраивает время уведомлений.") + + +async def handle_notification_time(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка ввода времени уведомлений.""" + if context.user_data.get("set_time"): + user_id = update.message.from_user.id + new_time = update.message.text + + try: + hour, minute = map(int, new_time.split(":")) + user = await get_user_from_chat_id(user_id) + if user: + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + settings.notification_time = f"{hour:02}:{minute:02}" + await sync_to_async(settings.save)() + print(f"Пользователь {user_id} установил новое время уведомлений: {new_time}") + await update.message.reply_text(f"Время уведомлений обновлено на {new_time}.") + except ValueError: + print(f"Пользователь {user_id} ввёл некорректное время: {new_time}") + await update.message.reply_text("Неверный формат. Введите время в формате HH:MM.") + finally: + context.user_data["set_time"] = False + + +async def settings_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Меню настроек уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await get_user_from_chat_id(user_id) + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + + telegram_status = "✅" if settings.telegram_enabled else "❌" + email_status = "✅" if settings.email_enabled else "❌" + + keyboard = [ + [InlineKeyboardButton(f"{telegram_status} Уведомления в Telegram", callback_data="toggle_telegram")], + [InlineKeyboardButton(f"{email_status} Уведомления по Email", callback_data="toggle_email")], + [InlineKeyboardButton("🕒 Настроить время уведомлений", callback_data="set_notification_time")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Настройки уведомлений:", reply_markup=reply_markup) + +async def send_telegram_notification(user, message): + """Отправка уведомления через Telegram.""" + + # bot = Bot(token="ВАШ_ТОКЕН") + if user.chat_id: + try: + await bot.send_message(chat_id=user.chat_id, text=message) + print(f"Telegram-уведомление отправлено пользователю {user.chat_id}: {message}") + except Exception as e: + print(f"Ошибка отправки Telegram-уведомления пользователю {user.chat_id}: {e}") + + +def send_email_notification(user, message): + """Отправка уведомления через Email.""" + if user.email: + try: + send_mail( + subject="Уведомление от системы", + message=message, + from_email="noreply@yourdomain.com", + recipient_list=[user.email], + fail_silently=False, + ) + print(f"Email-уведомление отправлено на {user.email}: {message}") + except Exception as e: + print(f"Ошибка отправки Email-уведомления пользователю {user.email}: {e}") + + +async def schedule_notifications(): + """Планировщик уведомлений.""" + print("Запуск планировщика уведомлений...") + now = datetime.now().strftime("%H:%M") + + users = await sync_to_async(list)(User.objects.all()) + for user in users: + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + + if settings.notification_time == now: + message = "Это ваше уведомление от системы." + if settings.telegram_enabled: + await send_telegram_notification(user, message) + if settings.email_enabled: + send_email_notification(user, message) + + +async def show_current_settings(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Отображение текущих настроек уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await get_user_from_chat_id(user_id) + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + telegram_status = "✅ Включены" if settings.telegram_enabled else "❌ Выключены" + email_status = "✅ Включены" if settings.email_enabled else "❌ Выключены" + notification_time = settings.notification_time or "Не установлено" + + message = ( + f"📋 Ваши настройки уведомлений:\n" + f"🔔 Telegram: {telegram_status}\n" + f"📧 Email: {email_status}\n" + f"🕒 Время: {notification_time}" + ) + await query.edit_message_text(message) + + + +async def check_pms_integration(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[2]) + hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) + pms_settings = hotel.pms.parser_settings # Настройки из связанной PMSConfiguration + + try: + # Выполняем запрос к PMS + response = requests.post( + url=pms_settings["url"], + headers={ + "Authorization": f"Bearer {hotel.api.api_key}", + "Content-Type": "application/json", + }, + json={ + "from": "2024-01-01T00:00:00Z", + "until": "2024-01-10T00:00:00Z", + "pagination": {"from": 0, "count": 10}, + }, + ) + + # Проверяем результат + if response.status_code == 200: + await sync_to_async(PMSIntegrationLog.objects.create)( + hotel=hotel, + status="success", + message="Интеграция успешно проверена.", + ) + await query.edit_message_text(f"Интеграция с PMS для отеля '{hotel.name}' успешна.") + else: + await sync_to_async(PMSIntegrationLog.objects.create)( + hotel=hotel, + status="error", + message=f"Ошибка: {response.status_code}", + ) + await query.edit_message_text(f"Ошибка интеграции с PMS для отеля '{hotel.name}': {response.status_code}") + except Exception as e: + await sync_to_async(PMSIntegrationLog.objects.create)( + hotel=hotel, + status="error", + message=str(e), + ) + await query.edit_message_text(f"Произошла ошибка: {str(e)}") diff --git a/bot/management/commands/run_bot.py b/bot/management/commands/run_bot.py index cfd191bc..3042ddba 100644 --- a/bot/management/commands/run_bot.py +++ b/bot/management/commands/run_bot.py @@ -1,28 +1,97 @@ import os import django +import asyncio +from apscheduler.schedulers.asyncio import AsyncIOScheduler from django.core.management.base import BaseCommand -from telegram.ext import Application, CommandHandler -from bot.handlers import start, list_users, list_hotels # Импорт обработчиков +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters, +) +from bot.handlers import ( + start, + handle_button_click, + manage_hotels, + hotel_actions, + delete_hotel, + check_pms, + setup_rooms, + settings_menu, + toggle_telegram, + toggle_email, + set_notification_time, + handle_notification_time, + schedule_notifications, + show_current_settings, +) # Настройка Django окружения os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'touchh.settings') django.setup() -def main(): - # Создаём приложение Telegram - application = Application.builder().token("8125171867:AAGxDcSpQxJy3_pmq3TDBWtqaAVCj7b-F5k").build() - # Регистрируем обработчики команд +async def start_bot(application): + """Настройка и запуск Telegram бота.""" + print("Настройка Telegram приложения...") + + # Регистрация обработчиков команд + print("Регистрация обработчиков команд...") application.add_handler(CommandHandler("start", start)) - application.add_handler(CommandHandler("users", list_users)) - application.add_handler(CommandHandler("hotels", list_hotels)) - # Запускаем бота - application.run_polling() + # Регистрация обработчиков кнопок + print("Регистрация обработчиков кнопок...") + application.add_handler(CallbackQueryHandler(handle_button_click)) + application.add_handler(CallbackQueryHandler(manage_hotels, pattern="^manage_hotels$")) + application.add_handler(CallbackQueryHandler(settings_menu, pattern="^settings$")) + application.add_handler(CallbackQueryHandler(toggle_telegram, pattern="^toggle_telegram$")) + application.add_handler(CallbackQueryHandler(toggle_email, pattern="^toggle_email$")) + application.add_handler(CallbackQueryHandler(set_notification_time, pattern="^set_notification_time$")) + application.add_handler(CallbackQueryHandler(show_current_settings, pattern="^current_settings$")) + application.add_handler(CallbackQueryHandler(hotel_actions, pattern="^hotel_")) + application.add_handler(CallbackQueryHandler(delete_hotel, pattern="^delete_hotel_")) + application.add_handler(CallbackQueryHandler(check_pms, pattern="^check_pms_")) + application.add_handler(CallbackQueryHandler(setup_rooms, pattern="^setup_rooms_")) + + # Регистрация обработчиков текстовых сообщений + print("Регистрация обработчиков текстовых сообщений...") + application.add_handler(MessageHandler(filters.TEXT & filters.ChatType.PRIVATE, handle_notification_time)) + + # Настройка планировщика + print("Настройка планировщика уведомлений...") + scheduler = AsyncIOScheduler() + scheduler.add_job(schedule_notifications, "cron", minute="*") + scheduler.start() + + # Запуск бота + print("Запуск Telegram бота...") + await application.initialize() + await application.start() + print("Бот успешно запущен. Ожидание событий...") + await application.updater.start_polling() + class Command(BaseCommand): help = "Запуск Telegram бота" def handle(self, *args, **options): - self.stdout.write("Запуск Telegram бота...") - main() + print("Запуск Telegram бота...") + + # Получаем текущий цикл событий + loop = asyncio.get_event_loop() + + # Создаём экземпляр приложения + application = Application.builder().token("8125171867:AAGxDcSpQxJy3_pmq3TDBWtqaAVCj7b-F5k").build() + + # Добавляем задачу для запуска бота + loop.create_task(start_bot(application)) + + # Запускаем цикл событий + try: + loop.run_forever() + except KeyboardInterrupt: + print("Остановка Telegram бота...") + loop.run_until_complete(application.stop()) + scheduler = AsyncIOScheduler() + scheduler.shutdown(wait=False) diff --git a/hotels/admin.py b/hotels/admin.py index 09fe2327..7609bbc9 100644 --- a/hotels/admin.py +++ b/hotels/admin.py @@ -1,12 +1,23 @@ from django.contrib import admin -from .models import Hotel, UserHotel +from .models import Hotel, UserHotel, APIConfiguration, APIRequestLog, PMSConfiguration, PMSIntegrationLog +from django import forms +class HotelForm(forms.ModelForm): + class Meta: + model = Hotel + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Исключаем API, которые уже связаны с другими отелями + used_apis = Hotel.objects.exclude(api__isnull=True).values_list('api', flat=True) + self.fields['api'].queryset = APIConfiguration.objects.exclude(id__in=used_apis) -@admin.register(Hotel) class HotelAdmin(admin.ModelAdmin): - list_display = ('name', 'pms_type', 'created_at') + form = HotelForm + list_display = ('name', 'api', 'created_at', 'pms') search_fields = ('name',) - list_filter = ('pms_type',) - ordering = ('-created_at',) + +admin.site.register(Hotel, HotelAdmin) @admin.register(UserHotel) class UserHotelAdmin(admin.ModelAdmin): @@ -14,4 +25,32 @@ class UserHotelAdmin(admin.ModelAdmin): search_fields = ('user', 'hotel') list_filter = ('hotel',) ordering = ('-hotel',) + + + +@admin.register(APIConfiguration) +class ApiConfigurationAdmin(admin.ModelAdmin): + list_display = ('name', 'url', 'token', 'username', 'password') + search_fields = ('name', 'url', 'token', 'username', 'password') + list_filter = ('name', 'url', 'token', 'username', 'password') + ordering = ('-name',) + +@admin.register(APIRequestLog) +class ApiRequestLogAdmin(admin.ModelAdmin): + list_display = ('api', 'request_time', 'response_status', 'response_data') + search_fields = ('api', 'request_time', 'response_status', 'response_data') + list_filter = ('api', 'request_time', 'response_status', 'response_data') + ordering = ('-api',) + + +@admin.register(PMSConfiguration) +class PMSConfigurationAdmin(admin.ModelAdmin): + list_display = ('name', 'parser_settings', 'description') + +@admin.register(PMSIntegrationLog) +class PMSIntegreationLogAdmin(admin.ModelAdmin): + list_display = ('hotel', 'checked_at', 'status', 'message') + search_fields = ('hotel', 'checked_at', 'status', 'message') + list_filter = ('hotel', 'checked_at', 'status', 'message') + ordering = ('-checked_at',) \ No newline at end of file diff --git a/hotels/migrations/0004_datalog.py b/hotels/migrations/0004_datalog.py new file mode 100644 index 00000000..9cc06059 --- /dev/null +++ b/hotels/migrations/0004_datalog.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.4 on 2024-12-06 13:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0003_alter_hotel_options_alter_userhotel_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='DataLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Название')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('data', models.JSONField(verbose_name='Данные')), + ], + options={ + 'verbose_name': 'Лог данных', + 'verbose_name_plural': 'Логи данных', + }, + ), + ] diff --git a/hotels/migrations/0005_apiconfiguration_apirequestlog_delete_datalog.py b/hotels/migrations/0005_apiconfiguration_apirequestlog_delete_datalog.py new file mode 100644 index 00000000..9c670d56 --- /dev/null +++ b/hotels/migrations/0005_apiconfiguration_apirequestlog_delete_datalog.py @@ -0,0 +1,47 @@ +# Generated by Django 5.1.4 on 2024-12-06 13:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0004_datalog'), + ] + + operations = [ + migrations.CreateModel( + name='APIConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Название API')), + ('url', models.URLField(verbose_name='URL API')), + ('token', models.CharField(blank=True, max_length=255, null=True, verbose_name='Токен')), + ('username', models.CharField(blank=True, max_length=255, null=True, verbose_name='Логин')), + ('password', models.CharField(blank=True, max_length=255, null=True, verbose_name='Пароль')), + ('last_updated', models.DateTimeField(auto_now=True, verbose_name='Дата последнего обновления')), + ], + options={ + 'verbose_name': 'Конфигурация API', + 'verbose_name_plural': 'Конфигурации API', + }, + ), + migrations.CreateModel( + name='APIRequestLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('request_time', models.DateTimeField(auto_now_add=True, verbose_name='Время запроса')), + ('response_status', models.IntegerField(verbose_name='HTTP статус ответа')), + ('response_data', models.JSONField(blank=True, null=True, verbose_name='Данные ответа')), + ('api', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.apiconfiguration', verbose_name='API')), + ], + options={ + 'verbose_name': 'Журнал запросов API', + 'verbose_name_plural': 'Журналы запросов API', + }, + ), + migrations.DeleteModel( + name='DataLog', + ), + ] diff --git a/hotels/migrations/0006_pmsconfiguration_remove_hotel_api_key_and_more.py b/hotels/migrations/0006_pmsconfiguration_remove_hotel_api_key_and_more.py new file mode 100644 index 00000000..c21e8176 --- /dev/null +++ b/hotels/migrations/0006_pmsconfiguration_remove_hotel_api_key_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 5.1.4 on 2024-12-06 14:09 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0005_apiconfiguration_apirequestlog_delete_datalog'), + ] + + operations = [ + migrations.CreateModel( + name='PMSConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Название PMS')), + ('parser_settings', models.JSONField(default=dict, verbose_name='Настройки разбора данных')), + ('description', models.TextField(blank=True, null=True, verbose_name='Описание')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ], + options={ + 'verbose_name': 'PMS система', + 'verbose_name_plural': 'PMS системы', + }, + ), + migrations.RemoveField( + model_name='hotel', + name='api_key', + ), + migrations.RemoveField( + model_name='hotel', + name='pms_type', + ), + migrations.RemoveField( + model_name='hotel', + name='public_key', + ), + migrations.AddField( + model_name='hotel', + name='api', + field=models.OneToOneField(blank=True, help_text='API, связанный с этим отелем.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.apiconfiguration', verbose_name='API'), + ), + ] diff --git a/hotels/migrations/0007_pmsintegrationlog.py b/hotels/migrations/0007_pmsintegrationlog.py new file mode 100644 index 00000000..fe50d71e --- /dev/null +++ b/hotels/migrations/0007_pmsintegrationlog.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.4 on 2024-12-06 23:02 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0006_pmsconfiguration_remove_hotel_api_key_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PMSIntegrationLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('checked_at', models.DateTimeField(auto_now_add=True, verbose_name='Время проверки')), + ('status', models.CharField(choices=[('success', 'Успех'), ('error', 'Ошибка')], max_length=50, verbose_name='Статус')), + ('message', models.TextField(blank=True, null=True, verbose_name='Сообщение')), + ('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')), + ], + options={ + 'verbose_name': 'Журнал интеграции PMS', + 'verbose_name_plural': 'Журналы интеграции PMS', + }, + ), + ] diff --git a/hotels/migrations/0008_hotel_pms.py b/hotels/migrations/0008_hotel_pms.py new file mode 100644 index 00000000..cad2ce81 --- /dev/null +++ b/hotels/migrations/0008_hotel_pms.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.4 on 2024-12-06 23:38 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0007_pmsintegrationlog'), + ] + + operations = [ + migrations.AddField( + model_name='hotel', + name='pms', + field=models.OneToOneField(blank=True, help_text='PMS система? используемая в заведении', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.pmsconfiguration', verbose_name='PMS система'), + ), + ] diff --git a/hotels/migrations/0009_alter_hotel_pms.py b/hotels/migrations/0009_alter_hotel_pms.py new file mode 100644 index 00000000..9e98065b --- /dev/null +++ b/hotels/migrations/0009_alter_hotel_pms.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.4 on 2024-12-06 23:57 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0008_hotel_pms'), + ] + + operations = [ + migrations.AlterField( + model_name='hotel', + name='pms', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.pmsconfiguration', verbose_name='PMS система'), + ), + ] diff --git a/hotels/migrations/0010_apirequestlog_hotels_apir_api_id_686bb0_idx_and_more.py b/hotels/migrations/0010_apirequestlog_hotels_apir_api_id_686bb0_idx_and_more.py new file mode 100644 index 00000000..38bb7e0a --- /dev/null +++ b/hotels/migrations/0010_apirequestlog_hotels_apir_api_id_686bb0_idx_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.4 on 2024-12-07 00:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0009_alter_hotel_pms'), + ] + + operations = [ + migrations.AddIndex( + model_name='apirequestlog', + index=models.Index(fields=['api'], name='hotels_apir_api_id_686bb0_idx'), + ), + migrations.AddIndex( + model_name='apirequestlog', + index=models.Index(fields=['request_time'], name='hotels_apir_request_f65147_idx'), + ), + migrations.AddIndex( + model_name='pmsintegrationlog', + index=models.Index(fields=['hotel'], name='hotels_pmsi_hotel_i_718b32_idx'), + ), + migrations.AddIndex( + model_name='pmsintegrationlog', + index=models.Index(fields=['checked_at'], name='hotels_pmsi_checked_e7768d_idx'), + ), + migrations.AddIndex( + model_name='pmsintegrationlog', + index=models.Index(fields=['status'], name='hotels_pmsi_status_dbf1d8_idx'), + ), + ] diff --git a/hotels/models.py b/hotels/models.py index 4ec5260e..95a6201b 100644 --- a/hotels/models.py +++ b/hotels/models.py @@ -1,25 +1,72 @@ from django.db import models from users.models import User -class Hotel(models.Model): - name = models.CharField(max_length=255, verbose_name="Название отеля") - pms_type = models.CharField( - max_length=50, - choices=[('bnovo', 'Bnovo'), ('travelline', 'Travel Line')], - verbose_name="PMS система" - ) - api_key = models.CharField(max_length=255, blank=True, null=True, verbose_name="API ключ") - public_key = models.CharField(max_length=255, blank=True, null=True, verbose_name="Публичный ключ") - created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан") + +class PMSConfiguration(models.Model): + name = models.CharField(max_length=255, verbose_name="Название PMS") + parser_settings = models.JSONField(default=dict, verbose_name="Настройки разбора данных") + description = models.TextField(blank=True, null=True, verbose_name="Описание") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") def __str__(self): return self.name - + + class Meta: + verbose_name = "PMS система" + verbose_name_plural = "PMS системы" + + +class APIConfiguration(models.Model): + name = models.CharField(max_length=255, verbose_name="Название API") + url = models.URLField(verbose_name="URL API") + token = models.CharField(max_length=255, blank=True, null=True, verbose_name="Токен") + username = models.CharField(max_length=255, blank=True, null=True, verbose_name="Логин") + password = models.CharField(max_length=255, blank=True, null=True, verbose_name="Пароль") + last_updated = models.DateTimeField(auto_now=True, verbose_name="Дата последнего обновления") + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Конфигурация API" + verbose_name_plural = "Конфигурации API" + +class Hotel(models.Model): + name = models.CharField(max_length=255, verbose_name="Название отеля") + api = models.OneToOneField( + APIConfiguration, + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name="API", + help_text="API, связанный с этим отелем." + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан") + pms = models.ForeignKey(PMSConfiguration, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="PMS система") + + def __str__(self): + return self.name + class Meta: verbose_name = "Отель" verbose_name_plural = "Отели" - +class PMSIntegrationLog(models.Model): + hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель") + checked_at = models.DateTimeField(auto_now_add=True, verbose_name="Время проверки") + status = models.CharField(max_length=50, verbose_name="Статус", choices=[('success', 'Успех'), ('error', 'Ошибка')]) + message = models.TextField(verbose_name="Сообщение", blank=True, null=True) + def __str__(self): + return f"{self.hotel.name} - {self.status} - {self.checked_at}" + class Meta: + verbose_name = "Журнал интеграции PMS" + verbose_name_plural = "Журналы интеграции PMS" + indexes = [ + models.Index(fields=['hotel']), + models.Index(fields=['checked_at']), + models.Index(fields=['status']), + ] + class UserHotel(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Пользователь") @@ -31,4 +78,26 @@ class UserHotel(models.Model): class Meta: verbose_name = "Пользователь отеля" verbose_name_plural = "Пользователи отелей" - \ No newline at end of file + +from django.db import models + + + +class APIRequestLog(models.Model): + api = models.ForeignKey(APIConfiguration, on_delete=models.CASCADE, verbose_name="API") + request_time = models.DateTimeField(auto_now_add=True, verbose_name="Время запроса") + response_status = models.IntegerField(verbose_name="HTTP статус ответа") + response_data = models.JSONField(verbose_name="Данные ответа", blank=True, null=True) + + def __str__(self): + return f"{self.api.name} - {self.request_time}" + class Meta: + verbose_name = "Журнал запросов API" + verbose_name_plural = "Журналы запросов API" + indexes = [ + models.Index(fields=['api']), + models.Index(fields=['request_time']), + ] + +from django.db import models + diff --git a/hotels/pms_check.py b/hotels/pms_check.py new file mode 100644 index 00000000..70744be9 --- /dev/null +++ b/hotels/pms_check.py @@ -0,0 +1,92 @@ +import os +import json +from datetime import datetime, timedelta +import requests +from django.db import models +from asgiref.sync import sync_to_async + + +class APIDataLogger: + """Класс для работы с API, сохранения и обработки данных.""" + + def __init__(self, name, url, token=None, username=None, password=None): + self.name = name + self.url = url + self.token = token + self.username = username + self.password = password + self.output_dir = "modules" + + def ensure_directory_exists(self, path): + """Создать директорию, если она не существует.""" + if not os.path.exists(path): + os.makedirs(path) + + def fetch_data(self, additional_data=None): + """Получить данные из API.""" + headers = {"Content-Type": "application/json"} + data = additional_data or {} + + if self.token: + data["token"] = self.token + + response = requests.post(self.url, headers=headers, json=data, auth=(self.username, self.password) if self.username and self.password else None) + if response.status_code != 200: + print(f'{self.name}: API запрос не удался. Код статуса: {response.status_code}') + return [] + + return response.json() + + def save_data(self, data, suffix): + """Сохранить данные в файл JSON.""" + now = datetime.now() + current_date = now.strftime('%Y-%m-%d') + directory = os.path.join(self.output_dir, current_date, self.name) + self.ensure_directory_exists(directory) + + filename = f"{self.name} {suffix}.json" + filepath = os.path.join(directory, filename) + with open(filepath, 'w') as file: + json.dump(data, file) + return filepath + + def load_previous_data(self, suffixes): + """Загрузить данные из файлов за текущий и предыдущий интервалы.""" + now = datetime.now() + current_date = now.strftime('%Y-%m-%d') + yesterday_date = (now - timedelta(days=1)).strftime('%Y-%m-%d') + + directories = [(yesterday_date, "21"), (current_date, "9")] if 9 <= now.hour < 21 else [(current_date, "9"), (current_date, "21")] + data_combined = [] + + for date, suffix in directories: + filepath = os.path.join(self.output_dir, date, self.name, f"{self.name} {suffix}.json") + try: + with open(filepath, 'r') as file: + data_combined.extend(json.load(file)) + except FileNotFoundError: + pass + + return data_combined + + def filter_data(self, data, filter_function): + """Фильтрация данных с использованием переданной функции.""" + return filter_function(data) + + def process_and_save(self, additional_data=None, filter_function=None): + """Основной процесс: запрос, сохранение, чтение, фильтрация.""" + now = datetime.now() + suffix = "9" if 9 <= now.hour < 21 else "21" + + # Шаг 1: Получить данные + raw_data = self.fetch_data(additional_data) + self.save_data(raw_data, suffix) + + # Шаг 2: Загрузить данные за текущий и предыдущий интервал + combined_data = self.load_previous_data(["9", "21"]) + + # Шаг 3: Фильтрация + if filter_function: + combined_data = self.filter_data(combined_data, filter_function) + + return combined_data \ No newline at end of file diff --git a/hotels/pms_parse.py b/hotels/pms_parse.py new file mode 100644 index 00000000..c7e69796 --- /dev/null +++ b/hotels/pms_parse.py @@ -0,0 +1,53 @@ +import json +from datetime import datetime + +def parse_pms_data(data, parser_settings): + date_format = parser_settings["date_format"] + fields_mapping = parser_settings["fields_mapping"] + conditions = parser_settings.get("conditions", {}) + + parsed_data = [] + + for record in data: + # Применение условий фильтрации + if conditions: + for condition_field, expected_value in conditions.items(): + if record.get(condition_field) != expected_value: + break + else: + # Условие выполнено + pass + else: + # Условие отсутствует + pass + + # Разбор полей + parsed_record = {} + for internal_field, external_field in fields_mapping.items(): + if "." in external_field: # Например, "guests[0].lastName" + keys = external_field.split(".") + value = record + try: + for key in keys: + if key.endswith("]"): # Обработка индексов, например, "guests[0]" + key, index = key[:-1].split("[") + value = value[key][int(index)] + else: + value = value[key] + except (KeyError, IndexError, TypeError): + value = None + else: + value = record.get(external_field) + + # Преобразование дат + if "date" in internal_field or "time" in internal_field: + try: + value = datetime.strptime(value, date_format) + except (ValueError, TypeError): + value = None + + parsed_record[internal_field] = value + + parsed_data.append(parsed_record) + + return parsed_data diff --git a/touchh/settings.py b/touchh/settings.py index 2fe74df6..69ed46dc 100644 --- a/touchh/settings.py +++ b/touchh/settings.py @@ -87,17 +87,10 @@ DATABASES = { 'wordpress': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'u1510415_wp832', -<<<<<<< HEAD - 'USER': 'root', - 'PASSWORD': 'R0sebud', - 'HOST': '0.0.0.0', - 'PORT': '3308', -======= 'USER': 'u1510415_wp832', 'PASSWORD': 'yZ1gV6kH6lzD2cQ3', 'HOST': 'server231.hosting.reg.ru', 'PORT': '3306', ->>>>>>> cea14d40d73e3a67c30dda8765458d91652e6bf4 }, } # Password validation diff --git a/users/admin.py b/users/admin.py index ba22a3e0..f71c89dd 100644 --- a/users/admin.py +++ b/users/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import User, UserConfirmation, UserActivityLog +from .models import User, UserConfirmation, UserActivityLog, NotificationSettings @admin.register(User) class UserAdmin(admin.ModelAdmin): @@ -20,4 +20,11 @@ class UserActivityLogAdmin(admin.ModelAdmin): search_fields = ('user_id', 'ip', 'datetime', 'agent', 'platform', 'version', 'model', 'device', 'UAString', 'location', 'page_id', 'url_parameters', 'page_title', 'type', 'last_counter', 'hits', 'honeypot', 'reply', 'page_url') list_filter = ('page_title', 'user_id', 'ip') ordering = ('-id',) + +@admin.register(NotificationSettings) +class NotificationSettingsAdmin(admin.ModelAdmin): + list_display = ('user', 'email', 'telegram_enabled', 'email_enabled', 'notification_time') + search_fields = ('user', 'email', 'telegram_enabled', 'email_enabled', 'notification_time') + list_filter = ('user', 'email', 'telegram_enabled', 'email_enabled', 'notification_time') + ordering = ('-id',) \ No newline at end of file diff --git a/users/migrations/0005_notificationsettings.py b/users/migrations/0005_notificationsettings.py new file mode 100644 index 00000000..2d2bc730 --- /dev/null +++ b/users/migrations/0005_notificationsettings.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.4 on 2024-12-06 12:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_alter_user_options_alter_userconfirmation_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='NotificationSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('telegram_enabled', models.BooleanField(default=True, verbose_name='Уведомления в Telegram')), + ('email_enabled', models.BooleanField(default=False, verbose_name='Уведомления по Email')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email для уведомлений')), + ('notification_time', models.TimeField(default='09:00', verbose_name='Время отправки уведомлений')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Способ оповещения', + 'verbose_name_plural': 'Способы оповещений', + }, + ), + ] diff --git a/users/models.py b/users/models.py index 10f06e79..c737be4a 100644 --- a/users/models.py +++ b/users/models.py @@ -112,4 +112,18 @@ class UserActivityLog(models.Model): verbose_name_plural = 'Журналы активности' def __str__(self): - return f"User {self.user_id} - {self.type} - {self.date_time}" \ No newline at end of file + return f"User {self.user_id} - {self.type} - {self.date_time}" + +class NotificationSettings(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="Пользователь") + telegram_enabled = models.BooleanField(default=True, verbose_name="Уведомления в Telegram") + email_enabled = models.BooleanField(default=False, verbose_name="Уведомления по Email") + email = models.EmailField(blank=True, null=True, verbose_name="Email для уведомлений") + notification_time = models.TimeField(default="09:00", verbose_name="Время отправки уведомлений") + + def __str__(self): + return f"Настройки уведомлений для {self.user.username}" + + class Meta: + verbose_name = "Способ оповещения" + verbose_name_plural = "Способы оповещений" \ No newline at end of file