Compare commits
13 Commits
PMSManager
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| eb662f7fe6 | |||
| d59ebaf005 | |||
| 31cf30e344 | |||
| 7350989113 | |||
| 5e3ed91b3a | |||
| 56175078d6 | |||
| 0e45074ea5 | |||
| bc865303c5 | |||
| 5c58a46e18 | |||
| 3dd5f2238e | |||
| 61fffd3be4 | |||
| c535a51953 | |||
| 5f434d8248 |
6
.docker/admin/.dockerignore
Normal file
6
.docker/admin/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.venv
|
||||||
|
.venv/
|
||||||
|
.log
|
||||||
|
__pycache__
|
||||||
|
.history
|
||||||
|
.vscode
|
||||||
30
.docker/admin/Dockerfile
Normal file
30
.docker/admin/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
FROM python:3.9-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Устанавливаем временную директорию
|
||||||
|
ENV TMPDIR=/tmp/tempdir
|
||||||
|
RUN mkdir -p $TMPDIR && chmod 1777 $TMPDIR
|
||||||
|
|
||||||
|
# Устанавливаем системные зависимости для Alpine
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
gcc \
|
||||||
|
musl-dev \
|
||||||
|
mariadb-dev \
|
||||||
|
netcat-openbsd \
|
||||||
|
net-tools \
|
||||||
|
iputils
|
||||||
|
|
||||||
|
# Копируем только requirements.txt для кэширования зависимостей
|
||||||
|
COPY .docker/admin/requirements.txt /app/requirements.txt
|
||||||
|
|
||||||
|
# Устанавливаем Python-зависимости
|
||||||
|
RUN pip install --upgrade pip && pip install --no-cache-dir -r /app/requirements.txt
|
||||||
|
|
||||||
|
# Копируем весь проект
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
RUN chmod +x .docker/admin/entrypoint.sh
|
||||||
|
ENTRYPOINT [".docker/admin/entrypoint.sh"]
|
||||||
|
|
||||||
|
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||||
14
.docker/admin/entrypoint.sh
Executable file
14
.docker/admin/entrypoint.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Ожидание доступности базы данных
|
||||||
|
until nc -z -v -w30 $DB_HOST $DB_PORT; do
|
||||||
|
echo "Ожидание базы данных..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Выполняем миграции
|
||||||
|
python manage.py makemigrations --no-input
|
||||||
|
python manage.py migrate --no-input
|
||||||
|
|
||||||
|
# Запускаем приложение
|
||||||
|
exec "$@"
|
||||||
44
.docker/admin/requirements.txt
Normal file
44
.docker/admin/requirements.txt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
ace_tools
|
||||||
|
aiohappyeyeballs
|
||||||
|
aiohttp
|
||||||
|
aiosignal
|
||||||
|
APScheduler
|
||||||
|
Django
|
||||||
|
django-environ
|
||||||
|
django_extensions
|
||||||
|
django-filter
|
||||||
|
django-health-check
|
||||||
|
django-jazzmin
|
||||||
|
django-jet
|
||||||
|
et_xmlfile
|
||||||
|
fonttools
|
||||||
|
fpdf2
|
||||||
|
geoip2
|
||||||
|
git-filter-repo
|
||||||
|
httpcore
|
||||||
|
httpx
|
||||||
|
jsonschema
|
||||||
|
jsonschema-specifications
|
||||||
|
maxminddb
|
||||||
|
multidict
|
||||||
|
PyMySQL
|
||||||
|
numpy
|
||||||
|
openpyxl
|
||||||
|
pandas
|
||||||
|
pathspec
|
||||||
|
pillow
|
||||||
|
propcache
|
||||||
|
psycopg
|
||||||
|
PyMySQL
|
||||||
|
python-dateutil
|
||||||
|
python-decouple
|
||||||
|
python-dotenv
|
||||||
|
python-telegram-bot
|
||||||
|
PyYAML
|
||||||
|
requests
|
||||||
|
sqlparse
|
||||||
|
ua-parser
|
||||||
|
ua-parser-builtins
|
||||||
|
user-agents
|
||||||
|
yarl
|
||||||
|
cryptography
|
||||||
6
.docker/bot/.dockerignore
Normal file
6
.docker/bot/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.venv
|
||||||
|
.venv/
|
||||||
|
.log
|
||||||
|
__pycache__
|
||||||
|
.history
|
||||||
|
.vscode
|
||||||
12
.docker/bot/Dockerfile
Normal file
12
.docker/bot/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем весь проект в контейнер
|
||||||
|
COPY ../../ /app
|
||||||
|
|
||||||
|
# Устанавливаем зависимости только для bot
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip && pip install --no-cache-dir -r .docker/bot/requirements.txt
|
||||||
|
# Команда запуска для бота
|
||||||
|
CMD ["python", "manage.py" ,"run_bot.py"]
|
||||||
44
.docker/bot/requirements.txt
Normal file
44
.docker/bot/requirements.txt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
ace_tools
|
||||||
|
aiohappyeyeballs
|
||||||
|
aiohttp
|
||||||
|
aiosignal
|
||||||
|
APScheduler
|
||||||
|
Django
|
||||||
|
django-environ
|
||||||
|
django_extensions
|
||||||
|
django-filter
|
||||||
|
django-health-check
|
||||||
|
django-jazzmin
|
||||||
|
django-jet
|
||||||
|
et_xmlfile
|
||||||
|
fonttools
|
||||||
|
fpdf2
|
||||||
|
geoip2
|
||||||
|
git-filter-repo
|
||||||
|
httpcore
|
||||||
|
httpx
|
||||||
|
jsonschema
|
||||||
|
jsonschema-specifications
|
||||||
|
maxminddb
|
||||||
|
multidict
|
||||||
|
PyMySQL
|
||||||
|
numpy
|
||||||
|
openpyxl
|
||||||
|
pandas
|
||||||
|
pathspec
|
||||||
|
pillow
|
||||||
|
propcache
|
||||||
|
psycopg
|
||||||
|
PyMySQL
|
||||||
|
python-dateutil
|
||||||
|
python-decouple
|
||||||
|
python-dotenv
|
||||||
|
python-telegram-bot
|
||||||
|
PyYAML
|
||||||
|
requests
|
||||||
|
sqlparse
|
||||||
|
ua-parser
|
||||||
|
ua-parser-builtins
|
||||||
|
user-agents
|
||||||
|
yarl
|
||||||
|
cryptography
|
||||||
14
.docker/scheduler/Dockerfile
Normal file
14
.docker/scheduler/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем весь проект в контейнер
|
||||||
|
COPY ../../ /app
|
||||||
|
|
||||||
|
RUN chmod +x .docker/scheduler/entrypoint.sh
|
||||||
|
ENTRYPOINT [".docker/scheduler/entrypoint.sh"]
|
||||||
|
# Устанавливаем зависимости только для scheduler
|
||||||
|
RUN pip install --upgrade pip && pip install --no-cache-dir -r .docker/scheduler/requirements.txt
|
||||||
|
|
||||||
|
# Команда запуска для планировщика
|
||||||
|
CMD ["python", "manage.py", "run_scheduler"]
|
||||||
6
.docker/scheduler/dockerignore
Normal file
6
.docker/scheduler/dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.venv
|
||||||
|
.venv/
|
||||||
|
.log
|
||||||
|
__pycache__
|
||||||
|
.history
|
||||||
|
.vscode
|
||||||
7
.docker/scheduler/entrypoint.sh
Executable file
7
.docker/scheduler/entrypoint.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Выполняем миграции
|
||||||
|
python manage.py migrate --no-input
|
||||||
|
|
||||||
|
# Запускаем приложение
|
||||||
|
exec "$@"
|
||||||
44
.docker/scheduler/requirements.txt
Normal file
44
.docker/scheduler/requirements.txt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
ace_tools
|
||||||
|
aiohappyeyeballs
|
||||||
|
aiohttp
|
||||||
|
aiosignal
|
||||||
|
APScheduler
|
||||||
|
Django
|
||||||
|
django-environ
|
||||||
|
django-extensions
|
||||||
|
django-filter
|
||||||
|
django-health-check
|
||||||
|
django-jazzmin
|
||||||
|
django-jet
|
||||||
|
et_xmlfile
|
||||||
|
fonttools
|
||||||
|
fpdf2
|
||||||
|
geoip2
|
||||||
|
git-filter-repo
|
||||||
|
httpcore
|
||||||
|
httpx
|
||||||
|
jsonschema
|
||||||
|
jsonschema-specifications
|
||||||
|
maxminddb
|
||||||
|
multidict
|
||||||
|
PyMySQL
|
||||||
|
numpy
|
||||||
|
openpyxl
|
||||||
|
pandas
|
||||||
|
pathspec
|
||||||
|
pillow
|
||||||
|
propcache
|
||||||
|
psycopg
|
||||||
|
PyMySQL
|
||||||
|
python-dateutil
|
||||||
|
python-decouple
|
||||||
|
python-dotenv
|
||||||
|
python-telegram-bot
|
||||||
|
PyYAML
|
||||||
|
requests
|
||||||
|
sqlparse
|
||||||
|
ua-parser
|
||||||
|
ua-parser-builtins
|
||||||
|
user-agents
|
||||||
|
yarl
|
||||||
|
cryptography
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
from decouple import config
|
||||||
|
from django.conf import settings
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
|
||||||
def load_database_settings(databases):
|
def load_database_settings(databases):
|
||||||
@@ -8,6 +10,18 @@ def load_database_settings(databases):
|
|||||||
"""
|
"""
|
||||||
LocalDatabase = apps.get_model('app_settings', 'LocalDatabase')
|
LocalDatabase = apps.get_model('app_settings', 'LocalDatabase')
|
||||||
|
|
||||||
|
local_db_settings = LocalDatabase.objects.all()
|
||||||
|
|
||||||
|
for db in local_db_settings:
|
||||||
|
# Пример добавления дополнительной базы данных
|
||||||
|
settings.DATABASES[db.name] = {
|
||||||
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
|
'NAME': db.db_name,
|
||||||
|
'USER': db.username,
|
||||||
|
'PASSWORD': db.password,
|
||||||
|
'HOST': db.host,
|
||||||
|
'PORT': db.port,
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
local_db_settings = LocalDatabase.objects.filter(is_active=True)
|
local_db_settings = LocalDatabase.objects.filter(is_active=True)
|
||||||
for db in local_db_settings:
|
for db in local_db_settings:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
9
app_settings/signals.py
Normal file
9
app_settings/signals.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from .models import GlobalSystemSettings
|
||||||
|
|
||||||
|
@receiver(post_save, sender=GlobalSystemSettings)
|
||||||
|
def update_system_settings(sender, instance, **kwargs):
|
||||||
|
# Безопасное использование сигнала
|
||||||
|
if instance:
|
||||||
|
print(f"Настройки системы обновлены: {instance.system_name}")
|
||||||
11
app_settings/urls.py
Normal file
11
app_settings/urls.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
app_name = 'settings'
|
||||||
|
|
||||||
|
def placeholder_view(request):
|
||||||
|
return HttpResponse("Placeholder for settings app.")
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', placeholder_view, name='settings_placeholder'),
|
||||||
|
]
|
||||||
13373
bnovo_page_1.json
Normal file
13373
bnovo_page_1.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -150,7 +150,7 @@ async def check_pms(update, context):
|
|||||||
# Проверка наличия fetch_data и вызов плагина
|
# Проверка наличия fetch_data и вызов плагина
|
||||||
if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data):
|
if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data):
|
||||||
report = await pms_manager.plugin.fetch_data()
|
report = await pms_manager.plugin.fetch_data()
|
||||||
logger.debug(f"Отчет типа: {type(report)}")
|
logger.debug(f"Отчет типа: {type(report)}: {report}")
|
||||||
else:
|
else:
|
||||||
logger.error("Плагин не поддерживает fetch_data.")
|
logger.error("Плагин не поддерживает fetch_data.")
|
||||||
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")
|
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")
|
||||||
|
|||||||
73
docker-compose.yml
Normal file
73
docker-compose.yml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
||||||
|
MYSQL_DATABASE: ${DB_NAME}
|
||||||
|
MYSQL_USER: ${DB_USER}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
|
TMPDIR: /var/tmp
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT}:3306"
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
- /var/tmp:/var/tmp
|
||||||
|
|
||||||
|
django-admin:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: .docker/admin/Dockerfile
|
||||||
|
container_name: django-admin
|
||||||
|
restart: on-failure
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- DJANGO_SETTINGS_MODULE=touchh.settings
|
||||||
|
- DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_NAME}
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL}
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
command: python manage.py runserver 0.0.0.0:8000
|
||||||
|
|
||||||
|
bot:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: .docker/bot/Dockerfile
|
||||||
|
container_name: bot
|
||||||
|
restart: on-failure
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
environment:
|
||||||
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||||
|
- DJANGO_SETTINGS_MODULE=project.settings
|
||||||
|
- DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_NAME}
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL}
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
|
||||||
|
scheduler:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: .docker/scheduler/Dockerfile
|
||||||
|
container_name: scheduler
|
||||||
|
restart: on-failure
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- DJANGO_SETTINGS_MODULE=project.settings
|
||||||
|
- DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_NAME}
|
||||||
|
- SCHEDULED_SYNC_LOG_LEVEL=${SCHEDULED_SYNC_LOG_LEVEL}
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
@@ -18,7 +18,6 @@ class PluginLoader:
|
|||||||
print("Загрузка плагинов:")
|
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)
|
||||||
@@ -26,8 +25,8 @@ class PluginLoader:
|
|||||||
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:
|
||||||
plugin_name = file[:-7] # Убираем `_pms` из имени файла
|
plugin_name = file[:-7] # Убираем `_pms` из имени файла
|
||||||
# print(f" Загружен плагин {plugin_name}: {cls.__name__}")
|
|
||||||
plugins[plugin_name] = cls
|
plugins[plugin_name] = cls
|
||||||
|
print(f" Загружен плагин {plugin_name}: {cls.__name__}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" Ошибка загрузки плагина {module_name}: {e}")
|
print(f" Ошибка загрузки плагина {module_name}: {e}")
|
||||||
return plugins
|
return plugins
|
||||||
@@ -40,6 +39,7 @@ class PMSIntegrationManager:
|
|||||||
"""
|
"""
|
||||||
self.hotel = hotel
|
self.hotel = hotel
|
||||||
self.plugin = None
|
self.plugin = None
|
||||||
|
self.plugins = PluginLoader.load_plugins()
|
||||||
|
|
||||||
def load_hotel(self):
|
def load_hotel(self):
|
||||||
"""
|
"""
|
||||||
@@ -52,6 +52,16 @@ class PMSIntegrationManager:
|
|||||||
"""
|
"""
|
||||||
Загружает плагин, соответствующий PMS конфигурации отеля.
|
Загружает плагин, соответствующий PMS конфигурации отеля.
|
||||||
"""
|
"""
|
||||||
|
<<<<<<< HEAD
|
||||||
|
pms_name = self.hotel.pms.plugin_name.lower()
|
||||||
|
if pms_name in self.plugins:
|
||||||
|
plugin_class = self.plugins[pms_name]
|
||||||
|
self.plugin = plugin_class(self.hotel)
|
||||||
|
print(f"Плагин {pms_name} успешно загружен.")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Неизвестный PMS: {pms_name}")
|
||||||
|
|
||||||
|
=======
|
||||||
pms_name = self.hotel.pms.plugin_name.lower() # Приводим название плагина к нижнему регистру
|
pms_name = self.hotel.pms.plugin_name.lower() # Приводим название плагина к нижнему регистру
|
||||||
|
|
||||||
# Формируем имя модуля и класса плагина
|
# Формируем имя модуля и класса плагина
|
||||||
@@ -72,6 +82,7 @@ class PMSIntegrationManager:
|
|||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
raise ValueError(f"Ошибка загрузки плагина для PMS {pms_name}: {e}")
|
raise ValueError(f"Ошибка загрузки плагина для PMS {pms_name}: {e}")
|
||||||
|
>>>>>>> PMSManager_refactor
|
||||||
def fetch_data(self):
|
def fetch_data(self):
|
||||||
"""
|
"""
|
||||||
Получает данные из PMS с использованием загруженного плагина.
|
Получает данные из PMS с использованием загруженного плагина.
|
||||||
|
|||||||
@@ -1,3 +1,413 @@
|
|||||||
|
# import requests
|
||||||
|
# import json
|
||||||
|
# from datetime import datetime, timedelta
|
||||||
|
# from asgiref.sync import sync_to_async
|
||||||
|
# from .base_plugin import BasePMSPlugin
|
||||||
|
# from pms_integration.models import PMSConfiguration
|
||||||
|
# from hotels.models import Reservation, Hotel
|
||||||
|
# from touchh.utils.log import CustomLogger
|
||||||
|
# import logging
|
||||||
|
|
||||||
|
# import logging
|
||||||
|
|
||||||
|
# # Настройка логирования
|
||||||
|
# logging.basicConfig(
|
||||||
|
# level=logging.WARNING, # Установите уровень логов для всех обработчиков
|
||||||
|
# format='%(asctime)s [%(levelname)s] %(message)s',
|
||||||
|
# handlers=[
|
||||||
|
# logging.FileHandler("bnovo_plugin.log"), # Логи пишутся в файл
|
||||||
|
# logging.StreamHandler() # Логи выводятся в консоль
|
||||||
|
# ]
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # Создаем отдельный логгер для консоли с уровнем INFO
|
||||||
|
# console_handler = logging.StreamHandler()
|
||||||
|
# console_handler.setLevel(logging.INFO) # Меняем уровень логов для консоли
|
||||||
|
# console_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
|
||||||
|
|
||||||
|
# # Основной логгер
|
||||||
|
# logger = logging.getLogger("BnovoPMS Plugin")
|
||||||
|
# logger.addHandler(console_handler)
|
||||||
|
# logger.setLevel(logging.WARNING) # Основной уровень логов
|
||||||
|
|
||||||
|
# class BnovoPMSPlugin(BasePMSPlugin):
|
||||||
|
# """Плагин для работы с PMS Bnovo."""
|
||||||
|
|
||||||
|
# def __init__(self, hotel):
|
||||||
|
# super().__init__(hotel)
|
||||||
|
|
||||||
|
# if not isinstance(hotel, Hotel):
|
||||||
|
# logger.error("Ожидался объект Hotel, но получен другой тип.")
|
||||||
|
# raise ValueError("Для инициализации плагина требуется объект Hotel.")
|
||||||
|
|
||||||
|
# self.hotel = hotel
|
||||||
|
# self.pms_config = hotel.pms # Связь отеля с PMSConfiguration
|
||||||
|
# if not self.pms_config:
|
||||||
|
# logger.error(f"Отель {hotel.id} не связан с конфигурацией PMS.")
|
||||||
|
# raise ValueError(f"Отель {hotel.id} не связан с конфигурацией PMS.")
|
||||||
|
|
||||||
|
# self.api_url = self.pms_config.url.rstrip("/")
|
||||||
|
# self.username = self.pms_config.username
|
||||||
|
# self.password = self.pms_config.password
|
||||||
|
# self.token = None
|
||||||
|
|
||||||
|
# if not self.api_url:
|
||||||
|
# logger.error("Не указан URL для работы плагина.")
|
||||||
|
# raise ValueError("Не указан URL для работы плагина.")
|
||||||
|
# if not self.username or not self.password:
|
||||||
|
# logger.error("Не указаны логин или пароль для авторизации.")
|
||||||
|
# raise ValueError("Не указаны логин или пароль для авторизации.")
|
||||||
|
|
||||||
|
# def get_default_parser_settings(self):
|
||||||
|
# """Возвращает настройки по умолчанию для обработки данных."""
|
||||||
|
# logger.debug("Получение настроек парсера по умолчанию.")
|
||||||
|
# return {
|
||||||
|
# "date_format": "%Y-%m-%dT%H:%M:%S",
|
||||||
|
# "timezone": "UTC"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# async def _get_stored_token(self):
|
||||||
|
# """Получение токена из конфигурации PMS отеля."""
|
||||||
|
# try:
|
||||||
|
# logger.debug(f"Попытка получения токена для отеля {self.hotel.id}.")
|
||||||
|
# token = self.pms_config.token
|
||||||
|
# logger.debug(f"Токен из базы данных: {token}")
|
||||||
|
# return token
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.warning(f"Ошибка при получении токена для отеля {self.hotel.id}: {e}")
|
||||||
|
# return None
|
||||||
|
|
||||||
|
# async def _save_token_to_db(self, sid):
|
||||||
|
# """Сохраняет токен (SID) в конфигурации PMS отеля."""
|
||||||
|
# try:
|
||||||
|
# logger.debug(f"Сохранение токена для отеля {self.hotel.id}: {sid}")
|
||||||
|
# self.pms_config.token = sid
|
||||||
|
# await sync_to_async(self.pms_config.save)()
|
||||||
|
# logger.debug("Токен успешно сохранен.")
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Ошибка сохранения токена для отеля {self.hotel.id}: {e}")
|
||||||
|
|
||||||
|
# async def _ensure_token(self):
|
||||||
|
# """
|
||||||
|
# Убеждается, что токен (SID) доступен. Если его нет, выполняет авторизацию.
|
||||||
|
# """
|
||||||
|
# logger.debug(f"Проверка токена для отеля {self.hotel.id}.")
|
||||||
|
# if not self.token:
|
||||||
|
# self.token = await self._get_stored_token()
|
||||||
|
|
||||||
|
# if not self.token:
|
||||||
|
# logger.info("Токен отсутствует, выполняем авторизацию.")
|
||||||
|
# await self._fetch_session()
|
||||||
|
# else:
|
||||||
|
# logger.debug(f"Используется сохраненный токен: {self.token}")
|
||||||
|
|
||||||
|
# def _get_auth_headers(self):
|
||||||
|
# """Создает заголовки авторизации."""
|
||||||
|
# logger.debug("Создание заголовков авторизации.")
|
||||||
|
# headers = {
|
||||||
|
# "Content-Type": "application/json",
|
||||||
|
# "Accept": "application/json",
|
||||||
|
# }
|
||||||
|
# if self.token:
|
||||||
|
# headers["Cookie"] = f"SID={self.token}"
|
||||||
|
# logger.debug(f"Добавлен токен в заголовки: {self.token}")
|
||||||
|
# return headers
|
||||||
|
|
||||||
|
# async def _fetch_session(self):
|
||||||
|
# """Получение нового токена (SID) через запрос."""
|
||||||
|
# url = f"{self.api_url}/"
|
||||||
|
# payload = {"username": self.username, "password": self.password}
|
||||||
|
# headers = self._get_auth_headers()
|
||||||
|
|
||||||
|
# logger.debug(f"Авторизация по адресу: {url} с данными: {json.dumps(payload)}")
|
||||||
|
# response = requests.post(url, json=payload, headers=headers, allow_redirects=False)
|
||||||
|
|
||||||
|
# logger.debug(f"Ответ авторизации: статус {response.status_code}, заголовки {response.headers}")
|
||||||
|
# if response.status_code == 302:
|
||||||
|
# cookies = response.cookies.get_dict()
|
||||||
|
# sid = cookies.get("SID")
|
||||||
|
# if sid:
|
||||||
|
# self.token = sid
|
||||||
|
# logger.debug(f"Получен новый SID: {sid}")
|
||||||
|
# await self._save_token_to_db(sid)
|
||||||
|
# else:
|
||||||
|
# logger.error("Не удалось извлечь SID из ответа.")
|
||||||
|
# raise ValueError("Не удалось извлечь SID из ответа.")
|
||||||
|
# else:
|
||||||
|
# logger.error(f"Ошибка авторизации: {response.status_code}, {response.text}")
|
||||||
|
# raise ValueError(f"Ошибка авторизации: {response.status_code}, {response.text}")
|
||||||
|
|
||||||
|
# async def _fetch_account_data(self):
|
||||||
|
# """Получение данных аккаунта через эндпоинт /account/current."""
|
||||||
|
# logger.info(f"Начало получения данных аккаунта для отеля {self.hotel.id}.")
|
||||||
|
# self.token = await self._get_stored_token()
|
||||||
|
|
||||||
|
# if not self.token:
|
||||||
|
# logger.info("Токен отсутствует, выполняем авторизацию.")
|
||||||
|
# await self._fetch_session()
|
||||||
|
|
||||||
|
# url = f"{self.api_url}/account/current"
|
||||||
|
# headers = self._get_auth_headers()
|
||||||
|
|
||||||
|
# logger.debug(f"Выполнение запроса к {url}")
|
||||||
|
# response = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
# if response.status_code != 200:
|
||||||
|
# logger.error(f"Ошибка при запросе данных аккаунта: {response.status_code}, {response.text}")
|
||||||
|
# raise ValueError("Ошибка запроса к /account/current")
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# account_data = response.json()
|
||||||
|
# logger.debug(f"Полученные данные аккаунта:")
|
||||||
|
# except json.JSONDecodeError as e:
|
||||||
|
# logger.error(f"Ошибка декодирования JSON: {e}")
|
||||||
|
# raise ValueError(f"Ошибка декодирования JSON: {e}")
|
||||||
|
|
||||||
|
# return account_data
|
||||||
|
|
||||||
|
# async def _fetch_and_log_account_data(self):
|
||||||
|
# """Вызов метода _fetch_account_data и вывод результата в лог."""
|
||||||
|
# logger.info(f"Запуск получения и логирования данных аккаунта для отеля {self.hotel.id}.")
|
||||||
|
# try:
|
||||||
|
# account_data = await self._fetch_account_data()
|
||||||
|
# logger.info(f"Успешно полученные данные аккаунта:")
|
||||||
|
# return account_data
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Ошибка при получении данных аккаунта: {e}")
|
||||||
|
# raise
|
||||||
|
|
||||||
|
# async def _fetch_data_with_account_info(self):
|
||||||
|
# """Получение данных аккаунта и бронирований."""
|
||||||
|
# logger.info(f"Запуск процесса получения данных аккаунта и бронирований для отеля {self.hotel.id}.")
|
||||||
|
# try:
|
||||||
|
# account_data = await self.fetch_and_log_account_data()
|
||||||
|
# logger.info("Данные аккаунта успешно получены, продолжение с бронированиями.")
|
||||||
|
# await self.__fetch_data()
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Ошибка при выполнении полной операции: {e}")
|
||||||
|
|
||||||
|
# async def _fetch_paginated_data(self):
|
||||||
|
# """
|
||||||
|
# Получает все данные с API, обрабатывая страницы с пагинацией.
|
||||||
|
# """
|
||||||
|
# logger.info("Начало получения данных с пагинацией.")
|
||||||
|
# await self._ensure_token()
|
||||||
|
# url = f"{self.api_url}/dashboard"
|
||||||
|
# headers = self._get_auth_headers()
|
||||||
|
|
||||||
|
# now = datetime.now()
|
||||||
|
# create_from = (now - timedelta(days=1)).strftime("%d.%m.%Y")
|
||||||
|
# create_to = now.strftime("%d.%m.%Y")
|
||||||
|
|
||||||
|
# params = {
|
||||||
|
# "create_from": create_from,
|
||||||
|
# "create_to": create_to,
|
||||||
|
# "advanced_search": 2,
|
||||||
|
# "c": 100,
|
||||||
|
# "page": 1,
|
||||||
|
# "order_by": "create_date.asc",
|
||||||
|
# }
|
||||||
|
|
||||||
|
# all_bookings = []
|
||||||
|
# try:
|
||||||
|
# while True:
|
||||||
|
# logger.debug(f"Запрос к {url} с параметрами: {json.dumps(params, indent=2)}")
|
||||||
|
# response = requests.get(url, headers=headers, params=params, allow_redirects=False)
|
||||||
|
|
||||||
|
# if response.status_code == 302:
|
||||||
|
# logger.warning("Получен код 302. Перенаправление.")
|
||||||
|
# await self._fetch_session()
|
||||||
|
# headers = self._get_auth_headers()
|
||||||
|
# response = requests.get(url, headers=headers, params=params)
|
||||||
|
|
||||||
|
# if response.status_code != 200:
|
||||||
|
# logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}")
|
||||||
|
# raise ValueError("Ошибка при получении данных.")
|
||||||
|
|
||||||
|
# data = response.json()
|
||||||
|
# bookings = data.get("bookings", [])
|
||||||
|
# all_bookings.extend(bookings)
|
||||||
|
|
||||||
|
# # Проверка окончания пагинации
|
||||||
|
# pages_info = data.get("pages", {})
|
||||||
|
# current_page = pages_info.get("current_page", 1)
|
||||||
|
# total_pages = pages_info.get("total_pages", 1)
|
||||||
|
|
||||||
|
# logger.debug(f"Информация о страницах: текущая {current_page}, всего {total_pages}")
|
||||||
|
# if current_page >= total_pages:
|
||||||
|
# break
|
||||||
|
|
||||||
|
# params["page"] += 1
|
||||||
|
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Ошибка при обработке данных: {e}")
|
||||||
|
# raise
|
||||||
|
|
||||||
|
# logger.info(f"Всего бронирований: {len(all_bookings)}")
|
||||||
|
# return all_bookings
|
||||||
|
|
||||||
|
# async def _save_hotel_data(self, hotel_data):
|
||||||
|
# """
|
||||||
|
# Сохраняет данные об отеле в базу.
|
||||||
|
# """
|
||||||
|
# try:
|
||||||
|
# hotel_id = hotel_data.get("id")
|
||||||
|
# if not hotel_id:
|
||||||
|
# logger.warning("Данные об отеле не содержат идентификатор.")
|
||||||
|
# return
|
||||||
|
|
||||||
|
# await sync_to_async(Hotel.objects.update_or_create)(
|
||||||
|
# external_id=hotel_id,
|
||||||
|
# defaults={
|
||||||
|
# "name": hotel_data.get("name"),
|
||||||
|
# "address": hotel_data.get("address"),
|
||||||
|
# "city": hotel_data.get("city"),
|
||||||
|
# "timezone": hotel_data.get("timezone"),
|
||||||
|
# "rating": hotel_data.get("rating"),
|
||||||
|
# "phone": hotel_data.get("phone"),
|
||||||
|
# "email": hotel_data.get("email"),
|
||||||
|
# "country": hotel_data.get("country"),
|
||||||
|
# "booking_url": hotel_data.get("booking_url"),
|
||||||
|
# "tripadvisor_url": hotel_data.get("tripadvisor_url"),
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
# logger.info(f"Информация об отеле {hotel_id} успешно обновлена.")
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Ошибка при сохранении данных об отеле: {e}")
|
||||||
|
# async def _fetch_data(self):
|
||||||
|
# """
|
||||||
|
# Получает данные о бронированиях с API и возвращает их.
|
||||||
|
# """
|
||||||
|
# logger.info("Начало процесса получения данных о бронированиях.")
|
||||||
|
# try:
|
||||||
|
# await self._ensure_token() # Проверка токена
|
||||||
|
# url = f"{self.api_url}/dashboard"
|
||||||
|
# headers = self._get_auth_headers()
|
||||||
|
# now = datetime.now()
|
||||||
|
# create_from = (now - timedelta(days=1)).strftime("%d.%m.%Y")
|
||||||
|
# create_to = now.strftime("%d.%m.%Y")
|
||||||
|
|
||||||
|
# params = {
|
||||||
|
# "create_from": create_from,
|
||||||
|
# "create_to": create_to,
|
||||||
|
# "advanced_search": 2,
|
||||||
|
# "c": 100,
|
||||||
|
# "page": 1,
|
||||||
|
# "order_by": "create_date.asc",
|
||||||
|
# }
|
||||||
|
|
||||||
|
# all_data = []
|
||||||
|
|
||||||
|
# while True:
|
||||||
|
# logger.debug(f"Запрос к {url} с параметрами: {json.dumps(params, indent=2)}")
|
||||||
|
# response = requests.get(url, headers=headers, params=params, allow_redirects=False)
|
||||||
|
|
||||||
|
# if response.status_code == 302:
|
||||||
|
# logger.warning("Получен код 302. Перенаправление.")
|
||||||
|
# await self._fetch_session() # Обновляем токен
|
||||||
|
# headers = self._get_auth_headers()
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# if response.status_code != 200:
|
||||||
|
# logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}")
|
||||||
|
# raise ValueError(f"Ошибка при получении данных: {response.text}")
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# data = response.json()
|
||||||
|
# except json.JSONDecodeError as e:
|
||||||
|
# logger.error(f"Ошибка декодирования JSON: {e}. Ответ: {response.text}")
|
||||||
|
# raise ValueError(f"Ошибка декодирования JSON: {e}")
|
||||||
|
|
||||||
|
# bookings = data.get("bookings", [])
|
||||||
|
# logger.debug(f"Получено бронирований: {len(bookings)}")
|
||||||
|
# all_data.extend(bookings)
|
||||||
|
|
||||||
|
# # Проверка окончания пагинации
|
||||||
|
# pages_info = data.get("pages", {})
|
||||||
|
# current_page = pages_info.get("current_page", 1)
|
||||||
|
# total_pages = pages_info.get("total_pages", 1)
|
||||||
|
|
||||||
|
# logger.debug(f"Текущая страница: {current_page}, всего страниц: {total_pages}")
|
||||||
|
# if current_page >= total_pages:
|
||||||
|
# break
|
||||||
|
|
||||||
|
# params["page"] += 1
|
||||||
|
|
||||||
|
# if not all_data:
|
||||||
|
# logger.error("Полученные данные пусты или отсутствуют бронирования.")
|
||||||
|
# raise ValueError("API вернуло пустые данные.")
|
||||||
|
|
||||||
|
# logger.info(f"Всего бронирований: {len(all_data)}")
|
||||||
|
# return all_data
|
||||||
|
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Ошибка при получении данных: {e}")
|
||||||
|
# raise
|
||||||
|
|
||||||
|
|
||||||
|
# async def _process_and_save_data(self, data):
|
||||||
|
# """
|
||||||
|
# Обрабатывает и сохраняет данные о бронированиях в базу.
|
||||||
|
# """
|
||||||
|
# logger.info("Начало обработки данных о бронированиях для сохранения в базу.")
|
||||||
|
# processed_items = 0
|
||||||
|
# errors = []
|
||||||
|
|
||||||
|
# for record in data:
|
||||||
|
# try:
|
||||||
|
# booking_id = record.get("id")
|
||||||
|
# room_number = record.get("current_room")
|
||||||
|
# arrival = record.get("arrival")
|
||||||
|
# departure = record.get("departure")
|
||||||
|
# status = record.get("status_name")
|
||||||
|
|
||||||
|
# # Проверка обязательных данных
|
||||||
|
# if not (booking_id and room_number and arrival and departure and status):
|
||||||
|
# logger.warning(f"Пропуск записи из-за отсутствия обязательных данных: {record}")
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# # Сохраняем или обновляем запись в базе данных
|
||||||
|
# reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||||
|
# external_id=booking_id,
|
||||||
|
# defaults={
|
||||||
|
# "hotel": self.hotel,
|
||||||
|
# "status": status,
|
||||||
|
# "room_number": room_number,
|
||||||
|
# "check_in": arrival,
|
||||||
|
# "check_out": departure,
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
# if created:
|
||||||
|
# logger.info(f"Создана новая запись бронирования: {reservation}")
|
||||||
|
# else:
|
||||||
|
# logger.info(f"Обновлено существующее бронирование: {reservation}")
|
||||||
|
|
||||||
|
# processed_items += 1
|
||||||
|
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Ошибка обработки бронирования {record.get('id')}: {e}")
|
||||||
|
# errors.append(str(e))
|
||||||
|
|
||||||
|
# logger.info(f"Обработано бронирований: {processed_items}, ошибок: {len(errors)}")
|
||||||
|
# return {"processed_items": processed_items, "errors": errors}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# async def fetch_and_process_data(self):
|
||||||
|
# """
|
||||||
|
# Загружает данные с API и сохраняет их в базу.
|
||||||
|
# """
|
||||||
|
# logger.info("Начало процесса загрузки и обработки данных.")
|
||||||
|
# try:
|
||||||
|
# data = await self._fetch_paginated_data()
|
||||||
|
# report = await self._process_and_save_data(data)
|
||||||
|
# logger.info(f"Загрузка и обработка завершены. Отчет: {report}")
|
||||||
|
# return report
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Ошибка в процессе загрузки и обработки данных: {e}")
|
||||||
|
# raise
|
||||||
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -5,14 +415,11 @@ from asgiref.sync import sync_to_async
|
|||||||
from .base_plugin import BasePMSPlugin
|
from .base_plugin import BasePMSPlugin
|
||||||
from pms_integration.models import PMSConfiguration
|
from pms_integration.models import PMSConfiguration
|
||||||
from hotels.models import Reservation, Hotel
|
from hotels.models import Reservation, Hotel
|
||||||
from touchh.utils.log import CustomLogger
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.WARNING, # Установите уровень логов для всех обработчиков
|
level=logging.WARNING,
|
||||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||||
handlers=[
|
handlers=[
|
||||||
logging.FileHandler("var/log/bnovo_plugin.log"), # Логи пишутся в файл
|
logging.FileHandler("var/log/bnovo_plugin.log"), # Логи пишутся в файл
|
||||||
@@ -20,46 +427,26 @@ logging.basicConfig(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Создаем отдельный логгер для консоли с уровнем INFO
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setLevel(logging.WARNING) # Меняем уровень логов для консоли
|
|
||||||
console_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
|
|
||||||
|
|
||||||
# Основной логгер
|
|
||||||
logger = logging.getLogger("BnovoPMS Plugin")
|
logger = logging.getLogger("BnovoPMS Plugin")
|
||||||
logger.addHandler(console_handler)
|
logger.setLevel(logging.INFO)
|
||||||
logger.setLevel(logging.WARNING) # Основной уровень логов
|
|
||||||
|
|
||||||
class BnovoPMSPlugin(BasePMSPlugin):
|
class BnovoPMSPlugin(BasePMSPlugin):
|
||||||
"""Плагин для работы с PMS Bnovo."""
|
"""Плагин для работы с PMS Bnovo."""
|
||||||
|
|
||||||
def __init__(self, hotel):
|
def __init__(self, hotel):
|
||||||
super().__init__(hotel)
|
super().__init__(hotel)
|
||||||
|
|
||||||
if not isinstance(hotel, Hotel):
|
|
||||||
logger.error("Ожидался объект Hotel, но получен другой тип.")
|
|
||||||
raise ValueError("Для инициализации плагина требуется объект Hotel.")
|
|
||||||
|
|
||||||
self.hotel = hotel
|
self.hotel = hotel
|
||||||
self.pms_config = hotel.pms # Связь отеля с PMSConfiguration
|
self.pms_config = hotel.pms
|
||||||
if not self.pms_config:
|
|
||||||
logger.error(f"Отель {hotel.id} не связан с конфигурацией PMS.")
|
|
||||||
raise ValueError(f"Отель {hotel.id} не связан с конфигурацией PMS.")
|
|
||||||
|
|
||||||
self.api_url = self.pms_config.url.rstrip("/")
|
self.api_url = self.pms_config.url.rstrip("/")
|
||||||
self.username = self.pms_config.username
|
self.username = self.pms_config.username
|
||||||
self.password = self.pms_config.password
|
self.password = self.pms_config.password
|
||||||
self.token = None
|
self.token = None
|
||||||
|
|
||||||
if not self.api_url:
|
|
||||||
logger.error("Не указан URL для работы плагина.")
|
|
||||||
raise ValueError("Не указан URL для работы плагина.")
|
|
||||||
if not self.username or not self.password:
|
|
||||||
logger.error("Не указаны логин или пароль для авторизации.")
|
|
||||||
raise ValueError("Не указаны логин или пароль для авторизации.")
|
|
||||||
|
|
||||||
def get_default_parser_settings(self):
|
def get_default_parser_settings(self):
|
||||||
"""Возвращает настройки по умолчанию для обработки данных."""
|
"""
|
||||||
|
Возвращает настройки по умолчанию для обработки данных.
|
||||||
|
"""
|
||||||
logger.debug("Получение настроек парсера по умолчанию.")
|
logger.debug("Получение настроек парсера по умолчанию.")
|
||||||
return {
|
return {
|
||||||
"date_format": "%Y-%m-%dT%H:%M:%S",
|
"date_format": "%Y-%m-%dT%H:%M:%S",
|
||||||
@@ -67,28 +454,25 @@ class BnovoPMSPlugin(BasePMSPlugin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def _get_stored_token(self):
|
async def _get_stored_token(self):
|
||||||
"""Получение токена из конфигурации PMS отеля."""
|
"""
|
||||||
|
Получает токен из конфигурации PMS отеля.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Попытка получения токена для отеля {self.hotel.id}.")
|
logger.debug(f"Попытка получения токена для отеля {self.hotel.id}.")
|
||||||
token = self.pms_config.token
|
token = self.pms_config.token
|
||||||
logger.debug(f"Токен из базы данных: {token}")
|
if not token:
|
||||||
|
logger.info("Токен отсутствует в конфигурации.")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Токен найден: {token}")
|
||||||
return token
|
return token
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Ошибка при получении токена для отеля {self.hotel.id}: {e}")
|
logger.error(f"Ошибка при получении токена для отеля {self.hotel.id}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _save_token_to_db(self, sid):
|
|
||||||
"""Сохраняет токен (SID) в конфигурации PMS отеля."""
|
|
||||||
try:
|
|
||||||
logger.debug(f"Сохранение токена для отеля {self.hotel.id}: {sid}")
|
|
||||||
self.pms_config.token = sid
|
|
||||||
await sync_to_async(self.pms_config.save)()
|
|
||||||
logger.debug("Токен успешно сохранен.")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка сохранения токена для отеля {self.hotel.id}: {e}")
|
|
||||||
|
|
||||||
def _get_auth_headers(self):
|
def _get_auth_headers(self):
|
||||||
"""Создает заголовки авторизации."""
|
"""
|
||||||
|
Создает заголовки авторизации для запросов к API.
|
||||||
|
"""
|
||||||
logger.debug("Создание заголовков авторизации.")
|
logger.debug("Создание заголовков авторизации.")
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -97,81 +481,54 @@ class BnovoPMSPlugin(BasePMSPlugin):
|
|||||||
if self.token:
|
if self.token:
|
||||||
headers["Cookie"] = f"SID={self.token}"
|
headers["Cookie"] = f"SID={self.token}"
|
||||||
logger.debug(f"Добавлен токен в заголовки: {self.token}")
|
logger.debug(f"Добавлен токен в заголовки: {self.token}")
|
||||||
|
else:
|
||||||
|
logger.warning("Токен отсутствует, запрос может быть неавторизованным.")
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
async def _fetch_session(self):
|
|
||||||
"""Получение нового токена (SID) через запрос."""
|
|
||||||
url = f"{self.api_url}/"
|
|
||||||
payload = {"username": self.username, "password": self.password}
|
|
||||||
headers = self._get_auth_headers()
|
|
||||||
|
|
||||||
logger.debug(f"Авторизация по адресу: {url} с данными: {json.dumps(payload)}")
|
async def _ensure_token(self):
|
||||||
response = requests.post(url, json=payload, headers=headers, allow_redirects=False)
|
"""
|
||||||
|
Убеждается, что токен (SID) доступен. Если его нет, выполняет авторизацию.
|
||||||
logger.debug(f"Ответ авторизации: статус {response.status_code}, заголовки {response.headers}")
|
"""
|
||||||
if response.status_code == 302:
|
logger.debug(f"Проверка токена для отеля {self.hotel.id}.")
|
||||||
cookies = response.cookies.get_dict()
|
if not self.token:
|
||||||
sid = cookies.get("SID")
|
|
||||||
if sid:
|
|
||||||
self.token = sid
|
|
||||||
logger.debug(f"Получен новый SID: {sid}")
|
|
||||||
await self._save_token_to_db(sid)
|
|
||||||
else:
|
|
||||||
logger.error("Не удалось извлечь SID из ответа.")
|
|
||||||
raise ValueError("Не удалось извлечь SID из ответа.")
|
|
||||||
else:
|
|
||||||
logger.error(f"Ошибка авторизации: {response.status_code}, {response.text}")
|
|
||||||
raise ValueError(f"Ошибка авторизации: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
async def _fetch_account_data(self):
|
|
||||||
"""Получение данных аккаунта через эндпоинт /account/current."""
|
|
||||||
logger.info(f"Начало получения данных аккаунта для отеля {self.hotel.id}.")
|
|
||||||
self.token = await self._get_stored_token()
|
self.token = await self._get_stored_token()
|
||||||
|
|
||||||
if not self.token:
|
if not self.token:
|
||||||
logger.info("Токен отсутствует, выполняем авторизацию.")
|
logger.info("Токен отсутствует, выполняем авторизацию.")
|
||||||
await self._fetch_session()
|
await self._fetch_session()
|
||||||
|
else:
|
||||||
|
logger.debug(f"Используется сохраненный токен: {self.token}")
|
||||||
|
|
||||||
url = f"{self.api_url}/account/current"
|
async def _save_token_to_db(self, sid):
|
||||||
headers = self._get_auth_headers()
|
"""
|
||||||
|
Сохраняет токен (SID) в конфигурации PMS отеля.
|
||||||
logger.debug(f"Выполнение запроса к {url}")
|
"""
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
logger.error(f"Ошибка при запросе данных аккаунта: {response.status_code}, {response.text}")
|
|
||||||
raise ValueError("Ошибка запроса к /account/current")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
account_data = response.json()
|
logger.debug(f"Сохранение токена для отеля {self.hotel.id}: {sid}")
|
||||||
logger.debug(f"Полученные данные аккаунта:")
|
self.pms_config.token = sid
|
||||||
except json.JSONDecodeError as e:
|
await sync_to_async(self.pms_config.save)()
|
||||||
logger.error(f"Ошибка декодирования JSON: {e}")
|
logger.info(f"Токен {sid} успешно сохранен в базу данных.")
|
||||||
raise ValueError(f"Ошибка декодирования JSON: {e}")
|
|
||||||
|
|
||||||
return account_data
|
|
||||||
|
|
||||||
async def _fetch_and_log_account_data(self):
|
|
||||||
"""Вызов метода _fetch_account_data и вывод результата в лог."""
|
|
||||||
logger.info(f"Запуск получения и логирования данных аккаунта для отеля {self.hotel.id}.")
|
|
||||||
try:
|
|
||||||
account_data = await self._fetch_account_data()
|
|
||||||
logger.info(f"Успешно полученные данные аккаунта:")
|
|
||||||
return account_data
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при получении данных аккаунта: {e}")
|
logger.error(f"Ошибка сохранения токена для отеля {self.hotel.id}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def _fetch_data_with_account_info(self):
|
async def _fetch_session(self):
|
||||||
"""Получение данных аккаунта и бронирований."""
|
"""Получение токена (SID) через авторизацию."""
|
||||||
logger.info(f"Запуск процесса получения данных аккаунта и бронирований для отеля {self.hotel.id}.")
|
url = f"{self.api_url}/"
|
||||||
try:
|
payload = {"username": self.username, "password": self.password}
|
||||||
account_data = await self.fetch_and_log_account_data()
|
headers = {"Content-Type": "application/json"}
|
||||||
logger.info("Данные аккаунта успешно получены, продолжение с бронированиями.")
|
await self._save_token_to_db(self.token)
|
||||||
await self.__fetch_data()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при выполнении полной операции: {e}")
|
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
response = requests.post(url, json=payload, headers=headers, allow_redirects=False)
|
||||||
|
if response.status_code == 302:
|
||||||
|
self.token = response.cookies.get("SID")
|
||||||
|
await self._save_token_to_db(self.token)
|
||||||
|
else:
|
||||||
|
logger.error(f"Ошибка авторизации: {response.status_code}, {response.text}")
|
||||||
|
raise ValueError("Ошибка авторизации")
|
||||||
|
=======
|
||||||
async def _fetch_data(self):
|
async def _fetch_data(self):
|
||||||
"""Получение данных о бронированиях с помощью эндпоинта /dashboard."""
|
"""Получение данных о бронированиях с помощью эндпоинта /dashboard."""
|
||||||
logger.info("Начало процесса получения данных о бронированиях.")
|
logger.info("Начало процесса получения данных о бронированиях.")
|
||||||
@@ -183,10 +540,19 @@ class BnovoPMSPlugin(BasePMSPlugin):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка получения данных аккаунта: {e}")
|
logger.error(f"Ошибка получения данных аккаунта: {e}")
|
||||||
raise
|
raise
|
||||||
|
>>>>>>> PMSManager_refactor
|
||||||
|
|
||||||
|
async def _fetch_paginated_data(self):
|
||||||
|
"""
|
||||||
|
Получает все данные с API, обрабатывая страницы с пагинацией.
|
||||||
|
"""
|
||||||
|
logger.info("Начало получения данных с пагинацией.")
|
||||||
|
await self._ensure_token()
|
||||||
url = f"{self.api_url}/dashboard"
|
url = f"{self.api_url}/dashboard"
|
||||||
|
headers = self._get_auth_headers()
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
create_from = (now - timedelta(days=1)).strftime("%d.%m.%Y")
|
create_from = (now - timedelta(days=7)).strftime("%d.%m.%Y") # Получаем данные за последнюю неделю
|
||||||
create_to = now.strftime("%d.%m.%Y")
|
create_to = now.strftime("%d.%m.%Y")
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
@@ -197,30 +563,25 @@ class BnovoPMSPlugin(BasePMSPlugin):
|
|||||||
"page": 1,
|
"page": 1,
|
||||||
"order_by": "create_date.asc",
|
"order_by": "create_date.asc",
|
||||||
}
|
}
|
||||||
headers = self._get_auth_headers()
|
|
||||||
|
|
||||||
all_bookings = []
|
all_bookings = []
|
||||||
|
try:
|
||||||
while True:
|
while True:
|
||||||
logger.debug(f"Запрос к {url} с параметрами: {json.dumps(params, indent=2)}")
|
logger.debug(f"Запрос к {url} с параметрами: {json.dumps(params, indent=2)}")
|
||||||
try:
|
|
||||||
response = requests.get(url, headers=headers, params=params, allow_redirects=False)
|
response = requests.get(url, headers=headers, params=params, allow_redirects=False)
|
||||||
|
|
||||||
if response.status_code == 302:
|
if response.status_code == 302:
|
||||||
logger.warning("Получен код 302. Перенаправление.")
|
logger.warning("Получен код 302. Перенаправление.")
|
||||||
redirected_url = response.headers.get("Location")
|
await self._fetch_session()
|
||||||
if redirected_url:
|
headers = self._get_auth_headers()
|
||||||
logger.debug(f"Перенаправление на {redirected_url}")
|
|
||||||
url = redirected_url
|
|
||||||
continue
|
continue
|
||||||
else:
|
|
||||||
logger.error("Ответ с кодом 302 не содержит заголовка Location.")
|
|
||||||
raise ValueError("Перенаправление без указанного URL.")
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}")
|
logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}")
|
||||||
raise ValueError("Ошибка запроса к /dashboard")
|
raise ValueError(f"Ошибка при получении данных: {response.text}")
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
logger.debug(f"Полученный ответ API: {json.dumps(data, indent=2, ensure_ascii=False)}")
|
||||||
bookings = data.get("bookings", [])
|
bookings = data.get("bookings", [])
|
||||||
rooms = data.get("rooms", [])
|
rooms = data.get("rooms", [])
|
||||||
|
|
||||||
@@ -228,54 +589,88 @@ class BnovoPMSPlugin(BasePMSPlugin):
|
|||||||
logger.debug(f'bookings: {bookings}\n rooms: {rooms}')
|
logger.debug(f'bookings: {bookings}\n rooms: {rooms}')
|
||||||
all_bookings.extend(bookings)
|
all_bookings.extend(bookings)
|
||||||
|
|
||||||
logger.info(f"Получено бронирований: {len(bookings)}. Всего: {len(all_bookings)}.")
|
|
||||||
|
|
||||||
pages_info = data.get("pages", {})
|
pages_info = data.get("pages", {})
|
||||||
current_page = pages_info.get("current_page", 1)
|
current_page = pages_info.get("current_page", 1)
|
||||||
total_pages = pages_info.get("total_pages", 1)
|
total_pages = pages_info.get("total_pages", 1)
|
||||||
|
|
||||||
logger.debug(f"Информация о страницах: текущая {current_page}, всего {total_pages}")
|
|
||||||
if current_page >= total_pages:
|
if current_page >= total_pages:
|
||||||
break
|
break
|
||||||
|
|
||||||
params["page"] += 1
|
params["page"] += 1
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error(f"Ошибка декодирования JSON: {e}. Ответ: {response.text}")
|
|
||||||
raise ValueError(f"Ошибка декодирования JSON: {e}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Неизвестная ошибка при обработке запроса: {e}")
|
logger.error(f"Ошибка при обработке данных: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Сопоставляем бронирования с существующими записями
|
|
||||||
for booking in all_bookings:
|
|
||||||
try:
|
|
||||||
booking_id = booking.get("id")
|
|
||||||
hotel_id = booking.get("hotel_id")
|
|
||||||
|
|
||||||
if hotel_id != str(self.hotel.external_id_pms):
|
async def _process_and_save_bookings(self, bookings):
|
||||||
logger.debug(f"Бронирование {booking_id} не относится к отелю {self.hotel.external_id_pms}. Пропуск.")
|
"""
|
||||||
|
Обрабатывает и сохраняет бронирования в базу.
|
||||||
|
"""
|
||||||
|
logger.info("Начало обработки данных о бронированиях для сохранения в базу.")
|
||||||
|
processed_items = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for record in bookings:
|
||||||
|
try:
|
||||||
|
booking_id = record.get("id")
|
||||||
|
room_number = record.get("current_room")
|
||||||
|
arrival = record.get("arrival")
|
||||||
|
departure = record.get("departure")
|
||||||
|
status = record.get("status_name")
|
||||||
|
|
||||||
|
# Проверка обязательных данных
|
||||||
|
if not (booking_id and room_number and arrival and departure and status):
|
||||||
|
logger.warning(f"Пропуск записи из-за отсутствия обязательных данных: {record}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Сохраняем или обновляем запись в базе данных
|
||||||
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||||
external_id=booking_id,
|
external_id=booking_id,
|
||||||
defaults={
|
defaults={
|
||||||
"hotel": self.hotel, # Объект модели Hotel
|
"hotel": self.hotel,
|
||||||
"status": booking.get("status_name"), # Статус бронирования
|
"status": status,
|
||||||
"room_number": booking.get("current_room"), # Номер комнаты (исправлено с create_date)
|
"room_number": room_number,
|
||||||
"check_in": booking.get("arrival"), # Дата заезда
|
"check_in": arrival,
|
||||||
"check_out": booking.get("departure"), # Дата выезда
|
"check_out": departure,
|
||||||
"room_type": booking.get("initial_room_type_name") # Тип комнаты
|
},
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
logger.info(f"Создана новая запись бронирования: {reservation}")
|
logger.info(f"Создана новая запись бронирования: {reservation}")
|
||||||
|
print(reservation)
|
||||||
else:
|
else:
|
||||||
logger.info(f"Обновлено существующее бронирование: {reservation}")
|
logger.info(f"Обновлено существующее бронирование: {reservation}")
|
||||||
|
|
||||||
except Exception as e:
|
processed_items += 1
|
||||||
logger.error(f"Ошибка обработки бронирования {booking.get('id')}: {e}")
|
|
||||||
|
|
||||||
logger.info(f"Все бронирования получены и обработаны. Итоговое количество: {len(all_bookings)}")
|
except Exception as e:
|
||||||
return all_bookings
|
logger.error(f"Ошибка обработки бронирования {record.get('id')}: {e}")
|
||||||
|
errors.append(str(e))
|
||||||
|
|
||||||
|
logger.info(f"Обработано бронирований: {processed_items}, ошибок: {len(errors)}")
|
||||||
|
return {"processed_items": processed_items, "errors": errors}
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_data(self):
|
||||||
|
"""
|
||||||
|
Получает данные о бронированиях с API и возвращает их.
|
||||||
|
"""
|
||||||
|
logger.info("Начало процесса получения данных о бронированиях.")
|
||||||
|
try:
|
||||||
|
bookings = await self._fetch_paginated_data() # Получаем данные с пагинацией
|
||||||
|
return bookings
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка в процессе получения данных: {e}")
|
||||||
|
raise ValueError("Ошибка при получении данных о бронированиях")
|
||||||
|
|
||||||
|
async def fetch_and_process_data(self):
|
||||||
|
"""Получение данных с API, обработка и сохранение в базу."""
|
||||||
|
logger.info("Начало загрузки данных с API")
|
||||||
|
try:
|
||||||
|
bookings = await self._fetch_paginated_data()
|
||||||
|
report = await self._process_and_save_bookings(bookings)
|
||||||
|
logger.info(f"Данные успешно обработаны. Отчет: {report}")
|
||||||
|
return report
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка загрузки и обработки данных: {e}")
|
||||||
|
raise
|
||||||
|
|||||||
@@ -1,3 +1,130 @@
|
|||||||
|
# import logging
|
||||||
|
# import requests
|
||||||
|
# from datetime import datetime, timedelta
|
||||||
|
# from asgiref.sync import sync_to_async
|
||||||
|
# from pms_integration.models import PMSConfiguration
|
||||||
|
# from hotels.models import Hotel, Reservation
|
||||||
|
# from .base_plugin import BasePMSPlugin
|
||||||
|
|
||||||
|
|
||||||
|
# class ShelterPMSPlugin(BasePMSPlugin):
|
||||||
|
# """
|
||||||
|
# Плагин для интеграции с PMS Shelter.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# def __init__(self, pms_config):
|
||||||
|
# super().__init__(pms_config)
|
||||||
|
|
||||||
|
# # Настройка логирования
|
||||||
|
# self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
# handler = logging.StreamHandler()
|
||||||
|
# formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
# handler.setFormatter(formatter)
|
||||||
|
# self.logger.addHandler(handler)
|
||||||
|
# self.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# # Инициализация параметров API
|
||||||
|
# self.api_url = pms_config.url
|
||||||
|
# self.token = pms_config.token
|
||||||
|
# self.username = pms_config.username
|
||||||
|
# self.password = pms_config.password
|
||||||
|
|
||||||
|
# def get_default_parser_settings(self):
|
||||||
|
# """
|
||||||
|
# Возвращает настройки парсера по умолчанию.
|
||||||
|
# """
|
||||||
|
# return {
|
||||||
|
# "field_mapping": {
|
||||||
|
# "check_in": "checkin",
|
||||||
|
# "check_out": "checkout",
|
||||||
|
# "room_number": "room_number",
|
||||||
|
# "room_type": "room_type",
|
||||||
|
# "status": "status",
|
||||||
|
# },
|
||||||
|
# "date_format": "%Y-%m-%dT%H:%M:%S"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# async def _fetch_data(self):
|
||||||
|
# """
|
||||||
|
# Получает данные из Shelter PMS API и сохраняет их в базу данных.
|
||||||
|
# """
|
||||||
|
# now = datetime.now()
|
||||||
|
# start_date = (now - timedelta(days=1)).strftime('%Y-%m-%d')
|
||||||
|
# end_date = now.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# headers = {
|
||||||
|
# "Authorization": f"Bearer {self.token}",
|
||||||
|
# "Content-Type": "application/json",
|
||||||
|
# }
|
||||||
|
# params = {
|
||||||
|
# "start_date": start_date,
|
||||||
|
# "end_date": end_date,
|
||||||
|
# }
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# response = await sync_to_async(requests.get)(f"{self.api_url}/reservations", headers=headers, params=params)
|
||||||
|
# response.raise_for_status()
|
||||||
|
# data = response.json()
|
||||||
|
# self.logger.debug(f"Получены данные с API: {data}")
|
||||||
|
# except requests.exceptions.RequestException as e:
|
||||||
|
# self.logger.error(f"Ошибка запроса к API Shelter: {e}")
|
||||||
|
# return []
|
||||||
|
|
||||||
|
# # Обработка и сохранение данных
|
||||||
|
# processed_data = []
|
||||||
|
# for item in data.get("reservations", []):
|
||||||
|
# processed_item = {
|
||||||
|
# "room_number": item.get("room_number"),
|
||||||
|
# "check_in": datetime.strptime(item.get("check_in"), '%Y-%m-%dT%H:%M:%S'),
|
||||||
|
# "check_out": datetime.strptime(item.get("check_out"), '%Y-%m-%dT%H:%M:%S'),
|
||||||
|
# "status": item.get("status"),
|
||||||
|
# "room_type": item.get("room_type"),
|
||||||
|
# }
|
||||||
|
# processed_data.append(processed_item)
|
||||||
|
# await self._save_to_db(processed_item)
|
||||||
|
|
||||||
|
# self.logger.debug("Все данные успешно сохранены в базу данных.")
|
||||||
|
# return processed_data
|
||||||
|
|
||||||
|
# async def _save_to_db(self, item):
|
||||||
|
# """
|
||||||
|
# Сохраняет данные в базу данных.
|
||||||
|
# """
|
||||||
|
# try:
|
||||||
|
# hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
|
||||||
|
|
||||||
|
# reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||||
|
# room_number=item["room_number"],
|
||||||
|
# check_in=item["check_in"],
|
||||||
|
# defaults={
|
||||||
|
# "check_out": item["check_out"],
|
||||||
|
# "status": item["status"],
|
||||||
|
# "hotel": hotel,
|
||||||
|
# "room_type": item["room_type"],
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
|
||||||
|
# if created:
|
||||||
|
# self.logger.debug(f"Создана новая запись бронирования: {reservation}")
|
||||||
|
# else:
|
||||||
|
# self.logger.debug(f"Обновлено существующее бронирование: {reservation}")
|
||||||
|
|
||||||
|
# except Exception as e:
|
||||||
|
# self.logger.error(f"Ошибка при сохранении данных в БД: {e}")
|
||||||
|
|
||||||
|
# async def fetch_and_process_data(self):
|
||||||
|
# """
|
||||||
|
# Загружает данные с API Shelter и сохраняет их в базу данных.
|
||||||
|
# """
|
||||||
|
# self.logger.info("Начало процесса загрузки данных из Shelter PMS.")
|
||||||
|
# try:
|
||||||
|
# data = await self._fetch_data()
|
||||||
|
# self.logger.info(f"Загрузка и обработка данных завершены. Обработано записей: {len(data)}")
|
||||||
|
# return data
|
||||||
|
# except Exception as e:
|
||||||
|
# self.logger.error(f"Ошибка в процессе загрузки данных: {e}")
|
||||||
|
# raise
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
@@ -6,6 +133,25 @@ from datetime import datetime, timedelta
|
|||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
from hotels.models import Hotel, Reservation
|
from hotels.models import Hotel, Reservation
|
||||||
from .base_plugin import BasePMSPlugin
|
from .base_plugin import BasePMSPlugin
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
|
class ShelterPMSPlugin(BasePMSPlugin):
|
||||||
|
"""
|
||||||
|
Плагин для интеграции с PMS Shelter (интерфейс для получения данных об отеле).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, pms_config):
|
||||||
|
super().__init__(pms_config)
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
self.logger.addHandler(handler)
|
||||||
|
self.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
self.api_url = "https://pms.frontdesk24.ru/sheltercloudapi/Reservations/"
|
||||||
|
self.access_token = "679CA9C5-9847-4151-883E-5F61181AA37E"
|
||||||
|
=======
|
||||||
from touchh.utils.log import CustomLogger
|
from touchh.utils.log import CustomLogger
|
||||||
class ShelterPMSPlugin(BasePMSPlugin):
|
class ShelterPMSPlugin(BasePMSPlugin):
|
||||||
"""
|
"""
|
||||||
@@ -34,24 +180,65 @@ class ShelterPMSPlugin(BasePMSPlugin):
|
|||||||
self.logger.addHandler(handler_console)
|
self.logger.addHandler(handler_console)
|
||||||
self.logger.addHandler(handler_file)
|
self.logger.addHandler(handler_file)
|
||||||
self.logger.setLevel(logging.WARNING)
|
self.logger.setLevel(logging.WARNING)
|
||||||
|
>>>>>>> PMSManager_refactor
|
||||||
|
|
||||||
def get_default_parser_settings(self):
|
def get_default_parser_settings(self):
|
||||||
"""
|
"""
|
||||||
Возвращает настройки парсера по умолчанию.
|
Возвращает настройки по умолчанию для обработки данных.
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"field_mapping": {
|
"field_mapping": {
|
||||||
|
<<<<<<< HEAD
|
||||||
|
"check_in": "check_in",
|
||||||
|
"check_out": "check_out",
|
||||||
|
"room_number": "room_number",
|
||||||
|
"status": "status",
|
||||||
|
=======
|
||||||
"check_in": "from",
|
"check_in": "from",
|
||||||
"check_out": "until",
|
"check_out": "until",
|
||||||
"room_number": "roomNumber",
|
"room_number": "roomNumber",
|
||||||
"room_type_name": "roomTypeName",
|
"room_type_name": "roomTypeName",
|
||||||
"status": "checkInStatus",
|
"status": "checkInStatus",
|
||||||
|
>>>>>>> PMSManager_refactor
|
||||||
},
|
},
|
||||||
"date_format": "%Y-%m-%dT%H:%M:%S"
|
"date_format": "%Y-%m-%dT%H:%M:%S",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _fetch_data(self):
|
async def _fetch_data(self):
|
||||||
"""
|
"""
|
||||||
|
<<<<<<< HEAD
|
||||||
|
Получает данные бронирований с API PMS Shelter.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Формируем параметры запроса
|
||||||
|
now = datetime.now()
|
||||||
|
create_from = (now - timedelta(days=7)).strftime("%Y-%m-%d")
|
||||||
|
create_to = now.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"from": create_from,
|
||||||
|
"until": create_to,
|
||||||
|
"pagination": {
|
||||||
|
"from": 0,
|
||||||
|
"count": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "text/plain",
|
||||||
|
"Authorization": f"Bearer {self.access_token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Логирование запроса
|
||||||
|
self.logger.debug(f"Отправка запроса к API: {self.api_url}")
|
||||||
|
self.logger.debug(f"Тело запроса: {data}")
|
||||||
|
self.logger.debug(f"Заголовки: {headers}")
|
||||||
|
|
||||||
|
# Выполняем запрос
|
||||||
|
response = requests.post(self.api_url, json=data, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
=======
|
||||||
Получает данные из PMS API и сохраняет их в базу.
|
Получает данные из PMS API и сохраняет их в базу.
|
||||||
"""
|
"""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
@@ -180,58 +367,69 @@ class ShelterPMSPlugin(BasePMSPlugin):
|
|||||||
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
|
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
|
||||||
self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
|
self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
|
||||||
return True
|
return True
|
||||||
|
>>>>>>> PMSManager_refactor
|
||||||
|
|
||||||
async def _save_to_db(self, item):
|
# Обрабатываем ответ
|
||||||
|
bookings = response.json()
|
||||||
|
self.logger.info(f"Получено бронирований: {len(bookings)}")
|
||||||
|
return bookings
|
||||||
|
|
||||||
|
except requests.HTTPError as http_err:
|
||||||
|
self.logger.error(f"HTTP ошибка: {http_err}")
|
||||||
|
self.logger.error(f"Текст ответа: {response.text if 'response' in locals() else 'Нет данных'}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Ошибка получения данных PMS Shelter: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def fetch_and_process_data(self):
|
||||||
"""
|
"""
|
||||||
Сохраняет данные в БД (например, информацию о номере).
|
Получение данных с API, обработка и сохранение в базу.
|
||||||
"""
|
"""
|
||||||
|
self.logger.info("Начало загрузки данных с API Shelter")
|
||||||
try:
|
try:
|
||||||
# Проверяем, что item — это словарь
|
bookings = await self._fetch_data()
|
||||||
if not isinstance(item, dict):
|
report = await self._process_and_save_bookings(bookings)
|
||||||
self.logger.error(f"Ожидался словарь, но получен: {type(item)}. Данные: {item}")
|
self.logger.info(f"Данные успешно обработаны. Отчет: {report}")
|
||||||
return
|
return report
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Ошибка загрузки и обработки данных: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
# Получаем отель по настройкам PMS
|
async def _process_and_save_bookings(self, bookings):
|
||||||
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
|
"""
|
||||||
self.logger.debug(f"Отель найден: {hotel.name}")
|
Обрабатывает и сохраняет бронирования в базу данных.
|
||||||
|
"""
|
||||||
|
self.logger.info("Начало обработки данных о бронированиях для сохранения в базу.")
|
||||||
|
processed_items = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
# Сохраняем данные бронирования
|
for record in bookings:
|
||||||
room_number = item.get("room_number")
|
try:
|
||||||
check_in = item.get("checkin")
|
# Пример обработки данных бронирования
|
||||||
check_out = item.get("checkout")
|
booking_id = record.get("id")
|
||||||
room_type = item.get("room_type")
|
room_number = record.get("room_number")
|
||||||
|
check_in = record.get("check_in")
|
||||||
|
check_out = record.get("check_out")
|
||||||
|
status = record.get("status")
|
||||||
|
|
||||||
# Логируем полученные данные
|
# Сохраняем или обновляем запись в базе данных
|
||||||
self.logger.debug(f"Полученные данные для сохранения: room_number={room_number}, check_in={check_in}, check_out={check_out}, room_type={room_type}")
|
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||||
|
external_id=booking_id,
|
||||||
# Проверяем, существует ли запись с таким номером и датой заезда
|
|
||||||
existing_reservation = await sync_to_async(
|
|
||||||
Reservation.objects.filter(room_number=room_number, check_in=check_in).first
|
|
||||||
)()
|
|
||||||
|
|
||||||
if existing_reservation:
|
|
||||||
self.logger.debug(f"Резервация с таким номером и датой заезда уже существует. Обновляем...")
|
|
||||||
await sync_to_async(Reservation.objects.update_or_create)(
|
|
||||||
room_number=room_number,
|
|
||||||
check_in=check_in,
|
|
||||||
defaults={
|
defaults={
|
||||||
|
"hotel": self.pms_config.hotel,
|
||||||
|
"room_number": room_number,
|
||||||
|
"check_in": check_in,
|
||||||
"check_out": check_out,
|
"check_out": check_out,
|
||||||
"hotel": hotel,
|
"status": status,
|
||||||
"room_type": room_type,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.logger.debug(f"Обновлена существующая резервация.")
|
|
||||||
else:
|
processed_items += 1
|
||||||
self.logger.debug(f"Резервация не найдена, создаем новую...")
|
|
||||||
reservation = await sync_to_async(Reservation.objects.create)(
|
|
||||||
room_number=room_number,
|
|
||||||
check_in=check_in,
|
|
||||||
check_out=check_out,
|
|
||||||
hotel=hotel,
|
|
||||||
room_type=room_type,
|
|
||||||
)
|
|
||||||
self.logger.debug(f"Создана новая резервация. ID: {reservation.reservation_id}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Ошибка сохранения данных: {e}")
|
self.logger.error(f"Ошибка обработки бронирования {record.get('id')}: {e}")
|
||||||
|
errors.append(str(e))
|
||||||
|
|
||||||
|
self.logger.info(f"Обработано бронирований: {processed_items}, ошибок: {len(errors)}")
|
||||||
|
return {"processed_items": processed_items, "errors": errors}
|
||||||
|
|||||||
65
req1.txt
Normal file
65
req1.txt
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
ace_tools==0.0
|
||||||
|
aiohappyeyeballs==2.4.4
|
||||||
|
aiohttp==3.11.11
|
||||||
|
aiosignal==1.3.2
|
||||||
|
anyio==4.7.0
|
||||||
|
APScheduler==3.11.0
|
||||||
|
asgiref==3.8.1
|
||||||
|
async-timeout==5.0.1
|
||||||
|
attrs==24.3.0
|
||||||
|
certifi==2024.12.14
|
||||||
|
cffi==1.17.1
|
||||||
|
charset-normalizer==3.4.0
|
||||||
|
cryptography==44.0.0
|
||||||
|
defusedxml==0.7.1
|
||||||
|
Django==5.1.4
|
||||||
|
django-environ==0.11.2
|
||||||
|
django-extensions==3.2.3
|
||||||
|
django-filter==24.3
|
||||||
|
django-health-check==3.18.3
|
||||||
|
django-jazzmin==3.0.1
|
||||||
|
django-jet==1.0.8
|
||||||
|
et_xmlfile==2.0.0
|
||||||
|
exceptiongroup==1.2.2
|
||||||
|
fonttools==4.55.3
|
||||||
|
fpdf2==2.8.2
|
||||||
|
frozenlist==1.5.0
|
||||||
|
geoip2==4.8.1
|
||||||
|
git-filter-repo==2.47.0
|
||||||
|
h11==0.14.0
|
||||||
|
httpcore==1.0.7
|
||||||
|
httpx==0.28.1
|
||||||
|
idna==3.10
|
||||||
|
jsonschema==4.23.0
|
||||||
|
jsonschema-specifications==2024.10.1
|
||||||
|
maxminddb==2.6.2
|
||||||
|
multidict==6.1.0
|
||||||
|
numpy==2.2.0
|
||||||
|
openpyxl==3.1.5
|
||||||
|
pandas==2.2.3
|
||||||
|
pathspec==0.12.1
|
||||||
|
pillow==11.0.0
|
||||||
|
propcache==0.2.1
|
||||||
|
psycopg==3.2.3
|
||||||
|
pycparser==2.22
|
||||||
|
PyMySQL==1.1.1
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
python-decouple==3.8
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
python-telegram-bot==21.9
|
||||||
|
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.3
|
||||||
|
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
|
||||||
|
user-agents==2.2.0
|
||||||
|
yarl==1.18.3
|
||||||
0
scheduler/management/commands/__init__.py
Normal file
0
scheduler/management/commands/__init__.py
Normal file
27
scheduler/management/commands/run_scheduler.py
Normal file
27
scheduler/management/commands/run_scheduler.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import os
|
||||||
|
import django
|
||||||
|
import asyncio
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from scheduler.tasks import setup_scheduler
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Запуск планировщика задач"
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# Устанавливаем Django окружение
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings")
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
# Основная асинхронная функция
|
||||||
|
async def start_scheduler():
|
||||||
|
scheduler = await setup_scheduler()
|
||||||
|
self.stdout.write(self.style.SUCCESS("Планировщик задач успешно запущен."))
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(3600) # Бесконечный цикл для поддержания работы
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
scheduler.shutdown()
|
||||||
|
|
||||||
|
# Запускаем планировщик в асинхронном режиме
|
||||||
|
asyncio.run(start_scheduler())
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2024-12-27 05:52
|
||||||
|
|
||||||
|
import scheduler.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('scheduler', '0013_alter_scheduledtask_function_path'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='scheduledtask',
|
||||||
|
name='function_path',
|
||||||
|
field=models.CharField(choices=scheduler.models.get_available_functions, max_length=500, verbose_name='Путь к функции (модуль.функция)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
95
scheduler/task_loader.py
Normal file
95
scheduler/task_loader.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import os
|
||||||
|
import inspect
|
||||||
|
import importlib
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import List, Tuple
|
||||||
|
from pathspec import PathSpec
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def reload_tasks_periodically(scheduler: AsyncIOScheduler):
|
||||||
|
"""Перезагрузка задач из базы данных каждые 5 минут."""
|
||||||
|
async def reload():
|
||||||
|
from scheduler.tasks import load_tasks_to_scheduler
|
||||||
|
try:
|
||||||
|
await load_tasks_to_scheduler(scheduler)
|
||||||
|
logger.info("Задачи успешно перезагружены.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка перезагрузки задач: {e}")
|
||||||
|
|
||||||
|
scheduler.add_job(lambda: asyncio.run(reload()), "interval", minutes=5)
|
||||||
|
|
||||||
|
def load_gitignore_patterns(project_root: str) -> PathSpec:
|
||||||
|
"""
|
||||||
|
Загружает паттерны из файла .gitignore.
|
||||||
|
"""
|
||||||
|
gitignore_path = os.path.join(project_root, ".gitignore")
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Ошибка загрузки .gitignore: {e}")
|
||||||
|
return PathSpec.from_lines("gitwildmatch", [])
|
||||||
|
|
||||||
|
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:
|
||||||
|
spec = importlib.util.find_spec(module_name)
|
||||||
|
if spec is not None:
|
||||||
|
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:
|
||||||
|
logger.error(f"Ошибка при обработке модуля {module_name}: {e}")
|
||||||
|
|
||||||
|
return functions
|
||||||
|
|
||||||
|
def execute_function(function_path: str):
|
||||||
|
"""
|
||||||
|
Выполняет функцию по указанному пути.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
module_name, func_name = function_path.rsplit(".", 1)
|
||||||
|
spec = importlib.util.find_spec(module_name)
|
||||||
|
if spec is None:
|
||||||
|
raise ImportError(f"Модуль {module_name} не найден")
|
||||||
|
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
if not hasattr(module, func_name):
|
||||||
|
raise AttributeError(f"Функция {func_name} отсутствует в модуле {module_name}")
|
||||||
|
|
||||||
|
func = getattr(module, func_name)
|
||||||
|
logger.info(f"Выполняется функция: {function_path}")
|
||||||
|
return func()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка выполнения функции {function_path}: {e}")
|
||||||
|
return None
|
||||||
14
scheduler/urls.py
Normal file
14
scheduler/urls.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
def placeholder_view(request):
|
||||||
|
"""
|
||||||
|
Заглушка для URL-адресов приложения scheduler.
|
||||||
|
"""
|
||||||
|
return HttpResponse("Это заглушка для приложения scheduler.")
|
||||||
|
|
||||||
|
app_name = "scheduler"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", placeholder_view, name="scheduler_placeholder"),
|
||||||
|
]
|
||||||
43
staticfiles/admin/js/collapse.js
Normal file
43
staticfiles/admin/js/collapse.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*global gettext*/
|
||||||
|
'use strict';
|
||||||
|
{
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
// Add anchor tag for Show/Hide link
|
||||||
|
const fieldsets = document.querySelectorAll('fieldset.collapse');
|
||||||
|
for (const [i, elem] of fieldsets.entries()) {
|
||||||
|
// Don't hide if fields in this fieldset have errors
|
||||||
|
if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) {
|
||||||
|
elem.classList.add('collapsed');
|
||||||
|
const h2 = elem.querySelector('h2');
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.id = 'fieldsetcollapser' + i;
|
||||||
|
link.className = 'collapse-toggle';
|
||||||
|
link.href = '#';
|
||||||
|
link.textContent = gettext('Show');
|
||||||
|
h2.appendChild(document.createTextNode(' ('));
|
||||||
|
h2.appendChild(link);
|
||||||
|
h2.appendChild(document.createTextNode(')'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add toggle to hide/show anchor tag
|
||||||
|
const toggleFunc = function(ev) {
|
||||||
|
if (ev.target.matches('.collapse-toggle')) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
const fieldset = ev.target.closest('fieldset');
|
||||||
|
if (fieldset.classList.contains('collapsed')) {
|
||||||
|
// Show
|
||||||
|
ev.target.textContent = gettext('Hide');
|
||||||
|
fieldset.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
// Hide
|
||||||
|
ev.target.textContent = gettext('Show');
|
||||||
|
fieldset.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.querySelectorAll('fieldset.module').forEach(function(el) {
|
||||||
|
el.addEventListener('click', toggleFunc);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user