hotel import template
This commit is contained in:
@@ -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
243
antifroud/data_sync.py
Normal 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
9
antifroud/forms.py
Normal 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
|
||||||
|
)
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
24
antifroud/migrations/0005_importedhotel.py
Normal file
24
antifroud/migrations/0005_importedhotel.py
Normal 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='Импортирован в основную базу')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
17
antifroud/migrations/0006_alter_importedhotel_options.py
Normal file
17
antifroud/migrations/0006_alter_importedhotel_options.py
Normal 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': 'Импортированные отели'},
|
||||||
|
),
|
||||||
|
]
|
||||||
18
antifroud/migrations/0007_useractivitylog_external_id.py
Normal file
18
antifroud/migrations/0007_useractivitylog_external_id.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
antifroud/migrations/0008_alter_useractivitylog_id.py
Normal file
18
antifroud/migrations/0008_alter_useractivitylog_id.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
antifroud/migrations/0009_importedhotel_display_name.py
Normal file
18
antifroud/migrations/0009_importedhotel_display_name.py
Normal 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='Отображаемое имя'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
97
antifroud/templates/antifroud/admin/import_hotels.html
Normal file
97
antifroud/templates/antifroud/admin/import_hotels.html
Normal 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 %}
|
||||||
@@ -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 %}
|
||||||
143
antifroud/templates/antifroud/admin/imported_hotels.html
Normal file
143
antifroud/templates/antifroud/admin/imported_hotels.html
Normal 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
10
antifroud/urls.py
Normal 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-адреса
|
||||||
|
]
|
||||||
@@ -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})
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
4302
import_hotels.log
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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
0
static/css/styles.css
Normal 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": [
|
||||||
|
|||||||
@@ -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')),
|
||||||
|
|
||||||
]
|
]
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user