Module antifroud added. Custom Admin template is working

This commit is contained in:
2024-12-12 22:46:19 +09:00
parent 85b4c809dd
commit 93964af71a
12 changed files with 635 additions and 6 deletions

0
antifroud/__init__.py Normal file
View File

137
antifroud/admin.py Normal file
View 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
View File

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

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

123
antifroud/models.py Normal file
View 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)

View File

@@ -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
View File

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

3
antifroud/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -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": { # Кастомные ссылки в боковом меню