diff --git a/antifroud/__init__.py b/antifroud/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/antifroud/admin.py b/antifroud/admin.py new file mode 100644 index 00000000..670ded7f --- /dev/null +++ b/antifroud/admin.py @@ -0,0 +1,137 @@ +from django.contrib import admin +from .models import UserActivityLog, ExternalDBSettings, RoomDiscrepancy +from django.urls import path +from django.http import JsonResponse +from django.shortcuts import render +from .models import ExternalDBSettings +import pymysql +from django.shortcuts import redirect +from django.urls import reverse + +@admin.register(ExternalDBSettings) +class ExternalDBSettingsAdmin(admin.ModelAdmin): + change_form_template = "antifroud/admin/external_db_settings_change_form.html" + 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) + + # Проверяем подключение к базе данных + import pymysql + 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): + """Возвращает первые 10 записей из выбранной таблицы.""" + 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", "user_id", "ip", "created", "page_title", "type", "hits") + search_fields = ("user_id", "ip", "page_title") + 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",) diff --git a/antifroud/apps.py b/antifroud/apps.py new file mode 100644 index 00000000..e051ceac --- /dev/null +++ b/antifroud/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AntifroudConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'antifroud' diff --git a/antifroud/migrations/0001_initial.py b/antifroud/migrations/0001_initial.py new file mode 100644 index 00000000..2ee9be3e --- /dev/null +++ b/antifroud/migrations/0001_initial.py @@ -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': 'Несовпадения в заселении', + }, + ), + ] diff --git a/antifroud/migrations/0002_remove_externaldbsettings_database_and_more.py b/antifroud/migrations/0002_remove_externaldbsettings_database_and_more.py new file mode 100644 index 00000000..9f0c28cd --- /dev/null +++ b/antifroud/migrations/0002_remove_externaldbsettings_database_and_more.py @@ -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), + ), + ] diff --git a/antifroud/migrations/0003_externaldbsettings_database_and_more.py b/antifroud/migrations/0003_externaldbsettings_database_and_more.py new file mode 100644 index 00000000..76732d77 --- /dev/null +++ b/antifroud/migrations/0003_externaldbsettings_database_and_more.py @@ -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), + ), + ] diff --git a/antifroud/migrations/__init__.py b/antifroud/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/antifroud/models.py b/antifroud/models.py new file mode 100644 index 00000000..4de3809f --- /dev/null +++ b/antifroud/models.py @@ -0,0 +1,123 @@ +from django.db import models +from hotels.models import Hotel, Reservation + + +class UserActivityLog(models.Model): + id = models.BigIntegerField(primary_key=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="", help_text="Имя базы данных.") + table_name = models.CharField(max_length=255, blank=True, 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) diff --git a/antifroud/templates/antifroud/admin/external_db_settings_change_form.html b/antifroud/templates/antifroud/admin/external_db_settings_change_form.html new file mode 100644 index 00000000..871d6dc9 --- /dev/null +++ b/antifroud/templates/antifroud/admin/external_db_settings_change_form.html @@ -0,0 +1,184 @@ +{% extends "admin/base_site.html" %} + +{% block content %} + + + +
+

Настройки подключения к БД

+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+
+ + +
+ + +
+
+ + +
+
+
+
+
+ +
+
+ +{% if original.id %} + +{% else %} + +{% endif %} + + +{% endblock %} diff --git a/antifroud/tests.py b/antifroud/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/antifroud/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/antifroud/views.py b/antifroud/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/antifroud/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/touchh/settings.py b/touchh/settings.py index b7151258..bd61c3b0 100644 --- a/touchh/settings.py +++ b/touchh/settings.py @@ -44,7 +44,8 @@ INSTALLED_APPS = [ 'pms_integration', 'hotels', 'users', - 'scheduler' + 'scheduler', + 'antifroud' ] MIDDLEWARE = [ @@ -154,10 +155,10 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' JAZZMIN_SETTINGS = { - "site_title": "Hotel Management", - "site_header": "Hotel Manager Admin", - "site_brand": "HotelPro", - "welcome_sign": "Welcome to Hotel Management System", + "site_title": "TOUCHH Hotel Management", + "site_header": "TOUCHH Hotel Manager Admin", + "site_brand": "TOUCHH", + "welcome_sign": "Welcome to TOUCHH Hotel Management System", "show_sidebar": True, "navigation_expanded": True, "hide_models": ["users", "guests"], @@ -179,7 +180,8 @@ JAZZMIN_SETTINGS = { "version": False, }, "dashboard_links": [ - {"name": "Google", "url": "https://google.com", "new_window": True}, + {"name": "Google", "url": "https://touchh.com", "new_window": True}, + {"name": "Smartsoltech", "url": "https://smartsoltech.kr", "new_window": True} ], "custom_links": { # Кастомные ссылки в боковом меню