settings application

.env db params+global settings in admin model
ECVI plugin module
This commit is contained in:
2024-12-14 20:50:11 +09:00
parent 09eb249d68
commit 93994ed929
328 changed files with 190143 additions and 538 deletions

View File

@@ -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']

View File

@@ -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():
"""
Плановая задача для синхронизации данных.

View File

@@ -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']

View 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': 'Журналы синхронизации',
},
),
]

View 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 отеля'),
),
]

View File

@@ -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 = "Журналы синхронизации"

View 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 %}

View File

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

View File

@@ -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})