diff --git a/antifroud/admin.py b/antifroud/admin.py
index 93aa8752..c5cefa90 100644
--- a/antifroud/admin.py
+++ b/antifroud/admin.py
@@ -5,7 +5,8 @@ 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, SyncLog, ViolationLog
-from hotels.models import Hotel
+
+from hotels.models import Hotel, Room
import pymysql
import logging
from django.urls import reverse
@@ -114,14 +115,32 @@ class UserActivityLogAdmin(admin.ModelAdmin):
search_fields = ("page_title", "url_parameters")
list_filter = ("page_title", "created")
readonly_fields = ("created", "timestamp")
+ def get_hotel_name(self):
+ """
+ Возвращает название отеля на основе связанного page_id.
+ """
+ if self.page_id:
+ try:
+ room = Room.objects.get(id=self.page_id)
+ return room.hotel.name
+ except Room.DoesNotExist:
+ return "Отель не найден"
+ return "Нет данных"
+ def get_room_number(self):
+ """
+ Возвращает номер комнаты на основе связанного page_id.
+ """
+ if self.page_id:
+ try:
+ room = Room.objects.get(id=self.page_id)
+ return room.number
+ except Room.DoesNotExist:
+ return "Комната не найдена"
+ return "Нет данных"
-@admin.register(RoomDiscrepancy)
-class RoomDiscrepancyAdmin(admin.ModelAdmin):
- list_display = ("hotel", "room_number", "booking_id", "check_in_date_expected", "check_in_date_actual", "discrepancy_type", "created_at")
- search_fields = ("hotel__name", "room_number", "booking_id")
- list_filter = ("discrepancy_type", "created_at")
- readonly_fields = ("created_at",)
+ get_hotel_name.short_description = "Отель"
+ get_room_number.short_description = "Комната"
from .views import import_selected_hotels
diff --git a/antifroud/check_fraud.py b/antifroud/check_fraud.py
index 9cf676bf..1471816d 100644
--- a/antifroud/check_fraud.py
+++ b/antifroud/check_fraud.py
@@ -1,79 +1,160 @@
import logging
-from datetime import datetime, timedelta
+from datetime import timedelta
from urllib.parse import parse_qs
+from django.utils import timezone
from django.db.models import Q
from hotels.models import Reservation, Hotel
from .models import UserActivityLog, ViolationLog
# Настройка логирования
-logging.basicConfig(level=logging.INFO) # Устанавливаем уровень логирования
-logger = logging.getLogger(__name__) # Создаем логгер для текущего модуля
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
-def check_reservations_and_generate_report():
- now = datetime.now()
- start_time = (now - timedelta(hours=12))
- end_time = now
- logger.info(f"Starting reservation check from {start_time} to {end_time}")
+class ReservationChecker:
+ """
+ Класс для проверки несоответствий между бронированиями и логами заселения.
+ """
- # Получаем логи активности за период
- user_logs = UserActivityLog.objects.filter(created__range=(start_time, end_time))
- logger.info(f"Found {len(user_logs)} logs for analysis.")
+ def __init__(self):
+ """
+ Инициализация времени проверки и списка нарушений.
+ """
+ self.start_time = timezone.now() - timedelta(days=30)
+ self.end_time = timezone.now()
+ self.violations = []
- violations = [] # Список для записи нарушений
+ def log_info(self, message):
+ """Логирование информационных сообщений."""
+ logger.info(message)
- for i, log in enumerate(user_logs, start=1):
- logger.debug(f"Processing log {i}: {log}")
+ def log_warning(self, message):
+ """Логирование предупреждений."""
+ logger.warning(message)
- if not log.url_parameters:
- logger.warning(f"Log {i} skipped due to missing URL parameters.")
- continue # Пропускаем записи без параметров URL
+ def log_error(self, message):
+ """Логирование ошибок."""
+ logger.error(message)
- # Парсим параметры URL
- params = parse_qs(log.url_parameters)
- external_id = params.get("utm_content", [None])[0] # ID отеля
- room_number = params.get("utm_term", [None])[0] # Номер комнаты
+ def fetch_user_logs(self):
+ """
+ Извлекает записи из UserActivityLog за последние 12 часов.
+ """
+ print(f"Fetching user logs from {self.start_time} to {self.end_time}")
+ user_logs = UserActivityLog.objects.filter(created__range=(self.start_time, self.end_time))
+ print(f"Found {user_logs.count()} logs for analysis.")
+ return user_logs
- logger.debug(f"Log {i} parsed parameters: external_id={external_id}, room_number={room_number}")
+ def fetch_hotels(self, hotel_ids):
+ """
+ Извлекает отели по hotel_id из логов.
+ """
+ hotels = {hotel.hotel_id: hotel for hotel in Hotel.objects.filter(hotel_id__in=hotel_ids)}
+ self.log_info(f"Найдено {len(hotels)} отелей для сверки.")
+ return hotels
- if not external_id or not room_number:
- logger.warning(f"Log {i} skipped due to missing external_id or room_number.")
- continue # Пропускаем, если данные не извлечены
+ def find_violations(self, user_logs, hotels):
+ """
+ Сопоставляет логи активности с бронированиями и фиксирует нарушения.
+ """
+ for log in user_logs:
+ if not log.url_parameters:
+ self.log_warning(f"Пропущена запись ID {log.id}: отсутствуют URL-параметры.")
+ continue
- try:
- # Находим отель по external_id
- hotel = Hotel.objects.get(external_id=external_id)
- logger.debug(f"Log {i}: Found hotel {hotel.name} with external_id {external_id}.")
- except Hotel.DoesNotExist:
- logger.error(f"Log {i} skipped: No hotel found with external_id {external_id}.")
- continue
+ # Парсинг параметров URL
+ params = parse_qs(log.url_parameters)
+ hotel_id = params.get("utm_content", [None])[0]
+ room_number = params.get("utm_term", [None])[0]
- # Ищем бронирование в Reservation
- matching_reservations = Reservation.objects.filter(
- hotel=hotel,
- room_number=room_number,
- check_in__lte=log.created,
- check_out__gte=log.created
- )
- logger.debug(f"Log {i}: Found {len(matching_reservations)} matching reservations.")
+ print(f"Processing log ID {log.id} with hotel ID {hotel_id} and room number {room_number}")
- if not matching_reservations.exists():
- # Если бронирование не найдено — записываем нарушение
- violation_details = (
- f"Log {i}: No reservation found for room {room_number} in hotel {external_id} at {log.created}."
- )
- violations.append(ViolationLog(
+ if not hotel_id or not room_number:
+ self.log_warning(f"Пропущена запись ID {log.id}: некорректные параметры URL.")
+ continue
+
+ if hotel_id not in hotels:
+ self.log_warning(f"Пропущена запись ID {log.id}: отель с ID {hotel_id} не найден.")
+ continue
+
+ hotel = hotels[hotel_id]
+ log_time = timezone.localtime(log.created)
+
+ # Проверка существования бронирования
+ matching_reservations = Reservation.objects.filter(
hotel=hotel,
room_number=room_number,
- violation_type="missed",
- violation_details=violation_details
- ))
- logger.warning(f"Log {i}: Violation recorded - {violation_details}")
+ check_in__lte=log_time,
+ check_out__gte=log_time
+ )
- # Сохраняем все нарушения в базу
- if violations:
- ViolationLog.objects.bulk_create(violations)
- logger.info(f"Created {len(violations)} records in violation log.")
- else:
- logger.info("No violations found during this check.")
+ print(f"Found {matching_reservations.count()} matching reservations")
- logger.info("Reservation check completed.")
+ if not matching_reservations.exists():
+ violation_details = (
+ f"Не найдено бронирование для номера {room_number} в отеле '{hotel.name}' на {log_time}."
+ )
+ # Добавляем нарушение, если его ещё нет в базе
+ if not ViolationLog.objects.filter(
+ hotel=hotel,
+ room_number=room_number,
+ violation_type="missed",
+ violation_details=violation_details
+ ).exists():
+ self.violations.append(ViolationLog(
+ hotel=hotel,
+ room_number=room_number,
+ violation_type="missed",
+ violation_details=violation_details
+ ))
+ self.log_warning(f"Зафиксировано нарушение: {violation_details}")
+
+ def save_violations(self):
+ """
+ Сохраняет найденные нарушения в базу данных.
+ """
+ if self.violations:
+ ViolationLog.objects.bulk_create(self.violations)
+ self.log_info(f"Создано {len(self.violations)} записей в ViolationLog.")
+ else:
+ self.log_info("Нарушений не обнаружено.")
+
+ def run_check(self):
+ """
+ Основной метод для запуска проверки.
+ """
+ self.log_info(f"Запуск проверки бронирований с {self.start_time} по {self.end_time}.")
+
+ try:
+ # Получаем логи пользователей
+ user_logs = self.fetch_user_logs()
+
+ # Извлекаем hotel_id из логов
+ hotel_ids = set()
+ for log in user_logs:
+ if log.url_parameters:
+ params = parse_qs(log.url_parameters)
+ hotel_id = params.get("utm_content", [None])[0]
+ if hotel_id:
+ hotel_ids.add(hotel_id)
+
+ # Предзагружаем отели
+ hotels = self.fetch_hotels(hotel_ids)
+
+ # Сравниваем логи с бронированиями
+ self.find_violations(user_logs, hotels)
+
+ # Сохраняем результаты
+ self.save_violations()
+
+ except Exception as e:
+ self.log_error(f"Произошла ошибка при выполнении проверки: {e}")
+
+ self.log_info("Проверка бронирований завершена.")
+
+# Функция для запуска проверки из планировщика
+def run_reservation_check():
+ """
+ Функция для запуска проверки бронирований.
+ """
+ checker = ReservationChecker()
+ checker.run_check()
\ No newline at end of file
diff --git a/antifroud/data_sync.py b/antifroud/data_sync.py
index 81ac3132..20d435e2 100644
--- a/antifroud/data_sync.py
+++ b/antifroud/data_sync.py
@@ -1,419 +1,691 @@
+# import logging
+# import pymysql
+# from datetime import datetime
+# from urllib.parse import unquote, parse_qs
+# import pytz
+# from django.utils import timezone
+# from django.db import transaction
+# from django.conf import settings
+# import chardet
+# import html
+# from .models import SyncLog, ExternalDBSettings, UserActivityLog, RoomDiscrepancy
+# from hotels.models import Room, Hotel, Reservation
+
+# class DatabaseConnector:
+# """
+# Класс для подключения к внешней или локальной базе данных.
+# """
+# def __init__(self, db_settings_id, use_local_db=False):
+# self.db_settings_id = db_settings_id
+# self.use_local_db = use_local_db
+# self.connection = None
+# self.db_settings = None
+# self.table_name = None
+# self.logger = self.setup_logger()
+
+# def setup_logger(self):
+# logger = logging.getLogger(__name__)
+# logger.setLevel(logging.DEBUG)
+# handler = logging.FileHandler("data_sync.log")
+# handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
+# logger.addHandler(handler)
+# return logger
+
+# def connect(self):
+# """Подключение к базе данных."""
+# try:
+# if self.use_local_db:
+# self.db_settings = settings.LocalDataBase.objects.first()
+# else:
+# self.db_settings = ExternalDBSettings.objects.get(id=self.db_settings_id)
+
+# 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",
+# cursorclass=pymysql.cursors.DictCursor,
+# )
+# self.table_name = self.db_settings.table_name
+# self.logger.info("Подключение к базе данных успешно установлено.")
+# except Exception as e:
+# self.logger.error(f"Ошибка подключения к БД: {e}")
+# raise ConnectionError(e)
+
+# def close(self):
+# """Закрывает соединение с базой данных."""
+# if self.connection:
+# self.connection.close()
+# self.logger.info("Соединение с базой данных закрыто.")
+
+# def execute_query(self, query):
+# """Выполнение запроса и возврат результатов."""
+# with self.connection.cursor() as cursor:
+# cursor.execute(query)
+# return cursor.fetchall()
+
+
+# class DataProcessor:
+# """
+# Обрабатывает и сохраняет данные.
+# """
+# def __init__(self, logger):
+# self.logger = logger
+
+# def decode_html_entities(self, text):
+# """Декодирует URL и HTML-сущности."""
+# if text and isinstance(text, str):
+# text = unquote(text)
+# text = html.unescape(text)
+# 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
+
+# def process_url_parameters(self, url_params):
+# """Парсит параметры URL и возвращает информацию о отеле и номере."""
+# decoded = unquote(url_params)
+# qs = parse_qs(decoded)
+# hotel_name = qs.get('utm_content', [None])[0]
+# room_number = qs.get('utm_term', [None])[0]
+# return {"hotel_name": hotel_name, "room_number": room_number}
+
+# def parse_datetime(self, dt_str):
+# """Преобразует строку даты в aware datetime."""
+# try:
+# if isinstance(dt_str, datetime):
+# return timezone.make_aware(dt_str) if timezone.is_naive(dt_str) else dt_str
+# if dt_str:
+# return timezone.make_aware(datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S"))
+# except Exception as e:
+# self.logger.error(f"Ошибка парсинга даты: {e}")
+# return None
+
+
+# class HotelRoomManager:
+# """
+# Управляет созданием отелей и номеров.
+# """
+# def __init__(self, logger):
+# self.logger = logger
+
+# def get_or_create_hotel(self, hotel_name):
+# """Создает или получает отель."""
+# if not hotel_name:
+# return None
+# hotel, created = Hotel.objects.get_or_create(
+# name=hotel_name,
+# defaults={"description": "Автоматически добавленный отель"}
+# )
+# if created:
+# self.logger.info(f"Создан отель: {hotel_name}")
+# return hotel
+
+# def get_or_create_room(self, hotel, room_number):
+# """Создает или получает номер отеля."""
+# if not hotel or not room_number:
+# return None
+# room, created = Room.objects.get_or_create(
+# hotel=hotel,
+# number=room_number,
+# defaults={"external_id": f"{hotel.name}_{room_number}"}
+# )
+# if created:
+# self.logger.info(f"Добавлен номер: {room_number} в отель {hotel.name}")
+# return room
+
+
+# class DataSyncManager:
+# """
+# Главный класс для синхронизации данных.
+# """
+# def __init__(self, db_settings_id, use_local_db=False):
+# self.logger = self.setup_logger()
+# self.db_connector = DatabaseConnector(db_settings_id, use_local_db)
+# self.data_processor = DataProcessor(self.logger)
+# self.hotel_manager = HotelRoomManager(self.logger)
+
+# def setup_logger(self):
+# logger = logging.getLogger(__name__)
+# logger.setLevel(logging.DEBUG)
+# handler = logging.FileHandler("data_sync.log")
+# handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
+# logger.addHandler(handler)
+# return logger
+
+# def get_last_saved_record(self):
+# """Получает ID последней записи."""
+# record = UserActivityLog.objects.order_by("-id").first()
+# return record.id if record else 0
+
+# def fetch_new_data(self, last_id):
+# """Получает новые данные из БД для переноса 1-в-1."""
+# query = f"""
+# SELECT id AS external_id, user_id, ip, created, timestamp, date_time,
+# referred, agent, platform, version, model, device, UAString, location,
+# page_id, url_parameters, page_title, type, last_counter, hits,
+# honeypot, reply, page_url
+# FROM `{self.db_connector.table_name}`
+# WHERE id > {last_id}
+# ORDER BY id ASC LIMIT 100;
+# """
+# return self.db_connector.execute_query(query)
+
+# def process_and_save_data(self, rows):
+# """Обрабатывает данные и сохраняет их 1-в-1 в UserActivityLog."""
+# for row in rows:
+# try:
+# # Парсинг и сохранение всех полей
+# UserActivityLog.objects.update_or_create(
+# external_id=row["external_id"],
+# defaults={
+# "user_id": row.get("user_id"),
+# "ip": row.get("ip"),
+# "created": self.data_processor.parse_datetime(row.get("created")),
+# "timestamp": row.get("timestamp"),
+# "date_time": self.data_processor.parse_datetime(row.get("date_time")),
+# "referred": row.get("referred"),
+# "agent": row.get("agent"),
+# "platform": row.get("platform"),
+# "version": row.get("version"),
+# "model": row.get("model"),
+# "device": row.get("device"),
+# "UAString": row.get("UAString"),
+# "location": row.get("location"),
+# "page_id": row.get("page_id"),
+# "url_parameters": row.get("url_parameters"),
+# "page_title": row.get("page_title"),
+# "type": row.get("type"),
+# "last_counter": row.get("last_counter"),
+# "hits": row.get("hits"),
+# "honeypot": row.get("honeypot"),
+# "reply": row.get("reply"),
+# "page_url": row.get("page_url"),
+# }
+# )
+# self.logger.info(f"Запись external_id={row['external_id']} успешно сохранена.")
+# except Exception as e:
+# self.logger.error(f"Ошибка обработки записи {row['external_id']}: {e}")
+
+
+# def check_and_store_room(self, hotel, room_number):
+# """
+# Проверяет и создает номер в отеле, если он еще не существует.
+# """
+# try:
+# Room.objects.get_or_create(
+# hotel=hotel,
+# number=room_number,
+# defaults={
+# "external_id": f"{hotel.hotel_id}_{room_number}",
+# "description": "Автоматически добавленный номер",
+# }
+# )
+# self.logger.info(f"Добавлен номер: {room_number} в отель {hotel.name}")
+# except Exception as e:
+# self.logger.error(f"Ошибка при добавлении номера {room_number} для отеля {hotel.name}: {e}")
+# def reconcile_data(self):
+# """
+# Сверяет данные UserActivityLog с Reservation и фиксирует несоответствия.
+# """
+# discrepancies = []
+# reservations = Reservation.objects.values("hotel_id", "room_number", "check_in", "check_out")
+
+# for log in UserActivityLog.objects.all():
+# try:
+# # Преобразование page_id в число
+# page_id = int(float(log.page_id))
+# except (ValueError, TypeError):
+# self.logger.warning(f"Некорректное значение page_id: {log.page_id} - пропущено.")
+# continue
+
+# # Сверка с бронированиями
+# matching_reservation = reservations.filter(
+# hotel_id=log.url_parameters, room_number=str(page_id)
+# ).first()
+
+# if not matching_reservation:
+# discrepancy = RoomDiscrepancy(
+# hotel_id=log.hotel_id,
+# room_number=page_id,
+# booking_id=f"Log-{log.id}",
+# check_in_date_expected=None,
+# check_in_date_actual=log.created.date() if log.created else None,
+# discrepancy_type="Mismatch",
+# )
+# discrepancies.append(discrepancy)
+
+# RoomDiscrepancy.objects.bulk_create(discrepancies)
+# self.logger.info(f"Обнаружено несоответствий: {len(discrepancies)}")
+
+# def write_to_db(self, data):
+# """
+# Записывает данные в UserActivityLog и при необходимости в ImportedHotel.
+# """
+# processed_records = 0
+# received_records = len(data["rows"])
+
+# self.logger.info(f"Начата обработка {received_records} записей.")
+
+# for row in data["rows"]:
+# # Обработка page_id с валидацией
+# raw_page_id = row.get("page_id")
+# try:
+# page_id = int(float(raw_page_id)) # Пытаемся привести к числу
+# except (ValueError, TypeError):
+# self.logger.warning(f"Некорректное значение page_id: {raw_page_id} - установлено 0")
+# page_id = 0 # Используем значение по умолчанию
+
+# # Получаем url_parameters
+# url_parameters = self.encode_html_entities(self.process_url_parameters(self.decode_html_entities(row.get("url_parameters", ""))))
+
+# # Проверка на пустое значение url_parameters
+# if not url_parameters:
+# self.logger.warning("Пропущена запись из-за отсутствующих url_parameters.")
+# continue
+
+# # Извлечение информации об отеле из url_parameters
+# hotel_info = self.process_url_parameters(url_parameters)
+# hotel_name = hotel_info.get("hotel_name")
+# hotel_id = hotel_info.get("hotel_id")
+
+# if not hotel_id:
+# self.logger.warning("Пропущена запись из-за отсутствующего hotel_id.")
+# continue
+
+# # Проверяем или создаем отель
+# hotel = self.check_and_store_imported_hotel(hotel_name=hotel_name, hotel_id=hotel_id)
+# if not hotel:
+# self.logger.error(f"Не удалось найти или создать отель: {hotel_id}")
+# continue
+
+# # Преобразование дат
+# created = self.parse_datetime(row.get("created"))
+# date_time = self.parse_datetime(row.get("date_time"))
+
+# # Запись данных в UserActivityLog
+# UserActivityLog.objects.update_or_create(
+# external_id=row.get("id"),
+# defaults={
+# "user_id": row.get("user_id"),
+# "ip": row.get("ip") or "0.0.0.0",
+# "created": created,
+# "timestamp": row.get("timestamp") or timezone.now().timestamp(),
+# "date_time": date_time,
+# "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_id": page_id, # Обновленный page_id
+# "url_parameters": url_parameters,
+# "page_title": self.decode_html_entities(row.get("page_title", "")),
+# "type": row.get("type"),
+# "last_counter": row.get("last_counter") or 0,
+# "hits": row.get("hits") or 0,
+# "honeypot": row.get("honeypot") or False,
+# "reply": row.get("reply") or False,
+# "page_url": self.decode_html_entities(row.get("page_url", "")),
+# },
+# )
+
+# processed_records += 1
+# self.logger.info(f"Запись успешно обработана: external_id={row.get('id')}")
+
+# self.logger.info(f"Обработано записей: {processed_records} из {received_records}.")
+
+# def sync(self):
+# """Запускает процесс синхронизации."""
+# self.db_connector.connect()
+# try:
+# last_id = self.get_last_saved_record()
+# rows = self.fetch_new_data(last_id)
+# self.process_and_save_data(rows)
+# self.logger.info("Синхронизация завершена.")
+# finally:
+# self.db_connector.close()
+
+
+# import logging
+# from concurrent.futures import ThreadPoolExecutor
+# from .models import ExternalDBSettings
+
+# logger = logging.getLogger(__name__)
+# logger.setLevel(logging.DEBUG)
+
+
+# def scheduled_sync():
+# """
+# Планировщик синхронизации для всех активных подключений.
+# Каждое подключение обрабатывается отдельно.
+# """
+# logger.info("Запуск планировщика синхронизации.")
+
+# # Получаем все активные настройки подключения
+# active_db_settings = ExternalDBSettings.objects.filter(is_active=True)
+# if not active_db_settings.exists():
+# logger.warning("Не найдено активных подключений для синхронизации.")
+# return
+
+# logger.info(f"Найдено активных подключений: {len(active_db_settings)}")
+
+# def sync_task(db_settings):
+# """
+# Выполняет синхронизацию для одного подключения.
+# """
+# try:
+# logger.info(f"Начало синхронизации для подключения: {db_settings.name} (ID={db_settings.id})")
+# sync_manager = DataSyncManager(db_settings.id)
+# sync_manager.sync()
+# logger.info(f"Синхронизация успешно завершена для подключения: {db_settings.name}")
+# except Exception as e:
+# logger.error(f"Ошибка синхронизации для подключения {db_settings.name}: {e}")
+
+# # Параллельное выполнение задач синхронизации
+# with ThreadPoolExecutor(max_workers=5) as executor: # Максимальное количество потоков = 5
+# for db_settings in active_db_settings:
+# executor.submit(sync_task, db_settings)
+
+# logger.info("Планировщик синхронизации завершил работу.")
+
+
import logging
import pymysql
from datetime import datetime
from urllib.parse import unquote, parse_qs
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
+from hotels.models import Room, Hotel
+from .models import UserActivityLog, ExternalDBSettings
-class DataSyncManager:
- """
- Класс для управления загрузкой, записью и сверкой данных.
- """
- def __init__(self, db_settings_id, use_local_db=False):
+class DatabaseConnector:
+ """
+ Класс для подключения к внешней базе данных.
+ """
+ def __init__(self, db_settings_id):
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 = self.setup_logger()
+ self.db_settings = None
- # Настройка логирования
- self.logger = logging.getLogger(__name__)
- self.logger.setLevel(logging.WARNING)
- 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 setup_logger(self):
+ logger = logging.getLogger(__name__)
+ logger.setLevel(logging.DEBUG)
+ handler = logging.FileHandler("data_sync.log")
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
+ logger.addHandler(handler)
+ return logger
- def connect_to_db(self):
- """
- Устанавливает соединение с БД в зависимости от настройки.
- """
+ def connect(self):
+ """Подключение к базе данных."""
try:
- 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,
- )
+ self.db_settings = ExternalDBSettings.objects.get(id=self.db_settings_id)
+ 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",
+ cursorclass=pymysql.cursors.DictCursor,
+ )
+ self.logger.info("Подключение к базе данных успешно установлено.")
+ except Exception as e:
+ self.logger.error(f"Ошибка подключения к БД: {e}")
+ raise ConnectionError(e)
- except ExternalDBSettings.DoesNotExist:
- raise ValueError("Настройки подключения не найдены.")
- except pymysql.MySQLError as e:
- raise ConnectionError(f"Ошибка подключения к базе данных: {e}")
- 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 close(self):
+ """Закрывает соединение с базой данных."""
+ if self.connection:
+ self.connection.close()
+ self.logger.info("Соединение с базой данных закрыто.")
- def fetch_new_data(self, last_id=0, limit=100):
- """
- Загружает новые записи из указанной таблицы, которые идут после last_id.
- """
- if not self.connection:
- self.connect_to_db()
-
- cursor = self.connection.cursor(pymysql.cursors.DictCursor) # Используем DictCursor для получения словарей
- try:
- # Формируем 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}")
+ def execute_query(self, query):
+ """Выполнение запроса и возврат результатов."""
+ with self.connection.cursor() as cursor:
cursor.execute(query)
-
- # Получаем результаты
- rows = cursor.fetchall()
- 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:
- self.logger.error(f"Ошибка выполнения запроса: {e}")
- return {"columns": [], "rows": []}
- finally:
- cursor.close()
+ return cursor.fetchall()
- 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
-
- 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 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
-
- return None
+class DataProcessor:
+ """
+ Обрабатывает и сохраняет данные.
+ """
+ def __init__(self, logger):
+ self.logger = logger
def decode_html_entities(self, text):
- """
- Декодирует URL и HTML-сущности в строке.
- Пытается автоматически декодировать строку в правильную кодировку.
- """
+ """Декодирует URL и HTML-сущности."""
if text and isinstance(text, str):
- text = unquote(text) # Декодируем URL
- text = html.unescape(text) # Расшифровываем HTML сущности
-
- # Попробуем определить кодировку и привести строку к utf-8
+ text = unquote(text)
+ text = html.unescape(text)
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
+ self.logger.error(f"Ошибка кодировки: {e}")
return text
- def process_url_parameters(self, url_params):
+ def parse_datetime(self, dt_str):
+ """Преобразует строку даты в aware datetime."""
+ try:
+ if isinstance(dt_str, datetime):
+ return timezone.make_aware(dt_str) if timezone.is_naive(dt_str) else dt_str
+ if dt_str:
+ return timezone.make_aware(datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S"))
+ except Exception as e:
+ self.logger.error(f"Ошибка парсинга даты: {e}")
+ return None
+ def url_parameters_parser(self, url_parameters):
"""
- Парсит url_parameters и возвращает hotel_name и hotel_id.
+ Парсит строку URL-параметров и возвращает словарь с ключами и значениями.
+
+ Пример входа:
+ url_parameters = "utm_medium=qr-3&utm_content=chisto-pozhit&utm_term=101"
+
+ Возвращает:
+ {
+ "utm_medium": "qr-3",
+ "utm_content": "chisto-pozhit",
+ "utm_term": "101"
+ }
"""
- # Проверка корректности данных
- if not url_params or not isinstance(url_params, str):
- self.logger.error(f"Ошибка: некорректные url_parameters: {url_params}")
+ try:
+ if not url_parameters:
+ return {}
+
+ # Декодируем параметры URL
+ decoded_params = unquote(url_parameters)
+ parsed_params = parse_qs(decoded_params)
+
+ # Преобразуем список значений в строку
+ result = {key: value[0] for key, value in parsed_params.items()}
+ return result
+
+ except Exception as e:
+ self.logger.error(f"Ошибка парсинга URL-параметров: {e}")
return {}
- # Декодируем параметры URL
- decoded = unquote(url_params)
- qs = parse_qs(decoded)
+class HotelRoomManager:
+ """
+ Управляет созданием отелей и номеров.
+ """
+ def __init__(self, logger):
+ self.logger = logger
- # Извлекаем hotel_name и hotel_id_term
- hotel_name = qs.get('utm_content', [None])[0] # Умолчание: None, если параметр не найден
- hotel_id_term = qs.get('utm_term', [None])[0] # Умолчание: None, если параметр не найден
+ def get_or_create_hotel(self, hotel_name, hotel_id):
+ """Создает или получает отель."""
+ if not hotel_name:
+ return None
+ hotel, created = Hotel.objects.get_or_create(
+ name=hotel_name,
+ hotel_id = hotel_id,
+ defaults={"description": "Автоматически добавленный отель"}
+ )
+ if created:
+ self.logger.info(f"Создан отель: {hotel_name}")
+ return hotel
- # Формируем 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 get_or_create_room(self, hotel, room_number):
+ """Создает или получает номер отеля."""
+ if not hotel or not room_number:
+ return None
+ room, created = Room.objects.get_or_create(
+ hotel=hotel,
+ number=room_number,
+ defaults={"external_id": f"{hotel.name}_{room_number}"}
+ )
+ if created:
+ self.logger.info(f"Добавлен номер: {room_number} в отель {hotel.name}")
+ return room
- def check_and_store_imported_hotel(self, hotel_name, hotel_id):
- """
- Проверяет, есть ли отель с данным ID в таблице ImportedHotel.
- Если отеля с таким external_id нет, добавляет новый в таблицы ImportedHotel и Hotel.
- """
- if not hotel_id or not hotel_name:
- return None
+class DataSyncManager:
+ """
+ Главный класс для синхронизации данных.
+ """
+ def __init__(self, db_settings_id):
+ self.logger = self.setup_logger()
+ self.db_connector = DatabaseConnector(db_settings_id)
+ self.data_processor = DataProcessor(self.logger)
+ self.hotel_manager = HotelRoomManager(self.logger)
- # Генерация external_id в формате 'hotel_name_hotel_id'
- external_id = f"{hotel_name}"
+ def setup_logger(self):
+ logger = logging.getLogger(__name__)
+ logger.setLevel(logging.DEBUG)
+ handler = logging.FileHandler("data_sync.log")
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
+ logger.addHandler(handler)
+ return logger
- # Проверяем, существует ли запись с таким external_id в ImportedHotel
- existing_hotel = ImportedHotel.objects.filter(external_id=external_id).first()
+ def get_last_saved_record(self):
+ """Получает ID последней записи."""
+ record = UserActivityLog.objects.order_by("-id").first()
+ return record.id if record else 0
- 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
-
- def write_to_db(self, data):
+ def fetch_new_data(self, last_id):
+ """Получает новые данные из БД."""
+ query = f"""
+ SELECT * FROM `{self.db_connector.db_settings.table_name}`
+ WHERE id > {last_id}
+ AND url_parameters IS NOT NULL
+ AND url_parameters LIKE '%utm_medium%'
+ ORDER BY id ASC
+ LIMIT 1000;
"""
- Записывает данные в UserActivityLog и при необходимости в ImportedHotel.
- Записывает лог синхронизации в SyncLog.
- """
- processed_records = 0
- received_records = len(data["rows"])
+ self.logger.info(f"Запрос на получение новых данных отправлен. \n Содержание запроса: {query}")
+ return self.db_connector.execute_query(query)
- print(f"Received records: {received_records}")
+ def process_and_save_data(self, rows):
+ """Обрабатывает и сохраняет данные, включая парсинг отсутствующих значений."""
+ for row in rows:
+ try:
+ # Декодирование параметров URL
+ url_params = self.data_processor.decode_html_entities(row.get("url_parameters", ""))
+ params = parse_qs(url_params)
+ param_dict = DataProcessor.url_parameters_parser(self.data_processor, url_params)
+ self.logger.info(f"Параметры URL успешно декодированы: {param_dict}")
+ # Извлечение и обработка данных
+ external_id = row.get("id")
+ created = self.data_processor.parse_datetime(row.get("created")) or timezone.now()
+ hotel_name = params.get("utm_content", [None])[0] or "Неизвестный отель"
+ room_number = params.get("utm_term", [None])[0] or "000"
+ page_title = row.get("page_title") or f"Информация отсутствует для ID {external_id}"
- for row in data["rows"]:
- 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')
+ # Создание отеля и номера, если они отсутствуют
+ hotel = self.hotel_manager.get_or_create_hotel(hotel_name, hotel_id=row.get("hotel_id"))
+ room = self.hotel_manager.get_or_create_room(hotel, room_number)
- # Проверка на пустое значение
- if not url_parameters:
- print(f"Error: url_parameters is empty in record {row}")
- continue # Пропускаем запись, если url_parameters отсутствует
+ # Заполнение отсутствующих данных
+ user_id = row.get("user_id") or 0
+ ip = row.get("ip") or "0.0.0.0"
+ hits = row.get("hits") or 0
+ date_time = row.get("date_time") or timezone.now().strftime("%Y-%m-%d %H:%M:%S")
+ timeatamp = row.get("timestamp") or timezone.now().timestamp()
+ # Создание или обновление записи в UserActivityLog
+ UserActivityLog.objects.update_or_create(
+ external_id=external_id,
+ defaults={
+ "user_id": user_id,
+ "ip": ip,
+ "timestamp": timeatamp,
+ "date_time": date_time,
+ "created": created,
+ "page_id": room.id if room else None,
+ "url_parameters": url_params,
+ "page_title": html.unescape(page_title),
+ "hits": hits,
+ }
+ )
- # Пытаемся извлечь информацию о отеле из url_parameters
- hotel_name = None
- hotel_id = None
+ self.logger.info(f"Запись ID {external_id} успешно обработана и дополнена.")
+ except Exception as e:
+ self.logger.error(f"Ошибка при обработке записи ID {row.get('id')}: {e}")
- if url_parameters:
- hotel_info = self.process_url_parameters(url_parameters)
- hotel_id = hotel_info.get('hotel_id')
-
- if not hotel_id:
- print(f"Error: hotel_id is empty in record {row}")
- continue # Пропускаем запись, если hotel_id отсутствует
-
- # Проверяем, существует ли отель с таким hotel_id
- hotel = self.check_and_store_imported_hotel(hotel_name=hotel_id, hotel_id=hotel_id)
-
- 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=row.get("id", None),
- defaults={
- "user_id": row.get("user_id"),
- "ip": row.get("ip"),
- "created": created,
- "timestamp": row.get("timestamp"),
- "date_time": date_time,
- "referred": referred,
- "agent": agent,
- "platform": platform,
- "version": version,
- "model": model,
- "device": device,
- "UAString": UAString,
- "location": location,
- "page_id": row.get("page_id"),
- "url_parameters": url_parameters,
- "page_title": page_title,
- "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
- и записывает несоответствия в таблицу 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):
+ """Запускает процесс синхронизации."""
+ self.db_connector.connect()
try:
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}")
+ rows = self.fetch_new_data(last_id)
+ self.process_and_save_data(rows)
+ self.logger.info("Синхронизация завершена.")
finally:
- if self.connection:
- self.connection.close()
+ self.db_connector.close()
+
+import logging
+from concurrent.futures import ThreadPoolExecutor
+from .models import ExternalDBSettings
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
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()
+ logger.info("Запуск планировщика синхронизации.")
+
+ # Получаем все активные настройки подключения
+ active_db_settings = ExternalDBSettings.objects.filter(is_active=True)
+ if not active_db_settings.exists():
+ logger.warning("Не найдено активных подключений для синхронизации.")
+ return
+
+ logger.info(f"Найдено активных подключений: {len(active_db_settings)}")
+
+ def sync_task(db_settings):
+ """
+ Выполняет синхронизацию для одного подключения.
+ """
+ try:
+ logger.info(f"Начало синхронизации для подключения: {db_settings.name} (ID={db_settings.id})")
+ sync_manager = DataSyncManager(db_settings.id)
+ sync_manager.sync()
+ logger.info(f"Синхронизация успешно завершена для подключения: {db_settings.name}")
+ except Exception as e:
+ logger.error(f"Ошибка синхронизации для подключения {db_settings.name}: {e}")
+
+ # Параллельное выполнение задач синхронизации
+ with ThreadPoolExecutor(max_workers=5) as executor: # Максимальное количество потоков = 5
+ for db_settings in active_db_settings:
+ executor.submit(sync_task, db_settings)
+
+ logger.info("Планировщик синхронизации завершил работу.")
diff --git a/antifroud/migrations/0013_alter_useractivitylog_timestamp.py b/antifroud/migrations/0013_alter_useractivitylog_timestamp.py
new file mode 100644
index 00000000..190e1b65
--- /dev/null
+++ b/antifroud/migrations/0013_alter_useractivitylog_timestamp.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.4 on 2024-12-17 03:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('antifroud', '0012_violationlog'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='timestamp',
+ field=models.BigIntegerField(blank=True, null=True, verbose_name='Метка времени'),
+ ),
+ ]
diff --git a/antifroud/migrations/0014_alter_useractivitylog_uastring_and_more.py b/antifroud/migrations/0014_alter_useractivitylog_uastring_and_more.py
new file mode 100644
index 00000000..b3d89e5d
--- /dev/null
+++ b/antifroud/migrations/0014_alter_useractivitylog_uastring_and_more.py
@@ -0,0 +1,68 @@
+# Generated by Django 5.1.4 on 2024-12-17 03:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('antifroud', '0013_alter_useractivitylog_timestamp'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='UAString',
+ field=models.TextField(blank=True, null=True, verbose_name='User-Agent строка'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='agent',
+ field=models.TextField(blank=True, null=True, verbose_name='Агент пользователя'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='created',
+ field=models.DateTimeField(blank=True, null=True, verbose_name='Дата создания'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='date_time',
+ field=models.DateTimeField(blank=True, null=True, verbose_name='Дата и время'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='hits',
+ field=models.IntegerField(blank=True, null=True, verbose_name='Количество обращений'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='honeypot',
+ field=models.BooleanField(blank=True, null=True, verbose_name='Метка honeypot'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='ip',
+ field=models.GenericIPAddressField(blank=True, null=True, verbose_name='IP-адрес'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='last_counter',
+ field=models.IntegerField(blank=True, null=True, verbose_name='Последний счетчик'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='reply',
+ field=models.BooleanField(blank=True, null=True, verbose_name='Ответ пользователя'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='type',
+ field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Тип'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='user_id',
+ field=models.BigIntegerField(blank=True, null=True, verbose_name='ID пользователя'),
+ ),
+ ]
diff --git a/antifroud/migrations/0015_alter_useractivitylog_page_id.py b/antifroud/migrations/0015_alter_useractivitylog_page_id.py
new file mode 100644
index 00000000..d857cc43
--- /dev/null
+++ b/antifroud/migrations/0015_alter_useractivitylog_page_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.4 on 2024-12-17 04:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('antifroud', '0014_alter_useractivitylog_uastring_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='page_id',
+ field=models.CharField(blank=True, max_length=255, null=True, verbose_name='ID страницы'),
+ ),
+ ]
diff --git a/antifroud/migrations/0016_alter_useractivitylog_created_and_more.py b/antifroud/migrations/0016_alter_useractivitylog_created_and_more.py
new file mode 100644
index 00000000..3bc93d7d
--- /dev/null
+++ b/antifroud/migrations/0016_alter_useractivitylog_created_and_more.py
@@ -0,0 +1,39 @@
+# Generated by Django 5.1.4 on 2024-12-17 05:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('antifroud', '0015_alter_useractivitylog_page_id'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='created',
+ field=models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Дата создания'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='external_id',
+ field=models.CharField(db_index=True, default=1, max_length=255, unique=True, verbose_name='Внешний ID'),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='ip',
+ field=models.GenericIPAddressField(blank=True, db_index=True, null=True, verbose_name='IP-адрес'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='page_id',
+ field=models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name='ID страницы'),
+ ),
+ migrations.AlterField(
+ model_name='useractivitylog',
+ name='user_id',
+ field=models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name='ID пользователя'),
+ ),
+ ]
diff --git a/antifroud/models.py b/antifroud/models.py
index f47e1d84..9d899a85 100644
--- a/antifroud/models.py
+++ b/antifroud/models.py
@@ -4,30 +4,40 @@ from hotels.models import Reservation
class UserActivityLog(models.Model):
- external_id = models.CharField(max_length=255, null=True, blank=True)
- user_id = models.BigIntegerField(verbose_name="ID пользователя")
- ip = models.GenericIPAddressField(verbose_name="IP-адрес")
- created = models.DateTimeField(verbose_name="Дата создания")
- timestamp = models.BigIntegerField(verbose_name="Метка времени")
- date_time = models.DateTimeField(verbose_name="Дата и время")
+ external_id = models.CharField(max_length=255, unique=True, verbose_name="Внешний ID", db_index=True)
+ user_id = models.BigIntegerField(verbose_name="ID пользователя", blank=True, null=True, db_index=True)
+ ip = models.GenericIPAddressField(verbose_name="IP-адрес", blank=True, null=True, db_index=True)
+ created = models.DateTimeField(verbose_name="Дата создания", blank=True, null=True, db_index=True)
+ timestamp = models.BigIntegerField(verbose_name="Метка времени", blank=True, null=True)
+ date_time = models.DateTimeField(verbose_name="Дата и время", blank=True, null=True)
referred = models.TextField(blank=True, null=True, verbose_name="Реферальная ссылка")
- agent = models.TextField(verbose_name="Агент пользователя")
+ agent = models.TextField(verbose_name="Агент пользователя", blank=True, null=True)
platform = models.CharField(max_length=255, blank=True, null=True, verbose_name="Платформа")
version = models.CharField(max_length=255, blank=True, null=True, verbose_name="Версия")
model = models.CharField(max_length=255, blank=True, null=True, verbose_name="Модель устройства")
device = models.CharField(max_length=255, blank=True, null=True, verbose_name="Тип устройства")
- UAString = models.TextField(verbose_name="User-Agent строка")
+ UAString = models.TextField(verbose_name="User-Agent строка", blank=True, null=True)
location = models.CharField(max_length=255, blank=True, null=True, verbose_name="Местоположение")
- page_id = models.BigIntegerField(blank=True, null=True, verbose_name="ID страницы")
+ page_id = models.BigIntegerField(blank=True, null=True, verbose_name="ID страницы", db_index=True)
url_parameters = models.TextField(blank=True, null=True, verbose_name="Параметры URL")
page_title = models.TextField(blank=True, null=True, verbose_name="Заголовок страницы")
- type = models.CharField(max_length=50, verbose_name="Тип")
- last_counter = models.IntegerField(verbose_name="Последний счетчик")
- hits = models.IntegerField(verbose_name="Количество обращений")
- honeypot = models.BooleanField(verbose_name="Метка honeypot")
- reply = models.BooleanField(verbose_name="Ответ пользователя")
+ type = models.CharField(max_length=50, verbose_name="Тип", blank=True, null=True)
+ last_counter = models.IntegerField(verbose_name="Последний счетчик", blank=True, null=True)
+ hits = models.IntegerField(verbose_name="Количество обращений", blank=True, null=True)
+ honeypot = models.BooleanField(verbose_name="Метка honeypot", blank=True, null=True)
+ reply = models.BooleanField(verbose_name="Ответ пользователя", blank=True, null=True)
page_url = models.URLField(blank=True, null=True, verbose_name="URL страницы")
+ class Meta:
+ indexes = [
+ models.Index(fields=["external_id"], name="idx_external_id"),
+ models.Index(fields=["user_id"], name="idx_user_id"),
+ models.Index(fields=["ip"], name="idx_ip"),
+ models.Index(fields=["created"], name="idx_created"),
+ models.Index(fields=["page_id"], name="idx_page_id"),
+ ]
+ verbose_name = "Лог активности пользователя"
+ verbose_name_plural = "Логи активности пользователей"
def __str__(self):
return f"UserActivityLog {self.id}: {self.page_title}"
diff --git a/hotels/admin.py b/hotels/admin.py
index 35706f23..48ae9d4f 100644
--- a/hotels/admin.py
+++ b/hotels/admin.py
@@ -5,7 +5,7 @@ from .models import (
UserHotel,
APIConfiguration,
Reservation,
- FraudLog
+ Room
)
from django.urls import path
from django.shortcuts import redirect
@@ -27,8 +27,9 @@ class HotelForm(forms.ModelForm):
@admin.register(Hotel)
class HotelAdmin(admin.ModelAdmin):
- list_display = ['name', 'hotel_id', 'pms', 'timezone', 'description']
+ list_display = ['name', 'hotel_id','room_count', 'pms', 'timezone', 'description']
list_filter = ['name', 'pms', 'timezone']
+ list_sorting = ['name', 'pms', 'room_count', 'timezone']
def sync_button(self, obj):
return format_html(
'Синхронизировать',
@@ -41,8 +42,36 @@ class HotelAdmin(admin.ModelAdmin):
path('sync//', self.sync_hotel_data),
]
return custom_urls + urls
+ def room_count(self, obj):
+ """
+ Подсчитывает количество комнат, связанных с отелем.
+ """
+ return Room.objects.filter(hotel=obj).count()
+ room_count.short_description = "Количество комнат"
+
+ def sync_button(self, obj):
+ """
+ Кнопка синхронизации данных.
+ """
+ return format_html(
+ 'Синхронизировать',
+ f"/admin/hotels/sync/{obj.id}/"
+ )
+
+ def get_urls(self):
+ """
+ Добавление кастомного URL для синхронизации.
+ """
+ urls = super().get_urls()
+ custom_urls = [
+ path('sync//', self.sync_hotel_data),
+ ]
+ return custom_urls + urls
def sync_hotel_data(self, request, hotel_id):
+ """
+ Метод синхронизации данных отеля.
+ """
try:
hotel = Hotel.objects.get(id=hotel_id)
client = APIClient(hotel.pms)
@@ -66,4 +95,9 @@ class ReservationAdmin(admin.ModelAdmin):
list_filter = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
ordering = ('-check_in',)
-
+@admin.register(Room)
+class RoomAdmin(admin.ModelAdmin):
+ list_display = ('hotel', 'number', 'external_id', 'description', 'created_at', 'updated_at')
+ search_fields = ('hotel', 'number', 'external_id', 'description')
+ list_filter = ('hotel', 'number', 'external_id','description', 'created_at', 'updated_at')
+ ordering = ('-hotel', '-number')
\ No newline at end of file
diff --git a/hotels/migrations/0011_room_alter_fraudlog_check_in_date_and_more.py b/hotels/migrations/0011_room_alter_fraudlog_check_in_date_and_more.py
new file mode 100644
index 00000000..50aee261
--- /dev/null
+++ b/hotels/migrations/0011_room_alter_fraudlog_check_in_date_and_more.py
@@ -0,0 +1,95 @@
+# Generated by Django 5.1.4 on 2024-12-17 02:51
+
+import django.core.validators
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('hotels', '0010_alter_hotel_timezone'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Room',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('number', models.CharField(max_length=50, verbose_name='Номер комнаты')),
+ ('external_id', models.CharField(max_length=255, unique=True, verbose_name='Внешний ID комнаты')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
+ ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
+ ],
+ options={
+ 'verbose_name': 'Номер',
+ 'verbose_name_plural': 'Номера',
+ },
+ ),
+ migrations.AlterField(
+ model_name='fraudlog',
+ name='check_in_date',
+ field=models.DateField(verbose_name='Дата заезда'),
+ ),
+ migrations.AlterField(
+ model_name='fraudlog',
+ name='detected_at',
+ field=models.DateTimeField(auto_now_add=True, verbose_name='Дата обнаружения'),
+ ),
+ migrations.AlterField(
+ model_name='fraudlog',
+ name='message',
+ field=models.TextField(verbose_name='Сообщение'),
+ ),
+ migrations.AlterField(
+ model_name='fraudlog',
+ name='reservation_id',
+ field=models.BigIntegerField(verbose_name='ID бронирования'),
+ ),
+ migrations.AlterField(
+ model_name='guest',
+ name='phone',
+ field=models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator(message='Введите корректный номер телефона (до 15 цифр).', regex='^\\+?1?\\d{9,15}$')], verbose_name='Телефон'),
+ ),
+ migrations.AlterField(
+ model_name='hotel',
+ name='phone',
+ field=models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator(message='Введите корректный номер телефона (до 15 цифр).', regex='^\\+?1?\\d{9,15}$')], verbose_name='Телефон'),
+ ),
+ migrations.AlterField(
+ model_name='hotel',
+ name='timezone',
+ field=models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Asmera', 'Africa/Asmera'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Timbuktu', 'Africa/Timbuktu'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/ComodRivadavia', 'America/Argentina/ComodRivadavia'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Atka', 'America/Atka'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Buenos_Aires', 'America/Buenos_Aires'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Catamarca', 'America/Catamarca'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Ciudad_Juarez', 'America/Ciudad_Juarez'), ('America/Coral_Harbour', 'America/Coral_Harbour'), ('America/Cordoba', 'America/Cordoba'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Ensenada', 'America/Ensenada'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fort_Wayne', 'America/Fort_Wayne'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Indianapolis', 'America/Indianapolis'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Jujuy', 'America/Jujuy'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Knox_IN', 'America/Knox_IN'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Louisville', 'America/Louisville'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Mendoza', 'America/Mendoza'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montreal', 'America/Montreal'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Acre', 'America/Porto_Acre'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Rosario', 'America/Rosario'), ('America/Santa_Isabel', 'America/Santa_Isabel'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Shiprock', 'America/Shiprock'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Virgin', 'America/Virgin'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/South_Pole', 'Antarctica/South_Pole'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Ashkhabad', 'Asia/Ashkhabad'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Calcutta', 'Asia/Calcutta'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Chongqing', 'Asia/Chongqing'), ('Asia/Chungking', 'Asia/Chungking'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Dacca', 'Asia/Dacca'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Harbin', 'Asia/Harbin'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Istanbul', 'Asia/Istanbul'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kashgar', 'Asia/Kashgar'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Katmandu', 'Asia/Katmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macao', 'Asia/Macao'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Rangoon', 'Asia/Rangoon'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Saigon', 'Asia/Saigon'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Tel_Aviv', 'Asia/Tel_Aviv'), ('Asia/Thimbu', 'Asia/Thimbu'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ujung_Pandang', 'Asia/Ujung_Pandang'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Ulan_Bator', 'Asia/Ulan_Bator'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faeroe', 'Atlantic/Faeroe'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Jan_Mayen', 'Atlantic/Jan_Mayen'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/ACT', 'Australia/ACT'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Canberra', 'Australia/Canberra'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/LHI', 'Australia/LHI'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/NSW', 'Australia/NSW'), ('Australia/North', 'Australia/North'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Queensland', 'Australia/Queensland'), ('Australia/South', 'Australia/South'), ('Australia/Sydney', 'Australia/Sydney'), ('Australia/Tasmania', 'Australia/Tasmania'), ('Australia/Victoria', 'Australia/Victoria'), ('Australia/West', 'Australia/West'), ('Australia/Yancowinna', 'Australia/Yancowinna'), ('Brazil/Acre', 'Brazil/Acre'), ('Brazil/DeNoronha', 'Brazil/DeNoronha'), ('Brazil/East', 'Brazil/East'), ('Brazil/West', 'Brazil/West'), ('CET', 'CET'), ('CST6CDT', 'CST6CDT'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Canada/Saskatchewan', 'Canada/Saskatchewan'), ('Canada/Yukon', 'Canada/Yukon'), ('Chile/Continental', 'Chile/Continental'), ('Chile/EasterIsland', 'Chile/EasterIsland'), ('Cuba', 'Cuba'), ('EET', 'EET'), ('EST', 'EST'), ('EST5EDT', 'EST5EDT'), ('Egypt', 'Egypt'), ('Eire', 'Eire'), ('Etc/GMT', 'Etc/GMT'), ('Etc/GMT+0', 'Etc/GMT+0'), ('Etc/GMT+1', 'Etc/GMT+1'), ('Etc/GMT+10', 'Etc/GMT+10'), ('Etc/GMT+11', 'Etc/GMT+11'), ('Etc/GMT+12', 'Etc/GMT+12'), ('Etc/GMT+2', 'Etc/GMT+2'), ('Etc/GMT+3', 'Etc/GMT+3'), ('Etc/GMT+4', 'Etc/GMT+4'), ('Etc/GMT+5', 'Etc/GMT+5'), ('Etc/GMT+6', 'Etc/GMT+6'), ('Etc/GMT+7', 'Etc/GMT+7'), ('Etc/GMT+8', 'Etc/GMT+8'), ('Etc/GMT+9', 'Etc/GMT+9'), ('Etc/GMT-0', 'Etc/GMT-0'), ('Etc/GMT-1', 'Etc/GMT-1'), ('Etc/GMT-10', 'Etc/GMT-10'), ('Etc/GMT-11', 'Etc/GMT-11'), ('Etc/GMT-12', 'Etc/GMT-12'), ('Etc/GMT-13', 'Etc/GMT-13'), ('Etc/GMT-14', 'Etc/GMT-14'), ('Etc/GMT-2', 'Etc/GMT-2'), ('Etc/GMT-3', 'Etc/GMT-3'), ('Etc/GMT-4', 'Etc/GMT-4'), ('Etc/GMT-5', 'Etc/GMT-5'), ('Etc/GMT-6', 'Etc/GMT-6'), ('Etc/GMT-7', 'Etc/GMT-7'), ('Etc/GMT-8', 'Etc/GMT-8'), ('Etc/GMT-9', 'Etc/GMT-9'), ('Etc/GMT0', 'Etc/GMT0'), ('Etc/Greenwich', 'Etc/Greenwich'), ('Etc/UCT', 'Etc/UCT'), ('Etc/UTC', 'Etc/UTC'), ('Etc/Universal', 'Etc/Universal'), ('Etc/Zulu', 'Etc/Zulu'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belfast', 'Europe/Belfast'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Nicosia', 'Europe/Nicosia'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Tiraspol', 'Europe/Tiraspol'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GB', 'GB'), ('GB-Eire', 'GB-Eire'), ('GMT', 'GMT'), ('GMT+0', 'GMT+0'), ('GMT-0', 'GMT-0'), ('GMT0', 'GMT0'), ('Greenwich', 'Greenwich'), ('HST', 'HST'), ('Hongkong', 'Hongkong'), ('Iceland', 'Iceland'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Iran', 'Iran'), ('Israel', 'Israel'), ('Jamaica', 'Jamaica'), ('Japan', 'Japan'), ('Kwajalein', 'Kwajalein'), ('Libya', 'Libya'), ('MET', 'MET'), ('MST', 'MST'), ('MST7MDT', 'MST7MDT'), ('Mexico/BajaNorte', 'Mexico/BajaNorte'), ('Mexico/BajaSur', 'Mexico/BajaSur'), ('Mexico/General', 'Mexico/General'), ('NZ', 'NZ'), ('NZ-CHAT', 'NZ-CHAT'), ('Navajo', 'Navajo'), ('PRC', 'PRC'), ('PST8PDT', 'PST8PDT'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Johnston', 'Pacific/Johnston'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Ponape', 'Pacific/Ponape'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Samoa', 'Pacific/Samoa'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Truk', 'Pacific/Truk'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('Pacific/Yap', 'Pacific/Yap'), ('Poland', 'Poland'), ('Portugal', 'Portugal'), ('ROC', 'ROC'), ('ROK', 'ROK'), ('Singapore', 'Singapore'), ('Turkey', 'Turkey'), ('UCT', 'UCT'), ('US/Alaska', 'US/Alaska'), ('US/Aleutian', 'US/Aleutian'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/East-Indiana', 'US/East-Indiana'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Indiana-Starke', 'US/Indiana-Starke'), ('US/Michigan', 'US/Michigan'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('US/Samoa', 'US/Samoa'), ('UTC', 'UTC'), ('Universal', 'Universal'), ('W-SU', 'W-SU'), ('WET', 'WET'), ('Zulu', 'Zulu')], default='UTC', max_length=63, verbose_name='Часовой пояс'),
+ ),
+ migrations.AlterField(
+ model_name='reservation',
+ name='room_number',
+ field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Номер комнаты'),
+ ),
+ migrations.AddIndex(
+ model_name='fraudlog',
+ index=models.Index(fields=['reservation_id'], name='hotels_frau_reserva_5a26b7_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='fraudlog',
+ index=models.Index(fields=['detected_at'], name='hotels_frau_detecte_07e626_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='reservation',
+ index=models.Index(fields=['hotel', 'check_in', 'check_out'], name='hotels_rese_hotel_i_6c527e_idx'),
+ ),
+ migrations.AddField(
+ model_name='room',
+ name='hotel',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='hotels.hotel', verbose_name='Отель'),
+ ),
+ migrations.AddIndex(
+ model_name='room',
+ index=models.Index(fields=['hotel', 'number'], name='hotels_room_hotel_i_a7c4fc_idx'),
+ ),
+ migrations.AddConstraint(
+ model_name='room',
+ constraint=models.UniqueConstraint(fields=('hotel', 'number'), name='unique_hotel_room'),
+ ),
+ ]
diff --git a/hotels/migrations/0012_alter_room_number.py b/hotels/migrations/0012_alter_room_number.py
new file mode 100644
index 00000000..c974215e
--- /dev/null
+++ b/hotels/migrations/0012_alter_room_number.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.4 on 2024-12-17 11:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('hotels', '0011_room_alter_fraudlog_check_in_date_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='room',
+ name='number',
+ field=models.CharField(max_length=50, unique=True, verbose_name='Номер комнаты'),
+ ),
+ ]
diff --git a/hotels/migrations/0013_alter_room_number.py b/hotels/migrations/0013_alter_room_number.py
new file mode 100644
index 00000000..45e2751b
--- /dev/null
+++ b/hotels/migrations/0013_alter_room_number.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.4 on 2024-12-17 11:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('hotels', '0012_alter_room_number'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='room',
+ name='number',
+ field=models.CharField(max_length=50, verbose_name='Номер комнаты'),
+ ),
+ ]
diff --git a/hotels/migrations/0014_alter_room_number.py b/hotels/migrations/0014_alter_room_number.py
new file mode 100644
index 00000000..307f6cc3
--- /dev/null
+++ b/hotels/migrations/0014_alter_room_number.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.4 on 2024-12-17 11:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('hotels', '0013_alter_room_number'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='room',
+ name='number',
+ field=models.CharField(max_length=50, unique=True, verbose_name='Номер комнаты'),
+ ),
+ ]
diff --git a/hotels/models.py b/hotels/models.py
index de59702d..63c39c76 100644
--- a/hotels/models.py
+++ b/hotels/models.py
@@ -1,5 +1,7 @@
from django.db import models
-from django.core.validators import MinValueValidator, MaxValueValidator
+from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
+from django.core.exceptions import ValidationError
+import pytz
class APIConfiguration(models.Model):
@@ -17,25 +19,33 @@ class APIConfiguration(models.Model):
verbose_name = "Конфигурация API"
verbose_name_plural = "Конфигурации API"
-import pytz
class Hotel(models.Model):
name = models.CharField(max_length=255, verbose_name="Название отеля")
hotel_id = models.CharField(max_length=255, unique=True, null=True, blank=True, verbose_name="ID отеля")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
- phone= models.CharField(max_length=50, null=True, blank=True, verbose_name="Телефон")
+ phone = models.CharField(
+ max_length=50,
+ null=True,
+ blank=True,
+ verbose_name="Телефон",
+ validators=[
+ RegexValidator(regex=r'^\+?1?\d{9,15}$', message="Введите корректный номер телефона (до 15 цифр).")
+ ],
+ )
email = models.EmailField(null=True, blank=True, verbose_name="Email")
address = models.CharField(max_length=255, null=True, blank=True, verbose_name="Адрес")
city = models.CharField(max_length=255, null=True, blank=True, verbose_name="Город")
timezone = models.CharField(
max_length=63,
- choices=[(tz, tz) for tz in pytz.all_timezones], # Список всех часовых поясов
- default='UTC', # Значение по умолчанию
+ choices=[(tz, tz) for tz in pytz.all_timezones],
+ default='UTC',
+ verbose_name="Часовой пояс",
)
description = models.TextField(null=True, blank=True, verbose_name="Описание")
-
+
pms = models.ForeignKey(
- 'pms_integration.PMSConfiguration',
+ 'pms_integration.PMSConfiguration',
on_delete=models.SET_NULL,
null=True,
blank=True,
@@ -48,13 +58,11 @@ class Hotel(models.Model):
class Meta:
verbose_name = "Отель"
verbose_name_plural = "Отели"
-
+
+
class Room(models.Model):
- """
- Модель номера отеля.
- """
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="rooms", verbose_name="Отель")
- number = models.CharField(max_length=50, verbose_name="Номер комнаты")
+ number = models.CharField(max_length=50, unique=True, verbose_name="Номер комнаты")
external_id = models.CharField(max_length=255, unique=True, verbose_name="Внешний ID комнаты")
description = models.TextField(blank=True, null=True, verbose_name="Описание")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
@@ -66,17 +74,23 @@ class Room(models.Model):
class Meta:
verbose_name = "Номер"
verbose_name_plural = "Номера"
- unique_together = ("hotel", "number") # Уникальность пары (отель, номер)
+ constraints = [
+ models.UniqueConstraint(fields=["hotel", "number"], name="unique_hotel_room")
+ ]
+ indexes = [
+ models.Index(fields=["hotel", "number"]),
+ ]
+
class UserHotel(models.Model):
user = models.ForeignKey(
- 'users.User', # Используем строковую ссылку
+ 'users.User',
on_delete=models.CASCADE,
related_name="user_hotels",
verbose_name="Пользователь"
)
hotel = models.ForeignKey(
- 'hotels.Hotel',
+ Hotel,
on_delete=models.CASCADE,
related_name="hotel_users",
verbose_name="Отель"
@@ -90,6 +104,76 @@ class UserHotel(models.Model):
verbose_name_plural = "Пользователи отелей"
+class Reservation(models.Model):
+ id = models.BigAutoField(primary_key=True, auto_created=True, verbose_name="ID")
+ hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
+ reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
+ room_number = models.CharField(max_length=255, null=True, blank=True, verbose_name="Номер комнаты")
+ room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
+ check_in = models.DateTimeField(verbose_name="Дата заезда")
+ check_out = models.DateTimeField(verbose_name="Дата выезда")
+ status = models.CharField(max_length=50, verbose_name="Статус")
+ price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Цена")
+ discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Скидка")
+
+ def clean(self):
+ if self.check_out and self.check_in and self.check_out <= self.check_in:
+ raise ValidationError("Дата выезда должна быть позже даты заезда.")
+
+ def __str__(self):
+ return f"Бронирование {self.reservation_id} - {self.hotel.name}"
+
+ class Meta:
+ verbose_name = "Бронирование"
+ verbose_name_plural = "Бронирования"
+ indexes = [
+ models.Index(fields=["hotel", "check_in", "check_out"]),
+ ]
+
+
+class Guest(models.Model):
+ reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE, related_name="guests", verbose_name="Бронирование")
+ name = models.CharField(max_length=255, verbose_name="Имя гостя")
+ birthdate = models.DateField(null=True, blank=True, verbose_name="Дата рождения")
+ phone = models.CharField(
+ max_length=50,
+ null=True,
+ blank=True,
+ verbose_name="Телефон",
+ validators=[
+ RegexValidator(regex=r'^\+?1?\d{9,15}$', message="Введите корректный номер телефона (до 15 цифр).")
+ ],
+ )
+ email = models.EmailField(null=True, blank=True, verbose_name="Email")
+
+ def __str__(self):
+ return f"{self.name} ({self.birthdate})" if self.birthdate else self.name
+
+ class Meta:
+ verbose_name = "Гость"
+ verbose_name_plural = "Гости"
+
+
+class FraudLog(models.Model):
+ hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="frauds")
+ reservation_id = models.BigIntegerField(verbose_name="ID бронирования")
+ guest_name = models.CharField(max_length=255, null=True, blank=True)
+ check_in_date = models.DateField(verbose_name="Дата заезда")
+ detected_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата обнаружения")
+ message = models.TextField(verbose_name="Сообщение")
+
+ def __str__(self):
+ return f"FRAUD: {self.guest_name} ({self.check_in_date})"
+
+ class Meta:
+ verbose_name = "Журнал мошенничества"
+ verbose_name_plural = "Журналы мошенничества"
+ indexes = [
+ models.Index(fields=["reservation_id"]),
+ models.Index(fields=["detected_at"]),
+ ]
+
+
class APIRequestLog(models.Model):
api = models.ForeignKey(APIConfiguration, on_delete=models.CASCADE, verbose_name="API")
request_time = models.DateTimeField(auto_now_add=True, verbose_name="Время запроса")
@@ -103,57 +187,6 @@ class APIRequestLog(models.Model):
verbose_name = "Журнал запросов API"
verbose_name_plural = "Журналы запросов API"
indexes = [
- models.Index(fields=['api']),
- models.Index(fields=['request_time']),
+ models.Index(fields=["api"]),
+ models.Index(fields=["request_time"]),
]
-
-
-class Reservation(models.Model):
- id = models.BigAutoField(primary_key=True, auto_created=True, verbose_name="ID")
- hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
- reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
- room_number = models.CharField(max_length=255, null=True, blank=True)
- room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
- check_in = models.DateTimeField(verbose_name="Дата заезда")
- check_out = models.DateTimeField(verbose_name="Дата выезда")
- status = models.CharField(max_length=50, verbose_name="Статус")
- price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Цена")
- discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Скидка")
-
- def __str__(self):
- return f"Бронирование {self.reservation_id} - {self.hotel.name}"
-
- class Meta:
- verbose_name = "Бронирование"
- verbose_name_plural = "Бронирования"
-
-
-class Guest(models.Model):
- reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE, related_name="guests", verbose_name="Бронирование")
- name = models.CharField(max_length=255, verbose_name="Имя гостя")
- birthdate = models.DateField(null=True, blank=True, verbose_name="Дата рождения")
- phone = models.CharField(max_length=50, null=True, blank=True, verbose_name="Телефон")
- email = models.EmailField(null=True, blank=True, verbose_name="Email")
-
- def __str__(self):
- return self.name
-
- class Meta:
- verbose_name = "Гость"
- verbose_name_plural = "Гости"
-
-
-class FraudLog(models.Model):
- hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="frauds")
- reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
- guest_name = models.CharField(max_length=255, null=True, blank=True)
- check_in_date = models.DateField()
- detected_at = models.DateTimeField(auto_now_add=True)
- message = models.TextField()
-
- def __str__(self):
- return f"FRAUD: {self.guest_name} ({self.check_in_date})"
-
- class Meta:
- verbose_name = "Журнал мошенничества"
- verbose_name_plural = "Журналы мошенничества"
\ No newline at end of file
diff --git a/settings/migrations/0007_alter_emailsettings_options_and_more.py b/settings/migrations/0007_alter_emailsettings_options_and_more.py
new file mode 100644
index 00000000..a887bdb5
--- /dev/null
+++ b/settings/migrations/0007_alter_emailsettings_options_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 5.1.4 on 2024-12-17 11:21
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('settings', '0006_remove_globalhotelsettings_timezone_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='emailsettings',
+ options={'verbose_name': 'E-mail', 'verbose_name_plural': 'E-mails'},
+ ),
+ migrations.AlterModelOptions(
+ name='globalhotelsettings',
+ options={'verbose_name': 'Настройки отеля', 'verbose_name_plural': 'Настройки отеля'},
+ ),
+ migrations.AlterModelOptions(
+ name='globalsystemsettings',
+ options={'verbose_name': 'Настройки системы', 'verbose_name_plural': 'Настройки системы'},
+ ),
+ migrations.AlterModelOptions(
+ name='localdatabase',
+ options={'verbose_name': 'База данных', 'verbose_name_plural': 'Базы данных'},
+ ),
+ migrations.AlterModelOptions(
+ name='telegramsettings',
+ options={'verbose_name': 'Telegram', 'verbose_name_plural': 'Telegram'},
+ ),
+ ]
diff --git a/settings/models.py b/settings/models.py
index 69899ac0..5dc8399a 100644
--- a/settings/models.py
+++ b/settings/models.py
@@ -15,8 +15,8 @@ class LocalDatabase(models.Model):
return self.name
class Meta:
- verbose_name = "Локальная база данных"
- verbose_name_plural = "Локальные базы данных"
+ verbose_name = "База данных"
+ verbose_name_plural = "Базы данных"
class TelegramSettings(models.Model):
bot_token = models.CharField(max_length=255, help_text="Токен вашего бота Telegram")
@@ -27,8 +27,8 @@ class TelegramSettings(models.Model):
return f"Telegram Bot ({self.username})"
class Meta:
- verbose_name = "Настройки Telegram"
- verbose_name_plural = "Настройки Telegram"
+ verbose_name = "Telegram"
+ verbose_name_plural = "Telegram"
class EmailSettings(models.Model):
@@ -39,8 +39,8 @@ class EmailSettings(models.Model):
from_email = models.EmailField(help_text="Email для отправки сообщений")
class Meta:
- verbose_name = "Настройки почты"
- verbose_name_plural = "Настройки почты"
+ verbose_name = "E-mail"
+ verbose_name_plural = "E-mails"
def __str__(self):
return f"Email Settings for {self.from_email}"
@@ -56,11 +56,11 @@ class GlobalHotelSettings(models.Model):
)
def __str__(self):
- return "Глобальные настройки отеля"
+ return "Настройки отеля"
class Meta:
- verbose_name = "Глобальные настройки отеля"
- verbose_name_plural = "Глобальные настройки отеля"
+ verbose_name = "Настройки отеля"
+ verbose_name_plural = "Настройки отеля"
class GlobalSystemSettings(models.Model):
system_name = models.CharField(max_length=255, help_text="Название системы")
@@ -71,9 +71,9 @@ class GlobalSystemSettings(models.Model):
default='UTC', # Значение по умолчанию
)
def __str__(self):
- return "Глобальные настройки системы"
+ return "Настройки системы"
class Meta:
- verbose_name = "Глобальные настройки системы"
- verbose_name_plural = "Глобальные настройки системы"
+ verbose_name = "Настройки системы"
+ verbose_name_plural = "Настройки системы"
\ No newline at end of file
diff --git a/touchh/settings.py b/touchh/settings.py
index 28cbd5ad..843688ed 100644
--- a/touchh/settings.py
+++ b/touchh/settings.py
@@ -31,10 +31,10 @@ 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!
DEBUG = True
-ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', '192.168.219.114', '5dec-182-226-158-253.ngrok-free.app', '*.ngrok-free.app']
+ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', '192.168.219.114', 'a66a-182-226-158-253.ngrok-free.app', '*.ngrok-free.app']
CSRF_TRUSTED_ORIGINS = [
- 'http://5dec-182-226-158-253.ngrok-free.app',
+ 'http://a66a-182-226-158-253.ngrok-free.app',
'https://*.ngrok-free.app', # Это подойдет для любых URL, связанных с ngrok
]