settings application
.env db params+global settings in admin model ECVI plugin module
This commit is contained in:
@@ -4,7 +4,7 @@ from django.http import JsonResponse
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from antifroud.models import UserActivityLog, ExternalDBSettings, RoomDiscrepancy, ImportedHotel
|
||||
from antifroud.models import UserActivityLog, ExternalDBSettings, RoomDiscrepancy, ImportedHotel, SyncLog
|
||||
from hotels.models import Hotel
|
||||
import pymysql
|
||||
import logging
|
||||
@@ -124,84 +124,6 @@ class RoomDiscrepancyAdmin(admin.ModelAdmin):
|
||||
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)
|
||||
@@ -250,4 +172,16 @@ class ImportedHotelAdmin(admin.ModelAdmin):
|
||||
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')
|
||||
return redirect('admin:antifroud_importedhotel_changelist')
|
||||
|
||||
|
||||
@admin.register(SyncLog)
|
||||
class SyncLogAdmin(admin.ModelAdmin):
|
||||
change_list_template = "antifroud/admin/sync_log.html"
|
||||
list_display =['id', 'hotel', 'recieved_records', 'processed_records']
|
||||
search_fields = ['id', 'hotel', 'received_records', 'processed_records']
|
||||
list_filter = ['id', 'hotel', 'processed_records']
|
||||
|
||||
class Meta:
|
||||
model = SyncLog
|
||||
fields = ['hotel', 'received_records', 'processed_records']
|
||||
@@ -1,11 +1,15 @@
|
||||
import logging
|
||||
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
|
||||
import pytz
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
from django.conf import settings
|
||||
from html import unescape
|
||||
import chardet
|
||||
import html
|
||||
from .models import SyncLog, ExternalDBSettings, UserActivityLog, RoomDiscrepancy, ImportedHotel
|
||||
from hotels.models import Reservation, Hotel
|
||||
|
||||
class DataSyncManager:
|
||||
@@ -13,54 +17,120 @@ class DataSyncManager:
|
||||
Класс для управления загрузкой, записью и сверкой данных.
|
||||
"""
|
||||
|
||||
def __init__(self, db_settings_id):
|
||||
def __init__(self, db_settings_id, use_local_db=False):
|
||||
self.db_settings_id = db_settings_id
|
||||
self.use_local_db = use_local_db # Если True, используем локальную БД
|
||||
self.db_settings = None
|
||||
self.connection = None
|
||||
self.table_name = None
|
||||
|
||||
# Настройка логирования
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
handler = logging.FileHandler('data_sync.log')
|
||||
handler.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
self.logger.addHandler(handler)
|
||||
|
||||
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
|
||||
)
|
||||
if self.use_local_db:
|
||||
# Подключаемся к локальной базе данных
|
||||
self.db_settings = settings.LocalDataBase.objects.first() # Получаем настройки первой базы
|
||||
self.table_name = self.db_settings.database
|
||||
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,
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
else:
|
||||
# Подключаемся к внешней базе данных
|
||||
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,
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
|
||||
except ExternalDBSettings.DoesNotExist:
|
||||
raise ValueError("Настройки подключения не найдены.")
|
||||
except pymysql.MySQLError as e:
|
||||
raise ConnectionError(f"Ошибка подключения к базе данных: {e}")
|
||||
|
||||
def fetch_data(self, limit=100):
|
||||
def get_last_saved_record(self):
|
||||
"""
|
||||
Загружает данные из указанной таблицы.
|
||||
Получает последнюю запись из таблицы UserActivityLog.
|
||||
"""
|
||||
last_record = UserActivityLog.objects.order_by('-id').first()
|
||||
if last_record:
|
||||
self.logger.info(f"Последняя запись в UserActivityLog: ID={last_record.id}")
|
||||
return last_record.id
|
||||
self.logger.info("Таблица UserActivityLog пуста.")
|
||||
return None
|
||||
|
||||
def fetch_new_data(self, last_id=0, limit=100):
|
||||
"""
|
||||
Загружает новые записи из указанной таблицы, которые идут после last_id.
|
||||
"""
|
||||
if not self.connection:
|
||||
self.connect_to_db()
|
||||
|
||||
cursor = self.connection.cursor()
|
||||
cursor = self.connection.cursor(pymysql.cursors.DictCursor) # Используем DictCursor для получения словарей
|
||||
try:
|
||||
cursor.execute(f"SELECT * FROM `{self.table_name}` LIMIT {limit};")
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
# Формируем SQL-запрос
|
||||
if last_id:
|
||||
query = f"""
|
||||
SELECT * FROM `{self.table_name}`
|
||||
WHERE id > {last_id} AND url_parameters IS NOT NULL AND url_parameters != ''
|
||||
ORDER BY id ASC
|
||||
LIMIT {limit};
|
||||
"""
|
||||
else:
|
||||
query = f"""
|
||||
SELECT * FROM `{self.table_name}`
|
||||
WHERE url_parameters IS NOT NULL AND url_parameters != ''
|
||||
ORDER BY id ASC
|
||||
LIMIT {limit};
|
||||
"""
|
||||
|
||||
self.logger.info(f"Выполняется запрос: {query}")
|
||||
cursor.execute(query)
|
||||
|
||||
# Получаем результаты
|
||||
rows = cursor.fetchall()
|
||||
return {"columns": columns, "rows": rows}
|
||||
if not rows:
|
||||
self.logger.info("Нет данных для загрузки.")
|
||||
return {"columns": [], "rows": []}
|
||||
|
||||
# Получаем названия колонок
|
||||
columns = rows[0].keys() if rows else []
|
||||
return {"columns": list(columns), "rows": rows}
|
||||
|
||||
except pymysql.MySQLError as e:
|
||||
raise RuntimeError(f"Ошибка выполнения запроса: {e}")
|
||||
self.logger.error(f"Ошибка выполнения запроса: {e}")
|
||||
return {"columns": [], "rows": []}
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def parse_datetime(self, dt_str):
|
||||
|
||||
def parse_datetime(self, dt_str, hotel_timezone=None):
|
||||
"""
|
||||
Преобразует строку формата 'YYYY-MM-DD HH:MM:SS' или 'YYYY-MM-DDTHH:MM:SS' в aware datetime.
|
||||
Преобразует время в часовой пояс отеля и корректирует на часовой пояс сервера.
|
||||
"""
|
||||
if dt_str is None:
|
||||
return None
|
||||
@@ -72,8 +142,22 @@ class DataSyncManager:
|
||||
|
||||
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
|
||||
try:
|
||||
# Преобразуем строку в naive datetime
|
||||
naive_dt = datetime.strptime(dt_str, fmt)
|
||||
|
||||
# Если передан часовой пояс отеля
|
||||
if hotel_timezone:
|
||||
# Переводим время из часового пояса отеля в UTC (если данные из PMS уже UTC+X)
|
||||
tz = pytz.timezone(hotel_timezone) # Часовой пояс отеля
|
||||
aware_dt = tz.localize(naive_dt) # Локализуем в часовой пояс отеля
|
||||
|
||||
# Теперь приводим время к серверному часовому поясу (например, Московское время)
|
||||
server_tz = timezone.get_default_timezone() # Например, Moscow (UTC+3)
|
||||
return aware_dt.astimezone(server_tz) # Переводим в серверное время
|
||||
|
||||
# Если часовой пояс отеля не передан, используем серверный часовой пояс по умолчанию
|
||||
return timezone.make_aware(naive_dt, timezone.get_default_timezone())
|
||||
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
@@ -81,95 +165,168 @@ class DataSyncManager:
|
||||
|
||||
def decode_html_entities(self, text):
|
||||
"""
|
||||
Раскодирует HTML-сущности в строке.
|
||||
Декодирует URL и HTML-сущности в строке.
|
||||
Пытается автоматически декодировать строку в правильную кодировку.
|
||||
"""
|
||||
if text and isinstance(text, str):
|
||||
return unescape(text)
|
||||
text = unquote(text) # Декодируем URL
|
||||
text = html.unescape(text) # Расшифровываем HTML сущности
|
||||
|
||||
# Попробуем определить кодировку и привести строку к utf-8
|
||||
try:
|
||||
detected = chardet.detect(text.encode())
|
||||
encoding = detected['encoding']
|
||||
if encoding and encoding != 'utf-8':
|
||||
text = text.encode(encoding).decode('utf-8', errors='ignore')
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при обработке кодировки: {e}")
|
||||
|
||||
return text
|
||||
return text
|
||||
|
||||
def process_url_parameters(self, url_params):
|
||||
"""
|
||||
Парсит url_parameters, извлекает utm_content (имя отеля) и utm_term (ID отеля).
|
||||
Парсит url_parameters и возвращает hotel_name и hotel_id.
|
||||
"""
|
||||
if not url_params:
|
||||
# Проверка корректности данных
|
||||
if not url_params or not isinstance(url_params, str):
|
||||
self.logger.error(f"Ошибка: некорректные url_parameters: {url_params}")
|
||||
return {}
|
||||
|
||||
# Декодируем параметры URL
|
||||
decoded = unquote(url_params)
|
||||
qs = parse_qs(decoded)
|
||||
hotel_name = qs.get('utm_content', [None])[0]
|
||||
hotel_id = qs.get('utm_term', [None])[0]
|
||||
|
||||
# Извлекаем hotel_name и hotel_id_term
|
||||
hotel_name = qs.get('utm_content', [None])[0] # Умолчание: None, если параметр не найден
|
||||
hotel_id_term = qs.get('utm_term', [None])[0] # Умолчание: None, если параметр не найден
|
||||
|
||||
# Формируем hotel_id
|
||||
hotel_id = f"{hotel_name}_{hotel_id_term}" if hotel_name and hotel_id_term else None
|
||||
|
||||
# Логирование для отладки
|
||||
self.logger.debug(f"Извлечено из url_parameters: hotel_name={hotel_name}, hotel_id_term={hotel_id_term}, hotel_id={hotel_id}")
|
||||
|
||||
# Возврат результата
|
||||
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
|
||||
"""
|
||||
Проверяет, есть ли отель с данным ID в таблице ImportedHotel.
|
||||
Если отеля с таким external_id нет, добавляет новый в таблицы ImportedHotel и Hotel.
|
||||
"""
|
||||
if not hotel_id or not hotel_name:
|
||||
return None
|
||||
|
||||
# Проверим, что 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
|
||||
# Генерация external_id в формате 'hotel_name_hotel_id'
|
||||
external_id = f"{hotel_name}_{hotel_id}"
|
||||
|
||||
if not hotel_exists:
|
||||
ImportedHotel.objects.update_or_create(
|
||||
external_id=str(hotel_id),
|
||||
defaults={
|
||||
'name': hotel_name
|
||||
}
|
||||
)
|
||||
# Проверяем, существует ли запись с таким external_id в ImportedHotel
|
||||
existing_hotel = ImportedHotel.objects.filter(external_id=external_id).first()
|
||||
|
||||
if existing_hotel:
|
||||
self.logger.info(f"Отель с external_id {external_id} уже существует в ImportedHotel.")
|
||||
else:
|
||||
try:
|
||||
# Создаем новую запись в ImportedHotel
|
||||
with transaction.atomic():
|
||||
imported_hotel = ImportedHotel.objects.create(
|
||||
external_id=external_id,
|
||||
name=hotel_name,
|
||||
display_name=hotel_name,
|
||||
imported=True # Отмечаем, что отель импортирован
|
||||
)
|
||||
self.logger.info(f"Отель с external_id {external_id} добавлен в ImportedHotel.")
|
||||
|
||||
# Создаем новый отель в основной таблице Hotel
|
||||
hotel = Hotel.objects.create(
|
||||
hotel_id=external_id,
|
||||
name=hotel_name,
|
||||
phone=None,
|
||||
email=None,
|
||||
address=None,
|
||||
city=None,
|
||||
timezone="UTC",
|
||||
description="Автоматически импортированный отель",
|
||||
|
||||
)
|
||||
self.logger.info(f"Отель с hotel_id {external_id} добавлен в Hotel с флагом is_imported=True.")
|
||||
return hotel
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при добавлении отеля {hotel_name} с external_id {external_id}: {e}")
|
||||
return None
|
||||
|
||||
return existing_hotel
|
||||
|
||||
@transaction.atomic
|
||||
def write_to_db(self, data):
|
||||
"""
|
||||
Записывает данные в UserActivityLog и при необходимости в ImportedHotel.
|
||||
Записывает лог синхронизации в SyncLog.
|
||||
"""
|
||||
processed_records = 0
|
||||
received_records = len(data["rows"])
|
||||
|
||||
print(f"Received records: {received_records}")
|
||||
|
||||
for row in data["rows"]:
|
||||
record = dict(zip(data["columns"], row))
|
||||
print(f'\n------\n row: {row}\n------\n')
|
||||
# record = dict(zip(data["columns"], row)) # Преобразуем строку в словарь
|
||||
# Получаем url_parameters из записи
|
||||
url_parameters = self.decode_html_entities(row.get("url_parameters", ""))
|
||||
print(f'\n------\n url_parameters: {url_parameters}\n------\n')
|
||||
|
||||
external_id = record.get("id", None)
|
||||
if external_id is not None:
|
||||
external_id = str(external_id)
|
||||
# Проверка на пустое значение
|
||||
if not url_parameters:
|
||||
print(f"Error: url_parameters is empty in record {row}")
|
||||
continue # Пропускаем запись, если url_parameters отсутствует
|
||||
|
||||
created = self.parse_datetime(record.get("created"))
|
||||
date_time = self.parse_datetime(record.get("date_time"))
|
||||
# Пытаемся извлечь информацию о отеле из url_parameters
|
||||
hotel_name = None
|
||||
hotel_id = None
|
||||
|
||||
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", ""))
|
||||
if url_parameters:
|
||||
hotel_info = self.process_url_parameters(url_parameters)
|
||||
hotel_id = hotel_info.get('hotel_id')
|
||||
|
||||
url_parameters = self.decode_html_entities(record.get("url_parameters", ""))
|
||||
hotel_info = self.process_url_parameters(url_parameters)
|
||||
if not hotel_id:
|
||||
print(f"Error: hotel_id is empty in record {row}")
|
||||
continue # Пропускаем запись, если hotel_id отсутствует
|
||||
|
||||
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']
|
||||
)
|
||||
# Проверяем, существует ли отель с таким hotel_id
|
||||
hotel = self.check_and_store_imported_hotel(hotel_name=hotel_id, hotel_id=hotel_id)
|
||||
|
||||
url_parameters = unquote(url_parameters)
|
||||
if not hotel:
|
||||
print(f"Error: Could not find or create hotel for hotel_id {hotel_id}")
|
||||
continue # Пропускаем запись, если отель не найден или не создан
|
||||
|
||||
# Преобразуем дату
|
||||
created = self.parse_datetime(row.get("created"))
|
||||
date_time = self.parse_datetime(row.get("date_time"))
|
||||
|
||||
# Декодируем все строки, которые могут содержать HTML-сущности
|
||||
referred = self.decode_html_entities(row.get("referred", ""))
|
||||
agent = self.decode_html_entities(row.get("agent", ""))
|
||||
platform = self.decode_html_entities(row.get("platform", ""))
|
||||
version = self.decode_html_entities(row.get("version", ""))
|
||||
model = self.decode_html_entities(row.get("model", ""))
|
||||
device = self.decode_html_entities(row.get("device", ""))
|
||||
UAString = self.decode_html_entities(row.get("UAString", ""))
|
||||
location = self.decode_html_entities(row.get("location", ""))
|
||||
page_title = self.decode_html_entities(row.get("page_title", ""))
|
||||
page_url = self.decode_html_entities(row.get("page_url", ""))
|
||||
|
||||
# Запись в UserActivityLog
|
||||
UserActivityLog.objects.update_or_create(
|
||||
external_id=external_id,
|
||||
external_id=row.get("id", None),
|
||||
defaults={
|
||||
"user_id": record.get("user_id"),
|
||||
"ip": record.get("ip"),
|
||||
"user_id": row.get("user_id"),
|
||||
"ip": row.get("ip"),
|
||||
"created": created,
|
||||
"timestamp": record.get("timestamp"),
|
||||
"timestamp": row.get("timestamp"),
|
||||
"date_time": date_time,
|
||||
"referred": referred,
|
||||
"agent": agent,
|
||||
@@ -179,18 +336,24 @@ class DataSyncManager:
|
||||
"device": device,
|
||||
"UAString": UAString,
|
||||
"location": location,
|
||||
"page_id": record.get("page_id"),
|
||||
"page_id": row.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"),
|
||||
"type": row.get("type"),
|
||||
"last_counter": row.get("last_counter"),
|
||||
"hits": row.get("hits"),
|
||||
"honeypot": row.get("honeypot"),
|
||||
"reply": row.get("reply"),
|
||||
"page_url": page_url,
|
||||
}
|
||||
)
|
||||
|
||||
processed_records += 1
|
||||
|
||||
# Логируем обработанные записи
|
||||
print(f"Processed records: {processed_records}")
|
||||
|
||||
|
||||
def reconcile_data(self):
|
||||
"""
|
||||
Сверяет данные таблицы user_activity_log с таблицей hotels.reservations
|
||||
@@ -218,21 +381,34 @@ class DataSyncManager:
|
||||
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()
|
||||
last_id = self.get_last_saved_record()
|
||||
self.logger.info(f"Синхронизация начата. Последний сохранённый ID: {last_id}")
|
||||
|
||||
# Загружаем новые данные
|
||||
data = self.fetch_new_data(last_id=last_id)
|
||||
print(f'\n------\n data: {data}\n------\n')
|
||||
if not data["rows"]:
|
||||
self.logger.info("Нет новых данных для синхронизации.")
|
||||
return
|
||||
|
||||
# Логирование типов данных
|
||||
self.logger.debug(f"Тип первой строки: {type(data['rows'][0])}")
|
||||
|
||||
first_row_id = data["rows"][0]["id"]
|
||||
if last_id is not None and first_row_id <= last_id:
|
||||
self.logger.info(f"Нет новых записей для синхронизации. Последний ID: {last_id}, первый ID из внешней таблицы: {first_row_id}.")
|
||||
return
|
||||
|
||||
processed_records = self.write_to_db(data)
|
||||
self.logger.info(f"Синхронизация завершена. Обработано записей: {processed_records}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка синхронизации данных: {e}")
|
||||
raise RuntimeError(f"Ошибка синхронизации данных: {e}")
|
||||
finally:
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
|
||||
|
||||
def scheduled_sync():
|
||||
"""
|
||||
Плановая задача для синхронизации данных.
|
||||
|
||||
@@ -7,3 +7,10 @@ class HotelImportForm(forms.Form):
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=True
|
||||
)
|
||||
|
||||
from .models import SyncLog
|
||||
|
||||
class SyncLogForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = SyncLog
|
||||
fields = ['hotel', 'processed_records']
|
||||
|
||||
30
antifroud/migrations/0010_synclog.py
Normal file
30
antifroud/migrations/0010_synclog.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-14 04:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0009_importedhotel_display_name'),
|
||||
('hotels', '0010_alter_hotel_timezone'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SyncLog',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(primary_key=True, serialize=False, unique=True, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('recieved_records', models.IntegerField(verbose_name='Полученные записи')),
|
||||
('processed_records', models.IntegerField(verbose_name='Обработанные записи')),
|
||||
('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
|
||||
('reservation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.reservation', verbose_name='Бронирование')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Журнал синхронизации',
|
||||
'verbose_name_plural': 'Журналы синхронизации',
|
||||
},
|
||||
),
|
||||
]
|
||||
18
antifroud/migrations/0011_alter_importedhotel_external_id.py
Normal file
18
antifroud/migrations/0011_alter_importedhotel_external_id.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-14 06:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0010_synclog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='importedhotel',
|
||||
name='external_id',
|
||||
field=models.CharField(max_length=255, verbose_name='Внешний ID отеля'),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.db import models
|
||||
from hotels.models import Hotel, Reservation
|
||||
from hotels.models import Hotel
|
||||
from hotels.models import Reservation
|
||||
|
||||
|
||||
class UserActivityLog(models.Model):
|
||||
@@ -126,7 +127,8 @@ 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 отеля")
|
||||
id = models.BigAutoField(primary_key=True, auto_created=True, verbose_name="ID")
|
||||
external_id = models.CharField(max_length=255, 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="Дата создания")
|
||||
@@ -151,3 +153,17 @@ class ImportedHotel(models.Model):
|
||||
self.display_name = self.name
|
||||
self.save()
|
||||
|
||||
class SyncLog(models.Model):
|
||||
"""
|
||||
Журнал синхронизации.
|
||||
"""
|
||||
id = models.BigIntegerField(primary_key=True, unique=True, verbose_name="ID")
|
||||
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
|
||||
reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE, verbose_name="Бронирование")
|
||||
created = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||
recieved_records = models.IntegerField(verbose_name="Полученные записи")
|
||||
processed_records = models.IntegerField(verbose_name="Обработанные записи")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Журнал синхронизации"
|
||||
verbose_name_plural = "Журналы синхронизации"
|
||||
96
antifroud/templates/antifroud/admin/sync_log.html
Normal file
96
antifroud/templates/antifroud/admin/sync_log.html
Normal file
@@ -0,0 +1,96 @@
|
||||
{% extends "admin/change_list.html" %}
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<div class="card shadow-sm mb-2 db-graph">
|
||||
<div class="card-header p-2">
|
||||
<h6 class="text-white m-0 font-md">Журнал синхронизации</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'antifroud:sync_log_create' %}">
|
||||
{% csrf_token %}
|
||||
<div class="form-row">
|
||||
<div class="col-md-9 col-xl-9">
|
||||
<div class="box-bg">
|
||||
<div class="form-row">
|
||||
<div class="col-md-2 col-xl-2 align-self-center font-md text-dark-blue">
|
||||
<label class="col-form-label p-0" for="hotel-id"><strong>Отель:</strong></label>
|
||||
</div>
|
||||
<div class="col-md-4 col-xl-3">
|
||||
<div class="form-group mb-0">
|
||||
<select class="custom-select custom-select-sm font-sm" name="hotel" id="hotel-id">
|
||||
<option value="">--Выберите Отель --</option>
|
||||
{% for hotel in hotels %}
|
||||
<option value="{{ hotel.id }}">{{ hotel.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-xl-3">
|
||||
<div class="box-bg">
|
||||
<div class="text-dark form-row">
|
||||
<div class="col-xl-5 offset-xl-0 align-self-center">
|
||||
<h6 class="mb-0 font-sm">Полученные записи:</h6>
|
||||
</div>
|
||||
<div class="col-xl-7 offset-xl-0 text-right align-self-center">
|
||||
<div class="form-group mb-1">
|
||||
<input class="form-control form-control-sm form-control font-sm" type="number" name="received_records" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-5 offset-xl-0 align-self-center">
|
||||
<h6 class="mb-0 font-sm">Обработанные записи:</h6>
|
||||
</div>
|
||||
<div class="col-xl-7 offset-xl-0 text-right align-self-center">
|
||||
<div class="form-group mb-1">
|
||||
<input class="form-control form-control-sm form-control font-sm" type="number" name="processed_records" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Список существующих журналов синхронизации -->
|
||||
<div class="table-responsive tbl-wfx mt-1 kot-table">
|
||||
<table class="table table-sm">
|
||||
<thead class="text-dark font-md">
|
||||
<tr class="text-dark-blue">
|
||||
<th>#</th>
|
||||
<th>Отель</th>
|
||||
<th>ID бронирования</th>
|
||||
<th>Обработанные записи</th>
|
||||
<th>Полученные записи</th>
|
||||
<th>Создан</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in sync_logs %}
|
||||
<tr>
|
||||
<td>{{ log.id }}</td>
|
||||
<td>{{ log.hotel.name }}</td>
|
||||
<td>{{ log.reservation_id }}</td>
|
||||
<td>{{ log.processed_records }}</td>
|
||||
<td>{{ log.recieved_records }}</td>
|
||||
<td>{{ log.created }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">Нет журналов.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -6,5 +6,6 @@ app_name = 'antifroud'
|
||||
|
||||
urlpatterns = [
|
||||
path('import_selected_hotels/', views.import_selected_hotels, name='importedhotels_import_selected_hotels'),
|
||||
path('sync-log/create/', views.sync_log_create, name='sync_log_create'),
|
||||
# Другие URL-адреса
|
||||
]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .models import ImportedHotel
|
||||
from .models import ImportedHotel, SyncLog
|
||||
from hotels.models import Hotel
|
||||
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.utils import timezone
|
||||
|
||||
from .forms import SyncLogForm
|
||||
# Создаем логгер
|
||||
logger = logging.getLogger('antifroud')
|
||||
|
||||
@@ -108,3 +108,15 @@ def import_hotels(request):
|
||||
form = HotelImportForm()
|
||||
|
||||
return render(request, 'antifroud/admin/import_hotels.html', {'form': form})
|
||||
|
||||
|
||||
def sync_log_create(request):
|
||||
if request.method == 'POST':
|
||||
form = SyncLogForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save() # Сохраняем новый SyncLog
|
||||
return redirect('admin:antifroud_synclog_changelist') # Перенаправляем обратно в список
|
||||
else:
|
||||
form = SyncLogForm()
|
||||
|
||||
return render(request, 'antifroud/admin/sync_log_create.html', {'form': form})
|
||||
Reference in New Issue
Block a user