27 Commits

Author SHA1 Message Date
eb662f7fe6 Merge branch 'PMSManager_refactor'
Some checks reported errors
continuous-integration/drone Build was killed
2025-07-19 19:15:03 +09:00
2ad382f01a ci/cd 2025-07-19 18:35:42 +09:00
67e487c378 ci/cd prepare 2025-07-19 18:34:51 +09:00
0cc3e469cb init commit 2025-02-26 22:35:18 +09:00
d771df32d5 remote changes 2025-02-26 22:29:39 +09:00
51569fb5c6 Merge branch 'zorn-dev' into PMSManager_refactor 2025-02-02 09:36:12 +09:00
6721ae3e8c xMerge branch 'zorn-dev' of ssh://git.smartsoltech.kr:2222/SmartSolTech/touchh_bot into zorn-dev 2025-02-02 09:28:20 +09:00
24f1a40561 RealtyCalendar plugin develop 2025-02-02 09:26:44 +09:00
zorn
d8bb7493e3 ReservationChecker 2025-02-01 22:03:25 +10:00
zorn
aca071f450 Fix migrations 2025-02-01 19:49:46 +10:00
zorn
6b98cda299 Add fraud_checked indexes 2025-02-01 19:38:47 +10:00
d826232dca Merge pull request 'zorn-dev' (#9) from zorn-dev into PMSManager_refactor
Reviewed-on: SmartSolTech/touchh_bot#9
2025-02-01 06:56:02 +00:00
05509f79fb init 2025-02-01 15:53:05 +09:00
157f47d86d some issues 2025-02-01 15:52:16 +09:00
e5e7a7f054 Merge pull request 'zorn-dev' (#8) from zorn-dev into PMSManager_refactor
Reviewed-on: SmartSolTech/touchh_bot#8
2025-01-29 07:53:46 +00:00
d59ebaf005 migration merger issues fix 2024-12-28 17:47:38 +09:00
31cf30e344 sMerge branch 'zorn-dev' 2024-12-28 13:50:12 +09:00
7350989113 mergse 2024-12-28 09:44:58 +09:00
5e3ed91b3a main Merge branch 'PMSManager_refactor' 2024-12-27 14:48:40 +09:00
56175078d6 sMerge branch 'PMSManager_refactor' 2024-12-27 10:01:58 +09:00
0e45074ea5 sadasd 2024-12-25 13:12:26 +09:00
bc865303c5 sssMerge branch 'pms_plugins' 2024-12-25 12:02:04 +09:00
5c58a46e18 ssMerge branch 'pms_plugins' 2024-12-24 21:36:24 +09:00
3dd5f2238e merge antifraud 2024-12-23 10:27:44 +09:00
61fffd3be4 sMerge branch 'master' of git.smartsoltech.kr:trevor/touchh_bot 2024-12-21 21:56:40 +09:00
c535a51953 merge 2024-12-21 21:56:15 +09:00
5f434d8248 Merge pull request 'antifraud' (#4) from antifraud into master
Reviewed-on: trevor/touchh_bot#4
2024-12-18 11:45:35 +00:00
68 changed files with 15719 additions and 1891 deletions

View File

@@ -0,0 +1,6 @@
.venv
.venv/
.log
__pycache__
.history
.vscode

30
.docker/admin/Dockerfile Normal file
View 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
View 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 "$@"

View 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

View File

@@ -0,0 +1,6 @@
.venv
.venv/
.log
__pycache__
.history
.vscode

12
.docker/bot/Dockerfile Normal file
View 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"]

View 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

View 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"]

View File

@@ -0,0 +1,6 @@
.venv
.venv/
.log
__pycache__
.history
.vscode

View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Выполняем миграции
python manage.py migrate --no-input
# Запускаем приложение
exec "$@"

View 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

View File

@@ -12,20 +12,11 @@ steps:
- git reset --hard $DRONE_COMMIT
# Шаг 2: Обновление и запуск с помощью update.sh
- name: deploy_app
image: docker:24
environment:
MYSQL_PASSWORD: touchh
volumes:
- name: docker_sock
path: /var/run/docker.sock
commands:
- apk add --no-cache bash
- chmod +x ./bin/update
- docker-compose up -d
- until docker inspect -f '{{.State.Running}}' src-web-1 | grep true; do echo "Waiting for container to be running..."; sleep 5; done
- git branch --set-upstream-to=origin/PMSManager_refactor PMSManager_refactor || true
- ./bin/update
- name: docker-build
image: plugins/docker
settings:
repo: trevor198507/touchh-py
dry_run: true
# Шаг 3: Миграция базы данных
- name: run_migrations

View File

@@ -151,6 +151,7 @@ class UserActivityLogAdmin(admin.ModelAdmin):
get_hotel_name.short_description = "Отель"
get_room_number.short_description = "Комната"
# from .views import import_selected_hotels
# # Регистрируем admin класс для ImportedHotel
# @admin.register(ImportedHotel)
@@ -247,4 +248,3 @@ class RoomDiscrepancyAdmin(admin.ModelAdmin):
class Meta:
model = RoomDiscrepancy
fields = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']

View File

@@ -1,27 +1,149 @@
# import json
# from datetime import timedelta
# from django.utils import timezone
# from django.db.models import Q
# from hotels.models import Reservation, Hotel
# from .models import UserActivityLog, RoomDiscrepancy
# from touchh.utils.log import CustomLogger
# # Настройка логирования
# logger = CustomLogger(__name__).get_logger()
# class ReservationChecker:
# """
# Класс для проверки несоответствий между бронированиями и логами заселения.
# """
# def __init__(self):
# self.checkin_diff_hours = 3
# def log_info(self, message):
# logger.info(message)
# def log_warning(self, message):
# logger.warning(message)
# def log_error(self, message):
# logger.error(message)
# def run_check(self):
# """Запуск проверки фродовых событий."""
# self.log_info("Запуск проверки фродовых данных.")
# try:
# check_in_diff = timedelta(hours=self.checkin_diff_hours)
# # Кэшируем отели в словарь для быстрого доступа
# hotels_map = {hotel.hotel_id: hotel for hotel in Hotel.objects.all()}
# # Загружаем бронирования и активности пользователей
# user_logs = UserActivityLog.objects.filter(fraud_checked=False)
# reservations = Reservation.objects.filter(fraud_checked=False).select_related('hotel')
# # Преобразуем бронирования в словарь для быстрого поиска
# reservations_map = {
# (res.hotel.hotel_id, res.room_number): res for res in reservations
# }
# violations = []
# missing_reservations = set(reservations) # Сет для поиска пропавших бронирований
# for user_log in user_logs:
# try:
# params = json.loads(user_log.url_parameters.replace("'", '"')) if user_log.url_parameters else {}
# hotel_id = params.get('utm_content')
# room = params.get('utm_term')
# if not hotel_id or not room:
# continue # Пропускаем записи без нужных параметров
# key = (hotel_id, room)
# reserv = reservations_map.get(key)
# discrepancy_type = None
# if reserv:
# if reserv in missing_reservations:
# missing_reservations.remove(reserv)
# if user_log.date_time < reserv.check_in:
# discrepancy_type = 'early'
# elif user_log.date_time > reserv.check_in + check_in_diff:
# discrepancy_type = 'late'
# else:
# discrepancy_type = 'no_booking'
# if discrepancy_type:
# violations.append(RoomDiscrepancy(
# hotel=hotels_map.get(hotel_id),
# room_number=room,
# discrepancy_type=discrepancy_type,
# booking_id=reserv.reservation_id if reserv else None,
# check_in_date_expected=reserv.check_in if reserv else None,
# check_in_date_actual=user_log.date_time,
# ))
# user_log.fraud_checked = True # Отмечаем логи как проверенные
# except json.JSONDecodeError:
# self.log_error(f"Ошибка декодирования JSON в URL-параметрах: {user_log.url_parameters}")
# except Exception as e:
# self.log_error(f"Ошибка при обработке логов: {e}")
# # Добавляем пропущенные бронирования
# for miss_reserv in missing_reservations:
# violations.append(RoomDiscrepancy(
# hotel=miss_reserv.hotel,
# room_number=miss_reserv.room_number,
# discrepancy_type='missed',
# booking_id=miss_reserv.reservation_id,
# check_in_date_expected=miss_reserv.check_in,
# ))
# # Массово сохраняем нарушения
# if violations:
# RoomDiscrepancy.objects.bulk_create(violations)
# self.log_info(f"Записано {len(violations)} новых несоответствий.")
# # Обновляем флаги fraud_checked
# UserActivityLog.objects.filter(id__in=[log.id for log in user_logs]).update(fraud_checked=True)
# Reservation.objects.filter(id__in=[res.id for res in reservations]).update(fraud_checked=True)
# except Exception as e:
# self.log_error(f"Ошибка при выполнении проверки: {e}")
# self.log_info("Проверка завершена.")
# # Функция для запуска из планировщика
# def run_reservation_check():
# """Запуск проверки через планировщик."""
# logger.info("Планировщик вызывает run_reservation_check.")
# try:
# checker = ReservationChecker()
# checker.run_check()
# except Exception as e:
# logger.error(f"Ошибка при запуске проверки: {e}")
# logger.info("run_reservation_check завершена.")
import json
from datetime import timedelta
from urllib.parse import parse_qs
from django.utils import timezone
from django.db.models import Q
from hotels.models import Reservation, Hotel
from .models import UserActivityLog, ViolationLog, RoomDiscrepancy
from .models import UserActivityLog, RoomDiscrepancy
from touchh.utils.log import CustomLogger
# Настройка логирования
logger = CustomLogger(__name__).get_logger()
class ReservationChecker:
"""
Класс для проверки несоответствий между бронированиями и логами заселения.
"""
def __init__(self):
"""
Инициализация времени проверки и списка нарушений.
"""
self.start_time = timezone.now() - timedelta(days=30)
self.end_time = timezone.now()
self.violations = []
self.checkin_diff_hours = 3
self.checkin_diff_hours = 3 # Разрешенное отклонение от времени заселения
def log_info(self, message):
logger.info(message)
@@ -32,112 +154,106 @@ class ReservationChecker:
def log_error(self, message):
logger.error(message)
def fetch_user_logs(self):
try:
self.log_info("Начинается извлечение логов активности пользователей.")
user_logs = UserActivityLog.objects.filter(created__range=(self.start_time, self.end_time))
self.log_info(f"Найдено {user_logs.count()} логов активности для анализа.")
return user_logs
except Exception as e:
self.log_error(f"Ошибка при извлечении логов активности: {e}")
return UserActivityLog.objects.none()
def fetch_reservations(self):
try:
self.log_info("Начинается извлечение бронирований.")
reservations = Reservation.objects.filter(
Q(check_in__lte=self.end_time) & Q(check_out__gte=self.start_time)
)
self.log_info(f"Найдено {reservations.count()} бронирований для анализа.")
return reservations
except Exception as e:
self.log_error(f"Ошибка при извлечении бронирований: {e}")
return Reservation.objects.none()
def find_violations(self):
self.log_info("Начинается анализ несоответствий.")
user_logs = self.fetch_user_logs()
reservations = self.fetch_reservations()
log_lookup = {}
for log in user_logs:
params = parse_qs(log.url_parameters or "")
hotel_id = params.get("utm_content", [None])[0]
room_number = params.get("utm_term", [None])[0]
if hotel_id and room_number:
key = (hotel_id, room_number)
log_lookup.setdefault(key, []).append(log)
for reservation in reservations:
key = (reservation.hotel.hotel_id, reservation.room_number)
logs = log_lookup.get(key, [])
if reservation.status == "заселен" and not logs:
self.record_violation(
hotel=reservation.hotel,
room_number=reservation.room_number,
violation_type="no_qr_scan",
details=f"Бронирование для номера {reservation.room_number} в отеле '{reservation.hotel.name}' "
f"не имеет записи сканирования QR."
)
for log in logs:
if log.created < reservation.check_in:
self.record_violation(
hotel=reservation.hotel,
room_number=reservation.room_number,
violation_type="early_check_in",
details=f"Раннее заселение: сканирование QR {log.created} раньше времени заезда "
f"{reservation.check_in} для номера {reservation.room_number}."
)
for (hotel_id, room_number), logs in log_lookup.items():
matching_reservations = reservations.filter(
hotel__hotel_id=hotel_id,
room_number=room_number
)
if not matching_reservations.exists():
for log in logs:
self.record_violation(
hotel=Hotel.objects.filter(hotel_id=hotel_id).first(),
room_number=room_number,
violation_type="no_reservation",
details=f"Сканирование QR {log.created} для номера {room_number} в отеле с ID '{hotel_id}' "
f"не соответствует ни одному бронированию."
)
def record_violation(self, hotel, room_number, violation_type, details):
if hotel:
self.violations.append(ViolationLog(
hotel=hotel,
room_number=room_number,
violation_type=violation_type,
violation_details=details
))
self.log_warning(f"Зафиксировано нарушение: {details}")
def save_violations(self):
if self.violations:
ViolationLog.objects.bulk_create(self.violations)
self.log_info(f"Создано {len(self.violations)} записей в ViolationLog.")
else:
self.log_info("Нарушений не обнаружено.")
def run_check(self):
self.log_info(f"Запуск проверки с {self.start_time} по {self.end_time}.")
"""Запуск проверки фродовых событий."""
self.log_info("🔍 Запуск проверки фродовых данных.")
try:
self.find_violations()
self.save_violations()
check_in_diff = timedelta(hours=self.checkin_diff_hours)
# Кэшируем отели в словарь для быстрого доступа
hotels_map = {hotel.hotel_id: hotel for hotel in Hotel.objects.all()}
# Загружаем бронирования и активности пользователей
user_logs = UserActivityLog.objects.filter(fraud_checked=False)
reservations = Reservation.objects.filter(fraud_checked=False).select_related('hotel')
# Преобразуем бронирования в словарь для быстрого поиска
reservations_map = {
(res.hotel.hotel_id, res.room_number): res for res in reservations
}
violations = []
checked_reservations = set() # Сет для бронирований, которые были проверены
self.log_info(f"✅ Загружено {len(user_logs)} логов активности и {len(reservations)} бронирований.")
for user_log in user_logs:
try:
params = json.loads(user_log.url_parameters.replace("'", '"')) if user_log.url_parameters else {}
hotel_id = params.get('utm_content')
room = params.get('utm_term')
if not hotel_id or not room:
self.log_warning(f"🚫 Пропущен лог без hotel_id или room_number: {user_log.url_parameters}")
continue # Пропускаем записи без нужных параметров
key = (hotel_id, room)
reserv = reservations_map.get(key)
discrepancy_type = "match" # По умолчанию считаем, что всё соответствует
if reserv:
checked_reservations.add(reserv)
if user_log.date_time < reserv.check_in:
discrepancy_type = 'early'
self.log_warning(f"⚠️ Обнаружено раннее заселение: {user_log.date_time} < {reserv.check_in}")
elif user_log.date_time > reserv.check_in + check_in_diff:
discrepancy_type = 'late'
self.log_warning(f"⚠️ Обнаружено позднее заселение: {user_log.date_time} > {reserv.check_in + check_in_diff}")
else:
discrepancy_type = 'no_booking'
self.log_warning(f"🚨 Заселение без бронирования: {user_log.date_time} (Отель {hotel_id}, Комната {room})")
violations.append(RoomDiscrepancy(
hotel=hotels_map.get(hotel_id),
room_number=room,
discrepancy_type=discrepancy_type,
booking_id=reserv.reservation_id if reserv else None,
check_in_date_expected=reserv.check_in if reserv else None,
check_in_date_actual=user_log.date_time,
))
user_log.fraud_checked = True # Отмечаем логи как проверенные
except json.JSONDecodeError:
self.log_error(f"❌ Ошибка декодирования JSON в URL-параметрах: {user_log.url_parameters}")
except Exception as e:
self.log_error(f"❌ Ошибка при обработке логов: {e}")
# Добавляем пропущенные бронирования (неявки)
for reserv in reservations:
if reserv not in checked_reservations:
violations.append(RoomDiscrepancy(
hotel=reserv.hotel,
room_number=reserv.room_number,
discrepancy_type='missed',
booking_id=reserv.reservation_id,
check_in_date_expected=reserv.check_in,
))
self.log_warning(f"⚠️ Обнаружена неявка (missed) | Отель: {reserv.hotel.hotel_id}, Номер: {reserv.room_number}, Ожидаемая дата заезда: {reserv.check_in}")
# Массово сохраняем все записи, включая корректные совпадения
if violations:
RoomDiscrepancy.objects.bulk_create(violations)
self.log_info(f"✅ Записано {len(violations)} новых записей в RoomDiscrepancy.")
# Обновляем флаги fraud_checked
UserActivityLog.objects.filter(id__in=[log.id for log in user_logs]).update(fraud_checked=True)
Reservation.objects.filter(id__in=[res.id for res in reservations]).update(fraud_checked=True)
except Exception as e:
self.log_error(f"Ошибка при выполнении проверки: {e}")
self.log_info("Проверка завершена.")
self.log_error(f"Ошибка при выполнении проверки: {e}")
self.log_info("✅ Проверка фродовых данных завершена.")
# Функция для запуска из планировщика
def run_reservation_check():
logger.info("Планировщик вызывает run_reservation_check.")
"""Запуск проверки через планировщик."""
logger.info("📅 Планировщик вызывает run_reservation_check.")
try:
checker = ReservationChecker()
checker.run_check()
except Exception as e:
logger.error(f"Ошибка при запуске проверки: {e}")
logger.info("run_reservation_check завершена.")
logger.error(f"Ошибка при запуске проверки: {e}")
logger.info("run_reservation_check завершена.")

View File

@@ -314,7 +314,7 @@ def scheduled_sync():
except Exception as e:
logger.error(f"Error syncing connection {db_settings}: {e}")
with ThreadPoolExecutor(max_workers=5) as executor:
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(sync_task, db_settings) for db_settings in active_db_settings]
for future in futures:
try:

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.4 on 2025-02-01 09:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0005_roomdiscrepancy_fraud_checked_and_more'),
]
operations = [
migrations.AlterField(
model_name='roomdiscrepancy',
name='fraud_checked',
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
),
migrations.AlterField(
model_name='useractivitylog',
name='fraud_checked',
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.4 on 2025-02-01 09:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0006_alter_roomdiscrepancy_fraud_checked_and_more'),
]
operations = [
migrations.RemoveField(
model_name='roomdiscrepancy',
name='fraud_checked',
),
]

View File

@@ -31,7 +31,7 @@ class UserActivityLog(models.Model):
honeypot = models.BooleanField(verbose_name="Метка honeypot", blank=True, null=True)
reply = models.BooleanField(verbose_name="Ответ пользователя", blank=True, null=True)
page_url = models.URLField(blank=True, null=True, verbose_name="URL страницы")
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия")
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True)
@property
def formatted_timestamp(self):
@@ -79,10 +79,10 @@ class UserActivityLog(models.Model):
except AddressNotFoundError:
return "IP-адрес не найден в базе"
except FileNotFoundError:
logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}")
# logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}")
return "Файл базы данных GeoIP не найден"
except Exception as e:
logger.error(f"Ошибка при определении местоположения: {e}")
# logger.error(f"Ошибка при определении местоположения: {e}")
return "Местоположение недоступно"
class ExternalDBSettings(models.Model):
name = models.CharField(max_length=255, unique=True, help_text="Имя подключения для идентификации.")
@@ -117,7 +117,6 @@ class RoomDiscrepancy(models.Model):
verbose_name="Тип несоответствия"
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия")
def __str__(self):
return f"{self.hotel.name} - Room {self.room_number}: {self.discrepancy_type}"

View File

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

View File

@@ -23,4 +23,4 @@ class TelegramSettingsAdmin(admin.ModelAdmin):
admin.site.register(EmailSettings) # Register your models here.
class EmailSettingsAdmin(admin.ModelAdmin):
list_display = ['email_host', 'email_port', 'email_host_user', 'email_host_password']
list_display = ['email_host', 'email_port', 'email_host_user', 'email_host_password']

View File

@@ -1,4 +1,6 @@
from decouple import config
from django.conf import settings
from django.apps import apps
def load_database_settings(databases):
@@ -8,6 +10,18 @@ def load_database_settings(databases):
"""
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:
local_db_settings = LocalDatabase.objects.filter(is_active=True)
for db in local_db_settings:

View File

@@ -4,4 +4,4 @@ from django.apps import AppConfig
class SettingsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'app_settings'
verbose_name="Настройки системы"
verbose_name="Настройки системы"

File diff suppressed because one or more lines are too long

View File

@@ -76,4 +76,4 @@ class GlobalSystemSettings(models.Model):
class Meta:
verbose_name = "Настройки системы"
verbose_name_plural = "Настройки системы"

9
app_settings/signals.py Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -150,7 +150,7 @@ async def check_pms(update, context):
# Проверка наличия fetch_data и вызов плагина
if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data):
report = await pms_manager.plugin.fetch_data()
logger.debug(f"Отчет типа: {type(report)}")
logger.debug(f"Отчет типа: {type(report)}: {report}")
else:
logger.error("Плагин не поддерживает fetch_data.")
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")

73
docker-compose.yml Normal file
View 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:

View File

@@ -85,14 +85,14 @@ class HotelAdmin(admin.ModelAdmin):
class UserHotelAdmin(admin.ModelAdmin):
list_display = ('user', 'hotel')
search_fields = ('user__username', 'hotel__name')
# list_filter = ('hotel',)
# ordering = ('-hotel',)
list_filter = ('hotel',)
ordering = ('-hotel',)
@admin.register(Reservation)
class ReservationAdmin(admin.ModelAdmin):
list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
search_fields = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
list_filter = ('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', 'fraud_checked')
search_fields = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status')
list_filter = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status')
ordering = ('-check_in',)
@admin.register(Room)

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-02-01 09:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0003_rename_external_id_hotel_external_id_pms_and_more'),
]
operations = [
migrations.AddField(
model_name='reservation',
name='fraud_checked',
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.4 on 2025-02-02 00:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0004_reservation_fraud_checked'),
]
operations = [
migrations.AlterField(
model_name='reservation',
name='check_in',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата заезда'),
),
migrations.AlterField(
model_name='reservation',
name='check_out',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата выезда'),
),
]

View File

@@ -111,11 +111,12 @@ class Reservation(models.Model):
reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
room_number = models.CharField(max_length=255, null=True, blank=True, verbose_name="Номер комнаты")
room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
check_in = models.DateTimeField(verbose_name="Дата заезда")
check_out = models.DateTimeField(verbose_name="Дата выезда")
check_in = models.DateTimeField(verbose_name="Дата заезда", null=True, blank=True)
check_out = models.DateTimeField(verbose_name="Дата выезда", null=True, blank=True)
status = models.CharField(max_length=50, verbose_name="Статус")
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Цена")
discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Скидка")
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True)
def clean(self):
if self.check_out and self.check_in and self.check_out <= self.check_in:

View File

@@ -18,7 +18,6 @@ class PluginLoader:
print("Загрузка плагинов:")
for file in os.listdir(PluginLoader.PLUGIN_PATH):
if file.endswith("_pms.py") and not file.startswith("__"):
# print(f" Plugin {file}")
module_name = f"pms_integration.plugins.{file[:-3]}"
try:
module = importlib.import_module(module_name)
@@ -26,8 +25,8 @@ class PluginLoader:
cls = getattr(module, attr)
if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin:
plugin_name = file[:-7] # Убираем `_pms` из имени файла
# print(f" Загружен плагин {plugin_name}: {cls.__name__}")
plugins[plugin_name] = cls
print(f" Загружен плагин {plugin_name}: {cls.__name__}")
except Exception as e:
print(f" Ошибка загрузки плагина {module_name}: {e}")
return plugins
@@ -40,6 +39,7 @@ class PMSIntegrationManager:
"""
self.hotel = hotel
self.plugin = None
self.plugins = PluginLoader.load_plugins()
def load_hotel(self):
"""
@@ -52,6 +52,16 @@ class PMSIntegrationManager:
"""
Загружает плагин, соответствующий 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() # Приводим название плагина к нижнему регистру
# Формируем имя модуля и класса плагина
@@ -72,6 +82,7 @@ class PMSIntegrationManager:
except ImportError as e:
raise ValueError(f"Ошибка загрузки плагина для PMS {pms_name}: {e}")
>>>>>>> PMSManager_refactor
def fetch_data(self):
"""
Получает данные из PMS с использованием загруженного плагина.

View File

@@ -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 json
from datetime import datetime, timedelta
@@ -5,14 +415,11 @@ 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, # Установите уровень логов для всех обработчиков
level=logging.WARNING,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
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.addHandler(console_handler)
logger.setLevel(logging.WARNING) # Основной уровень логов
logger.setLevel(logging.INFO)
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.pms_config = hotel.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",
@@ -67,28 +454,25 @@ class BnovoPMSPlugin(BasePMSPlugin):
}
async def _get_stored_token(self):
"""Получение токена из конфигурации PMS отеля."""
"""
Получает токен из конфигурации PMS отеля.
"""
try:
logger.debug(f"Попытка получения токена для отеля {self.hotel.id}.")
token = self.pms_config.token
logger.debug(f"Токен из базы данных: {token}")
if not token:
logger.info("Токен отсутствует в конфигурации.")
else:
logger.debug(f"Токен найден: {token}")
return token
except Exception as e:
logger.warning(f"Ошибка при получении токена для отеля {self.hotel.id}: {e}")
logger.error(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}")
def _get_auth_headers(self):
"""Создает заголовки авторизации."""
"""
Создает заголовки авторизации для запросов к API.
"""
logger.debug("Создание заголовков авторизации.")
headers = {
"Content-Type": "application/json",
@@ -97,81 +481,54 @@ class BnovoPMSPlugin(BasePMSPlugin):
if self.token:
headers["Cookie"] = f"SID={self.token}"
logger.debug(f"Добавлен токен в заголовки: {self.token}")
else:
logger.warning("Токен отсутствует, запрос может быть неавторизованным.")
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()
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}")
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")
async def _save_token_to_db(self, sid):
"""
Сохраняет токен (SID) в конфигурации PMS отеля.
"""
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
logger.debug(f"Сохранение токена для отеля {self.hotel.id}: {sid}")
self.pms_config.token = sid
await sync_to_async(self.pms_config.save)()
logger.info(f"Токен {sid} успешно сохранен в базу данных.")
except Exception as e:
logger.error(f"Ошибка при получении данных аккаунта: {e}")
logger.error(f"Ошибка сохранения токена для отеля {self.hotel.id}: {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_session(self):
"""Получение токена (SID) через авторизацию."""
url = f"{self.api_url}/"
payload = {"username": self.username, "password": self.password}
headers = {"Content-Type": "application/json"}
await self._save_token_to_db(self.token)
<<<<<<< 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):
"""Получение данных о бронированиях с помощью эндпоинта /dashboard."""
logger.info("Начало процесса получения данных о бронированиях.")
@@ -183,10 +540,19 @@ class BnovoPMSPlugin(BasePMSPlugin):
except Exception as e:
logger.error(f"Ошибка получения данных аккаунта: {e}")
raise
>>>>>>> PMSManager_refactor
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_from = (now - timedelta(days=7)).strftime("%d.%m.%Y") # Получаем данные за последнюю неделю
create_to = now.strftime("%d.%m.%Y")
params = {
@@ -197,30 +563,25 @@ class BnovoPMSPlugin(BasePMSPlugin):
"page": 1,
"order_by": "create_date.asc",
}
headers = self._get_auth_headers()
all_bookings = []
while True:
logger.debug(f"Запрос к {url} с параметрами: {json.dumps(params, indent=2)}")
try:
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. Перенаправление.")
redirected_url = response.headers.get("Location")
if redirected_url:
logger.debug(f"Перенаправление на {redirected_url}")
url = redirected_url
continue
else:
logger.error("Ответ с кодом 302 не содержит заголовка Location.")
raise ValueError("Перенаправление без указанного URL.")
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("Ошибка запроса к /dashboard")
raise ValueError(f"Ошибка при получении данных: {response.text}")
data = response.json()
logger.debug(f"Полученный ответ API: {json.dumps(data, indent=2, ensure_ascii=False)}")
bookings = data.get("bookings", [])
rooms = data.get("rooms", [])
@@ -228,54 +589,88 @@ class BnovoPMSPlugin(BasePMSPlugin):
logger.debug(f'bookings: {bookings}\n rooms: {rooms}')
all_bookings.extend(bookings)
logger.info(f"Получено бронирований: {len(bookings)}. Всего: {len(all_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
except json.JSONDecodeError as e:
logger.error(f"Ошибка декодирования JSON: {e}. Ответ: {response.text}")
raise ValueError(f"Ошибка декодирования JSON: {e}")
except Exception as e:
logger.error(f"Неизвестная ошибка при обработке запроса: {e}")
raise
async def _process_and_save_bookings(self, bookings):
"""
Обрабатывает и сохраняет бронирования в базу.
"""
logger.info("Начало обработки данных о бронированиях для сохранения в базу.")
processed_items = 0
errors = []
# Сопоставляем бронирования с существующими записями
for booking in all_bookings:
for record in bookings:
try:
booking_id = booking.get("id")
hotel_id = booking.get("hotel_id")
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 hotel_id != str(self.hotel.external_id_pms):
logger.debug(f"Бронирование {booking_id} не относится к отелю {self.hotel.external_id_pms}. Пропуск.")
# Проверка обязательных данных
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, # Объект модели Hotel
"status": booking.get("status_name"), # Статус бронирования
"room_number": booking.get("current_room"), # Номер комнаты (исправлено с create_date)
"check_in": booking.get("arrival"), # Дата заезда
"check_out": booking.get("departure"), # Дата выезда
"room_type": booking.get("initial_room_type_name") # Тип комнаты
}
defaults={
"hotel": self.hotel,
"status": status,
"room_number": room_number,
"check_in": arrival,
"check_out": departure,
},
)
if created:
logger.info(f"Создана новая запись бронирования: {reservation}")
print(reservation)
else:
logger.info(f"Обновлено существующее бронирование: {reservation}")
except Exception as e:
logger.error(f"Ошибка обработки бронирования {booking.get('id')}: {e}")
processed_items += 1
logger.info(f"Все бронирования получены и обработаны. Итоговое количество: {len(all_bookings)}")
return all_bookings
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_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

View File

@@ -1,4 +1,144 @@
# # ecvi_pms.py
# import logging
# import requests
# import json
# import os
# from datetime import datetime, timedelta
# from asgiref.sync import sync_to_async
# from hotels.models import Hotel, Reservation
# from .base_plugin import BasePMSPlugin
# class EcviPMSPlugin(BasePMSPlugin):
# """
# Плагин для интеграции с PMS Ecvi.
# """
# def __init__(self, hotel):
# super().__init__(hotel.pms)
# self.hotel = hotel
# if not self.hotel.pms:
# raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
# self.api_url = self.hotel.pms.url.rstrip("/")
# self.token = self.hotel.pms.token
# self.username = self.hotel.pms.username
# self.password = self.hotel.pms.password
# self.logger = logging.getLogger(self.__class__.__name__)
# handler_console = logging.StreamHandler()
# handler_file = logging.FileHandler('var/log/ecvi_pms_plugin.log')
# formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
# handler_console.setFormatter(formatter)
# handler_file.setFormatter(formatter)
# self.logger.addHandler(handler_console)
# self.logger.addHandler(handler_file)
# self.logger.setLevel(logging.WARNING)
# def get_default_parser_settings(self):
# return {
# "field_mapping": {
# "check_in": "checkin",
# "check_out": "checkout",
# "room_number": "room_name",
# "room_type_name": "room_type",
# "status": "occupancy",
# },
# "date_format": "%Y-%m-%d %H:%M:%S"
# }
# async def _fetch_data(self):
# headers = {"Content-Type": "application/json"}
# data = {"token": self.token}
# try:
# response = await sync_to_async(requests.post)(
# self.api_url, headers=headers, json=data, auth=(self.username, self.password)
# )
# response.raise_for_status()
# response_data = response.json()
# self.logger.debug(f"Полученные данные с API: {response_data}")
# # Группировка данных по номеру комнаты
# structured_data = {}
# for item in response_data:
# room_number = item.get("room_name", "unknown")
# if room_number not in structured_data:
# structured_data[room_number] = []
# structured_data[room_number].append(item)
# # Сохранение данных во временный JSON-файл
# temp_dir = os.path.join("temp", "ecvi")
# os.makedirs(temp_dir, exist_ok=True)
# temp_file = os.path.join(temp_dir, f"ecvi_data_{datetime.now().strftime('%Y%m%d%H%M%S')}.json")
# with open(temp_file, 'w') as file:
# json.dump(structured_data, file, indent=4, ensure_ascii=False)
# return await self._process_data(response_data)
# except requests.exceptions.RequestException as e:
# self.logger.error(f"Ошибка API: {e}")
# return {
# "processed_intervals": 0,
# "processed_items": 0,
# "errors": [str(e)]
# }
# async def _process_data(self, data):
# processed_items = 0
# errors = []
# date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
# for item in data:
# try:
# checkin = item['checkin']
# checkout = item['checkout']
# if checkin in [None, "0000-00-00 00:00:00"] or checkout in [None, "0000-00-00 00:00:00"]:
# continue
# checkin = self._parse_date(checkin, date_formats)
# checkout = self._parse_date(checkout, date_formats)
# reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
# reservation_id=item['task_id'],
# defaults={
# 'room_number': item['room_name'],
# 'room_type': item['room_type'],
# 'check_in': checkin,
# 'check_out': checkout,
# 'status': item['occupancy'],
# 'hotel': self.hotel,
# }
# )
# processed_items += 1
# except Exception as e:
# self.logger.error(f"Ошибка обработки записи: {e}")
# errors.append(str(e))
# return {
# "processed_intervals": 1,
# "processed_items": processed_items,
# "errors": errors
# }
# @staticmethod
# def _parse_date(date_str, formats):
# for fmt in formats:
# try:
# return datetime.strptime(date_str, fmt)
# except ValueError:
# continue
# raise ValueError(f"Дата '{date_str}' не соответствует ожидаемым форматам: {formats}")
# def validate_plugin(self):
# required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"]
# for method in required_methods:
# if not hasattr(self, method):
# raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
# self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
# return True
import logging
import os
import json
from datetime import datetime, timedelta
import requests
from asgiref.sync import sync_to_async
@@ -11,20 +151,17 @@ class EcviPMSPlugin(BasePMSPlugin):
"""
def __init__(self, hotel):
super().__init__(hotel.pms) # Передаем PMS-конфигурацию в базовый класс
self.hotel = hotel # Сохраняем объект отеля
super().__init__(hotel.pms)
self.hotel = hotel
# Проверка PMS-конфигурации
if not self.hotel.pms:
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
# Инициализация параметров API
self.api_url = self.hotel.pms.url
self.token = self.hotel.pms.token
self.username = self.hotel.pms.username
self.password = self.hotel.pms.password
# Настройка логгера
self.logger = logging.getLogger(self.__class__.__name__)
handler_console = logging.StreamHandler()
handler_file = logging.FileHandler('var/log/ecvi_pms_plugin.log')
@@ -35,6 +172,10 @@ class EcviPMSPlugin(BasePMSPlugin):
self.logger.addHandler(handler_file)
self.logger.setLevel(logging.WARNING)
# Директория для сохранения JSON-файлов
self.data_dir = "var/data/ecvi"
os.makedirs(self.data_dir, exist_ok=True)
def get_default_parser_settings(self):
"""
Возвращает настройки парсера по умолчанию.
@@ -47,7 +188,7 @@ class EcviPMSPlugin(BasePMSPlugin):
"room_type_name": "room_type",
"status": "occupancy",
},
"date_format": "%Y-%m-%d %H:%M:%S" # Формат изменен на соответствующий данным
"date_format": "%Y-%m-%d %H:%M:%S"
}
async def _fetch_data(self):
@@ -58,14 +199,23 @@ class EcviPMSPlugin(BasePMSPlugin):
data = {"token": self.token}
try:
# Запрос данных из PMS API
response = await sync_to_async(requests.post)(
self.api_url, headers=headers, json=data, auth=(self.username, self.password)
)
response.raise_for_status()
response_data = response.json()
self.logger.debug(f"Полученные данные с API: {response_data}")
# Сохраняем весь ответ API в файл для анализа
file_name = f"ecvi_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
file_path = os.path.join(self.data_dir, file_name)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(response_data, f, ensure_ascii=False, indent=4)
self.logger.info(f"API-ответ сохранен в файл: {file_path}")
return await self._process_data(response_data)
except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка API: {e}")
return {
@@ -80,22 +230,62 @@ class EcviPMSPlugin(BasePMSPlugin):
"""
processed_items = 0
errors = []
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
unix_epoch = datetime(1970, 1, 1, 0, 0, 0)
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] # Поддержка нескольких форматов даты
valid_reservations = []
print(data)
for item in data:
try:
# Парсинг даты с поддержкой нескольких форматов
checkin = self._parse_date(item['checkin'], date_formats)
checkout = self._parse_date(item['checkout'], date_formats)
checkin = item.get('checkin')
checkout = item.get('checkout')
# Фильтруем записи с некорректными датами
if checkin in [None, "0000-00-00 00:00:00", "1970-01-01 00:00:00", 0] or \
checkout in [None, "0000-00-00 00:00:00", "1970-01-01 00:00:00", 0]:
self.logger.warning(f"Игнорируется запись с некорректной датой: {item}")
continue
checkin = self._parse_date(checkin, date_formats)
checkout = self._parse_date(checkout, date_formats)
# Проверяем на Unix epoch
if checkin == unix_epoch or checkout == unix_epoch:
self.logger.warning(f"Игнорируется запись с Unix epoch датой: {item}")
continue
# Проверяем timestamp
if checkin.timestamp() == 0 or checkout.timestamp() == 0:
self.logger.warning(f"Игнорируется запись с timestamp = 0: {item}")
continue
valid_reservations.append(item)
except Exception as e:
self.logger.error(f"Ошибка обработки записи: {e}")
errors.append(str(e))
# Логируем количество отфильтрованных записей
self.logger.info(f"Обработано бронирований: {len(valid_reservations)}")
# Сохранение валидных бронирований в JSON для проверки
valid_file_name = f"valid_reservations_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
valid_file_path = os.path.join(self.data_dir, valid_file_name)
with open(valid_file_path, "w", encoding="utf-8") as f:
json.dump(valid_reservations, f, ensure_ascii=False, indent=4)
self.logger.info(f"Валидные бронирования сохранены в файл: {valid_file_path}")
# Сохранение данных в БД
for item in valid_reservations:
try:
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=item['task_id'],
defaults={
'room_number': item['room_name'],
'room_type': item['room_type'],
'check_in': checkin,
'check_out': checkout,
'check_in': self._parse_date(item['checkin'], date_formats),
'check_out': self._parse_date(item['checkout'], date_formats),
'status': item['occupancy'],
'hotel': self.hotel,
}
@@ -109,7 +299,7 @@ class EcviPMSPlugin(BasePMSPlugin):
processed_items += 1
except Exception as e:
self.logger.error(f"Ошибка обработки записи: {e}")
self.logger.error(f"Ошибка сохранения бронирования: {e}")
errors.append(str(e))
return {

View File

@@ -1,313 +1,111 @@
import logging
import requests
import hashlib
import json
from .base_plugin import BasePMSPlugin
import os
from datetime import datetime, timedelta
from asgiref.sync import sync_to_async
from touchh.utils.log import CustomLogger
from hotels.models import Hotel, Reservation
from app_settings.models import GlobalHotelSettings
from django.utils import timezone
from .base_plugin import BasePMSPlugin
class RealtyCalendarPlugin(BasePMSPlugin):
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("/")
self.logger = CustomLogger(name="RealtyCalendarPlugin", log_level="DEBUG").get_logger()
if not self.public_key or not self.private_key:
raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar")
class RealtyCalendarPMSPlugin(BasePMSPlugin):
def __init__(self, hotel):
super().__init__(hotel.pms)
self.hotel = hotel
if not self.hotel.pms:
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
self.api_url = self.hotel.pms.url.rstrip("/")
self.public_key = self.hotel.pms.public_key
self.private_key = self.hotel.pms.private_key
self.logger = logging.getLogger(self.__class__.__name__)
handler_console = logging.StreamHandler()
handler_file = logging.FileHandler('var/log/realtycalendar_pms_plugin.log')
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler_console.setFormatter(formatter)
handler_file.setFormatter(formatter)
self.logger.addHandler(handler_console)
self.logger.addHandler(handler_file)
self.logger.setLevel(logging.WARNING)
def get_default_parser_settings(self):
"""
Возвращает настройки по умолчанию для обработки данных.
"""
return {
"date_format": "%Y-%m-%dT%H:%M:%S",
"timezone": "UTC"
"field_mapping": {
"check_in": "begin_date",
"check_out": "end_date",
"room_number": "apartment_id",
"room_type_name": "notes",
"status": "status",
},
"date_format": "%Y-%m-%d %H:%M:%S"
}
def _get_sorted_keys(self, obj):
sorted_keys = sorted(obj.keys())
self.logger.debug(f"Отсортированные ключи: {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)
self.logger.debug(f"Сформированная строка данных: {string}")
return string + self.private_key
def _generate_md5(self, string):
md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest()
self.logger.debug(f"Сформированный MD5-хеш: {md5_hash}")
return md5_hash
def _generate_sign(self, data):
data_string = self._generate_data_string(data)
self.logger.debug(f"Строка для подписи: {data_string}")
sign = self._generate_md5(data_string)
self.logger.debug(f"Подпись: {sign}")
return sign
# async def _fetch_data(self):
# self.logger.debug("Начало выполнения функции _fetch_data")
# base_url = f"{self.api_url}/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)
# try:
# response = requests.post(url=base_url, headers=headers, json=data)
# self.logger.debug(f"Статус ответа: {response.status_code}")
# if response.status_code != 200:
# self.logger.error(f"Ошибка API: {response.status_code}, {response.text}")
# raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}")
# try:
# response_data = response.json()
# bookings = response_data.get("bookings", [])
# # self.logger.debug(f"Полученные данные: {bookings}")
# if not isinstance(bookings, list):
# self.logger.error(f"Ожидался список, но получен: {type(bookings)}")
# raise ValueError("Некорректный формат данных для bookings")
# except json.JSONDecodeError as e:
# self.logger.error(f"Ошибка декодирования JSON: {e}")
# raise ValueError("Ответ не является корректным JSON.")
# except Exception as e:
# self.logger.error(f"Ошибка обработки ответа API: {e}")
# raise
# except Exception as e:
# self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}")
# raise
# try:
# hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
# hotel_tz = hotel.timezone
# self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}")
# hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)()
# check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00"
# check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00"
# except Exception as e:
# self.logger.error(f"Ошибка получения настроек отеля: {e}")
# check_in_time, check_out_time = "14:00:00", "12:00:00"
# filtered_data = []
# for item in bookings:
# try:
# if not isinstance(item, dict):
# self.logger.error(f"Некорректный формат элемента: {item}")
# continue
# reservation_id = item.get('id')
# if not reservation_id:
# self.logger.error(f"ID резервации отсутствует: {item}")
# continue
# begin_date = item.get('begin_date')
# end_date = item.get('end_date')
# if not begin_date or not end_date:
# self.logger.error(f"Отсутствуют даты в записи: {item}")
# continue
# checkin = timezone.make_aware(
# datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S")
# )
# checkout = timezone.make_aware(
# datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S")
# )
# filtered_data.append({
# 'reservation_id': reservation_id,
# 'checkin': checkin,
# 'checkout': checkout,
# 'room_number': item.get('apartment_id'),
# 'room_type': item.get('notes', 'Описание отсутствует'),
# 'status': item.get('status')
# })
# except Exception as e:
# self.logger.error(f"Ошибка обработки элемента: {e}")
# # self.logger.debug(f"Отфильтрованные данные: {filtered_data}")
# await self._save_to_db(filtered_data)
async def _fetch_data(self):
self.logger.debug("Начало выполнения функции _fetch_data")
base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/"
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}
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"),
"sign": self._generate_sign()
}
data["sign"] = self._generate_sign(data)
try:
response = requests.post(url=base_url, headers=headers, json=data)
self.logger.debug(f"Статус ответа: {response.status_code}")
if response.status_code != 200:
self.logger.error(f"Ошибка API: {response.status_code}, {response.text}")
return {
"processed_intervals": 0,
"processed_items": 0,
"errors": [f"Ошибка API RealtyCalendar: {response.status_code}"]
}
response = await sync_to_async(requests.post)(
f"{self.api_url}/api/v1/bookings/{self.public_key}/",
headers=headers, json=data
)
response.raise_for_status()
response_data = response.json()
bookings = response_data.get("bookings", [])
return await self._process_data(response_data)
except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка API: {e}")
return {"processed_intervals": 0, "processed_items": 0, "errors": [str(e)]}
if not isinstance(bookings, list):
self.logger.error(f"Ожидался список, но получен: {type(bookings)}")
return {
"processed_intervals": 0,
"processed_items": 0,
"errors": ["Некорректный формат данных для bookings"]
}
except json.JSONDecodeError as e:
self.logger.error(f"Ошибка декодирования JSON: {e}")
return {
"processed_intervals": 0,
"processed_items": 0,
"errors": ["Ответ не является корректным JSON."]
}
except Exception as e:
self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}")
return {
"processed_intervals": 0,
"processed_items": 0,
"errors": [str(e)]
}
# Получение настроек отеля
try:
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
hotel_tz = hotel.timezone
self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}")
hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)()
check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00"
check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00"
except Exception as e:
self.logger.error(f"Ошибка получения настроек отеля: {e}")
check_in_time, check_out_time = "14:00:00", "12:00:00"
# Обработка записей
async def _process_data(self, data):
processed_items = 0
errors = []
filtered_data = []
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
for item in bookings:
for item in data.get("bookings", []):
try:
if not isinstance(item, dict):
raise ValueError(f"Некорректный формат элемента: {item}")
checkin = self._parse_date(item['begin_date'], date_formats)
checkout = self._parse_date(item['end_date'], date_formats)
reservation_id = item.get('id')
if not reservation_id:
raise ValueError(f"ID резервации отсутствует: {item}")
begin_date = item.get('begin_date')
end_date = item.get('end_date')
if not begin_date or not end_date:
raise ValueError(f"Отсутствуют даты в записи: {item}")
checkin = timezone.make_aware(
datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S")
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=item['id'],
defaults={
'room_number': item['apartment_id'],
'room_type': item.get('notes', 'Описание отсутствует'),
'check_in': checkin,
'check_out': checkout,
'status': item['status'],
'hotel': self.hotel,
}
)
checkout = timezone.make_aware(
datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S")
)
filtered_data.append({
'reservation_id': reservation_id,
'checkin': checkin,
'checkout': checkout,
'room_number': item.get('apartment_id'),
'room_type': item.get('notes', 'Описание отсутствует'),
'status': item.get('status')
})
processed_items += 1
except Exception as e:
self.logger.error(f"Ошибка обработки элемента: {e}")
self.logger.error(f"Ошибка обработки записи: {e}")
errors.append(str(e))
# Сохранение в БД
try:
await self._save_to_db(filtered_data)
except Exception as e:
self.logger.error(f"Ошибка сохранения данных в БД: {e}")
errors.append(f"Ошибка сохранения данных в БД: {str(e)}")
return {"processed_intervals": 1, "processed_items": processed_items, "errors": errors}
# Формирование отчета
report = {
"processed_intervals": 1, # Пример значения
"processed_items": processed_items,
"errors": errors
}
self.logger.debug(f"Сформированный отчет: {report}")
return report
def _generate_sign(self):
return hashlib.md5((self.public_key + self.private_key).encode("utf-8")).hexdigest()
async def _save_to_db(self, data):
if not isinstance(data, list):
self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}")
return
for index, item in enumerate(data, start=1):
@staticmethod
def _parse_date(date_str, formats):
for fmt in formats:
try:
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
reservation_id = item.get('reservation_id')
if not reservation_id:
self.logger.error(f"Пропущена запись {index}: отсутствует 'id'")
continue
existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id)
existing_reservation = await sync_to_async(existing_reservation.first)()
defaults = {
'room_number': item['room_number'],
'room_type': item['room_type'],
'check_in': item['checkin'],
'check_out': item['checkout'],
'status': item['status'],
'hotel': hotel
}
if existing_reservation:
await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=reservation_id, defaults=defaults
)
self.logger.debug(f"Резервация {reservation_id} обновлена. ")
else:
await sync_to_async(Reservation.objects.create)(
reservation_id=reservation_id, **defaults
)
self.logger.debug(f"Создана новая резервация {reservation_id}")
except Exception as e:
self.logger.error(f"Ошибка при обработке записи {index}: {e}")
return datetime.strptime(date_str, fmt)
except ValueError:
continue
raise ValueError(f"Дата '{date_str}' не соответствует ожидаемым форматам: {formats}")
def validate_plugin(self):
required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"]
for m in required_methods:
if not hasattr(self, m):
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.")
self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
for method in required_methods:
if not hasattr(self, method):
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
self.logger.debug(f"Плагин {self.__class__.__name__} успешно прошел валидацию.")
return True

View File

@@ -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 requests
import json
@@ -6,6 +133,25 @@ from datetime import datetime, timedelta
from asgiref.sync import sync_to_async
from hotels.models import Hotel, Reservation
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
class ShelterPMSPlugin(BasePMSPlugin):
"""
@@ -34,24 +180,65 @@ class ShelterPMSPlugin(BasePMSPlugin):
self.logger.addHandler(handler_console)
self.logger.addHandler(handler_file)
self.logger.setLevel(logging.WARNING)
>>>>>>> PMSManager_refactor
def get_default_parser_settings(self):
"""
Возвращает настройки парсера по умолчанию.
Возвращает настройки по умолчанию для обработки данных.
"""
return {
"field_mapping": {
<<<<<<< HEAD
"check_in": "check_in",
"check_out": "check_out",
"room_number": "room_number",
"status": "status",
=======
"check_in": "from",
"check_out": "until",
"room_number": "roomNumber",
"room_type_name": "roomTypeName",
"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):
"""
<<<<<<< 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 и сохраняет их в базу.
"""
now = datetime.now()
@@ -180,58 +367,69 @@ class ShelterPMSPlugin(BasePMSPlugin):
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
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:
# Проверяем, что item — это словарь
if not isinstance(item, dict):
self.logger.error(f"Ожидался словарь, но получен: {type(item)}. Данные: {item}")
return
bookings = await self._fetch_data()
report = await self._process_and_save_bookings(bookings)
self.logger.info(f"Данные успешно обработаны. Отчет: {report}")
return report
except Exception as e:
self.logger.error(f"Ошибка загрузки и обработки данных: {e}")
raise
# Получаем отель по настройкам PMS
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
self.logger.debug(f"Отель найден: {hotel.name}")
async def _process_and_save_bookings(self, bookings):
"""
Обрабатывает и сохраняет бронирования в базу данных.
"""
self.logger.info("Начало обработки данных о бронированиях для сохранения в базу.")
processed_items = 0
errors = []
# Сохраняем данные бронирования
room_number = item.get("room_number")
check_in = item.get("checkin")
check_out = item.get("checkout")
room_type = item.get("room_type")
for record in bookings:
try:
# Пример обработки данных бронирования
booking_id = record.get("id")
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}")
# Проверяем, существует ли запись с таким номером и датой заезда
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,
# Сохраняем или обновляем запись в базе данных
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
external_id=booking_id,
defaults={
"hotel": self.pms_config.hotel,
"room_number": room_number,
"check_in": check_in,
"check_out": check_out,
"hotel": hotel,
"room_type": room_type,
"status": status,
},
)
self.logger.debug(f"Обновлена существующая резервация.")
else:
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:
self.logger.error(f"Ошибка сохранения данных: {e}")
processed_items += 1
except Exception as 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
View 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

View 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

View File

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

View File

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

View File

@@ -1,3 +1,9 @@
from django.test import TestCase
# Create your tests here.
def test_function():
"""тестовая функция для проверки планировщика
"""
print("Hello, World!")
return "Hello, World!"

14
scheduler/urls.py Normal file
View 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"),
]

View File

@@ -0,0 +1,18 @@
.ml-4 {
margin-left: 1rem !important;
}
.ml-6 {
margin-left: 1.5rem !important;
}
.nav-sidebar ul.nav-treeview .nav-link {
padding-left: 2rem;
}
.nav-sidebar .nav-link {
display: flex;
}
.nav-sidebar .nav-link .nav-icon {
margin-top: .2rem;
margin-right: .3rem;
}

View 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);
});
});
}

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

18
tmp/tests.py Normal file
View File

@@ -0,0 +1,18 @@
import os
import django
import unittest
from antifroud.check_fraud import run_reservation_check
# Установка переменной окружения для Django settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings")
django.setup()
class FraudCheckTests(unittest.TestCase):
def test_run_reservation_check(self):
"""Тест проверки бронирования на мошенничество"""
result = run_reservation_check()
self.assertIsNotNone(result) # Проверяем, что результат не None
self.assertIsInstance(result, dict) # Проверяем, что возвращается словарь
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load Diff