hotel import template

This commit is contained in:
2024-12-13 22:25:11 +09:00
parent 93964af71a
commit 3c6f849b78
28 changed files with 10049 additions and 178 deletions

View File

@@ -1,47 +1,44 @@
from django.contrib import admin from django.contrib import admin
from .models import UserActivityLog, ExternalDBSettings, RoomDiscrepancy
from django.urls import path from django.urls import path
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import render from django.shortcuts import redirect, get_object_or_404
from .models import ExternalDBSettings 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 pymysql
from django.shortcuts import redirect import logging
from django.urls import reverse from django.urls import reverse
logger = logging.getLogger(__name__)
@admin.register(ExternalDBSettings) @admin.register(ExternalDBSettings)
class ExternalDBSettingsAdmin(admin.ModelAdmin): class ExternalDBSettingsAdmin(admin.ModelAdmin):
change_form_template = "antifroud/admin/external_db_settings_change_form.html" 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): def add_view(self, request, form_url='', extra_context=None):
# Создаем новую запись
new_instance = ExternalDBSettings.objects.create( new_instance = ExternalDBSettings.objects.create(
name="Новая настройка", # Задайте значение по умолчанию name="Новая настройка", # Значение по умолчанию
host="", host="",
port=3306, port=3306,
user="", user="",
password="", password="",
is_active=False is_active=False
) )
# Перенаправляем пользователя на страницу редактирования новой записи
return redirect(reverse('admin:antifroud_externaldbsettings_change', args=(new_instance.id,))) return redirect(reverse('admin:antifroud_externaldbsettings_change', args=(new_instance.id,)))
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
custom_urls = [ custom_urls = [
path( path('test-connection/', self.admin_site.admin_view(self.test_connection), name='test_connection'),
'test-connection/', path('fetch-tables/', self.admin_site.admin_view(self.fetch_tables), name='fetch_tables'),
self.admin_site.admin_view(self.test_connection), path('fetch-table-data/', self.admin_site.admin_view(self.fetch_table_data), name='fetch_table_data'),
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 return custom_urls + urls
@@ -50,15 +47,10 @@ class ExternalDBSettingsAdmin(admin.ModelAdmin):
if not db_id: if not db_id:
return JsonResponse({"status": "error", "message": "ID подключения отсутствует."}, status=400) return JsonResponse({"status": "error", "message": "ID подключения отсутствует."}, status=400)
try: try:
# Получаем объект настроек подключения
db_settings = ExternalDBSettings.objects.get(id=db_id) db_settings = ExternalDBSettings.objects.get(id=db_id)
# Проверяем, что все необходимые поля заполнены
if not db_settings.user or not db_settings.password: if not db_settings.user or not db_settings.password:
return JsonResponse({"status": "error", "message": "Имя пользователя или пароль не указаны."}, status=400) return JsonResponse({"status": "error", "message": "Имя пользователя или пароль не указаны."}, status=400)
# Проверяем подключение к базе данных
import pymysql
connection = pymysql.connect( connection = pymysql.connect(
host=db_settings.host, host=db_settings.host,
port=db_settings.port, port=db_settings.port,
@@ -75,9 +67,7 @@ class ExternalDBSettingsAdmin(admin.ModelAdmin):
except Exception as e: except Exception as e:
return JsonResponse({"status": "error", "message": f"Неизвестная ошибка: {str(e)}"}, status=500) return JsonResponse({"status": "error", "message": f"Неизвестная ошибка: {str(e)}"}, status=500)
def fetch_tables(self, request): def fetch_tables(self, request):
"""Возвращает список таблиц в базе данных."""
try: try:
db_id = request.GET.get('db_id') db_id = request.GET.get('db_id')
db_settings = ExternalDBSettings.objects.get(id=db_id) db_settings = ExternalDBSettings.objects.get(id=db_id)
@@ -97,7 +87,6 @@ class ExternalDBSettingsAdmin(admin.ModelAdmin):
return JsonResponse({"status": "error", "message": str(e)}) return JsonResponse({"status": "error", "message": str(e)})
def fetch_table_data(self, request): def fetch_table_data(self, request):
"""Возвращает первые 10 записей из выбранной таблицы."""
try: try:
db_id = request.GET.get('db_id') db_id = request.GET.get('db_id')
table_name = request.GET.get('table_name') table_name = request.GET.get('table_name')
@@ -118,20 +107,147 @@ class ExternalDBSettingsAdmin(admin.ModelAdmin):
except Exception as e: except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}) return JsonResponse({"status": "error", "message": str(e)})
@admin.register(UserActivityLog) @admin.register(UserActivityLog)
class UserActivityLogAdmin(admin.ModelAdmin): class UserActivityLogAdmin(admin.ModelAdmin):
list_display = ("id", "user_id", "ip", "created", "page_title", "type", "hits") list_display = ("id", "timestamp", "date_time", "page_id", "url_parameters", "created", "page_title", "type", "hits")
search_fields = ("user_id", "ip", "page_title") search_fields = ("page_title", "url_parameters")
list_filter = ("type", "created") list_filter = ("type", "created")
readonly_fields = ("created", "timestamp") readonly_fields = ("created", "timestamp")
@admin.register(RoomDiscrepancy) @admin.register(RoomDiscrepancy)
class RoomDiscrepancyAdmin(admin.ModelAdmin): class RoomDiscrepancyAdmin(admin.ModelAdmin):
list_display = ("hotel", "room_number", "booking_id", "check_in_date_expected", "check_in_date_actual", "discrepancy_type", "created_at") 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") search_fields = ("hotel__name", "room_number", "booking_id")
list_filter = ("discrepancy_type", "created_at") list_filter = ("discrepancy_type", "created_at")
readonly_fields = ("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')

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,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

@@ -3,7 +3,7 @@ from hotels.models import Hotel, Reservation
class UserActivityLog(models.Model): class UserActivityLog(models.Model):
id = models.BigIntegerField(primary_key=True) external_id = models.CharField(max_length=255, null=True, blank=True)
user_id = models.BigIntegerField(verbose_name="ID пользователя") user_id = models.BigIntegerField(verbose_name="ID пользователя")
ip = models.GenericIPAddressField(verbose_name="IP-адрес") ip = models.GenericIPAddressField(verbose_name="IP-адрес")
created = models.DateTimeField(verbose_name="Дата создания") created = models.DateTimeField(verbose_name="Дата создания")
@@ -41,8 +41,8 @@ class ExternalDBSettings(models.Model):
port = models.PositiveIntegerField(default=3306, help_text="Порт сервера базы данных.") port = models.PositiveIntegerField(default=3306, help_text="Порт сервера базы данных.")
user = models.CharField(max_length=255, help_text="Имя пользователя базы данных.") user = models.CharField(max_length=255, help_text="Имя пользователя базы данных.")
password = models.CharField(max_length=255, help_text="Пароль для подключения.") password = models.CharField(max_length=255, help_text="Пароль для подключения.")
database = models.CharField(max_length=255, default="", help_text="Имя базы данных.") database = models.CharField(max_length=255, default="u1510415_wp832", help_text="Имя базы данных.")
table_name = models.CharField(max_length=255, blank=True, null=True, 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="Список полей для загрузки (через запятую).") selected_fields = models.TextField(blank=True, null=True, help_text="Список полей для загрузки (через запятую).")
is_active = models.BooleanField(default=True, help_text="Флаг активности подключения.") is_active = models.BooleanField(default=True, help_text="Флаг активности подключения.")
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
@@ -52,7 +52,7 @@ class ExternalDBSettings(models.Model):
return f"{self.name} ({self.host}:{self.port})" return f"{self.name} ({self.host}:{self.port})"
class Meta: class Meta:
verbose_name = "Настройки подключения к БД" verbose_name = "Настройка подключения к БД"
verbose_name_plural = "Настройки подключений к БД" verbose_name_plural = "Настройки подключений к БД"
@@ -121,3 +121,33 @@ class RoomDiscrepancy(models.Model):
)) ))
RoomDiscrepancy.objects.bulk_create(discrepancies) 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

@@ -3,30 +3,31 @@
{% block content %} {% block content %}
<style> <style>
#table-data-preview {
max-height: 300px; /* Ограничиваем высоту предпросмотра */
overflow-y: auto; /* Прокрутка по вертикали */
overflow-x: auto; /* Прокрутка по горизонтали */
}
#table-data-preview table { #table-data-preview table {
width: 100%; /* Установить ширину таблицы */ width: auto; /* Автоматическая ширина таблицы */
table-layout: auto; /* Автоматическая ширина колонок */
} }
#table-data-preview thead { #table-data-preview th,
position: sticky; #table-data-preview td {
top: 0; white-space: nowrap; /* Предотвращаем перенос текста */
background-color: #f8f9fa; /* Цвет фона заголовка */ overflow: hidden; /* Скрываем текст, выходящий за границы ячейки */
text-overflow: ellipsis; /* Добавляем многоточие для обрезанного текста */
padding: 8px; /* Внутренний отступ */
height: 40px; /* Фиксированная высота строк */
} }
#table-data-preview tbody { #table-data-preview th {
display: block; position: sticky; /* Фиксируем заголовки при прокрутке */
max-height: 200px; /* Ограничить высоту предпросмотра */ top: 0; /* Располагаем заголовки вверху таблицы */
overflow-y: auto; /* Добавить вертикальную прокрутку */ background-color: #f8f9fa; /* Цвет фона заголовков */
} z-index: 1; /* Заголовки перекрывают содержимое */
#table-data-preview tr {
height: 40px; /* Установить фиксированную высоту строки */
}
#table-data-preview td, #table-data-preview th {
white-space: nowrap; /* Обрезать текст вместо переноса */
overflow: hidden;
text-overflow: ellipsis; /* Добавить многоточие для длинного текста */
} }
</style> </style>
@@ -71,14 +72,12 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="table-data-preview">Столбцы и данные</label> <label for="table-data-preview">Столбцы и данные</label>
<div id="table-data-preview" class="table-responsive"> <div id="table-data-preview" class="table-responsive">
<table class="table table-bordered" style="table-layout: fixed;"> <table class="table table-bordered">
<thead id="table-header"></thead> <thead id="table-header"></thead>
<tbody id="table-body"></tbody> <tbody id="table-body"></tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="is-active">Активное подключение</label> <label for="is-active">Активное подключение</label>
<input id="is-active" class="form-check-input" type="checkbox" name="is_active" {% if original.is_active %}checked{% endif %} /> <input id="is-active" class="form-check-input" type="checkbox" name="is_active" {% if original.is_active %}checked{% endif %} />
@@ -124,7 +123,6 @@
.then(data => { .then(data => {
if (data.status === "success") { if (data.status === "success") {
document.getElementById("connection-status").innerHTML = `<div class="alert alert-success">${data.message}</div>`; document.getElementById("connection-status").innerHTML = `<div class="alert alert-success">${data.message}</div>`;
// Загрузить таблицы
fetch(`/admin/antifroud/externaldbsettings/fetch-tables/?db_id=${dbId}`) fetch(`/admin/antifroud/externaldbsettings/fetch-tables/?db_id=${dbId}`)
.then(response => response.json()) .then(response => response.json())
.then(tableData => { .then(tableData => {
@@ -153,16 +151,14 @@
document.getElementById("table-body").innerHTML = ""; document.getElementById("table-body").innerHTML = "";
return; return;
} }
fetch(`/admin/antifroud/externaldbsettings/fetch-table-data/?db_id=${dbId}&table_name=${tableName}`) fetch(`/admin/antifroud/externaldbsettings/fetch-table-data/?db_id=${dbId}&table_name=${tableName}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.status === "success") { if (data.status === "success") {
// 1. Отобразить заголовки
const headerRow = data.columns.map(col => `<th>${col}</th>`).join(""); const headerRow = data.columns.map(col => `<th>${col}</th>`).join("");
document.getElementById("table-header").innerHTML = `<tr>${headerRow}</tr>`; document.getElementById("table-header").innerHTML = `<tr>${headerRow}</tr>`;
// 2. Отобразить строки данных
const rows = data.rows.map(row => { const rows = data.rows.map(row => {
const cells = row.map(cell => `<td>${cell}</td>`).join(""); const cells = row.map(cell => `<td>${cell}</td>`).join("");
return `<tr>${cells}</tr>`; return `<tr>${cells}</tr>`;
@@ -177,8 +173,5 @@
console.error(error); console.error(error);
}); });
}); });
</script> </script>
{% endblock %} {% 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 %}

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-адреса
]

View File

@@ -1,3 +1,110 @@
import logging
from django.http import JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from .models import ImportedHotel
from hotels.models import Hotel
# Create your views here. 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

@@ -51,13 +51,6 @@ class HotelAdmin(admin.ModelAdmin):
except Exception as e: except Exception as e:
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):
@@ -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,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='Импортированный отель'),
),
]

4302
import_hotels.log Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

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

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
@@ -45,7 +49,11 @@ INSTALLED_APPS = [
'hotels', 'hotels',
'users', 'users',
'scheduler', 'scheduler',
'antifroud' 'antifroud',
'health_check',
'health_check.db',
'health_check.cache',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -114,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', 'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': 'import_hotels.log', # Лог будет записываться в этот файл
}, },
}, },
'root': { 'loggers': {
'handlers': ['console'], 'django': {
'level': 'WARNING', '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)
@@ -155,28 +179,28 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
JAZZMIN_SETTINGS = { JAZZMIN_SETTINGS = {
"use_bootstrap5": True,
"site_title": "TOUCHH Hotel Management", "site_title": "TOUCHH Hotel Management",
"site_header": "TOUCHH Hotel Manager Admin", "site_header": "TOUCHH Hotel Manager Admin",
"site_brand": "TOUCHH", "site_brand": "TOUCHH",
"welcome_sign": "Welcome to TOUCHH Hotel Management System", "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": [

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,19 +8,7 @@ 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):
list_display = ('user', 'email', 'telegram_enabled', 'email_enabled', 'notification_time') list_display = ('user', 'email', 'telegram_enabled', 'email_enabled', 'notification_time')

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.4 on 2024-12-13 00:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='LocalUserActivityLog',
),
migrations.DeleteModel(
name='UserActivityLog',
),
migrations.DeleteModel(
name='UserConfirmation',
),
]

View File

@@ -47,73 +47,7 @@ class User(AbstractUser):
verbose_name_plural = "Пользователи" 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="Пользователь")
telegram_enabled = models.BooleanField(default=True, verbose_name="Уведомления в Telegram") telegram_enabled = models.BooleanField(default=True, verbose_name="Уведомления в Telegram")