From 0bf2bb8dffa2ab4dfaa0297aa80c528b60fa5054 Mon Sep 17 00:00:00 2001 From: trevor Date: Tue, 17 Dec 2024 21:39:33 +0900 Subject: [PATCH] antifroud_check --- antifroud/admin.py | 33 +- antifroud/check_fraud.py | 195 +++- antifroud/data_sync.py | 994 +++++++++++------- .../0013_alter_useractivitylog_timestamp.py | 18 + ...alter_useractivitylog_uastring_and_more.py | 68 ++ .../0015_alter_useractivitylog_page_id.py | 18 + ..._alter_useractivitylog_created_and_more.py | 39 + antifroud/models.py | 38 +- hotels/admin.py | 40 +- ...m_alter_fraudlog_check_in_date_and_more.py | 95 ++ hotels/migrations/0012_alter_room_number.py | 18 + hotels/migrations/0013_alter_room_number.py | 18 + hotels/migrations/0014_alter_room_number.py | 18 + hotels/models.py | 169 +-- ...07_alter_emailsettings_options_and_more.py | 33 + settings/models.py | 24 +- touchh/settings.py | 4 +- 17 files changed, 1298 insertions(+), 524 deletions(-) create mode 100644 antifroud/migrations/0013_alter_useractivitylog_timestamp.py create mode 100644 antifroud/migrations/0014_alter_useractivitylog_uastring_and_more.py create mode 100644 antifroud/migrations/0015_alter_useractivitylog_page_id.py create mode 100644 antifroud/migrations/0016_alter_useractivitylog_created_and_more.py create mode 100644 hotels/migrations/0011_room_alter_fraudlog_check_in_date_and_more.py create mode 100644 hotels/migrations/0012_alter_room_number.py create mode 100644 hotels/migrations/0013_alter_room_number.py create mode 100644 hotels/migrations/0014_alter_room_number.py create mode 100644 settings/migrations/0007_alter_emailsettings_options_and_more.py 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 ]