bnovo plugin

scheduller
This commit is contained in:
2024-12-10 20:07:23 +09:00
parent 8dce756a27
commit 806c611cc7
38 changed files with 1301 additions and 277 deletions

3
.gitignore vendored
View File

@@ -9,3 +9,6 @@ __pycache__
node_modules node_modules
package-lock.json package-lock.json
package.json package.json
old_bot
# Ignore files

BIN
1db.sqlite3 Normal file

Binary file not shown.

149
1db.sqlite3.sql Normal file
View File

@@ -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;

0
bot.log Normal file
View File

View File

@@ -5,63 +5,56 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from telegram.ext import Application from telegram.ext import Application
from bot.utils.bot_setup import setup_bot from bot.utils.bot_setup import setup_bot
from bot.utils.scheduler import setup_scheduler from scheduler.tasks import load_tasks_to_scheduler
from dotenv import load_dotenv
from bot.operations.users import show_users
# Загрузка переменных окружения
load_dotenv()
class Command(BaseCommand): class Command(BaseCommand):
help = "Запуск Telegram бота" help = "Запуск Telegram бота и планировщика"
def handle(self, *args, **options): def handle(self, *args, **options):
print("Запуск Telegram бота...") # Установка Django окружения
# Настройка Django окружения
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings")
django.setup() 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") bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
if not bot_token: if not bot_token:
raise ValueError("Токен бота не найден в переменных окружения.") raise ValueError("Токен бота не найден в переменных окружения.")
application = Application.builder().token(bot_token).build() application = Application.builder().token(bot_token).build()
# Настройка бота и обработчиков
setup_bot(application) setup_bot(application)
# Основная асинхронная функция
async def main(): async def main():
print("Настройка планировщика...") await application.initialize()
scheduler = setup_scheduler() await application.start()
scheduler.start() await application.updater.start_polling()
self.stdout.write(self.style.SUCCESS("Telegram бот и планировщик успешно запущены."))
try: try:
print("Инициализация Telegram бота...")
await application.initialize() # Инициализация приложения
print("Бот запущен. Ожидание сообщений...")
await application.start() # Запуск приложения
await application.updater.start_polling() # Запуск обработки сообщений
# Бесконечный цикл для удержания приложения активным
while True: while True:
await asyncio.sleep(3600) # Ожидание 1 час await asyncio.sleep(3600)
except Exception as e: except asyncio.CancelledError:
print(f"Ошибка во время работы бота: {e}") await application.stop()
finally: scheduler.shutdown()
print("Остановка Telegram бота...")
await application.stop() # Завершаем приложение перед shutdown
print("Остановка планировщика...")
scheduler.shutdown(wait=False)
print("Планировщик остановлен.")
# Запуск асинхронной программы
try: 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()) loop.run_until_complete(main())
else: except KeyboardInterrupt:
raise self.stdout.write(self.style.ERROR("Завершение работы Telegram бота и планировщика"))
finally:
loop.close()

View File

@@ -138,18 +138,18 @@ async def check_pms(update, context):
# Создаем экземпляр PMSIntegrationManager # Создаем экземпляр PMSIntegrationManager
pms_manager = PMSIntegrationManager(hotel_id=hotel_id) 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)() 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 # Плагин поддерживает метод 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: elif pms_config.api_url and pms_config.token:
# Используем прямой запрос к API # Используем прямой запрос к API
from pms_integration.api_client import APIClient from pms_integration.api_client import APIClient
api_client = APIClient(base_url=pms_config.api_url, access_token=pms_config.token) 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: else:
# Если подходящий способ не найден # Если подходящий способ не найден
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.") 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} завершена успешно.") await query.edit_message_text(f"Интеграция PMS {pms_config.name} завершена успешно.")
except Exception as e: 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): async def setup_rooms(update: Update, context):
"""Настроить номера отеля.""" """Настроить номера отеля."""

BIN
db2.sqlite3 Normal file

Binary file not shown.

186
db2.sqlite3.sql Normal file
View File

@@ -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;

View File

@@ -4,9 +4,7 @@ from .models import (
Hotel, Hotel,
UserHotel, UserHotel,
APIConfiguration, APIConfiguration,
APIRequestLog,
Reservation, Reservation,
Guest,
FraudLog FraudLog
) )
from django.urls import path from django.urls import path
@@ -69,22 +67,6 @@ class UserHotelAdmin(admin.ModelAdmin):
# ordering = ('-hotel',) # 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) @admin.register(Reservation)
class ReservationAdmin(admin.ModelAdmin): class ReservationAdmin(admin.ModelAdmin):
list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount') 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',) 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',)

View File

@@ -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)

View File

@@ -2,6 +2,17 @@
"""Django's command-line utility for administrative tasks.""" """Django's command-line utility for administrative tasks."""
import os import os
import sys 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(): def main():

View File

@@ -8,52 +8,33 @@ from django.shortcuts import render
from django import forms from django import forms
from pms_integration.models import PMSConfiguration, PMSIntegrationLog from pms_integration.models import PMSConfiguration, PMSIntegrationLog
class PMSConfigurationForm(forms.ModelForm): class PMSConfigurationForm(forms.ModelForm):
class Meta: class Meta:
model = PMSConfiguration model = PMSConfiguration
fields = "__all__" fields = '__all__'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Загружаем доступные плагины
plugins = PluginLoader.load_plugins() 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) @admin.register(PMSConfiguration)
class PMSConfigurationAdmin(admin.ModelAdmin): class PMSConfigurationAdmin(admin.ModelAdmin):
form = PMSConfigurationForm form = PMSConfigurationForm
list_display = ('name', 'plugin_name', 'created_at', 'check_plugins_button') list_display = ('name', 'plugin_name', 'created_at')
search_fields = ('name', 'description') search_fields = ('name', 'plugin_name')
list_filter = ('created_at',)
ordering = ('-created_at',) ordering = ('-created_at',)
def get_urls(self): def save_model(self, request, obj, form, change):
"""Добавляем 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):
"""Проверка и отображение плагинов."""
plugins = PluginLoader.load_plugins() plugins = PluginLoader.load_plugins()
plugin_details = [ if obj.plugin_name and obj.plugin_name not in plugins.keys():
{"name": plugin_name, "doc": plugins[plugin_name].__doc__ or "Нет документации"} raise ValueError(f"Выберите корректный плагин. '{obj.plugin_name}' нет среди допустимых значений.")
for plugin_name in plugins super().save_model(request, obj, form, change)
]
context = {
"title": "Проверка плагинов",
"plugin_details": plugin_details,
}
return render(request, "admin/check_plugins.html", context)
def check_plugins_button(self, obj):
"""Добавляем кнопку для проверки плагинов."""
return format_html(
'<a class="button" href="{}">Проверить плагины</a>',
"/admin/pms_integration/pmsconfiguration/check-plugins/",
)
check_plugins_button.short_description = "Проверить плагины"
@admin.register(PMSIntegrationLog) @admin.register(PMSIntegrationLog)
class PMSIntegrationLogAdmin(admin.ModelAdmin): class PMSIntegrationLogAdmin(admin.ModelAdmin):

View File

@@ -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()]

View File

@@ -7,27 +7,31 @@ from asgiref.sync import sync_to_async
class PluginLoader: class PluginLoader:
PLUGIN_PATH = Path(__file__).parent / "plugins" PLUGIN_PATH = Path(__file__).parent / "plugins"
print("Путь к папке плагинов:", PLUGIN_PATH.resolve())
print("Содержимое папки:", list(PLUGIN_PATH.iterdir()))
@staticmethod @staticmethod
def load_plugins(): def load_plugins():
plugins = {} plugins = {}
if not PluginLoader.PLUGIN_PATH.exists():
print("Папка с плагинами не существует:", PluginLoader.PLUGIN_PATH)
return plugins
print("Загрузка плагинов:")
for file in os.listdir(PluginLoader.PLUGIN_PATH): for file in os.listdir(PluginLoader.PLUGIN_PATH):
if file.endswith("_pms.py") and not file.startswith("__"): if file.endswith("_pms.py") and not file.startswith("__"):
print(f" Plugin {file}")
module_name = f"pms_integration.plugins.{file[:-3]}" module_name = f"pms_integration.plugins.{file[:-3]}"
try: try:
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
for attr in dir(module): for attr in dir(module):
cls = getattr(module, attr) cls = getattr(module, attr)
if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin: if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin:
plugins[cls.__name__] = cls plugin_name = file[:-7] # Убираем `_pms` из имени файла
print(f"Загружен плагин: {cls.__name__}") print(f" Загружен плагин {plugin_name}: {cls.__name__}")
plugins[plugin_name] = cls
except Exception as e: except Exception as e:
print(f"Ошибка при загрузке модуля {module_name}: {e}") print(f" Ошибка загрузки плагина {module_name}: {e}")
print(f"Итоговый список плагинов: {list(plugins.keys())}")
return plugins return plugins
class PMSIntegrationManager: class PMSIntegrationManager:
def __init__(self, hotel_id): def __init__(self, hotel_id):
self.hotel_id = hotel_id self.hotel_id = hotel_id

View File

@@ -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),
),
]

View File

@@ -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='Плагин'),
),
]

View File

@@ -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='Плагин'),
),
]

View File

@@ -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='Публичный ключ'),
),
]

View File

@@ -8,10 +8,12 @@ class PMSConfiguration(models.Model):
name = models.CharField(max_length=255, verbose_name="Название PMS") name = models.CharField(max_length=255, verbose_name="Название PMS")
url = models.URLField(verbose_name="URL API") url = models.URLField(verbose_name="URL API")
token = models.CharField(max_length=255, blank=True, null=True, verbose_name="Токен") 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="Логин") username = models.CharField(max_length=255, blank=True, null=True, verbose_name="Логин")
password = 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="Название плагина") plugin_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Плагин")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") # Добавлено поле created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
def __str__(self): def __str__(self):

View File

@@ -1,71 +1,136 @@
import requests import requests
import json
from datetime import datetime, timedelta
from .base_plugin import BasePMSPlugin from .base_plugin import BasePMSPlugin
from asgiref.sync import sync_to_async
from pms_integration.models import PMSConfiguration # Убедитесь, что модель существует
class BnovoPMS(BasePMSPlugin): class BnovoPMSPlugin(BasePMSPlugin):
""" """Плагин для работы с PMS Bnovo."""
Плагин для интеграции с Bnovo.
""" def __init__(self, config):
json_schema = { super().__init__(config)
"type": "object", self.api_url = config.url.rstrip("/") # Убираем лишний `/` в конце URL
"properties": { self.username = config.username
"id": {"type": "integer"}, self.password = config.password
"number": {"type": "integer"}, self.token = None # SID
"roomTypeName": {"type": "string"},
"checkInStatus": {"type": "string"}, if not self.api_url:
"guests": {"type": "array"}, raise ValueError("Не указан URL для работы плагина.")
}, if not self.username or not self.password:
"required": ["id", "number", "roomTypeName", "checkInStatus", "guests"] raise ValueError("Не указаны логин или пароль для авторизации.")
}
def get_default_parser_settings(self): def get_default_parser_settings(self):
""" """Возвращает настройки по умолчанию для обработки данных."""
Возвращает настройки парсера по умолчанию.
"""
return { return {
"field_mapping": { "date_format": "%Y-%m-%dT%H:%M:%S",
"room_name": "roomNumber", "timezone": "UTC"
"check_in": "from",
"check_out": "until",
},
"date_format": "%Y-%m-%dT%H:%M:%S"
} }
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()
# Проверка структуры async def _save_token_to_db(self, sid):
expected_fields = self.pms_config.parser_settings.get("fields_mapping", {}) """Сохраняет токен (SID) в базу данных."""
for field in expected_fields.values(): try:
if field not in data[0]: # Проверяем первую запись await sync_to_async(PMSConfiguration.objects.update_or_create)(
raise ValueError(f"Поле {field} отсутствует в ответе API.") plugin_name="bnovo",
defaults={"token": sid}
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) # Проверка соответствия структуры print(f"[DEBUG] Токен сохранен в БД: {sid}")
except Exception as e:
print(f"[ERROR] Ошибка сохранения токена в БД: {e}")
def _get_auth_headers(self):
"""Создает заголовки авторизации."""
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if self.token:
headers["Cookie"] = f"SID={self.token}"
return headers
async def _fetch_session(self):
"""Получает идентификатор сессии (SID) через запрос."""
url = f"{self.api_url}/"
payload = {
"username": self.username,
"password": self.password,
}
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: if response.status_code != 200:
raise ValueError(f"Ошибка запроса к PMS Bnovo: {response.text}") raise ValueError(f"Ошибка при получении данных: {response.status_code}, {response.text}")
data = response.json() data = response.json()
parsed_data = self.parse_data(data) print(json.dumps(data, indent=2))
return parsed_data bookings = data.get("bookings", [])
all_bookings.extend(bookings)
def parse_data(self, data): print(f"[DEBUG] Получено бронирований: {len(bookings)}")
# Пример разбора данных на основе JSON-маски print(f"[DEBUG] Всего бронирований: {len(all_bookings)}")
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
# Проверка на наличие следующей страницы
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

View File

@@ -1,78 +1,254 @@
import hashlib # import requests
import requests # import hashlib
import json # import json
from datetime import datetime # from .base_plugin import BasePMSPlugin
from hotels.models import Reservation # 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): class RealtyCalendarPlugin(BasePMSPlugin):
""" """Плагин для импорта данных из системы RealtyCalendar
Плагин для взаимодействия с RealtyCalendar.
""" """
def __init__(self, config): def __init__(self, config):
super().__init__(config) super().__init__(config)
self.public_key = config.token # Используем `token` как публичный ключ self.public_key = config.public_key
self.private_key = config.password # Используем `password` как приватный ключ self.private_key = config.private_key
self.base_url = config.url self.api_url = config.url.rstrip("/")
def generate_sign(self, params): if not self.public_key or not self.private_key:
""" raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar")
Генерация подписи запроса.
: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()
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: return {
start_date = datetime.now().strftime('%Y-%m-%d') "date_format": "%Y-%m-%dT%H:%M:%S",
if not end_date: "timezone": "UTC"
end_date = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
params = {
'begin_date': start_date,
'end_date': end_date,
} }
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 = { headers = {
'Accept': 'application/json', "Accept": "application/json",
'Content-Type': '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() print(f"[DEBUG] Даты выборки: {data}")
return data.get('bookings', [])
@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: from hotels.models import Reservation, Hotel
Reservation.objects.update_or_create(
external_id=booking['id'], 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={ defaults={
'check_in': booking['begin_date'], "room_number": item.get("apartment_id", ""), # ID квартиры
'check_out': booking['end_date'], "check_in": datetime.strptime(item["begin_date"], "%Y-%m-%d"), # Дата заезда
'amount': booking['amount'], "check_out": datetime.strptime(item["end_date"], "%Y-%m-%d"), # Дата выезда
'notes': booking.get('notes', ''), "status": item.get("status", ""), # Статус бронирования
'guest_name': booking['client']['fio'], "price": item.get("amount", 0), # Сумма оплаты
'guest_phone': booking['client']['phone'], "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}")

View File

@@ -8,6 +8,9 @@ from hotels.models import Hotel
class Shelter(BasePMSPlugin): class Shelter(BasePMSPlugin):
"""
Плагин для PMS Shelter Coud.
"""
def __init__(self, config): def __init__(self, config):
super().__init__(config) super().__init__(config)
self.token = config.token self.token = config.token

View File

@@ -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}")

View File

@@ -1,7 +1,12 @@
ace_tools==0.0 ace_tools==0.0
aiohappyeyeballs==2.4.4
aiohttp==3.11.10
aiosignal==1.3.1
anyio==4.6.2.post1 anyio==4.6.2.post1
APScheduler==3.11.0 APScheduler==3.11.0
asgiref==3.8.1 asgiref==3.8.1
async-timeout==5.0.1
attrs==24.2.0
certifi==2024.8.30 certifi==2024.8.30
charset-normalizer==3.4.0 charset-normalizer==3.4.0
Django==5.1.4 Django==5.1.4
@@ -9,26 +14,41 @@ django-filter==24.3
django-jazzmin==3.0.1 django-jazzmin==3.0.1
django-jet==1.0.8 django-jet==1.0.8
et_xmlfile==2.0.0 et_xmlfile==2.0.0
exceptiongroup==1.2.2
fpdf==1.7.2 fpdf==1.7.2
frozenlist==1.5.0
geoip2==4.8.1
h11==0.14.0 h11==0.14.0
httpcore==1.0.7 httpcore==1.0.7
httpx==0.28.0 httpx==0.28.0
idna==3.10 idna==3.10
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
maxminddb==2.6.2
multidict==6.1.0
numpy==2.1.3 numpy==2.1.3
openpyxl==3.1.5 openpyxl==3.1.5
pandas==2.2.3 pandas==2.2.3
pathspec==0.12.1
pillow==11.0.0 pillow==11.0.0
propcache==0.2.1
PyMySQL==1.1.1 PyMySQL==1.1.1
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-dotenv==1.0.1 python-dotenv==1.0.1
python-telegram-bot==21.8 python-telegram-bot==21.8
pytz==2024.2 pytz==2024.2
PyYAML==6.0.2 PyYAML==6.0.2
referencing==0.35.1
requests==2.32.3 requests==2.32.3
rpds-py==0.22.3
six==1.17.0 six==1.17.0
sniffio==1.3.1 sniffio==1.3.1
sqlparse==0.5.2 sqlparse==0.5.2
typing_extensions==4.12.2
tzdata==2024.2 tzdata==2024.2
tzlocal==5.2 tzlocal==5.2
ua-parser==1.0.0
ua-parser-builtins==0.18.0.post1
urllib3==2.2.3 urllib3==2.2.3
jsonschema user-agents==2.2.0
yarl==1.18.3

0
scheduler/__init__.py Normal file
View File

59
scheduler/admin.py Normal file
View File

@@ -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 = "Последний запуск"

6
scheduler/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class SchedulerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'scheduler'

View File

@@ -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='Последний запуск')),
],
),
]

View File

47
scheduler/models.py Normal file
View File

@@ -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

View File

@@ -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;
}

66
scheduler/tasks.py Normal file
View File

@@ -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}")

View File

@@ -0,0 +1,42 @@
{% extends "admin/change_form.html" %}
{% load static %}
{% block extrahead %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'scheduler/admin.css' %}">
{% endblock %}
{% block content %}
<div class="form-container">
<form method="post" class="change-form" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{{ adminform.non_field_errors }}
<div>
{% for fieldset in adminform %}
<fieldset class="{{ fieldset.classes }}">
<legend>{{ fieldset.name }}</legend>
{% for line in fieldset %}
<div class="form-row {{ line.field.field.name }}">
{% if line.field.name == 'last_run' %}
<div>
<label>{{ line.label }}</label>
<span>{{ line.field.contents }}</span>
</div>
{% else %}
{{ line.errors }}
{{ line.field }}
{% if line.field.is_checkbox %}
<div class="checkbox-row">{{ line.field }}</div>
{% endif %}
{% endif %}
</div>
{% endfor %}
</fieldset>
{% endfor %}
</div>
<div class="submit-row">
{{ submit_buttons }}
</div>
</form>
</div>
{% endblock %}

6
scheduler/test_module.py Normal file
View File

@@ -0,0 +1,6 @@
def test_function():
"""тестовая функция для проверки планировщика
"""
print("Hello, World!")
return "Hello, World!"

3
scheduler/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

71
scheduler/utils.py Normal file
View File

@@ -0,0 +1,71 @@
from apscheduler.schedulers.asyncio import AsyncIOScheduler
import os
import inspect
import importlib
from typing import List, Tuple
from pathspec import PathSpec
def reload_tasks_periodically(scheduler: AsyncIOScheduler):
"""Перезагрузка задач из базы данных каждые 5 минут."""
from scheduler.tasks import load_tasks_to_scheduler
scheduler.add_job(lambda: load_tasks_to_scheduler(scheduler), "interval", minutes=5)
def load_gitignore_patterns(project_root: str) -> PathSpec:
"""
Загружает паттерны из файла .gitignore.
"""
gitignore_path = os.path.join(project_root, ".gitignore")
if os.path.exists(gitignore_path):
with open(gitignore_path, "r", encoding="utf-8") as f:
patterns = f.readlines()
return PathSpec.from_lines("gitwildmatch", patterns)
return PathSpec.from_lines("gitwildmatch", []) # Пустой PathSpec
def get_project_functions() -> List[Tuple[str, str]]:
"""
Сканирует проект и возвращает список всех функций в формате (путь, имя функции),
исключая файлы и папки, указанные в .gitignore.
"""
functions = []
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Загружаем паттерны из .gitignore
gitignore_spec = load_gitignore_patterns(project_root)
for root, dirs, files in os.walk(project_root):
# Исключаем директории, указанные в .gitignore
dirs[:] = [d for d in dirs if not gitignore_spec.match_file(os.path.relpath(os.path.join(root, d), project_root))]
for file in files:
file_path = os.path.relpath(os.path.join(root, file), project_root)
if (
file.endswith(".py") and
not file.startswith("__") and
not gitignore_spec.match_file(file_path)
):
module_path = os.path.relpath(os.path.join(root, file), project_root)
module_name = module_path.replace(os.sep, ".").replace(".py", "")
try:
module = importlib.import_module(module_name)
for name, func in inspect.getmembers(module, inspect.isfunction):
functions.append((f"{module_name}.{name}", name))
except Exception as e:
print(f"Ошибка загрузки модуля {module_name}: {e}")
return functions
import importlib
def execute_function(function_path):
"""
Выполняет функцию по указанному пути.
"""
module_name, func_name = function_path.rsplit(".", 1)
module = importlib.import_module(module_name)
func = getattr(module, func_name)
return func()

3
scheduler/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -44,6 +44,7 @@ INSTALLED_APPS = [
'pms_integration', 'pms_integration',
'hotels', 'hotels',
'users', 'users',
'scheduler'
] ]
MIDDLEWARE = [ MIDDLEWARE = [