diff --git a/.gitignore b/.gitignore index 1bdca65a..935ee588 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ __pycache__ node_modules package-lock.json package.json +old_bot + +# Ignore files \ No newline at end of file diff --git a/1db.sqlite3 b/1db.sqlite3 new file mode 100644 index 00000000..d3b8457e Binary files /dev/null and b/1db.sqlite3 differ diff --git a/1db.sqlite3.sql b/1db.sqlite3.sql new file mode 100644 index 00000000..214d86fe --- /dev/null +++ b/1db.sqlite3.sql @@ -0,0 +1,149 @@ +BEGIN TRANSACTION; +INSERT INTO "django_migrations" ("id","app","name","applied") VALUES (1,'contenttypes','0001_initial','2024-12-09 09:30:10.251024'), + (2,'auth','0001_initial','2024-12-09 09:30:10.285571'), + (3,'admin','0001_initial','2024-12-09 09:30:10.304106'), + (4,'admin','0002_logentry_remove_auto_add','2024-12-09 09:30:10.327893'), + (5,'admin','0003_logentry_add_action_flag_choices','2024-12-09 09:30:10.347643'), + (6,'contenttypes','0002_remove_content_type_name','2024-12-09 09:30:10.391061'), + (7,'auth','0002_alter_permission_name_max_length','2024-12-09 09:30:10.411017'), + (8,'auth','0003_alter_user_email_max_length','2024-12-09 09:30:10.433375'), + (9,'auth','0004_alter_user_username_opts','2024-12-09 09:30:10.456000'), + (10,'auth','0005_alter_user_last_login_null','2024-12-09 09:30:10.487091'), + (11,'auth','0006_require_contenttypes_0002','2024-12-09 09:30:10.490205'), + (12,'auth','0007_alter_validators_add_error_messages','2024-12-09 09:30:10.505490'), + (13,'auth','0008_alter_user_username_max_length','2024-12-09 09:30:10.531170'), + (14,'auth','0009_alter_user_last_name_max_length','2024-12-09 09:30:10.554633'), + (15,'auth','0010_alter_group_name_max_length','2024-12-09 09:30:10.572891'), + (16,'auth','0011_update_proxy_permissions','2024-12-09 09:30:10.595627'), + (17,'auth','0012_alter_user_first_name_max_length','2024-12-09 09:30:10.626279'), + (18,'users','0001_initial','2024-12-09 09:30:10.697462'), + (19,'hotels','0001_initial','2024-12-09 09:30:10.728173'), + (20,'pms_integration','0001_initial','2024-12-09 09:30:10.753819'), + (21,'hotels','0002_initial','2024-12-09 09:30:10.883078'), + (22,'hotels','0003_initial','2024-12-09 09:30:11.011514'), + (23,'sessions','0001_initial','2024-12-09 09:30:11.033336'); +INSERT INTO "django_admin_log" ("id","object_id","object_repr","action_flag","change_message","content_type_id","user_id","action_time") VALUES (1,'1','Shelter Golden Hills 3',1,'[{"added": {}}]',7,1,'2024-12-09 09:32:31.355706'), + (2,'2','Shelter Golden Hills 4',1,'[{"added": {}}]',7,1,'2024-12-09 09:33:10.530095'), + (3,'1','Golden Hills 3',1,'[{"added": {}}]',15,1,'2024-12-09 09:34:11.465732'), + (4,'2','Golden Hills 4',1,'[{"added": {}}]',15,1,'2024-12-09 09:34:21.783766'), + (5,'1','andrew',1,'[{"added": {}}]',18,1,'2024-12-09 09:35:20.367524'), + (6,'1','Настройки уведомлений для andrew',1,'[{"added": {}}]',19,1,'2024-12-09 09:35:40.518128'), + (7,'1','andrew - Golden Hills 3',1,'[{"added": {}}]',13,1,'2024-12-09 09:35:57.888800'), + (8,'2','andrew - Golden Hills 4',1,'[{"added": {}}]',13,1,'2024-12-09 09:36:06.799616'), + (9,'3','Как дома',1,'[{"added": {}}]',15,1,'2024-12-09 10:19:47.100883'); +INSERT INTO "django_content_type" ("id","app_label","model") VALUES (1,'admin','logentry'), + (2,'auth','permission'), + (3,'auth','group'), + (4,'auth','user'), + (5,'contenttypes','contenttype'), + (6,'sessions','session'), + (7,'pms_integration','pmsconfiguration'), + (8,'pms_integration','pmsintegrationlog'), + (9,'hotels','apiconfiguration'), + (10,'hotels','fraudlog'), + (11,'hotels','guest'), + (12,'hotels','reservation'), + (13,'hotels','userhotel'), + (14,'hotels','apirequestlog'), + (15,'hotels','hotel'), + (16,'users','localuseractivitylog'), + (17,'users','useractivitylog'), + (18,'users','user'), + (19,'users','notificationsettings'), + (20,'users','userconfirmation'); +INSERT INTO "auth_permission" ("id","content_type_id","codename","name") VALUES (1,1,'add_logentry','Can add log entry'), + (2,1,'change_logentry','Can change log entry'), + (3,1,'delete_logentry','Can delete log entry'), + (4,1,'view_logentry','Can view log entry'), + (5,2,'add_permission','Can add permission'), + (6,2,'change_permission','Can change permission'), + (7,2,'delete_permission','Can delete permission'), + (8,2,'view_permission','Can view permission'), + (9,3,'add_group','Can add group'), + (10,3,'change_group','Can change group'), + (11,3,'delete_group','Can delete group'), + (12,3,'view_group','Can view group'), + (13,4,'add_user','Can add user'), + (14,4,'change_user','Can change user'), + (15,4,'delete_user','Can delete user'), + (16,4,'view_user','Can view user'), + (17,5,'add_contenttype','Can add content type'), + (18,5,'change_contenttype','Can change content type'), + (19,5,'delete_contenttype','Can delete content type'), + (20,5,'view_contenttype','Can view content type'), + (21,6,'add_session','Can add session'), + (22,6,'change_session','Can change session'), + (23,6,'delete_session','Can delete session'), + (24,6,'view_session','Can view session'), + (25,7,'add_pmsconfiguration','Can add PMS система'), + (26,7,'change_pmsconfiguration','Can change PMS система'), + (27,7,'delete_pmsconfiguration','Can delete PMS система'), + (28,7,'view_pmsconfiguration','Can view PMS система'), + (29,8,'add_pmsintegrationlog','Can add Журнал интеграции PMS'), + (30,8,'change_pmsintegrationlog','Can change Журнал интеграции PMS'), + (31,8,'delete_pmsintegrationlog','Can delete Журнал интеграции PMS'), + (32,8,'view_pmsintegrationlog','Can view Журнал интеграции PMS'), + (33,9,'add_apiconfiguration','Can add Конфигурация API'), + (34,9,'change_apiconfiguration','Can change Конфигурация API'), + (35,9,'delete_apiconfiguration','Can delete Конфигурация API'), + (36,9,'view_apiconfiguration','Can view Конфигурация API'), + (37,10,'add_fraudlog','Can add Журнал мошенничества'), + (38,10,'change_fraudlog','Can change Журнал мошенничества'), + (39,10,'delete_fraudlog','Can delete Журнал мошенничества'), + (40,10,'view_fraudlog','Can view Журнал мошенничества'), + (41,11,'add_guest','Can add Гость'), + (42,11,'change_guest','Can change Гость'), + (43,11,'delete_guest','Can delete Гость'), + (44,11,'view_guest','Can view Гость'), + (45,12,'add_reservation','Can add Бронирование'), + (46,12,'change_reservation','Can change Бронирование'), + (47,12,'delete_reservation','Can delete Бронирование'), + (48,12,'view_reservation','Can view Бронирование'), + (49,13,'add_userhotel','Can add Пользователь отеля'), + (50,13,'change_userhotel','Can change Пользователь отеля'), + (51,13,'delete_userhotel','Can delete Пользователь отеля'), + (52,13,'view_userhotel','Can view Пользователь отеля'), + (53,14,'add_apirequestlog','Can add Журнал запросов API'), + (54,14,'change_apirequestlog','Can change Журнал запросов API'), + (55,14,'delete_apirequestlog','Can delete Журнал запросов API'), + (56,14,'view_apirequestlog','Can view Журнал запросов API'), + (57,15,'add_hotel','Can add Отель'), + (58,15,'change_hotel','Can change Отель'), + (59,15,'delete_hotel','Can delete Отель'), + (60,15,'view_hotel','Can view Отель'), + (61,16,'add_localuseractivitylog','Can add local user activity log'), + (62,16,'change_localuseractivitylog','Can change local user activity log'), + (63,16,'delete_localuseractivitylog','Can delete local user activity log'), + (64,16,'view_localuseractivitylog','Can view local user activity log'), + (65,17,'add_useractivitylog','Can add Журнал активности'), + (66,17,'change_useractivitylog','Can change Журнал активности'), + (67,17,'delete_useractivitylog','Can delete Журнал активности'), + (68,17,'view_useractivitylog','Can view Журнал активности'), + (69,18,'add_user','Can add Пользователь'), + (70,18,'change_user','Can change Пользователь'), + (71,18,'delete_user','Can delete Пользователь'), + (72,18,'view_user','Can view Пользователь'), + (73,19,'add_notificationsettings','Can add Способ оповещения'), + (74,19,'change_notificationsettings','Can change Способ оповещения'), + (75,19,'delete_notificationsettings','Can delete Способ оповещения'), + (76,19,'view_notificationsettings','Can view Способ оповещения'), + (77,20,'add_userconfirmation','Can add Подтверждение пользователя'), + (78,20,'change_userconfirmation','Can change Подтверждение пользователя'), + (79,20,'delete_userconfirmation','Can delete Подтверждение пользователя'), + (80,20,'view_userconfirmation','Can view Подтверждение пользователя'); +INSERT INTO "auth_user" ("id","password","last_login","is_superuser","username","last_name","email","is_staff","is_active","date_joined","first_name") VALUES (1,'pbkdf2_sha256$870000$0tWRKvUavKHjKmwWjWsfYc$mfqBdzr5TB74K1f9OHCI3w/66VZE7vY53MEpgUT73/4=','2024-12-09 09:31:26.675259',1,'trevor1985','','shadow85@list.ru',1,1,'2024-12-09 09:30:44.551380',''); +INSERT INTO "users_user" ("id","password","last_login","is_superuser","username","first_name","last_name","email","is_staff","is_active","date_joined","telegram_id","chat_id","role","confirmed") VALUES (1,'Andrey K. Tsoy','2024-12-09 09:34:41',1,'andrew','','','',0,1,'2024-12-09 09:34:27',556399210,556399210,'hotel_user',0); +INSERT INTO "users_notificationsettings" ("id","telegram_enabled","email_enabled","email","notification_time","user_id") VALUES (1,0,1,'a.choi@smartsoltech.kr','09:00:00',1); +INSERT INTO "hotels_hotel" ("id","name","created_at","api_id","pms_id") VALUES (1,'Golden Hills 3','2024-12-09 09:34:11.463934',NULL,1), + (2,'Golden Hills 4','2024-12-09 09:34:21.782571',NULL,2), + (3,'Как дома','2024-12-09 10:19:47.095457',NULL,NULL); +INSERT INTO "pms_integration_pmsconfiguration" ("id","name","url","token","username","password","plugin_name","created_at") VALUES (1,'Shelter Golden Hills 3','https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter','A0DD4B7F-0381-4096-B46E-D22F1A571996',NULL,NULL,'Shelter PMS','2024-12-09 09:32:31.348604'), + (2,'Shelter Golden Hills 4','https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter','A0DD4B7F-0381-4096-B46E-D22F1A571996',NULL,NULL,'Shelter PMS','2024-12-09 09:33:10.514492'); +INSERT INTO "pms_integration_pmsintegrationlog" ("id","checked_at","status","message","hotel_id") VALUES (1,'2024-12-09 09:36:43.044421','error','Плагин для PMS Shelter PMS не найден.',2), + (2,'2024-12-09 09:38:09.595349','error','Плагин для PMS Shelter PMS не найден.',1), + (3,'2024-12-09 09:40:38.721678','error','Плагин для PMS Shelter PMS не найден.',2), + (4,'2024-12-09 10:07:44.968856','error','Плагин для PMS Shelter PMS не найден.',1); +INSERT INTO "hotels_userhotel" ("id","hotel_id","user_id") VALUES (1,1,1), + (2,2,1); +INSERT INTO "django_session" ("session_key","session_data","expire_date") VALUES ('wjcybubh02eoyth1e4pm9cgjc8c9rk1v','.eJxVjEEOwiAQRe_C2hBgpgVcuvcMZIBBqoYmpV0Z765NutDtf-_9lwi0rTVsnZcwZXEWWpx-t0jpwW0H-U7tNss0t3WZotwVedAur3Pm5-Vw_w4q9fqthxwtITBY9MlGtJ6ADZiiAdyQ7KhVyVgcGY7gEL1BVqMhH51iNl68P9F6N0w:1tKa6w:zJT5PgcESbBBG5gKuJsyfV6EOxizdevxDzI4QrGbLsc','2024-12-23 09:31:26.682056'); +COMMIT; diff --git a/bot.log b/bot.log new file mode 100644 index 00000000..e69de29b diff --git a/bot/management/commands/run_bot.py b/bot/management/commands/run_bot.py index b79d0bea..0d042a30 100644 --- a/bot/management/commands/run_bot.py +++ b/bot/management/commands/run_bot.py @@ -5,63 +5,56 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from django.core.management.base import BaseCommand from telegram.ext import Application from bot.utils.bot_setup import setup_bot -from bot.utils.scheduler import setup_scheduler -from dotenv import load_dotenv -from bot.operations.users import show_users - -# Загрузка переменных окружения -load_dotenv() +from scheduler.tasks import load_tasks_to_scheduler class Command(BaseCommand): - help = "Запуск Telegram бота" + help = "Запуск Telegram бота и планировщика" def handle(self, *args, **options): - print("Запуск Telegram бота...") - - # Настройка Django окружения + # Установка Django окружения os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings") django.setup() - # Создание приложения Telegram + # Создаем новый цикл событий + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Настройка планировщика + scheduler = AsyncIOScheduler(event_loop=loop) + scheduler.start() + + # Загрузка задач в планировщик + try: + load_tasks_to_scheduler(scheduler) + except Exception as e: + self.stderr.write(f"Ошибка при загрузке задач в планировщик: {e}") + return + + # Настройка Telegram бота bot_token = os.getenv("TELEGRAM_BOT_TOKEN") if not bot_token: raise ValueError("Токен бота не найден в переменных окружения.") application = Application.builder().token(bot_token).build() - - # Настройка бота и обработчиков setup_bot(application) + # Основная асинхронная функция async def main(): - print("Настройка планировщика...") - scheduler = setup_scheduler() - scheduler.start() - + await application.initialize() + await application.start() + await application.updater.start_polling() + self.stdout.write(self.style.SUCCESS("Telegram бот и планировщик успешно запущены.")) try: - print("Инициализация Telegram бота...") - await application.initialize() # Инициализация приложения - print("Бот запущен. Ожидание сообщений...") - await application.start() # Запуск приложения - await application.updater.start_polling() # Запуск обработки сообщений - - # Бесконечный цикл для удержания приложения активным while True: - await asyncio.sleep(3600) # Ожидание 1 час - except Exception as e: - print(f"Ошибка во время работы бота: {e}") - finally: - print("Остановка Telegram бота...") - await application.stop() # Завершаем приложение перед shutdown - print("Остановка планировщика...") - scheduler.shutdown(wait=False) - print("Планировщик остановлен.") + await asyncio.sleep(3600) + except asyncio.CancelledError: + await application.stop() + scheduler.shutdown() + # Запуск асинхронной программы try: - asyncio.run(main()) - except RuntimeError as e: - if str(e) == "This event loop is already running": - print("Цикл событий уже запущен. Используем другой подход для запуска.") - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) - else: - raise + loop.run_until_complete(main()) + except KeyboardInterrupt: + self.stdout.write(self.style.ERROR("Завершение работы Telegram бота и планировщика")) + finally: + loop.close() diff --git a/bot/operations/hotels.py b/bot/operations/hotels.py index bbac570d..7b5b55cd 100644 --- a/bot/operations/hotels.py +++ b/bot/operations/hotels.py @@ -138,18 +138,18 @@ async def check_pms(update, context): # Создаем экземпляр PMSIntegrationManager pms_manager = PMSIntegrationManager(hotel_id=hotel_id) - await sync_to_async(pms_manager.load_hotel)() + await pms_manager.load_hotel() await sync_to_async(pms_manager.load_plugin)() # Проверяем, какой способ интеграции использовать - if hasattr(pms_manager.plugin, 'fetch_data'): + if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data): # Плагин поддерживает метод fetch_data - data = await sync_to_async(pms_manager.plugin.fetch_data)() + data = await pms_manager.plugin.fetch_data() elif pms_config.api_url and pms_config.token: # Используем прямой запрос к API from pms_integration.api_client import APIClient api_client = APIClient(base_url=pms_config.api_url, access_token=pms_config.token) - data = await sync_to_async(api_client.fetch_reservations)() + data = api_client.fetch_reservations() else: # Если подходящий способ не найден await query.edit_message_text("Подходящий способ интеграции с PMS не найден.") @@ -163,7 +163,8 @@ async def check_pms(update, context): await query.edit_message_text(f"Интеграция PMS {pms_config.name} завершена успешно.") except Exception as e: # Обрабатываем и логируем ошибки - await query.edit_message_text(f"Ошибка: {str(e)}") + await query.edit_message_text(f"❌ Ошибка: {str(e)}") + async def setup_rooms(update: Update, context): """Настроить номера отеля.""" diff --git a/db2.sqlite3 b/db2.sqlite3 new file mode 100644 index 00000000..5ded71a7 Binary files /dev/null and b/db2.sqlite3 differ diff --git a/db2.sqlite3.sql b/db2.sqlite3.sql new file mode 100644 index 00000000..a1138d4b --- /dev/null +++ b/db2.sqlite3.sql @@ -0,0 +1,186 @@ +BEGIN TRANSACTION; +INSERT INTO "django_migrations" ("id","app","name","applied") VALUES (1,'contenttypes','0001_initial','2024-12-10 01:46:55.727280'), + (2,'auth','0001_initial','2024-12-10 01:46:55.748342'), + (3,'admin','0001_initial','2024-12-10 01:46:55.761857'), + (4,'admin','0002_logentry_remove_auto_add','2024-12-10 01:46:55.771717'), + (5,'admin','0003_logentry_add_action_flag_choices','2024-12-10 01:46:55.783546'), + (6,'contenttypes','0002_remove_content_type_name','2024-12-10 01:46:55.798657'), + (7,'auth','0002_alter_permission_name_max_length','2024-12-10 01:46:55.812626'), + (8,'auth','0003_alter_user_email_max_length','2024-12-10 01:46:55.826016'), + (9,'auth','0004_alter_user_username_opts','2024-12-10 01:46:55.836404'), + (10,'auth','0005_alter_user_last_login_null','2024-12-10 01:46:55.846555'), + (11,'auth','0006_require_contenttypes_0002','2024-12-10 01:46:55.850370'), + (12,'auth','0007_alter_validators_add_error_messages','2024-12-10 01:46:55.858610'), + (13,'auth','0008_alter_user_username_max_length','2024-12-10 01:46:55.868657'), + (14,'auth','0009_alter_user_last_name_max_length','2024-12-10 01:46:55.880462'), + (15,'auth','0010_alter_group_name_max_length','2024-12-10 01:46:55.893095'), + (16,'auth','0011_update_proxy_permissions','2024-12-10 01:46:55.900517'), + (17,'auth','0012_alter_user_first_name_max_length','2024-12-10 01:46:55.911606'), + (18,'users','0001_initial','2024-12-10 01:46:55.941858'), + (19,'hotels','0001_initial','2024-12-10 01:46:55.957904'), + (20,'pms_integration','0001_initial','2024-12-10 01:46:55.968957'), + (21,'hotels','0002_initial','2024-12-10 01:46:56.002327'), + (22,'hotels','0003_initial','2024-12-10 01:46:56.025083'), + (23,'sessions','0001_initial','2024-12-10 01:46:56.034581'), + (24,'pms_integration','0002_alter_pmsconfiguration_plugin_name','2024-12-10 02:52:26.722876'), + (25,'pms_integration','0003_alter_pmsconfiguration_plugin_name','2024-12-10 02:58:35.733611'), + (26,'pms_integration','0004_alter_pmsconfiguration_plugin_name','2024-12-10 03:00:06.725614'), + (27,'pms_integration','0005_pmsconfiguration_private_key_and_more','2024-12-10 03:04:00.440483'), + (28,'scheduler','0001_initial','2024-12-10 06:42:05.633067'), + (29,'scheduler','0002_alter_scheduledtask_options_and_more','2024-12-10 06:49:38.460869'), + (30,'scheduler','0003_alter_scheduledtask_options_and_more','2024-12-10 07:13:54.438968'); +INSERT INTO "django_admin_log" ("id","object_id","object_repr","action_flag","change_message","content_type_id","user_id","action_time") VALUES (1,'1','Shelter Golden Hills 3',1,'[{"added": {}}]',7,1,'2024-12-09 09:32:31.355706'), + (2,'2','Shelter Golden Hills 4',1,'[{"added": {}}]',7,1,'2024-12-09 09:33:10.530095'), + (3,'1','Golden Hills 3',1,'[{"added": {}}]',15,1,'2024-12-09 09:34:11.465732'), + (4,'2','Golden Hills 4',1,'[{"added": {}}]',15,1,'2024-12-09 09:34:21.783766'), + (5,'1','andrew',1,'[{"added": {}}]',18,1,'2024-12-09 09:35:20.367524'), + (6,'1','Настройки уведомлений для andrew',1,'[{"added": {}}]',19,1,'2024-12-09 09:35:40.518128'), + (7,'1','andrew - Golden Hills 3',1,'[{"added": {}}]',13,1,'2024-12-09 09:35:57.888800'), + (8,'2','andrew - Golden Hills 4',1,'[{"added": {}}]',13,1,'2024-12-09 09:36:06.799616'), + (9,'3','Как дома',1,'[{"added": {}}]',15,1,'2024-12-09 10:19:47.100883'), + (10,'2','Shelter Golden Hills 4',2,'[{"changed": {"fields": ["\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u043b\u0430\u0433\u0438\u043d\u0430"]}}]',7,1,'2024-12-10 02:47:47.763286'), + (11,'1','Shelter Golden Hills 3',2,'[{"changed": {"fields": ["\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u043b\u0430\u0433\u0438\u043d\u0430"]}}]',7,1,'2024-12-10 02:47:51.978891'), + (12,'2','Shelter Golden Hills 4',2,'[{"changed": {"fields": ["plugin_name"]}}]',7,1,'2024-12-10 03:00:54.761679'), + (13,'2','Shelter Golden Hills 4',2,'[]',7,1,'2024-12-10 03:01:06.448068'), + (14,'3','Как дома / RealtyCalendar',1,'[{"added": {}}]',7,1,'2024-12-10 03:07:04.219584'), + (15,'3','andrew - Как дома',1,'[{"added": {}}]',13,1,'2024-12-10 03:07:13.403477'), + (16,'3','Как дома',2,'[{"changed": {"fields": ["PMS \u0441\u0438\u0441\u0442\u0435\u043c\u0430"]}}]',15,1,'2024-12-10 03:16:16.331411'), + (17,'4','Bnovo',1,'[{"added": {}}]',7,1,'2024-12-10 04:18:40.567772'), + (18,'4','Bnovo',2,'[{"changed": {"fields": ["\u041b\u043e\u0433\u0438\u043d", "\u041f\u0430\u0440\u043e\u043b\u044c"]}}]',7,1,'2024-12-10 04:22:39.182778'), + (19,'4','Test',1,'[{"added": {}}]',15,1,'2024-12-10 04:25:37.535780'), + (20,'4','andrew - Test',1,'[{"added": {}}]',13,1,'2024-12-10 04:25:43.910574'), + (21,'1','bot running',1,'[{"added": {}}]',21,1,'2024-12-10 07:04:41.982258'), + (22,'2','bot.check_pms',1,'[{"added": {}}]',21,1,'2024-12-10 07:07:20.515061'), + (23,'2','bot.check_pms',2,'[{"changed": {"fields": ["\u0424\u0443\u043d\u043a\u0446\u0438\u044f", "\u0410\u043a\u0442\u0438\u0432\u043d\u043e", "\u0414\u043d\u0438 \u043d\u0435\u0434\u0435\u043b\u0438"]}}]',21,1,'2024-12-10 08:14:49.799413'), + (24,'1','bot running',2,'[{"changed": {"fields": ["\u0424\u0443\u043d\u043a\u0446\u0438\u044f", "\u0414\u043d\u0438 \u043d\u0435\u0434\u0435\u043b\u0438", "\u0414\u043d\u0438 \u043d\u0435\u0434\u0435\u043b\u0438"]}}]',21,1,'2024-12-10 08:15:52.093648'), + (25,'1','andrew',2,'[{"changed": {"fields": ["\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d"]}}]',18,1,'2024-12-10 08:35:12.673349'); +INSERT INTO "django_content_type" ("id","app_label","model") VALUES (1,'admin','logentry'), + (2,'auth','permission'), + (3,'auth','group'), + (4,'auth','user'), + (5,'contenttypes','contenttype'), + (6,'sessions','session'), + (7,'pms_integration','pmsconfiguration'), + (8,'pms_integration','pmsintegrationlog'), + (9,'hotels','apiconfiguration'), + (10,'hotels','fraudlog'), + (11,'hotels','guest'), + (12,'hotels','reservation'), + (13,'hotels','userhotel'), + (14,'hotels','apirequestlog'), + (15,'hotels','hotel'), + (16,'users','localuseractivitylog'), + (17,'users','useractivitylog'), + (18,'users','user'), + (19,'users','notificationsettings'), + (20,'users','userconfirmation'), + (21,'scheduler','scheduledtask'); +INSERT INTO "auth_permission" ("id","content_type_id","codename","name") VALUES (1,1,'add_logentry','Can add log entry'), + (2,1,'change_logentry','Can change log entry'), + (3,1,'delete_logentry','Can delete log entry'), + (4,1,'view_logentry','Can view log entry'), + (5,2,'add_permission','Can add permission'), + (6,2,'change_permission','Can change permission'), + (7,2,'delete_permission','Can delete permission'), + (8,2,'view_permission','Can view permission'), + (9,3,'add_group','Can add group'), + (10,3,'change_group','Can change group'), + (11,3,'delete_group','Can delete group'), + (12,3,'view_group','Can view group'), + (13,4,'add_user','Can add user'), + (14,4,'change_user','Can change user'), + (15,4,'delete_user','Can delete user'), + (16,4,'view_user','Can view user'), + (17,5,'add_contenttype','Can add content type'), + (18,5,'change_contenttype','Can change content type'), + (19,5,'delete_contenttype','Can delete content type'), + (20,5,'view_contenttype','Can view content type'), + (21,6,'add_session','Can add session'), + (22,6,'change_session','Can change session'), + (23,6,'delete_session','Can delete session'), + (24,6,'view_session','Can view session'), + (25,7,'add_pmsconfiguration','Can add PMS система'), + (26,7,'change_pmsconfiguration','Can change PMS система'), + (27,7,'delete_pmsconfiguration','Can delete PMS система'), + (28,7,'view_pmsconfiguration','Can view PMS система'), + (29,8,'add_pmsintegrationlog','Can add Журнал интеграции PMS'), + (30,8,'change_pmsintegrationlog','Can change Журнал интеграции PMS'), + (31,8,'delete_pmsintegrationlog','Can delete Журнал интеграции PMS'), + (32,8,'view_pmsintegrationlog','Can view Журнал интеграции PMS'), + (33,9,'add_apiconfiguration','Can add Конфигурация API'), + (34,9,'change_apiconfiguration','Can change Конфигурация API'), + (35,9,'delete_apiconfiguration','Can delete Конфигурация API'), + (36,9,'view_apiconfiguration','Can view Конфигурация API'), + (37,10,'add_fraudlog','Can add Журнал мошенничества'), + (38,10,'change_fraudlog','Can change Журнал мошенничества'), + (39,10,'delete_fraudlog','Can delete Журнал мошенничества'), + (40,10,'view_fraudlog','Can view Журнал мошенничества'), + (41,11,'add_guest','Can add Гость'), + (42,11,'change_guest','Can change Гость'), + (43,11,'delete_guest','Can delete Гость'), + (44,11,'view_guest','Can view Гость'), + (45,12,'add_reservation','Can add Бронирование'), + (46,12,'change_reservation','Can change Бронирование'), + (47,12,'delete_reservation','Can delete Бронирование'), + (48,12,'view_reservation','Can view Бронирование'), + (49,13,'add_userhotel','Can add Пользователь отеля'), + (50,13,'change_userhotel','Can change Пользователь отеля'), + (51,13,'delete_userhotel','Can delete Пользователь отеля'), + (52,13,'view_userhotel','Can view Пользователь отеля'), + (53,14,'add_apirequestlog','Can add Журнал запросов API'), + (54,14,'change_apirequestlog','Can change Журнал запросов API'), + (55,14,'delete_apirequestlog','Can delete Журнал запросов API'), + (56,14,'view_apirequestlog','Can view Журнал запросов API'), + (57,15,'add_hotel','Can add Отель'), + (58,15,'change_hotel','Can change Отель'), + (59,15,'delete_hotel','Can delete Отель'), + (60,15,'view_hotel','Can view Отель'), + (61,16,'add_localuseractivitylog','Can add local user activity log'), + (62,16,'change_localuseractivitylog','Can change local user activity log'), + (63,16,'delete_localuseractivitylog','Can delete local user activity log'), + (64,16,'view_localuseractivitylog','Can view local user activity log'), + (65,17,'add_useractivitylog','Can add Журнал активности'), + (66,17,'change_useractivitylog','Can change Журнал активности'), + (67,17,'delete_useractivitylog','Can delete Журнал активности'), + (68,17,'view_useractivitylog','Can view Журнал активности'), + (69,18,'add_user','Can add Пользователь'), + (70,18,'change_user','Can change Пользователь'), + (71,18,'delete_user','Can delete Пользователь'), + (72,18,'view_user','Can view Пользователь'), + (73,19,'add_notificationsettings','Can add Способ оповещения'), + (74,19,'change_notificationsettings','Can change Способ оповещения'), + (75,19,'delete_notificationsettings','Can delete Способ оповещения'), + (76,19,'view_notificationsettings','Can view Способ оповещения'), + (77,20,'add_userconfirmation','Can add Подтверждение пользователя'), + (78,20,'change_userconfirmation','Can change Подтверждение пользователя'), + (79,20,'delete_userconfirmation','Can delete Подтверждение пользователя'), + (80,20,'view_userconfirmation','Can view Подтверждение пользователя'), + (81,21,'add_scheduledtask','Can add Задача'), + (82,21,'change_scheduledtask','Can change Задача'), + (83,21,'delete_scheduledtask','Can delete Задача'), + (84,21,'view_scheduledtask','Can view Задача'); +INSERT INTO "auth_user" ("id","password","last_login","is_superuser","username","last_name","email","is_staff","is_active","date_joined","first_name") VALUES (1,'pbkdf2_sha256$870000$0tWRKvUavKHjKmwWjWsfYc$mfqBdzr5TB74K1f9OHCI3w/66VZE7vY53MEpgUT73/4=','2024-12-10 06:59:08.529292',1,'trevor1985','','shadow85@list.ru',1,1,'2024-12-09 09:30:44.551380',''); +INSERT INTO "users_user" ("id","password","last_login","is_superuser","username","first_name","last_name","email","is_staff","is_active","date_joined","telegram_id","chat_id","role","confirmed") VALUES (1,'Andrey K. Tsoy','2024-12-09 09:34:41',1,'andrew','','','',0,1,'2024-12-09 09:34:27',556399210,556399210,'hotel_user',1); +INSERT INTO "users_notificationsettings" ("id","telegram_enabled","email_enabled","email","notification_time","user_id") VALUES (1,0,1,'a.choi@smartsoltech.kr','09:00:00',1); +INSERT INTO "hotels_hotel" ("id","name","created_at","api_id","pms_id") VALUES (1,'Golden Hills 3','2024-12-09 09:34:11.463934',NULL,1), + (2,'Golden Hills 4','2024-12-09 09:34:21.782571',NULL,2), + (3,'Как дома','2024-12-09 10:19:47.095457',NULL,3), + (4,'Test','2024-12-10 04:25:37.534927',NULL,4); +INSERT INTO "pms_integration_pmsintegrationlog" ("id","checked_at","status","message","hotel_id") VALUES (1,'2024-12-09 09:36:43.044421','error','Плагин для PMS Shelter PMS не найден.',2), + (2,'2024-12-09 09:38:09.595349','error','Плагин для PMS Shelter PMS не найден.',1), + (3,'2024-12-09 09:40:38.721678','error','Плагин для PMS Shelter PMS не найден.',2), + (4,'2024-12-09 10:07:44.968856','error','Плагин для PMS Shelter PMS не найден.',1); +INSERT INTO "hotels_userhotel" ("id","hotel_id","user_id") VALUES (1,1,1), + (2,2,1), + (3,3,1), + (4,4,1); +INSERT INTO "django_session" ("session_key","session_data","expire_date") VALUES ('wjcybubh02eoyth1e4pm9cgjc8c9rk1v','.eJxVjEEOwiAQRe_C2hBgpgVcuvcMZIBBqoYmpV0Z765NutDtf-_9lwi0rTVsnZcwZXEWWpx-t0jpwW0H-U7tNss0t3WZotwVedAur3Pm5-Vw_w4q9fqthxwtITBY9MlGtJ6ADZiiAdyQ7KhVyVgcGY7gEL1BVqMhH51iNl68P9F6N0w:1tKa6w:zJT5PgcESbBBG5gKuJsyfV6EOxizdevxDzI4QrGbLsc','2024-12-23 09:31:26.682056'), + ('zhfmhutrq2o277smded60dpbxurhyqrv','.eJxVjEEOwiAQRe_C2hBgpgVcuvcMZIBBqoYmpV0Z765NutDtf-_9lwi0rTVsnZcwZXEWWpx-t0jpwW0H-U7tNss0t3WZotwVedAur3Pm5-Vw_w4q9fqthxwtITBY9MlGtJ6ADZiiAdyQ7KhVyVgcGY7gEL1BVqMhH51iNl68P9F6N0w:1tKpiq:sIJzaN8dmZGRQlRUklQ9SNyhbyMuSLYMj-62RXTX5no','2024-12-24 02:11:36.624898'), + ('mz2w18vdzao572ss2ikr5tgj0w2rrbff','.eJxVjEEOwiAQRe_C2hBgpgVcuvcMZIBBqoYmpV0Z765NutDtf-_9lwi0rTVsnZcwZXEWWpx-t0jpwW0H-U7tNss0t3WZotwVedAur3Pm5-Vw_w4q9fqthxwtITBY9MlGtJ6ADZiiAdyQ7KhVyVgcGY7gEL1BVqMhH51iNl68P9F6N0w:1tKuD6:CESE5WIEOm7OzSRbIws0uyat8L3VjPyjOBTBeK_YzFY','2024-12-24 06:59:08.533787'); +INSERT INTO "pms_integration_pmsconfiguration" ("id","name","url","token","username","password","created_at","plugin_name","private_key","public_key") VALUES (1,'Shelter Golden Hills 3','https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter','A0DD4B7F-0381-4096-B46E-D22F1A571996',NULL,NULL,'2024-12-09 09:32:31.348604','Shelter',NULL,NULL), + (2,'Shelter Golden Hills 4','https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter','A0DD4B7F-0381-4096-B46E-D22F1A571996',NULL,NULL,'2024-12-09 09:33:10.514492','shelter',NULL,NULL), + (3,'Как дома / RealtyCalendar','https://realtycalendar.ru/api/v1/bookings/',NULL,NULL,NULL,'2024-12-10 03:07:04.218499','realtycalendar','a3669a349b9911cf774ec30ba9523582','b95e293cf07c84dfce44ec41bdced96a'), + (4,'Bnovo','https://online.bnovo.ru',NULL,'16798','a46da27476f02d1f','2024-12-10 04:18:40.567212','bnovo',NULL,NULL); +INSERT INTO "scheduler_scheduledtask" ("id","last_run","days","hours","minutes","months","active","task_name","function_path","weekdays") VALUES (1,NULL,'*','*','*','*',1,'bot running','bot.utils.bot_setup.setup_bot','[6]'), + (2,NULL,'*','*','*','*',1,'bot.check_pms','manage.main','2'); +COMMIT; diff --git a/hotels/admin.py b/hotels/admin.py index 300dfbb6..8348a062 100644 --- a/hotels/admin.py +++ b/hotels/admin.py @@ -4,9 +4,7 @@ from .models import ( Hotel, UserHotel, APIConfiguration, - APIRequestLog, Reservation, - Guest, FraudLog ) from django.urls import path @@ -69,22 +67,6 @@ class UserHotelAdmin(admin.ModelAdmin): # ordering = ('-hotel',) -@admin.register(APIConfiguration) -class ApiConfigurationAdmin(admin.ModelAdmin): - list_display = ('name', 'url', 'token', 'username', 'password', 'last_updated') - search_fields = ('name', 'url', 'token', 'username') - list_filter = ('last_updated',) - ordering = ('-last_updated',) - - -@admin.register(APIRequestLog) -class ApiRequestLogAdmin(admin.ModelAdmin): - list_display = ('api', 'request_time', 'response_status', 'response_data') - search_fields = ('api__name', 'request_time', 'response_status') - list_filter = ('api', 'response_status', 'request_time') - ordering = ('-request_time',) - - @admin.register(Reservation) class ReservationAdmin(admin.ModelAdmin): list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount') @@ -93,9 +75,3 @@ class ReservationAdmin(admin.ModelAdmin): ordering = ('-check_in',) -@admin.register(Guest) -class GuestAdmin(admin.ModelAdmin): - list_display = ('reservation', 'name', 'birthdate', 'phone', 'email') - search_fields = ('reservation__reservation_id', 'name', 'phone', 'email') - list_filter = ('reservation',) - ordering = ('-reservation',) diff --git a/hotels/booking_analyzer.py b/hotels/booking_analyzer.py deleted file mode 100644 index c7cc64da..00000000 --- a/hotels/booking_analyzer.py +++ /dev/null @@ -1,33 +0,0 @@ -import json -import pandas as pd - -# Load the JSON file -file_path = '../../modules/analyzed_9.json' -with open(file_path, 'r', encoding='utf-8') as file: - data = json.load(file) - -# Process the data into a structured format -reservations = [] -for booking in data: - for guest in booking.get("guests", []): - reservations.append({ - "Reservation ID": booking.get("id"), - "Hotel Name": booking.get("hotelName"), - "Room Number": booking.get("roomNumber"), - "Room Type": booking.get("roomTypeName"), - "Check-in": booking.get("from"), - "Check-out": booking.get("until"), - "Status": booking.get("checkInStatus"), - "Price": booking.get("reservationPrice"), - "Discount": booking.get("discount"), - "Guest Name": f"{guest.get('lastName', '')} {guest.get('firstName', '')} {guest.get('middleName', '')}".strip(), - "Guest Birthdate": guest.get("birthDate"), - "Guest Phone": guest.get("phone"), - "Guest Email": guest.get("email"), - }) - -# Convert to DataFrame for better visualization -df_reservations = pd.DataFrame(reservations) - -# Display the structured data -import ace_tools as tools; tools.display_dataframe_to_user(name="Structured Reservations Data", dataframe=df_reservations) diff --git a/manage.py b/manage.py index 850eae6b..915feb02 100755 --- a/manage.py +++ b/manage.py @@ -2,6 +2,17 @@ """Django's command-line utility for administrative tasks.""" import os import sys +import logging + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, # Уровень логирования (можно DEBUG для полной информации) + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("bot.log"), # Логи будут записываться в файл bot.log + logging.StreamHandler() # Логи также будут отображаться в консоли + ] +) def main(): diff --git a/pms_integration/admin.py b/pms_integration/admin.py index ccbcf45a..ff70a6b5 100644 --- a/pms_integration/admin.py +++ b/pms_integration/admin.py @@ -8,53 +8,34 @@ from django.shortcuts import render from django import forms from pms_integration.models import PMSConfiguration, PMSIntegrationLog + class PMSConfigurationForm(forms.ModelForm): class Meta: model = PMSConfiguration - fields = "__all__" + fields = '__all__' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # Загружаем доступные плагины plugins = PluginLoader.load_plugins() - self.fields['plugin_name'].choices = [(plugin, plugin) for plugin in plugins.keys()] + plugin_choices = [(plugin_name, plugin_name) for plugin_name in plugins.keys()] + self.fields['plugin_name'] = forms.ChoiceField(choices=plugin_choices, required=False) @admin.register(PMSConfiguration) class PMSConfigurationAdmin(admin.ModelAdmin): form = PMSConfigurationForm - list_display = ('name', 'plugin_name', 'created_at', 'check_plugins_button') - search_fields = ('name', 'description') - list_filter = ('created_at',) + list_display = ('name', 'plugin_name', 'created_at') + search_fields = ('name', 'plugin_name') ordering = ('-created_at',) - def get_urls(self): - """Добавляем URL для проверки плагинов.""" - urls = super().get_urls() - custom_urls = [ - path("check-plugins/", self.check_plugins, name="check-plugins"), - ] - return custom_urls + urls - - def check_plugins(self, request): - """Проверка и отображение плагинов.""" + def save_model(self, request, obj, form, change): + # Проверка на наличие плагина plugins = PluginLoader.load_plugins() - plugin_details = [ - {"name": plugin_name, "doc": plugins[plugin_name].__doc__ or "Нет документации"} - for plugin_name in plugins - ] - context = { - "title": "Проверка плагинов", - "plugin_details": plugin_details, - } - return render(request, "admin/check_plugins.html", context) - - def check_plugins_button(self, obj): - """Добавляем кнопку для проверки плагинов.""" - return format_html( - 'Проверить плагины', - "/admin/pms_integration/pmsconfiguration/check-plugins/", - ) - check_plugins_button.short_description = "Проверить плагины" - + if obj.plugin_name and obj.plugin_name not in plugins.keys(): + raise ValueError(f"Выберите корректный плагин. '{obj.plugin_name}' нет среди допустимых значений.") + super().save_model(request, obj, form, change) + + @admin.register(PMSIntegrationLog) class PMSIntegrationLogAdmin(admin.ModelAdmin): list_display = ('hotel', 'checked_at', 'status', 'message') diff --git a/pms_integration/forms.py b/pms_integration/forms.py index 6d052da7..e69de29b 100644 --- a/pms_integration/forms.py +++ b/pms_integration/forms.py @@ -1,13 +0,0 @@ -from django import forms -from .models import PMSConfiguration -from .manager import PluginLoader - -class PMSConfigurationForm(forms.ModelForm): - class Meta: - model = PMSConfiguration - fields = "__all__" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - plugins = PluginLoader.load_plugins() - self.fields['plugin_name'].choices = [(plugin, plugin) for plugin in plugins.keys()] \ No newline at end of file diff --git a/pms_integration/manager.py b/pms_integration/manager.py index c37813b3..6c2823a0 100644 --- a/pms_integration/manager.py +++ b/pms_integration/manager.py @@ -7,27 +7,31 @@ from asgiref.sync import sync_to_async class PluginLoader: PLUGIN_PATH = Path(__file__).parent / "plugins" - print("Путь к папке плагинов:", PLUGIN_PATH.resolve()) - print("Содержимое папки:", list(PLUGIN_PATH.iterdir())) + @staticmethod def load_plugins(): plugins = {} + if not PluginLoader.PLUGIN_PATH.exists(): + print("Папка с плагинами не существует:", PluginLoader.PLUGIN_PATH) + return plugins + + print("Загрузка плагинов:") for file in os.listdir(PluginLoader.PLUGIN_PATH): if file.endswith("_pms.py") and not file.startswith("__"): + print(f" Plugin {file}") module_name = f"pms_integration.plugins.{file[:-3]}" try: module = importlib.import_module(module_name) for attr in dir(module): cls = getattr(module, attr) if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin: - plugins[cls.__name__] = cls - print(f"Загружен плагин: {cls.__name__}") + plugin_name = file[:-7] # Убираем `_pms` из имени файла + print(f" Загружен плагин {plugin_name}: {cls.__name__}") + plugins[plugin_name] = cls except Exception as e: - print(f"Ошибка при загрузке модуля {module_name}: {e}") - print(f"Итоговый список плагинов: {list(plugins.keys())}") + print(f" Ошибка загрузки плагина {module_name}: {e}") return plugins - class PMSIntegrationManager: def __init__(self, hotel_id): self.hotel_id = hotel_id diff --git a/pms_integration/migrations/0002_alter_pmsconfiguration_plugin_name.py b/pms_integration/migrations/0002_alter_pmsconfiguration_plugin_name.py new file mode 100644 index 00000000..014b8df1 --- /dev/null +++ b/pms_integration/migrations/0002_alter_pmsconfiguration_plugin_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2024-12-10 02:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pms_integration', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='pmsconfiguration', + name='plugin_name', + field=models.CharField(blank=True, choices=[], max_length=255, null=True), + ), + ] diff --git a/pms_integration/migrations/0003_alter_pmsconfiguration_plugin_name.py b/pms_integration/migrations/0003_alter_pmsconfiguration_plugin_name.py new file mode 100644 index 00000000..9335ab81 --- /dev/null +++ b/pms_integration/migrations/0003_alter_pmsconfiguration_plugin_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2024-12-10 02:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pms_integration', '0002_alter_pmsconfiguration_plugin_name'), + ] + + operations = [ + migrations.AlterField( + model_name='pmsconfiguration', + name='plugin_name', + field=models.CharField(blank=True, choices=[], max_length=255, null=True, verbose_name='Плагин'), + ), + ] diff --git a/pms_integration/migrations/0004_alter_pmsconfiguration_plugin_name.py b/pms_integration/migrations/0004_alter_pmsconfiguration_plugin_name.py new file mode 100644 index 00000000..eb3629b8 --- /dev/null +++ b/pms_integration/migrations/0004_alter_pmsconfiguration_plugin_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2024-12-10 03:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pms_integration', '0003_alter_pmsconfiguration_plugin_name'), + ] + + operations = [ + migrations.AlterField( + model_name='pmsconfiguration', + name='plugin_name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Плагин'), + ), + ] diff --git a/pms_integration/migrations/0005_pmsconfiguration_private_key_and_more.py b/pms_integration/migrations/0005_pmsconfiguration_private_key_and_more.py new file mode 100644 index 00000000..dd9fe759 --- /dev/null +++ b/pms_integration/migrations/0005_pmsconfiguration_private_key_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.4 on 2024-12-10 03:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pms_integration', '0004_alter_pmsconfiguration_plugin_name'), + ] + + operations = [ + migrations.AddField( + model_name='pmsconfiguration', + name='private_key', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Приватный ключ'), + ), + migrations.AddField( + model_name='pmsconfiguration', + name='public_key', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Публичный ключ'), + ), + ] diff --git a/pms_integration/models.py b/pms_integration/models.py index 5083d409..63636bb0 100644 --- a/pms_integration/models.py +++ b/pms_integration/models.py @@ -8,10 +8,12 @@ class PMSConfiguration(models.Model): name = models.CharField(max_length=255, verbose_name="Название PMS") url = models.URLField(verbose_name="URL API") token = models.CharField(max_length=255, blank=True, null=True, verbose_name="Токен") + public_key = models.CharField(max_length=255, blank=True, null=True, verbose_name="Публичный ключ") + private_key = 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="Пароль") - plugin_name = models.CharField(max_length=255, verbose_name="Название плагина") - created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") # Добавлено поле + plugin_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Плагин") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") def __str__(self): diff --git a/pms_integration/plugins/bnovo_pms.py b/pms_integration/plugins/bnovo_pms.py index 89374bee..1d3f32e2 100644 --- a/pms_integration/plugins/bnovo_pms.py +++ b/pms_integration/plugins/bnovo_pms.py @@ -1,71 +1,136 @@ import requests +import json +from datetime import datetime, timedelta from .base_plugin import BasePMSPlugin +from asgiref.sync import sync_to_async +from pms_integration.models import PMSConfiguration # Убедитесь, что модель существует -class BnovoPMS(BasePMSPlugin): - """ - Плагин для интеграции с Bnovo. - """ - json_schema = { - "type": "object", - "properties": { - "id": {"type": "integer"}, - "number": {"type": "integer"}, - "roomTypeName": {"type": "string"}, - "checkInStatus": {"type": "string"}, - "guests": {"type": "array"}, - }, - "required": ["id", "number", "roomTypeName", "checkInStatus", "guests"] - } +class BnovoPMSPlugin(BasePMSPlugin): + """Плагин для работы с PMS Bnovo.""" + + def __init__(self, config): + super().__init__(config) + self.api_url = config.url.rstrip("/") # Убираем лишний `/` в конце URL + self.username = config.username + self.password = config.password + self.token = None # SID + + if not self.api_url: + raise ValueError("Не указан URL для работы плагина.") + if not self.username or not self.password: + raise ValueError("Не указаны логин или пароль для авторизации.") + def get_default_parser_settings(self): - """ - Возвращает настройки парсера по умолчанию. - """ + """Возвращает настройки по умолчанию для обработки данных.""" return { - "field_mapping": { - "room_name": "roomNumber", - "check_in": "from", - "check_out": "until", - }, - "date_format": "%Y-%m-%dT%H:%M:%S" + "date_format": "%Y-%m-%dT%H:%M:%S", + "timezone": "UTC" } - def fetch_data(self): - response = requests.get(self.pms_config.url, headers={"Authorization": f"Bearer {self.pms_config.token}"}) - response.raise_for_status() - data = response.json() - # Проверка структуры - expected_fields = self.pms_config.parser_settings.get("fields_mapping", {}) - for field in expected_fields.values(): - if field not in data[0]: # Проверяем первую запись - raise ValueError(f"Поле {field} отсутствует в ответе API.") + async def _save_token_to_db(self, sid): + """Сохраняет токен (SID) в базу данных.""" + try: + await sync_to_async(PMSConfiguration.objects.update_or_create)( + plugin_name="bnovo", + defaults={"token": sid} + ) + print(f"[DEBUG] Токен сохранен в БД: {sid}") + except Exception as e: + print(f"[ERROR] Ошибка сохранения токена в БД: {e}") - return data - - def fetch_and_parse(self): - response = requests.get( - self.pms_config.url, - headers={"Authorization": f"Bearer {self.pms_config.token}"} - ) - self.validate_response(response) # Проверка соответствия структуры - if response.status_code != 200: - raise ValueError(f"Ошибка запроса к PMS Bnovo: {response.text}") + def _get_auth_headers(self): + """Создает заголовки авторизации.""" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + if self.token: + headers["Cookie"] = f"SID={self.token}" + return headers - data = response.json() - parsed_data = self.parse_data(data) - return parsed_data + async def _fetch_session(self): + """Получает идентификатор сессии (SID) через запрос.""" + url = f"{self.api_url}/" + payload = { + "username": self.username, + "password": self.password, + } - def parse_data(self, data): - # Пример разбора данных на основе JSON-маски - reservations = [] - for item in data["reservations"]: - reservation = { - "id": item["id"], - "room_number": item["roomNumber"], - "check_in": item["checkIn"], - "check_out": item["checkOut"], - "status": item["status"], - } - reservations.append(reservation) - return reservations - + print(f"[DEBUG] URL авторизации: {url}") + print(f"[DEBUG] Тело запроса: {json.dumps(payload, indent=2)}") + headers = self._get_auth_headers() + + session = requests.Session() + response = session.post(url, json=payload, headers=headers, allow_redirects=False) + + print(f"[DEBUG] Статус ответа: {response.status_code}") + print(f"[DEBUG] Ответ заголовков: {response.headers}") + print(f"[DEBUG] Cookies: {session.cookies}") + + if response.status_code == 302 and "SID" in session.cookies: + sid = session.cookies.get("SID") + self.token = sid + print(f"[DEBUG] Получен SID: {sid}") + + # Правильное сохранение в БД через sync_to_async + try: + await self._save_token_to_db(sid) + print(f"[DEBUG] Токен сохранен в БД") + except Exception as e: + print(f"[ERROR] Ошибка сохранения токена в БД: {e}") + else: + raise ValueError(f"Не удалось получить SID из ответа: {response.text}") + + async def _fetch_data(self): + """Получает данные о бронированиях с помощью эндпоинта `/dashboard`.""" + await self._fetch_session() # Авторизуемся перед каждым запросом + + now = datetime.now() + create_from = (now - timedelta(days=90)).strftime("%d.%m.%Y") # Диапазон: последние 90 дней + create_to = now.strftime("%d.%m.%Y") + + params = { + "create_from": create_from, + "create_to": create_to, + "status_ids": "1", + "advanced_search": 2, # Обязательный параметр + "c": 100, # Количество элементов на странице (максимум 100) + "page": 1, # Начальная страница + "order_by": "create_date.asc", # Сортировка по возрастанию даты создания + } + + headers = self._get_auth_headers() + + all_bookings = [] # Для сохранения всех бронирований + while True: + print(f"[DEBUG] Запрос к /dashboard с параметрами: {json.dumps(params, indent=2)}") + response = requests.get(f"{self.api_url}/dashboard", headers=headers, params=params) + + print(f"[DEBUG] Статус ответа: {response.status_code}") + if response.status_code != 200: + raise ValueError(f"Ошибка при получении данных: {response.status_code}, {response.text}") + + data = response.json() + print(json.dumps(data, indent=2)) + bookings = data.get("bookings", []) + all_bookings.extend(bookings) + + print(f"[DEBUG] Получено бронирований: {len(bookings)}") + print(f"[DEBUG] Всего бронирований: {len(all_bookings)}") + + # Проверка на наличие следующей страницы + pages_info = data.get("pages", {}) + current_page = pages_info.get("current_page", 1) + total_pages = pages_info.get("total_pages", 1) + + if current_page >= total_pages: + break # Все страницы загружены + + params["page"] += 1 # Переход на следующую страницу + + if not all_bookings: + print("[DEBUG] Нет бронирований за указанный период.") + else: + print(f"[DEBUG] Полученные бронирования: {json.dumps(all_bookings, indent=2)}") + return all_bookings \ No newline at end of file diff --git a/pms_integration/plugins/realtycalendar_pms.py b/pms_integration/plugins/realtycalendar_pms.py index f7316142..f9176617 100644 --- a/pms_integration/plugins/realtycalendar_pms.py +++ b/pms_integration/plugins/realtycalendar_pms.py @@ -1,78 +1,254 @@ -import hashlib -import requests -import json -from datetime import datetime -from hotels.models import Reservation +# import requests +# import hashlib +# import json +# from .base_plugin import BasePMSPlugin +# from datetime import datetime, timedelta +# from asgiref.sync import sync_to_async -from pms_integration.plugins.base_plugin import BasePMSPlugin + +# class RealtyCalendarPlugin(BasePMSPlugin): +# """Плагин для импорта данных из системы RealtyCalendar +# """ +# def __init__(self, config): +# super().__init__(config) +# self.public_key = config.public_key +# self.private_key = config.private_key +# self.api_url = config.url.rstrip("/") # Убираем лишний `/` в конце URL + +# if not self.public_key or not self.private_key: +# raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar") +# def get_default_parser_settings(self): +# """ +# Возвращает настройки по умолчанию для обработки данных. +# """ +# return { +# "date_format": "%Y-%m-%dT%H:%M:%S", +# "timezone": "UTC" +# } +# def _get_sorted_keys(self, obj): +# """ +# Возвращает отсортированный по имени список ключей. +# """ +# return sorted(list(obj.keys())) + +# def _generate_data_string(self, obj): +# """ +# Формирует строку параметров для подписи. +# """ +# sorted_keys = self._get_sorted_keys(obj) +# string = "".join(f"{key}={obj[key]}" for key in sorted_keys) +# return string + self.private_key + +# def _generate_md5(self, string): +# """ +# Генерирует MD5-хеш от строки. +# """ +# return hashlib.md5(string.encode("utf-8")).hexdigest() + +# def _generate_sign(self, data): +# """ +# Генерирует подпись для данных запроса. +# """ +# data_string = self._generate_data_string(data) +# return self._generate_md5(data_string) + +# def fetch_data(self): +# """ +# Выполняет запрос к API RealtyCalendar для получения данных о бронированиях. +# """ +# base_url = f"https://realtycalendar.ru/api/v1/bookings/{self.public_key}/" +# headers = { +# "Accept": "application/json", +# "Content-Type": "application/json", +# } + +# # Определяем даты выборки +# now = datetime.now() +# data = { +# "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"), +# "end_date": now.strftime("%Y-%m-%d"), +# } + +# # Генерация подписи +# data["sign"] = self._generate_sign(data) + +# # Отправляем запрос +# print(f"URL запроса: {base_url}") +# print(f"Заголовки: {headers}") +# print(f"Данные запроса: {data}") + +# response = requests.post(url=base_url, headers=headers, json=data) + +# # Логируем результат +# print(f"Статус ответа: {response.status_code}") +# print(f"Ответ: {response.text}") + +# # Проверяем успешность запроса +# if response.status_code == 200: +# return response.json().get("bookings", []) +# else: +# raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}, {response.text}") + +# async def _save_to_db(self, data, hotel_id): +# """ +# Сохраняет данные о бронированиях в базу данных. +# """ +# from hotels.models import Reservation, Hotel + +# hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) +# for item in data: +# try: +# reservation, created = await sync_to_async(Reservation.objects.update_or_create)( +# reservation_id=item["id"], +# hotel=hotel, +# defaults={ +# "room_number": item.get("apartment_id", ""), # ID квартиры +# "check_in": datetime.strptime(item["begin_date"], "%Y-%m-%d"), # Дата заезда +# "check_out": datetime.strptime(item["end_date"], "%Y-%m-%d"), # Дата выезда +# "status": item.get("status", ""), # Статус бронирования +# "price": item.get("amount", 0), # Сумма оплаты +# "client_name": item["client"].get("fio", ""), # Имя клиента +# "client_email": item["client"].get("email", ""), # Email клиента +# "client_phone": item["client"].get("phone", ""), # Телефон клиента +# } +# ) +# print(f"{'Создана' if created else 'Обновлена'} запись: {reservation}") +# except Exception as e: +# print(f"Ошибка при сохранении бронирования ID {item['id']}: {e}") + + +import requests +import hashlib +import json +from .base_plugin import BasePMSPlugin +from datetime import datetime, timedelta +from asgiref.sync import sync_to_async class RealtyCalendarPlugin(BasePMSPlugin): - """ - Плагин для взаимодействия с RealtyCalendar. + """Плагин для импорта данных из системы RealtyCalendar """ def __init__(self, config): super().__init__(config) - self.public_key = config.token # Используем `token` как публичный ключ - self.private_key = config.password # Используем `password` как приватный ключ - self.base_url = config.url + self.public_key = config.public_key + self.private_key = config.private_key + self.api_url = config.url.rstrip("/") - def generate_sign(self, params): - """ - Генерация подписи запроса. - :param params: Параметры запроса. - :return: Подпись. - """ - sorted_keys = sorted(params.keys()) - data_string = ''.join(f"{key}={params[key]}" for key in sorted_keys) - sign_string = f"{data_string}{self.private_key}" - return hashlib.md5(sign_string.encode('utf-8')).hexdigest() + if not self.public_key or not self.private_key: + raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar") - def fetch_data(self, start_date=None, end_date=None): + def get_default_parser_settings(self): """ - Получение данных из RealtyCalendar. - :param start_date: Начальная дата (формат YYYY-MM-DD). - :param end_date: Конечная дата (формат YYYY-MM-DD). - :return: Список данных бронирования. + Возвращает настройки по умолчанию для обработки данных. """ - if not start_date: - start_date = datetime.now().strftime('%Y-%m-%d') - if not end_date: - end_date = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d') - - params = { - 'begin_date': start_date, - 'end_date': end_date, + return { + "date_format": "%Y-%m-%dT%H:%M:%S", + "timezone": "UTC" } - params['sign'] = self.generate_sign(params) - url = f"{self.base_url}/bookings/{self.public_key}/" + def _get_sorted_keys(self, obj): + """ + Возвращает отсортированный по имени список ключей. + """ + sorted_keys = sorted(list(obj.keys())) + print(f"[DEBUG] Отсортированные ключи: {sorted_keys}") + return sorted_keys + + def _generate_data_string(self, obj): + """ + Формирует строку параметров для подписи. + """ + sorted_keys = self._get_sorted_keys(obj) + string = "".join(f"{key}={obj[key]}" for key in sorted_keys) + print(f"[DEBUG] Сформированная строка данных: {string}") + return string + self.private_key + + def _generate_md5(self, string): + """ + Генерирует MD5-хеш от строки. + """ + md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest() + print(f"[DEBUG] Сформированный MD5-хеш: {md5_hash}") + return md5_hash + + def _generate_sign(self, data): + """ + Генерирует подпись для данных запроса. + """ + data_string = self._generate_data_string(data) + print(f"[DEBUG] Строка для подписи: {data_string}") + sign = self._generate_md5(data_string) + print(f"[DEBUG] Подпись: {sign}") + return sign + + def _fetch_data(self): + """ + Выполняет запрос к API RealtyCalendar для получения данных о бронированиях. + """ + base_url = f"https://realtycalendar.ru/api/v1/bookings/{self.public_key}/" headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', + "Accept": "application/json", + "Content-Type": "application/json", } - response = requests.post(url, json=params, headers=headers) - response.raise_for_status() + # Определяем даты выборки + now = datetime.now() + data = { + "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"), + "end_date": now.strftime("%Y-%m-%d"), + } - data = response.json() - return data.get('bookings', []) + print(f"[DEBUG] Даты выборки: {data}") - @staticmethod - def save_data(bookings): + # Генерация подписи + data["sign"] = self._generate_sign(data) + + # Отправляем запрос + print(f"[DEBUG] URL запроса: {base_url}") + print(f"[DEBUG] Заголовки: {headers}") + print(f"[DEBUG] Данные запроса: {data}") + + response = requests.post(url=base_url, headers=headers, json=data) + + # Логируем результат + print(f"[DEBUG] Статус ответа: {response.status_code}") + print(f"[DEBUG] Ответ: {response.text}") + + # Проверяем успешность запроса + if response.status_code == 200: + bookings = response.json().get("bookings", []) + print(f"[DEBUG] Полученные данные бронирований: {bookings}") + return bookings + else: + raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}, {response.text}") + + + async def _save_to_db(self, data, hotel_id): """ - Сохранение данных бронирования в базу данных. - :param bookings: Список бронирований. + Сохраняет данные о бронированиях в базу данных. """ - for booking in bookings: - Reservation.objects.update_or_create( - external_id=booking['id'], - defaults={ - 'check_in': booking['begin_date'], - 'check_out': booking['end_date'], - 'amount': booking['amount'], - 'notes': booking.get('notes', ''), - 'guest_name': booking['client']['fio'], - 'guest_phone': booking['client']['phone'], - }, - ) + from hotels.models import Reservation, Hotel + + hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) + print(f"[DEBUG] Загружен отель: {hotel.name}") + + for item in data: + print(f"[DEBUG] Обработка бронирования: {item}") + try: + reservation, created = await sync_to_async(Reservation.objects.update_or_create)( + reservation_id=item["id"], + hotel=hotel, + defaults={ + "room_number": item.get("apartment_id", ""), # ID квартиры + "check_in": datetime.strptime(item["begin_date"], "%Y-%m-%d"), # Дата заезда + "check_out": datetime.strptime(item["end_date"], "%Y-%m-%d"), # Дата выезда + "status": item.get("status", ""), # Статус бронирования + "price": item.get("amount", 0), # Сумма оплаты + "client_name": item["client"].get("fio", ""), # Имя клиента + "client_email": item["client"].get("email", ""), # Email клиента + "client_phone": item["client"].get("phone", ""), # Телефон клиента + } + ) + print(f"[DEBUG] {'Создана' if created else 'Обновлена'} запись: {reservation}") + except Exception as e: + print(f"[DEBUG] Ошибка при сохранении бронирования ID {item['id']}: {e}") diff --git a/pms_integration/plugins/shelter_pms.py b/pms_integration/plugins/shelter_pms.py index 22aad21a..98c04aca 100644 --- a/pms_integration/plugins/shelter_pms.py +++ b/pms_integration/plugins/shelter_pms.py @@ -8,6 +8,9 @@ from hotels.models import Hotel class Shelter(BasePMSPlugin): + """ + Плагин для PMS Shelter Coud. + """ def __init__(self, config): super().__init__(config) self.token = config.token diff --git a/pms_integration/test_requests.py b/pms_integration/test_requests.py new file mode 100644 index 00000000..d15fd51c --- /dev/null +++ b/pms_integration/test_requests.py @@ -0,0 +1,73 @@ +# import requests +# import json + +# # Функция авторизации +# def authorize(username, password): +# url = "https://online.bnovo.ru/" +# headers = { +# "accept": "application/json", +# "Content-Type": "application/json" +# } +# payload = { +# "username": username, +# "password": password +# } + +# response = requests.post(url, headers=headers, json=payload, allow_redirects=False) +# print(f"[DEBUG] Статус авторизации: {response.status_code}") +# print(f"[DEBUG] Заголовки ответа: {response.headers}") + +# if response.status_code == 302 and "SID" in response.cookies: +# sid = response.cookies.get("SID") +# print(f"[DEBUG] Получен SID: {sid}") +# return sid +# else: +# raise ValueError(f"Ошибка авторизации: {response.text}") + +# # Функция получения данных с /dashboard +# def fetch_dashboard(sid, create_from, create_to, status_ids, page=1, count=10): +# url = f"https://online.bnovo.ru/dashboard" +# headers = { +# "accept": "application/json", +# "Cookie": f"SID={sid}" +# } +# params = { +# "create_from": create_from, +# "create_to": create_to, +# "advanced_search": 2, +# "status_ids": status_ids, +# "c": count, +# "page": page, +# "order_by": "create_date.asc" +# } + +# response = requests.get(url, headers=headers, params=params) +# print(f"[DEBUG] Статус запроса: {response.status_code}") +# print(f"[DEBUG] Ответ: {json.dumps(response.json(), indent=2, ensure_ascii=False)}") + +# if response.status_code == 200: +# return response.json() +# else: +# raise ValueError(f"Ошибка при запросе данных: {response.text}") + +# # Тестовый вызов +# try: +# username = "cto@hotelantifraud.ru" +# password = "tD8wC1zP9tiT6mY1" + +# # Авторизация +# sid = authorize(username, password) + +# # Получение бронирований +# bookings = fetch_dashboard( +# sid=sid, +# create_from="25.09.2024", +# create_to="05.10.2024", +# status_ids="1", +# page=1, +# count=10 +# ) +# print(f"[INFO] Полученные бронирования: {json.dumps(bookings, indent=2, ensure_ascii=False)}") + +# except Exception as e: +# print(f"[ERROR] {e}") diff --git a/requirements.txt b/requirements.txt index 037e7ec7..095d2ade 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,12 @@ ace_tools==0.0 +aiohappyeyeballs==2.4.4 +aiohttp==3.11.10 +aiosignal==1.3.1 anyio==4.6.2.post1 APScheduler==3.11.0 asgiref==3.8.1 +async-timeout==5.0.1 +attrs==24.2.0 certifi==2024.8.30 charset-normalizer==3.4.0 Django==5.1.4 @@ -9,26 +14,41 @@ django-filter==24.3 django-jazzmin==3.0.1 django-jet==1.0.8 et_xmlfile==2.0.0 +exceptiongroup==1.2.2 fpdf==1.7.2 +frozenlist==1.5.0 +geoip2==4.8.1 h11==0.14.0 httpcore==1.0.7 httpx==0.28.0 idna==3.10 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +maxminddb==2.6.2 +multidict==6.1.0 numpy==2.1.3 openpyxl==3.1.5 pandas==2.2.3 +pathspec==0.12.1 pillow==11.0.0 +propcache==0.2.1 PyMySQL==1.1.1 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 python-telegram-bot==21.8 pytz==2024.2 PyYAML==6.0.2 +referencing==0.35.1 requests==2.32.3 +rpds-py==0.22.3 six==1.17.0 sniffio==1.3.1 sqlparse==0.5.2 +typing_extensions==4.12.2 tzdata==2024.2 tzlocal==5.2 +ua-parser==1.0.0 +ua-parser-builtins==0.18.0.post1 urllib3==2.2.3 -jsonschema \ No newline at end of file +user-agents==2.2.0 +yarl==1.18.3 diff --git a/scheduler/__init__.py b/scheduler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scheduler/admin.py b/scheduler/admin.py new file mode 100644 index 00000000..8bb0d59f --- /dev/null +++ b/scheduler/admin.py @@ -0,0 +1,59 @@ +from django.contrib import admin +from django import forms +from django.utils.functional import cached_property +from .models import ScheduledTask +from django.templatetags.static import static +from scheduler.utils import get_project_functions + +class CustomAdmin(admin.ModelAdmin): + class Media: + css = {"all": (static("scheduler/admin.css"),)} + js = (static("scheduler/admin.js"),) + +class ScheduledTaskForm(forms.ModelForm): + DAYS_OF_WEEK_CHOICES = [ + (0, "Воскресенье"), + (1, "Понедельник"), + (2, "Вторник"), + (3, "Среда"), + (4, "Четверг"), + (5, "Пятница"), + (6, "Суббота"), + ] + + weekdays = forms.MultipleChoiceField( + choices=DAYS_OF_WEEK_CHOICES, + widget=forms.CheckboxSelectMultiple, + label="Дни недели", + required=False, # Опционально + ) + + class Meta: + model = ScheduledTask + fields = [ + "task_name", + "function_path", + "minutes", + "hours", + "months", + "weekdays", # Используем только поле с галочками + "active", + ] + + def clean_weekdays(self): + """ + Преобразуем список выбранных дней в строку для хранения в базе. + """ + weekdays = self.cleaned_data.get("weekdays", []) + return ",".join(map(str, weekdays)) + +@admin.register(ScheduledTask) +class ScheduledTaskAdmin(admin.ModelAdmin): + form = ScheduledTaskForm + list_display = ("task_name", "function_path", "active", "formatted_last_run") + list_filter = ("active",) + search_fields = ("task_name", "function_path") + + def formatted_last_run(self, obj): + return obj.last_run.strftime("%Y-%m-%d %H:%M:%S") if obj.last_run else "Никогда" + formatted_last_run.short_description = "Последний запуск" \ No newline at end of file diff --git a/scheduler/apps.py b/scheduler/apps.py new file mode 100644 index 00000000..3a3846a7 --- /dev/null +++ b/scheduler/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SchedulerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'scheduler' diff --git a/scheduler/migrations/0001_initial.py b/scheduler/migrations/0001_initial.py new file mode 100644 index 00000000..b67db412 --- /dev/null +++ b/scheduler/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.4 on 2024-12-10 08:38 + +import scheduler.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ScheduledTask', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('task_name', models.CharField(max_length=255, verbose_name='Название задачи')), + ('function_path', models.CharField(choices=scheduler.models.get_available_functions, max_length=500, verbose_name='Путь к функции (модуль.функция)')), + ('minutes', models.CharField(default='*', max_length=255, verbose_name='Минуты')), + ('hours', models.CharField(default='*', max_length=255, verbose_name='Часы')), + ('days', models.CharField(default='*', max_length=255, verbose_name='Дни')), + ('months', models.CharField(default='*', max_length=255, verbose_name='Месяцы')), + ('weekdays', models.JSONField(default=list, verbose_name='Дни недели')), + ('active', models.BooleanField(default=True, verbose_name='Активно')), + ('last_run', models.DateTimeField(blank=True, null=True, verbose_name='Последний запуск')), + ], + ), + ] diff --git a/scheduler/migrations/__init__.py b/scheduler/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scheduler/models.py b/scheduler/models.py new file mode 100644 index 00000000..bb49a1e7 --- /dev/null +++ b/scheduler/models.py @@ -0,0 +1,47 @@ +from django.db import models +from django.utils.timezone import now + + +class ScheduledTask(models.Model): + task_name = models.CharField(max_length=255) + function_path = models.CharField(max_length=255) + minutes = models.CharField(max_length=255) + hours = models.CharField(max_length=255) + months = models.CharField(max_length=255) + weekdays = models.CharField(max_length=100, blank=True, default="") + active = models.BooleanField(default=True) + last_run = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return self.name + + def clean_weekdays(self): + """Приводим список в строку при сохранении.""" + if isinstance(self.weekdays, list): + self.weekdays = ",".join(map(str, self.weekdays)) + + class Meta: + verbose_name = "Запланированная задача" + verbose_name_plural = "Запланированные задачи" + +def get_available_functions(): + from scheduler.utils import get_project_functions + return [(path, name) for path, name in get_project_functions()] + +class ScheduledTask(models.Model): + task_name = models.CharField(max_length=255, verbose_name="Название задачи") + function_path = models.CharField( + max_length=500, + choices=get_available_functions, + verbose_name="Путь к функции (модуль.функция)", + ) + minutes = models.CharField(max_length=255, verbose_name="Минуты", default="*") + hours = models.CharField(max_length=255, verbose_name="Часы", default="*") + days = models.CharField(max_length=255, verbose_name="Дни", default="*") + months = models.CharField(max_length=255, verbose_name="Месяцы", default="*") + weekdays = models.JSONField(default=list, verbose_name="Дни недели") + active = models.BooleanField(default=True, verbose_name="Активно") + last_run = models.DateTimeField(blank=True, null=True, verbose_name="Последний запуск") + + def __str__(self): + return self.task_name \ No newline at end of file diff --git a/scheduler/static/scheduler/admin.css b/scheduler/static/scheduler/admin.css new file mode 100644 index 00000000..2a6396a1 --- /dev/null +++ b/scheduler/static/scheduler/admin.css @@ -0,0 +1,16 @@ +.checkbox-row { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.checkbox-row label { + display: inline-block; + width: auto; + margin-right: 10px; +} + +.form-row.last_run span { + font-weight: bold; + margin-left: 10px; +} diff --git a/scheduler/tasks.py b/scheduler/tasks.py new file mode 100644 index 00000000..eb93f250 --- /dev/null +++ b/scheduler/tasks.py @@ -0,0 +1,66 @@ +from apscheduler.schedulers.base import BaseScheduler +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from importlib import import_module +from scheduler.models import ScheduledTask +import importlib +from apscheduler.triggers.cron import CronTrigger + + +def format_weekdays(weekdays): + """Преобразует список дней недели в строку.""" + if isinstance(weekdays, list): + return ",".join(map(str, weekdays)) + return str(weekdays) + +def run_task(task): + """ + Выполняет задачу, указанную в модели ScheduledTask. + """ + module_name, func_name = task.module_path.rsplit(".", 1) + module = import_module(module_name) + func = getattr(module, func_name) + func() + +def setup_scheduler(): + """Настройка планировщика задач из БД.""" + print("Настройка планировщика задач...") + scheduler = AsyncIOScheduler() + + tasks = ScheduledTask.objects.filter(active=True) + for task in tasks: + scheduler.add_job( + run_task, + "cron", + id=task.name, + minute=task.cron_minute, + hour=task.cron_hour, + day=task.cron_day, + month=task.cron_month, + day_of_week=task.cron_weekday, + args=[task], + ) + scheduler.start() + print("Планировщик запущен.") + return scheduler + +def load_tasks_to_scheduler(scheduler: BaseScheduler): + tasks = ScheduledTask.objects.filter(active=True) + for task in tasks: + try: + module_name, func_name = task.function_path.rsplit('.', 1) + module = import_module(module_name) + func = getattr(module, func_name) + + scheduler.add_job( + func, + trigger="cron", + minute=task.minutes, + hour=task.hours, + day=task.days or "*", + month=task.months or "*", + day_of_week=task.weekdays or "*", + id=str(task.id), + replace_existing=True, + ) + except Exception as e: + print(f"Ошибка при добавлении задачи '{task.task_name}': {e}") \ No newline at end of file diff --git a/scheduler/templates/admin/scheduler/scheduledtasks/change_form.html b/scheduler/templates/admin/scheduler/scheduledtasks/change_form.html new file mode 100644 index 00000000..660591fa --- /dev/null +++ b/scheduler/templates/admin/scheduler/scheduledtasks/change_form.html @@ -0,0 +1,42 @@ +{% extends "admin/change_form.html" %} +{% load static %} + +{% block extrahead %} + {{ block.super }} + +{% endblock %} + +{% block content %} +