Module antifroud added. Custom Admin template is working
This commit is contained in:
0
antifroud/__init__.py
Normal file
0
antifroud/__init__.py
Normal file
137
antifroud/admin.py
Normal file
137
antifroud/admin.py
Normal file
@@ -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",)
|
||||||
6
antifroud/apps.py
Normal file
6
antifroud/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AntifroudConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'antifroud'
|
||||||
86
antifroud/migrations/0001_initial.py
Normal file
86
antifroud/migrations/0001_initial.py
Normal 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': 'Несовпадения в заселении',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
antifroud/migrations/__init__.py
Normal file
0
antifroud/migrations/__init__.py
Normal file
123
antifroud/models.py
Normal file
123
antifroud/models.py
Normal file
@@ -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)
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#table-data-preview table {
|
||||||
|
width: 100%; /* Установить ширину таблицы */
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-data-preview thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: #f8f9fa; /* Цвет фона заголовка */
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-data-preview tbody {
|
||||||
|
display: block;
|
||||||
|
max-height: 200px; /* Ограничить высоту предпросмотра */
|
||||||
|
overflow-y: auto; /* Добавить вертикальную прокрутку */
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-data-preview tr {
|
||||||
|
height: 40px; /* Установить фиксированную высоту строки */
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-data-preview td, #table-data-preview th {
|
||||||
|
white-space: nowrap; /* Обрезать текст вместо переноса */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis; /* Добавить многоточие для длинного текста */
|
||||||
|
}
|
||||||
|
</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" style="table-layout: fixed;">
|
||||||
|
<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") {
|
||||||
|
// 1. Отобразить заголовки
|
||||||
|
const headerRow = data.columns.map(col => `<th>${col}</th>`).join("");
|
||||||
|
document.getElementById("table-header").innerHTML = `<tr>${headerRow}</tr>`;
|
||||||
|
|
||||||
|
// 2. Отобразить строки данных
|
||||||
|
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 %}
|
||||||
3
antifroud/tests.py
Normal file
3
antifroud/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
antifroud/views.py
Normal file
3
antifroud/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
@@ -44,7 +44,8 @@ INSTALLED_APPS = [
|
|||||||
'pms_integration',
|
'pms_integration',
|
||||||
'hotels',
|
'hotels',
|
||||||
'users',
|
'users',
|
||||||
'scheduler'
|
'scheduler',
|
||||||
|
'antifroud'
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -154,10 +155,10 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||||||
|
|
||||||
|
|
||||||
JAZZMIN_SETTINGS = {
|
JAZZMIN_SETTINGS = {
|
||||||
"site_title": "Hotel Management",
|
"site_title": "TOUCHH Hotel Management",
|
||||||
"site_header": "Hotel Manager Admin",
|
"site_header": "TOUCHH Hotel Manager Admin",
|
||||||
"site_brand": "HotelPro",
|
"site_brand": "TOUCHH",
|
||||||
"welcome_sign": "Welcome to Hotel Management System",
|
"welcome_sign": "Welcome to TOUCHH Hotel Management System",
|
||||||
"show_sidebar": True,
|
"show_sidebar": True,
|
||||||
"navigation_expanded": True,
|
"navigation_expanded": True,
|
||||||
"hide_models": ["users", "guests"],
|
"hide_models": ["users", "guests"],
|
||||||
@@ -179,7 +180,8 @@ JAZZMIN_SETTINGS = {
|
|||||||
"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": { # Кастомные ссылки в боковом меню
|
||||||
|
|||||||
Reference in New Issue
Block a user