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)
class UserActivityLogAdmin(admin.ModelAdmin):
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")
readonly_fields = ("created", "timestamp")
@@ -151,69 +151,83 @@ class UserActivityLogAdmin(admin.ModelAdmin):
get_hotel_name.short_description = "Отель"
get_room_number.short_description = "Комната"
from .views import import_selected_hotels
# Регистрируем admin класс для ImportedHotel
@admin.register(ImportedHotel)
class ImportedHotelAdmin(admin.ModelAdmin):
change_list_template = "antifroud/admin/import_hotels.html"
list_display = ("external_id", "display_name", "name", "created", "updated", "imported")
search_fields = ("name", "display_name", "external_id")
list_filter = ("name", "display_name", "external_id")
actions = ['mark_as_imported', 'delete_selected_hotels_action']
# from .views import import_selected_hotels
# # Регистрируем admin класс для ImportedHotel
# @admin.register(ImportedHotel)
# class ImportedHotelAdmin(admin.ModelAdmin):
# change_list_template = "antifroud/admin/import_hotels.html"
# list_display = ("external_id", "display_name", "name", "created", "updated", "imported")
# search_fields = ("name", "display_name", "external_id")
# list_filter = ("name", "display_name", "external_id")
# actions = ['mark_as_imported', 'delete_selected_hotels_action']
def get_urls(self):
# Получаем стандартные URL-адреса и добавляем наши
urls = super().get_urls()
custom_urls = [
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_hotel/<int:hotel_id>/', self.delete_hotel, name='delete_hotel'), # Изменили на URL параметр
]
return custom_urls + urls
# def get_urls(self):
# # Получаем стандартные URL-адреса и добавляем наши
# urls = super().get_urls()
# custom_urls = [
# 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_hotel/<int:hotel_id>/', self.delete_hotel, name='delete_hotel'), # Изменили на URL параметр
# ]
# return custom_urls + urls
@transaction.atomic
def delete_selected_hotels(self, request):
if request.method == 'POST':
selected = request.POST.get('selected', '')
if selected:
external_ids = selected.split(',')
deleted_count, _ = ImportedHotel.objects.filter(external_id__in=external_ids).delete()
messages.success(request, f"Удалено отелей: {deleted_count}")
else:
messages.warning(request, "Не выбрано ни одного отеля для удаления.")
return redirect('admin:antifroud_importedhotel_changelist')
# @transaction.atomic
# def delete_selected_hotels(self, request):
# if request.method == 'POST':
# selected = request.POST.get('selected', '')
# if selected:
# external_ids = selected.split(',')
# deleted_count, _ = ImportedHotel.objects.filter(external_id__in=external_ids).delete()
# messages.success(request, f"Удалено отелей: {deleted_count}")
# else:
# messages.warning(request, "Не выбрано ни одного отеля для удаления.")
# return redirect('admin:antifroud_importedhotel_changelist')
def delete_selected_hotels(self, request, queryset):
deleted_count, _ = queryset.delete()
self.message_user(request, f'{deleted_count} отелей было удалено.')
delete_selected_hotels.short_description = "Удалить выбранные отели"
# def delete_selected_hotels(self, request, queryset):
# deleted_count, _ = queryset.delete()
# self.message_user(request, f'{deleted_count} отелей было удалено.')
# delete_selected_hotels.short_description = "Удалить выбранные отели"
def mark_as_imported(self, request, queryset):
updated = queryset.update(imported=True)
self.message_user(request, f"Отмечено как импортированное: {updated}", messages.SUCCESS)
mark_as_imported.short_description = "Отметить выбранные как импортированные"
# def mark_as_imported(self, request, queryset):
# updated = queryset.update(imported=True)
# self.message_user(request, f"Отмечено как импортированное: {updated}", messages.SUCCESS)
# mark_as_imported.short_description = "Отметить выбранные как импортированные"
# Метод для удаления одного отеля
@transaction.atomic
def delete_hotel(self, request, hotel_id):
imported_hotel = get_object_or_404(ImportedHotel, id=hotel_id)
imported_hotel.delete()
messages.success(request, f"Отель {imported_hotel.name} успешно удалён.")
return redirect('admin:antifroud_importedhotel_changelist')
# # Метод для удаления одного отеля
# @transaction.atomic
# def delete_hotel(self, request, hotel_id):
# imported_hotel = get_object_or_404(ImportedHotel, id=hotel_id)
# imported_hotel.delete()
# messages.success(request, f"Отель {imported_hotel.name} успешно удалён.")
# return redirect('admin:antifroud_importedhotel_changelist')
@admin.register(SyncLog)
class SyncLogAdmin(admin.ModelAdmin):
change_list_template = "antifroud/admin/sync_log.html"
list_display =['id', 'hotel', 'created', 'recieved_records', 'processed_records']
search_fields = ['id', 'hotel', 'created', 'recieved_records', 'processed_records']
list_filter = ['id', 'hotel', 'created', 'recieved_records', 'processed_records']
change_list_template = "antifroud/admin/sync_log.html" # Путь к вашему кастомному шаблону
list_display = ['id', 'hotel', 'created', 'recieved_records', 'processed_records']
search_fields = ['id', 'hotel__name', 'recieved_records', 'processed_records']
list_filter = ['hotel', 'created']
class Meta:
model = SyncLog
fields = ['hotel', 'recieved_records', 'processed_records']
def changelist_view(self, request, extra_context=None):
"""
Добавляет фильтрацию по отелям в шаблон.
"""
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)
class ViolationLogAdmin(admin.ModelAdmin):
list_display = ['id', 'hotel', 'room_number' , 'hits', 'created_at', 'violation_type', 'violation_details', 'detected_at']
@@ -223,3 +237,14 @@ class ViolationLogAdmin(admin.ModelAdmin):
class Meta:
model = ViolationLog
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 urllib.parse import parse_qs
from django.utils import timezone
from django.db.models import Q
from hotels.models import Reservation, Hotel
from .models import UserActivityLog, ViolationLog
from touchh.utils.log import CustomLogger
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger = CustomLogger(__name__).get_logger()
class ReservationChecker:
"""

View File

@@ -202,63 +202,56 @@ class DataSyncManager:
self.logger.error(f"Error fetching data: {e}")
return []
def update_sync_log(self, hotel, recieved_records, processed_records):
"""
Обновляет или создает запись в таблице SyncLog.
"""
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:
log.recieved_records = recieved_records
log.processed_records = processed_records
self.logger.info(f"Sync log created for hotel '{hotel.name}'.")
else:
log.recieved_records += recieved_records
log.processed_records += processed_records
log.save()
self.logger.info(f"Sync log updated for hotel '{hotel.name}'.")
except Exception as e:
self.logger.error(f"Error updating sync log for hotel '{hotel.name}': {e}")
def process_and_save_data(self, rows):
"""
Обрабатывает и сохраняет данные из внешнего источника.
self.logger.info(f"Attempting to update sync log for hotel: {hotel.name}")
self.update_sync_log(hotel, recieved_records, processed_records)
:param rows: Список строк данных, полученных из базы данных.
"""
seen_entries = set()
def process_and_save_data(self, rows):
hotel_processed_counts = {} # Словарь для подсчёта записей по каждому отелю
for row in rows:
# Получение и декодирование URL-параметров
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") # Извлекаем hotel_id из параметров
room_number = parsed_params.get("utm_term") # Извлекаем room_number из параметров
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
# Проверка на дубликаты
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:
# Получение или создание отеля
hotel = self.hotel_manager.get_or_create_hotel(hotel_id, row.get("page_title"))
if not hotel:
self.logger.warning(f"Skipping record: Failed to create or retrieve hotel with ID {hotel_id}")
continue
# Получение или создание комнаты
room = self.hotel_manager.get_or_create_room(hotel, room_number)
if not room:
self.logger.warning(f"Skipping record: Failed to create or retrieve room {room_number} in hotel {hotel.name}")
continue
# Создание или обновление записи активности пользователя
UserActivityLog.objects.update_or_create(
external_id=row.get("id"),
defaults={
@@ -270,14 +263,23 @@ class DataSyncManager:
"url_parameters": parsed_params,
"page_title": self.data_processor.decode_html_entities(row.get("page_title")) or "Untitled",
"page_url": row.get("page_url") or "",
"page_id": row.get("page_id") or 0,
"hits": row.get("hits") or 0,
}
)
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:
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):
@@ -292,7 +294,8 @@ class DataSyncManager:
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.")
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>
#table-data-preview {
max-height: 300px; /* Ограничиваем высоту предпросмотра */
max-height: 500px; /* Ограничиваем высоту предпросмотра */
overflow-y: auto; /* Прокрутка по вертикали */
overflow-x: auto; /* Прокрутка по горизонтали */
}
@@ -36,27 +36,27 @@
<form id="connection-form" method="post">
{% csrf_token %}
<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 />
</div>
<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 />
</div>
<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 />
</div>
<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 />
</div>
<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 }}" />
</div>
<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 />
</div>
<div class="form-group mb-3">
@@ -78,10 +78,11 @@
</table>
</div>
</div>
<div class="form-group mb-3">
<label for="is-active">Активное подключение</label>
<input id="is-active" class="form-check-input" type="checkbox" name="is_active" {% if original.is_active %}checked{% endif %} />
</div>
<div class="form-group mb-3" style="text-center">
<input class="form-check-input" id="is-active" class="form-check-input" type="checkbox" name="is_active" {% if original.is_active %}checked{% endif %}>
<label class="form-check-label" for="inlineCheckbox2"><b>Активное подключение</b></label>
<div class="form-group text-center">
<button class="btn btn-success" type="submit">Сохранить</button>
<button class="btn btn-secondary" type="button" id="close-button">Закрыть</button>

View File

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

View File

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

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 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 бота и планировщика"
help = "Запуск Telegram бота"
def handle(self, *args, **options):
# Установка Django окружения
@@ -21,42 +84,31 @@ class Command(BaseCommand):
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 бот и планировщик успешно запущены."))
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 бота и планировщика"))
self.stdout.write(self.style.ERROR("Завершение работы Telegram бота"))
finally:
loop.close()

View File

@@ -4,6 +4,9 @@ from hotels.models import Hotel, UserHotel
from users.models import User
from pms_integration.manager import PMSIntegrationManager
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):
"""Отображение списка отелей, связанных с пользователем."""
query = update.callback_query
@@ -81,7 +84,8 @@ async def check_pms(update, context):
try:
# Получение ID отеля из callback_data
hotel_id = query.data.split("_")[2]
logger.debug(f"Hotel ID: {hotel_id}")
logger.debug(f"Hotel ID type : {type(hotel_id)}")
# Получение конфигурации отеля и PMS
hotel = await sync_to_async(Hotel.objects.select_related('pms').get)(id=hotel_id)
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):
# Плагин поддерживает метод fetch_data
report = await pms_manager.plugin._fetch_data()
else:
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")
return
@@ -110,6 +115,7 @@ async def check_pms(update, context):
f"Обработано записей: {report['processed_items']}\n"
f"Ошибки: {len(report['errors'])}"
)
logger.info(f'Result_Message: {result_message}\n Result_meaage_type: {type(result_message)}')
if 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
import os
import traceback
import logging
from touchh.utils.log import CustomLogger
logger = CustomLogger(__name__).get_logger()
async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Вывод списка отелей для статистики."""
query = update.callback_query
@@ -33,7 +38,7 @@ async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Формируем кнопки для выбора отеля
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
]
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):
# 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
'%Y-%m-%d %H:%M:%S'. If the given value is a naive datetime object,
it is converted to a timezone-aware datetime object using
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
:param value: Значение для преобразования
:type value: str, datetime или другое
:return: timezone-aware datetime
"""
if isinstance(value, str):
value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
if isinstance(value, datetime) and is_naive(value):
value = make_aware(value)
# print(f"statistics.py [DEBUG] ensure_datetime: Returning value: {value} ({type(value)})")
return value
if isinstance(value, datetime):
# Если это объект datetime, проверяем, наивен ли он
return make_aware(value) if is_naive(value) else value
elif isinstance(value, str):
# Если это строка, пытаемся преобразовать в datetime
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):
"""Генерация и отправка статистики."""
@@ -90,24 +99,15 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE
try:
hotel_id = context.user_data.get("selected_hotel")
if not hotel_id:
raise ValueError(f"ID отеля не найден в user_data: {context.user_data}")
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)
try:
# Получаем бронирования
reservations = await sync_to_async(list)(
Reservation.objects.filter(
hotel_id=hotel_id,
@@ -116,51 +116,28 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE
).select_related('hotel')
)
except Exception as e:
raise RuntimeError(f"statistics.py Ошибка при выборке бронирований: {e}") from e
if not reservations:
await query.edit_message_text("statistics.py Нет данных для статистики за выбранный период.")
await query.edit_message_text("Нет данных для статистики за выбранный период.")
return
try:
# Получаем данные об отеле
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)
except Exception as e:
raise RuntimeError(f"statistics.py [ERROR] Ошибка при генерации PDF-отчета: {e}") from e
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):
os.remove(file_path)
except Exception as e:
# Логируем стек вызовов для детального анализа
error_trace = traceback.format_exc()
logging.error(f"Ошибка в generate_statistics: {str(e)}", exc_info=True)
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)}")
def get_period_dates(period, now):
now = ensure_datetime(now)
if period == "day":
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)
@@ -178,6 +155,7 @@ def get_period_dates(period, now):
end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
return start_date, end_date
async def stats_back(update: Update, context):
"""Возврат к выбору отеля."""
query = update.callback_query
@@ -203,3 +181,6 @@ async def stats_back(update: Update, context):
keyboard.append([InlineKeyboardButton("🏠 Главная", callback_data="main_menu")])
reply_markup = InlineKeyboardMarkup(keyboard)
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):
"""Преобразует строку или 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):
value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
if isinstance(value, datetime) and is_naive(value):
value = make_aware(value)
return value
try:
return make_aware(datetime.strptime(value, '%Y-%m-%d %H:%M:%S'))
except ValueError:
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
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 = {
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,
"pms": getattr(res.hotel, 'pms', 'N/A'),
"reservation_id": res.reservation_id,
@@ -42,8 +69,6 @@ def get_reservation_data(res):
"check_out": check_out,
"status": res.status,
}
# print(f"[DEBUG] Reservation data: {result}")
return result
@@ -61,27 +86,21 @@ class CustomPDF(FPDF):
self.end_date = end_date
def header(self):
"""Добавление заголовка и заголовков таблицы на каждой странице."""
# Заголовок отчёта
if self.page == 1: # Заголовок отчёта только на первой странице
if self.page == 1:
self.set_font("DejaVuSans-Bold", size=14)
self.cell(0, 10, f"Отчет о бронированиях отеля {self.hotel_name}", ln=1, align="C")
self.ln(5)
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.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):
"""Добавление колонтитула внизу страницы."""
self.set_y(-15)
@@ -97,13 +116,68 @@ class CustomPDF(FPDF):
text = text[:-1]
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):
# Создание экземпляра 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.alias_nb_pages()
pdf.add_page() # Заголовок отчёта и таблица будут добавлены через методы header и footer
pdf.add_page()
# Таблица
pdf.set_font("DejaVuSans", size=8)
col_widths = [30, 30, 30, 60, 35, 35, 30]
row_height = 10
@@ -112,28 +186,35 @@ async def generate_pdf_report(hotel_name, reservations, start_date, end_date):
try:
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 = [
res_data["hotel_name"],
str(res_data["hotel_name"]),
str(res_data["reservation_id"]),
res_data["room_number"],
res_data["room_type"],
str(res_data["room_number"]),
str(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"],
str(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}")
print(f"[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')
print("[DEBUG] PDF metadata:", vars(pdf))
pdf_output_path = os.path.join(REPORTS_DIR, f"{hotel_name_safe}_report_{start_date_str}-{end_date_str}.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")
pdf.output(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("Загрузка плагинов:")
for file in os.listdir(PluginLoader.PLUGIN_PATH):
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]}"
try:
module = importlib.import_module(module_name)
@@ -26,7 +26,7 @@ class PluginLoader:
cls = getattr(module, attr)
if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin:
plugin_name = file[:-7] # Убираем `_pms` из имени файла
print(f" Загружен плагин {plugin_name}: {cls.__name__}")
# print(f" Загружен плагин {plugin_name}: {cls.__name__}")
plugins[plugin_name] = cls
except Exception as 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:
self.logger.error(f"Ошибка запроса: {e}")
return []
self.logger.debug(f"\n\n\n\n\ndata: {data}\n\n\n\n\n")
# Фильтрация данных
filtered_data = [
{
@@ -87,7 +87,6 @@ class EcviPMSPlugin(BasePMSPlugin):
} for item in data if isinstance(item, dict) and item.get('occupancy') in ['проживание', 'под выезд', 'под заезд']
]
self.logger.debug(f"filtered_data: {filtered_data}")
# Сохранение данных в базу данных
@@ -108,7 +107,6 @@ class EcviPMSPlugin(BasePMSPlugin):
# Проверяем, существует ли уже резервация с таким внешним ID
reservation_id = item.get('reservation_id')
self.logger.debug(f"-----\n\n\nITEM : {item}\n\n\n---")
if not reservation_id:
self.logger.error("Ошибка: 'reservation_id' отсутствует в данных.")
return
@@ -123,6 +121,7 @@ class EcviPMSPlugin(BasePMSPlugin):
await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=reservation_id,
defaults={
''
'room_number': item.get('room_number'),
'room_type': item.get('room_type'),
'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 hashlib
import json
from .base_plugin import BasePMSPlugin
from datetime import datetime, timedelta
from asgiref.sync import sync_to_async
from touchh.utils.log import CustomLogger
from hotels.models import Hotel, Reservation
from app_settings.models import GlobalHotelSettings
from django.utils import timezone
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("/")
self.logger = CustomLogger(name="RealtyCalendarPlugin", log_level="DEBUG").get_logger()
if not self.public_key or not self.private_key:
raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar")
@@ -150,8 +31,8 @@ class RealtyCalendarPlugin(BasePMSPlugin):
"""
Возвращает отсортированный по имени список ключей.
"""
sorted_keys = sorted(list(obj.keys()))
print(f"[DEBUG] Отсортированные ключи: {sorted_keys}")
sorted_keys = sorted(obj.keys())
self.logger.debug(f"Отсортированные ключи: {sorted_keys}")
return sorted_keys
def _generate_data_string(self, obj):
@@ -160,7 +41,7 @@ class RealtyCalendarPlugin(BasePMSPlugin):
"""
sorted_keys = self._get_sorted_keys(obj)
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
def _generate_md5(self, string):
@@ -168,7 +49,7 @@ class RealtyCalendarPlugin(BasePMSPlugin):
Генерирует MD5-хеш от строки.
"""
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
def _generate_sign(self, data):
@@ -176,79 +57,133 @@ class RealtyCalendarPlugin(BasePMSPlugin):
Генерирует подпись для данных запроса.
"""
data_string = self._generate_data_string(data)
print(f"[DEBUG] Строка для подписи: {data_string}")
self.logger.debug(f"Строка для подписи: {data_string}")
sign = self._generate_md5(data_string)
print(f"[DEBUG] Подпись: {sign}")
self.logger.debug(f"Подпись: {sign}")
return sign
def _fetch_data(self):
async def _fetch_data(self):
"""
Выполняет запрос к 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 = {
"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"),
}
print(f"[DEBUG] Даты выборки: {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)
self.logger.debug(f"Статус ответа: {response.status_code}")
# Логируем результат
print(f"[DEBUG] Статус ответа: {response.status_code}")
print(f"[DEBUG] Ответ: {response.text}")
if response.status_code != 200:
self.logger.error(f"Ошибка API: {response.status_code}, {response.text}")
raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}")
# Проверяем успешность запроса
if response.status_code == 200:
bookings = response.json().get("bookings", [])
print(f"[DEBUG] Полученные данные бронирований: {bookings}")
return bookings
else:
raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}, {response.text}")
async def _save_to_db(self, data, hotel_id):
"""
Сохраняет данные о бронированиях в базу данных.
"""
from hotels.models import Reservation, Hotel
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
print(f"[DEBUG] Загружен отель: {hotel.name}")
for item in data:
print(f"[DEBUG] Обработка бронирования: {item}")
try:
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=item["id"],
hotel=hotel,
defaults={
"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"[DEBUG] {'Создана' if created else 'Обновлена'} запись: {reservation}")
response_data = response.json()
bookings = response_data.get("bookings", [])
if not isinstance(bookings, list):
raise ValueError(f"Ожидался список, но получен {type(bookings)}")
except Exception as e:
print(f"[DEBUG] Ошибка при сохранении бронирования ID {item['id']}: {e}")
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"
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):
"""
Сохраняет данные в БД (например, информацию о номере).
"""
if not isinstance(data, list):
self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}")
return
for index, item in enumerate(data, start=1):
try:
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
reservation_id = item.get('reservation_id')
if not reservation_id:
self.logger.error(f"Пропущена запись {index}: отсутствует 'id'")
continue
existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id)
existing_reservation = await sync_to_async(existing_reservation.first)()
defaults = {
'room_number': item['room_number'],
'room_type': item['room_type'],
'check_in': item['checkin'],
'check_out': item['checkout'],
'status': item['status'],
'hotel': hotel
}
if existing_reservation:
await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=reservation_id, defaults=defaults
)
self.logger.debug(f"Резервация {reservation_id} обновлена.")
else:
await sync_to_async(Reservation.objects.create)(
reservation_id=reservation_id, **defaults
)
self.logger.debug(f"Создана новая резервация {reservation_id}")
except Exception as e:
self.logger.error(f"Ошибка при обработке записи {index}: {e}")
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 import forms
from django.utils.functional import cached_property
from .models import ScheduledTask
from django.templatetags.static import static
from scheduler.utils import get_project_functions
class CustomAdmin(admin.ModelAdmin):
class Media:
css = {"all": (static("scheduler/admin.css"),)}
js = (static("scheduler/admin.js"),)
class ScheduledTaskForm(forms.ModelForm):
"""
Форма для модели ScheduledTask с кастомным полем для выбора дней недели.
"""
DAYS_OF_WEEK_CHOICES = [
(0, "Воскресенье"),
(1, "Понедельник"),
@@ -25,7 +23,7 @@ class ScheduledTaskForm(forms.ModelForm):
choices=DAYS_OF_WEEK_CHOICES,
widget=forms.CheckboxSelectMultiple,
label="Дни недели",
required=False, # Опционально
required=False,
)
class Meta:
@@ -36,24 +34,32 @@ class ScheduledTaskForm(forms.ModelForm):
"minutes",
"hours",
"months",
"weekdays", # Используем только поле с галочками
"weekdays",
"active",
]
def clean_weekdays(self):
"""
Преобразуем список выбранных дней в строку для хранения в базе.
Преобразует список выбранных дней в строку для сохранения в базе.
"""
weekdays = self.cleaned_data.get("weekdays", [])
return ",".join(map(str, weekdays))
@admin.register(ScheduledTask)
class ScheduledTaskAdmin(admin.ModelAdmin):
"""
Кастомный класс для управления ScheduledTask в админке.
"""
form = ScheduledTaskForm
list_display = ("task_name", "function_path", "minutes", "hours", "months", "weekdays", "active", "formatted_last_run")
list_filter = ("active",)
search_fields = ("task_name", "function_path")
def formatted_last_run(self, obj):
"""
Отформатированный вывод времени последнего запуска задачи.
"""
return obj.last_run.strftime("%Y-%m-%d %H:%M:%S") if obj.last_run else "Никогда"
formatted_last_run.short_description = "Последний запуск"

View File

@@ -1,7 +1,15 @@
from django.apps import AppConfig
from apscheduler.schedulers.asyncio import AsyncIOScheduler
scheduler_instance = AsyncIOScheduler()
class SchedulerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
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
from apscheduler.triggers.cron import CronTrigger
def format_weekdays(weekdays):
"""Преобразует список дней недели в строку."""
if isinstance(weekdays, list):
@@ -43,7 +42,10 @@ def setup_scheduler():
print("Планировщик запущен.")
return scheduler
def load_tasks_to_scheduler(scheduler: BaseScheduler):
def load_tasks_to_scheduler(scheduler):
"""
Загружает активные задачи в планировщик.
"""
tasks = ScheduledTask.objects.filter(active=True)
for task in tasks:
try:

View File

@@ -16,6 +16,9 @@ import os
from dotenv import 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'.
@@ -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!
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 = [
'http://a66a-182-226-158-253.ngrok-free.app',
'https://8f6e-182-226-158-253.ngrok-free.app',
'https://*.ngrok-free.app', # Это подойдет для любых URL, связанных с ngrok
]
@@ -103,9 +106,11 @@ DATABASES = {
'PASSWORD': os.getenv('DB_PASSWORD'), # Пароль пользователя
'HOST': os.getenv('DB_HOST', default='0.0.0.0'), # Хост (по умолчанию localhost)
'PORT': os.getenv('DB_PORT', default=3308), # Порт (по умолчанию 3306)
'ATOMIC_REQUESTS': True,
},
}
# load_database_settings(DATABASES)
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@@ -197,6 +202,28 @@ JAZZMIN_SETTINGS = {
"welcome_sign": "Welcome to Touchh Admin", # Приветствие на странице входа
"copyright": "Touchh", # Кастомный текст в футере
"icons": {
# Приложения
"hotels": "fas fa-hotel",
"scheduler": "fas fa-clock",
"pms_integration": "fas fa-sync",
"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",
@@ -205,12 +232,24 @@ JAZZMIN_SETTINGS = {
"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",
"dark_mode_theme": "darkly",
"footer": {
@@ -223,10 +262,10 @@ JAZZMIN_SETTINGS = {
],
"show_ui_builder": True,
"show_ui_builder_breadcrumbs": True,
"show_ui_builder_tabs": True,
"show_ui_builder_tabs_breadcrumbs": True,
"show_ui_builder": False,
"show_ui_builder_breadcrumbs": False,
"show_ui_builder_tabs": False,
"show_ui_builder_tabs_breadcrumbs": False,
}

View File

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

View File

@@ -21,7 +21,10 @@ class CustomLogger:
# Уровень логирования по умолчанию из .env
default_level = os.getenv("LOG_LEVEL", "INFO").upper()
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()

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