ssMerge branch 'pms_plugins'

This commit is contained in:
2024-12-24 21:36:24 +09:00
66 changed files with 859 additions and 1967 deletions

View File

@@ -112,7 +112,7 @@ class ExternalDBSettingsAdmin(admin.ModelAdmin):
@admin.register(UserActivityLog) @admin.register(UserActivityLog)
class UserActivityLogAdmin(admin.ModelAdmin): class UserActivityLogAdmin(admin.ModelAdmin):
list_display = ("id", 'get_location',"formatted_timestamp", "date_time", "page_id", "url_parameters", "page_url" ,"created", "page_title", "type", "hits") list_display = ("id", 'get_location',"formatted_timestamp", "date_time", "page_id", "url_parameters", "page_url" ,"created", "page_title", "type", "hits")
search_fields = ("page_title", "url_parameters") search_fields = ("page_title", "url_parameters", "page_title")
list_filter = ("page_title", "created") list_filter = ("page_title", "created")
readonly_fields = ("created", "timestamp") readonly_fields = ("created", "timestamp")
@@ -151,69 +151,83 @@ class UserActivityLogAdmin(admin.ModelAdmin):
get_hotel_name.short_description = "Отель" get_hotel_name.short_description = "Отель"
get_room_number.short_description = "Комната" get_room_number.short_description = "Комната"
from .views import import_selected_hotels # from .views import import_selected_hotels
# Регистрируем admin класс для ImportedHotel # # Регистрируем admin класс для ImportedHotel
@admin.register(ImportedHotel) # @admin.register(ImportedHotel)
class ImportedHotelAdmin(admin.ModelAdmin): # class ImportedHotelAdmin(admin.ModelAdmin):
change_list_template = "antifroud/admin/import_hotels.html" # change_list_template = "antifroud/admin/import_hotels.html"
list_display = ("external_id", "display_name", "name", "created", "updated", "imported") # list_display = ("external_id", "display_name", "name", "created", "updated", "imported")
search_fields = ("name", "display_name", "external_id") # search_fields = ("name", "display_name", "external_id")
list_filter = ("name", "display_name", "external_id") # list_filter = ("name", "display_name", "external_id")
actions = ['mark_as_imported', 'delete_selected_hotels_action'] # actions = ['mark_as_imported', 'delete_selected_hotels_action']
def get_urls(self): # def get_urls(self):
# Получаем стандартные URL-адреса и добавляем наши # # Получаем стандартные URL-адреса и добавляем наши
urls = super().get_urls() # urls = super().get_urls()
custom_urls = [ # custom_urls = [
path('import_selected_hotels/', import_selected_hotels, name='antifroud_importedhotels_import_selected_hotels'), # path('import_selected_hotels/', import_selected_hotels, name='antifroud_importedhotels_import_selected_hotels'),
path('delete_selected_hotels/', self.delete_selected_hotels, name='delete_selected_hotels'), # path('delete_selected_hotels/', self.delete_selected_hotels, name='delete_selected_hotels'),
path('delete_hotel/<int:hotel_id>/', self.delete_hotel, name='delete_hotel'), # Изменили на URL параметр # path('delete_hotel/<int:hotel_id>/', self.delete_hotel, name='delete_hotel'), # Изменили на URL параметр
] # ]
return custom_urls + urls # return custom_urls + urls
@transaction.atomic # @transaction.atomic
def delete_selected_hotels(self, request): # def delete_selected_hotels(self, request):
if request.method == 'POST': # if request.method == 'POST':
selected = request.POST.get('selected', '') # selected = request.POST.get('selected', '')
if selected: # if selected:
external_ids = selected.split(',') # external_ids = selected.split(',')
deleted_count, _ = ImportedHotel.objects.filter(external_id__in=external_ids).delete() # deleted_count, _ = ImportedHotel.objects.filter(external_id__in=external_ids).delete()
messages.success(request, f"Удалено отелей: {deleted_count}") # messages.success(request, f"Удалено отелей: {deleted_count}")
else: # else:
messages.warning(request, "Не выбрано ни одного отеля для удаления.") # messages.warning(request, "Не выбрано ни одного отеля для удаления.")
return redirect('admin:antifroud_importedhotel_changelist') # return redirect('admin:antifroud_importedhotel_changelist')
def delete_selected_hotels(self, request, queryset): # def delete_selected_hotels(self, request, queryset):
deleted_count, _ = queryset.delete() # deleted_count, _ = queryset.delete()
self.message_user(request, f'{deleted_count} отелей было удалено.') # self.message_user(request, f'{deleted_count} отелей было удалено.')
delete_selected_hotels.short_description = "Удалить выбранные отели" # delete_selected_hotels.short_description = "Удалить выбранные отели"
def mark_as_imported(self, request, queryset): # def mark_as_imported(self, request, queryset):
updated = queryset.update(imported=True) # updated = queryset.update(imported=True)
self.message_user(request, f"Отмечено как импортированное: {updated}", messages.SUCCESS) # self.message_user(request, f"Отмечено как импортированное: {updated}", messages.SUCCESS)
mark_as_imported.short_description = "Отметить выбранные как импортированные" # mark_as_imported.short_description = "Отметить выбранные как импортированные"
# Метод для удаления одного отеля # # Метод для удаления одного отеля
@transaction.atomic # @transaction.atomic
def delete_hotel(self, request, hotel_id): # def delete_hotel(self, request, hotel_id):
imported_hotel = get_object_or_404(ImportedHotel, id=hotel_id) # imported_hotel = get_object_or_404(ImportedHotel, id=hotel_id)
imported_hotel.delete() # imported_hotel.delete()
messages.success(request, f"Отель {imported_hotel.name} успешно удалён.") # messages.success(request, f"Отель {imported_hotel.name} успешно удалён.")
return redirect('admin:antifroud_importedhotel_changelist') # return redirect('admin:antifroud_importedhotel_changelist')
@admin.register(SyncLog) @admin.register(SyncLog)
class SyncLogAdmin(admin.ModelAdmin): class SyncLogAdmin(admin.ModelAdmin):
change_list_template = "antifroud/admin/sync_log.html" change_list_template = "antifroud/admin/sync_log.html" # Путь к вашему кастомному шаблону
list_display =['id', 'hotel', 'created', 'recieved_records', 'processed_records'] list_display = ['id', 'hotel', 'created', 'recieved_records', 'processed_records']
search_fields = ['id', 'hotel', 'created', 'recieved_records', 'processed_records'] search_fields = ['id', 'hotel__name', 'recieved_records', 'processed_records']
list_filter = ['id', 'hotel', 'created', 'recieved_records', 'processed_records'] list_filter = ['hotel', 'created']
class Meta: def changelist_view(self, request, extra_context=None):
model = SyncLog """
fields = ['hotel', 'recieved_records', 'processed_records'] Добавляет фильтрацию по отелям в шаблон.
"""
extra_context = extra_context or {}
# Получаем выбранный фильтр отеля из GET-параметров
hotel_id = request.GET.get('hotel')
hotels = Hotel.objects.all()
sync_logs = SyncLog.objects.all()
if hotel_id:
sync_logs = sync_logs.filter(hotel_id=hotel_id)
extra_context['sync_logs'] = sync_logs
extra_context['hotels'] = hotels
extra_context['selected_hotel'] = hotel_id # Чтобы отобразить выбранный отель
return super().changelist_view(request, extra_context=extra_context)
@admin.register(ViolationLog) @admin.register(ViolationLog)
class ViolationLogAdmin(admin.ModelAdmin): class ViolationLogAdmin(admin.ModelAdmin):
list_display = ['id', 'hotel', 'room_number' , 'hits', 'created_at', 'violation_type', 'violation_details', 'detected_at'] list_display = ['id', 'hotel', 'room_number' , 'hits', 'created_at', 'violation_type', 'violation_details', 'detected_at']
@@ -222,4 +236,15 @@ class ViolationLogAdmin(admin.ModelAdmin):
class Meta: class Meta:
model = ViolationLog model = ViolationLog
fields = ['hotel', 'room_number', 'created_at', 'violation_type', 'violation_details', 'detected_at'] fields = ['hotel', 'room_number', 'created_at', 'violation_type', 'violation_details', 'detected_at']
@admin.register(RoomDiscrepancy)
class RoomDiscrepancyAdmin(admin.ModelAdmin):
list_display = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']
search_fields = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']
list_filter = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']
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,14 +1,13 @@
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
from django.db.models import Q from django.db.models import Q
from hotels.models import Reservation, Hotel from hotels.models import Reservation, Hotel
from .models import UserActivityLog, ViolationLog from .models import UserActivityLog, ViolationLog
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}")
self.logger.info(f"Attempting to update sync log for hotel: {hotel.name}")
self.update_sync_log(hotel, recieved_records, processed_records)
def process_and_save_data(self, rows): def process_and_save_data(self, rows):
""" hotel_processed_counts = {} # Словарь для подсчёта записей по каждому отелю
Обрабатывает и сохраняет данные из внешнего источника.
:param rows: Список строк данных, полученных из базы данных.
"""
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

@@ -4,7 +4,7 @@
<style> <style>
#table-data-preview { #table-data-preview {
max-height: 300px; /* Ограничиваем высоту предпросмотра */ max-height: 500px; /* Ограничиваем высоту предпросмотра */
overflow-y: auto; /* Прокрутка по вертикали */ overflow-y: auto; /* Прокрутка по вертикали */
overflow-x: auto; /* Прокрутка по горизонтали */ overflow-x: auto; /* Прокрутка по горизонтали */
} }
@@ -36,27 +36,27 @@
<form id="connection-form" method="post"> <form id="connection-form" method="post">
{% csrf_token %} {% csrf_token %}
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="db-name">Name</label> <label for="db-name">Имя подключения</label>
<input id="db-name" class="form-control" type="text" name="name" value="{{ original.name }}" required /> <input id="db-name" class="form-control" type="text" name="name" value="{{ original.name }}" required />
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="db-host">DB Host</label> <label for="db-host">Сервер БД</label>
<input id="db-host" class="form-control" type="text" name="host" value="{{ original.host }}" required /> <input id="db-host" class="form-control" type="text" name="host" value="{{ original.host }}" required />
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="db-port">DB Port</label> <label for="db-port">Порт БД</label>
<input id="db-port" class="form-control" type="number" name="port" value="{{ original.port }}" required /> <input id="db-port" class="form-control" type="number" name="port" value="{{ original.port }}" required />
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="db-user">User</label> <label for="db-user">Пользователь</label>
<input id="db-user" class="form-control" type="text" name="user" value="{{ original.user }}" required /> <input id="db-user" class="form-control" type="text" name="user" value="{{ original.user }}" required />
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="db-password">Password</label> <label for="db-password">Пароль</label>
<input id="db-password" class="form-control" type="password" name="password" value="{{ original.password }}" /> <input id="db-password" class="form-control" type="password" name="password" value="{{ original.password }}" />
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="db-database">Database</label> <label for="db-database">Имя Базы данных</label>
<input id="db-database" class="form-control" type="text" name="database" value="{{ original.database }}" required /> <input id="db-database" class="form-control" type="text" name="database" value="{{ original.database }}" required />
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
@@ -78,10 +78,11 @@
</table> </table>
</div> </div>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3" style="text-center">
<label for="is-active">Активное подключение</label>
<input id="is-active" class="form-check-input" type="checkbox" name="is_active" {% if original.is_active %}checked{% endif %} /> <input class="form-check-input" id="is-active" class="form-check-input" type="checkbox" name="is_active" {% if original.is_active %}checked{% endif %}>
</div> <label class="form-check-label" for="inlineCheckbox2"><b>Активное подключение</b></label>
<div class="form-group text-center"> <div class="form-group text-center">
<button class="btn btn-success" type="submit">Сохранить</button> <button class="btn btn-success" type="submit">Сохранить</button>
<button class="btn btn-secondary" type="button" id="close-button">Закрыть</button> <button class="btn btn-secondary" type="button" id="close-button">Закрыть</button>

View File

@@ -1,7 +1,6 @@
{% extends "admin/change_list.html" %} {% extends "admin/change_list.html" %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<div class="row mt-4"> <div class="row mt-4">
<div class="col"> <div class="col">
<div class="card shadow-sm mb-2 db-graph"> <div class="card shadow-sm mb-2 db-graph">
@@ -9,53 +8,22 @@
<h6 class="text-white m-0 font-md">Журнал синхронизации</h6> <h6 class="text-white m-0 font-md">Журнал синхронизации</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post" action="{% url 'antifroud:sync_log_create' %}"> <!-- Форма фильтрации по отелям -->
{% csrf_token %} <form method="get" class="form-inline mb-3">
<div class="form-row"> <label for="hotel-filter" class="mr-2">Фильтр по отелям:</label>
<div class="col-md-9 col-xl-9"> <select name="hotel" id="hotel-filter" class="form-control mr-2">
<div class="box-bg"> <option value="">-- Все отели --</option>
<div class="form-row"> {% for hotel in hotels %}
<div class="col-md-2 col-xl-2 align-self-center font-md text-dark-blue"> <option value="{{ hotel.id }}" {% if hotel.id|stringformat:"s" == selected_hotel %}selected{% endif %}>
<label class="col-form-label p-0" for="hotel-id"><strong>Отель:</strong></label> {{ hotel.name }}
</div> </option>
<div class="col-md-4 col-xl-3"> {% endfor %}
<div class="form-group mb-0"> </select>
<select class="custom-select custom-select-sm font-sm" name="hotel" id="hotel-id"> <button type="submit" class="btn btn-primary">Применить</button>
<option value="">--Выберите Отель --</option>
{% for hotel in hotels %}
<option value="{{ hotel.id }}">{{ hotel.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-xl-3">
<div class="box-bg">
<div class="text-dark form-row">
<div class="col-xl-5 offset-xl-0 align-self-center">
<h6 class="mb-0 font-sm">Полученные записи:</h6>
</div>
<div class="col-xl-7 offset-xl-0 text-right align-self-center">
<div class="form-group mb-1">
<input class="form-control form-control-sm form-control font-sm" type="number" name="received_records" required />
</div>
</div>
<div class="col-xl-5 offset-xl-0 align-self-center">
<h6 class="mb-0 font-sm">Обработанные записи:</h6>
</div>
<div class="col-xl-7 offset-xl-0 text-right align-self-center">
<div class="form-group mb-1">
<input class="form-control form-control-sm form-control font-sm" type="number" name="processed_records" required />
</div>
</div>
</div>
</div>
</div>
</div>
</form> </form>
<!-- Список существующих журналов синхронизации --> <!-- Список существующих журналов синхронизации -->
<div class="table-responsive tbl-wfx mt-1 kot-table"> <div class="table-responsive tbl-wfx mt-1 kot-table">
<table class="table table-sm"> <table class="table table-sm">
@@ -63,10 +31,8 @@
<tr class="text-dark-blue"> <tr class="text-dark-blue">
<th>#</th> <th>#</th>
<th>Отель</th> <th>Отель</th>
<th>ID бронирования</th> <th> Дата синхронизации</th>
<th>Обработанные записи</th> <th>Обработанные записи</th>
<th>Полученные записи</th>
<th>Создан</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -74,14 +40,12 @@
<tr> <tr>
<td>{{ log.id }}</td> <td>{{ log.id }}</td>
<td>{{ log.hotel.name }}</td> <td>{{ log.hotel.name }}</td>
<td>{{ log.reservation_id }}</td>
<td>{{ log.processed_records }}</td>
<td>{{ log.recieved_records }}</td>
<td>{{ log.created }}</td> <td>{{ log.created }}</td>
<td>{{ log.processed_records }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="6" class="text-center">Нет журналов.</td> <td colspan="5" class="text-center">Нет записей.</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -93,4 +57,4 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,5 @@
# settings.py
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
from .models import LocalDatabase from .models import LocalDatabase
from decouple import config from decouple import config
@@ -11,11 +11,17 @@ def load_database_settings():
======= =======
from decouple import config from decouple import config
from django.conf import settings from django.conf import settings
=======
>>>>>>> pms_plugins
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')
<<<<<<< HEAD
>>>>>>> antifraud >>>>>>> antifraud
local_db_settings = LocalDatabase.objects.all() local_db_settings = LocalDatabase.objects.all()
@@ -29,32 +35,20 @@ def load_database_settings():
'HOST': db.host, 'HOST': db.host,
'PORT': db.port, 'PORT': db.port,
} }
=======
>>>>>>> pms_plugins
# Вызов этой функции при старте проекта, например, в файле wsgi.py try:
load_database_settings() local_db_settings = LocalDatabase.objects.filter(is_active=True)
for db in local_db_settings:
# Чтение локальных баз данных databases[db.name] = {
local_databases = LocalDatabase.objects.filter(is_active=True) 'ENGINE': db.engine, # Можно хранить тип движка в базе
'NAME': db.database,
# Основная база данных 'USER': db.user,
DATABASES = { 'PASSWORD': db.password,
'default': { 'HOST': db.host,
'ENGINE': 'django.db.backends.postgresql', 'PORT': db.port,
'NAME': config('DB_NAME'), 'ATOMIC_REQUESTS': True, # Убедитесь, что добавляете ATOMIC_REQUESTS
'USER': config('DB_USER'), }
'PASSWORD': config('DB_PASSWORD'), except Exception as e:
'HOST': config('DB_HOST'), print(f"Ошибка загрузки локальных баз данных: {e}")
'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,
}

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

@@ -11,8 +11,13 @@ from datetime import datetime
from django.utils.timezone import make_aware, is_aware, is_naive from django.utils.timezone import make_aware, is_aware, is_naive
import os import os
import traceback import traceback
import logging
from touchh.utils.log import CustomLogger
logger = CustomLogger(__name__).get_logger()
async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Вывод списка отелей для статистики.""" """Вывод списка отелей для статистики."""
query = update.callback_query query = update.callback_query
@@ -33,7 +38,7 @@ async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Формируем кнопки для выбора отеля # Формируем кнопки для выбора отеля
keyboard = [ keyboard = [
[InlineKeyboardButton(hotel.hotel.name, callback_data=f"stats_hotel_{hotel.hotel.id}")] [InlineKeyboardButton(f'🏨 {hotel.hotel.name}', callback_data=f"stats_hotel_{hotel.hotel.id}")]
for hotel in user_hotels for hotel in user_hotels
] ]
keyboard.append([InlineKeyboardButton("🏠 Главная", callback_data="main_menu")]) keyboard.append([InlineKeyboardButton("🏠 Главная", callback_data="main_menu")])
@@ -61,27 +66,31 @@ async def stats_select_period(update: Update, context: ContextTypes.DEFAULT_TYPE
def ensure_datetime(value): def ensure_datetime(value):
# print(f"statistics.py [DEBUG] ensure_datetime: Received value: {value} ({type(value)})")
""" """
Ensure that the given value is a timezone-aware datetime object. Преобразует значение в timezone-aware datetime объект, если это возможно.
If the given value is a string, it is assumed to be in the format :param value: Значение для преобразования
'%Y-%m-%d %H:%M:%S'. If the given value is a naive datetime object, :type value: str, datetime или другое
it is converted to a timezone-aware datetime object using :return: timezone-aware datetime
django.utils.timezone.make_aware.
:param value: The value to be converted
:type value: str or datetime
:return: A timezone-aware datetime object
:rtype: datetime
""" """
if isinstance(value, str): if isinstance(value, datetime):
value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') # Если это объект datetime, проверяем, наивен ли он
if isinstance(value, datetime) and is_naive(value): return make_aware(value) if is_naive(value) else value
value = make_aware(value) elif isinstance(value, str):
# print(f"statistics.py [DEBUG] ensure_datetime: Returning value: {value} ({type(value)})") # Если это строка, пытаемся преобразовать в datetime
return value try:
return make_aware(datetime.strptime(value, '%Y-%m-%d %H:%M:%S'))
except ValueError:
# Если формат не соответствует, пробуем более общую обработку
try:
return make_aware(datetime.fromisoformat(value))
except ValueError:
# Если не получилось распознать формат
logging.warning(f"Невозможно преобразовать строку в datetime: {value}")
else:
# Если тип неизвестен, просто возвращаем None
logging.warning(f"Получено значение неизвестного типа для преобразования в datetime: {value}")
return None
async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Генерация и отправка статистики.""" """Генерация и отправка статистики."""
@@ -90,77 +99,45 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE
try: try:
hotel_id = context.user_data.get("selected_hotel") hotel_id = context.user_data.get("selected_hotel")
if not hotel_id: if not hotel_id:
raise ValueError(f"ID отеля не найден в user_data: {context.user_data}") raise ValueError(f"ID отеля не найден в user_data: {context.user_data}")
period = query.data.split("_")[2] period = query.data.split("_")[2]
now = ensure_datetime(datetime.utcnow())
# Получаем диапазон дат
now = datetime.utcnow().replace(tzinfo=timezone.utc)
print(type(now))
print(type(period))
start_date, end_date = get_period_dates(period, now) start_date, end_date = get_period_dates(period, now)
reservations = await sync_to_async(list)(
try: Reservation.objects.filter(
# Получаем бронирования hotel_id=hotel_id,
reservations = await sync_to_async(list)( check_in__gte=start_date,
Reservation.objects.filter( check_in__lte=end_date
hotel_id=hotel_id, ).select_related('hotel')
check_in__gte=start_date, )
check_in__lte=end_date
).select_related('hotel')
)
except Exception as e:
raise RuntimeError(f"statistics.py Ошибка при выборке бронирований: {e}") from e
if not reservations: if not reservations:
await query.edit_message_text("statistics.py Нет данных для статистики за выбранный период.") await query.edit_message_text("Нет данных для статистики за выбранный период.")
return return
try: hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
# Получаем данные об отеле
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
except Hotel.DoesNotExist:
raise RuntimeError(f"statistics.py Отель с ID {hotel_id} не найден")
except Exception as e:
raise RuntimeError(f"statistics.py Ошибка при выборке отеля: {e}") from e
try: file_path = await generate_pdf_report(hotel.name, reservations, start_date, end_date)
# Генерация отчета
file_path = await generate_pdf_report(hotel.name, reservations, start_date, end_date)
except Exception as e: with open(file_path, "rb") as file:
raise RuntimeError(f"statistics.py [ERROR] Ошибка при генерации PDF-отчета: {e}") from e await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf")
try:
# Отправка файла через Telegram
with open(file_path, "rb") as file:
await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf")
except Exception as e:
raise RuntimeError(f"Ошибка при отправке PDF-файла: {e}") from e
# Удаляем временный файл
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
except Exception as e: except Exception as e:
# Логируем стек вызовов для детального анализа logging.error(f"Ошибка в generate_statistics: {str(e)}", exc_info=True)
error_trace = traceback.format_exc() logging.error(f'start_date_type: {type(start_date)}, \n end_date_type: {type(end_date)}\n')
await query.edit_message_text(f"Произошла ошибка: {str(e)}") await query.edit_message_text(f"Произошла ошибка: {str(e)}")
def get_period_dates(period, now): def get_period_dates(period, now):
now = ensure_datetime(now)
if period == "day": if period == "day":
start_date = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) start_date = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
@@ -178,6 +155,7 @@ def get_period_dates(period, now):
end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
return start_date, end_date return start_date, end_date
async def stats_back(update: Update, context): async def stats_back(update: Update, context):
"""Возврат к выбору отеля.""" """Возврат к выбору отеля."""
query = update.callback_query query = update.callback_query
@@ -203,3 +181,6 @@ async def stats_back(update: Update, context):
keyboard.append([InlineKeyboardButton("🏠 Главная", callback_data="main_menu")]) keyboard.append([InlineKeyboardButton("🏠 Главная", callback_data="main_menu")])
reply_markup = InlineKeyboardMarkup(keyboard) reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите отель:", reply_markup=reply_markup) await query.edit_message_text("Выберите отель:", reply_markup=reply_markup)

53
bot/utils/date_utils.py Normal file
View File

@@ -0,0 +1,53 @@
# bot/utils/date_utils.py
from datetime import datetime, timedelta
import pytz
def ensure_datetime(value):
"""
Приводит значение к объекту datetime с учетом временной зоны.
:param value: Значение даты (строка или datetime).
:return: Объект datetime.
:raises: TypeError, если передан некорректный тип.
"""
if isinstance(value, str):
try:
# Если строка соответствует формату ISO 8601
return datetime.fromisoformat(value)
except ValueError:
raise ValueError(f"Некорректный формат даты: {value}")
elif isinstance(value, datetime):
return value
else:
raise TypeError(f"Ожидался тип str или datetime, получено: {type(value)}")
def get_period_dates(period, now=None):
"""
Возвращает диапазон дат (start_date, end_date) для заданного периода.
:param period: Период (строка: 'today', 'yesterday', 'last_week', 'last_month').
:param now: Текущая дата/время (опционально).
:return: Кортеж (start_date, end_date).
:raises: ValueError, если период не поддерживается.
"""
if now is None:
now = datetime.now(pytz.UTC)
if period == "today":
start_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
elif period == "yesterday":
start_date = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = (now - timedelta(days=1)).replace(hour=23, minute=59, second=59, microsecond=999999)
elif period == "last_week":
start_date = (now - timedelta(days=now.weekday() + 7)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = (start_date + timedelta(days=6)).replace(hour=23, minute=59, second=59, microsecond=999999)
elif period == "last_month":
first_day_of_current_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
last_day_of_previous_month = first_day_of_current_month - timedelta(days=1)
start_date = last_day_of_previous_month.replace(day=1)
end_date = last_day_of_previous_month.replace(hour=23, minute=59, second=59, microsecond=999999)
else:
raise ValueError(f"Неподдерживаемый период: {period}")
return start_date, end_date

View File

@@ -17,22 +17,49 @@ os.makedirs(REPORTS_DIR, exist_ok=True)
# Асинхронная функция для извлечения данных о бронировании # Асинхронная функция для извлечения данных о бронировании
def ensure_datetime(value): def ensure_datetime(value):
"""Преобразует строку или naive datetime в timezone-aware datetime.""" """
Преобразует строку или naive datetime в timezone-aware datetime.
Если значение не удается преобразовать, возвращается None.
"""
if isinstance(value, datetime):
return make_aware(value) if is_naive(value) else value
if isinstance(value, str): if isinstance(value, str):
value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') try:
if isinstance(value, datetime) and is_naive(value): return make_aware(datetime.strptime(value, '%Y-%m-%d %H:%M:%S'))
value = make_aware(value) except ValueError:
return value print(f"[WARNING] Невозможно преобразовать строку в datetime: {value}")
return None
# @sync_to_async
# def get_reservation_data(res):
# print(f"[DEBUG] Processing reservation {res.id}")
# # Убедитесь, что даты являются timezone-aware
# check_in = ensure_datetime(res.check_in)
# check_out = ensure_datetime(res.check_out)
# result = {
# "hotel_name": res.hotel.name,
# "pms": getattr(res.hotel, 'pms', 'N/A'),
# "reservation_id": res.reservation_id,
# "room_number": res.room_number if res.room_number else "Не указан",
# "room_type": res.room_type,
# "check_in": check_in,
# "check_out": check_out,
# "status": res.status,
# }
# # print(f"[DEBUG] Reservation data: {result}")
# return result
@sync_to_async @sync_to_async
def get_reservation_data(res): def get_reservation_data(res):
print(f"[DEBUG] Processing reservation {res.id}")
# Убедитесь, что даты являются timezone-aware
check_in = ensure_datetime(res.check_in) check_in = ensure_datetime(res.check_in)
check_out = ensure_datetime(res.check_out) check_out = ensure_datetime(res.check_out)
result = { if not check_in or not check_out:
raise ValueError(f"Некорректные даты бронирования: check_in={res.check_in}, check_out={res.check_out}")
return {
"hotel_name": res.hotel.name, "hotel_name": res.hotel.name,
"pms": getattr(res.hotel, 'pms', 'N/A'), "pms": getattr(res.hotel, 'pms', 'N/A'),
"reservation_id": res.reservation_id, "reservation_id": res.reservation_id,
@@ -42,8 +69,6 @@ def get_reservation_data(res):
"check_out": check_out, "check_out": check_out,
"status": res.status, "status": res.status,
} }
# print(f"[DEBUG] Reservation data: {result}")
return result
@@ -61,27 +86,21 @@ class CustomPDF(FPDF):
self.end_date = end_date self.end_date = end_date
def header(self): def header(self):
"""Добавление заголовка и заголовков таблицы на каждой странице.""" if self.page == 1:
# Заголовок отчёта
if self.page == 1: # Заголовок отчёта только на первой странице
self.set_font("DejaVuSans-Bold", size=14) self.set_font("DejaVuSans-Bold", size=14)
self.cell(0, 10, f"Отчет о бронированиях отеля {self.hotel_name}", ln=1, align="C") self.cell(0, 10, f"Отчет о бронированиях отеля {self.hotel_name}", ln=1, align="C")
self.ln(5) self.ln(5)
self.set_font("DejaVuSans", size=10) self.set_font("DejaVuSans", size=10)
self.cell(0, 10, f"за период {self.start_date.strftime('%Y-%m-%d %H:%M:%S')} - {self.end_date.strftime('%Y-%m-%d %H:%M:%S')}", ln=1, align="C") self.cell(
0,
10,
f"за период {self.start_date.strftime('%Y-%m-%d %H:%M:%S')} - {self.end_date.strftime('%Y-%m-%d %H:%M:%S')}",
ln=1,
align="C"
)
self.ln(10) self.ln(10)
# Заголовки таблицы
self.set_font("DejaVuSans-Bold", size=8)
headers = ["Отель", "№ бронирования", "№ комнаты", "Тип комнаты", "Заезд", "Выезд", "Статус"]
col_widths = [30, 30, 30, 60, 35, 35, 30]
row_height = 10
for col_width, header in zip(col_widths, headers):
self.cell(col_width, row_height, header, border=1, align="C")
self.ln()
def footer(self): def footer(self):
"""Добавление колонтитула внизу страницы.""" """Добавление колонтитула внизу страницы."""
self.set_y(-15) self.set_y(-15)
@@ -97,13 +116,68 @@ class CustomPDF(FPDF):
text = text[:-1] text = text[:-1]
return text + "..." if len(text) > 3 else text return text + "..." if len(text) > 3 else text
# async def generate_pdf_report(hotel_name, reservations, start_date, end_date):
# # Преобразование дат в timezone-aware datetime
# start_date = ensure_datetime(start_date)
# end_date = ensure_datetime(end_date)
# if not start_date or not end_date:
# raise ValueError(f"Некорректные периоды: start_date={start_date}, end_date={end_date}")
# # Создание экземпляра PDF с передачей параметров
# pdf = CustomPDF(hotel_name=hotel_name, start_date=start_date, end_date=end_date, orientation="L", unit="mm", format="A4")
# pdf.alias_nb_pages()
# pdf.add_page() # Заголовок отчёта и таблица будут добавлены через методы header и footer
# # Таблица
# pdf.set_font("DejaVuSans", size=8)
# col_widths = [30, 30, 30, 60, 35, 35, 30]
# row_height = 10
# for res in reservations:
# try:
# res_data = await get_reservation_data(res)
# row_data = [
# res_data["hotel_name"],
# str(res_data["reservation_id"]),
# res_data["room_number"],
# res_data["room_type"],
# res_data["check_in"].strftime('%Y-%m-%d %H:%M:%S'),
# res_data["check_out"].strftime('%Y-%m-%d %H:%M:%S'),
# res_data["status"],
# ]
# for col_width, data in zip(col_widths, row_data):
# pdf.cell(col_width, row_height, data, border=1, align="C")
# pdf.ln()
# except Exception as e:
# print(f"pdf_report.py [ERROR] Error processing reservation {res.id}: {e}")
# # Сохранение PDF
# hotel_name_safe = hotel_name.replace(" ", "_").replace("/", "_")
# start_date_str = start_date.strftime('%Y-%m-%d')
# end_date_str = end_date.strftime('%Y-%m-%d')
# pdf_output_path = os.path.join(REPORTS_DIR, f"{hotel_name_safe}_report_{start_date_str}-{end_date_str}.pdf")
# pdf.output(pdf_output_path)
# if not os.path.exists(pdf_output_path):
# raise RuntimeError(f"PDF file was not created at: {pdf_output_path}")
# return pdf_output_path
async def generate_pdf_report(hotel_name, reservations, start_date, end_date): async def generate_pdf_report(hotel_name, reservations, start_date, end_date):
# Создание экземпляра PDF с передачей параметров # Преобразование дат
start_date = ensure_datetime(start_date)
end_date = ensure_datetime(end_date)
pdf = CustomPDF(hotel_name=hotel_name, start_date=start_date, end_date=end_date, orientation="L", unit="mm", format="A4") pdf = CustomPDF(hotel_name=hotel_name, start_date=start_date, end_date=end_date, orientation="L", unit="mm", format="A4")
pdf.alias_nb_pages() pdf.alias_nb_pages()
pdf.add_page() # Заголовок отчёта и таблица будут добавлены через методы header и footer pdf.add_page()
# Таблица
pdf.set_font("DejaVuSans", size=8) pdf.set_font("DejaVuSans", size=8)
col_widths = [30, 30, 30, 60, 35, 35, 30] col_widths = [30, 30, 30, 60, 35, 35, 30]
row_height = 10 row_height = 10
@@ -111,29 +185,36 @@ async def generate_pdf_report(hotel_name, reservations, start_date, end_date):
for res in reservations: for res in reservations:
try: try:
res_data = await get_reservation_data(res) res_data = await get_reservation_data(res)
# Отладочный вывод
print(f"[DEBUG] Reservation Data: {res_data}")
print(f"[DEBUG] check_in type: {type(res_data['check_in'])}, value: {res_data['check_in']}")
print(f"[DEBUG] check_out type: {type(res_data['check_out'])}, value: {res_data['check_out']}")
# Проверка и корректировка данных
res_data["check_in"] = ensure_datetime(res_data["check_in"])
res_data["check_out"] = ensure_datetime(res_data["check_out"])
row_data = [ row_data = [
res_data["hotel_name"], str(res_data["hotel_name"]),
str(res_data["reservation_id"]), str(res_data["reservation_id"]),
res_data["room_number"], str(res_data["room_number"]),
res_data["room_type"], str(res_data["room_type"]),
res_data["check_in"].strftime('%Y-%m-%d %H:%M:%S'), res_data["check_in"].strftime('%Y-%m-%d %H:%M:%S'),
res_data["check_out"].strftime('%Y-%m-%d %H:%M:%S'), res_data["check_out"].strftime('%Y-%m-%d %H:%M:%S'),
res_data["status"], str(res_data["status"]),
] ]
for col_width, data in zip(col_widths, row_data): for col_width, data in zip(col_widths, row_data):
pdf.cell(col_width, row_height, data, border=1, align="C") pdf.cell(col_width, row_height, data, border=1, align="C")
pdf.ln() pdf.ln()
except Exception as e: except Exception as e:
print(f"pdf_report.py [ERROR] Error processing reservation {res.id}: {e}") print(f"[ERROR] Error processing reservation {res.id}: {e}")
print("[DEBUG] PDF metadata:", vars(pdf))
# Сохранение PDF pdf_output_path = os.path.join(REPORTS_DIR, f"{hotel_name.replace(' ', '_')}_report_{start_date.strftime('%Y-%m-%d')}-{end_date.strftime('%Y-%m-%d')}.pdf")
hotel_name_safe = hotel_name.replace(" ", "_").replace("/", "_")
start_date_str = start_date.strftime('%Y-%m-%d')
end_date_str = end_date.strftime('%Y-%m-%d')
pdf_output_path = os.path.join(REPORTS_DIR, f"{hotel_name_safe}_report_{start_date_str}-{end_date_str}.pdf")
pdf.output(pdf_output_path) pdf.output(pdf_output_path)
if not os.path.exists(pdf_output_path): if not os.path.exists(pdf_output_path):

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

@@ -1,139 +1,20 @@
# import requests
# import hashlib
# import json
# from .base_plugin import BasePMSPlugin
# from datetime import datetime, timedelta
# from asgiref.sync import sync_to_async
# class RealtyCalendarPlugin(BasePMSPlugin):
# """Плагин для импорта данных из системы RealtyCalendar
# """
# def __init__(self, config):
# super().__init__(config)
# self.public_key = config.public_key
# self.private_key = config.private_key
# self.api_url = config.url.rstrip("/") # Убираем лишний `/` в конце URL
# if not self.public_key or not self.private_key:
# raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar")
# def get_default_parser_settings(self):
# """
# Возвращает настройки по умолчанию для обработки данных.
# """
# return {
# "date_format": "%Y-%m-%dT%H:%M:%S",
# "timezone": "UTC"
# }
# def _get_sorted_keys(self, obj):
# """
# Возвращает отсортированный по имени список ключей.
# """
# return sorted(list(obj.keys()))
# def _generate_data_string(self, obj):
# """
# Формирует строку параметров для подписи.
# """
# sorted_keys = self._get_sorted_keys(obj)
# string = "".join(f"{key}={obj[key]}" for key in sorted_keys)
# return string + self.private_key
# def _generate_md5(self, string):
# """
# Генерирует MD5-хеш от строки.
# """
# return hashlib.md5(string.encode("utf-8")).hexdigest()
# def _generate_sign(self, data):
# """
# Генерирует подпись для данных запроса.
# """
# data_string = self._generate_data_string(data)
# return self._generate_md5(data_string)
# def fetch_data(self):
# """
# Выполняет запрос к API RealtyCalendar для получения данных о бронированиях.
# """
# base_url = f"https://realtycalendar.ru/api/v1/bookings/{self.public_key}/"
# headers = {
# "Accept": "application/json",
# "Content-Type": "application/json",
# }
# # Определяем даты выборки
# now = datetime.now()
# data = {
# "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"),
# "end_date": now.strftime("%Y-%m-%d"),
# }
# # Генерация подписи
# data["sign"] = self._generate_sign(data)
# # Отправляем запрос
# print(f"URL запроса: {base_url}")
# print(f"Заголовки: {headers}")
# print(f"Данные запроса: {data}")
# response = requests.post(url=base_url, headers=headers, json=data)
# # Логируем результат
# print(f"Статус ответа: {response.status_code}")
# print(f"Ответ: {response.text}")
# # Проверяем успешность запроса
# if response.status_code == 200:
# return response.json().get("bookings", [])
# else:
# raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}, {response.text}")
# async def _save_to_db(self, data, hotel_id):
# """
# Сохраняет данные о бронированиях в базу данных.
# """
# from hotels.models import Reservation, Hotel
# hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
# for item in data:
# try:
# reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
# reservation_id=item["id"],
# hotel=hotel,
# defaults={
# "room_number": item.get("apartment_id", ""), # ID квартиры
# "check_in": datetime.strptime(item["begin_date"], "%Y-%m-%d"), # Дата заезда
# "check_out": datetime.strptime(item["end_date"], "%Y-%m-%d"), # Дата выезда
# "status": item.get("status", ""), # Статус бронирования
# "price": item.get("amount", 0), # Сумма оплаты
# "client_name": item["client"].get("fio", ""), # Имя клиента
# "client_email": item["client"].get("email", ""), # Email клиента
# "client_phone": item["client"].get("phone", ""), # Телефон клиента
# }
# )
# print(f"{'Создана' if created else 'Обновлена'} запись: {reservation}")
# except Exception as e:
# print(f"Ошибка при сохранении бронирования ID {item['id']}: {e}")
import requests import requests
import hashlib import hashlib
import json 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 touchh.utils.log import CustomLogger
from hotels.models import Hotel, Reservation
from app_settings.models import GlobalHotelSettings
from django.utils import timezone
class RealtyCalendarPlugin(BasePMSPlugin): class RealtyCalendarPlugin(BasePMSPlugin):
"""Плагин для импорта данных из системы 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")
@@ -150,8 +31,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):
@@ -160,7 +41,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):
@@ -168,7 +49,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):
@@ -176,79 +57,133 @@ 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",
} }
# Определяем даты выборки
now = datetime.now() now = datetime.now()
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"),
} }
print(f"[DEBUG] Даты выборки: {data}")
# Генерация подписи
data["sign"] = self._generate_sign(data) data["sign"] = self._generate_sign(data)
# Отправляем запрос
print(f"[DEBUG] URL запроса: {base_url}")
print(f"[DEBUG] Заголовки: {headers}")
print(f"[DEBUG] Данные запроса: {data}")
response = requests.post(url=base_url, headers=headers, json=data) response = requests.post(url=base_url, headers=headers, json=data)
self.logger.debug(f"Статус ответа: {response.status_code}")
# Логируем результат if response.status_code != 200:
print(f"[DEBUG] Статус ответа: {response.status_code}") self.logger.error(f"Ошибка API: {response.status_code}, {response.text}")
print(f"[DEBUG] Ответ: {response.text}") raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}")
# Проверяем успешность запроса try:
if response.status_code == 200: response_data = response.json()
bookings = response.json().get("bookings", []) bookings = response_data.get("bookings", [])
print(f"[DEBUG] Полученные данные бронирований: {bookings}") if not isinstance(bookings, list):
return bookings raise ValueError(f"Ожидался список, но получен {type(bookings)}")
else: except Exception as e:
raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}, {response.text}") self.logger.error(f"Ошибка обработки ответа API: {e}")
raise
# Получаем глобальные настройки отеля
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
hotel_tz = hotel.timezone
try:
hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)()
check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S")
check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S")
except AttributeError:
# Используем значения по умолчанию, если настроек нет
check_in_time = "14:00:00"
check_out_time = "12:00:00"
async def _save_to_db(self, data, hotel_id): filtered_data = [
{
'reservation_id': item.get('id'),
'checkin': timezone.make_aware(
datetime.strptime(
f"{item.get('begin_date')} {check_in_time}",
"%Y-%m-%d %H:%M:%S"
)
),
'checkout': timezone.make_aware(
datetime.strptime(
f"{item.get('end_date')} {check_out_time}",
"%Y-%m-%d %H:%M:%S"
)
),
'room_number': item.get('apartment_id'),
'room_type': item.get('notes', 'Описание отсутствует'),
'status': item.get('status')
}
for item in bookings
if isinstance(item, dict) and item.get("status") in ["booked", "request"]
]
await self._save_to_db(filtered_data)
async def _save_to_db(self, data):
""" """
Сохраняет данные о бронированиях в базу данных. Сохраняет данные в БД (например, информацию о номере).
""" """
from hotels.models import Reservation, Hotel if not isinstance(data, list):
self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}")
return
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) for index, item in enumerate(data, start=1):
print(f"[DEBUG] Загружен отель: {hotel.name}")
for item in data:
print(f"[DEBUG] Обработка бронирования: {item}")
try: try:
reservation, created = await sync_to_async(Reservation.objects.update_or_create)( hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
reservation_id=item["id"], reservation_id = item.get('reservation_id')
hotel=hotel, if not reservation_id:
defaults={ self.logger.error(f"Пропущена запись {index}: отсутствует 'id'")
"room_number": item.get("apartment_id", ""), # ID квартиры continue
"check_in": datetime.strptime(item["begin_date"], "%Y-%m-%d"), # Дата заезда
"check_out": datetime.strptime(item["end_date"], "%Y-%m-%d"), # Дата выезда existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id)
"status": item.get("status", ""), # Статус бронирования existing_reservation = await sync_to_async(existing_reservation.first)()
"price": item.get("amount", 0), # Сумма оплаты
"client_name": item["client"].get("fio", ""), # Имя клиента defaults = {
"client_email": item["client"].get("email", ""), # Email клиента 'room_number': item['room_number'],
"client_phone": item["client"].get("phone", ""), # Телефон клиента 'room_type': item['room_type'],
} 'check_in': item['checkin'],
) 'check_out': item['checkout'],
print(f"[DEBUG] {'Создана' if created else 'Обновлена'} запись: {reservation}") '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: except Exception as e:
print(f"[DEBUG] Ошибка при сохранении бронирования ID {item['id']}: {e}") self.logger.error(f"Ошибка при обработке записи {index}: {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

@@ -0,0 +1,116 @@
import logging
import requests
from datetime import datetime
from .base_plugin import BasePMSPlugin
class TravelLinePMSPlugin(BasePMSPlugin):
"""
Плагин для интеграции с PMS TravelLine.
"""
BASE_URL = "https://partner.tlintegration.com/api/webpms/v1"
def __init__(self, pms_config):
"""
Инициализация плагина с конфигурацией PMS.
:param pms_config: Конфигурация PMS (объект PMSConfiguration).
"""
super().__init__(pms_config)
self.api_key = pms_config.api_key
self.logger = logging.getLogger(self.__class__.__name__)
def _get_headers(self):
"""
Возвращает заголовки для запросов.
"""
return {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
def _fetch_data(self):
"""
Получение данных из API TravelLine (поиск бронирований).
:return: Список номеров бронирований.
"""
url = f"{self.BASE_URL}/bookings"
params = {
"roomId": self.pms_config.room_id,
"modifiedFrom": self.pms_config.modified_from,
"modifiedTo": self.pms_config.modified_to,
"state": self.pms_config.state,
"affectsPeriodFrom": self.pms_config.affects_period_from,
"affectsPeriodTo": self.pms_config.affects_period_to,
}
params = {k: v for k, v in params.items() if v is not None}
try:
response = requests.get(url, headers=self._get_headers(), params=params)
response.raise_for_status()
self.logger.info("Данные успешно получены из API TravelLine.")
return response.json().get("bookingNumbers", [])
except requests.RequestException as e:
self.logger.error(f"Ошибка при запросе к API TravelLine: {e}")
return []
def fetch_data(self):
"""
Обертка для получения данных из API TravelLine с дополнительной обработкой.
:return: Список номеров бронирований.
"""
return self._fetch_data()
def get_default_parser_settings(self):
"""
Возвращает настройки парсера по умолчанию.
"""
return {
"field_mapping": {
"reservation_id": "bookingNumber",
"check_in": "actualCheckInDateTime",
"check_out": "actualCheckOutDateTime",
"room_number": "roomId",
"status": "state",
},
"date_format": "%Y-%m-%dT%H:%M"
}
def process_data(self, booking_number, room_stay_id, action, actual_date_time):
"""
Обработка данных для заселения или выселения проживания.
:param booking_number: Номер бронирования.
:param room_stay_id: Идентификатор проживания.
:param action: Действие ("check-in" или "check-out").
:param actual_date_time: Фактические дата и время.
:return: Ответ API.
"""
if action not in ["check-in", "check-out"]:
raise ValueError("Invalid action. Must be 'check-in' or 'check-out'.")
url = f"{self.BASE_URL}/bookings/{booking_number}/room-stays/{room_stay_id}/{action}"
payload = {
f"actual{action.capitalize()}DateTime": actual_date_time
}
try:
response = requests.post(url, headers=self._get_headers(), json=payload)
response.raise_for_status()
self.logger.info(f"Успешно выполнено действие '{action}' для бронирования {booking_number}.")
return response.json()
except requests.RequestException as e:
self.logger.error(f"Ошибка при выполнении действия '{action}': {e}")
return {}
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.info(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', '8f6e-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', 'https://8f6e-182-226-158-253.ngrok-free.app',
'https://*.ngrok-free.app', # Это подойдет для любых URL, связанных с ngrok 'https://*.ngrok-free.app', # Это подойдет для любых URL, связанных с ngrok
] ]
@@ -103,9 +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
@@ -197,20 +202,54 @@ JAZZMIN_SETTINGS = {
"welcome_sign": "Welcome to Touchh Admin", # Приветствие на странице входа "welcome_sign": "Welcome to Touchh Admin", # Приветствие на странице входа
"copyright": "Touchh", # Кастомный текст в футере "copyright": "Touchh", # Кастомный текст в футере
"icons": { "icons": {
"auth": "fas fa-users-cog", # Приложения
"users": "fas fa-user-circle",
"hotels": "fas fa-hotel", "hotels": "fas fa-hotel",
"hotels.hotel": "fas fa-hotel",
"hotels.room":"fas fa-bed",
"hotels.reservation":"fas fa-calendar-week",
"hotels.userhotel":"fas fa-user-shield",
"app_settings": "fas fa-wrench",
"scheduler": "fas fa-clock", "scheduler": "fas fa-clock",
"pms_integration": "fas fa-sync", "pms_integration": "fas fa-sync",
"antifroud": "fas fa-shield-alt", "antifraud": "fas fa-shield-alt",
"app_settings": "fas fa-wrench",
# Модели
"hotels.reservation": "fas fa-calendar-check", # Бронирования
"hotels.userhotel": "fas fa-user-shield", # Пользователи отеля
"app_settings.telegramsettings": "fab fa-telegram", # Настройки Telegram
"app_settings.emailsettings": "fas fa-envelope", # Настройки Email
"app_settings.localdatabase": "fas fa-database", # Локальная база данных
"app_settings.globalhotelsettings": "fas fa-tools", # Глобальные настройки отеля
"app_settings.globalsystemsettings": "fas fa-cogs", # Глобальные системные настройки
"antifraud.externaldbsettings": "fas fa-server", # Подключение к внешним базам данных
"antifraud.roomdiscrepancy": "fas fa-exclamation-circle", # Несоответствия в комнатах
"scheduler.scheduledtask": "fas fa-tasks", # Запланированные задачи
"antifraud.violationlog": "fas fa-ban", # Журнал нарушений
"hotels.importedhotel": "fas fa-download", # Импортированные отели
"hotels.synclog": "fas fa-sync-alt", # Журнал синхронизации
"auth": "fas fa-users-cog",
"users": "fas fa-user-circle",
"hotels": "fas fa-hotel",
"hotels.hotel": "fas fa-hotel",
"hotels.room":"fas fa-bed",
"hotels.reservation":"fas fa-calendar-week",
"hotels.userhotel":"fas fa-user-shield",
"app_settings": "fas fa-wrench",
"app_settings.telegramsettings":"fab fa-telegram",
"app_settings.emailsettings":"fab fa-at",
"app_settings.localdatabase":"fas fa-database",
"app_settings.globalhotelsettings":"fas fa-hammer",
"app_settings.globalsystemsettings":"fas fa-cogs",
"scheduler": "fas fa-clock",
"pms_integration": "fas fa-sync",
"antifroud": "fas fa-shield-alt",
"antifroud.externaldbsettings": "fas fa-database",
"antifroud.roomdiscrepancy": "fas fa-user-secret",
"antifroud.violationlog": "fas fa-ban",
"antifroud.importedhotel": "fas fa-download",
"antifroud.synclog": "fas fa-sync-alt",
"antifroud.useractivitylog": "fas fa-qrcode",
},
},
"theme": "sandstone", "theme": "sandstone",
"dark_mode_theme": "darkly", "dark_mode_theme": "darkly",
"footer": { "footer": {
@@ -223,10 +262,10 @@ JAZZMIN_SETTINGS = {
], ],
"show_ui_builder": True, "show_ui_builder": False,
"show_ui_builder_breadcrumbs": True, "show_ui_builder_breadcrumbs": False,
"show_ui_builder_tabs": True, "show_ui_builder_tabs": False,
"show_ui_builder_tabs_breadcrumbs": True, "show_ui_builder_tabs_breadcrumbs": False,
} }

View File

@@ -5,7 +5,8 @@ from antifroud import views
app_name = 'touchh' app_name = 'touchh'
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls),
path('', admin.site.urls),
path('health/', include('health_check.urls')), path('health/', include('health_check.urls')),
path('antifroud/', include('antifroud.urls')), path('antifroud/', include('antifroud.urls')),

View File

@@ -21,7 +21,10 @@ class CustomLogger:
# Уровень логирования по умолчанию из .env # Уровень логирования по умолчанию из .env
default_level = os.getenv("LOG_LEVEL", "INFO").upper() default_level = os.getenv("LOG_LEVEL", "INFO").upper()
self.log_level = getattr(logging, log_level.upper(), getattr(logging, default_level, logging.INFO)) if log_level:
self.log_level = getattr(logging, log_level.upper(), getattr(logging, default_level, logging.INFO))
else:
self.log_level = getattr(logging, default_level, logging.INFO)
self.setup_logger() self.setup_logger()

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