RealtyCalendar plugin

This commit is contained in:
2024-12-24 15:43:43 +09:00
parent 77bf0b8381
commit d5622f41a2
58 changed files with 374 additions and 1714 deletions

View File

@@ -1,4 +1,3 @@
import logging
from datetime import timedelta from datetime import timedelta
from urllib.parse import parse_qs from urllib.parse import parse_qs
from django.utils import timezone from django.utils import timezone
@@ -7,8 +6,8 @@ from hotels.models import Reservation, Hotel
from .models import UserActivityLog, ViolationLog from .models import UserActivityLog, ViolationLog
from touchh.utils.log import CustomLogger from touchh.utils.log import CustomLogger
# Настройка логирования # Настройка логирования
logging.basicConfig(level=logging.INFO) logger = CustomLogger(__name__).get_logger()
logger = logging.getLogger(__name__)
class ReservationChecker: class ReservationChecker:
""" """

View File

@@ -202,63 +202,56 @@ class DataSyncManager:
self.logger.error(f"Error fetching data: {e}") self.logger.error(f"Error fetching data: {e}")
return [] return []
def update_sync_log(self, hotel, recieved_records, processed_records): def update_sync_log(self, hotel, recieved_records, processed_records):
"""
Обновляет или создает запись в таблице SyncLog.
"""
try: try:
log, created = SyncLog.objects.get_or_create(hotel=hotel) log, created = SyncLog.objects.update_or_create(
hotel=hotel,
defaults={
"recieved_records": recieved_records,
"processed_records": processed_records,
"created": timezone.now(), # Убедитесь, что дата обновляется
}
)
if created: if created:
log.recieved_records = recieved_records self.logger.info(f"Sync log created for hotel '{hotel.name}'.")
log.processed_records = processed_records
else: else:
log.recieved_records += recieved_records self.logger.info(f"Sync log updated for hotel '{hotel.name}'.")
log.processed_records += processed_records
log.save()
self.logger.info(f"Sync log updated for hotel '{hotel.name}'.")
except Exception as e: except Exception as e:
self.logger.error(f"Error updating sync log for hotel '{hotel.name}': {e}") self.logger.error(f"Error updating sync log for hotel '{hotel.name}': {e}")
def process_and_save_data(self, rows): self.logger.info(f"Attempting to update sync log for hotel: {hotel.name}")
""" self.update_sync_log(hotel, recieved_records, processed_records)
Обрабатывает и сохраняет данные из внешнего источника.
:param rows: Список строк данных, полученных из базы данных. def process_and_save_data(self, rows):
""" hotel_processed_counts = {} # Словарь для подсчёта записей по каждому отелю
seen_entries = set()
for row in rows: for row in rows:
# Получение и декодирование URL-параметров
url_parameters = row.get("url_parameters")
if not url_parameters:
self.logger.warning(f"Skipping record with missing URL parameters: {row}")
continue
parsed_params = self.data_processor.parse_url_parameters(url_parameters)
hotel_id = parsed_params.get("utm_content") # Извлекаем hotel_id из параметров
room_number = parsed_params.get("utm_term") # Извлекаем room_number из параметров
if not hotel_id or not room_number:
self.logger.warning(f"Skipping record with missing data: hotel_id={hotel_id}, room_number={room_number}")
continue
# Проверка на дубликаты
if (hotel_id, room_number) in seen_entries:
self.logger.warning(f"Duplicate record skipped: hotel_id={hotel_id}, room_number={room_number}")
continue
seen_entries.add((hotel_id, room_number))
try: try:
# Получение или создание отеля url_parameters = row.get("url_parameters")
if not url_parameters:
self.logger.warning(f"Skipping record with missing URL parameters: {row}")
continue
parsed_params = self.data_processor.parse_url_parameters(url_parameters)
hotel_id = parsed_params.get("utm_content")
room_number = parsed_params.get("utm_term")
if not hotel_id or not room_number:
self.logger.warning(f"Skipping record with missing data: hotel_id={hotel_id}, room_number={room_number}")
continue
hotel = self.hotel_manager.get_or_create_hotel(hotel_id, row.get("page_title")) hotel = self.hotel_manager.get_or_create_hotel(hotel_id, row.get("page_title"))
if not hotel: if not hotel:
self.logger.warning(f"Skipping record: Failed to create or retrieve hotel with ID {hotel_id}") self.logger.warning(f"Skipping record: Failed to create or retrieve hotel with ID {hotel_id}")
continue continue
# Получение или создание комнаты
room = self.hotel_manager.get_or_create_room(hotel, room_number) room = self.hotel_manager.get_or_create_room(hotel, room_number)
if not room: if not room:
self.logger.warning(f"Skipping record: Failed to create or retrieve room {room_number} in hotel {hotel.name}") self.logger.warning(f"Skipping record: Failed to create or retrieve room {room_number} in hotel {hotel.name}")
continue continue
# Создание или обновление записи активности пользователя
UserActivityLog.objects.update_or_create( UserActivityLog.objects.update_or_create(
external_id=row.get("id"), external_id=row.get("id"),
defaults={ defaults={
@@ -270,14 +263,23 @@ class DataSyncManager:
"url_parameters": parsed_params, "url_parameters": parsed_params,
"page_title": self.data_processor.decode_html_entities(row.get("page_title")) or "Untitled", "page_title": self.data_processor.decode_html_entities(row.get("page_title")) or "Untitled",
"page_url": row.get("page_url") or "", "page_url": row.get("page_url") or "",
"page_id": row.get("page_id") or 0,
"hits": row.get("hits") or 0, "hits": row.get("hits") or 0,
} }
) )
self.logger.info(f"Record ID {row.get('id')} processed successfully.") self.logger.info(f"Record ID {row.get('id')} processed successfully.")
if hotel.id not in hotel_processed_counts:
hotel_processed_counts[hotel.id] = {"recieved_records": 0, "processed_records": 0}
hotel_processed_counts[hotel.id]["processed_records"] += 1
except Exception as e: except Exception as e:
self.logger.error(f"Error processing record ID {row.get('id')}: {e}") self.logger.error(f"Error processing record ID {row.get('id')}: {e}")
self.logger.info(f"Data processing completed. Processed {len(seen_entries)} unique records.") for hotel_id, counts in hotel_processed_counts.items():
hotel = Hotel.objects.get(id=hotel_id)
self.update_sync_log(hotel, recieved_records=len(rows), processed_records=counts["processed_records"])
def sync(self): def sync(self):
@@ -292,7 +294,8 @@ class DataSyncManager:
def scheduled_sync(): def scheduled_sync():
logger = CustomLogger(name="DatabaseSyncScheduler", log_level="ERROR").get_logger() import os
logger = CustomLogger(name="DatabaseSyncScheduler", log_level=os.getenv("SCHEDULED_SYNC_LOG_LEVEL", default="ERROR")).get_logger()
logger.info("Starting scheduled sync.") logger.info("Starting scheduled sync.")
active_db_settings = ExternalDBSettings.objects.filter(is_active=True) active_db_settings = ExternalDBSettings.objects.filter(is_active=True)

View File

@@ -1,86 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-12 12:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('hotels', '0004_alter_reservation_room_number'),
]
operations = [
migrations.CreateModel(
name='ExternalDBSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Имя подключения для идентификации.', max_length=255, unique=True)),
('host', models.CharField(help_text='Адрес сервера базы данных.', max_length=255)),
('port', models.PositiveIntegerField(default=3306, help_text='Порт сервера базы данных.')),
('database', models.CharField(help_text='Имя базы данных.', max_length=255)),
('user', models.CharField(help_text='Имя пользователя базы данных.', max_length=255)),
('password', models.CharField(help_text='Пароль для подключения.', max_length=255)),
('table_name', models.CharField(blank=True, help_text='Имя таблицы для загрузки данных.', max_length=255, null=True)),
('selected_fields', models.TextField(blank=True, help_text='Список полей для загрузки (через запятую).', null=True)),
('is_active', models.BooleanField(default=True, help_text='Флаг активности подключения.')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Настройки подключения к БД',
'verbose_name_plural': 'Настройки подключений к БД',
},
),
migrations.CreateModel(
name='UserActivityLog',
fields=[
('id', models.BigIntegerField(primary_key=True, serialize=False)),
('user_id', models.BigIntegerField(verbose_name='ID пользователя')),
('ip', models.GenericIPAddressField(verbose_name='IP-адрес')),
('created', models.DateTimeField(verbose_name='Дата создания')),
('timestamp', models.BigIntegerField(verbose_name='Метка времени')),
('date_time', models.DateTimeField(verbose_name='Дата и время')),
('referred', models.TextField(blank=True, null=True, verbose_name='Реферальная ссылка')),
('agent', models.TextField(verbose_name='Агент пользователя')),
('platform', models.CharField(blank=True, max_length=255, null=True, verbose_name='Платформа')),
('version', models.CharField(blank=True, max_length=255, null=True, verbose_name='Версия')),
('model', models.CharField(blank=True, max_length=255, null=True, verbose_name='Модель устройства')),
('device', models.CharField(blank=True, max_length=255, null=True, verbose_name='Тип устройства')),
('UAString', models.TextField(verbose_name='User-Agent строка')),
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Местоположение')),
('page_id', models.BigIntegerField(blank=True, null=True, verbose_name='ID страницы')),
('url_parameters', models.TextField(blank=True, null=True, verbose_name='Параметры URL')),
('page_title', models.TextField(blank=True, null=True, verbose_name='Заголовок страницы')),
('type', models.CharField(max_length=50, verbose_name='Тип')),
('last_counter', models.IntegerField(verbose_name='Последний счетчик')),
('hits', models.IntegerField(verbose_name='Количество обращений')),
('honeypot', models.BooleanField(verbose_name='Метка honeypot')),
('reply', models.BooleanField(verbose_name='Ответ пользователя')),
('page_url', models.URLField(blank=True, null=True, verbose_name='URL страницы')),
],
options={
'verbose_name': 'Регистрация посетителей',
'verbose_name_plural': 'Регистрации посетителей',
},
),
migrations.CreateModel(
name='RoomDiscrepancy',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('room_number', models.CharField(max_length=50, verbose_name='Номер комнаты')),
('booking_id', models.CharField(max_length=255, verbose_name='ID бронирования')),
('check_in_date_expected', models.DateField(verbose_name='Ожидаемая дата заселения')),
('check_in_date_actual', models.DateField(verbose_name='Фактическая дата заселения')),
('discrepancy_type', models.CharField(choices=[('early', 'Раннее заселение'), ('late', 'Позднее заселение'), ('missed', 'Неявка')], max_length=50, verbose_name='Тип несоответствия')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
],
options={
'verbose_name': 'Несовпадение в заселении',
'verbose_name_plural': 'Несовпадения в заселении',
},
),
]

View File

@@ -1,42 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-12 13:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='externaldbsettings',
name='database',
),
migrations.AlterField(
model_name='externaldbsettings',
name='host',
field=models.CharField(default='', help_text='Адрес сервера базы данных.', max_length=255),
),
migrations.AlterField(
model_name='externaldbsettings',
name='is_active',
field=models.BooleanField(default=False, help_text='Флаг активности подключения.'),
),
migrations.AlterField(
model_name='externaldbsettings',
name='name',
field=models.CharField(default='Новая настройка', help_text='Имя подключения для идентификации.', max_length=255, unique=True),
),
migrations.AlterField(
model_name='externaldbsettings',
name='password',
field=models.CharField(default='', help_text='Пароль для подключения.', max_length=255),
),
migrations.AlterField(
model_name='externaldbsettings',
name='user',
field=models.CharField(default='', help_text='Имя пользователя базы данных.', max_length=255),
),
]

View File

@@ -1,43 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-12 13:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0002_remove_externaldbsettings_database_and_more'),
]
operations = [
migrations.AddField(
model_name='externaldbsettings',
name='database',
field=models.CharField(default='', help_text='Имя базы данных.', max_length=255),
),
migrations.AlterField(
model_name='externaldbsettings',
name='host',
field=models.CharField(help_text='Адрес сервера базы данных.', max_length=255),
),
migrations.AlterField(
model_name='externaldbsettings',
name='is_active',
field=models.BooleanField(default=True, help_text='Флаг активности подключения.'),
),
migrations.AlterField(
model_name='externaldbsettings',
name='name',
field=models.CharField(help_text='Имя подключения для идентификации.', max_length=255, unique=True),
),
migrations.AlterField(
model_name='externaldbsettings',
name='password',
field=models.CharField(help_text='Пароль для подключения.', max_length=255),
),
migrations.AlterField(
model_name='externaldbsettings',
name='user',
field=models.CharField(help_text='Имя пользователя базы данных.', max_length=255),
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-12 14:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0003_externaldbsettings_database_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='externaldbsettings',
options={'verbose_name': 'Настройка подключения к БД', 'verbose_name_plural': 'Настройки подключений к БД'},
),
migrations.AlterField(
model_name='externaldbsettings',
name='database',
field=models.CharField(default='u1510415_wp832', help_text='Имя базы данных.', max_length=255),
),
migrations.AlterField(
model_name='externaldbsettings',
name='table_name',
field=models.CharField(blank=True, default='wpts_user_activity_log', help_text='Имя таблицы для загрузки данных.', max_length=255, null=True),
),
]

View File

@@ -1,24 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-12 23:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0004_alter_externaldbsettings_options_and_more'),
]
operations = [
migrations.CreateModel(
name='ImportedHotel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('external_id', models.CharField(max_length=255, unique=True, verbose_name='Внешний ID отеля')),
('name', models.CharField(max_length=255, verbose_name='Имя отеля')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
('imported', models.BooleanField(default=False, verbose_name='Импортирован в основную базу')),
],
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-12 23:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0005_importedhotel'),
]
operations = [
migrations.AlterModelOptions(
name='importedhotel',
options={'verbose_name': 'Импортированный отель', 'verbose_name_plural': 'Импортированные отели'},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-13 00:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0006_alter_importedhotel_options'),
]
operations = [
migrations.AddField(
model_name='useractivitylog',
name='external_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-13 00:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0007_useractivitylog_external_id'),
]
operations = [
migrations.AlterField(
model_name='useractivitylog',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-13 00:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0008_alter_useractivitylog_id'),
]
operations = [
migrations.AddField(
model_name='importedhotel',
name='display_name',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Отображаемое имя'),
),
]

View File

@@ -1,30 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-14 04:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0009_importedhotel_display_name'),
('hotels', '0010_alter_hotel_timezone'),
]
operations = [
migrations.CreateModel(
name='SyncLog',
fields=[
('id', models.BigIntegerField(primary_key=True, serialize=False, unique=True, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('recieved_records', models.IntegerField(verbose_name='Полученные записи')),
('processed_records', models.IntegerField(verbose_name='Обработанные записи')),
('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
('reservation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.reservation', verbose_name='Бронирование')),
],
options={
'verbose_name': 'Журнал синхронизации',
'verbose_name_plural': 'Журналы синхронизации',
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-14 06:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0010_synclog'),
]
operations = [
migrations.AlterField(
model_name='importedhotel',
name='external_id',
field=models.CharField(max_length=255, verbose_name='Внешний ID отеля'),
),
]

View File

@@ -1,31 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-16 23:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0011_alter_importedhotel_external_id'),
('hotels', '0010_alter_hotel_timezone'),
]
operations = [
migrations.CreateModel(
name='ViolationLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('room_number', models.CharField(blank=True, max_length=50, null=True, verbose_name='Номер комнаты')),
('violation_type', models.CharField(choices=[('missed', 'Неявка'), ('early', 'Раннее заселение'), ('late', 'Позднее заселение')], max_length=50, verbose_name='Тип нарушения')),
('violation_details', models.TextField(blank=True, null=True, verbose_name='Детали нарушения')),
('detected_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата обнаружения')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
],
options={
'verbose_name': 'Журнал нарушений',
'verbose_name_plural': 'Журналы нарушений',
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-17 03:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0012_violationlog'),
]
operations = [
migrations.AlterField(
model_name='useractivitylog',
name='timestamp',
field=models.BigIntegerField(blank=True, null=True, verbose_name='Метка времени'),
),
]

View File

@@ -1,68 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-17 03:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0013_alter_useractivitylog_timestamp'),
]
operations = [
migrations.AlterField(
model_name='useractivitylog',
name='UAString',
field=models.TextField(blank=True, null=True, verbose_name='User-Agent строка'),
),
migrations.AlterField(
model_name='useractivitylog',
name='agent',
field=models.TextField(blank=True, null=True, verbose_name='Агент пользователя'),
),
migrations.AlterField(
model_name='useractivitylog',
name='created',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='useractivitylog',
name='date_time',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата и время'),
),
migrations.AlterField(
model_name='useractivitylog',
name='hits',
field=models.IntegerField(blank=True, null=True, verbose_name='Количество обращений'),
),
migrations.AlterField(
model_name='useractivitylog',
name='honeypot',
field=models.BooleanField(blank=True, null=True, verbose_name='Метка honeypot'),
),
migrations.AlterField(
model_name='useractivitylog',
name='ip',
field=models.GenericIPAddressField(blank=True, null=True, verbose_name='IP-адрес'),
),
migrations.AlterField(
model_name='useractivitylog',
name='last_counter',
field=models.IntegerField(blank=True, null=True, verbose_name='Последний счетчик'),
),
migrations.AlterField(
model_name='useractivitylog',
name='reply',
field=models.BooleanField(blank=True, null=True, verbose_name='Ответ пользователя'),
),
migrations.AlterField(
model_name='useractivitylog',
name='type',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Тип'),
),
migrations.AlterField(
model_name='useractivitylog',
name='user_id',
field=models.BigIntegerField(blank=True, null=True, verbose_name='ID пользователя'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-17 04:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0014_alter_useractivitylog_uastring_and_more'),
]
operations = [
migrations.AlterField(
model_name='useractivitylog',
name='page_id',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='ID страницы'),
),
]

View File

@@ -1,39 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-17 05:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0015_alter_useractivitylog_page_id'),
]
operations = [
migrations.AlterField(
model_name='useractivitylog',
name='created',
field=models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='useractivitylog',
name='external_id',
field=models.CharField(db_index=True, default=1, max_length=255, unique=True, verbose_name='Внешний ID'),
preserve_default=False,
),
migrations.AlterField(
model_name='useractivitylog',
name='ip',
field=models.GenericIPAddressField(blank=True, db_index=True, null=True, verbose_name='IP-адрес'),
),
migrations.AlterField(
model_name='useractivitylog',
name='page_id',
field=models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name='ID страницы'),
),
migrations.AlterField(
model_name='useractivitylog',
name='user_id',
field=models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name='ID пользователя'),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-18 01:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0016_alter_useractivitylog_created_and_more'),
]
operations = [
migrations.AddField(
model_name='violationlog',
name='hits',
field=models.IntegerField(default=1, verbose_name='Срабатывания'),
preserve_default=False,
),
]

View File

@@ -1,44 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-18 04:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0017_violationlog_hits'),
('hotels', '0014_alter_room_number'),
]
operations = [
migrations.RemoveField(
model_name='synclog',
name='reservation',
),
migrations.AlterField(
model_name='synclog',
name='created',
field=models.DateTimeField(auto_now=True, verbose_name='Дата обновления'),
),
migrations.AlterField(
model_name='synclog',
name='hotel',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', unique=True, verbose_name='Отель'),
),
migrations.AlterField(
model_name='synclog',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='synclog',
name='processed_records',
field=models.IntegerField(default=0, verbose_name='Обработанные записи'),
),
migrations.AlterField(
model_name='synclog',
name='recieved_records',
field=models.IntegerField(default=0, verbose_name='Полученные записи'),
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-18 04:55
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0018_remove_synclog_reservation_alter_synclog_created_and_more'),
('hotels', '0014_alter_room_number'),
]
operations = [
migrations.AlterField(
model_name='synclog',
name='hotel',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-18 10:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('antifroud', '0019_alter_synclog_hotel'),
]
operations = [
migrations.AlterField(
model_name='useractivitylog',
name='hits',
field=models.IntegerField(blank=True, default='0', null=True, verbose_name='Количество обращений'),
),
]

View File

@@ -1,50 +1,24 @@
# settings.py
from decouple import config
from django.conf import settings
from django.apps import apps from django.apps import apps
def load_database_settings(): def load_database_settings(databases):
# Загружаем настройки из базы данных """
Загружает дополнительные базы данных из таблицы LocalDatabase и добавляет их в конфигурацию.
:param databases: Существующий словарь DATABASES
"""
LocalDatabase = apps.get_model('app_settings', 'LocalDatabase') LocalDatabase = apps.get_model('app_settings', 'LocalDatabase')
local_db_settings = LocalDatabase.objects.all()
for db in local_db_settings: try:
# Пример добавления дополнительной базы данных local_db_settings = LocalDatabase.objects.filter(is_active=True)
settings.DATABASES[db.name] = { for db in local_db_settings:
'ENGINE': 'django.db.backends.mysql', databases[db.name] = {
'NAME': db.db_name, 'ENGINE': db.engine, # Можно хранить тип движка в базе
'USER': db.username, 'NAME': db.database,
'PASSWORD': db.password, 'USER': db.user,
'HOST': db.host, 'PASSWORD': db.password,
'PORT': db.port, 'HOST': db.host,
} 'PORT': db.port,
'ATOMIC_REQUESTS': True, # Убедитесь, что добавляете ATOMIC_REQUESTS
# Вызов этой функции при старте проекта, например, в файле wsgi.py }
load_database_settings() except Exception as e:
print(f"Ошибка загрузки локальных баз данных: {e}")
# Чтение локальных баз данных
local_databases = LocalDatabase.objects.filter(is_active=True)
# Основная база данных
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST'),
'PORT': config('DB_PORT'),
},
}
# Добавление локальных баз данных
for db in local_databases:
DATABASES[db.name] = {
'ENGINE': 'django.db.backends.postgresql',
'NAME': db.name,
'USER': db.user,
'PASSWORD': db.password,
'HOST': db.host,
'PORT': db.port,
}

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,79 @@
# import os
# import django
# import asyncio
# from apscheduler.schedulers.asyncio import AsyncIOScheduler
# from django.core.management.base import BaseCommand
# from telegram.ext import Application
# from bot.utils.bot_setup import setup_bot
# from scheduler.tasks import load_tasks_to_scheduler
# from app_settings.models import TelegramSettings
# from touchh.utils.log import CustomLogger
# class Command(BaseCommand):
# help = "Запуск Telegram бота и планировщика"
# def handle(self, *args, **options):
# # Установка Django окружения
# os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings")
# django.setup()
# # Создаем новый цикл событий
# loop = asyncio.new_event_loop()
# asyncio.set_event_loop(loop)
# # Настройка планировщика
# scheduler = AsyncIOScheduler(event_loop=loop)
# scheduler.start()
# # Загрузка задач в планировщик
# try:
# load_tasks_to_scheduler(scheduler)
# except Exception as e:
# self.stderr.write(f"Ошибка при загрузке задач в планировщик: {e}")
# return
# # Настройка Telegram бота
# # bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
# bot_token = TelegramSettings.objects.first().bot_token
# if not bot_token:
# raise ValueError("Токен бота не найден в переменных окружения.")
# application = Application.builder().token(bot_token).build()
# setup_bot(application)
# # Основная асинхронная функция
# async def main():
# await application.initialize()
# await application.start()
# await application.updater.start_polling()
# self.stdout.write(self.style.SUCCESS("Telegram бот и планировщик успешно запущены."))
# try:
# while True:
# await asyncio.sleep(3600)
# except asyncio.CancelledError:
# await application.stop()
# scheduler.shutdown()
# # Запуск асинхронной программы
# try:
# loop.run_until_complete(main())
# except KeyboardInterrupt:
# self.stdout.write(self.style.ERROR("Завершение работы Telegram бота и планировщика"))
# finally:
# loop.close()
import os import os
import django import django
import asyncio import asyncio
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from telegram.ext import Application from telegram.ext import Application
from bot.utils.bot_setup import setup_bot from bot.utils.bot_setup import setup_bot
from scheduler.tasks import load_tasks_to_scheduler
from app_settings.models import TelegramSettings from app_settings.models import TelegramSettings
from touchh.utils.log import CustomLogger from touchh.utils.log import CustomLogger
class Command(BaseCommand): class Command(BaseCommand):
help = "Запуск Telegram бота и планировщика" help = "Запуск Telegram бота"
def handle(self, *args, **options): def handle(self, *args, **options):
# Установка Django окружения # Установка Django окружения
@@ -21,42 +84,31 @@ class Command(BaseCommand):
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
# Настройка планировщика
scheduler = AsyncIOScheduler(event_loop=loop)
scheduler.start()
# Загрузка задач в планировщик
try:
load_tasks_to_scheduler(scheduler)
except Exception as e:
self.stderr.write(f"Ошибка при загрузке задач в планировщик: {e}")
return
# Настройка Telegram бота # Настройка Telegram бота
# bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
bot_token = TelegramSettings.objects.first().bot_token bot_token = TelegramSettings.objects.first().bot_token
if not bot_token: if not bot_token:
raise ValueError("Токен бота не найден в переменных окружения.") raise ValueError("Токен бота не найден в переменных окружения.")
application = Application.builder().token(bot_token).build() application = Application.builder().token(bot_token).build()
setup_bot(application) setup_bot(application)
# Основная асинхронная функция # Основная асинхронная функция
async def main(): async def main():
await application.initialize() await application.initialize()
await application.start() await application.start()
await application.updater.start_polling() await application.updater.start_polling()
self.stdout.write(self.style.SUCCESS("Telegram бот и планировщик успешно запущены.")) self.stdout.write(self.style.SUCCESS("Telegram бот успешно запущен."))
try: try:
while True: while True:
await asyncio.sleep(3600) await asyncio.sleep(3600)
except asyncio.CancelledError: except asyncio.CancelledError:
await application.stop() await application.stop()
scheduler.shutdown()
# Запуск асинхронной программы # Запуск асинхронной программы
try: try:
loop.run_until_complete(main()) loop.run_until_complete(main())
except KeyboardInterrupt: except KeyboardInterrupt:
self.stdout.write(self.style.ERROR("Завершение работы Telegram бота и планировщика")) self.stdout.write(self.style.ERROR("Завершение работы Telegram бота"))
finally: finally:
loop.close() loop.close()

View File

@@ -4,6 +4,9 @@ from hotels.models import Hotel, UserHotel
from users.models import User from users.models import User
from pms_integration.manager import PMSIntegrationManager from pms_integration.manager import PMSIntegrationManager
from bot.utils.froud_check import detect_fraud from bot.utils.froud_check import detect_fraud
from touchh.utils.log import CustomLogger
logger = CustomLogger(name="BOT-hotels Manager", log_level="DEBUG").get_logger()
async def manage_hotels(update: Update, context): async def manage_hotels(update: Update, context):
"""Отображение списка отелей, связанных с пользователем.""" """Отображение списка отелей, связанных с пользователем."""
query = update.callback_query query = update.callback_query
@@ -81,7 +84,8 @@ async def check_pms(update, context):
try: try:
# Получение ID отеля из callback_data # Получение ID отеля из callback_data
hotel_id = query.data.split("_")[2] hotel_id = query.data.split("_")[2]
logger.debug(f"Hotel ID: {hotel_id}")
logger.debug(f"Hotel ID type : {type(hotel_id)}")
# Получение конфигурации отеля и PMS # Получение конфигурации отеля и PMS
hotel = await sync_to_async(Hotel.objects.select_related('pms').get)(id=hotel_id) hotel = await sync_to_async(Hotel.objects.select_related('pms').get)(id=hotel_id)
pms_config = hotel.pms pms_config = hotel.pms
@@ -99,6 +103,7 @@ async def check_pms(update, context):
if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data): if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data):
# Плагин поддерживает метод fetch_data # Плагин поддерживает метод fetch_data
report = await pms_manager.plugin._fetch_data() report = await pms_manager.plugin._fetch_data()
else: else:
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.") await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")
return return
@@ -110,6 +115,7 @@ async def check_pms(update, context):
f"Обработано записей: {report['processed_items']}\n" f"Обработано записей: {report['processed_items']}\n"
f"Ошибки: {len(report['errors'])}" f"Ошибки: {len(report['errors'])}"
) )
logger.info(f'Result_Message: {result_message}\n Result_meaage_type: {type(result_message)}')
if report["errors"]: if report["errors"]:
result_message += "\n\nСписок ошибок:\n" + "\n".join(report["errors"]) result_message += "\n\nСписок ошибок:\n" + "\n".join(report["errors"])

View File

@@ -1,10 +0,0 @@
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from bot.operations.notifications import schedule_notifications
def setup_scheduler():
"""Настройка планировщика уведомлений."""
print("Настройка планировщика...")
scheduler = AsyncIOScheduler()
scheduler.add_job(schedule_notifications, "cron", minute="*")
return scheduler

View File

@@ -1,116 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-09 09:30
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='APIConfiguration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Название API')),
('url', models.URLField(verbose_name='URL API')),
('token', models.CharField(blank=True, max_length=255, null=True, verbose_name='Токен')),
('username', models.CharField(blank=True, max_length=255, null=True, verbose_name='Логин')),
('password', models.CharField(blank=True, max_length=255, null=True, verbose_name='Пароль')),
('last_updated', models.DateTimeField(auto_now=True, verbose_name='Дата последнего обновления')),
],
options={
'verbose_name': 'Конфигурация API',
'verbose_name_plural': 'Конфигурации API',
},
),
migrations.CreateModel(
name='FraudLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reservation_id', models.BigIntegerField(unique=True, verbose_name='ID бронирования')),
('guest_name', models.CharField(blank=True, max_length=255, null=True)),
('check_in_date', models.DateField()),
('detected_at', models.DateTimeField(auto_now_add=True)),
('message', models.TextField()),
],
options={
'verbose_name': 'Журнал мошенничества',
'verbose_name_plural': 'Журналы мошенничества',
},
),
migrations.CreateModel(
name='Guest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Имя гостя')),
('birthdate', models.DateField(blank=True, null=True, verbose_name='Дата рождения')),
('phone', models.CharField(blank=True, max_length=50, null=True, verbose_name='Телефон')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
],
options={
'verbose_name': 'Гость',
'verbose_name_plural': 'Гости',
},
),
migrations.CreateModel(
name='Reservation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reservation_id', models.BigIntegerField(unique=True, verbose_name='ID бронирования')),
('room_number', models.CharField(max_length=50, verbose_name='Номер комнаты')),
('room_type', models.CharField(max_length=255, verbose_name='Тип комнаты')),
('check_in', models.DateTimeField(verbose_name='Дата заезда')),
('check_out', models.DateTimeField(verbose_name='Дата выезда')),
('status', models.CharField(max_length=50, verbose_name='Статус')),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Цена')),
('discount', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Скидка')),
],
options={
'verbose_name': 'Бронирование',
'verbose_name_plural': 'Бронирования',
},
),
migrations.CreateModel(
name='UserHotel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'verbose_name': 'Пользователь отеля',
'verbose_name_plural': 'Пользователи отелей',
},
),
migrations.CreateModel(
name='APIRequestLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('request_time', models.DateTimeField(auto_now_add=True, verbose_name='Время запроса')),
('response_status', models.IntegerField(validators=[django.core.validators.MinValueValidator(100), django.core.validators.MaxValueValidator(599)], verbose_name='HTTP статус ответа')),
('response_data', models.JSONField(blank=True, null=True, verbose_name='Данные ответа')),
('api', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.apiconfiguration', verbose_name='API')),
],
options={
'verbose_name': 'Журнал запросов API',
'verbose_name_plural': 'Журналы запросов API',
},
),
migrations.CreateModel(
name='Hotel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Название отеля')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создан')),
('api', models.OneToOneField(blank=True, help_text='API, связанный с этим отелем.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.apiconfiguration', verbose_name='API')),
],
options={
'verbose_name': 'Отель',
'verbose_name_plural': 'Отели',
},
),
]

View File

@@ -1,42 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-09 09:30
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('hotels', '0001_initial'),
('pms_integration', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='hotel',
name='pms',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pms_integration.pmsconfiguration', verbose_name='PMS система'),
),
migrations.AddField(
model_name='fraudlog',
name='hotel',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frauds', to='hotels.hotel'),
),
migrations.AddField(
model_name='reservation',
name='hotel',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель'),
),
migrations.AddField(
model_name='guest',
name='reservation',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guests', to='hotels.reservation', verbose_name='Бронирование'),
),
migrations.AddField(
model_name='userhotel',
name='hotel',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hotel_users', to='hotels.hotel', verbose_name='Отель'),
),
]

View File

@@ -1,30 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-09 09:30
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('hotels', '0002_initial'),
('users', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='userhotel',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotels', to='users.user', verbose_name='Пользователь'),
),
migrations.AddIndex(
model_name='apirequestlog',
index=models.Index(fields=['api'], name='hotels_apir_api_id_686bb0_idx'),
),
migrations.AddIndex(
model_name='apirequestlog',
index=models.Index(fields=['request_time'], name='hotels_apir_request_f65147_idx'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-11 10:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0003_initial'),
]
operations = [
migrations.AlterField(
model_name='reservation',
name='room_number',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -1,29 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-13 01:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0004_alter_reservation_room_number'),
]
operations = [
migrations.AddField(
model_name='hotel',
name='import_status',
field=models.CharField(choices=[('not_started', 'Не начат'), ('in_progress', 'В процессе'), ('completed', 'Завершен')], default='not_started', max_length=50, verbose_name='Статус импорта'),
),
migrations.AddField(
model_name='hotel',
name='imported_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата импорта'),
),
migrations.AddField(
model_name='hotel',
name='imported_from',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='imported_hotels', to='hotels.hotel', verbose_name='Импортированный отель'),
),
]

View File

@@ -1,33 +0,0 @@
# hotels/migrations/0006_remove_hotel_import_status_remove_hotel_imported_at_and_more.py
from django.db import migrations
def remove_unused_fields(apps, schema_editor):
Hotel = apps.get_model('hotels', 'Hotel')
Hotel._meta.get_field('import_status').remote_field.model._meta.db_table
Hotel._meta.get_field('imported_at').remote_field.model._meta.db_table
class Migration(migrations.Migration):
dependencies = [
('hotels', '0005_hotel_import_status_hotel_imported_at_and_more'),
]
operations = [
migrations.RemoveField(
model_name='hotel',
name='import_status',
),
migrations.RemoveField(
model_name='hotel',
name='imported_at',
),
migrations.RemoveField(
model_name='hotel',
name='imported_from',
),
migrations.RemoveField(
model_name='hotel',
name='api',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-14 02:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0006_remove_hotel_api_remove_hotel_import_status_and_more'),
]
operations = [
migrations.AddField(
model_name='hotel',
name='hotel_id',
field=models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='ID отеля'),
),
]

View File

@@ -1,38 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-14 02:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0007_hotel_hotel_id'),
]
operations = [
migrations.AddField(
model_name='hotel',
name='address',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Адрес'),
),
migrations.AddField(
model_name='hotel',
name='city',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Город'),
),
migrations.AddField(
model_name='hotel',
name='email',
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email'),
),
migrations.AddField(
model_name='hotel',
name='phone',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Телефон'),
),
migrations.AddField(
model_name='hotel',
name='timezone',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Часовой пояс'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-14 02:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0008_hotel_address_hotel_city_hotel_email_hotel_phone_and_more'),
]
operations = [
migrations.AddField(
model_name='hotel',
name='description',
field=models.TextField(blank=True, null=True, verbose_name='Описание'),
),
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-17 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0011_room_alter_fraudlog_check_in_date_and_more'),
]
operations = [
migrations.AlterField(
model_name='room',
name='number',
field=models.CharField(max_length=50, unique=True, verbose_name='Номер комнаты'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-17 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0012_alter_room_number'),
]
operations = [
migrations.AlterField(
model_name='room',
name='number',
field=models.CharField(max_length=50, verbose_name='Номер комнаты'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-17 11:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0013_alter_room_number'),
]
operations = [
migrations.AlterField(
model_name='room',
name='number',
field=models.CharField(max_length=50, unique=True, verbose_name='Номер комнаты'),
),
]

View File

@@ -18,7 +18,7 @@ class PluginLoader:
print("Загрузка плагинов:") print("Загрузка плагинов:")
for file in os.listdir(PluginLoader.PLUGIN_PATH): for file in os.listdir(PluginLoader.PLUGIN_PATH):
if file.endswith("_pms.py") and not file.startswith("__"): if file.endswith("_pms.py") and not file.startswith("__"):
print(f" Plugin {file}") # print(f" Plugin {file}")
module_name = f"pms_integration.plugins.{file[:-3]}" module_name = f"pms_integration.plugins.{file[:-3]}"
try: try:
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
@@ -26,7 +26,7 @@ class PluginLoader:
cls = getattr(module, attr) cls = getattr(module, attr)
if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin: if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin:
plugin_name = file[:-7] # Убираем `_pms` из имени файла plugin_name = file[:-7] # Убираем `_pms` из имени файла
print(f" Загружен плагин {plugin_name}: {cls.__name__}") # print(f" Загружен плагин {plugin_name}: {cls.__name__}")
plugins[plugin_name] = cls plugins[plugin_name] = cls
except Exception as e: except Exception as e:
print(f" Ошибка загрузки плагина {module_name}: {e}") print(f" Ошибка загрузки плагина {module_name}: {e}")

View File

@@ -1,48 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-09 09:30
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('hotels', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='PMSConfiguration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Название PMS')),
('url', models.URLField(verbose_name='URL API')),
('token', models.CharField(blank=True, max_length=255, null=True, verbose_name='Токен')),
('username', models.CharField(blank=True, max_length=255, null=True, verbose_name='Логин')),
('password', models.CharField(blank=True, max_length=255, null=True, verbose_name='Пароль')),
('plugin_name', models.CharField(max_length=255, verbose_name='Название плагина')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
],
options={
'verbose_name': 'PMS система',
'verbose_name_plural': 'PMS системы',
},
),
migrations.CreateModel(
name='PMSIntegrationLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('checked_at', models.DateTimeField(auto_now_add=True, verbose_name='Время проверки')),
('status', models.CharField(choices=[('success', 'Успех'), ('error', 'Ошибка')], max_length=50, verbose_name='Статус')),
('message', models.TextField(blank=True, null=True, verbose_name='Сообщение')),
('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
],
options={
'verbose_name': 'Журнал интеграции PMS',
'verbose_name_plural': 'Журналы интеграции PMS',
'indexes': [models.Index(fields=['hotel'], name='pms_integra_hotel_i_ade4da_idx'), models.Index(fields=['checked_at'], name='pms_integra_checked_938acc_idx'), models.Index(fields=['status'], name='pms_integra_status_358b64_idx')],
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-10 02:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pms_integration', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='pmsconfiguration',
name='plugin_name',
field=models.CharField(blank=True, choices=[], max_length=255, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-10 02:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pms_integration', '0002_alter_pmsconfiguration_plugin_name'),
]
operations = [
migrations.AlterField(
model_name='pmsconfiguration',
name='plugin_name',
field=models.CharField(blank=True, choices=[], max_length=255, null=True, verbose_name='Плагин'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-10 03:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pms_integration', '0003_alter_pmsconfiguration_plugin_name'),
]
operations = [
migrations.AlterField(
model_name='pmsconfiguration',
name='plugin_name',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Плагин'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-10 03:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pms_integration', '0004_alter_pmsconfiguration_plugin_name'),
]
operations = [
migrations.AddField(
model_name='pmsconfiguration',
name='private_key',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Приватный ключ'),
),
migrations.AddField(
model_name='pmsconfiguration',
name='public_key',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Публичный ключ'),
),
]

View File

@@ -74,7 +74,7 @@ class EcviPMSPlugin(BasePMSPlugin):
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
self.logger.error(f"Ошибка запроса: {e}") self.logger.error(f"Ошибка запроса: {e}")
return [] return []
self.logger.debug(f"\n\n\n\n\ndata: {data}\n\n\n\n\n")
# Фильтрация данных # Фильтрация данных
filtered_data = [ filtered_data = [
{ {
@@ -87,7 +87,6 @@ class EcviPMSPlugin(BasePMSPlugin):
} for item in data if isinstance(item, dict) and item.get('occupancy') in ['проживание', 'под выезд', 'под заезд'] } for item in data if isinstance(item, dict) and item.get('occupancy') in ['проживание', 'под выезд', 'под заезд']
] ]
self.logger.debug(f"filtered_data: {filtered_data}") self.logger.debug(f"filtered_data: {filtered_data}")
# Сохранение данных в базу данных # Сохранение данных в базу данных
@@ -108,7 +107,6 @@ class EcviPMSPlugin(BasePMSPlugin):
# Проверяем, существует ли уже резервация с таким внешним ID # Проверяем, существует ли уже резервация с таким внешним ID
reservation_id = item.get('reservation_id') reservation_id = item.get('reservation_id')
self.logger.debug(f"-----\n\n\nITEM : {item}\n\n\n---")
if not reservation_id: if not reservation_id:
self.logger.error("Ошибка: 'reservation_id' отсутствует в данных.") self.logger.error("Ошибка: 'reservation_id' отсутствует в данных.")
return return
@@ -123,6 +121,7 @@ class EcviPMSPlugin(BasePMSPlugin):
await sync_to_async(Reservation.objects.update_or_create)( await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=reservation_id, reservation_id=reservation_id,
defaults={ defaults={
''
'room_number': item.get('room_number'), 'room_number': item.get('room_number'),
'room_type': item.get('room_type'), 'room_type': item.get('room_type'),
'check_in': item.get('checkin'), 'check_in': item.get('checkin'),

View File

@@ -4,17 +4,16 @@ import json
from .base_plugin import BasePMSPlugin from .base_plugin import BasePMSPlugin
from datetime import datetime, timedelta from datetime import datetime, timedelta
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from math import ceil from touchh.utils.log import CustomLogger
from hotels.models import Hotel, Reservation
class RealtyCalendarPlugin(BasePMSPlugin): class RealtyCalendarPlugin(BasePMSPlugin):
"""Плагин для импорта данных из системы RealtyCalendar """Плагин для импорта данных из системы RealtyCalendar."""
"""
def __init__(self, config): def __init__(self, config):
super().__init__(config) super().__init__(config)
self.public_key = config.public_key self.public_key = config.public_key
self.private_key = config.private_key self.private_key = config.private_key
self.api_url = config.url.rstrip("/") 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: if not self.public_key or not self.private_key:
raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar") raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar")
@@ -31,8 +30,8 @@ class RealtyCalendarPlugin(BasePMSPlugin):
""" """
Возвращает отсортированный по имени список ключей. Возвращает отсортированный по имени список ключей.
""" """
sorted_keys = sorted(list(obj.keys())) sorted_keys = sorted(obj.keys())
print(f"[DEBUG] Отсортированные ключи: {sorted_keys}") self.logger.debug(f"Отсортированные ключи: {sorted_keys}")
return sorted_keys return sorted_keys
def _generate_data_string(self, obj): def _generate_data_string(self, obj):
@@ -41,7 +40,7 @@ class RealtyCalendarPlugin(BasePMSPlugin):
""" """
sorted_keys = self._get_sorted_keys(obj) sorted_keys = self._get_sorted_keys(obj)
string = "".join(f"{key}={obj[key]}" for key in sorted_keys) string = "".join(f"{key}={obj[key]}" for key in sorted_keys)
print(f"[DEBUG] Сформированная строка данных: {string}") self.logger.debug(f"Сформированная строка данных: {string}")
return string + self.private_key return string + self.private_key
def _generate_md5(self, string): def _generate_md5(self, string):
@@ -49,7 +48,7 @@ class RealtyCalendarPlugin(BasePMSPlugin):
Генерирует MD5-хеш от строки. Генерирует MD5-хеш от строки.
""" """
md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest() md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest()
print(f"[DEBUG] Сформированный MD5-хеш: {md5_hash}") self.logger.debug(f"Сформированный MD5-хеш: {md5_hash}")
return md5_hash return md5_hash
def _generate_sign(self, data): def _generate_sign(self, data):
@@ -57,16 +56,17 @@ class RealtyCalendarPlugin(BasePMSPlugin):
Генерирует подпись для данных запроса. Генерирует подпись для данных запроса.
""" """
data_string = self._generate_data_string(data) data_string = self._generate_data_string(data)
print(f"[DEBUG] Строка для подписи: {data_string}") self.logger.debug(f"Строка для подписи: {data_string}")
sign = self._generate_md5(data_string) sign = self._generate_md5(data_string)
print(f"[DEBUG] Подпись: {sign}") self.logger.debug(f"Подпись: {sign}")
return sign return sign
def _fetch_data(self): async def _fetch_data(self):
""" """
Выполняет запрос к API RealtyCalendar для получения данных о бронированиях. Выполняет запрос к API RealtyCalendar для получения данных о бронированиях.
""" """
base_url = f"https://realtycalendar.ru/api/v1/bookings/{self.public_key}/" self.logger.debug("Начало выполнения функции _fetch_data")
base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/"
headers = { headers = {
"Accept": "application/json", "Accept": "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -77,81 +77,150 @@ class RealtyCalendarPlugin(BasePMSPlugin):
data = { data = {
"begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"), "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"),
"end_date": now.strftime("%Y-%m-%d"), "end_date": now.strftime("%Y-%m-%d"),
"room_number": ""
} }
print(f"[DEBUG] Даты выборки: {data}") self.logger.debug(f"Даты выборки: {data}")
# Генерация подписи # Генерация подписи
data["sign"] = self._generate_sign(data) data["sign"] = self._generate_sign(data)
# Отправляем запрос # Отправляем запрос
print(f"[DEBUG] URL запроса: {base_url}") self.logger.debug(f"URL запроса: {base_url}")
print(f"[DEBUG] Заголовки: {headers}") self.logger.debug(f"Заголовки: {headers}")
print(f"[DEBUG] Данные запроса: {data}") self.logger.debug(f"Данные запроса: {data}")
response = requests.post(url=base_url, headers=headers, json=data) response = requests.post(url=base_url, headers=headers, json=data)
self.logger.debug(f"Запрос: {response}")
self.logger.debug(f"Статус ответа: {response.status_code}")
# self.logger.debug(f"Ответ: {response.text}")
# Логируем результат
print(f"[DEBUG] Статус ответа: {response.status_code}")
print(f"[DEBUG] Ответ: {response.text}")
# Проверяем успешность запроса
if response.status_code == 200: if response.status_code == 200:
bookings = response.json().get("bookings", []) try:
print(f"[DEBUG] Полученные данные бронирований: {bookings}") response_data = response.json()
return bookings self.logger.debug(f"Тип данных ответа: {type(response_data)}")
else: if not isinstance(response_data, dict) or "bookings" not in response_data:
raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}, {response.text}") raise ValueError(f"Неожиданная структура ответа: {response_data}")
bookings = response_data.get("bookings", [])
# self.logger.debug(f"Полученные данные бронирований: {bookings}")
except json.JSONDecodeError as e:
self.logger.error(f"Ошибка декодирования JSON: {e}")
raise ValueError("Ошибка декодирования JSON ответа.")
# Фильтрация данных
filtered_data = [
{
"id": item.get("id"),
"begin_date": item.get("begin_date"),
"end_date": item.get("end_date"),
"amount": item.get("amount"),
"status": item.get("status"),
"is_delete": item.get("is_delete"),
"apartment_id": item.get("apartment_id"),
"prepayment": item.get("prepayment"),
"deposit": item.get("deposit"),
"source": item.get("source"),
"notes": item.get("notes"),
}
for item in bookings
if isinstance(item, dict) and item.get("status") in ["booked", "request"]
]
self.logger.debug(f"Отфильтрованные данные: {type(filtered_data)}")
async def _save_to_db(self, data, hotel_id, batch_size=50): for item in filtered_data:
self.logger.debug(f"Данные бронирования: {item}")
await self._save_to_db(item)
from django.utils import timezone
async def _save_to_db(self, data):
from django.utils import timezone
""" """
Сохраняет данные о бронированиях в базу данных партиями. Сохраняет данные в БД (например, информацию о номере).
""" """
from hotels.models import Reservation, Hotel
try: try:
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) # Проверяем общее количество записей для обработки
self.logger.info(f"Загружен отель: {hotel.name}") if not isinstance(data, list):
self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}")
return
# Разделение данных на батчи
total_records = len(data) total_records = len(data)
batches = [data[i:i + batch_size] for i in range(0, total_records, batch_size)] self.logger.info(f"Общее количество записей для обработки: {total_records}")
self.logger.info(f"Обработка {total_records} записей в {len(batches)} партиях...")
for batch_index, batch in enumerate(batches): for index, item in enumerate(data, start=1):
self.logger.info(f"Обработка партии {batch_index + 1}/{len(batches)}") try:
self.logger.info(f"Обработка записи {index}/{total_records}")
for item in batch: # Проверка типа данных
if not isinstance(item, dict):
self.logger.error(f"Пропущена запись {index}/{total_records}: ожидался dict, но получен {type(item).__name__}")
continue
# Получаем отель по настройкам PMS
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
self.logger.debug(f"Отель найден: {hotel.name}")
# Проверяем, существует ли уже резервация с таким внешним ID
reservation_id = item.get('id')
if not reservation_id:
self.logger.error(f"Пропущена запись {index}/{total_records}: отсутствует 'id' в данных.")
continue
# Преобразуем даты в "aware" объекты
try: try:
if item.get("is_delete", False): check_in = timezone.make_aware(datetime.strptime(item.get('begin_date'), "%Y-%m-%d"))
self.logger.info(f"Пропущена запись с ID {item.get('id')} (удалена).") check_out = timezone.make_aware(datetime.strptime(item.get('end_date'), "%Y-%m-%d"))
continue
client_data = item.get("client", {})
if not item.get("id") or not item.get("begin_date") or not item.get("end_date"):
self.logger.warning(f"Пропущена запись с неполными данными: {item}")
continue
reservation_defaults = {
"room_number": item.get("apartment_id", ""),
"check_in": datetime.strptime(item["begin_date"], "%Y-%m-%d"),
"check_out": datetime.strptime(item["end_date"], "%Y-%m-%d"),
"status": item.get("status", ""),
"price": item.get("amount", 0),
"client_name": client_data.get("fio", ""),
"client_email": client_data.get("email", ""),
"client_phone": client_data.get("phone", ""),
}
await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=item["id"],
hotel=hotel,
defaults=reservation_defaults
)
self.logger.info(f"Сохранена запись для бронирования ID {item['id']}.")
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка при обработке бронирования ID {item.get('id', 'неизвестно')}: {e}") self.logger.error(f"Ошибка преобразования дат для записи {index}/{total_records}: {e}")
continue
existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id)
# Теперь вызываем .first() после асинхронного вызова
existing_reservation = await sync_to_async(existing_reservation.first)()
if existing_reservation:
self.logger.debug(f"Резервация {reservation_id} уже существует. Обновляем...")
await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=reservation_id,
defaults={
'room_number': item.get('apartment_id'),
'room_type': 'Описание отсутствует',
'check_in': check_in,
'check_out': check_out,
'status': item.get('status'),
'hotel': hotel
}
)
self.logger.debug(f"Резервация обновлена.")
else:
self.logger.debug(f"Резервация не найдена, создаем новую...")
reservation = await sync_to_async(Reservation.objects.create)(
reservation_id=reservation_id,
room_number=item.get('apartment_id'),
room_type='Описание отсутствует',
check_in=check_in,
check_out=check_out,
status=item.get('status'),
hotel=hotel
)
self.logger.debug(f"Новая резервация создана с ID: {reservation.reservation_id}")
except Exception as e:
self.logger.error(f"Ошибка при обработке записи {index}/{total_records}: {e}")
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка при обработке данных: {e}") self.logger.error(f"Ошибка обработки данных в _save_to_db: {e}")
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__} прошел валидацию.")
return True

View File

@@ -1,16 +1,14 @@
from django.contrib import admin from django.contrib import admin
from django import forms from django import forms
from django.utils.functional import cached_property
from .models import ScheduledTask from .models import ScheduledTask
from django.templatetags.static import static from django.templatetags.static import static
from scheduler.utils import get_project_functions from scheduler.utils import get_project_functions
class CustomAdmin(admin.ModelAdmin):
class Media:
css = {"all": (static("scheduler/admin.css"),)}
js = (static("scheduler/admin.js"),)
class ScheduledTaskForm(forms.ModelForm): class ScheduledTaskForm(forms.ModelForm):
"""
Форма для модели ScheduledTask с кастомным полем для выбора дней недели.
"""
DAYS_OF_WEEK_CHOICES = [ DAYS_OF_WEEK_CHOICES = [
(0, "Воскресенье"), (0, "Воскресенье"),
(1, "Понедельник"), (1, "Понедельник"),
@@ -25,7 +23,7 @@ class ScheduledTaskForm(forms.ModelForm):
choices=DAYS_OF_WEEK_CHOICES, choices=DAYS_OF_WEEK_CHOICES,
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
label="Дни недели", label="Дни недели",
required=False, # Опционально required=False,
) )
class Meta: class Meta:
@@ -36,24 +34,32 @@ class ScheduledTaskForm(forms.ModelForm):
"minutes", "minutes",
"hours", "hours",
"months", "months",
"weekdays", # Используем только поле с галочками "weekdays",
"active", "active",
] ]
def clean_weekdays(self): def clean_weekdays(self):
""" """
Преобразуем список выбранных дней в строку для хранения в базе. Преобразует список выбранных дней в строку для сохранения в базе.
""" """
weekdays = self.cleaned_data.get("weekdays", []) weekdays = self.cleaned_data.get("weekdays", [])
return ",".join(map(str, weekdays)) return ",".join(map(str, weekdays))
@admin.register(ScheduledTask) @admin.register(ScheduledTask)
class ScheduledTaskAdmin(admin.ModelAdmin): class ScheduledTaskAdmin(admin.ModelAdmin):
"""
Кастомный класс для управления ScheduledTask в админке.
"""
form = ScheduledTaskForm form = ScheduledTaskForm
list_display = ("task_name", "function_path", "minutes", "hours", "months", "weekdays", "active", "formatted_last_run") list_display = ("task_name", "function_path", "minutes", "hours", "months", "weekdays", "active", "formatted_last_run")
list_filter = ("active",) list_filter = ("active",)
search_fields = ("task_name", "function_path") search_fields = ("task_name", "function_path")
def formatted_last_run(self, obj): def formatted_last_run(self, obj):
"""
Отформатированный вывод времени последнего запуска задачи.
"""
return obj.last_run.strftime("%Y-%m-%d %H:%M:%S") if obj.last_run else "Никогда" return obj.last_run.strftime("%Y-%m-%d %H:%M:%S") if obj.last_run else "Никогда"
formatted_last_run.short_description = "Последний запуск" formatted_last_run.short_description = "Последний запуск"

View File

@@ -1,7 +1,15 @@
from django.apps import AppConfig from django.apps import AppConfig
from apscheduler.schedulers.asyncio import AsyncIOScheduler
scheduler_instance = AsyncIOScheduler()
class SchedulerConfig(AppConfig): class SchedulerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'scheduler' name = 'scheduler'
verbose_name="Планировщик заданий" verbose_name = 'Планировщик задач'
def ready(self):
"""
Метод ready вызывается при старте приложения.
Здесь не нужно запускать scheduler_instance.start(), чтобы избежать ошибок.
"""
pass

View File

@@ -0,0 +1,39 @@
import asyncio
from django.core.management.base import BaseCommand
from scheduler.apps import scheduler_instance
from scheduler.tasks import load_tasks_to_scheduler
class Command(BaseCommand):
help = "Запуск планировщика задач"
def handle(self, *args, **kwargs):
"""
Создаёт новый event loop, запускает планировщик и загружает задачи.
"""
try:
print("Проверка состояния перед запуском:")
print(f"Scheduler instance: {scheduler_instance}")
# Создаём новый event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# Устанавливаем event loop в планировщик
scheduler_instance.configure(event_loop=loop)
# Запускаем планировщик
scheduler_instance.start()
# Загружаем задачи
load_tasks_to_scheduler(scheduler_instance)
self.stdout.write(self.style.SUCCESS("Планировщик успешно запущен."))
# Удерживаем цикл событий
loop.run_forever()
except KeyboardInterrupt:
self.stdout.write(self.style.WARNING("Остановка планировщика."))
finally:
# Завершаем работу планировщика
scheduler_instance.shutdown(wait=False)

View File

@@ -1,30 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-10 08:38
import scheduler.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ScheduledTask',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('task_name', models.CharField(max_length=255, verbose_name='Название задачи')),
('function_path', models.CharField(choices=scheduler.models.get_available_functions, max_length=500, verbose_name='Путь к функции (модуль.функция)')),
('minutes', models.CharField(default='*', max_length=255, verbose_name='Минуты')),
('hours', models.CharField(default='*', max_length=255, verbose_name='Часы')),
('days', models.CharField(default='*', max_length=255, verbose_name='Дни')),
('months', models.CharField(default='*', max_length=255, verbose_name='Месяцы')),
('weekdays', models.JSONField(default=list, verbose_name='Дни недели')),
('active', models.BooleanField(default=True, verbose_name='Активно')),
('last_run', models.DateTimeField(blank=True, null=True, verbose_name='Последний запуск')),
],
),
]

17
scheduler/reload_tasks.py Normal file
View File

@@ -0,0 +1,17 @@
from django.core.management.base import BaseCommand
from scheduler.tasks import load_tasks_to_scheduler
from scheduler.apps import scheduler_instance
class Command(BaseCommand):
help = "Перезагрузка задач в планировщике"
def handle(self, *args, **kwargs):
try:
# Удаляем все существующие задачи
scheduler_instance.remove_all_jobs()
# Загружаем задачи заново
load_tasks_to_scheduler(scheduler_instance)
self.stdout.write(self.style.SUCCESS("Задачи успешно перезагружены."))
except Exception as e:
self.stdout.write(self.style.ERROR(f"Ошибка перезагрузки задач: {e}"))

View File

@@ -5,7 +5,6 @@ from scheduler.models import ScheduledTask
import importlib import importlib
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
def format_weekdays(weekdays): def format_weekdays(weekdays):
"""Преобразует список дней недели в строку.""" """Преобразует список дней недели в строку."""
if isinstance(weekdays, list): if isinstance(weekdays, list):
@@ -43,7 +42,10 @@ def setup_scheduler():
print("Планировщик запущен.") print("Планировщик запущен.")
return scheduler return scheduler
def load_tasks_to_scheduler(scheduler: BaseScheduler): def load_tasks_to_scheduler(scheduler):
"""
Загружает активные задачи в планировщик.
"""
tasks = ScheduledTask.objects.filter(active=True) tasks = ScheduledTask.objects.filter(active=True)
for task in tasks: for task in tasks:
try: try:

View File

@@ -16,6 +16,9 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
import os
from pprint import pprint
from app_settings.app_settings import load_database_settings
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -31,10 +34,10 @@ SECRET_KEY = 'django-insecure-l_8uu8#p*^zf)9zry80)6u+!+2g1a4tg!wx7@^!uw(+^axyh&h
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', '192.168.219.114', 'a66a-182-226-158-253.ngrok-free.app', '*.ngrok-free.app'] ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', 'localhost', '192.168.219.114', '588a-182-226-158-253.ngrok-free.app', '*.ngrok-free.app']
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
'http://a66a-182-226-158-253.ngrok-free.app', 'http://588a-182-226-158-253.ngrok-free.app',
'https://*.ngrok-free.app', # Это подойдет для любых URL, связанных с ngrok 'https://*.ngrok-free.app', # Это подойдет для любых URL, связанных с ngrok
] ]
@@ -103,10 +106,11 @@ DATABASES = {
'PASSWORD': os.getenv('DB_PASSWORD'), # Пароль пользователя 'PASSWORD': os.getenv('DB_PASSWORD'), # Пароль пользователя
'HOST': os.getenv('DB_HOST', default='0.0.0.0'), # Хост (по умолчанию localhost) 'HOST': os.getenv('DB_HOST', default='0.0.0.0'), # Хост (по умолчанию localhost)
'PORT': os.getenv('DB_PORT', default=3308), # Порт (по умолчанию 3306) 'PORT': os.getenv('DB_PORT', default=3308), # Порт (по умолчанию 3306)
'ATOMIC_REQUESTS': True,
}, },
} }
# load_database_settings(DATABASES)
# Password validation # Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators

View File

@@ -1,120 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-09 09:30
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='LocalUserActivityLog',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('user_id', models.IntegerField()),
('activity_type', models.CharField(max_length=255)),
('timestamp', models.DateTimeField()),
('additional_data', models.JSONField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='UserActivityLog',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
('user_id', models.BigIntegerField(verbose_name='ID пользователя')),
('ip', models.CharField(blank=True, max_length=100, null=True, verbose_name='IP адрес')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Создан')),
('timestamp', models.IntegerField(verbose_name='Время')),
('date_time', models.DateTimeField(verbose_name='Дата')),
('referred', models.CharField(blank=True, max_length=255, null=True)),
('agent', models.CharField(blank=True, max_length=255, null=True, verbose_name='Браузер')),
('platform', models.CharField(blank=True, max_length=255, null=True)),
('version', models.CharField(blank=True, max_length=50, null=True)),
('model', models.CharField(blank=True, max_length=255, null=True)),
('device', models.CharField(blank=True, max_length=50, null=True)),
('UAString', models.TextField(blank=True, null=True)),
('location', models.CharField(blank=True, max_length=255, null=True)),
('page_id', models.BigIntegerField(blank=True, null=True)),
('url_parameters', models.TextField(blank=True, null=True)),
('page_title', models.CharField(blank=True, max_length=255, null=True)),
('type', models.CharField(blank=True, max_length=50, null=True)),
('last_counter', models.IntegerField(blank=True, null=True)),
('hits', models.IntegerField(blank=True, null=True)),
('honeypot', models.BooleanField(blank=True, null=True)),
('reply', models.BooleanField(blank=True, null=True)),
('page_url', models.CharField(blank=True, max_length=255, null=True)),
],
options={
'verbose_name': 'Журнал активности',
'verbose_name_plural': 'Журналы активности',
'db_table': 'user_activity_log',
},
),
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('telegram_id', models.BigIntegerField(blank=True, null=True, unique=True, verbose_name='ID Телеграм')),
('chat_id', models.BigIntegerField(blank=True, null=True, unique=True, verbose_name='ID чата в телеграм')),
('role', models.CharField(choices=[('admin', 'Администратор системы'), ('hotel_user', 'Сотрудник отеля')], default='hotel_user', max_length=20, verbose_name='Роль')),
('confirmed', models.BooleanField(default=False, verbose_name='Подтвержден')),
('groups', models.ManyToManyField(blank=True, related_name='custom_user_set', to='auth.group')),
('user_permissions', models.ManyToManyField(blank=True, related_name='custom_user_set', to='auth.permission')),
],
options={
'verbose_name': 'Пользователь',
'verbose_name_plural': 'Пользователи',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='NotificationSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('telegram_enabled', models.BooleanField(default=True, verbose_name='Уведомления в Telegram')),
('email_enabled', models.BooleanField(default=False, verbose_name='Уведомления по Email')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email для уведомлений')),
('notification_time', models.TimeField(default='09:00', verbose_name='Время отправки уведомлений')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='Пользователь')),
],
options={
'verbose_name': 'Способ оповещения',
'verbose_name_plural': 'Способы оповещений',
},
),
migrations.CreateModel(
name='UserConfirmation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('confirmation_code', models.UUIDField(default=uuid.uuid4, verbose_name='Код подтверждения')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создан: ')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='Пользователь')),
],
options={
'verbose_name': 'Подтверждение пользователя',
'verbose_name_plural': 'Подтверждения пользователей',
},
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 5.1.4 on 2024-12-13 00:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='LocalUserActivityLog',
),
migrations.DeleteModel(
name='UserActivityLog',
),
migrations.DeleteModel(
name='UserConfirmation',
),
]