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-lock.json
package.json package.json
old_bot old_bot
*.mmdb
*.log
# Ignore files # 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 pms_manager.save_log("error", str(e))
# await query.edit_message_text(f"❌ Ошибка: {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): async def check_pms(update, context):
query = update.callback_query 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): if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data):
# Плагин поддерживает метод fetch_data # Плагин поддерживает метод fetch_data
data = await pms_manager.plugin.fetch_data() report = 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: else:
# Если подходящий способ не найден
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.") await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")
return return
# Сохраняем данные в базу # Формируем сообщение о результатах
from bot.utils.database import save_reservations result_message = (
await sync_to_async(save_reservations)(data) 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(result_message)
await query.edit_message_text(f"Интеграция PMS {pms_config.name} завершена успешно.")
except Exception as e: except Exception as e:
# Обрабатываем и логируем ошибки # Обрабатываем и логируем ошибки
await query.edit_message_text(f"❌ Ошибка: {str(e)}") await query.edit_message_text(f"❌ Ошибка: {str(e)}")
async def setup_rooms(update: Update, context): async def setup_rooms(update: Update, context):
"""Настроить номера отеля.""" """Настроить номера отеля."""
query = update.callback_query 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 import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import ContextTypes from telegram.ext import ContextTypes
from asgiref.sync import sync_to_async 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.pdf_report import generate_pdf_report
from bot.utils.database import get_hotels_for_user, get_hotel_by_name 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): 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) reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите период времени:", reply_markup=reply_markup) await query.edit_message_text("Выберите период времени:", reply_markup=reply_markup)
async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Генерация и отправка статистики.""" """Генерация и отправка статистики."""
query = update.callback_query query = update.callback_query
@@ -66,47 +67,70 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE
return return
period = query.data.split("_")[2] 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": if period == "day":
start_date = (now - timedelta(days=1)).date() # Вчерашняя дата start_date = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now.date() # Сегодняшняя дата end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
elif period == "week": elif period == "week":
start_date = (now - timedelta(days=7)).date() start_date = (now - timedelta(days=7)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now.date() end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
elif period == "month": elif period == "month":
start_date = (now - timedelta(days=30)).date() start_date = (now - timedelta(days=30)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now.date() end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
else: else: # "all"
start_date = None start_date = None
end_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: if start_date and end_date:
reservations = await sync_to_async(list)( reservations = await sync_to_async(list)(
Reservation.objects.filter( Reservation.objects.filter(
hotel_id=hotel_id, hotel_id=hotel_id,
check_in__date__gte=start_date, check_in__gte=start_date,
check_in__date__lte=end_date check_in__lte=end_date
).prefetch_related('guests') ).select_related('hotel')
) )
else: else: # Без фильтра по дате
reservations = await sync_to_async(list)( 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: if not reservations:
await query.edit_message_text("Нет данных для статистики за выбранный период.") await query.edit_message_text("Нет данных для статистики за выбранный период.")
return return
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) 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) 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: with open(file_path, "rb") as file:
await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf") await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf")
async def stats_back(update: Update, context): async def stats_back(update: Update, context):
"""Возврат к выбору отеля.""" """Возврат к выбору отеля."""
query = update.callback_query 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

@@ -52,13 +52,6 @@ class HotelAdmin(admin.ModelAdmin):
self.message_user(request, f"Ошибка: {str(e)}", level="error") self.message_user(request, f"Ошибка: {str(e)}", level="error")
return redirect("..") 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) @admin.register(UserHotel)
class UserHotelAdmin(admin.ModelAdmin): class UserHotelAdmin(admin.ModelAdmin):
list_display = ('user', 'hotel') list_display = ('user', 'hotel')
@@ -66,7 +59,6 @@ class UserHotelAdmin(admin.ModelAdmin):
# list_filter = ('hotel',) # list_filter = ('hotel',)
# ordering = ('-hotel',) # ordering = ('-hotel',)
@admin.register(Reservation) @admin.register(Reservation)
class ReservationAdmin(admin.ModelAdmin): class ReservationAdmin(admin.ModelAdmin):
list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount') list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', '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): class Reservation(models.Model):
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель") hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования") 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="Тип комнаты") room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
check_in = models.DateTimeField(verbose_name="Дата заезда") check_in = models.DateTimeField(verbose_name="Дата заезда")
check_out = 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 datetime import datetime, timedelta
from .base_plugin import BasePMSPlugin from .base_plugin import BasePMSPlugin
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from pms_integration.models import PMSConfiguration # Убедитесь, что модель существует from pms_integration.models import PMSConfiguration
class BnovoPMSPlugin(BasePMSPlugin): class BnovoPMSPlugin(BasePMSPlugin):

View File

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

View File

@@ -1,103 +1,212 @@
import requests import requests
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from asgiref.sync import sync_to_async
from .base_plugin import BasePMSPlugin
from hotels.models import Reservation
from hotels.models import Hotel
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): class Shelter(BasePMSPlugin):
""" """
Плагин для PMS Shelter Coud. Плагин для интеграции с Shelter PMS.
""" """
def __init__(self, config): def __init__(self, pms_config):
super().__init__(config) super().__init__(pms_config)
self.token = config.token self.api_url = pms_config.url
self.token = pms_config.token
self.pagination_count = 50
def get_default_parser_settings(self): def get_default_parser_settings(self):
""" """
Возвращает настройки по умолчанию для обработки данных. Возвращает настройки по умолчанию для разбора данных PMS Shelter.
""" """
return { 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",
},
"date_format": "%Y-%m-%dT%H:%M:%S", "date_format": "%Y-%m-%dT%H:%M:%S",
"timezone": "UTC" "timezone": "UTC",
} }
def _fetch_data(self): 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 = { headers = {
'accept': 'text/plain', 'accept': 'text/plain',
'Authorization': f'Bearer {self.token}', 'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
from_index = 0 print(f"[DEBUG] Fetching data from {start_date} to {end_date}")
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')
while total_count is None or from_index < total_count: # Результаты выполнения
data = { report = {
"from": start_date, "processed_intervals": 0,
"until": end_date, "processed_items": 0,
"pagination": { "errors": [],
"from": from_index,
"count": count_per_request
}
} }
response = requests.post(url, headers=headers, data=json.dumps(data)) # Разделение на временные интервалы
if response.status_code == 200: interval_days = 5 # Например, каждые 5 дней
response_data = response.json() current_start_date = start_date
items = response_data.get("items", [])
all_items.extend(items)
if total_count is None: while current_start_date < end_date:
total_count = response_data.get("count", 0) current_end_date = min(current_start_date + timedelta(days=interval_days), end_date)
from_index += len(items) # Формирование payload
else: payload = {
raise ValueError(f'Shelter API Error: {response.status_code}') "from": current_start_date.strftime('%Y-%m-%dT%H:%M:%SZ'),
"until": current_end_date.strftime('%Y-%m-%dT%H:%M:%SZ'),
return all_items }
print(f"[DEBUG] Sending payload: {json.dumps(payload)}")
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}")
try: 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"], reservation_id=item["id"],
hotel=hotel, hotel=hotel,
defaults={ defaults={
"room_number": item.get("roomNumber", ""), # Номер комнаты "room_number": room_number,
"room_type": item.get("roomTypeName", ""), # Тип комнаты "room_type": item.get("roomTypeName", ""),
"check_in": datetime.strptime(item["from"], '%Y-%m-%dT%H:%M:%S'), # Дата заезда "check_in": check_in,
"check_out": datetime.strptime(item["until"], '%Y-%m-%dT%H:%M:%S'), # Дата выезда "check_out": check_out,
"status": item.get("checkInStatus", ""), # Статус бронирования "status": item.get("checkInStatus", ""),
"price": item.get("reservationPrice", 0), # Цена "price": item.get("reservationPrice", 0),
"discount": item.get("discount", 0), # Скидка "discount": item.get("discount", 0),
} },
) )
if created: print(f"[DEBUG] Updated existing reservation {item['id']}.")
print(f"Создана запись: {reservation}")
else: else:
print(f"Обновлена запись: {reservation}") 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: except Exception as e:
print(f"Ошибка при сохранении бронирования ID {item['id']}: {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 urllib3==2.2.3
user-agents==2.2.0 user-agents==2.2.0
yarl==1.18.3 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) @admin.register(ScheduledTask)
class ScheduledTaskAdmin(admin.ModelAdmin): class ScheduledTaskAdmin(admin.ModelAdmin):
form = ScheduledTaskForm 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",) list_filter = ("active",)
search_fields = ("task_name", "function_path") 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', '192.168.219.114'] 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 # Application definition
@@ -44,7 +48,12 @@ INSTALLED_APPS = [
'pms_integration', 'pms_integration',
'hotels', 'hotels',
'users', 'users',
'scheduler' 'scheduler',
'antifroud',
'health_check',
'health_check.db',
'health_check.cache',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -113,30 +122,46 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# settings.py
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
'handlers': { 'handlers': {
'console': { 'file': {
'class': 'logging.StreamHandler',
},
},
'root': {
'handlers': ['console'],
'level': 'WARNING', 'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': 'import_hotels.log', # Лог будет записываться в этот файл
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'WARNING',
'propagate': True,
},
'antifroud': {
'handlers': ['file'],
'level': 'WARNING',
'propagate': True,
},
}, },
} }
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/ # 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_I18N = True
USE_TZ = True USE_L10N = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
@@ -154,32 +179,33 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
JAZZMIN_SETTINGS = { JAZZMIN_SETTINGS = {
"site_title": "Hotel Management", "use_bootstrap5": True,
"site_header": "Hotel Manager Admin", "site_title": "TOUCHH Hotel Management",
"site_brand": "HotelPro", "site_header": "TOUCHH Hotel Manager Admin",
"welcome_sign": "Welcome to Hotel Management System", "site_brand": "TOUCHH",
"welcome_sign": "Welcome to TOUCHH Hotel Management System",
"show_sidebar": True, "show_sidebar": True,
"navigation_expanded": True, "navigation_expanded": False,
"hide_models": ["users", "guests"], "hide_models": ["auth.Users", "guests"],
"site_logo": None, # Путь к логотипу, например "static/images/logo.png" "site_logo": None, # Путь к логотипу, например "static/images/logo.png"
"site_logo_classes": "img-circle", # Классы CSS для логотипа "site_logo_classes": "img-circle", # Классы CSS для логотипа
"site_icon": None, # Иконка сайта (favicon), например "static/images/favicon.ico" "site_icon": None, # Иконка сайта (favicon), например "static/images/favicon.ico"
"welcome_sign": "Welcome to Touchh Admin", # Приветствие на странице входа "welcome_sign": "Welcome to Touchh Admin", # Приветствие на странице входа
"copyright": "Touchh © 2024", # Кастомный текст в футере "copyright": "Touchh", # Кастомный текст в футере
"search_model": "auth.User", # Модель для строки поиска
"icons": { "icons": {
"auth": "fas fa-users-cog", "auth": "fas fa-users-cog",
"users": "fas fa-user-circle", "users": "fas fa-user-circle",
"hotels": "fas fa-hotel", "hotels": "fas fa-hotel",
}, },
"theme": "flatly", "theme": "sandstone",
"dark_mode_theme": "cyborg", "dark_mode_theme": "darkly",
"footer": { "footer": {
"copyright": "Touchh © 2024", "copyright": "SmartSolTech.kr © 2024",
"version": False, "version": False,
}, },
"dashboard_links": [ "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": { # Кастомные ссылки в боковом меню "custom_links": { # Кастомные ссылки в боковом меню

View File

@@ -1,6 +1,12 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from antifroud import views
app_name = 'touchh'
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), 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 django.contrib import admin
from .models import User, UserConfirmation, UserActivityLog, NotificationSettings from .models import User, NotificationSettings
@admin.register(User) @admin.register(User)
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
@@ -8,18 +8,6 @@ class UserAdmin(admin.ModelAdmin):
list_filter = ('role', 'confirmed') list_filter = ('role', 'confirmed')
ordering = ('-id',) 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) @admin.register(NotificationSettings)
class NotificationSettingsAdmin(admin.ModelAdmin): class NotificationSettingsAdmin(admin.ModelAdmin):

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,72 +47,6 @@ class User(AbstractUser):
verbose_name_plural = "Пользователи" 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): class NotificationSettings(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="Пользователь") user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="Пользователь")