Merge pull request 'plugins' (#2) from plugins into master

Reviewed-on: trevor/touchh_bot#2
This commit is contained in:
2024-12-13 23:09:40 +00:00
52 changed files with 12228 additions and 582 deletions

2
.gitignore vendored
View File

@@ -10,5 +10,7 @@ node_modules
package-lock.json
package.json
old_bot
*.mmdb
*.log
# Ignore files

Binary file not shown.

View File

@@ -1,149 +0,0 @@
BEGIN TRANSACTION;
INSERT INTO "django_migrations" ("id","app","name","applied") VALUES (1,'contenttypes','0001_initial','2024-12-09 09:30:10.251024'),
(2,'auth','0001_initial','2024-12-09 09:30:10.285571'),
(3,'admin','0001_initial','2024-12-09 09:30:10.304106'),
(4,'admin','0002_logentry_remove_auto_add','2024-12-09 09:30:10.327893'),
(5,'admin','0003_logentry_add_action_flag_choices','2024-12-09 09:30:10.347643'),
(6,'contenttypes','0002_remove_content_type_name','2024-12-09 09:30:10.391061'),
(7,'auth','0002_alter_permission_name_max_length','2024-12-09 09:30:10.411017'),
(8,'auth','0003_alter_user_email_max_length','2024-12-09 09:30:10.433375'),
(9,'auth','0004_alter_user_username_opts','2024-12-09 09:30:10.456000'),
(10,'auth','0005_alter_user_last_login_null','2024-12-09 09:30:10.487091'),
(11,'auth','0006_require_contenttypes_0002','2024-12-09 09:30:10.490205'),
(12,'auth','0007_alter_validators_add_error_messages','2024-12-09 09:30:10.505490'),
(13,'auth','0008_alter_user_username_max_length','2024-12-09 09:30:10.531170'),
(14,'auth','0009_alter_user_last_name_max_length','2024-12-09 09:30:10.554633'),
(15,'auth','0010_alter_group_name_max_length','2024-12-09 09:30:10.572891'),
(16,'auth','0011_update_proxy_permissions','2024-12-09 09:30:10.595627'),
(17,'auth','0012_alter_user_first_name_max_length','2024-12-09 09:30:10.626279'),
(18,'users','0001_initial','2024-12-09 09:30:10.697462'),
(19,'hotels','0001_initial','2024-12-09 09:30:10.728173'),
(20,'pms_integration','0001_initial','2024-12-09 09:30:10.753819'),
(21,'hotels','0002_initial','2024-12-09 09:30:10.883078'),
(22,'hotels','0003_initial','2024-12-09 09:30:11.011514'),
(23,'sessions','0001_initial','2024-12-09 09:30:11.033336');
INSERT INTO "django_admin_log" ("id","object_id","object_repr","action_flag","change_message","content_type_id","user_id","action_time") VALUES (1,'1','Shelter Golden Hills 3',1,'[{"added": {}}]',7,1,'2024-12-09 09:32:31.355706'),
(2,'2','Shelter Golden Hills 4',1,'[{"added": {}}]',7,1,'2024-12-09 09:33:10.530095'),
(3,'1','Golden Hills 3',1,'[{"added": {}}]',15,1,'2024-12-09 09:34:11.465732'),
(4,'2','Golden Hills 4',1,'[{"added": {}}]',15,1,'2024-12-09 09:34:21.783766'),
(5,'1','andrew',1,'[{"added": {}}]',18,1,'2024-12-09 09:35:20.367524'),
(6,'1','Настройки уведомлений для andrew',1,'[{"added": {}}]',19,1,'2024-12-09 09:35:40.518128'),
(7,'1','andrew - Golden Hills 3',1,'[{"added": {}}]',13,1,'2024-12-09 09:35:57.888800'),
(8,'2','andrew - Golden Hills 4',1,'[{"added": {}}]',13,1,'2024-12-09 09:36:06.799616'),
(9,'3','Как дома',1,'[{"added": {}}]',15,1,'2024-12-09 10:19:47.100883');
INSERT INTO "django_content_type" ("id","app_label","model") VALUES (1,'admin','logentry'),
(2,'auth','permission'),
(3,'auth','group'),
(4,'auth','user'),
(5,'contenttypes','contenttype'),
(6,'sessions','session'),
(7,'pms_integration','pmsconfiguration'),
(8,'pms_integration','pmsintegrationlog'),
(9,'hotels','apiconfiguration'),
(10,'hotels','fraudlog'),
(11,'hotels','guest'),
(12,'hotels','reservation'),
(13,'hotels','userhotel'),
(14,'hotels','apirequestlog'),
(15,'hotels','hotel'),
(16,'users','localuseractivitylog'),
(17,'users','useractivitylog'),
(18,'users','user'),
(19,'users','notificationsettings'),
(20,'users','userconfirmation');
INSERT INTO "auth_permission" ("id","content_type_id","codename","name") VALUES (1,1,'add_logentry','Can add log entry'),
(2,1,'change_logentry','Can change log entry'),
(3,1,'delete_logentry','Can delete log entry'),
(4,1,'view_logentry','Can view log entry'),
(5,2,'add_permission','Can add permission'),
(6,2,'change_permission','Can change permission'),
(7,2,'delete_permission','Can delete permission'),
(8,2,'view_permission','Can view permission'),
(9,3,'add_group','Can add group'),
(10,3,'change_group','Can change group'),
(11,3,'delete_group','Can delete group'),
(12,3,'view_group','Can view group'),
(13,4,'add_user','Can add user'),
(14,4,'change_user','Can change user'),
(15,4,'delete_user','Can delete user'),
(16,4,'view_user','Can view user'),
(17,5,'add_contenttype','Can add content type'),
(18,5,'change_contenttype','Can change content type'),
(19,5,'delete_contenttype','Can delete content type'),
(20,5,'view_contenttype','Can view content type'),
(21,6,'add_session','Can add session'),
(22,6,'change_session','Can change session'),
(23,6,'delete_session','Can delete session'),
(24,6,'view_session','Can view session'),
(25,7,'add_pmsconfiguration','Can add PMS система'),
(26,7,'change_pmsconfiguration','Can change PMS система'),
(27,7,'delete_pmsconfiguration','Can delete PMS система'),
(28,7,'view_pmsconfiguration','Can view PMS система'),
(29,8,'add_pmsintegrationlog','Can add Журнал интеграции PMS'),
(30,8,'change_pmsintegrationlog','Can change Журнал интеграции PMS'),
(31,8,'delete_pmsintegrationlog','Can delete Журнал интеграции PMS'),
(32,8,'view_pmsintegrationlog','Can view Журнал интеграции PMS'),
(33,9,'add_apiconfiguration','Can add Конфигурация API'),
(34,9,'change_apiconfiguration','Can change Конфигурация API'),
(35,9,'delete_apiconfiguration','Can delete Конфигурация API'),
(36,9,'view_apiconfiguration','Can view Конфигурация API'),
(37,10,'add_fraudlog','Can add Журнал мошенничества'),
(38,10,'change_fraudlog','Can change Журнал мошенничества'),
(39,10,'delete_fraudlog','Can delete Журнал мошенничества'),
(40,10,'view_fraudlog','Can view Журнал мошенничества'),
(41,11,'add_guest','Can add Гость'),
(42,11,'change_guest','Can change Гость'),
(43,11,'delete_guest','Can delete Гость'),
(44,11,'view_guest','Can view Гость'),
(45,12,'add_reservation','Can add Бронирование'),
(46,12,'change_reservation','Can change Бронирование'),
(47,12,'delete_reservation','Can delete Бронирование'),
(48,12,'view_reservation','Can view Бронирование'),
(49,13,'add_userhotel','Can add Пользователь отеля'),
(50,13,'change_userhotel','Can change Пользователь отеля'),
(51,13,'delete_userhotel','Can delete Пользователь отеля'),
(52,13,'view_userhotel','Can view Пользователь отеля'),
(53,14,'add_apirequestlog','Can add Журнал запросов API'),
(54,14,'change_apirequestlog','Can change Журнал запросов API'),
(55,14,'delete_apirequestlog','Can delete Журнал запросов API'),
(56,14,'view_apirequestlog','Can view Журнал запросов API'),
(57,15,'add_hotel','Can add Отель'),
(58,15,'change_hotel','Can change Отель'),
(59,15,'delete_hotel','Can delete Отель'),
(60,15,'view_hotel','Can view Отель'),
(61,16,'add_localuseractivitylog','Can add local user activity log'),
(62,16,'change_localuseractivitylog','Can change local user activity log'),
(63,16,'delete_localuseractivitylog','Can delete local user activity log'),
(64,16,'view_localuseractivitylog','Can view local user activity log'),
(65,17,'add_useractivitylog','Can add Журнал активности'),
(66,17,'change_useractivitylog','Can change Журнал активности'),
(67,17,'delete_useractivitylog','Can delete Журнал активности'),
(68,17,'view_useractivitylog','Can view Журнал активности'),
(69,18,'add_user','Can add Пользователь'),
(70,18,'change_user','Can change Пользователь'),
(71,18,'delete_user','Can delete Пользователь'),
(72,18,'view_user','Can view Пользователь'),
(73,19,'add_notificationsettings','Can add Способ оповещения'),
(74,19,'change_notificationsettings','Can change Способ оповещения'),
(75,19,'delete_notificationsettings','Can delete Способ оповещения'),
(76,19,'view_notificationsettings','Can view Способ оповещения'),
(77,20,'add_userconfirmation','Can add Подтверждение пользователя'),
(78,20,'change_userconfirmation','Can change Подтверждение пользователя'),
(79,20,'delete_userconfirmation','Can delete Подтверждение пользователя'),
(80,20,'view_userconfirmation','Can view Подтверждение пользователя');
INSERT INTO "auth_user" ("id","password","last_login","is_superuser","username","last_name","email","is_staff","is_active","date_joined","first_name") VALUES (1,'pbkdf2_sha256$870000$0tWRKvUavKHjKmwWjWsfYc$mfqBdzr5TB74K1f9OHCI3w/66VZE7vY53MEpgUT73/4=','2024-12-09 09:31:26.675259',1,'trevor1985','','shadow85@list.ru',1,1,'2024-12-09 09:30:44.551380','');
INSERT INTO "users_user" ("id","password","last_login","is_superuser","username","first_name","last_name","email","is_staff","is_active","date_joined","telegram_id","chat_id","role","confirmed") VALUES (1,'Andrey K. Tsoy','2024-12-09 09:34:41',1,'andrew','','','',0,1,'2024-12-09 09:34:27',556399210,556399210,'hotel_user',0);
INSERT INTO "users_notificationsettings" ("id","telegram_enabled","email_enabled","email","notification_time","user_id") VALUES (1,0,1,'a.choi@smartsoltech.kr','09:00:00',1);
INSERT INTO "hotels_hotel" ("id","name","created_at","api_id","pms_id") VALUES (1,'Golden Hills 3','2024-12-09 09:34:11.463934',NULL,1),
(2,'Golden Hills 4','2024-12-09 09:34:21.782571',NULL,2),
(3,'Как дома','2024-12-09 10:19:47.095457',NULL,NULL);
INSERT INTO "pms_integration_pmsconfiguration" ("id","name","url","token","username","password","plugin_name","created_at") VALUES (1,'Shelter Golden Hills 3','https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter','A0DD4B7F-0381-4096-B46E-D22F1A571996',NULL,NULL,'Shelter PMS','2024-12-09 09:32:31.348604'),
(2,'Shelter Golden Hills 4','https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter','A0DD4B7F-0381-4096-B46E-D22F1A571996',NULL,NULL,'Shelter PMS','2024-12-09 09:33:10.514492');
INSERT INTO "pms_integration_pmsintegrationlog" ("id","checked_at","status","message","hotel_id") VALUES (1,'2024-12-09 09:36:43.044421','error','Плагин для PMS Shelter PMS не найден.',2),
(2,'2024-12-09 09:38:09.595349','error','Плагин для PMS Shelter PMS не найден.',1),
(3,'2024-12-09 09:40:38.721678','error','Плагин для PMS Shelter PMS не найден.',2),
(4,'2024-12-09 10:07:44.968856','error','Плагин для PMS Shelter PMS не найден.',1);
INSERT INTO "hotels_userhotel" ("id","hotel_id","user_id") VALUES (1,1,1),
(2,2,1);
INSERT INTO "django_session" ("session_key","session_data","expire_date") VALUES ('wjcybubh02eoyth1e4pm9cgjc8c9rk1v','.eJxVjEEOwiAQRe_C2hBgpgVcuvcMZIBBqoYmpV0Z765NutDtf-_9lwi0rTVsnZcwZXEWWpx-t0jpwW0H-U7tNss0t3WZotwVedAur3Pm5-Vw_w4q9fqthxwtITBY9MlGtJ6ADZiiAdyQ7KhVyVgcGY7gEL1BVqMhH51iNl68P9F6N0w:1tKa6w:zJT5PgcESbBBG5gKuJsyfV6EOxizdevxDzI4QrGbLsc','2024-12-23 09:31:26.682056');
COMMIT;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 MiB

0
antifroud/__init__.py Normal file
View File

253
antifroud/admin.py Normal file
View File

@@ -0,0 +1,253 @@
from django.contrib import admin
from django.urls import path
from django.http import JsonResponse
from django.shortcuts import redirect, get_object_or_404
from django.contrib import messages
from django.db import transaction
from antifroud.models import UserActivityLog, ExternalDBSettings, RoomDiscrepancy, ImportedHotel
from hotels.models import Hotel
import pymysql
import logging
from django.urls import reverse
logger = logging.getLogger(__name__)
@admin.register(ExternalDBSettings)
class ExternalDBSettingsAdmin(admin.ModelAdmin):
change_form_template = "antifroud/admin/external_db_settings_change_form.html"
list_display = ("name", "host", "port", "user", "database", "table_name", "is_active", "created_at", "updated_at")
search_fields = ("name", "host", "user", "database")
list_filter = ("is_active", "created_at", "updated_at")
readonly_fields = ("created_at", "updated_at")
def add_view(self, request, form_url='', extra_context=None):
new_instance = ExternalDBSettings.objects.create(
name="Новая настройка", # Значение по умолчанию
host="",
port=3306,
user="",
password="",
is_active=False
)
return redirect(reverse('admin:antifroud_externaldbsettings_change', args=(new_instance.id,)))
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('test-connection/', self.admin_site.admin_view(self.test_connection), name='test_connection'),
path('fetch-tables/', self.admin_site.admin_view(self.fetch_tables), name='fetch_tables'),
path('fetch-table-data/', self.admin_site.admin_view(self.fetch_table_data), name='fetch_table_data'),
]
return custom_urls + urls
def test_connection(self, request):
db_id = request.GET.get('db_id')
if not db_id:
return JsonResponse({"status": "error", "message": "ID подключения отсутствует."}, status=400)
try:
db_settings = ExternalDBSettings.objects.get(id=db_id)
if not db_settings.user or not db_settings.password:
return JsonResponse({"status": "error", "message": "Имя пользователя или пароль не указаны."}, status=400)
connection = pymysql.connect(
host=db_settings.host,
port=db_settings.port,
user=db_settings.user,
password=db_settings.password,
database=db_settings.database
)
connection.close()
return JsonResponse({"status": "success", "message": "Подключение успешно установлено."})
except ExternalDBSettings.DoesNotExist:
return JsonResponse({"status": "error", "message": "Настройки подключения не найдены."}, status=404)
except pymysql.MySQLError as e:
return JsonResponse({"status": "error", "message": f"Ошибка MySQL: {str(e)}"}, status=500)
except Exception as e:
return JsonResponse({"status": "error", "message": f"Неизвестная ошибка: {str(e)}"}, status=500)
def fetch_tables(self, request):
try:
db_id = request.GET.get('db_id')
db_settings = ExternalDBSettings.objects.get(id=db_id)
connection = pymysql.connect(
host=db_settings.host,
port=db_settings.port,
user=db_settings.user,
password=db_settings.password,
database=db_settings.database
)
cursor = connection.cursor()
cursor.execute("SHOW TABLES;")
tables = [row[0] for row in cursor.fetchall()]
connection.close()
return JsonResponse({"status": "success", "tables": tables})
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)})
def fetch_table_data(self, request):
try:
db_id = request.GET.get('db_id')
table_name = request.GET.get('table_name')
db_settings = ExternalDBSettings.objects.get(id=db_id)
connection = pymysql.connect(
host=db_settings.host,
port=db_settings.port,
user=db_settings.user,
password=db_settings.password,
database=db_settings.database
)
cursor = connection.cursor()
cursor.execute(f"SELECT * FROM `{table_name}` LIMIT 10;")
columns = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
connection.close()
return JsonResponse({"status": "success", "columns": columns, "rows": rows})
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)})
@admin.register(UserActivityLog)
class UserActivityLogAdmin(admin.ModelAdmin):
list_display = ("id", "timestamp", "date_time", "page_id", "url_parameters", "created", "page_title", "type", "hits")
search_fields = ("page_title", "url_parameters")
list_filter = ("type", "created")
readonly_fields = ("created", "timestamp")
@admin.register(RoomDiscrepancy)
class RoomDiscrepancyAdmin(admin.ModelAdmin):
list_display = ("hotel", "room_number", "booking_id", "check_in_date_expected", "check_in_date_actual", "discrepancy_type", "created_at")
search_fields = ("hotel__name", "room_number", "booking_id")
list_filter = ("discrepancy_type", "created_at")
readonly_fields = ("created_at",)
# @admin.register(ImportedHotel)
# class ImportedHotelAdmin(admin.ModelAdmin):
# change_list_template = "antifroud/admin/imported_hotels.html"
# list_display = ("external_id", "display_name", "name", "created", "updated", "imported")
# search_fields = ("name", "display_name", "external_id")
# list_filter = ("imported", "created", "updated")
# actions = ['mark_as_imported', 'delete_selected_hotels_action']
# def get_urls(self):
# urls = super().get_urls()
# custom_urls = [
# path('import_selected_hotels/', self.import_selected_hotels, name='antifroud_importedhotels_import_selected_hotels'),
# path('delete_selected_hotels/', self.delete_selected_hotels, name='delete_selected_hotels'),
# path('edit_hotel/', self.edit_hotel, name='edit_hotel'),
# path('delete_hotel/', self.delete_hotel, name='delete_hotel'),
# ]
# return custom_urls + urls
# @transaction.atomic
# def import_selected_hotels(self, request): # Метод теперь правильно принимает request
# if request.method == 'POST':
# selected_hotels = request.POST.getlist('hotels')
# if selected_hotels:
# # Обновление статуса импорта для выбранных отелей
# ImportedHotel.objects.filter(id__in=selected_hotels).update(imported=True)
# return JsonResponse({'success': True})
# else:
# return JsonResponse({'success': False})
# return JsonResponse({'success': False})
# @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_hotel(self, request):
# if request.method == 'POST':
# hotel_id = request.POST.get('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')
# def delete_selected_hotels_action(self, request, queryset):
# deleted_count, _ = queryset.delete()
# self.message_user(request, f'{deleted_count} отелей было удалено.')
# delete_selected_hotels_action.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 edit_hotel(self, request):
# if request.method == 'POST':
# hotel_id = request.POST.get('hotel_id')
# display_name = request.POST.get('display_name')
# original_name = request.POST.get('original_name')
# imported = request.POST.get('imported') == 'True'
# imported_hotel = get_object_or_404(ImportedHotel, id=hotel_id)
# imported_hotel.display_name = display_name
# imported_hotel.name = original_name
# imported_hotel.imported = imported
# imported_hotel.save()
# messages.success(request, f"Отель {imported_hotel.name} успешно обновлён.")
# return redirect('admin:antifroud_importedhotel_changelist')
# return redirect('admin:antifroud_importedhotel_changelist')
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
@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 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')

6
antifroud/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AntifroudConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'antifroud'

243
antifroud/data_sync.py Normal file
View File

@@ -0,0 +1,243 @@
import pymysql
from django.db import transaction
from django.utils import timezone
from datetime import datetime
from html import unescape
from urllib.parse import unquote, parse_qs
from .models import ExternalDBSettings, UserActivityLog, RoomDiscrepancy, ImportedHotel
from hotels.models import Reservation, Hotel
class DataSyncManager:
"""
Класс для управления загрузкой, записью и сверкой данных.
"""
def __init__(self, db_settings_id):
self.db_settings_id = db_settings_id
self.db_settings = None
self.connection = None
self.table_name = None
def connect_to_db(self):
"""
Устанавливает соединение с внешней базой данных и получает имя таблицы.
"""
try:
self.db_settings = ExternalDBSettings.objects.get(id=self.db_settings_id)
self.table_name = self.db_settings.table_name
self.connection = pymysql.connect(
host=self.db_settings.host,
port=self.db_settings.port,
user=self.db_settings.user,
password=self.db_settings.password,
database=self.db_settings.database,
charset='utf8mb4',
use_unicode=True
)
except ExternalDBSettings.DoesNotExist:
raise ValueError("Настройки подключения не найдены.")
except pymysql.MySQLError as e:
raise ConnectionError(f"Ошибка подключения к базе данных: {e}")
def fetch_data(self, limit=100):
"""
Загружает данные из указанной таблицы.
"""
if not self.connection:
self.connect_to_db()
cursor = self.connection.cursor()
try:
cursor.execute(f"SELECT * FROM `{self.table_name}` LIMIT {limit};")
columns = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
return {"columns": columns, "rows": rows}
except pymysql.MySQLError as e:
raise RuntimeError(f"Ошибка выполнения запроса: {e}")
finally:
cursor.close()
def parse_datetime(self, dt_str):
"""
Преобразует строку формата 'YYYY-MM-DD HH:MM:SS' или 'YYYY-MM-DDTHH:MM:SS' в aware datetime.
"""
if dt_str is None:
return None
if isinstance(dt_str, datetime):
if timezone.is_naive(dt_str):
return timezone.make_aware(dt_str, timezone.get_default_timezone())
return dt_str
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
try:
naive_dt = datetime.strptime(dt_str, fmt)
return timezone.make_aware(naive_dt, timezone.get_default_timezone())
except ValueError:
continue
return None
def decode_html_entities(self, text):
"""
Раскодирует HTML-сущности в строке.
"""
if text and isinstance(text, str):
return unescape(text)
return text
def process_url_parameters(self, url_params):
"""
Парсит url_parameters, извлекает utm_content (имя отеля) и utm_term (ID отеля).
"""
if not url_params:
return {}
decoded = unquote(url_params)
qs = parse_qs(decoded)
hotel_name = qs.get('utm_content', [None])[0]
hotel_id = qs.get('utm_term', [None])[0]
return {
'hotel_name': hotel_name,
'hotel_id': hotel_id
}
def check_and_store_imported_hotel(self, hotel_name, hotel_id):
"""
Проверяет, есть ли отель с данным ID в основной БД.
Если hotel_id не число или отеля с таким ID нет, добавляет во временную таблицу ImportedHotel.
"""
if not hotel_id or not hotel_name:
return
# Проверим, что hotel_id — число
if hotel_id.isdigit():
hotel_id_int = int(hotel_id)
hotel_exists = Hotel.objects.filter(id=hotel_id_int).exists()
else:
# Если не число, считаем что отеля в основной БД нет
hotel_exists = False
if not hotel_exists:
ImportedHotel.objects.update_or_create(
external_id=str(hotel_id),
defaults={
'name': hotel_name
}
)
@transaction.atomic
def write_to_db(self, data):
"""
Записывает данные в UserActivityLog и при необходимости в ImportedHotel.
"""
for row in data["rows"]:
record = dict(zip(data["columns"], row))
external_id = record.get("id", None)
if external_id is not None:
external_id = str(external_id)
created = self.parse_datetime(record.get("created"))
date_time = self.parse_datetime(record.get("date_time"))
referred = self.decode_html_entities(record.get("referred", ""))
agent = self.decode_html_entities(record.get("agent", ""))
platform = self.decode_html_entities(record.get("platform", ""))
version = self.decode_html_entities(record.get("version", ""))
model = self.decode_html_entities(record.get("model", ""))
device = self.decode_html_entities(record.get("device", ""))
UAString = self.decode_html_entities(record.get("UAString", ""))
location = self.decode_html_entities(record.get("location", ""))
page_title = self.decode_html_entities(record.get("page_title", ""))
page_url = self.decode_html_entities(record.get("page_url", ""))
url_parameters = self.decode_html_entities(record.get("url_parameters", ""))
hotel_info = self.process_url_parameters(url_parameters)
if hotel_info.get('hotel_name') and hotel_info.get('hotel_id'):
self.check_and_store_imported_hotel(
hotel_name=hotel_info['hotel_name'],
hotel_id=hotel_info['hotel_id']
)
url_parameters = unquote(url_parameters)
UserActivityLog.objects.update_or_create(
external_id=external_id,
defaults={
"user_id": record.get("user_id"),
"ip": record.get("ip"),
"created": created,
"timestamp": record.get("timestamp"),
"date_time": date_time,
"referred": referred,
"agent": agent,
"platform": platform,
"version": version,
"model": model,
"device": device,
"UAString": UAString,
"location": location,
"page_id": record.get("page_id"),
"url_parameters": url_parameters,
"page_title": page_title,
"type": record.get("type"),
"last_counter": record.get("last_counter"),
"hits": record.get("hits"),
"honeypot": record.get("honeypot"),
"reply": record.get("reply"),
"page_url": page_url,
}
)
def reconcile_data(self):
"""
Сверяет данные таблицы user_activity_log с таблицей hotels.reservations
и записывает несоответствия в таблицу RoomDiscrepancy.
"""
discrepancies = []
reservations = Reservation.objects.values("hotel_id", "room_number", "check_in", "check_out")
for log in UserActivityLog.objects.all():
for reservation in reservations:
if (
log.page_id != reservation["room_number"] or
log.created.date() < reservation["check_in"] or
log.created.date() > reservation["check_out"]
):
discrepancies.append(RoomDiscrepancy(
hotel_id=reservation["hotel_id"],
room_number=log.page_id,
booking_id=f"Log-{log.id}",
check_in_date_expected=reservation["check_in"],
check_in_date_actual=log.created.date() if log.created else None,
discrepancy_type="Mismatch",
))
RoomDiscrepancy.objects.bulk_create(discrepancies)
def sync(self):
"""
Основной метод для загрузки, записи и сверки данных.
"""
try:
self.connect_to_db()
data = self.fetch_data()
self.write_to_db(data)
self.reconcile_data()
except Exception as e:
raise RuntimeError(f"Ошибка синхронизации данных: {e}")
finally:
if self.connection:
self.connection.close()
def scheduled_sync():
"""
Плановая задача для синхронизации данных.
"""
db_settings_list = ExternalDBSettings.objects.filter(is_active=True)
for db_settings in db_settings_list:
sync_manager = DataSyncManager(db_settings.id)
sync_manager.sync()

9
antifroud/forms.py Normal file
View File

@@ -0,0 +1,9 @@
from django import forms
from .models import Hotel
class HotelImportForm(forms.Form):
hotels = forms.ModelMultipleChoiceField(
queryset=Hotel.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=True
)

View File

@@ -0,0 +1,86 @@
# 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

@@ -0,0 +1,42 @@
# 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

@@ -0,0 +1,43 @@
# 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

@@ -0,0 +1,27 @@
# 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

@@ -0,0 +1,24 @@
# 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

@@ -0,0 +1,17 @@
# 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

@@ -0,0 +1,18 @@
# 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

@@ -0,0 +1,18 @@
# 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

@@ -0,0 +1,18 @@
# 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

153
antifroud/models.py Normal file
View File

@@ -0,0 +1,153 @@
from django.db import models
from hotels.models import Hotel, Reservation
class UserActivityLog(models.Model):
external_id = models.CharField(max_length=255, null=True, blank=True)
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(max_length=255, blank=True, null=True, verbose_name="Платформа")
version = models.CharField(max_length=255, blank=True, null=True, verbose_name="Версия")
model = models.CharField(max_length=255, blank=True, null=True, verbose_name="Модель устройства")
device = models.CharField(max_length=255, blank=True, null=True, verbose_name="Тип устройства")
UAString = models.TextField(verbose_name="User-Agent строка")
location = models.CharField(max_length=255, blank=True, 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 страницы")
def __str__(self):
return f"UserActivityLog {self.id}: {self.page_title}"
class Meta:
verbose_name = "Регистрация посетителей"
verbose_name_plural = "Регистрации посетителей"
class ExternalDBSettings(models.Model):
name = models.CharField(max_length=255, unique=True, help_text="Имя подключения для идентификации.")
host = models.CharField(max_length=255, help_text="Адрес сервера базы данных.")
port = models.PositiveIntegerField(default=3306, help_text="Порт сервера базы данных.")
user = models.CharField(max_length=255, help_text="Имя пользователя базы данных.")
password = models.CharField(max_length=255, help_text="Пароль для подключения.")
database = models.CharField(max_length=255, default="u1510415_wp832", help_text="Имя базы данных.")
table_name = models.CharField(max_length=255, blank=True, default="wpts_user_activity_log", null=True, help_text="Имя таблицы для загрузки данных.")
selected_fields = models.TextField(blank=True, null=True, help_text="Список полей для загрузки (через запятую).")
is_active = models.BooleanField(default=True, help_text="Флаг активности подключения.")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.name} ({self.host}:{self.port})"
class Meta:
verbose_name = "Настройка подключения к БД"
verbose_name_plural = "Настройки подключений к БД"
class RoomDiscrepancy(models.Model):
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
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(
max_length=50,
choices=[("early", "Раннее заселение"), ("late", "Позднее заселение"), ("missed", "Неявка")],
verbose_name="Тип несоответствия"
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
def __str__(self):
return f"{self.hotel.name} - Room {self.room_number}: {self.discrepancy_type}"
class Meta:
verbose_name = "Несовпадение в заселении"
verbose_name_plural = "Несовпадения в заселении"
@staticmethod
def detect_discrepancies(expected_bookings, actual_check_ins):
"""
Сравнение ожидаемых и фактических данных о заселении.
"""
discrepancies = []
# Преобразуем фактические заселения в словарь для быстрого доступа
actual_dict = {
(entry.hotel_id, entry.room_number): entry.check_in_date
for entry in actual_check_ins
}
for booking in expected_bookings:
key = (booking.hotel_id, booking.room_number)
actual_date = actual_dict.get(key)
if actual_date is None:
discrepancies.append(RoomDiscrepancy(
hotel=booking.hotel,
room_number=booking.room_number,
booking_id=booking.booking_id,
check_in_date_expected=booking.check_in_date,
discrepancy_type="missed"
))
elif actual_date < booking.check_in_date:
discrepancies.append(RoomDiscrepancy(
hotel=booking.hotel,
room_number=booking.room_number,
booking_id=booking.booking_id,
check_in_date_expected=booking.check_in_date,
check_in_date_actual=actual_date,
discrepancy_type="early"
))
elif actual_date > booking.check_in_date:
discrepancies.append(RoomDiscrepancy(
hotel=booking.hotel,
room_number=booking.room_number,
booking_id=booking.booking_id,
check_in_date_expected=booking.check_in_date,
check_in_date_actual=actual_date,
discrepancy_type="late"
))
RoomDiscrepancy.objects.bulk_create(discrepancies)
from urllib.parse import unquote
from html import unescape
class ImportedHotel(models.Model):
external_id = models.CharField(max_length=255, unique=True, verbose_name="Внешний ID отеля")
name = models.CharField(max_length=255, verbose_name="Имя отеля")
display_name = models.CharField(max_length=255, null=True, blank=True, 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="Импортирован в основную базу")
def __str__(self):
return f"{self.display_name or self.name} ({self.external_id})"
class Meta:
verbose_name = "Импортированный отель"
verbose_name_plural = "Импортированные отели"
def set_display_name_from_page_title(self, page_title):
"""
Декодирует HTML-сущности, URL-кодировку и устанавливает display_name.
"""
if page_title:
decoded = unquote(unescape(page_title))
self.display_name = decoded
else:
self.display_name = self.name
self.save()

View File

@@ -0,0 +1,177 @@
{% extends "admin/base_site.html" %}
{% block content %}
<style>
#table-data-preview {
max-height: 300px; /* Ограничиваем высоту предпросмотра */
overflow-y: auto; /* Прокрутка по вертикали */
overflow-x: auto; /* Прокрутка по горизонтали */
}
#table-data-preview table {
width: auto; /* Автоматическая ширина таблицы */
table-layout: auto; /* Автоматическая ширина колонок */
}
#table-data-preview th,
#table-data-preview td {
white-space: nowrap; /* Предотвращаем перенос текста */
overflow: hidden; /* Скрываем текст, выходящий за границы ячейки */
text-overflow: ellipsis; /* Добавляем многоточие для обрезанного текста */
padding: 8px; /* Внутренний отступ */
height: 40px; /* Фиксированная высота строк */
}
#table-data-preview th {
position: sticky; /* Фиксируем заголовки при прокрутке */
top: 0; /* Располагаем заголовки вверху таблицы */
background-color: #f8f9fa; /* Цвет фона заголовков */
z-index: 1; /* Заголовки перекрывают содержимое */
}
</style>
<div class="container mt-4">
<h2 class="text-center">Настройки подключения к БД</h2>
<form id="connection-form" method="post">
{% csrf_token %}
<div class="form-group mb-3">
<label for="db-name">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>
<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>
<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>
<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>
<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>
<input id="db-database" class="form-control" type="text" name="database" value="{{ original.database }}" required />
</div>
<div class="form-group mb-3">
<label for="table-selector">Таблицы</label>
<select id="table-selector" class="form-select" name="table_name">
{% if original.table_name %}
<option value="{{ original.table_name }}" selected>{{ original.table_name }}</option>
{% else %}
<option value="">-- Выберите таблицу --</option>
{% endif %}
</select>
</div>
<div class="form-group mb-3">
<label for="table-data-preview">Столбцы и данные</label>
<div id="table-data-preview" class="table-responsive">
<table class="table table-bordered">
<thead id="table-header"></thead>
<tbody id="table-body"></tbody>
</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 text-center">
<button class="btn btn-success" type="submit">Сохранить</button>
<button class="btn btn-secondary" type="button" id="close-button">Закрыть</button>
</div>
</form>
<hr>
<div id="connection-status" class="mt-4"></div>
<div class="text-center mt-3">
<button id="test-connection" class="btn btn-primary" type="button">Проверить подключение</button>
</div>
</div>
{% if original.id %}
<script>
const dbId = "{{ original.id }}";
</script>
{% else %}
<script>
const dbId = null;
document.getElementById("test-connection").style.display = "none";
alert("Сохраните запись перед выполнением проверки подключения.");
</script>
{% endif %}
<script>
// Закрыть окно
document.getElementById("close-button").addEventListener("click", function() {
window.history.back(); // Вернуться назад
});
// Проверить подключение и загрузить таблицы
document.getElementById("test-connection").addEventListener("click", function() {
if (!dbId) {
alert("ID подключения отсутствует.");
return;
}
fetch(`/admin/antifroud/externaldbsettings/test-connection/?db_id=${dbId}`)
.then(response => response.json())
.then(data => {
if (data.status === "success") {
document.getElementById("connection-status").innerHTML = `<div class="alert alert-success">${data.message}</div>`;
fetch(`/admin/antifroud/externaldbsettings/fetch-tables/?db_id=${dbId}`)
.then(response => response.json())
.then(tableData => {
if (tableData.status === "success") {
const selector = document.getElementById("table-selector");
selector.innerHTML = tableData.tables.map(table => `<option value="${table}">${table}</option>`).join("");
} else {
alert("Ошибка при загрузке таблиц: " + tableData.message);
}
});
} else {
document.getElementById("connection-status").innerHTML = `<div class="alert alert-danger">${data.message}</div>`;
}
})
.catch(error => {
alert("Ошибка при проверке подключения.");
console.error(error);
});
});
// При выборе таблицы загрузить столбцы и строки данных
document.getElementById("table-selector").addEventListener("change", function () {
const tableName = this.value;
if (!tableName) {
document.getElementById("table-header").innerHTML = "";
document.getElementById("table-body").innerHTML = "";
return;
}
fetch(`/admin/antifroud/externaldbsettings/fetch-table-data/?db_id=${dbId}&table_name=${tableName}`)
.then(response => response.json())
.then(data => {
if (data.status === "success") {
const headerRow = data.columns.map(col => `<th>${col}</th>`).join("");
document.getElementById("table-header").innerHTML = `<tr>${headerRow}</tr>`;
const rows = data.rows.map(row => {
const cells = row.map(cell => `<td>${cell}</td>`).join("");
return `<tr>${cells}</tr>`;
}).join("");
document.getElementById("table-body").innerHTML = rows;
} else {
alert("Ошибка при загрузке данных таблицы: " + data.message);
}
})
.catch(error => {
alert("Ошибка при загрузке данных таблицы.");
console.error(error);
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{% extends "admin/change_list.html" %}
{% block content %}
<!-- Кнопка импорта -->
<div class="form-group mb-3">
<button type="submit" class="btn btn-primary" form="importHotelsForm">
Импортировать выбранные отели
</button>
</div>
<!-- Действия админки -->
{{ block.super }}
<!-- Уведомление -->
<div id="notification" class="alert alert-info d-none" role="alert">
Здесь появятся уведомления.
</div>
<!-- Форма для импорта отелей -->
<form id="importHotelsForm" method="POST">
{% csrf_token %}
{{ form.as_p }} <!-- Отображаем форму -->
</form>
{% endblock %}
{% block extrahead %}
{{ block.super }}
<!-- Подключаем Bootstrap 4 -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const importButton = document.querySelector("button[type='submit']");
const notificationElement = document.getElementById('notification');
// Слушатель для отправки формы
importButton.addEventListener('click', function(e) {
e.preventDefault(); // предотвращаем стандартное поведение кнопки
// Извлекаем выбранные отели
const checkboxes = document.querySelectorAll('input[name="hotels"]:checked');
const selectedHotels = [];
console.log("Чекбоксы:", checkboxes); // Консольная отладка
checkboxes.forEach(function(checkbox) {
selectedHotels.push(checkbox.value);
console.log("Выбранный отель:", checkbox.value); // Консольная отладка
});
// Если выбраны отели
if (selectedHotels.length > 0) {
// Преобразуем CSRF токен
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
console.log("CSRF токен:", csrfToken); // Консольная отладка
// Отправляем данные на сервер
fetch('/import_hotels/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify({ hotels: selectedHotels })
})
.then(response => response.json())
.then(data => {
console.log("Ответ от сервера:", data); // Консольная отладка
// Показать успешное уведомление
notificationElement.classList.remove('d-none');
notificationElement.classList.add('alert-success');
notificationElement.textContent = data.message || "Отели успешно импортированы!";
})
.catch((error) => {
console.error("Ошибка при импорте:", error); // Консольная отладка
// Показать ошибку
notificationElement.classList.remove('d-none');
notificationElement.classList.add('alert-danger');
notificationElement.textContent = "Произошла ошибка при импорте отелей.";
})
.finally(() => {
// Сброс кнопки
importButton.disabled = false;
importButton.textContent = 'Импортировать выбранные отели';
});
} else {
console.log("Не выбраны отели"); // Консольная отладка
notificationElement.classList.remove('d-none');
notificationElement.classList.add('alert-warning');
notificationElement.textContent = "Пожалуйста, выберите хотя бы один отель для импорта.";
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "admin/base_site.html" %}
{% load i18n static %}
{% block title %}Редактирование отеля{% endblock %}
{% block content %}
<div class="container-fluid">
<h3 class="text-dark mb-4">Редактирование отеля</h3>
<form method="post" action="{% url 'admin:save_edited_hotel' hotel.id %}">
{% csrf_token %}
<div class="form-group mb-3">
<label for="display_name" class="form-label">Отображаемое имя</label>
<input class="form-control" type="text" id="display_name" name="display_name" value="{{ hotel.display_name }}" required>
</div>
<div class="form-group mb-3">
<label for="original_name" class="form-label">Оригинальное имя</label>
<input class="form-control" type="text" id="original_name" name="original_name" value="{{ hotel.name }}" required>
</div>
<div class="form-group mb-3">
<label for="imported" class="form-label">Импортирован</label>
<select class="form-select" id="imported" name="imported">
<option value="True" {% if hotel.imported %} selected {% endif %}>Да</option>
<option value="False" {% if not hotel.imported %} selected {% endif %}>Нет</option>
</select>
</div>
<div class="form-group mb-3">
<button type="submit" class="btn btn-primary">Сохранить изменения</button>
<a href="{% url 'admin:hotel_list' %}" class="btn btn-secondary">Назад</a>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,143 @@
{% extends "admin/change_list.html" %}
{% block content %}
<!-- Кнопка импорта -->
<div class="form-group mb-3">
<button type="button" class="btn btn-primary" id="importHotelsButton">
Импортировать выбранные отели
</button>
</div>
<!-- Уведомление -->
<div id="notification" class="alert alert-info d-none" role="alert">
Здесь появятся уведомления.
</div>
<!-- Действия админки -->
{{ block.super }}
<!-- Список отелей для выбора в виде таблицы -->
<form id="importHotelsForm" method="POST">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-bordered">
<thead class="thead-dark">
<tr>
<!-- Чекбокс для выбора всех отелей -->
<th><input type="checkbox" id="select-all" /></th>
<th>Внешний ID</th>
<th>Отображаемое имя</th>
<th>Имя отеля</th>
<th>Дата создания</th>
<th>Дата обновления</th>
<th>Импортирован в основную базу</th>
</tr>
</thead>
<tbody>
{% for hotel in imported_hotels %}
<tr>
<td>
<input type="checkbox" name="hotels" value="{{ hotel.id }}" id="hotel{{ hotel.id }}" class="select-row" />
</td>
<td>{{ hotel.external_id }}</td>
<td>{{ hotel.display_name }}</td>
<td>{{ hotel.name }}</td>
<td>{{ hotel.creation_date }}</td>
<td>{{ hotel.updated_at }}</td>
<td>{{ hotel.imported_to_main_db }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Здесь вы можете добавить скрытые поля или другие элементы формы, если они нужны -->
</form>
{% endblock %}
{% block extrahead %}
{{ block.super }}
<!-- Подключаем Bootstrap 4 -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Обработчик для выбора всех чекбоксов
const selectAllCheckbox = document.getElementById('select-all');
selectAllCheckbox.addEventListener('change', function() {
const checkboxes = document.querySelectorAll(".select-row");
checkboxes.forEach(function(checkbox) {
checkbox.checked = selectAllCheckbox.checked;
});
});
// Кнопка импорта
const importButton = document.getElementById('importHotelsButton');
const notificationElement = document.getElementById('notification');
importButton.addEventListener('click', function(e) {
e.preventDefault(); // предотвращаем стандартное поведение кнопки
// Даем время DOM полностью загрузиться
setTimeout(function() {
const checkboxes = document.querySelectorAll('input[name="hotels"]:checked');
const selectedHotels = [];
console.log("Чекбоксы:", checkboxes); // Отладка: выводим все выбранные чекбоксы
checkboxes.forEach(function(checkbox) {
selectedHotels.push(checkbox.value);
console.log("Выбранный отель:", checkbox.value); // Отладка: выводим ID выбранного отеля
});
if (selectedHotels.length > 0) {
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
console.log("CSRF токен:", csrfToken);
importButton.disabled = true;
importButton.textContent = 'Импортируем...';
// Отправка выбранных отелей на сервер через fetch
fetch('/import_hotels/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify({ hotels: selectedHotels })
})
.then(response => response.json())
.then(data => {
console.log("Ответ сервера:", data);
notificationElement.classList.remove('d-none');
notificationElement.classList.add('alert-success');
notificationElement.textContent = "Отели успешно импортированы!";
checkboxes.forEach(checkbox => checkbox.checked = false); // Снимаем выделение с чекбоксов
})
.catch((error) => {
console.error("Ошибка при импорте:", error);
notificationElement.classList.remove('d-none');
notificationElement.classList.add('alert-danger');
notificationElement.textContent = "Произошла ошибка при импорте отелей.";
})
.finally(() => {
importButton.disabled = false;
importButton.textContent = 'Импортировать выбранные отели';
});
} else {
console.log("Не выбраны отели");
notificationElement.classList.remove('d-none');
notificationElement.classList.add('alert-warning');
notificationElement.textContent = "Пожалуйста, выберите хотя бы один отель для импорта.";
}
}, 100); // Задержка 100ms, чтобы дождаться рендеринга всех элементов
});
});
document.addEventListener('DOMContentLoaded', function() {
const checkboxes = document.querySelectorAll('input[name="hotels"]');
console.log("Чекбоксы на странице:", checkboxes); // Проверим, есть ли чекбоксы
});
</script>
{% endblock %}

3
antifroud/tests.py Normal file
View File

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

10
antifroud/urls.py Normal file
View File

@@ -0,0 +1,10 @@
# antifroud/urls.py
from django.urls import path
from . import views
app_name = 'antifroud'
urlpatterns = [
path('import_selected_hotels/', views.import_selected_hotels, name='importedhotels_import_selected_hotels'),
# Другие URL-адреса
]

110
antifroud/views.py Normal file
View File

@@ -0,0 +1,110 @@
import logging
from django.http import JsonResponse
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from .models import ImportedHotel
from hotels.models import Hotel
from django.contrib.admin.views.decorators import staff_member_required
from django.utils import timezone
# Создаем логгер
logger = logging.getLogger('antifroud')
@staff_member_required
def import_selected_hotels(request):
if request.method != 'POST':
logger.error("Invalid request method. Only POST is allowed.")
return JsonResponse({'success': False, 'error': 'Invalid request method'})
selected_hotels = request.POST.getlist('hotels')
if not selected_hotels:
logger.warning("No hotels selected for import.")
return JsonResponse({'success': False, 'error': 'No hotels selected'})
try:
logger.info("Fetching selected hotels from ImportedHotel model.")
# Получаем отели, которые были выбраны для импорта
imported_hotels = ImportedHotel.objects.filter(id__in=selected_hotels)
logger.info(f"Found {imported_hotels.count()} selected hotels for import.")
# Список для хранения новых объектов отелей
hotels_to_import = []
for imported_hotel in imported_hotels:
logger.debug(f"Preparing hotel data for import: {imported_hotel.name}, {imported_hotel.city}")
# Получаем APIConfiguration (если имеется)
api_configuration = None
if imported_hotel.api:
api_configuration = imported_hotel.api
# Получаем PMSConfiguration (если имеется)
pms_configuration = None
if imported_hotel.pms:
pms_configuration = imported_hotel.pms
# Проверяем, импортирован ли отель из другого отеля (imported_from)
imported_from = None
if imported_hotel.imported_from:
imported_from = imported_hotel.imported_from
# Подготовим данные для нового отеля
hotel_data = {
'name': imported_hotel.name,
'api': api_configuration,
'pms': pms_configuration,
'imported_from': imported_from,
'imported_at': timezone.now(), # Устанавливаем дату импорта
'import_status': 'completed', # Устанавливаем статус импорта
}
# Создаем новый объект Hotel
hotel = Hotel(**hotel_data)
hotels_to_import.append(hotel)
# Массово сохраняем новые отели в таблице Hotels
logger.info(f"Importing {len(hotels_to_import)} hotels into Hotel model.")
Hotel.objects.bulk_create(hotels_to_import)
logger.info("Hotels imported successfully.")
# Обновляем статус импортированных отелей
imported_hotels.update(imported=True)
logger.info(f"Updated {imported_hotels.count()} imported hotels' status.")
return JsonResponse({'success': True})
except Exception as e:
logger.error(f"Error during hotel import: {str(e)}", exc_info=True)
return JsonResponse({'success': False, 'error': str(e)})
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from .models import Hotel
from .forms import HotelImportForm
@csrf_exempt # Или используйте @login_required, если нужно ограничить доступ
def import_hotels(request):
if request.method == 'POST':
form = HotelImportForm(request.POST)
if form.is_valid():
# Получаем выбранные отели
selected_hotels = form.cleaned_data['hotels']
# Логика импорта отелей (например, можно их обновить или импортировать в другую базу)
# Для примера, просто устанавливаем флаг "imported" в True
for hotel in selected_hotels:
hotel.imported_to_main_db = True
hotel.save()
# Возвращаем успешный ответ
return JsonResponse({"message": "Отели успешно импортированы!"}, status=200)
else:
# Если форма невалидна
return JsonResponse({"message": "Ошибка при импорте отелей."}, status=400)
else:
# GET-запрос, просто показываем форму
form = HotelImportForm()
return render(request, 'antifroud/admin/import_hotels.html', {'form': form})

4671
bot.log

File diff suppressed because it is too large Load Diff

View File

@@ -121,6 +121,52 @@ async def delete_hotel(update: Update, context):
# await pms_manager.save_log("error", str(e))
# await query.edit_message_text(f"❌ Ошибка: {e}")
# async def check_pms(update, context):
# query = update.callback_query
# try:
# # Получение ID отеля из callback_data
# hotel_id = query.data.split("_")[2]
# # Получение конфигурации отеля и PMS
# hotel = await sync_to_async(Hotel.objects.select_related('pms').get)(id=hotel_id)
# pms_config = hotel.pms
# if not pms_config:
# await query.edit_message_text("PMS конфигурация не найдена.")
# return
# # Создаем экземпляр PMSIntegrationManager
# pms_manager = PMSIntegrationManager(hotel_id=hotel_id)
# await pms_manager.load_hotel()
# await sync_to_async(pms_manager.load_plugin)()
# # Проверяем, какой способ интеграции использовать
# if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data):
# # Плагин поддерживает метод fetch_data
# data = await pms_manager.plugin.fetch_data()
# elif pms_config.api_url and pms_config.token:
# # Используем прямой запрос к API
# from pms_integration.api_client import APIClient
# api_client = APIClient(base_url=pms_config.api_url, access_token=pms_config.token)
# data = api_client.fetch_reservations()
# else:
# # Если подходящий способ не найден
# await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")
# return
# # Сохраняем данные в базу
# from bot.utils.database import save_reservations
# await sync_to_async(save_reservations)(data)
# # Уведомляем об успешной интеграции
# await query.edit_message_text(f"Интеграция PMS {pms_config.name} завершена успешно.")
# except Exception as e:
# # Обрабатываем и логируем ошибки
# await query.edit_message_text(f"❌ Ошибка: {str(e)}")
async def check_pms(update, context):
query = update.callback_query
@@ -144,28 +190,28 @@ async def check_pms(update, context):
# Проверяем, какой способ интеграции использовать
if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data):
# Плагин поддерживает метод fetch_data
data = await pms_manager.plugin.fetch_data()
elif pms_config.api_url and pms_config.token:
# Используем прямой запрос к API
from pms_integration.api_client import APIClient
api_client = APIClient(base_url=pms_config.api_url, access_token=pms_config.token)
data = api_client.fetch_reservations()
report = await pms_manager.plugin._fetch_data()
else:
# Если подходящий способ не найден
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")
return
# Сохраняем данные в базу
from bot.utils.database import save_reservations
await sync_to_async(save_reservations)(data)
# Формируем сообщение о результатах
result_message = (
f"Интеграция PMS завершена успешно.\n"
f"Обработано интервалов: {report['processed_intervals']}\n"
f"Обработано записей: {report['processed_items']}\n"
f"Ошибки: {len(report['errors'])}"
)
if report["errors"]:
result_message += "\n\nСписок ошибок:\n" + "\n".join(report["errors"])
# Уведомляем об успешной интеграции
await query.edit_message_text(f"Интеграция PMS {pms_config.name} завершена успешно.")
await query.edit_message_text(result_message)
except Exception as e:
# Обрабатываем и логируем ошибки
await query.edit_message_text(f"❌ Ошибка: {str(e)}")
async def setup_rooms(update: Update, context):
"""Настроить номера отеля."""
query = update.callback_query

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import ContextTypes
from asgiref.sync import sync_to_async
@@ -7,7 +7,7 @@ from users.models import User
from bot.utils.pdf_report import generate_pdf_report
from bot.utils.database import get_hotels_for_user, get_hotel_by_name
from django.utils.timezone import make_aware
async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Вывод списка отелей для статистики."""
@@ -55,6 +55,7 @@ async def stats_select_period(update: Update, context: ContextTypes.DEFAULT_TYPE
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите период времени:", reply_markup=reply_markup)
async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Генерация и отправка статистики."""
query = update.callback_query
@@ -66,47 +67,70 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE
return
period = query.data.split("_")[2]
print(f'Period raw: {query.data}')
print(f'Selected period: {period}')
now = datetime.utcnow().replace(tzinfo=timezone.utc) # Используем timezone.utc
now = datetime.now()
if period == "day":
start_date = (now - timedelta(days=1)).date() # Вчерашняя дата
end_date = now.date() # Сегодняшняя дата
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)
elif period == "week":
start_date = (now - timedelta(days=7)).date()
end_date = now.date()
start_date = (now - timedelta(days=7)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
elif period == "month":
start_date = (now - timedelta(days=30)).date()
end_date = now.date()
else:
start_date = (now - timedelta(days=30)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
else: # "all"
start_date = None
end_date = None
print(f'Raw start_date: {start_date}, Raw end_date: {end_date}')
# Убедитесь, что даты имеют временную зону UTC
if start_date:
start_date = make_aware(start_date) if start_date.tzinfo is None else start_date
if end_date:
end_date = make_aware(end_date) if end_date.tzinfo is None else end_date
print(f'Filtered start_date: {start_date}, Filtered end_date: {end_date}')
# Фильтрация по "дата заезда"
if start_date and end_date:
reservations = await sync_to_async(list)(
Reservation.objects.filter(
hotel_id=hotel_id,
check_in__date__gte=start_date,
check_in__date__lte=end_date
).prefetch_related('guests')
check_in__gte=start_date,
check_in__lte=end_date
).select_related('hotel')
)
else:
else: # Без фильтра по дате
reservations = await sync_to_async(list)(
Reservation.objects.filter(hotel_id=hotel_id).prefetch_related('guests')
Reservation.objects.filter(
hotel_id=hotel_id
).select_related('hotel')
)
print(f'Filtered reservations count: {len(reservations)}')
if not reservations:
await query.edit_message_text("Нет данных для статистики за выбранный период.")
return
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
print(f'Hotel: {hotel.name}')
for reservation in reservations:
print(f"Reservation ID: {reservation.reservation_id}, Hotel: {reservation.hotel.name}, "
f"Room number: {reservation.room_number}, Check-in: {reservation.check_in}, Check-out: {reservation.check_out}")
# Генерация PDF отчета (пример)
file_path = generate_pdf_report(hotel.name, reservations, start_date, end_date)
print(f'Generated file path: {file_path}')
with open(file_path, "rb") as file:
await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf")
async def stats_back(update: Update, context):
"""Возврат к выбору отеля."""
query = update.callback_query

Binary file not shown.

View File

@@ -1,186 +0,0 @@
BEGIN TRANSACTION;
INSERT INTO "django_migrations" ("id","app","name","applied") VALUES (1,'contenttypes','0001_initial','2024-12-10 01:46:55.727280'),
(2,'auth','0001_initial','2024-12-10 01:46:55.748342'),
(3,'admin','0001_initial','2024-12-10 01:46:55.761857'),
(4,'admin','0002_logentry_remove_auto_add','2024-12-10 01:46:55.771717'),
(5,'admin','0003_logentry_add_action_flag_choices','2024-12-10 01:46:55.783546'),
(6,'contenttypes','0002_remove_content_type_name','2024-12-10 01:46:55.798657'),
(7,'auth','0002_alter_permission_name_max_length','2024-12-10 01:46:55.812626'),
(8,'auth','0003_alter_user_email_max_length','2024-12-10 01:46:55.826016'),
(9,'auth','0004_alter_user_username_opts','2024-12-10 01:46:55.836404'),
(10,'auth','0005_alter_user_last_login_null','2024-12-10 01:46:55.846555'),
(11,'auth','0006_require_contenttypes_0002','2024-12-10 01:46:55.850370'),
(12,'auth','0007_alter_validators_add_error_messages','2024-12-10 01:46:55.858610'),
(13,'auth','0008_alter_user_username_max_length','2024-12-10 01:46:55.868657'),
(14,'auth','0009_alter_user_last_name_max_length','2024-12-10 01:46:55.880462'),
(15,'auth','0010_alter_group_name_max_length','2024-12-10 01:46:55.893095'),
(16,'auth','0011_update_proxy_permissions','2024-12-10 01:46:55.900517'),
(17,'auth','0012_alter_user_first_name_max_length','2024-12-10 01:46:55.911606'),
(18,'users','0001_initial','2024-12-10 01:46:55.941858'),
(19,'hotels','0001_initial','2024-12-10 01:46:55.957904'),
(20,'pms_integration','0001_initial','2024-12-10 01:46:55.968957'),
(21,'hotels','0002_initial','2024-12-10 01:46:56.002327'),
(22,'hotels','0003_initial','2024-12-10 01:46:56.025083'),
(23,'sessions','0001_initial','2024-12-10 01:46:56.034581'),
(24,'pms_integration','0002_alter_pmsconfiguration_plugin_name','2024-12-10 02:52:26.722876'),
(25,'pms_integration','0003_alter_pmsconfiguration_plugin_name','2024-12-10 02:58:35.733611'),
(26,'pms_integration','0004_alter_pmsconfiguration_plugin_name','2024-12-10 03:00:06.725614'),
(27,'pms_integration','0005_pmsconfiguration_private_key_and_more','2024-12-10 03:04:00.440483'),
(28,'scheduler','0001_initial','2024-12-10 06:42:05.633067'),
(29,'scheduler','0002_alter_scheduledtask_options_and_more','2024-12-10 06:49:38.460869'),
(30,'scheduler','0003_alter_scheduledtask_options_and_more','2024-12-10 07:13:54.438968');
INSERT INTO "django_admin_log" ("id","object_id","object_repr","action_flag","change_message","content_type_id","user_id","action_time") VALUES (1,'1','Shelter Golden Hills 3',1,'[{"added": {}}]',7,1,'2024-12-09 09:32:31.355706'),
(2,'2','Shelter Golden Hills 4',1,'[{"added": {}}]',7,1,'2024-12-09 09:33:10.530095'),
(3,'1','Golden Hills 3',1,'[{"added": {}}]',15,1,'2024-12-09 09:34:11.465732'),
(4,'2','Golden Hills 4',1,'[{"added": {}}]',15,1,'2024-12-09 09:34:21.783766'),
(5,'1','andrew',1,'[{"added": {}}]',18,1,'2024-12-09 09:35:20.367524'),
(6,'1','Настройки уведомлений для andrew',1,'[{"added": {}}]',19,1,'2024-12-09 09:35:40.518128'),
(7,'1','andrew - Golden Hills 3',1,'[{"added": {}}]',13,1,'2024-12-09 09:35:57.888800'),
(8,'2','andrew - Golden Hills 4',1,'[{"added": {}}]',13,1,'2024-12-09 09:36:06.799616'),
(9,'3','Как дома',1,'[{"added": {}}]',15,1,'2024-12-09 10:19:47.100883'),
(10,'2','Shelter Golden Hills 4',2,'[{"changed": {"fields": ["\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u043b\u0430\u0433\u0438\u043d\u0430"]}}]',7,1,'2024-12-10 02:47:47.763286'),
(11,'1','Shelter Golden Hills 3',2,'[{"changed": {"fields": ["\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u043b\u0430\u0433\u0438\u043d\u0430"]}}]',7,1,'2024-12-10 02:47:51.978891'),
(12,'2','Shelter Golden Hills 4',2,'[{"changed": {"fields": ["plugin_name"]}}]',7,1,'2024-12-10 03:00:54.761679'),
(13,'2','Shelter Golden Hills 4',2,'[]',7,1,'2024-12-10 03:01:06.448068'),
(14,'3','Как дома / RealtyCalendar',1,'[{"added": {}}]',7,1,'2024-12-10 03:07:04.219584'),
(15,'3','andrew - Как дома',1,'[{"added": {}}]',13,1,'2024-12-10 03:07:13.403477'),
(16,'3','Как дома',2,'[{"changed": {"fields": ["PMS \u0441\u0438\u0441\u0442\u0435\u043c\u0430"]}}]',15,1,'2024-12-10 03:16:16.331411'),
(17,'4','Bnovo',1,'[{"added": {}}]',7,1,'2024-12-10 04:18:40.567772'),
(18,'4','Bnovo',2,'[{"changed": {"fields": ["\u041b\u043e\u0433\u0438\u043d", "\u041f\u0430\u0440\u043e\u043b\u044c"]}}]',7,1,'2024-12-10 04:22:39.182778'),
(19,'4','Test',1,'[{"added": {}}]',15,1,'2024-12-10 04:25:37.535780'),
(20,'4','andrew - Test',1,'[{"added": {}}]',13,1,'2024-12-10 04:25:43.910574'),
(21,'1','bot running',1,'[{"added": {}}]',21,1,'2024-12-10 07:04:41.982258'),
(22,'2','bot.check_pms',1,'[{"added": {}}]',21,1,'2024-12-10 07:07:20.515061'),
(23,'2','bot.check_pms',2,'[{"changed": {"fields": ["\u0424\u0443\u043d\u043a\u0446\u0438\u044f", "\u0410\u043a\u0442\u0438\u0432\u043d\u043e", "\u0414\u043d\u0438 \u043d\u0435\u0434\u0435\u043b\u0438"]}}]',21,1,'2024-12-10 08:14:49.799413'),
(24,'1','bot running',2,'[{"changed": {"fields": ["\u0424\u0443\u043d\u043a\u0446\u0438\u044f", "\u0414\u043d\u0438 \u043d\u0435\u0434\u0435\u043b\u0438", "\u0414\u043d\u0438 \u043d\u0435\u0434\u0435\u043b\u0438"]}}]',21,1,'2024-12-10 08:15:52.093648'),
(25,'1','andrew',2,'[{"changed": {"fields": ["\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d"]}}]',18,1,'2024-12-10 08:35:12.673349');
INSERT INTO "django_content_type" ("id","app_label","model") VALUES (1,'admin','logentry'),
(2,'auth','permission'),
(3,'auth','group'),
(4,'auth','user'),
(5,'contenttypes','contenttype'),
(6,'sessions','session'),
(7,'pms_integration','pmsconfiguration'),
(8,'pms_integration','pmsintegrationlog'),
(9,'hotels','apiconfiguration'),
(10,'hotels','fraudlog'),
(11,'hotels','guest'),
(12,'hotels','reservation'),
(13,'hotels','userhotel'),
(14,'hotels','apirequestlog'),
(15,'hotels','hotel'),
(16,'users','localuseractivitylog'),
(17,'users','useractivitylog'),
(18,'users','user'),
(19,'users','notificationsettings'),
(20,'users','userconfirmation'),
(21,'scheduler','scheduledtask');
INSERT INTO "auth_permission" ("id","content_type_id","codename","name") VALUES (1,1,'add_logentry','Can add log entry'),
(2,1,'change_logentry','Can change log entry'),
(3,1,'delete_logentry','Can delete log entry'),
(4,1,'view_logentry','Can view log entry'),
(5,2,'add_permission','Can add permission'),
(6,2,'change_permission','Can change permission'),
(7,2,'delete_permission','Can delete permission'),
(8,2,'view_permission','Can view permission'),
(9,3,'add_group','Can add group'),
(10,3,'change_group','Can change group'),
(11,3,'delete_group','Can delete group'),
(12,3,'view_group','Can view group'),
(13,4,'add_user','Can add user'),
(14,4,'change_user','Can change user'),
(15,4,'delete_user','Can delete user'),
(16,4,'view_user','Can view user'),
(17,5,'add_contenttype','Can add content type'),
(18,5,'change_contenttype','Can change content type'),
(19,5,'delete_contenttype','Can delete content type'),
(20,5,'view_contenttype','Can view content type'),
(21,6,'add_session','Can add session'),
(22,6,'change_session','Can change session'),
(23,6,'delete_session','Can delete session'),
(24,6,'view_session','Can view session'),
(25,7,'add_pmsconfiguration','Can add PMS система'),
(26,7,'change_pmsconfiguration','Can change PMS система'),
(27,7,'delete_pmsconfiguration','Can delete PMS система'),
(28,7,'view_pmsconfiguration','Can view PMS система'),
(29,8,'add_pmsintegrationlog','Can add Журнал интеграции PMS'),
(30,8,'change_pmsintegrationlog','Can change Журнал интеграции PMS'),
(31,8,'delete_pmsintegrationlog','Can delete Журнал интеграции PMS'),
(32,8,'view_pmsintegrationlog','Can view Журнал интеграции PMS'),
(33,9,'add_apiconfiguration','Can add Конфигурация API'),
(34,9,'change_apiconfiguration','Can change Конфигурация API'),
(35,9,'delete_apiconfiguration','Can delete Конфигурация API'),
(36,9,'view_apiconfiguration','Can view Конфигурация API'),
(37,10,'add_fraudlog','Can add Журнал мошенничества'),
(38,10,'change_fraudlog','Can change Журнал мошенничества'),
(39,10,'delete_fraudlog','Can delete Журнал мошенничества'),
(40,10,'view_fraudlog','Can view Журнал мошенничества'),
(41,11,'add_guest','Can add Гость'),
(42,11,'change_guest','Can change Гость'),
(43,11,'delete_guest','Can delete Гость'),
(44,11,'view_guest','Can view Гость'),
(45,12,'add_reservation','Can add Бронирование'),
(46,12,'change_reservation','Can change Бронирование'),
(47,12,'delete_reservation','Can delete Бронирование'),
(48,12,'view_reservation','Can view Бронирование'),
(49,13,'add_userhotel','Can add Пользователь отеля'),
(50,13,'change_userhotel','Can change Пользователь отеля'),
(51,13,'delete_userhotel','Can delete Пользователь отеля'),
(52,13,'view_userhotel','Can view Пользователь отеля'),
(53,14,'add_apirequestlog','Can add Журнал запросов API'),
(54,14,'change_apirequestlog','Can change Журнал запросов API'),
(55,14,'delete_apirequestlog','Can delete Журнал запросов API'),
(56,14,'view_apirequestlog','Can view Журнал запросов API'),
(57,15,'add_hotel','Can add Отель'),
(58,15,'change_hotel','Can change Отель'),
(59,15,'delete_hotel','Can delete Отель'),
(60,15,'view_hotel','Can view Отель'),
(61,16,'add_localuseractivitylog','Can add local user activity log'),
(62,16,'change_localuseractivitylog','Can change local user activity log'),
(63,16,'delete_localuseractivitylog','Can delete local user activity log'),
(64,16,'view_localuseractivitylog','Can view local user activity log'),
(65,17,'add_useractivitylog','Can add Журнал активности'),
(66,17,'change_useractivitylog','Can change Журнал активности'),
(67,17,'delete_useractivitylog','Can delete Журнал активности'),
(68,17,'view_useractivitylog','Can view Журнал активности'),
(69,18,'add_user','Can add Пользователь'),
(70,18,'change_user','Can change Пользователь'),
(71,18,'delete_user','Can delete Пользователь'),
(72,18,'view_user','Can view Пользователь'),
(73,19,'add_notificationsettings','Can add Способ оповещения'),
(74,19,'change_notificationsettings','Can change Способ оповещения'),
(75,19,'delete_notificationsettings','Can delete Способ оповещения'),
(76,19,'view_notificationsettings','Can view Способ оповещения'),
(77,20,'add_userconfirmation','Can add Подтверждение пользователя'),
(78,20,'change_userconfirmation','Can change Подтверждение пользователя'),
(79,20,'delete_userconfirmation','Can delete Подтверждение пользователя'),
(80,20,'view_userconfirmation','Can view Подтверждение пользователя'),
(81,21,'add_scheduledtask','Can add Задача'),
(82,21,'change_scheduledtask','Can change Задача'),
(83,21,'delete_scheduledtask','Can delete Задача'),
(84,21,'view_scheduledtask','Can view Задача');
INSERT INTO "auth_user" ("id","password","last_login","is_superuser","username","last_name","email","is_staff","is_active","date_joined","first_name") VALUES (1,'pbkdf2_sha256$870000$0tWRKvUavKHjKmwWjWsfYc$mfqBdzr5TB74K1f9OHCI3w/66VZE7vY53MEpgUT73/4=','2024-12-10 06:59:08.529292',1,'trevor1985','','shadow85@list.ru',1,1,'2024-12-09 09:30:44.551380','');
INSERT INTO "users_user" ("id","password","last_login","is_superuser","username","first_name","last_name","email","is_staff","is_active","date_joined","telegram_id","chat_id","role","confirmed") VALUES (1,'Andrey K. Tsoy','2024-12-09 09:34:41',1,'andrew','','','',0,1,'2024-12-09 09:34:27',556399210,556399210,'hotel_user',1);
INSERT INTO "users_notificationsettings" ("id","telegram_enabled","email_enabled","email","notification_time","user_id") VALUES (1,0,1,'a.choi@smartsoltech.kr','09:00:00',1);
INSERT INTO "hotels_hotel" ("id","name","created_at","api_id","pms_id") VALUES (1,'Golden Hills 3','2024-12-09 09:34:11.463934',NULL,1),
(2,'Golden Hills 4','2024-12-09 09:34:21.782571',NULL,2),
(3,'Как дома','2024-12-09 10:19:47.095457',NULL,3),
(4,'Test','2024-12-10 04:25:37.534927',NULL,4);
INSERT INTO "pms_integration_pmsintegrationlog" ("id","checked_at","status","message","hotel_id") VALUES (1,'2024-12-09 09:36:43.044421','error','Плагин для PMS Shelter PMS не найден.',2),
(2,'2024-12-09 09:38:09.595349','error','Плагин для PMS Shelter PMS не найден.',1),
(3,'2024-12-09 09:40:38.721678','error','Плагин для PMS Shelter PMS не найден.',2),
(4,'2024-12-09 10:07:44.968856','error','Плагин для PMS Shelter PMS не найден.',1);
INSERT INTO "hotels_userhotel" ("id","hotel_id","user_id") VALUES (1,1,1),
(2,2,1),
(3,3,1),
(4,4,1);
INSERT INTO "django_session" ("session_key","session_data","expire_date") VALUES ('wjcybubh02eoyth1e4pm9cgjc8c9rk1v','.eJxVjEEOwiAQRe_C2hBgpgVcuvcMZIBBqoYmpV0Z765NutDtf-_9lwi0rTVsnZcwZXEWWpx-t0jpwW0H-U7tNss0t3WZotwVedAur3Pm5-Vw_w4q9fqthxwtITBY9MlGtJ6ADZiiAdyQ7KhVyVgcGY7gEL1BVqMhH51iNl68P9F6N0w:1tKa6w:zJT5PgcESbBBG5gKuJsyfV6EOxizdevxDzI4QrGbLsc','2024-12-23 09:31:26.682056'),
('zhfmhutrq2o277smded60dpbxurhyqrv','.eJxVjEEOwiAQRe_C2hBgpgVcuvcMZIBBqoYmpV0Z765NutDtf-_9lwi0rTVsnZcwZXEWWpx-t0jpwW0H-U7tNss0t3WZotwVedAur3Pm5-Vw_w4q9fqthxwtITBY9MlGtJ6ADZiiAdyQ7KhVyVgcGY7gEL1BVqMhH51iNl68P9F6N0w:1tKpiq:sIJzaN8dmZGRQlRUklQ9SNyhbyMuSLYMj-62RXTX5no','2024-12-24 02:11:36.624898'),
('mz2w18vdzao572ss2ikr5tgj0w2rrbff','.eJxVjEEOwiAQRe_C2hBgpgVcuvcMZIBBqoYmpV0Z765NutDtf-_9lwi0rTVsnZcwZXEWWpx-t0jpwW0H-U7tNss0t3WZotwVedAur3Pm5-Vw_w4q9fqthxwtITBY9MlGtJ6ADZiiAdyQ7KhVyVgcGY7gEL1BVqMhH51iNl68P9F6N0w:1tKuD6:CESE5WIEOm7OzSRbIws0uyat8L3VjPyjOBTBeK_YzFY','2024-12-24 06:59:08.533787');
INSERT INTO "pms_integration_pmsconfiguration" ("id","name","url","token","username","password","created_at","plugin_name","private_key","public_key") VALUES (1,'Shelter Golden Hills 3','https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter','A0DD4B7F-0381-4096-B46E-D22F1A571996',NULL,NULL,'2024-12-09 09:32:31.348604','Shelter',NULL,NULL),
(2,'Shelter Golden Hills 4','https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter','A0DD4B7F-0381-4096-B46E-D22F1A571996',NULL,NULL,'2024-12-09 09:33:10.514492','shelter',NULL,NULL),
(3,'Как дома / RealtyCalendar','https://realtycalendar.ru/api/v1/bookings/',NULL,NULL,NULL,'2024-12-10 03:07:04.218499','realtycalendar','a3669a349b9911cf774ec30ba9523582','b95e293cf07c84dfce44ec41bdced96a'),
(4,'Bnovo','https://online.bnovo.ru',NULL,'16798','a46da27476f02d1f','2024-12-10 04:18:40.567212','bnovo',NULL,NULL);
INSERT INTO "scheduler_scheduledtask" ("id","last_run","days","hours","minutes","months","active","task_name","function_path","weekdays") VALUES (1,NULL,'*','*','*','*',1,'bot running','bot.utils.bot_setup.setup_bot','[6]'),
(2,NULL,'*','*','*','*',1,'bot.check_pms','manage.main','2');
COMMIT;

View File

@@ -51,13 +51,6 @@ class HotelAdmin(admin.ModelAdmin):
except Exception as e:
self.message_user(request, f"Ошибка: {str(e)}", level="error")
return redirect("..")
@admin.register(FraudLog)
class FroudAdmin(admin.ModelAdmin):
list_display = ('hotel', 'reservation_id', 'guest_name', 'check_in_date', 'detected_at', 'message')
search_fields = ('hotel__name', 'reservation_id', 'guest_name', 'check_in_date', 'message')
list_filter = ('hotel', 'check_in_date', 'detected_at')
ordering = ('-detected_at',)
@admin.register(UserHotel)
class UserHotelAdmin(admin.ModelAdmin):
@@ -66,7 +59,6 @@ class UserHotelAdmin(admin.ModelAdmin):
# list_filter = ('hotel',)
# ordering = ('-hotel',)
@admin.register(Reservation)
class ReservationAdmin(admin.ModelAdmin):
list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')

View File

@@ -0,0 +1,18 @@
# 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

@@ -0,0 +1,29 @@
# 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

@@ -88,7 +88,7 @@ class APIRequestLog(models.Model):
class Reservation(models.Model):
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
room_number = models.CharField(max_length=50, verbose_name="Номер комнаты")
room_number = models.CharField(max_length=255, null=True, blank=True)
room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
check_in = models.DateTimeField(verbose_name="Дата заезда")
check_out = models.DateTimeField(verbose_name="Дата выезда")

4302
import_hotels.log Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ import json
from datetime import datetime, timedelta
from .base_plugin import BasePMSPlugin
from asgiref.sync import sync_to_async
from pms_integration.models import PMSConfiguration # Убедитесь, что модель существует
from pms_integration.models import PMSConfiguration
class BnovoPMSPlugin(BasePMSPlugin):

View File

@@ -1,11 +1,11 @@
from .base_plugin import BasePMSPlugin
import requests
class EcviPMS(BasePMSPlugin):
"""
Плагин для PMS Shelter.
Плагин для PMS ECVI.
"""
def fetch_data(self):
def _fetch_data(self):
print("Fetching data from Ecvi PMS...")
# Реализация метода получения данных из PMS Shelter
response = requests.get(self.pms_config.url, headers={"Authorization": f"Bearer {self.pms_config.token}"})

View File

@@ -1,103 +1,212 @@
import requests
import json
from datetime import datetime, timedelta
from asgiref.sync import sync_to_async
from .base_plugin import BasePMSPlugin
from hotels.models import Reservation
from hotels.models import Hotel
from datetime import datetime, timedelta, timezone
from asgiref.sync import sync_to_async
from hotels.models import Reservation, Hotel
from .base_plugin import BasePMSPlugin
from pms_integration.models import PMSConfiguration
class Shelter(BasePMSPlugin):
"""
Плагин для PMS Shelter Coud.
Плагин для интеграции с Shelter PMS.
"""
def __init__(self, config):
super().__init__(config)
self.token = config.token
def __init__(self, pms_config):
super().__init__(pms_config)
self.api_url = pms_config.url
self.token = pms_config.token
self.pagination_count = 50
def get_default_parser_settings(self):
"""
Возвращает настройки по умолчанию для обработки данных.
"""
return {
"date_format": "%Y-%m-%dT%H:%M:%S",
"timezone": "UTC"
}
"""
Возвращает настройки по умолчанию для разбора данных PMS Shelter.
"""
return {
"fields_mapping": {
"reservation_id": "id",
"hotel_id": "hotelId",
"hotel_name": "hotelName",
"check_in": "from",
"check_out": "until",
"reservation_date": "date",
"room_type_id": "roomTypeId",
"room_id": "roomId",
"room_number": "roomNumber",
"room_type_name": "roomTypeName",
"check_in_status": "checkInStatus",
"is_annul": "isAnnul",
"reservation_price": "reservationPrice",
"discount": "discount",
def _fetch_data(self):
},
"date_format": "%Y-%m-%dT%H:%M:%S",
"timezone": "UTC",
}
async def _get_last_saved_date(self):
"""
Выполняет запрос к API PMS для получения данных.
Получает дату последнего сохраненного бронирования для отеля.
"""
url = 'https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter'
try:
last_reservation = await sync_to_async(
Reservation.objects.filter(hotel__pms=self.pms_config).order_by('-check_in').first
)()
return last_reservation.check_in if last_reservation else None
except Exception as e:
print(f"[ERROR] Ошибка получения последнего сохраненного бронирования: {e}")
return None
async def _fetch_data(self):
"""
Получает данные о бронированиях из PMS.
Данные обрабатываются по временным промежуткам и сразу записываются в БД.
Возвращает отчёт о проделанной работе.
"""
now = datetime.utcnow().replace(tzinfo=timezone.utc)
start_date = await self._get_last_saved_date() or (now - timedelta(days=90))
end_date = now + timedelta(days=30)
headers = {
'accept': 'text/plain',
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json',
}
from_index = 0
count_per_request = 50
total_count = None
all_items = []
now = datetime.now()
start_date = (now - timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ')
end_date = (now + timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ')
print(f"[DEBUG] Fetching data from {start_date} to {end_date}")
while total_count is None or from_index < total_count:
data = {
"from": start_date,
"until": end_date,
"pagination": {
"from": from_index,
"count": count_per_request
}
# Результаты выполнения
report = {
"processed_intervals": 0,
"processed_items": 0,
"errors": [],
}
# Разделение на временные интервалы
interval_days = 5 # Например, каждые 5 дней
current_start_date = start_date
while current_start_date < end_date:
current_end_date = min(current_start_date + timedelta(days=interval_days), end_date)
# Формирование payload
payload = {
"from": current_start_date.strftime('%Y-%m-%dT%H:%M:%SZ'),
"until": current_end_date.strftime('%Y-%m-%dT%H:%M:%SZ'),
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
response_data = response.json()
items = response_data.get("items", [])
all_items.extend(items)
if total_count is None:
total_count = response_data.get("count", 0)
from_index += len(items)
else:
raise ValueError(f'Shelter API Error: {response.status_code}')
return all_items
async def _save_to_db(self, data, hotel_id):
"""
Сохраняет данные о бронированиях в таблицу Reservation.
:param data: Список данных о бронированиях.
:param hotel_id: ID отеля, к которому относятся бронирования.
"""
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
for item in data:
print(f"Данные для сохранения: {item}")
print(f"[DEBUG] Sending payload: {json.dumps(payload)}")
try:
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
response = await sync_to_async(requests.post)(self.api_url, headers=headers, data=json.dumps(payload))
response.raise_for_status()
except requests.exceptions.RequestException as e:
error_message = f"[ERROR] Request error between {current_start_date} and {current_end_date}: {e}"
print(error_message)
report["errors"].append(error_message)
current_start_date = current_end_date
continue
try:
data = response.json()
print(f"[DEBUG] Received response: {data}")
except json.JSONDecodeError as e:
error_message = f"[ERROR] Ошибка декодирования JSON между {current_start_date} и {current_end_date}: {e}"
print(error_message)
report["errors"].append(error_message)
current_start_date = current_end_date
continue
total_count = data.get("count", 0)
items = data.get("items", [])
print(f"[DEBUG] Retrieved {len(items)} items (Total: {total_count}).")
# Если данных нет, пропускаем текущий интервал
if not items:
print(f"[WARNING] No items found between {current_start_date} and {current_end_date}.")
else:
for idx, item in enumerate(items, start=1):
print(f"[DEBUG] Processing item #{idx}/{len(items)}: {item}")
try:
await self._save_to_db(item)
report["processed_items"] += 1
except Exception as e:
error_message = f"[ERROR] Error processing item {item.get('id', 'Unknown')}: {e}"
print(error_message)
report["errors"].append(error_message)
# Отмечаем обработанный интервал
report["processed_intervals"] += 1
# Обновляем начало интервала
current_start_date = current_end_date
print(f"[DEBUG] Data fetching completed from {start_date} to {end_date}.")
return report
async def _save_to_db(self, item):
"""
Сохраняет данные о бронировании в БД.
Проверяет, существует ли уже бронирование с таким ID.
"""
try:
print(f"[DEBUG] Starting to save reservation {item['id']} to the database.")
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
print(f"[DEBUG] Hotel found: {hotel.name}")
date_format = '%Y-%m-%dT%H:%M:%S'
print(f"[DEBUG] Parsing check-in and check-out dates for reservation {item['id']}")
try:
check_in = datetime.strptime(item["from"], date_format).replace(tzinfo=timezone.utc)
check_out = datetime.strptime(item["until"], date_format).replace(tzinfo=timezone.utc)
print(f"[DEBUG] Parsed check-in: {check_in}, check-out: {check_out}")
except Exception as e:
print(f"[ERROR] Ошибка парсинга дат для бронирования {item['id']}: {e}")
raise
room_number = item.get("roomNumber", "") or "Unknown"
print(f"[DEBUG] Room number determined: {room_number}")
print(f"[DEBUG] Checking if reservation with ID {item['id']} already exists.")
existing_reservation = await sync_to_async(
Reservation.objects.filter(reservation_id=item["id"]).first
)()
if existing_reservation:
print(f"[DEBUG] Reservation with ID {item['id']} already exists. Updating it...")
print(f"[DEBUG] Updating existing reservation {item['id']}.")
await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=item["id"],
hotel=hotel,
defaults={
"room_number": item.get("roomNumber", ""), # Номер комнаты
"room_type": item.get("roomTypeName", ""), # Тип комнаты
"check_in": datetime.strptime(item["from"], '%Y-%m-%dT%H:%M:%S'), # Дата заезда
"check_out": datetime.strptime(item["until"], '%Y-%m-%dT%H:%M:%S'), # Дата выезда
"status": item.get("checkInStatus", ""), # Статус бронирования
"price": item.get("reservationPrice", 0), # Цена
"discount": item.get("discount", 0), # Скидка
}
"room_number": room_number,
"room_type": item.get("roomTypeName", ""),
"check_in": check_in,
"check_out": check_out,
"status": item.get("checkInStatus", ""),
"price": item.get("reservationPrice", 0),
"discount": item.get("discount", 0),
},
)
if created:
print(f"Создана запись: {reservation}")
else:
print(f"Обновлена запись: {reservation}")
except Exception as e:
print(f"Ошибка при сохранении бронирования ID {item['id']}: {e}")
print(f"[DEBUG] Updated existing reservation {item['id']}.")
else:
print(f"[DEBUG] No existing reservation found for ID {item['id']}. Creating a new one...")
print(f"[DEBUG] Creating a new reservation {item['id']}.")
await sync_to_async(Reservation.objects.create)(
reservation_id=item["id"],
hotel=hotel,
room_number=room_number,
room_type=item.get("roomTypeName", ""),
check_in=check_in,
check_out=check_out,
status=item.get("checkInStatus", ""),
price=item.get("reservationPrice", 0),
discount=item.get("discount", 0),
)
print(f"[DEBUG] Created a new reservation {item['id']}.")
print(f"[DEBUG] {'Created' if not existing_reservation else 'Updated'} reservation {item['id']} / {hotel.name}.")
except ValueError as ve:
print(f"[ERROR] Ошибка обработки даты для бронирования {item['id']}: {ve}")
except Exception as e:
print(f"[ERROR] Ошибка сохранения бронирования {item['id']}: {e}")

Binary file not shown.

Binary file not shown.

25
req.txt
View File

@@ -1,25 +0,0 @@
anyio==4.6.2.post1
asgiref==3.8.1
certifi==2024.8.30
Django==5.1.4
django-filter==24.3
django-jazzmin==3.0.1
django-jet==1.0.8
et_xmlfile==2.0.0
h11==0.14.0
httpcore==1.0.7
httpx==0.28.0
idna==3.10
numpy==2.1.3
openpyxl==3.1.5
pandas==2.2.3
pillow==11.0.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-telegram-bot==21.8
pytz==2024.2
PyYAML==6.0.2
six==1.17.0
sniffio==1.3.1
sqlparse==0.5.2
tzdata==2024.2

View File

@@ -52,3 +52,58 @@ ua-parser-builtins==0.18.0.post1
urllib3==2.2.3
user-agents==2.2.0
yarl==1.18.3
ace_tools==0.0
aiohappyeyeballs==2.4.4
aiohttp==3.11.10
aiosignal==1.3.1
anyio==4.6.2.post1
APScheduler==3.11.0
asgiref==3.8.1
async-timeout==5.0.1
attrs==24.2.0
certifi==2024.8.30
charset-normalizer==3.4.0
Django==5.1.4
django-filter==24.3
django-health-check==3.18.3
django-jazzmin==3.0.1
django-jet==1.0.8
et_xmlfile==2.0.0
exceptiongroup==1.2.2
fpdf==1.7.2
frozenlist==1.5.0
geoip2==4.8.1
h11==0.14.0
httpcore==1.0.7
httpx==0.28.0
idna==3.10
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
maxminddb==2.6.2
multidict==6.1.0
numpy==2.1.3
openpyxl==3.1.5
pandas==2.2.3
pathspec==0.12.1
pillow==11.0.0
propcache==0.2.1
PyMySQL==1.1.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-telegram-bot==21.8
pytz==2024.2
PyYAML==6.0.2
referencing==0.35.1
requests==2.32.3
rpds-py==0.22.3
six==1.17.0
sniffio==1.3.1
sqlparse==0.5.2
typing_extensions==4.12.2
tzdata==2024.2
tzlocal==5.2
ua-parser==1.0.0
ua-parser-builtins==0.18.0.post1
urllib3==2.2.3
user-agents==2.2.0
yarl==1.18.3

View File

@@ -50,7 +50,7 @@ class ScheduledTaskForm(forms.ModelForm):
@admin.register(ScheduledTask)
class ScheduledTaskAdmin(admin.ModelAdmin):
form = ScheduledTaskForm
list_display = ("task_name", "function_path", "active", "formatted_last_run")
list_display = ("task_name", "function_path", "minutes", "hours", "months", "weekdays", "active", "formatted_last_run")
list_filter = ("active",)
search_fields = ("task_name", "function_path")

0
static/css/styles.css Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -27,8 +27,12 @@ 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']
ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', '192.168.219.114', 'c710-182-226-158-253.ngrok-free.app']
CSRF_TRUSTED_ORIGINS = [
'https://c710-182-226-158-253.ngrok-free.app',
'https://*.ngrok.io', # Это подойдет для любых URL, связанных с ngrok
]
# Application definition
@@ -44,7 +48,12 @@ INSTALLED_APPS = [
'pms_integration',
'hotels',
'users',
'scheduler'
'scheduler',
'antifroud',
'health_check',
'health_check.db',
'health_check.cache',
]
MIDDLEWARE = [
@@ -113,30 +122,46 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': 'import_hotels.log', # Лог будет записываться в этот файл
},
},
'root': {
'handlers': ['console'],
'level': 'WARNING',
'loggers': {
'django': {
'handlers': ['file'],
'level': 'WARNING',
'propagate': True,
},
'antifroud': {
'handlers': ['file'],
'level': 'WARNING',
'propagate': True,
},
},
}
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'ru-RU'
LANGUAGE_CODE = 'ru'
TIME_ZONE = 'UTC'
TIME_ZONE = 'Europe/Moscow'
USE_TZ = True
USE_I18N = True
USE_TZ = True
USE_L10N = True
# Static files (CSS, JavaScript, Images)
@@ -154,32 +179,33 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
JAZZMIN_SETTINGS = {
"site_title": "Hotel Management",
"site_header": "Hotel Manager Admin",
"site_brand": "HotelPro",
"welcome_sign": "Welcome to Hotel Management System",
"use_bootstrap5": True,
"site_title": "TOUCHH Hotel Management",
"site_header": "TOUCHH Hotel Manager Admin",
"site_brand": "TOUCHH",
"welcome_sign": "Welcome to TOUCHH Hotel Management System",
"show_sidebar": True,
"navigation_expanded": True,
"hide_models": ["users", "guests"],
"navigation_expanded": False,
"hide_models": ["auth.Users", "guests"],
"site_logo": None, # Путь к логотипу, например "static/images/logo.png"
"site_logo_classes": "img-circle", # Классы CSS для логотипа
"site_icon": None, # Иконка сайта (favicon), например "static/images/favicon.ico"
"welcome_sign": "Welcome to Touchh Admin", # Приветствие на странице входа
"copyright": "Touchh © 2024", # Кастомный текст в футере
"search_model": "auth.User", # Модель для строки поиска
"copyright": "Touchh", # Кастомный текст в футере
"icons": {
"auth": "fas fa-users-cog",
"users": "fas fa-user-circle",
"hotels": "fas fa-hotel",
},
"theme": "flatly",
"dark_mode_theme": "cyborg",
"theme": "sandstone",
"dark_mode_theme": "darkly",
"footer": {
"copyright": "Touchh © 2024",
"copyright": "SmartSolTech.kr © 2024",
"version": False,
},
"dashboard_links": [
{"name": "Google", "url": "https://google.com", "new_window": True},
{"name": "Google", "url": "https://touchh.com", "new_window": True},
{"name": "Smartsoltech", "url": "https://smartsoltech.kr", "new_window": True}
],
"custom_links": { # Кастомные ссылки в боковом меню

View File

@@ -1,6 +1,12 @@
from django.contrib import admin
from django.urls import path, include
from antifroud import views
app_name = 'touchh'
urlpatterns = [
path('admin/', admin.site.urls),
path('health/', include('health_check.urls')),
path('antifroud/', include('antifroud.urls')),
]

View File

@@ -1,5 +1,5 @@
from django.contrib import admin
from .models import User, UserConfirmation, UserActivityLog, NotificationSettings
from .models import User, NotificationSettings
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
@@ -8,19 +8,7 @@ class UserAdmin(admin.ModelAdmin):
list_filter = ('role', 'confirmed')
ordering = ('-id',)
@admin.register(UserConfirmation)
class UserConfirmationAdmin(admin.ModelAdmin):
list_display = ('user', 'confirmation_code', 'created_at')
search_fields = ('user__username', 'confirmation_code')
list_filter = ('created_at',)
@admin.register(UserActivityLog)
class UserActivityLogAdmin(admin.ModelAdmin):
list_display = ( 'id', 'user_id', 'ip', 'timestamp', 'date_time', 'agent', 'platform', 'version', 'model', 'device', 'UAString', 'location', 'page_id', 'url_parameters', 'page_title', 'type', 'last_counter', 'hits', 'honeypot', 'reply', 'page_url')
search_fields = ('user_id', 'ip', 'datetime', 'agent', 'platform', 'version', 'model', 'device', 'UAString', 'location', 'page_id', 'url_parameters', 'page_title', 'type', 'last_counter', 'hits', 'honeypot', 'reply', 'page_url')
list_filter = ('page_title', 'user_id', 'ip')
ordering = ('-id',)
@admin.register(NotificationSettings)
class NotificationSettingsAdmin(admin.ModelAdmin):
list_display = ('user', 'email', 'telegram_enabled', 'email_enabled', 'notification_time')

View File

@@ -0,0 +1,22 @@
# 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',
),
]

View File

@@ -47,73 +47,7 @@ class User(AbstractUser):
verbose_name_plural = "Пользователи"
class UserConfirmation(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Пользователь")
confirmation_code = models.UUIDField(default=uuid.uuid4, verbose_name="Код подтверждения")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан: ")
def __str__(self):
return f"Confirmation for {self.user.username} - {self.confirmation_code}"
class Meta:
verbose_name = "Подтверждение пользователя"
verbose_name_plural = "Подтверждения пользователей"
class WordPressUserActivityLog(models.Model):
id = models.AutoField(primary_key=True)
user_id = models.IntegerField()
activity_type = models.CharField(max_length=255)
timestamp = models.DateTimeField()
additional_data = models.JSONField(null=True, blank=True)
class Meta:
db_table = 'wpts_user_activity_log' # Название таблицы в базе данных WordPress
managed = False # Django не будет управлять этой таблицей
app_label = 'Users' # Замените на имя вашего приложения
class LocalUserActivityLog(models.Model):
id = models.AutoField(primary_key=True)
user_id = models.IntegerField()
activity_type = models.CharField(max_length=255)
timestamp = models.DateTimeField()
additional_data = models.JSONField(null=True, blank=True)
def __str__(self):
return f"User {self.user_id} - {self.activity_type}"
class UserActivityLog(models.Model):
id = models.BigAutoField(primary_key=True, verbose_name="ID")
user_id = models.BigIntegerField( verbose_name="ID пользователя")
ip = models.CharField(max_length=100, null=True, blank=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(max_length=255, null=True, blank=True)
agent = models.CharField(max_length=255, null=True, blank=True, verbose_name="Браузер")
platform = models.CharField(max_length=255, null=True, blank=True)
version = models.CharField(max_length=50, null=True, blank=True)
model = models.CharField(max_length=255, null=True, blank=True)
device = models.CharField(max_length=50, null=True, blank=True)
UAString = models.TextField(null=True, blank=True)
location = models.CharField(max_length=255, null=True, blank=True)
page_id = models.BigIntegerField(null=True, blank=True)
url_parameters = models.TextField(null=True, blank=True)
page_title = models.CharField(max_length=255, null=True, blank=True)
type = models.CharField(max_length=50, null=True, blank=True)
last_counter = models.IntegerField(null=True, blank=True)
hits = models.IntegerField(null=True, blank=True)
honeypot = models.BooleanField(null=True, blank=True)
reply = models.BooleanField(null=True, blank=True)
page_url = models.CharField(max_length=255, null=True, blank=True)
class Meta:
db_table = 'user_activity_log' # Название таблицы в локальной базе
verbose_name = 'Журнал активности'
verbose_name_plural = 'Журналы активности'
def __str__(self):
return f"User {self.user_id} - {self.type} - {self.date_time}"
class NotificationSettings(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="Пользователь")
telegram_enabled = models.BooleanField(default=True, verbose_name="Уведомления в Telegram")